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/) | | | | | 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/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/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; } 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 2af4fb2a3e7..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); - 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/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/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/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;