diff --git a/doc/release-notes-20861.md b/doc/release-notes-20861.md index 5c68e4ab0c9..2a7902e1194 100644 --- a/doc/release-notes-20861.md +++ b/doc/release-notes-20861.md @@ -1,3 +1,25 @@ +Wallet and signing +------------------ + +- ELEMENTS: The default sighash used when signing pre-Taproot (legacy and + segwit v0) inputs now commits to the output rangeproofs on chains where + `SIGHASH_RANGEPROOF` is active (i.e. dynafed is active). Concretely, when no + sighash type is specified, the wallet, the `signrawtransactionwithkey` / + `signrawtransactionwithwallet` / `walletprocesspsbt` / `descriptorprocesspsbt` + RPCs, and `elements-tx` now default to `SIGHASH_ALL|RANGEPROOF` instead of + `SIGHASH_ALL`. This removes the previous default's third-party rangeproof + (witness) malleability and matches the rangeproof coverage that Taproot inputs + already have. + + The new default is gated on activation: node-backed signing checks live + dynafed activation at the current tip, while the offline `elements-tx` tool + gates on the selected chain's parameters (only chains where dynafed is always + active). On chains where `SIGHASH_RANGEPROOF` is not active, the historical + `SIGHASH_ALL` default is used so that signatures remain standard and valid. + Taproot signing is unaffected: the `SIGHASH_RANGEPROOF` bit is ignored for + Taproot (which always commits to rangeproofs). Users can still request any + specific sighash type explicitly to override the default. + Updated RPCs ------------ diff --git a/src/bitcoin-tx.cpp b/src/bitcoin-tx.cpp index 966e6431f43..82c99a6f872 100644 --- a/src/bitcoin-tx.cpp +++ b/src/bitcoin-tx.cpp @@ -7,6 +7,8 @@ #endif #include +#include +#include #include #include #include @@ -602,7 +604,16 @@ static CAmount AmountFromValue(const UniValue& value) static void MutateTxSign(CMutableTransaction& tx, const std::string& flagStr) { - int nHashType = SIGHASH_ALL; + // ELEMENTS: bitcoin-tx has no chainstate, so we cannot check live dynafed + // activation. Gate the default on chain parameters instead: commit to + // rangeproofs by default on chains where dynafed (which enables + // SCRIPT_SIGHASH_RANGEPROOF) is known to be active. Otherwise use the + // historical SIGHASH_ALL default so offline-built txs stay standard and valid. + // See CChainParams::SighashRangeproofActiveByParams() for the liquidv1 nuance. + int nHashType = DefaultSighashType(Params().SighashRangeproofActiveByParams()); + // DefaultSighashType may return SIGHASH_DEFAULT (0); for the legacy tool path + // treat that as SIGHASH_ALL. + if (nHashType == SIGHASH_DEFAULT) nHashType = SIGHASH_ALL; if (flagStr.size() > 0) if (!findSighashFlags(nHashType, flagStr)) diff --git a/src/blind.cpp b/src/blind.cpp index 9cb9ea7a7d8..4a5b5f6f65f 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 @@ -546,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++; @@ -621,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 97404a4b018..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); } @@ -53,10 +57,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); @@ -190,7 +201,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; } @@ -209,7 +224,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) { @@ -498,9 +517,23 @@ 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; + // PSET v0 does not require an output amount (it is only enforced for + // m_psbt_version >= 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; @@ -556,16 +589,27 @@ 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/chain.h b/src/chain.h index 6e023158d12..1a470011dfa 100644 --- a/src/chain.h +++ b/src/chain.h @@ -332,7 +332,10 @@ class CBlockIndex block.hashPrevBlock = pprev->GetBlockHash(); block.hashMerkleRoot = hashMerkleRoot; block.nTime = nTime; - if (g_con_blockheightinheader) { + // Dynafed headers always serialize block_height as part of their + // identity (see CBlockHeader::Serialize), so it must be reconstructed + // regardless of the legacy -con_blockheightinheader option. + if (g_con_blockheightinheader || is_dynafed_block()) { block.block_height = nHeight; } block.nBits = nBits; @@ -542,7 +545,10 @@ class CDiskBlockIndex : public CBlockIndex block.hashPrevBlock = hashPrev; block.hashMerkleRoot = hashMerkleRoot; block.nTime = nTime; - if (g_con_blockheightinheader) { + // Dynafed headers always serialize block_height as part of their + // identity (see CBlockHeader::Serialize), so it must be reconstructed + // regardless of the legacy -con_blockheightinheader option. + if (g_con_blockheightinheader || is_dynafed_block()) { block.block_height = nHeight; } block.nBits = nBits; diff --git a/src/chainparams.h b/src/chainparams.h index 3e318577eca..9087c733ce5 100644 --- a/src/chainparams.h +++ b/src/chainparams.h @@ -135,6 +135,21 @@ class CChainParams bool MineBlocksOnDemand() const { return consensus.fPowNoRetargeting; } /** Return the network string */ std::string NetworkIDString() const { return strNetworkID; } + /** + * ELEMENTS: Whether SIGHASH_RANGEPROOF can be assumed active for this chain + * from the chain parameters alone (i.e. without inspecting the current tip). + * Used by offline tooling such as elements-tx that has no chainstate to + * decide the default sighash. This is true when dynafed (which enables + * SCRIPT_SIGHASH_RANGEPROOF) is configured ALWAYS_ACTIVE (e.g. elementsregtest + * with dynafed enabled, or liquidv1test), or on liquidv1 where dynafed is + * height-activated (nStartTime is a block height, not the ALWAYS_ACTIVE + * sentinel) but is long since active on the live chain. + */ + bool SighashRangeproofActiveByParams() const + { + return consensus.vDeployments[Consensus::DEPLOYMENT_DYNA_FED].nStartTime == Consensus::BIP9Deployment::ALWAYS_ACTIVE + || NetworkIDString() == CBaseChainParams::LIQUID1; + } /** Return the list of hostnames to look up for DNS seeds */ const std::vector& DNSSeeds() const { return vSeeds; } const std::vector& Base58Prefix(Base58Type type) const { return base58Prefixes[type]; } @@ -162,6 +177,9 @@ class CChainParams PeginSubsidy GetPeginSubsidy() const { return pegin_subsidy; } PeginMinimum GetPeginMinimum() const { return pegin_minimum; } + // 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/dynafed.cpp b/src/dynafed.cpp index cb288a83ed7..9918f8de7a4 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; } diff --git a/src/init.cpp b/src/init.cpp index fdbf6644f5f..07f30b910c6 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -1060,6 +1060,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/interfaces/chain.h b/src/interfaces/chain.h index 96aae06e820..3d98072630b 100644 --- a/src/interfaces/chain.h +++ b/src/interfaces/chain.h @@ -163,6 +163,13 @@ class Chain //! Check if transaction is RBF opt in. virtual RBFTransactionState isRBFOptIn(const CTransaction& tx) = 0; + //! ELEMENTS: Check whether SIGHASH_RANGEPROOF is active for signing at the + //! current chain tip (i.e. dynafed, which enables SCRIPT_SIGHASH_RANGEPROOF, + //! is active). Used to decide the default pre-Taproot sighash so that we only + //! commit to output rangeproofs when doing so yields standard, valid + //! signatures. + virtual bool isSighashRangeproofActive() = 0; + //! Check if transaction is in mempool. virtual bool isInMempool(const uint256& txid) = 0; diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index 2cfa2fc6aef..38ae5dc31c7 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -572,6 +572,14 @@ class ChainImpl : public Chain LOCK(m_node.mempool->cs); return IsRBFOptIn(tx, *m_node.mempool); } + bool isSighashRangeproofActive() override + { + // Mirror the mempool standardness check in MemPoolAccept: dynafed being + // active after the current tip enables SCRIPT_SIGHASH_RANGEPROOF, which + // is what makes SIGHASH_RANGEPROOF signatures standard and valid. + LOCK(::cs_main); + return DeploymentActiveAfter(chainman().ActiveChain().Tip(), Params().GetConsensus(), Consensus::DEPLOYMENT_DYNA_FED); + } bool isInMempool(const uint256& txid) override { if (!m_node.mempool) return false; 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/blockchain.cpp b/src/rpc/blockchain.cpp index 04c5b5fb6c3..d5fe8fe0997 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -3106,6 +3107,7 @@ static RPCHelpMan getsidechaininfo() {RPCResult::Type::ARR, "current_fedpegscripts", "The currently-enforced fedpegscripts in hex. Peg-ins for any entries on this list are honored by consensus and policy. Newest first. Two total entries are possible", {{RPCResult::Type::STR_HEX, "", "active fedpegscript"}}}, {RPCResult::Type::STR_HEX, "pegged_asset", "Pegged asset type"}, + {RPCResult::Type::STR_HEX, "fee_asset", "The asset used for transaction fees and relay policy"}, {RPCResult::Type::STR, "min_peg_diff", "The minimum difficulty parent chain header target. Peg-in headers that have less work will be rejected as an anti-Dos measure"}, {RPCResult::Type::STR_HEX, "parent_blockhash", "The parent genesis blockhash as source of pegged-in funds"}, {RPCResult::Type::BOOL, "parent_chain_has_pow", "Whether parent chain has pow or signed blocks"}, @@ -3147,6 +3149,7 @@ static RPCHelpMan getsidechaininfo() obj.pushKV("current_fedpeg_programs", fedpeg_prog_entries); obj.pushKV("current_fedpegscripts", fedpeg_entries); obj.pushKV("pegged_asset", consensus.pegged_asset.GetHex()); + obj.pushKV("fee_asset", policyAsset.GetHex()); obj.pushKV("min_peg_diff", consensus.parentChainPowLimit.GetHex()); obj.pushKV("parent_blockhash", parent_blockhash.GetHex()); obj.pushKV("parent_chain_has_pow", consensus.ParentChainHasPow()); diff --git a/src/rpc/misc.cpp b/src/rpc/misc.cpp index 05a758f948b..2f35c419259 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", @@ -869,6 +887,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/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 7defd97c88c..37db5020905 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -988,7 +989,10 @@ static RPCHelpMan signrawtransactionwithkey() ParsePrevouts(request.params[2], &keystore, coins); UniValue result(UniValue::VOBJ); - SignTransaction(mtx, &keystore, coins, request.params[3], result, chainman.ActiveChain().Tip()); + const auto [tip, sighash_rangeproof_active] = WITH_LOCK(::cs_main, return std::make_pair( + chainman.ActiveChain().Tip(), + DeploymentActiveAfter(chainman.ActiveChain().Tip(), Params().GetConsensus(), Consensus::DEPLOYMENT_DYNA_FED))); + SignTransaction(mtx, &keystore, coins, request.params[3], result, tip, sighash_rangeproof_active); return result; }, }; diff --git a/src/rpc/rawtransaction_util.cpp b/src/rpc/rawtransaction_util.cpp index e41679401ac..874307376cf 100644 --- a/src/rpc/rawtransaction_util.cpp +++ b/src/rpc/rawtransaction_util.cpp @@ -560,9 +560,11 @@ bool ValidateTransactionPeginInputs(const CMutableTransaction& mtx, const CBlock return immature_pegin; } -void SignTransaction(CMutableTransaction& mtx, const SigningProvider* keystore, const std::map& coins, const UniValue& hashType, UniValue& result, const CBlockIndex* active_chain_tip) +void SignTransaction(CMutableTransaction& mtx, const SigningProvider* keystore, const std::map& coins, const UniValue& hashType, UniValue& result, const CBlockIndex* active_chain_tip, bool sighash_rangeproof_active) { - int nHashType = ParseSighashString(hashType); + // ELEMENTS: when no sighash is specified, default to committing to + // rangeproofs if SIGHASH_RANGEPROOF is active at the current tip. + int nHashType = hashType.isNull() ? DefaultSighashType(sighash_rangeproof_active) : ParseSighashString(hashType); // Script verification errors std::map input_errors; diff --git a/src/rpc/rawtransaction_util.h b/src/rpc/rawtransaction_util.h index 6776472b538..58d201cc57e 100644 --- a/src/rpc/rawtransaction_util.h +++ b/src/rpc/rawtransaction_util.h @@ -34,7 +34,7 @@ class SigningProvider; * @param hashType The signature hash type * @param result JSON object where signed transaction results accumulate */ -void SignTransaction(CMutableTransaction& mtx, const SigningProvider* keystore, const std::map& coins, const UniValue& hashType, UniValue& result, const CBlockIndex* active_chain_tip); +void SignTransaction(CMutableTransaction& mtx, const SigningProvider* keystore, const std::map& coins, const UniValue& hashType, UniValue& result, const CBlockIndex* active_chain_tip, bool sighash_rangeproof_active); void SignTransactionResultToJSON(CMutableTransaction& mtx, bool complete, const std::map& coins, const std::map& input_errors, bool immature_pegin, UniValue& result); /** diff --git a/src/script/interpreter.cpp b/src/script/interpreter.cpp index dea5506b77f..8a1b4eb3f4c 100644 --- a/src/script/interpreter.cpp +++ b/src/script/interpreter.cpp @@ -2831,6 +2831,9 @@ int SigHashCache::CacheIndex(int32_t hash_type) const noexcept { // Note that we do not distinguish between BASE and WITNESS_V0 to determine the cache index, // because no input can simultaneously use both. + // ELEMENTS: SIGHASH_RANGEPROOF changes the preimage (segwit v0 appends hashRangeproofs; the + // legacy serializer appends each output's rangeproof and surjectionproof), so it must be a + // dimension of the cache key. return 8 * !!(hash_type & SIGHASH_RANGEPROOF) + // bit 3 3 * !!(hash_type & SIGHASH_ANYONECANPAY) + // bit 2 2 * ((hash_type & 0x1f) == SIGHASH_SINGLE) + // bit 1 diff --git a/src/script/interpreter.h b/src/script/interpreter.h index 3de1b2272b7..b8ee0b4ac40 100644 --- a/src/script/interpreter.h +++ b/src/script/interpreter.h @@ -37,6 +37,15 @@ enum // ELEMENTS: // A flag that means the rangeproofs should be included in the sighash. SIGHASH_RANGEPROOF = 0x40, + + // ELEMENTS: + // The default sighash used by wallets/tools when signing pre-Taproot + // (BASE/WITNESS_V0) inputs on chains where SIGHASH_RANGEPROOF is active. + // This commits to the output rangeproofs, closing the pre-Taproot + // rangeproof (witness) malleability gap. Note this must only be used once + // dynafed (which enables SCRIPT_SIGHASH_RANGEPROOF) is active for the target + // chain; otherwise the resulting signatures are non-standard and invalid. + SIGHASH_ALL_WITH_RANGEPROOF = SIGHASH_ALL | SIGHASH_RANGEPROOF, }; /** Script verification flags. 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; diff --git a/src/script/sign.cpp b/src/script/sign.cpp index cc8a9f0d83d..ed112ff46f5 100644 --- a/src/script/sign.cpp +++ b/src/script/sign.cpp @@ -36,6 +36,15 @@ MutableTransactionSignatureCreator::MutableTransactionSignatureCreator(const CMu { } +int DefaultSighashType(bool sighash_rangeproof_active) +{ + // When SIGHASH_RANGEPROOF is active for the chain, default to committing to + // rangeproofs for pre-Taproot inputs. The 0x40 bit is stripped for Taproot + // signing (see CreateSchnorrSig), so this is a safe universal default. + // Otherwise fall back to SIGHASH_DEFAULT (== SIGHASH_ALL for pre-Taproot). + return sighash_rangeproof_active ? SIGHASH_ALL_WITH_RANGEPROOF : SIGHASH_DEFAULT; +} + bool MutableTransactionSignatureCreator::CreateSig(const SigningProvider& provider, std::vector& vchSig, const CKeyID& address, const CScript& scriptCode, SigVersion sigversion, unsigned int flags) const { assert(sigversion == SigVersion::BASE || sigversion == SigVersion::WITNESS_V0); @@ -83,12 +92,16 @@ bool MutableTransactionSignatureCreator::CreateSchnorrSig(const SigningProvider& execdata.m_tapleaf_hash_init = true; execdata.m_tapleaf_hash = *leaf_hash; } + // ELEMENTS: SIGHASH_RANGEPROOF is a pre-Taproot-only flag; the BIP341-style + // sighash always commits to rangeproofs and rejects the 0x40 bit. Strip it so + // that a universal default of SIGHASH_ALL_WITH_RANGEPROOF still produces valid + // Taproot signatures. + const int taproot_hashtype = nHashType & ~SIGHASH_RANGEPROOF; uint256 hash; - if (!SignatureHashSchnorr(hash, execdata, *txTo, nIn, nHashType, sigversion, *m_txdata, MissingDataBehavior::FAIL)) return false; + if (!SignatureHashSchnorr(hash, execdata, *txTo, nIn, taproot_hashtype, sigversion, *m_txdata, MissingDataBehavior::FAIL)) return false; sig.resize(64); - // Use uint256{} as aux_rnd for now. if (!key.SignSchnorr(hash, sig, merkle_root, {})) return false; - if (nHashType) sig.push_back(nHashType); + if (taproot_hashtype) sig.push_back(taproot_hashtype); return true; } @@ -702,7 +715,7 @@ bool SignTransaction(CMutableTransaction& mtx, const SigningProvider* keystore, } ScriptError serror = SCRIPT_ERR_OK; - if (!VerifyScript(txin.scriptSig, prevPubKey, &inWitness.scriptWitness, STANDARD_SCRIPT_VERIFY_FLAGS, TransactionSignatureChecker(&txConst, i, amount, txdata, MissingDataBehavior::FAIL), &serror)) { + if (!sigdata.complete && !VerifyScript(txin.scriptSig, prevPubKey, &inWitness.scriptWitness, STANDARD_SCRIPT_VERIFY_FLAGS, TransactionSignatureChecker(&txConst, i, amount, txdata, MissingDataBehavior::FAIL), &serror)) { if (serror == SCRIPT_ERR_INVALID_STACK_OPERATION) { // Unable to sign input and verification failed (possible attempt to partially sign). input_errors[i] = Untranslated("Unable to sign input, invalid stack size (possibly missing key)"); diff --git a/src/script/sign.h b/src/script/sign.h index 33b63c62c61..12dad781510 100644 --- a/src/script/sign.h +++ b/src/script/sign.h @@ -105,4 +105,18 @@ bool IsSegWitOutput(const SigningProvider& provider, const CScript& script); /** Sign the CMutableTransaction */ bool SignTransaction(CMutableTransaction& mtx, const SigningProvider* provider, const std::map& coins, int sighash, const uint256& hash_genesis_block, std::map& input_errors); +/** + * ELEMENTS: Return the default sighash type to use when the caller did not + * specify one. When SIGHASH_RANGEPROOF is active for the target chain, the + * default commits to output rangeproofs (SIGHASH_ALL_WITH_RANGEPROOF for + * pre-Taproot inputs); otherwise the historical default (SIGHASH_DEFAULT, which + * is equivalent to SIGHASH_ALL for pre-Taproot) is used so that signatures stay + * standard and valid on chains where dynafed is not active. + * + * Note: for Taproot inputs the sighash byte's rangeproof bit is ignored (the + * BIP341-style sighash always commits to rangeproofs), so this default is only + * meaningful for BASE/WITNESS_V0 signing. + */ +int DefaultSighashType(bool sighash_rangeproof_active); + #endif // BITCOIN_SCRIPT_SIGN_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 */ 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/test/sighash_tests.cpp b/src/test/sighash_tests.cpp index 06d0ed758ac..80a8d07dbe8 100644 --- a/src/test/sighash_tests.cpp +++ b/src/test/sighash_tests.cpp @@ -7,6 +7,7 @@ #include #include