From 212c43f475fc202b5b9e6dbb1f1c616e1a06a6f7 Mon Sep 17 00:00:00 2001 From: Byron Hambly Date: Mon, 3 Aug 2026 12:53:50 +0200 Subject: [PATCH 01/12] fix: range proof cache bind to asset and scriptpubkey (cherry picked from commit 6253d7e103655ec015097de1505b5f3785ff6447) --- src/script/sigcache.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/script/sigcache.cpp b/src/script/sigcache.cpp index 865c7e9e2c3..9f7bb9592b5 100644 --- a/src/script/sigcache.cpp +++ b/src/script/sigcache.cpp @@ -72,9 +72,9 @@ class CSignatureCache } // ELEMENTS: - void ComputeEntryRangeProof(uint256& entry, const std::vector& proof, const std::vector& commitment) { + void ComputeEntryRangeProof(uint256& entry, const std::vector& proof, const std::vector& commitment, const std::vector& asset_commitment, const CScript& scriptPubKey) { CSHA256 hasher = m_salted_hasher_range_proof; - hasher.Write(proof.data(), proof.size()).Write(commitment.data(), commitment.size()).Finalize(entry.begin()); + hasher.Write(proof.data(), proof.size()).Write(commitment.data(), commitment.size()).Write(asset_commitment.data(), asset_commitment.size()).Write(scriptPubKey.data(), scriptPubKey.size()).Finalize(entry.begin()); } void ComputeEntrySurjectionProof(uint256& entry, const uint256 &hash, const std::vector& proof, const std::vector& commitment) { CSHA256 hasher = m_salted_hasher_surjection_proof; @@ -176,7 +176,7 @@ void InitSurjectionproofCache() bool CachingRangeProofChecker::VerifyRangeProof(const std::vector& vchRangeProof, const std::vector& vchValueCommitment, const std::vector& vchAssetCommitment, const CScript& scriptPubKey, const secp256k1_context* secp256k1_ctx_verify_amounts) const { uint256 entry; - rangeProofCache.ComputeEntryRangeProof(entry, vchRangeProof, vchValueCommitment); + rangeProofCache.ComputeEntryRangeProof(entry, vchRangeProof, vchValueCommitment, vchAssetCommitment, scriptPubKey); if (rangeProofCache.Get(entry, !store)) { return true; From 87327c76a96d40c9ffc3b0a48410a371fb14113c Mon Sep 17 00:00:00 2001 From: Byron Hambly Date: Tue, 1 Sep 2026 10:25:04 +0200 Subject: [PATCH 02/12] blindpsbt: return error instead of asserting on surjection proof failure CreateAssetSurjectionProof asserted on secp256k1_surjectionproof_generate and _verify failure. A crafted PSET can supply unrelated tags/generators with no known discrete-log relationship, causing generation to fail and the assert to abort the process. Make these recoverable errors by returning false. (cherry picked from commit 2391041c60b0f261edb99b6cfb5a1ab848892bb6) --- src/blindpsbt.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/blindpsbt.cpp b/src/blindpsbt.cpp index 97404a4b018..2729bce7ba9 100644 --- a/src/blindpsbt.cpp +++ b/src/blindpsbt.cpp @@ -53,10 +53,17 @@ bool CreateAssetSurjectionProof(std::vector& output_proof, const } // Using the input chosen, build proof ret = secp256k1_surjectionproof_generate(secp256k1_blind_context, &proof, &ephemeral_input_tags[0], ephemeral_input_tags.size(), &output_asset_tag, input_index, input_asset_blinders[input_index].begin(), output_asset_blinder.begin()); - assert(ret == 1); + if (ret != 1) { + // Attacker-selected tags/generators without a known discrete-log + // relationship cause generation to fail; this must be a recoverable + // PSET error, not a process abort. + return false; + } // Double-check answer ret = secp256k1_surjectionproof_verify(secp256k1_blind_context, &proof, &ephemeral_input_tags[0], ephemeral_input_tags.size(), &output_asset_tag); - assert(ret == 1); + if (ret != 1) { + return false; + } // Serialize into output witness structure size_t output_len = secp256k1_surjectionproof_serialized_size(secp256k1_blind_context, &proof); From dd97f1336bb393ddf0e54d250db6b12fd5861134 Mon Sep 17 00:00:00 2001 From: Byron Hambly Date: Tue, 1 Sep 2026 10:25:04 +0200 Subject: [PATCH 03/12] blindpsbt: reject off-curve blinding pubkey before ECDH BlindPSBT passed the blinding pubkey straight to CKey::ECDH, whose only validation is an assert on the peer key, so a crafted off-curve pubkey (reaching IsBlinded() but failing IsFullyValid()) aborted the process. Mirror the non-PSET path and return BlindingStatus::INVALID_BLINDER when the pubkey is not fully valid. (cherry picked from commit 0ce3c24d7f962a555cbe1f69b2dae06d027842c1) --- src/blindpsbt.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/blindpsbt.cpp b/src/blindpsbt.cpp index 2729bce7ba9..861200380cb 100644 --- a/src/blindpsbt.cpp +++ b/src/blindpsbt.cpp @@ -563,6 +563,13 @@ BlindingStatus BlindPSBT(PartiallySignedTransaction& psbt, std::map Date: Tue, 1 Sep 2026 10:25:04 +0200 Subject: [PATCH 04/12] blindpsbt: refuse to blind a PSET output with no amount BlindPSBT dereferenced output.amount without a nullopt check. A crafted v0 PSET output (m_blinder_index set, amount absent) reached the blinding loop and dereferenced a disengaged std::optional, which is undefined behaviour. Refuse such outputs with BlindingStatus::INVALID_BLINDER. (cherry picked from commit 6a49991b75f8c688ddec061ae36d311eeb6e73f8) --- src/blindpsbt.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/blindpsbt.cpp b/src/blindpsbt.cpp index 861200380cb..8211375312c 100644 --- a/src/blindpsbt.cpp +++ b/src/blindpsbt.cpp @@ -508,6 +508,14 @@ BlindingStatus BlindPSBT(PartiallySignedTransaction& psbt, std::map= 2), so a crafted v0 PSET can reach the blinding + // loop with output.amount == nullopt. Dereferencing it is undefined + // behaviour. Refuse to blind such an output. + if (output.amount == std::nullopt) { + return BlindingStatus::INVALID_BLINDER; + } + // Things we are going to stuff into the PSBTOutput if everything is successful CConfidentialValue value_commitment; CConfidentialAsset asset_commitment; From 6257051594e72e2afa16308a20da380d9aac47bc Mon Sep 17 00:00:00 2001 From: Byron Hambly Date: Tue, 1 Sep 2026 10:25:04 +0200 Subject: [PATCH 05/12] dynafed: require at least four-fifths approval for parameter transition NextBlockIsParameterTransition computed the approval threshold as (epoch_length*4)/5, which floor-divides. For epoch lengths not divisible by 5 this is below the intended at-least-four-fifths rule, so a transition could pass with fewer than 80% of the epoch's blocks voting for it. Use the overflow-safe ceiling N - N/5 (== ceil(N*4/5)). This is a no-op for epoch lengths divisible by 5 (the only currently deployed case) and only corrects the under-approximation for non-divisible epoch lengths. (cherry picked from commit 43092822794d3daeba096464716a01d09d7c2e74) --- src/dynafed.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/dynafed.cpp b/src/dynafed.cpp index d7197951491..fb6e51066a7 100644 --- a/src/dynafed.cpp +++ b/src/dynafed.cpp @@ -14,6 +14,10 @@ bool NextBlockIsParameterTransition(const CBlockIndex* pindexPrev, const Consens } std::map vote_tally; assert(next_height >= consensus.dynamic_epoch_length); + // Require at least four-fifths of the epoch's votes. (epoch_length*4)/5 + // floor-divides, under-approximating the 80% threshold for epoch lengths + // not divisible by 5; N - N/5 is the overflow-safe ceiling of N*4/5. + const uint32_t threshold = consensus.dynamic_epoch_length - consensus.dynamic_epoch_length / 5; for (int32_t height = next_height - 1; height >= (int32_t)(next_height - consensus.dynamic_epoch_length); --height) { const CBlockIndex* p_epoch_walk = pindexPrev->GetAncestor(height); assert(p_epoch_walk); @@ -25,8 +29,7 @@ bool NextBlockIsParameterTransition(const CBlockIndex* pindexPrev, const Consens const uint256 proposal_root = proposal.CalculateRoot(); vote_tally[proposal_root]++; // Short-circuit once 4/5 threshold is reached - if (!proposal_root.IsNull() && vote_tally[proposal_root] >= - (consensus.dynamic_epoch_length*4)/5) { + if (!proposal_root.IsNull() && vote_tally[proposal_root] >= threshold) { winning_entry = proposal; return true; } From db055239ba3634bb36b6c113bb9703a8a24364e7 Mon Sep 17 00:00:00 2001 From: Byron Hambly Date: Tue, 1 Sep 2026 10:25:04 +0200 Subject: [PATCH 06/12] blindpsbt: require both range bounds to match claim in VerifyBlindValueProof A range-membership proof whose lower bound equalled the displayed PSET amount was accepted even when the committed value was larger, because only min_value was compared. Require both verified bounds to equal the claimed amount so a proof can no longer understate an output's value. (cherry picked from commit 99e9f250fc9a87b9f59b860545eaffd65f20bdb4) --- src/blindpsbt.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/blindpsbt.cpp b/src/blindpsbt.cpp index 8211375312c..3efd5c39d75 100644 --- a/src/blindpsbt.cpp +++ b/src/blindpsbt.cpp @@ -216,7 +216,11 @@ bool VerifyBlindValueProof(CAmount value, const CConfidentialValue& conf_value, if (secp256k1_rangeproof_verify(secp256k1_blind_context, &min_value, &max_value, &value_commit, proof.data(), proof.size(), /* extra_commit */ nullptr, /* extra_commit_len */ 0, &gen) == 0) { return false; } - return min_value == (uint64_t)value; + // A range-membership proof is only meaningful as an equality proof if the + // proven interval collapses to the claimed amount. Comparing solely the + // lower bound would accept a proof whose committed value is larger than + // the displayed amount. Require both bounds to equal `value`. + return min_value == (uint64_t)value && max_value == (uint64_t)value; } BlindProofResult VerifyBlindProofs(const PSBTOutput& o) { From 11f8ac6b170a99fcb5b60f85dc22f92b653ac664 Mon Sep 17 00:00:00 2001 From: Byron Hambly Date: Tue, 1 Sep 2026 10:25:04 +0200 Subject: [PATCH 07/12] blindpsbt: require genuine commitments in VerifyBlindValueProof An explicit 9-byte value (or a null field) passed the IsNull() check and its buffer was then parsed as a 33-byte Pedersen commitment, reading past the end. Require IsCommitment() on both the value and asset fields so the parser's length precondition holds and the out-of-bounds read is avoided. (cherry picked from commit 3e11e03125ce92827f2d47c2022ff1d085bcc192) --- src/blindpsbt.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/blindpsbt.cpp b/src/blindpsbt.cpp index 3efd5c39d75..73b781b4e2f 100644 --- a/src/blindpsbt.cpp +++ b/src/blindpsbt.cpp @@ -197,7 +197,11 @@ bool CreateBlindAssetProof(std::vector& assetproof, const CAsset& bool VerifyBlindValueProof(CAmount value, const CConfidentialValue& conf_value, const std::vector& proof, const CConfidentialAsset& conf_asset) { - if (conf_value.IsNull() || conf_asset.IsNull()) { + // The value and asset must be genuine commitments (33-byte, PrefixA/B) + // before their buffers are handed to libsecp256k1, which consumes exactly + // 33 serialized bytes. An explicit 9-byte value (or a null field) must not + // reach the parser, which would otherwise read out of bounds. + if (!conf_value.IsCommitment() || !conf_asset.IsCommitment()) { return false; } From f1f9a3fe798a9bd392f7a26e94d2cb61ed701e76 Mon Sep 17 00:00:00 2001 From: Byron Hambly Date: Tue, 1 Sep 2026 10:25:04 +0200 Subject: [PATCH 08/12] blind: reject empty surjection-target set in SurjectOutput The raw-blind RPC path can reach SurjectOutput with an empty surjection_targets vector (a zero-input tx with multiple blindable outputs), which indexed element [0] of the empty vector and passed it to secp256k1_surjectionproof_initialize, triggering undefined behaviour. Reject empty target sets up front, matching the existing over-limit guard. (cherry picked from commit 7c23bd1097e48c880f565412cbfc0823532cabf2) --- src/blind.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/blind.cpp b/src/blind.cpp index 9cb9ea7a7d8..34d5841bf32 100644 --- a/src/blind.cpp +++ b/src/blind.cpp @@ -206,9 +206,12 @@ bool SurjectOutput(CTxOutWitness& txoutwit, const std::vector SECP256K1_SURJECTIONPROOF_MAX_N_INPUTS) { + if (surjection_targets.empty() || surjection_targets.size() > SECP256K1_SURJECTIONPROOF_MAX_N_INPUTS) { // We must return false here to avoid triggering an assertion within - // secp256k1_surjectionproof_initialize on the next line. + // secp256k1_surjectionproof_initialize on the next line: the + // cryptographic API requires a non-empty set of surjection targets, + // and the raw-blinding path can reach us with an empty vector + // (zero-input tx with multiple blindable outputs). return false; } // Find correlation between asset tag and listed input tags From 60488fa994323cb4ad3053f1ffdfdb565606c9d5 Mon Sep 17 00:00:00 2001 From: merge-script Date: Mon, 31 Aug 2026 15:16:00 +0100 Subject: [PATCH 09/12] Merge ElementsProject/elements#1589: simplicity: update subtree to abede47e 04aa60ca0d21f29f2ecd65a85c7c693f10d1f4d7 build: add simplicity/cmr.c to src/CMakeLists.txt (Byron Hambly) e15a625977dd459c84a9608dde8bd8e61a2f703e Squashed 'src/simplicity/' changes from 49b96499a6..abede47eb8 (Byron Hambly) Pull request description: Updates the simplicity subtree to abede47eb835f5d39568cc705cefe5bf9e6ee769 matching BlockstreamResearch/simplicity#348 ACKs for top commit: tomt1664: ACK 04aa60ca0d21f29f2ecd65a85c7c693f10d1f4d7 tested locally Tree-SHA512: 6e4b1a640398f8a3017bab2ea94704bb6166612b239069512e9fcb831cbac502a4efab4517ca9c54d39fdee731d9a1cce9374df4d16ee6f63ff23bb090d42c19 (cherry picked from commit f80fb307a0b2bdff856d7405e3383326be784c5e) --- src/simplicity/CMakeLists.txt | 38 +++++ src/simplicity/Makefile | 4 +- src/simplicity/bitcoin/cmr.c | 22 +++ src/simplicity/bitcoin/exec.c | 136 ++++++++++++++++++ src/simplicity/cmr.c | 41 ++++++ src/simplicity/cmr.h | 24 ++++ src/simplicity/dag.c | 9 +- src/simplicity/elements-sources.mk | 2 + src/simplicity/elements/cmr.c | 25 +--- src/simplicity/eval.c | 4 +- .../include/simplicity/bitcoin/cmr.h | 23 +++ .../include/simplicity/bitcoin/exec.h | 40 ++++++ src/simplicity/simplicity_assert.h | 14 ++ 13 files changed, 353 insertions(+), 29 deletions(-) create mode 100644 src/simplicity/CMakeLists.txt create mode 100644 src/simplicity/bitcoin/cmr.c create mode 100644 src/simplicity/bitcoin/exec.c create mode 100644 src/simplicity/cmr.c create mode 100644 src/simplicity/cmr.h create mode 100644 src/simplicity/include/simplicity/bitcoin/cmr.h create mode 100644 src/simplicity/include/simplicity/bitcoin/exec.h diff --git a/src/simplicity/CMakeLists.txt b/src/simplicity/CMakeLists.txt new file mode 100644 index 00000000000..e1d146c2dd9 --- /dev/null +++ b/src/simplicity/CMakeLists.txt @@ -0,0 +1,38 @@ +cmake_minimum_required(VERSION 3.16) + +project(BitcoinSimplicity) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_EXTENSIONS OFF) + +add_library(BitcoinSimplicity STATIC + bitstream.c + cmr.c + dag.c + deserialize.c + eval.c + frame.c + jets-secp256k1.c + jets.c + rsort.c + sha256.c + type.c + typeInference.c + bitcoin/env.c + bitcoin/exec.c + bitcoin/bitcoinJets.c + bitcoin/cmr.c + bitcoin/ops.c + bitcoin/primitive.c + bitcoin/txEnv.c +) + +option(PRODUCTION "Enable production build" ON) +if (PRODUCTION) + target_compile_definitions(BitcoinSimplicity PRIVATE "PRODUCTION") +endif() + +target_include_directories(BitcoinSimplicity PUBLIC + $ + $ + ) diff --git a/src/simplicity/Makefile b/src/simplicity/Makefile index dcc9a4f7f50..e3d90ac515f 100644 --- a/src/simplicity/Makefile +++ b/src/simplicity/Makefile @@ -1,5 +1,5 @@ -CORE_OBJS := bitstream.o dag.o deserialize.o eval.o frame.o jets.o jets-secp256k1.o rsort.o sha256.o type.o typeInference.o -BITCOIN_OBJS := bitcoin/env.o bitcoin/ops.o bitcoin/bitcoinJets.o bitcoin/primitive.o bitcoin/txEnv.o +CORE_OBJS := bitstream.o cmr.o dag.o deserialize.o eval.o frame.o jets.o jets-secp256k1.o rsort.o sha256.o type.o typeInference.o +BITCOIN_OBJS := bitcoin/env.o bitcoin/exec.o bitcoin/ops.o bitcoin/bitcoinJets.o bitcoin/primitive.o bitcoin/cmr.o bitcoin/txEnv.o ELEMENTS_OBJS := elements/env.o elements/exec.o elements/ops.o elements/elementsJets.o elements/primitive.o elements/cmr.o elements/txEnv.o TEST_OBJS := test.o ctx8Pruned.o ctx8Unpruned.o hashBlock.o regression4.o schnorr0.o schnorr6.o typeSkipTest.o elements/checkSigHashAllTx1.o diff --git a/src/simplicity/bitcoin/cmr.c b/src/simplicity/bitcoin/cmr.c new file mode 100644 index 00000000000..82135b27ed5 --- /dev/null +++ b/src/simplicity/bitcoin/cmr.c @@ -0,0 +1,22 @@ +#include + +#include "../cmr.h" +#include "primitive.h" + +/* Deserialize a Simplicity 'program' and compute its CMR. + * + * Caution: no typechecking is performed, only a well-formedness check. + * + * If at any time malloc fails then '*error' is set to 'SIMPLICITY_ERR_MALLOC' and 'false' is returned, + * Otherwise, 'true' is returned indicating that the result was successfully computed and returned in the '*error' value. + * + * If the operation completes successfully then '*error' is set to 'SIMPLICITY_NO_ERROR', and the 'cmr' array is filled in with the program's computed CMR. + * + * Precondition: NULL != error; + * unsigned char cmr[32] + * unsigned char program[program_len] + */ +bool simplicity_bitcoin_computeCmr( simplicity_err* error, unsigned char* cmr + , const unsigned char* program, size_t program_len) { + return simplicity_computeCmr(error, cmr, simplicity_bitcoin_decodeJet, program, program_len); +} diff --git a/src/simplicity/bitcoin/exec.c b/src/simplicity/bitcoin/exec.c new file mode 100644 index 00000000000..e1a7f5da046 --- /dev/null +++ b/src/simplicity/bitcoin/exec.c @@ -0,0 +1,136 @@ +#include + +#include +#include +#include "primitive.h" +#include "txEnv.h" +#include "../deserialize.h" +#include "../eval.h" +#include "../limitations.h" +#include "../simplicity_alloc.h" +#include "../simplicity_assert.h" +#include "../typeInference.h" + +/* Deserialize a Simplicity 'program' with its 'witness' data and execute it in the environment of the 'ix'th input of 'tx' with `taproot`. + * + * If at any time malloc fails then '*error' is set to 'SIMPLICITY_ERR_MALLOC' and 'false' is returned, + * meaning we were unable to determine the result of the simplicity program. + * Otherwise, 'true' is returned indicating that the result was successfully computed and returned in the '*error' value. + * + * If deserialization, analysis, or execution fails, then '*error' is set to some simplicity_err. + * In particular, if the cost analysis exceeds the budget, or exceeds BUDGET_MAX, then '*error' is set to 'SIMPLICITY_ERR_EXEC_BUDGET'. + * On the other hand, if the cost analysis is less than or equal to minCost, then '*error' is set to 'SIMPLICITY_ERR_OVERWEIGHT'. + * + * Note that minCost and budget parameters are in WU, while the cost analysis will be performed in milliWU. + * Thus the minCost and budget specify a half open interval (minCost, budget] of acceptable cost values in milliWU. + * Setting minCost to 0 effectively disables the minCost check as every Simplicity program has a non-zero cost analysis. + * + * If 'amr != NULL' and the annotated Merkle root of the decoded expression doesn't match 'amr' then '*error' is set to 'SIMPLICITY_ERR_AMR'. + * + * Otherwise '*error' is set to 'SIMPLICITY_NO_ERROR'. + * + * If 'ihr != NULL' and '*error' is set to 'SIMPLICITY_NO_ERROR', then the identity hash of the root of the decoded expression is written to 'ihr'. + * Otherwise if 'ihr != NULL' and '*error' is not set to 'SIMPLICITY_NO_ERROR', then 'ihr' may or may not be written to. + * + * Precondition: NULL != error; + * NULL != ihr implies unsigned char ihr[32] + * NULL != tx; + * NULL != taproot; + * 0 <= minCost <= budget; + * NULL != amr implies unsigned char amr[32] + * unsigned char program[program_len] + * unsigned char witness[witness_len] + */ +extern bool simplicity_bitcoin_execSimplicity( simplicity_err* error, unsigned char* ihr + , const bitcoinTransaction* tx, uint_fast32_t ix, const bitcoinTapEnv* taproot + , int64_t minCost, int64_t budget + , const unsigned char* amr + , const unsigned char* program, size_t program_len + , const unsigned char* witness, size_t witness_len) { + simplicity_assert(NULL != error); + simplicity_assert(NULL != tx); + simplicity_assert(NULL != taproot); + simplicity_assert(0 <= minCost); + simplicity_assert(minCost <= budget); + simplicity_assert(NULL != program || 0 == program_len); + simplicity_assert(NULL != witness || 0 == witness_len); + + combinator_counters census; + dag_node* dag = NULL; + int_fast32_t dag_len; + sha256_midstate amr_hash; + + if (amr) sha256_toMidstate(amr_hash.s, amr); + + { + bitstream stream = initializeBitstream(program, program_len); + dag_len = simplicity_decodeMallocDag(&dag, simplicity_bitcoin_decodeJet, &census, &stream); + if (dag_len <= 0) { + simplicity_assert(dag_len < 0); + *error = (simplicity_err)dag_len; + return IS_PERMANENT(*error); + } + simplicity_assert(NULL != dag); + simplicity_assert((uint_fast32_t)dag_len <= DAG_LEN_MAX); + *error = simplicity_closeBitstream(&stream); + } + + if (IS_OK(*error)) { + if (0 != memcmp(taproot->scriptCMR.s, dag[dag_len-1].cmr.s, sizeof(uint32_t[8]))) { + *error = SIMPLICITY_ERR_CMR; + } + } + + if (IS_OK(*error)) { + type* type_dag = NULL; + *error = simplicity_mallocTypeInference(&type_dag, simplicity_bitcoin_mallocBoundVars, dag, (uint_fast32_t)dag_len, &census); + if (IS_OK(*error)) { + simplicity_assert(NULL != type_dag); + if (0 != dag[dag_len-1].sourceType || 0 != dag[dag_len-1].targetType) { + *error = SIMPLICITY_ERR_TYPE_INFERENCE_NOT_PROGRAM; + } + } + if (IS_OK(*error)) { + bitstream witness_stream = initializeBitstream(witness, witness_len); + *error = simplicity_fillWitnessData(dag, type_dag, (uint_fast32_t)dag_len, &witness_stream); + if (IS_OK(*error)) { + *error = simplicity_closeBitstream(&witness_stream); + if (SIMPLICITY_ERR_BITSTREAM_TRAILING_BYTES == *error) *error = SIMPLICITY_ERR_WITNESS_TRAILING_BYTES; + if (SIMPLICITY_ERR_BITSTREAM_ILLEGAL_PADDING == *error) *error = SIMPLICITY_ERR_WITNESS_ILLEGAL_PADDING; + } + } + if (IS_OK(*error)) { + sha256_midstate ihr_buf; + *error = simplicity_verifyNoDuplicateIdentityHashes(&ihr_buf, dag, type_dag, (uint_fast32_t)dag_len); + if (IS_OK(*error) && ihr) sha256_fromMidstate(ihr, ihr_buf.s); + } + if (IS_OK(*error) && amr) { + static_assert(DAG_LEN_MAX <= SIZE_MAX / sizeof(analyses), "analysis array too large."); + static_assert(1 <= DAG_LEN_MAX, "DAG_LEN_MAX is zero."); + static_assert(DAG_LEN_MAX - 1 <= UINT32_MAX, "analysis array index does not fit in uint32_t."); + analyses *analysis = simplicity_malloc((size_t)dag_len * sizeof(analyses)); + if (analysis) { + simplicity_computeAnnotatedMerkleRoot(analysis, dag, type_dag, (uint_fast32_t)dag_len); + if (0 != memcmp(amr_hash.s, analysis[dag_len-1].annotatedMerkleRoot.s, sizeof(uint32_t[8]))) { + *error = SIMPLICITY_ERR_AMR; + } + } else { + /* malloc failed which counts as a transient error. */ + *error = SIMPLICITY_ERR_MALLOC; + } + simplicity_free(analysis); + } + if (IS_OK(*error)) { + txEnv env = simplicity_bitcoin_build_txEnv(tx, taproot, ix); + static_assert(BUDGET_MAX <= UBOUNDED_MAX, "BUDGET_MAX doesn't fit in ubounded."); + *error = evalTCOProgram( dag, type_dag, (size_t)dag_len + , minCost <= BUDGET_MAX ? (ubounded)minCost : BUDGET_MAX + , &(ubounded){budget <= BUDGET_MAX ? (ubounded)budget : BUDGET_MAX} + , &env); + } + simplicity_free(type_dag); + } + + simplicity_free(dag); + return IS_PERMANENT(*error); +} diff --git a/src/simplicity/cmr.c b/src/simplicity/cmr.c new file mode 100644 index 00000000000..e02ae4b6668 --- /dev/null +++ b/src/simplicity/cmr.c @@ -0,0 +1,41 @@ +#include "cmr.h" + +#include "limitations.h" +#include "simplicity_alloc.h" +#include "simplicity_assert.h" + +/* Deserialize a Simplicity 'program' and compute its CMR. + * + * Caution: no typechecking is performed, only a well-formedness check. + * + * If at any time malloc fails then '*error' is set to 'SIMPLICITY_ERR_MALLOC' and 'false' is returned, + * Otherwise, 'true' is returned indicating that the result was successfully computed and returned in the '*error' value. + * + * If the operation completes successfully then '*error' is set to 'SIMPLICITY_NO_ERROR', and the 'cmr' array is filled in with the program's computed CMR. + * + * Precondition: NULL != error; + * unsigned char cmr[32] + * unsigned char program[program_len] + */ +bool simplicity_computeCmr( simplicity_err* error, unsigned char* cmr, simplicity_callback_decodeJet decodeJet + , const unsigned char* program, size_t program_len) { + simplicity_assert(NULL != error); + simplicity_assert(NULL != cmr); + simplicity_assert(NULL != program || 0 == program_len); + + bitstream stream = initializeBitstream(program, program_len); + dag_node* dag = NULL; + int_fast32_t dag_len = simplicity_decodeMallocDag(&dag, decodeJet, NULL, &stream); + if (dag_len <= 0) { + simplicity_assert(dag_len < 0); + *error = (simplicity_err)dag_len; + } else { + simplicity_assert(NULL != dag); + simplicity_assert((uint_fast32_t)dag_len <= DAG_LEN_MAX); + *error = simplicity_closeBitstream(&stream); + sha256_fromMidstate(cmr, dag[dag_len-1].cmr.s); + } + + simplicity_free(dag); + return IS_PERMANENT(*error); +} diff --git a/src/simplicity/cmr.h b/src/simplicity/cmr.h new file mode 100644 index 00000000000..9e7b0f86523 --- /dev/null +++ b/src/simplicity/cmr.h @@ -0,0 +1,24 @@ +#ifndef SIMPLICITY_CMR_H +#define SIMPLICITY_CMR_H + +#include +#include +#include +#include "deserialize.h" + +/* Deserialize a Simplicity 'program' and compute its CMR. + * + * Caution: no typechecking is performed, only a well-formedness check. + * + * If at any time malloc fails then '*error' is set to 'SIMPLICITY_ERR_MALLOC' and 'false' is returned, + * Otherwise, 'true' is returned indicating that the result was successfully computed and returned in the '*error' value. + * + * If the operation completes successfully then '*error' is set to 'SIMPLICITY_NO_ERROR', and the 'cmr' array is filled in with the program's computed CMR. + * + * Precondition: NULL != error; + * unsigned char cmr[32] + * unsigned char program[program_len] + */ +extern bool simplicity_computeCmr( simplicity_err* error, unsigned char* cmr, simplicity_callback_decodeJet decodeJet + , const unsigned char* program, size_t program_len); +#endif diff --git a/src/simplicity/dag.c b/src/simplicity/dag.c index d09cd2740cb..f26b647eff7 100644 --- a/src/simplicity/dag.c +++ b/src/simplicity/dag.c @@ -116,6 +116,7 @@ sha256_midstate simplicity_computeWordCMR(const bitstring* value, size_t n) { case 0: i = getBit(value, 0); break; case 1: i = 2 + ((1U * getBit(value, 0) << 1) | getBit(value, 1)); break; case 2: i = 6 + ((1U * getBit(value, 0) << 3) | (1U * getBit(value, 1) << 2) | (1U * getBit(value, 2) << 1) | getBit(value, 3)); break; + default: SIMPLICITY_UNREACHABLE; } memcpy(stack_ptr, &word_cmr[i], sizeof(uint32_t[8])); } else { @@ -174,7 +175,7 @@ void simplicity_computeCommitmentMerkleRoot(dag_node* dag, const uint_fast32_t i case PAIR: memcpy(block + j, dag[dag[i].child[1]].cmr.s, sizeof(uint32_t[8])); j = 0; - /*@fallthrough@*/ + SIMPLICITY_FALLTHROUGH; case DISCONNECT: /* Only the first child is used in the CMR. */ case INJL: case INJR: @@ -182,6 +183,7 @@ void simplicity_computeCommitmentMerkleRoot(dag_node* dag, const uint_fast32_t i case DROP: memcpy(block + j, dag[dag[i].child[0]].cmr.s, sizeof(uint32_t[8])); simplicity_sha256_compression(dag[i].cmr.s, block); + SIMPLICITY_FALLTHROUGH; case IDEN: case UNIT: case WITNESS: @@ -224,13 +226,14 @@ static void computeIdentityHashRoots(sha256_midstate* ihr, const dag_node* dag, case DISCONNECT: memcpy(block + j, ihr[dag[i].child[1]].s, sizeof(uint32_t[8])); j = 0; - /*@fallthrough@*/ + SIMPLICITY_FALLTHROUGH; case INJL: case INJR: case TAKE: case DROP: memcpy(block + j, ihr[dag[i].child[0]].s, sizeof(uint32_t[8])); simplicity_sha256_compression(ihr[i].s, block); + SIMPLICITY_FALLTHROUGH; case IDEN: case UNIT: case HIDDEN: @@ -420,6 +423,7 @@ simplicity_err simplicity_verifyCanonicalOrder(dag_node* dag, const uint_fast32_ continue; } if (bottom == child) bottom++; + SIMPLICITY_FALLTHROUGH; case IDEN: case UNIT: case WITNESS: @@ -444,6 +448,7 @@ simplicity_err simplicity_verifyCanonicalOrder(dag_node* dag, const uint_fast32_ continue; } if (bottom == child) bottom++; + SIMPLICITY_FALLTHROUGH; case INJL: case INJR: case TAKE: diff --git a/src/simplicity/elements-sources.mk b/src/simplicity/elements-sources.mk index 1c0dc4b9a9b..bc01af62117 100644 --- a/src/simplicity/elements-sources.mk +++ b/src/simplicity/elements-sources.mk @@ -12,6 +12,7 @@ ELEMENTS_SIMPLICITY_DIST_HEADERS_INT += %reldir%/include/simplicity/elements/exe ELEMENTS_SIMPLICITY_LIB_SOURCES_INT = ELEMENTS_SIMPLICITY_LIB_SOURCES_INT += %reldir%/bitstream.c +ELEMENTS_SIMPLICITY_LIB_SOURCES_INT += %reldir%/cmr.c ELEMENTS_SIMPLICITY_LIB_SOURCES_INT += %reldir%/dag.c ELEMENTS_SIMPLICITY_LIB_SOURCES_INT += %reldir%/deserialize.c ELEMENTS_SIMPLICITY_LIB_SOURCES_INT += %reldir%/eval.c @@ -33,6 +34,7 @@ ELEMENTS_SIMPLICITY_LIB_SOURCES_INT += %reldir%/elements/txEnv.c ELEMENTS_SIMPLICITY_LIB_HEADERS_INT = ELEMENTS_SIMPLICITY_LIB_HEADERS_INT += %reldir%/bitstream.h +ELEMENTS_SIMPLICITY_LIB_HEADERS_INT += %reldir%/cmr.h ELEMENTS_SIMPLICITY_LIB_HEADERS_INT += %reldir%/bitstring.h ELEMENTS_SIMPLICITY_LIB_HEADERS_INT += %reldir%/bounded.h ELEMENTS_SIMPLICITY_LIB_HEADERS_INT += %reldir%/dag.h diff --git a/src/simplicity/elements/cmr.c b/src/simplicity/elements/cmr.c index dd73be4ed93..3f7dedebe40 100644 --- a/src/simplicity/elements/cmr.c +++ b/src/simplicity/elements/cmr.c @@ -1,9 +1,6 @@ #include -#include "../deserialize.h" -#include "../limitations.h" -#include "../simplicity_alloc.h" -#include "../simplicity_assert.h" +#include "../cmr.h" #include "primitive.h" /* Deserialize a Simplicity 'program' and compute its CMR. @@ -21,23 +18,5 @@ */ bool simplicity_elements_computeCmr( simplicity_err* error, unsigned char* cmr , const unsigned char* program, size_t program_len) { - simplicity_assert(NULL != error); - simplicity_assert(NULL != cmr); - simplicity_assert(NULL != program || 0 == program_len); - - bitstream stream = initializeBitstream(program, program_len); - dag_node* dag = NULL; - int_fast32_t dag_len = simplicity_decodeMallocDag(&dag, simplicity_elements_decodeJet, NULL, &stream); - if (dag_len <= 0) { - simplicity_assert(dag_len < 0); - *error = (simplicity_err)dag_len; - } else { - simplicity_assert(NULL != dag); - simplicity_assert((uint_fast32_t)dag_len <= DAG_LEN_MAX); - *error = simplicity_closeBitstream(&stream); - sha256_fromMidstate(cmr, dag[dag_len-1].cmr.s); - } - - simplicity_free(dag); - return IS_PERMANENT(*error); + return simplicity_computeCmr(error, cmr, simplicity_elements_decodeJet, program, program_len); } diff --git a/src/simplicity/eval.c b/src/simplicity/eval.c index 6c9e9dc05e8..706e09b4c78 100644 --- a/src/simplicity/eval.c +++ b/src/simplicity/eval.c @@ -462,7 +462,7 @@ static simplicity_err runTCO(evalState state, call* stack, const dag_node* dag, skip(state.activeWriteFrame, pad( INJR == dag[pc].tag , type_dag[INJ_B(dag, type_dag, pc)].bitSize , type_dag[INJ_C(dag, type_dag, pc)].bitSize)); - /*@fallthrough@*/ + SIMPLICITY_FALLTHROUGH; case TAKE: simplicity_debug_assert(calling); /* TAIL_CALL(dag[pc].child[0], SAME_TCO); */ @@ -496,7 +496,7 @@ static simplicity_err runTCO(evalState state, call* stack, const dag_node* dag, } else { writeValue(state.activeWriteFrame, &dag[pc].compactValue, dag[pc].targetType, type_dag); } - /*@fallthrough@*/ + SIMPLICITY_FALLTHROUGH; case UNIT: simplicity_debug_assert(calling); if (get_tco_flag(&stack[pc])) { diff --git a/src/simplicity/include/simplicity/bitcoin/cmr.h b/src/simplicity/include/simplicity/bitcoin/cmr.h new file mode 100644 index 00000000000..2a1f82aaef4 --- /dev/null +++ b/src/simplicity/include/simplicity/bitcoin/cmr.h @@ -0,0 +1,23 @@ +#ifndef SIMPLICITY_BITCOIN_CMR_H +#define SIMPLICITY_BITCOIN_CMR_H + +#include +#include +#include + +/* Deserialize a Simplicity 'program' and compute its CMR. + * + * Caution: no typechecking is performed, only a well-formedness check. + * + * If at any time malloc fails then '*error' is set to 'SIMPLICITY_ERR_MALLOC' and 'false' is returned, + * Otherwise, 'true' is returned indicating that the result was successfully computed and returned in the '*error' value. + * + * If the operation completes successfully then '*error' is set to 'SIMPLICITY_NO_ERROR', and the 'cmr' array is filled in with the program's computed CMR. + * + * Precondition: NULL != error; + * unsigned char cmr[32] + * unsigned char program[program_len] + */ +extern bool simplicity_bitcoin_computeCmr( simplicity_err* error, unsigned char* cmr + , const unsigned char* program, size_t program_len); +#endif diff --git a/src/simplicity/include/simplicity/bitcoin/exec.h b/src/simplicity/include/simplicity/bitcoin/exec.h new file mode 100644 index 00000000000..787f2dd9298 --- /dev/null +++ b/src/simplicity/include/simplicity/bitcoin/exec.h @@ -0,0 +1,40 @@ +#ifndef SIMPLICITY_BITCOIN_EXEC_H +#define SIMPLICITY_BITCOIN_EXEC_H + +#include +#include +#include +#include +#include + +/* Deserialize a Simplicity 'program' with its 'witness' data and execute it in the environment of the 'ix'th input of 'tx' with `taproot`. + * + * If at any time malloc fails then '*error' is set to 'SIMPLICITY_ERR_MALLOC' and 'false' is returned, + * meaning we were unable to determine the result of the simplicity program. + * Otherwise, 'true' is returned indicating that the result was successfully computed and returned in the '*error' value. + * + * If deserialization, analysis, or execution fails, then '*error' is set to some simplicity_err. + * + * If 'amr != NULL' and the annotated Merkle root of the decoded expression doesn't match 'amr' then '*error' is set to 'SIMPLICITY_ERR_AMR'. + * + * Otherwise '*error' is set to 'SIMPLICITY_NO_ERROR'. + * + * If 'ihr != NULL' and '*error' is set to 'SIMPLICITY_NO_ERROR', then the identity hash of the root of the decoded expression is written to 'ihr'. + * Otherwise if 'ihr != NULL' and '*error' is not set to 'SIMPLICITY_NO_ERROR', then 'ihr' may or may not be written to. + * + * Precondition: NULL != error; + * NULL != ihr implies unsigned char ihr[32] + * NULL != tx; + * NULL != taproot; + * 0 <= minCost <= budget; + * NULL != amr implies unsigned char amr[32] + * unsigned char program[program_len] + * unsigned char witness[witness_len] + */ +extern bool simplicity_bitcoin_execSimplicity( simplicity_err* error, unsigned char* ihr + , const bitcoinTransaction* tx, uint_fast32_t ix, const bitcoinTapEnv* taproot + , int64_t minCost, int64_t budget + , const unsigned char* amr + , const unsigned char* program, size_t program_len + , const unsigned char* witness, size_t witness_len); +#endif diff --git a/src/simplicity/simplicity_assert.h b/src/simplicity/simplicity_assert.h index a321c938ea8..e74b41f7717 100644 --- a/src/simplicity/simplicity_assert.h +++ b/src/simplicity/simplicity_assert.h @@ -34,4 +34,18 @@ # define SIMPLICITY_UNREACHABLE assert(NULL == "SIMPLICITY_UNCREACHABLE was reached") #endif +/* Defines a FALLTHROUGH macro to annotate intentional switch fallthroughs, silencing -Wimplicit-fallthrough + * warnings on compilers that support the 'fallthrough' attribute. + * No-op on compilers (e.g. MSVC) that don't support it. + */ +#if defined(__has_attribute) +# if __has_attribute(fallthrough) +# define SIMPLICITY_FALLTHROUGH __attribute__((fallthrough)) +# endif +#endif + +#ifndef SIMPLICITY_FALLTHROUGH +# define SIMPLICITY_FALLTHROUGH ((void)0) +#endif + #endif /* SIMPLICITY_SIMPLICITY_ASSERT_H */ From 45af24e8084b3798a2c1045609fa0c23ad64fe24 Mon Sep 17 00:00:00 2001 From: Byron Hambly Date: Tue, 21 Oct 2025 16:31:56 +0200 Subject: [PATCH 10/12] DecomposePeginWitness: fix deserialization flags for MerkleBlock proof In CreatePeginWitnessInner, the MerkleBlock is always serialized without witness: PROTOCOL_VERSION | SERIALIZE_TRANSACTION_NO_WITNESS In DecomposePeginWitness before this change, the MerkleBlock was deserialized with witness: PROTOCOL_VERSION This was only noticed as an issue in the pegin subsidy implementation, in a failure in the feature_dynafed functional test. In the test_transition_mempool_eject test case, the Merkle block proof is coming from the same chain where we are creating a pegin. See the comment: "hack: since we're not validating peg-ins in parent chain, just make both the funding and claim tx on same chain (printing money)" I haven't investigated enough to explain why this causes a deserialization failure in this specific case, but presumably this change is correct since we're always serializing without witness. Before this DecomposePeginWitness was only used in src/psbt.cpp (cherry picked from commit f3b63f4b8c12076b94586b743bc0bbf04e676551) --- src/pegins.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pegins.cpp b/src/pegins.cpp index 2af4fb2a3e7..3871e526379 100644 --- a/src/pegins.cpp +++ b/src/pegins.cpp @@ -577,7 +577,7 @@ bool DecomposePeginWitness(const CScriptWitness& witness, CAmount& value, CAsset tx = elem_tx; } - CDataStream ss_proof(stack[5], SER_NETWORK, PROTOCOL_VERSION); + CDataStream ss_proof(stack[5], SER_NETWORK, PROTOCOL_VERSION | SERIALIZE_TRANSACTION_NO_WITNESS); if (Params().GetConsensus().ParentChainHasPow()) { Sidechain::Bitcoin::CMerkleBlock tx_proof; ss_proof >> tx_proof; From 3c8b45343856b021cf6c60eca8e6793ad75a83a6 Mon Sep 17 00:00:00 2001 From: merge-script Date: Tue, 1 Sep 2026 19:37:47 +0200 Subject: [PATCH 11/12] Merge ElementsProject/elements#1593: Fix RPC return errors for psbt and invalid rangeproofs a9db3b1c1fce0288f47dac21915f9fceaaeb231a Add startup warning for signed-blocks parent chain (Tom Trevethan) 246c5ab63adb7c3a3f672992109e227ab668d096 Add virtual desctructor to CChainParams (Tom Trevethan) 779e71f545d82d706bd5bec083261dfa03c582ce PAK enforcement on confidential nAsset (Tom Trevethan) ffd91c0512eddcb58b74489b3c07939470f3e79e PartiallySignedTransaction::SetupFromTx indexes vtxinwit checked (Tom Trevethan) 84a05e35aa94bf86b80b9e27ba4e25809068d666 check pubkey validity in tweakfedpegscript to prevent assert failure (Tom Trevethan) 3a8dec1258eceac38e110c20fd7b5d7a04d4c536 Return error for psbt if explicit amounts/assets deleted (Tom Trevethan) 4e5ca94f6b6ce0eecb7dc1fa65780d9a724f67bb Return error for invalid rangproof amounts (Tom Trevethan) Pull request description: Fixes for a number of issues with RPC errors for invalid PSBTs and amounts/rangeproofs. ACKs for top commit: delta1: ACK a9db3b1c1fce0288f47dac21915f9fceaaeb231a; tested locally Tree-SHA512: bf35348a5fad30e0f1f2b3caa2ec35ec521b583155e97f3a5f2504a3d70b41677f215fc01b28ccd30706ff5a7d021afb74c110a2c8f270942f5cba344544a55d (cherry picked from commit 3d7134f42cc0f38412245039cbfa9ffb73c27263) --- src/blind.cpp | 12 ++++-- src/blindpsbt.cpp | 18 ++++++++- src/blindpsbt.h | 2 + src/chainparams.h | 3 ++ src/init.cpp | 7 ++++ src/pegins.cpp | 84 ++++++++++++++++++++++++++-------------- src/primitives/pak.cpp | 10 +++++ src/primitives/pak.h | 2 + src/psbt.cpp | 15 +++++-- src/rpc/misc.cpp | 22 +++++++++++ src/test/blind_tests.cpp | 72 ++++++++++++++++++++++++++++++++++ src/util/error.cpp | 2 + src/util/error.h | 1 + src/validation.cpp | 3 ++ src/wallet/wallet.cpp | 22 +++++++---- 15 files changed, 230 insertions(+), 45 deletions(-) diff --git a/src/blind.cpp b/src/blind.cpp index 34d5841bf32..4a5b5f6f65f 100644 --- a/src/blind.cpp +++ b/src/blind.cpp @@ -549,7 +549,9 @@ int BlindTransaction(std::vector& input_value_blinding_factors, const // Generate rangeproof, no script committed for issuances bool rangeresult = GenerateRangeproof((nPseudo ? txinwit.vchInflationKeysRangeproof : txinwit.vchIssuanceAmountRangeproof), value_blindptrs, nonce, amount, CScript(), value_commit, asset_gen, asset, asset_blindptrs); - assert(rangeresult); + if (!rangeresult) { + return -1; + } // Successfully blinded this issuance num_blinded++; @@ -624,9 +626,13 @@ int BlindTransaction(std::vector& input_value_blinding_factors, const // Generate rangeproof bool rangeresult = GenerateRangeproof(txoutwit.vchRangeproof, value_blindptrs, nonce, amount, out.scriptPubKey, value_commit, asset_gen, asset, asset_blindptrs); - assert(rangeresult); + if (!rangeresult) { + return -1; + } - // Create surjection proof for this output + // Failed surjection proof is a foreseeable condition + // (no suitable input asset to prove against) and is reported to the + // caller via the returned count. See naive_blinding_test. if (!SurjectOutput(txoutwit, surjection_targets, target_asset_generators, target_asset_blinders, asset_blindptrs, asset_gen, asset)) { continue; } diff --git a/src/blindpsbt.cpp b/src/blindpsbt.cpp index 73b781b4e2f..f9dce4a9fdb 100644 --- a/src/blindpsbt.cpp +++ b/src/blindpsbt.cpp @@ -31,6 +31,10 @@ std::string GetBlindingStatusError(const BlindingStatus& status) return "Unable to create an asset surjection proof"; case BlindingStatus::NO_BLIND_OUTPUTS: return "Transaction has blind inputs belonging to this blinder but does not have outputs to blind"; + case BlindingStatus::RANGEPROOF_UNABLE: + return "Unable to create a value rangeproof for an output"; + case BlindingStatus::INVALID_AMOUNT: + return "Zero-valued output to a spendable script cannot be blinded"; } assert(false); } @@ -513,6 +517,12 @@ BlindingStatus BlindPSBT(PartiallySignedTransaction& psbt, std::mapIsUnspendable()) { + return BlindingStatus::INVALID_AMOUNT; + } + // Check this is our output to blind if (output.m_blinder_index == std::nullopt || our_input_data.count(*output.m_blinder_index) == 0) continue; @@ -590,12 +600,16 @@ BlindingStatus BlindPSBT(PartiallySignedTransaction& psbt, std::map blind_value_proof; rangeresult = CreateBlindValueProof(blind_value_proof, value_blinder, *output.amount, value_commit, asset_generator); - assert(rangeresult); + if (!rangeresult) { + return BlindingStatus::RANGEPROOF_UNABLE; + } // Create surjection proof for this output if (!CreateAssetSurjectionProof(asp, fixed_input_tags, ephemeral_input_tags, input_asset_blinders, asset_blinder, asset_generator, asset)) { diff --git a/src/blindpsbt.h b/src/blindpsbt.h index d79e4e77d43..0eaf1a582e5 100644 --- a/src/blindpsbt.h +++ b/src/blindpsbt.h @@ -28,6 +28,8 @@ enum class BlindingStatus INVALID_BLINDER, ASP_UNABLE, NO_BLIND_OUTPUTS, + RANGEPROOF_UNABLE, + INVALID_AMOUNT, }; enum class BlindProofResult { diff --git a/src/chainparams.h b/src/chainparams.h index 840a22ba5c4..4b0e145743a 100644 --- a/src/chainparams.h +++ b/src/chainparams.h @@ -139,6 +139,9 @@ class CChainParams bool GetAcceptDiscountCT() const { return accept_discount_ct; } bool GetCreateDiscountCT() const { return create_discount_ct; } + // ELEMENTS: Elements adds classes with their own members so the base pointer needs a virtual destructor. + virtual ~CChainParams() = default; + protected: CChainParams() {} diff --git a/src/init.cpp b/src/init.cpp index 38112fbeeef..d83d9bd463b 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -1056,6 +1056,13 @@ bool AppInitParameterInteraction(const ArgsManager& args) LogPrintf("Increasing minrelaytxfee to %s to match incrementalrelayfee\n",::minRelayTxFee.ToString()); } + if (chainparams.GetConsensus().has_parent_chain && !chainparams.GetConsensus().ParentChainHasPow()) { + LogPrintf("This chain is configured with a signed-blocks parent chain. " + "Peg-ins referencing a parent block that has activated dynamic " + "federations will be rejected: such headers cannot be " + "authenticated. See doc/ for details.\n"); + } + // Sanity check argument for min fee for including tx in block // TODO: Harmonize which arguments need sanity checking and where that happens if (args.IsArgSet("-blockmintxfee")) { diff --git a/src/pegins.cpp b/src/pegins.cpp index 3871e526379..b7d22d4cc23 100644 --- a/src/pegins.cpp +++ b/src/pegins.cpp @@ -554,39 +554,63 @@ bool DecomposePeginWitness(const CScriptWitness& witness, CAmount& value, CAsset if (stack.size() != 6) return false; - CDataStream stream(stack[0], SER_NETWORK, PROTOCOL_VERSION); - stream >> value; - - CAsset tmp_asset(stack[1]); - asset = tmp_asset; - - uint256 gh(stack[2]); - genesis_hash = gh; - - CScript s(stack[3].begin(), stack[3].end()); - claim_script = s; + // Fixed-width fields must be size-checked before construction: the + // base_blob vector constructor asserts on a length mismatch + // (uint256.cpp:15), and an assert is not catchable by the try below. + // CAsset delegates to the same constructor. + if (stack[1].size() != 32) return false; // asset + if (stack[2].size() != 32) return false; // parent genesis hash + + // Decompose into locals so a failure part-way through cannot leave the + // caller's out-parameters partially populated. + CAmount tmp_value{0}; + CAsset tmp_asset; + uint256 tmp_genesis_hash; + CScript tmp_claim_script; + std::variant tmp_tx; + std::variant tmp_merkle_block; - CDataStream ss_tx(stack[4], SER_NETWORK, PROTOCOL_VERSION); - if (Params().GetConsensus().ParentChainHasPow()) { - Sidechain::Bitcoin::CTransactionRef btc_tx; - ss_tx >> btc_tx; - tx = btc_tx; - } else { - CTransactionRef elem_tx; - ss_tx >> elem_tx; - tx = elem_tx; - } + try { + CDataStream stream(stack[0], SER_NETWORK, PROTOCOL_VERSION); + stream >> tmp_value; + + tmp_asset = CAsset(stack[1]); + tmp_genesis_hash = uint256(stack[2]); + tmp_claim_script = CScript(stack[3].begin(), stack[3].end()); + + CDataStream ss_tx(stack[4], SER_NETWORK, PROTOCOL_VERSION); + if (Params().GetConsensus().ParentChainHasPow()) { + Sidechain::Bitcoin::CTransactionRef btc_tx; + ss_tx >> btc_tx; + tmp_tx = btc_tx; + } else { + CTransactionRef elem_tx; + ss_tx >> elem_tx; + tmp_tx = elem_tx; + } - CDataStream ss_proof(stack[5], SER_NETWORK, PROTOCOL_VERSION | SERIALIZE_TRANSACTION_NO_WITNESS); - if (Params().GetConsensus().ParentChainHasPow()) { - Sidechain::Bitcoin::CMerkleBlock tx_proof; - ss_proof >> tx_proof; - merkle_block = tx_proof; - } else { - CMerkleBlock tx_proof; - ss_proof >> tx_proof; - merkle_block = tx_proof; + CDataStream ss_proof(stack[5], SER_NETWORK, PROTOCOL_VERSION | SERIALIZE_TRANSACTION_NO_WITNESS); + if (Params().GetConsensus().ParentChainHasPow()) { + Sidechain::Bitcoin::CMerkleBlock tx_proof; + ss_proof >> tx_proof; + tmp_merkle_block = tx_proof; + } else { + CMerkleBlock tx_proof; + ss_proof >> tx_proof; + tmp_merkle_block = tx_proof; + } + } catch (const std::exception&) { + // Malformed encoding. Report failure rather than propagating, so that + // the bool return means what the signature implies. Callers such as + // PartiallySignedTransaction::SetupFromTx have no exception handling. + return false; } + value = tmp_value; + asset = tmp_asset; + genesis_hash = tmp_genesis_hash; + claim_script = tmp_claim_script; + tx = std::move(tmp_tx); + merkle_block = std::move(tmp_merkle_block); return true; } diff --git a/src/primitives/pak.cpp b/src/primitives/pak.cpp index 308af502640..63b67917341 100644 --- a/src/primitives/pak.cpp +++ b/src/primitives/pak.cpp @@ -208,3 +208,13 @@ bool IsPAKValidTx(const CTransaction& tx, const CPAKList& paklist, const uint256 } return true; } + +bool HasConfidentialPegoutOutput(const CTransaction& tx, const uint256& parent_gen_hash) +{ + for (const auto& txout : tx.vout) { + if (txout.scriptPubKey.IsPegoutScript(parent_gen_hash) && !txout.nAsset.IsExplicit()) { + return true; + } + } + return false; +} \ No newline at end of file diff --git a/src/primitives/pak.h b/src/primitives/pak.h index ba840757682..9bde80036de 100644 --- a/src/primitives/pak.h +++ b/src/primitives/pak.h @@ -68,4 +68,6 @@ bool IsPAKValidOutput(const CTxOut& txout, const CPAKList& paklist, const uint25 bool IsPAKValidTx(const CTransaction& tx, const CPAKList& paklist, const uint256& parent_gen_hash, const CAsset& peg_asset); +bool HasConfidentialPegoutOutput(const CTransaction& tx, const uint256& parent_gen_hash); + #endif // BITCOIN_PRIMITIVES_PAK_H diff --git a/src/psbt.cpp b/src/psbt.cpp index 3733b16448a..b8ebc0323f6 100644 --- a/src/psbt.cpp +++ b/src/psbt.cpp @@ -868,12 +868,21 @@ void PartiallySignedTransaction::SetupFromTx(const CMutableTransaction& tx) } } // Peg-in things - if (txin.m_is_pegin) { + if (txin.m_is_pegin && i < tx.witness.vtxinwit.size()) { CAmount peg_in_value; CAsset asset; - if (DecomposePeginWitness(tx.witness.vtxinwit[i].m_pegin_witness, peg_in_value, asset, input.m_peg_in_genesis_hash, input.m_peg_in_claim_script, input.m_peg_in_tx, input.m_peg_in_txout_proof)) { + uint256 genesis_hash; + CScript claim_script; + std::variant peg_in_tx; + std::variant txout_proof; + if (DecomposePeginWitness(tx.witness.vtxinwit[i].m_pegin_witness, peg_in_value, asset, + genesis_hash, claim_script, peg_in_tx, txout_proof) + && asset == Params().GetConsensus().pegged_asset) { input.m_peg_in_value = peg_in_value; - assert(asset == Params().GetConsensus().pegged_asset); + input.m_peg_in_genesis_hash = genesis_hash; + input.m_peg_in_claim_script = claim_script; + input.m_peg_in_tx = peg_in_tx; + input.m_peg_in_txout_proof = txout_proof; } } } diff --git a/src/rpc/misc.cpp b/src/rpc/misc.cpp index b1a941e9aab..0fbfdbd961d 100644 --- a/src/rpc/misc.cpp +++ b/src/rpc/misc.cpp @@ -839,6 +839,24 @@ static RPCHelpMan getindexinfo() // // ELEMENTS CALLS +static bool FedpegScriptPubkeysAreValid(const CScript& script) +{ + const bool is_liquidv1_watchman = MatchLiquidWatchman(script); + bool liquid_op_else_found = false; + CScript::const_iterator pc = script.begin(); + opcodetype opcode; + std::vector vch; + while (script.GetOp(pc, opcode, vch)) { + if (is_liquidv1_watchman && opcode == OP_ELSE) { + liquid_op_else_found = true; + } + if (vch.size() == 33 && !liquid_op_else_found && !CPubKey(vch).IsFullyValid()) { + return false; + } + } + return true; +} + static RPCHelpMan tweakfedpegscript() { return RPCHelpMan{"tweakfedpegscript", @@ -868,6 +886,10 @@ static RPCHelpMan tweakfedpegscript() if (IsHex(request.params[1].get_str())) { std::vector fedpeg_byte = ParseHex(request.params[1].get_str()); fedpegscript = CScript(fedpeg_byte.begin(), fedpeg_byte.end()); + if (!FedpegScriptPubkeysAreValid(fedpegscript)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, + "fedpegscript contains a 33-byte push that is not a valid compressed public key"); + } } else { throw JSONRPCError(RPC_TYPE_ERROR, "fedpegscript must be a hex string"); } diff --git a/src/test/blind_tests.cpp b/src/test/blind_tests.cpp index 02e53a01b15..8cb328f195d 100644 --- a/src/test/blind_tests.cpp +++ b/src/test/blind_tests.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -368,4 +369,75 @@ BOOST_AUTO_TEST_CASE(naive_blinding_test) BOOST_CHECK(!VerifyAmounts(inputs, CTransaction(txtemp), nullptr, false)); } } +BOOST_AUTO_TEST_CASE(rangeproof_zero_value_spendable_script) +{ + // A rangeproof over a spendable script uses min_value = 1 + // (`min_value = scriptPubKey.IsUnspendable() ? 0 : 1`), and + // secp256k1_rangeproof_sign returns 0 when min_value > value. A zero-valued + // output to a spendable script therefore has no valid rangeproof, and the + // creation helpers must report that rather than assert on it. + + const CAsset asset(GetRandHash()); + const uint256 asset_blinder = GetRandHash(); + const uint256 value_blinder = GetRandHash(); + const uint256 nonce = GetRandHash(); + + const CScript spendable = CScript() << OP_TRUE; + const CScript unspendable = CScript() << OP_RETURN; + BOOST_CHECK(!spendable.IsUnspendable()); + BOOST_CHECK(unspendable.IsUnspendable()); + + // Asset generator, shared by every case below + CConfidentialAsset conf_asset; + secp256k1_generator asset_gen; + CreateAssetCommitment(conf_asset, asset_gen, asset, asset_blinder); + + // Commitments to 0 and to 1 under that generator + CConfidentialValue conf_value_zero, conf_value_one; + secp256k1_pedersen_commitment value_commit_zero, value_commit_one; + CreateValueCommitment(conf_value_zero, value_commit_zero, value_blinder, asset_gen, 0); + CreateValueCommitment(conf_value_one, value_commit_one, value_blinder, asset_gen, 1); + + std::vector rangeproof; + + // Zero to a spendable script is unprovable. Before the fix, the caller at + // blindpsbt.cpp:562 turns this false into assert(rangeresult) -> SIGABRT. + BOOST_CHECK(!CreateValueRangeProof(rangeproof, value_blinder, nonce, 0, spendable, + value_commit_zero, asset_gen, asset, asset_blinder)); + + // Zero to an unspendable script gives min_value = 0 and must keep working: + // this is the fee / issuance / OP_RETURN shape. + BOOST_CHECK(CreateValueRangeProof(rangeproof, value_blinder, nonce, 0, unspendable, + value_commit_zero, asset_gen, asset, asset_blinder)); + + // The ordinary case is unaffected. + BOOST_CHECK(CreateValueRangeProof(rangeproof, value_blinder, nonce, 1, spendable, + value_commit_one, asset_gen, asset, asset_blinder)); + + // Confirm the boundary is min_value and not something incidental, mirroring + // the rangeproof_info check in naive_blinding_test. + { + secp256k1_context* ctx = secp256k1_context_create(SECP256K1_CONTEXT_VERIFY); + int exp = 0; + int mantissa = 0; + uint64_t min_value = 0; + uint64_t max_value = 0; + BOOST_CHECK(secp256k1_rangeproof_info(ctx, &exp, &mantissa, &min_value, &max_value, + rangeproof.data(), rangeproof.size()) == 1); + BOOST_CHECK_EQUAL(min_value, 1ULL); + secp256k1_context_destroy(ctx); + } + + std::vector value_blindptrs; + std::vector asset_blindptrs; + value_blindptrs.push_back(const_cast(value_blinder.begin())); + asset_blindptrs.push_back(asset_blinder.begin()); + + BOOST_CHECK(!GenerateRangeproof(rangeproof, value_blindptrs, nonce, 0, spendable, + value_commit_zero, asset_gen, asset, asset_blindptrs)); + BOOST_CHECK(GenerateRangeproof(rangeproof, value_blindptrs, nonce, 0, unspendable, + value_commit_zero, asset_gen, asset, asset_blindptrs)); + BOOST_CHECK(GenerateRangeproof(rangeproof, value_blindptrs, nonce, 1, spendable, + value_commit_one, asset_gen, asset, asset_blindptrs)); +} BOOST_AUTO_TEST_SUITE_END() diff --git a/src/util/error.cpp b/src/util/error.cpp index 3549a29f5c3..f2c27e96aec 100644 --- a/src/util/error.cpp +++ b/src/util/error.cpp @@ -49,6 +49,8 @@ bilingual_str TransactionErrorString(const TransactionError err) return Untranslated("Wallet does not have necessary blinding key"); case TransactionError::MISSING_SIDECHANNEL_DATA: return Untranslated("A rangeproof did not encode necessary blinding data"); + case TransactionError::MISSING_EXPLICIT_OUTPUT_DATA: + return Untranslated("Explicit output data is missing for a blinded output"); // no default case, so the compiler can warn about missing cases } assert(false); diff --git a/src/util/error.h b/src/util/error.h index 4b798b84a6b..29ece0a972d 100644 --- a/src/util/error.h +++ b/src/util/error.h @@ -39,6 +39,7 @@ enum class TransactionError { INVALID_ASSET_PROOF, MISSING_BLINDING_KEY, MISSING_SIDECHANNEL_DATA, + MISSING_EXPLICIT_OUTPUT_DATA, }; bilingual_str TransactionErrorString(const TransactionError error); diff --git a/src/validation.cpp b/src/validation.cpp index 95feba0b775..52bc6e7de7d 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -717,6 +717,9 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws) // And now do PAK checks. Filtered by next blocks' enforced list if (chainparams.GetEnforcePak()) { + if (HasConfidentialPegoutOutput(tx, chainparams.ParentGenesisBlockHash())) { + return state.Invalid(TxValidationResult::TX_NOT_STANDARD, "confidential-pegout-asset"); + } if (!IsPAKValidTx(tx, GetActivePAKList(m_active_chainstate.m_chain.Tip(), chainparams.GetConsensus()), chainparams.ParentGenesisBlockHash(), chainparams.GetConsensus().pegged_asset)) { return state.Invalid(TxValidationResult::TX_NOT_STANDARD, "invalid-pegout-proof"); } diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 58391081ac0..32e9db69db1 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -2039,6 +2039,13 @@ TransactionError CWallet::SignPSBT(PartiallySignedTransaction& psbtx, bool& comp } if (o.script && IsMine(*o.script)) { + // A counterparty blinding our receive output can + // omit them, disabling both, and we would sign a commitment + // to whatever value they chose. Our own blinder always + // preserves these fields, so requiring them is safe. + if (o.amount == std::nullopt || o.m_asset.IsNull()) { + return TransactionError::MISSING_EXPLICIT_OUTPUT_DATA; + } CKey blinding_key; if ((blinding_key = GetBlindingKey(&*o.script)).IsValid()) { CAmount value; @@ -2049,14 +2056,15 @@ TransactionError CWallet::SignPSBT(PartiallySignedTransaction& psbtx, bool& comp CConfidentialNonce nonce; nonce.vchCommitment.insert(nonce.vchCommitment.end(), o.m_ecdh_pubkey.begin(), o.m_ecdh_pubkey.end()); if (UnblindConfidentialPair(blinding_key, o.m_value_commitment, o.m_asset_commitment, nonce, *o.script, o.m_value_rangeproof, value, value_factor, asset, asset_factor)) { - // These assertions are cryptographically impossible to trigger, as we - // checked the proofs above, and then `UnblindConfidentialPair` checks - // the extracted value/asset against the commitments. - if (o.amount) { - assert(*o.amount == value); + // The explicit fields are required above, so + // VerifyBlindProofs has checked both proofs and + // these should not differ. Return rather than + // assert: the inputs originate off-host. + if (*o.amount != value) { + return TransactionError::INVALID_VALUE_PROOF; } - if (!o.m_asset.IsNull()) { - assert(CAsset(o.m_asset) == asset); + if (CAsset(o.m_asset) != asset) { + return TransactionError::INVALID_ASSET_PROOF; } } else { return TransactionError::MISSING_SIDECHANNEL_DATA; From 6c2c5626bafed503c673ec6f83be9bfec861d26f Mon Sep 17 00:00:00 2001 From: Pablo Greco Date: Thu, 3 Sep 2026 15:13:01 -0700 Subject: [PATCH 12/12] build: fix depends Qt download link Github-Pull: #15973 Rebased-From: 7fd790144126ea89463934039641182f8abc4deb (cherry picked from commit ff69c2fb50bfb7c5eed3e2885d64d4b221fff092) --- depends/packages/qt.mk | 2 +- doc/dependencies.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/depends/packages/qt.mk b/depends/packages/qt.mk index e5fb135da05..6282c39330c 100644 --- a/depends/packages/qt.mk +++ b/depends/packages/qt.mk @@ -1,6 +1,6 @@ package=qt $(package)_version=5.15.3 -$(package)_download_path=https://download.qt.io/official_releases/qt/5.15/$($(package)_version)/submodules +$(package)_download_path=https://download.qt.io/archive/qt/5.15/$($(package)_version)/submodules $(package)_suffix=everywhere-opensource-src-$($(package)_version).tar.xz $(package)_file_name=qtbase-$($(package)_suffix) $(package)_sha256_hash=26394ec9375d52c1592bd7b689b1619c6b8dbe9b6f91fdd5c355589787f3a0b6 diff --git a/doc/dependencies.md b/doc/dependencies.md index 57d2b994df6..1af1d8eb15a 100644 --- a/doc/dependencies.md +++ b/doc/dependencies.md @@ -20,7 +20,7 @@ These are the dependencies currently used by Bitcoin Core. You can find instruct | PCRE | | | | | [Yes](https://github.com/bitcoin/bitcoin/blob/master/depends/packages/qt.mk) | | Python (tests) | | [3.6](https://www.python.org/downloads) | | | | | qrencode | [3.4.4](https://fukuchi.org/works/qrencode) | | No | | | -| Qt | [5.15.3](https://download.qt.io/official_releases/qt/) | [5.9.5](https://github.com/bitcoin/bitcoin/issues/20104) | No | | | +| Qt | [5.15.3](https://download.qt.io/archive/qt/) | [5.9.5](https://github.com/bitcoin/bitcoin/issues/20104) | No | | | | SQLite | [3.32.1](https://sqlite.org/download.html) | [3.7.17](https://github.com/bitcoin/bitcoin/pull/19077) | | | | | XCB | | | | | [Yes](https://github.com/bitcoin/bitcoin/blob/master/depends/packages/qt.mk) (Linux only) | | systemtap ([tracing](tracing.md))| [4.5](https://sourceware.org/systemtap/ftp/releases/) | | | | |