This commit is contained in:
Salvatore Ingala 2025-04-29 11:54:07 +02:00 committed by GitHub
commit e092c32873
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 1651 additions and 44 deletions

View file

@ -32,6 +32,7 @@ constexpr bool ValidDeployment(BuriedDeployment dep) { return dep <= DEPLOYMENT_
enum DeploymentPos : uint16_t {
DEPLOYMENT_TESTDUMMY,
DEPLOYMENT_TAPROOT, // Deployment of Schnorr/Taproot (BIPs 340-342)
DEPLOYMENT_CHECKCONTRACTVERIFY,
// NOTE: Also add new deployments to VersionBitsDeploymentInfo in deploymentinfo.cpp
MAX_VERSION_BITS_DEPLOYMENTS
};

View file

@ -17,6 +17,10 @@ const struct VBDeploymentInfo VersionBitsDeploymentInfo[Consensus::MAX_VERSION_B
/*.name =*/ "taproot",
/*.gbt_force =*/ true,
},
{
/*.name =*/ "checkcontractverify",
/*.gbt_force =*/ true,
},
};
std::string DeploymentName(Consensus::BuriedDeployment dep)

View file

@ -117,6 +117,12 @@ public:
consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].nTimeout = 1628640000; // August 11th, 2021
consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].min_activation_height = 709632; // Approximately November 12th, 2021
// Deployment of OP_CHECKCONTRACTVERIFY
consensus.vDeployments[Consensus::DEPLOYMENT_CHECKCONTRACTVERIFY].bit = 3;
consensus.vDeployments[Consensus::DEPLOYMENT_CHECKCONTRACTVERIFY].nStartTime = Consensus::BIP9Deployment::NEVER_ACTIVE;
consensus.vDeployments[Consensus::DEPLOYMENT_CHECKCONTRACTVERIFY].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT;
consensus.vDeployments[Consensus::DEPLOYMENT_CHECKCONTRACTVERIFY].min_activation_height = 0;
consensus.nMinimumChainWork = uint256{"0000000000000000000000000000000000000000b1f3b93b65b16d035a82be84"};
consensus.defaultAssumeValid = uint256{"00000000000000000001b658dd1120e82e66d2790811f89ede9742ada3ed6d77"}; // 886157
@ -229,6 +235,12 @@ public:
consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].nTimeout = 1628640000; // August 11th, 2021
consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].min_activation_height = 0; // No activation delay
// Deployment of OP_CHECKCONTRACTVERIFY
consensus.vDeployments[Consensus::DEPLOYMENT_CHECKCONTRACTVERIFY].bit = 3;
consensus.vDeployments[Consensus::DEPLOYMENT_CHECKCONTRACTVERIFY].nStartTime = Consensus::BIP9Deployment::NEVER_ACTIVE;
consensus.vDeployments[Consensus::DEPLOYMENT_CHECKCONTRACTVERIFY].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT;
consensus.vDeployments[Consensus::DEPLOYMENT_CHECKCONTRACTVERIFY].min_activation_height = 0;
consensus.nMinimumChainWork = uint256{"0000000000000000000000000000000000000000000015f5e0c9f13455b0eb17"};
consensus.defaultAssumeValid = uint256{"00000000000003fc7967410ba2d0a8a8d50daedc318d43e8baf1a9782c236a57"}; // 3974606
@ -322,6 +334,12 @@ public:
consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT;
consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].min_activation_height = 0; // No activation delay
// Deployment of OP_CHECKCONTRACTVERIFY
consensus.vDeployments[Consensus::DEPLOYMENT_CHECKCONTRACTVERIFY].bit = 3;
consensus.vDeployments[Consensus::DEPLOYMENT_CHECKCONTRACTVERIFY].nStartTime = Consensus::BIP9Deployment::NEVER_ACTIVE;
consensus.vDeployments[Consensus::DEPLOYMENT_CHECKCONTRACTVERIFY].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT;
consensus.vDeployments[Consensus::DEPLOYMENT_CHECKCONTRACTVERIFY].min_activation_height = 0;
consensus.nMinimumChainWork = uint256{"0000000000000000000000000000000000000000000001d6dce8651b6094e4c1"};
consensus.defaultAssumeValid = uint256{"0000000000003ed4f08dbdf6f7d6b271a6bcffce25675cb40aa9fa43179a89f3"}; // 72600
@ -454,6 +472,12 @@ public:
consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT;
consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].min_activation_height = 0; // No activation delay
// Deployment of OP_CHECKCONTRACTVERIFY
consensus.vDeployments[Consensus::DEPLOYMENT_CHECKCONTRACTVERIFY].bit = 3;
consensus.vDeployments[Consensus::DEPLOYMENT_CHECKCONTRACTVERIFY].nStartTime = Consensus::BIP9Deployment::NEVER_ACTIVE;
consensus.vDeployments[Consensus::DEPLOYMENT_CHECKCONTRACTVERIFY].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT;
consensus.vDeployments[Consensus::DEPLOYMENT_CHECKCONTRACTVERIFY].min_activation_height = 0;
// message start is defined as the first 4 bytes of the sha256d of the block script
HashWriter h{};
h << consensus.signet_challenge;
@ -529,6 +553,12 @@ public:
consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT;
consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].min_activation_height = 0; // No activation delay
// Deployment of OP_CHECKCONTRACTVERIFY
consensus.vDeployments[Consensus::DEPLOYMENT_CHECKCONTRACTVERIFY].bit = 3;
consensus.vDeployments[Consensus::DEPLOYMENT_CHECKCONTRACTVERIFY].nStartTime = Consensus::BIP9Deployment::ALWAYS_ACTIVE;
consensus.vDeployments[Consensus::DEPLOYMENT_CHECKCONTRACTVERIFY].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT;
consensus.vDeployments[Consensus::DEPLOYMENT_CHECKCONTRACTVERIFY].min_activation_height = 0;
consensus.nMinimumChainWork = uint256{};
consensus.defaultAssumeValid = uint256{};

View file

@ -273,6 +273,40 @@ std::optional<std::pair<XOnlyPubKey, bool>> XOnlyPubKey::CreateTapTweak(const ui
return ret;
}
bool XOnlyPubKey::CheckDoubleTweak(const XOnlyPubKey& naked_key, const std::vector<unsigned char>& data, const uint256* merkle_root) const
{
int parity;
secp256k1_xonly_pubkey internal_xonly;
if (data.empty()) {
// No data tweak; the internal key is the naked key
if (!secp256k1_xonly_pubkey_parse(secp256k1_context_static, &internal_xonly, naked_key.data())) return false;
} else {
// Compute the sha256 of naked_key || data
uint256 data_tweak = (HashWriter{} << naked_key << MakeUCharSpan(data)).GetSHA256();
secp256k1_xonly_pubkey naked_key_parsed;
if (!secp256k1_xonly_pubkey_parse(secp256k1_context_static, &naked_key_parsed, naked_key.data())) return false;
secp256k1_pubkey internal;
if (!secp256k1_xonly_pubkey_tweak_add(secp256k1_context_static, &internal, &naked_key_parsed, data_tweak.data())) return false;
if (!secp256k1_xonly_pubkey_from_pubkey(secp256k1_context_static, &internal_xonly, &parity, &internal)) return false;
}
secp256k1_xonly_pubkey expected_xonly;
if (!secp256k1_xonly_pubkey_parse(secp256k1_context_static, &expected_xonly, m_keydata.data())) return false;
if (merkle_root != nullptr) {
// Compute the taptweak based on merkle_root
unsigned char pubkey_bytes[32];
secp256k1_xonly_pubkey_serialize(secp256k1_context_static, pubkey_bytes, &internal_xonly);
XOnlyPubKey internal_key = XOnlyPubKey(pubkey_bytes);
uint256 tweak = internal_key.ComputeTapTweakHash(merkle_root);
secp256k1_pubkey result;
if (!secp256k1_xonly_pubkey_tweak_add(secp256k1_context_static, &result, &internal_xonly, tweak.begin())) return false;
secp256k1_xonly_pubkey result_xonly;
if (!secp256k1_xonly_pubkey_from_pubkey(secp256k1_context_static, &result_xonly, &parity, &result)) return false;
return secp256k1_xonly_pubkey_cmp(secp256k1_context_static, &result_xonly, &expected_xonly) == 0;
} else {
// If merkle_root is nullptr, compare internal_xonly with expected_xonly
return secp256k1_xonly_pubkey_cmp(secp256k1_context_static, &internal_xonly, &expected_xonly) == 0;
}
}
bool CPubKey::Verify(const uint256 &hash, const std::vector<unsigned char>& vchSig) const {
if (!IsValid())

View file

@ -282,6 +282,12 @@ public:
/** Construct a Taproot tweaked output point with this point as internal key. */
std::optional<std::pair<XOnlyPubKey, bool>> CreateTapTweak(const uint256* merkle_root) const;
/** Verify that this key is obtained from the x-only pubkey `naked` after applying in sequence:
* - the tweak with `data` (this tweak is skipped if `data` is empty);
* - the taptweak with `merkle_root` (unless it's null).
*/
bool CheckDoubleTweak(const XOnlyPubKey& naked_key, const std::vector<unsigned char>& data, const uint256* merkle_root) const;
/** Returns a list of CKeyIDs for the CPubKeys that could have been used to create this XOnlyPubKey.
* This is needed for key lookups since keys are indexed by CKeyID.
*/

View file

@ -14,6 +14,15 @@
typedef std::vector<unsigned char> valtype;
//! Flag to mark an OP_CHECKCONTRACVERIFY as referring to an input.
const int CCV_MODE_CHECK_INPUT = -1;
//! Flag to specify that an OP_CHECKCONTRACVERIFY which refers to an output, with the default amount logic.
const int CCV_MODE_CHECK_OUTPUT = 0;
//! Flag to specify that an OP_CHECKCONTRACVERIFY which refers to an output does not check the output amount.
const int CCV_MODE_CHECK_OUTPUT_IGNORE_AMOUNT = 1;
//! Flag to specify that an OP_CHECKCONTRACVERIFY referring to an output deducts the amount of its output from the current input amount for future calls.
const int CCV_MODE_CHECK_OUTPUT_DEDUCT_AMOUNT = 2;
namespace {
inline bool set_success(ScriptError* ret)
@ -403,7 +412,7 @@ static bool EvalChecksig(const valtype& sig, const valtype& pubkey, CScript::con
assert(false);
}
bool EvalScript(std::vector<std::vector<unsigned char> >& stack, const CScript& script, unsigned int flags, const BaseSignatureChecker& checker, SigVersion sigversion, ScriptExecutionData& execdata, ScriptError* serror)
bool EvalScript(std::vector<std::vector<unsigned char> >& stack, const CScript& script, unsigned int flags, const BaseSignatureChecker& checker, SigVersion sigversion, ScriptExecutionData& execdata, ScriptError* serror, TransactionExecutionData* tx_exec_data)
{
static const CScriptNum bnZero(0);
static const CScriptNum bnOne(1);
@ -1101,6 +1110,51 @@ bool EvalScript(std::vector<std::vector<unsigned char> >& stack, const CScript&
}
break;
case OP_CHECKCONTRACTVERIFY:
{
// OP_CHECKCONTRACTVERIFY is only available in Tapscript
if (sigversion == SigVersion::BASE || sigversion == SigVersion::WITNESS_V0) return set_error(serror, SCRIPT_ERR_BAD_OPCODE);
// we expect at least the flag to be on the stack
if (stack.empty())
return set_error(serror, SCRIPT_ERR_INVALID_STACK_OPERATION);
// initially, read only a single parameter at the top of stack
int flags = CScriptNum(stacktop(-1), fRequireMinimal).getint();
if (flags < -1 || flags > CCV_MODE_CHECK_OUTPUT_DEDUCT_AMOUNT) {
// undefined values of the flags; keep OP_SUCCESS behavior
// in order to enable future upgrades via soft-fork
stack = { {1} };
return set_success(serror);
}
// all currently defined versions require exactly 5 stack elements
// (data index pk taptree flags -- )
if (stack.size() < 5)
return set_error(serror, SCRIPT_ERR_INVALID_STACK_OPERATION);
valtype& data = stacktop(-5);
int index = CScriptNum(stacktop(-4), fRequireMinimal).getint();
valtype& pk = stacktop(-3);
valtype& taptree = stacktop(-2);
if (!pk.empty() && pk != std::vector<unsigned char>{0x81} && pk.size() != 32) {
return set_error(serror, SCRIPT_ERR_CHECKCONTRACTVERIFY_WRONG_ARGS);
}
if (!checker.CheckContract(flags, index, pk, data, taptree, execdata, serror, tx_exec_data)) {
return false; // serror is set
}
popstack(stack);
popstack(stack);
popstack(stack);
popstack(stack);
popstack(stack);
}
break;
case OP_CHECKMULTISIG:
case OP_CHECKMULTISIGVERIFY:
{
@ -1233,10 +1287,10 @@ bool EvalScript(std::vector<std::vector<unsigned char> >& stack, const CScript&
return set_success(serror);
}
bool EvalScript(std::vector<std::vector<unsigned char> >& stack, const CScript& script, unsigned int flags, const BaseSignatureChecker& checker, SigVersion sigversion, ScriptError* serror)
bool EvalScript(std::vector<std::vector<unsigned char> >& stack, const CScript& script, unsigned int flags, const BaseSignatureChecker& checker, SigVersion sigversion, ScriptError* serror, TransactionExecutionData* tx_exec_data)
{
ScriptExecutionData execdata;
return EvalScript(stack, script, flags, checker, sigversion, execdata, serror);
return EvalScript(stack, script, flags, checker, sigversion, execdata, serror, tx_exec_data);
}
namespace {
@ -1781,14 +1835,126 @@ bool GenericTransactionSignatureChecker<T>::CheckSequence(const CScriptNum& nSeq
return true;
}
template <class T>
bool GenericTransactionSignatureChecker<T>::CheckContract(int mode, int index, const std::vector<unsigned char>& pubkey, const std::vector<unsigned char>& data, const std::vector<unsigned char>& taptree, ScriptExecutionData& ScriptExecutionData, ScriptError* serror, TransactionExecutionData* tx_exec_data) const
{
assert(ScriptExecutionData.m_internal_key.has_value());
assert(ScriptExecutionData.m_taproot_merkle_root.has_value());
assert(tx_exec_data != nullptr);
if (!(txdata->m_bip341_taproot_ready && txdata->m_spent_outputs_ready)) {
return HandleMissingData(m_mdb);
}
bool use_current_taptree = taptree.size() == 1 && taptree.data()[0] == 0x81;
bool use_current_pubkey = pubkey.size() == 1 && pubkey.data()[0] == 0x81;
uint256 merkle_tree;
const uint256 *merkle_tree_ptr = nullptr;
if (taptree.empty()) {
// no taptweak, leave nullptr
} else if (use_current_taptree) {
merkle_tree_ptr = &ScriptExecutionData.m_taproot_merkle_root.value();
} else if (taptree.size() == 32) {
merkle_tree = uint256(taptree);
merkle_tree_ptr = &merkle_tree;
} else {
return set_error(serror, SCRIPT_ERR_CHECKCONTRACTVERIFY_WRONG_ARGS);
}
XOnlyPubKey initialXOnlyKey;
if (use_current_pubkey) {
initialXOnlyKey = ScriptExecutionData.m_internal_key.value();
} else if (pubkey.empty()) {
initialXOnlyKey = XOnlyPubKey::NUMS_H;
} else {
initialXOnlyKey = XOnlyPubKey{std::span<const unsigned char>{pubkey.data(), pubkey.data() + 32}};
}
if (index == -1) {
index = nIn;
}
auto indexLimit = (mode == CCV_MODE_CHECK_INPUT ? txTo->vin.size() : txTo->vout.size());
if (index < 0 || index >= static_cast<int>(indexLimit)) {
return set_error(serror, SCRIPT_ERR_CHECKCONTRACTVERIFY_OUT_OF_BOUNDS);
}
CScript scriptPubKey = (mode == CCV_MODE_CHECK_INPUT) ? txdata->m_spent_outputs[index].scriptPubKey : txTo->vout.at(index).scriptPubKey;
if (scriptPubKey.size() != 1 + 1 + 32 || scriptPubKey[0] != OP_1 || scriptPubKey[1] != 32) {
return set_error(serror, SCRIPT_ERR_CHECKCONTRACTVERIFY_WRONG_ARGS);
}
const XOnlyPubKey finalXOnlyKey{std::span<const unsigned char>{scriptPubKey.data() + 2, scriptPubKey.data() + 34}};
if (!finalXOnlyKey.CheckDoubleTweak(initialXOnlyKey, data, merkle_tree_ptr)) {
return set_error(serror, SCRIPT_ERR_CHECKCONTRACTVERIFY_MISMATCH);
}
if (!ScriptExecutionData.m_ccv_amount_init) {
ScriptExecutionData.m_ccv_amount = amount;
ScriptExecutionData.m_ccv_amount_init = true;
}
{
LOCK(tx_exec_data->m_mutex);
if (tx_exec_data->m_error.has_value()) {
// an error related to tx_exec_data already occurred while validating the transaction
return set_error(serror, tx_exec_data->m_error.value());
}
switch (mode) {
case CCV_MODE_CHECK_OUTPUT:
if (tx_exec_data->m_ccv_output_checked_deduct[index]) {
tx_exec_data->m_error = SCRIPT_ERR_CHECKCONTRACTVERIFY_WRONG_AMOUNT;
return set_error(serror, SCRIPT_ERR_CHECKCONTRACTVERIFY_WRONG_AMOUNT);
}
tx_exec_data->m_ccv_output_checked_default[index] = true;
tx_exec_data->m_ccv_output_min_amount[index] += ScriptExecutionData.m_ccv_amount;
ScriptExecutionData.m_ccv_amount = 0;
if (txTo->vout[index].nValue < tx_exec_data->m_ccv_output_min_amount[index]) {
tx_exec_data->m_error = SCRIPT_ERR_CHECKCONTRACTVERIFY_WRONG_AMOUNT;
return set_error(serror, SCRIPT_ERR_CHECKCONTRACTVERIFY_WRONG_AMOUNT);
}
break;
case CCV_MODE_CHECK_OUTPUT_IGNORE_AMOUNT:
// amount checking is disabled
break;
case CCV_MODE_CHECK_OUTPUT_DEDUCT_AMOUNT:
if (tx_exec_data->m_ccv_output_checked_default[index] || tx_exec_data->m_ccv_output_checked_deduct[index]) {
tx_exec_data->m_error = SCRIPT_ERR_CHECKCONTRACTVERIFY_WRONG_AMOUNT;
return set_error(serror, SCRIPT_ERR_CHECKCONTRACTVERIFY_WRONG_AMOUNT);
}
tx_exec_data->m_ccv_output_checked_deduct[index] = true;
// subtract amount from input
if (txTo->vout[index].nValue > ScriptExecutionData.m_ccv_amount) {
tx_exec_data->m_error = SCRIPT_ERR_CHECKCONTRACTVERIFY_WRONG_AMOUNT;
return set_error(serror, SCRIPT_ERR_CHECKCONTRACTVERIFY_WRONG_AMOUNT);
}
ScriptExecutionData.m_ccv_amount -= txTo->vout[index].nValue;
break;
default:
break;
}
}
return true;
}
// explicit instantiation
template class GenericTransactionSignatureChecker<CTransaction>;
template class GenericTransactionSignatureChecker<CMutableTransaction>;
static bool ExecuteWitnessScript(const std::span<const valtype>& stack_span, const CScript& exec_script, unsigned int flags, SigVersion sigversion, const BaseSignatureChecker& checker, ScriptExecutionData& execdata, ScriptError* serror)
static bool ExecuteWitnessScript(const std::span<const valtype>& stack_span, const CScript& exec_script, unsigned int flags, SigVersion sigversion, const BaseSignatureChecker& checker, ScriptExecutionData& execdata, TransactionExecutionData* tx_exec_data, ScriptError* serror)
{
std::vector<valtype> stack{stack_span.begin(), stack_span.end()};
const bool is_checkcontractverify_active = (flags & SCRIPT_VERIFY_CHECKCONTRACTVERIFY);
if (sigversion == SigVersion::TAPSCRIPT) {
// OP_SUCCESSx processing overrides everything, including stack element size limits
CScript::const_iterator pc = exec_script.begin();
@ -1799,7 +1965,9 @@ static bool ExecuteWitnessScript(const std::span<const valtype>& stack_span, con
return set_error(serror, SCRIPT_ERR_BAD_OPCODE);
}
// New opcodes will be listed here. May use a different sigversion to modify existing opcodes.
if (IsOpSuccess(opcode)) {
if (is_checkcontractverify_active && opcode == OP_CHECKCONTRACTVERIFY) {
continue;
} else if (IsOpSuccess(opcode)) {
if (flags & SCRIPT_VERIFY_DISCOURAGE_OP_SUCCESS) {
return set_error(serror, SCRIPT_ERR_DISCOURAGE_OP_SUCCESS);
}
@ -1817,7 +1985,7 @@ static bool ExecuteWitnessScript(const std::span<const valtype>& stack_span, con
}
// Run the script interpreter.
if (!EvalScript(stack, exec_script, flags, checker, sigversion, execdata, serror)) return false;
if (!EvalScript(stack, exec_script, flags, checker, sigversion, execdata, serror, tx_exec_data)) return false;
// Scripts inside witness implicitly require cleanstack behaviour
if (stack.size() != 1) return set_error(serror, SCRIPT_ERR_CLEANSTACK);
@ -1856,21 +2024,22 @@ uint256 ComputeTaprootMerkleRoot(std::span<const unsigned char> control, const u
return k;
}
static bool VerifyTaprootCommitment(const std::vector<unsigned char>& control, const std::vector<unsigned char>& program, const uint256& tapleaf_hash)
static bool VerifyTaprootCommitment(const std::vector<unsigned char>& control, const std::vector<unsigned char>& program, const uint256& tapleaf_hash, std::optional<XOnlyPubKey>& internal_key, std::optional<uint256>& merkle_root)
{
assert(control.size() >= TAPROOT_CONTROL_BASE_SIZE);
assert(program.size() >= uint256::size());
//! The internal pubkey (x-only, so no Y coordinate parity).
const XOnlyPubKey p{std::span{control}.subspan(1, TAPROOT_CONTROL_BASE_SIZE - 1)};
internal_key = p;
//! The output pubkey (taken from the scriptPubKey).
const XOnlyPubKey q{program};
// Compute the Merkle root from the leaf and the provided path.
const uint256 merkle_root = ComputeTaprootMerkleRoot(control, tapleaf_hash);
merkle_root = ComputeTaprootMerkleRoot(control, tapleaf_hash);
// Verify that the output pubkey matches the tweaked internal pubkey, after correcting for parity.
return q.CheckTapTweak(p, merkle_root, control[0] & 1);
return q.CheckTapTweak(p, merkle_root.value(), control[0] & 1);
}
static bool VerifyWitnessProgram(const CScriptWitness& witness, int witversion, const std::vector<unsigned char>& program, unsigned int flags, const BaseSignatureChecker& checker, ScriptError* serror, bool is_p2sh)
static bool VerifyWitnessProgram(const CScriptWitness& witness, int witversion, const std::vector<unsigned char>& program, unsigned int flags, const BaseSignatureChecker& checker, ScriptError* serror, bool is_p2sh, TransactionExecutionData* tx_exec_data = nullptr)
{
CScript exec_script; //!< Actually executed script (last stack item in P2WSH; implied P2PKH script in P2WPKH; leaf script in P2TR)
std::span stack{witness.stack};
@ -1889,14 +2058,14 @@ static bool VerifyWitnessProgram(const CScriptWitness& witness, int witversion,
if (memcmp(hash_exec_script.begin(), program.data(), 32)) {
return set_error(serror, SCRIPT_ERR_WITNESS_PROGRAM_MISMATCH);
}
return ExecuteWitnessScript(stack, exec_script, flags, SigVersion::WITNESS_V0, checker, execdata, serror);
return ExecuteWitnessScript(stack, exec_script, flags, SigVersion::WITNESS_V0, checker, execdata, tx_exec_data, serror);
} else if (program.size() == WITNESS_V0_KEYHASH_SIZE) {
// BIP141 P2WPKH: 20-byte witness v0 program (which encodes Hash160(pubkey))
if (stack.size() != 2) {
return set_error(serror, SCRIPT_ERR_WITNESS_PROGRAM_MISMATCH); // 2 items in witness
}
exec_script << OP_DUP << OP_HASH160 << program << OP_EQUALVERIFY << OP_CHECKSIG;
return ExecuteWitnessScript(stack, exec_script, flags, SigVersion::WITNESS_V0, checker, execdata, serror);
return ExecuteWitnessScript(stack, exec_script, flags, SigVersion::WITNESS_V0, checker, execdata, tx_exec_data, serror);
} else {
return set_error(serror, SCRIPT_ERR_WITNESS_PROGRAM_WRONG_LENGTH);
}
@ -1927,7 +2096,7 @@ static bool VerifyWitnessProgram(const CScriptWitness& witness, int witversion,
return set_error(serror, SCRIPT_ERR_TAPROOT_WRONG_CONTROL_SIZE);
}
execdata.m_tapleaf_hash = ComputeTapleafHash(control[0] & TAPROOT_LEAF_MASK, script);
if (!VerifyTaprootCommitment(control, program, execdata.m_tapleaf_hash)) {
if (!VerifyTaprootCommitment(control, program, execdata.m_tapleaf_hash, execdata.m_internal_key, execdata.m_taproot_merkle_root)) {
return set_error(serror, SCRIPT_ERR_WITNESS_PROGRAM_MISMATCH);
}
execdata.m_tapleaf_hash_init = true;
@ -1936,7 +2105,7 @@ static bool VerifyWitnessProgram(const CScriptWitness& witness, int witversion,
exec_script = CScript(script.begin(), script.end());
execdata.m_validation_weight_left = ::GetSerializeSize(witness.stack) + VALIDATION_WEIGHT_OFFSET;
execdata.m_validation_weight_left_init = true;
return ExecuteWitnessScript(stack, exec_script, flags, SigVersion::TAPSCRIPT, checker, execdata, serror);
return ExecuteWitnessScript(stack, exec_script, flags, SigVersion::TAPSCRIPT, checker, execdata, tx_exec_data, serror);
}
if (flags & SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_TAPROOT_VERSION) {
return set_error(serror, SCRIPT_ERR_DISCOURAGE_UPGRADABLE_TAPROOT_VERSION);
@ -1955,7 +2124,7 @@ static bool VerifyWitnessProgram(const CScriptWitness& witness, int witversion,
// There is intentionally no return statement here, to be able to use "control reaches end of non-void function" warnings to detect gaps in the logic above.
}
bool VerifyScript(const CScript& scriptSig, const CScript& scriptPubKey, const CScriptWitness* witness, unsigned int flags, const BaseSignatureChecker& checker, ScriptError* serror)
bool VerifyScript(const CScript& scriptSig, const CScript& scriptPubKey, const CScriptWitness* witness, unsigned int flags, const BaseSignatureChecker& checker, ScriptError* serror, TransactionExecutionData* tx_exec_data)
{
static const CScriptWitness emptyWitness;
if (witness == nullptr) {
@ -1995,7 +2164,7 @@ bool VerifyScript(const CScript& scriptSig, const CScript& scriptPubKey, const C
// The scriptSig must be _exactly_ CScript(), otherwise we reintroduce malleability.
return set_error(serror, SCRIPT_ERR_WITNESS_MALLEATED);
}
if (!VerifyWitnessProgram(*witness, witnessversion, witnessprogram, flags, checker, serror, /*is_p2sh=*/false)) {
if (!VerifyWitnessProgram(*witness, witnessversion, witnessprogram, flags, checker, serror, /*is_p2sh=*/false, tx_exec_data)) {
return false;
}
// Bypass the cleanstack check at the end. The actual stack is obviously not clean
@ -2040,7 +2209,7 @@ bool VerifyScript(const CScript& scriptSig, const CScript& scriptPubKey, const C
// reintroduce malleability.
return set_error(serror, SCRIPT_ERR_WITNESS_MALLEATED_P2SH);
}
if (!VerifyWitnessProgram(*witness, witnessversion, witnessprogram, flags, checker, serror, /*is_p2sh=*/true)) {
if (!VerifyWitnessProgram(*witness, witnessversion, witnessprogram, flags, checker, serror, /*is_p2sh=*/true, tx_exec_data)) {
return false;
}
// Bypass the cleanstack check at the end. The actual stack is obviously not clean

View file

@ -9,13 +9,16 @@
#include <consensus/amount.h>
#include <hash.h>
#include <primitives/transaction.h>
#include <pubkey.h>
#include <script/script_error.h> // IWYU pragma: export
#include <span.h>
#include <sync.h>
#include <uint256.h>
#include <cstddef>
#include <cstdint>
#include <optional>
#include <unordered_map>
#include <vector>
class CPubKey;
@ -143,6 +146,9 @@ enum : uint32_t {
// Making unknown public key versions (in BIP 342 scripts) non-standard
SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_PUBKEYTYPE = (1U << 20),
// OP_CHECKCONTRACTVERIFY
SCRIPT_VERIFY_CHECKCONTRACTVERIFY = (1U << 21),
// Constants to point to the highest flag in use. Add new flags above this line.
//
SCRIPT_VERIFY_END_MARKER
@ -201,6 +207,10 @@ struct ScriptExecutionData
bool m_tapleaf_hash_init = false;
//! The tapleaf hash.
uint256 m_tapleaf_hash;
//! The taproot internal key.
std::optional<XOnlyPubKey> m_internal_key = std::nullopt;
//! The merkle root of the taproot tree.
std::optional<uint256> m_taproot_merkle_root = std::nullopt;
//! Whether m_codeseparator_pos is initialized.
bool m_codeseparator_pos_init = false;
@ -221,6 +231,73 @@ struct ScriptExecutionData
//! The hash of the corresponding output
std::optional<uint256> m_output_hash;
//! Whether m_ccv_amount is initialized.
bool m_ccv_amount_init = false;
//! Residual amount of the current input according to CHECKCONTRACTVERIFY semantics.
CAmount m_ccv_amount;
};
/** The state of the script interpreter that persists across inputs of a single transaction.
*
* As access happens across different worker threads, it is crucial that access to this struct
* is properly synchronized. Code accessing its members must hold the m_mutex lock.
* Currently only used for OP_CHECKCONTRACTVERIFY. */
struct TransactionExecutionData {
const CTransaction* m_tx;
Mutex m_mutex;
// If an error occurs during the evaluation of a condition related to the access to this
// struct, it must be stored here.
// The caller _must_ check if this field has value immediately after acquiring the lock,
// and return the corresponding script error if it does.
// This makes sure that the checks are idempotent if repeated with the same instance
// of TransactionExecutionData. This is necessary because the checks might mutate the
// state of the struct, and the check might be repeated multiple times.
std::optional<ScriptError> m_error GUARDED_BY(m_mutex) = std::nullopt;
// For each output, the minimum amount of that output in order for the transaction
// to be considered valid. Accumulated during the evaluation of OP_CHECKCONTRACTVERIFY
// with the 'default' semantics.
std::vector<CAmount> m_ccv_output_min_amount GUARDED_BY(m_mutex);
// Set to true if this output has been checked with OP_CHECKCONTRACTVERIFY
// with the 'default' semantics.
std::vector<bool> m_ccv_output_checked_default GUARDED_BY(m_mutex);
// Set to true if this output has been checked with OP_CHECKCONTRACTVERIFY
// with the 'deduct' semantics.
std::vector<bool> m_ccv_output_checked_deduct GUARDED_BY(m_mutex);
TransactionExecutionData(const CTransaction* tx)
: m_ccv_output_min_amount(tx ? tx->vout.size() : 0, 0),
m_ccv_output_checked_default(tx ? tx->vout.size() : 0, false),
m_ccv_output_checked_deduct(tx ? tx->vout.size() : 0, false)
{
assert(tx != nullptr); // Ensure the transaction pointer is valid
}
};
/** Creates and stores TransactionExecutionData instances for each transaction. */
class TransactionExecutionDataStore {
private:
Mutex m_mutex;
std::unordered_map<const CTransaction*, std::unique_ptr<TransactionExecutionData>> m_store GUARDED_BY(m_mutex);
public:
TransactionExecutionDataStore() = default;
// Retrieves the TransactionExecutionData for the given transaction.
// If it does not exist, it creates a new instance.
TransactionExecutionData* getOrCreate(const CTransaction* tx) EXCLUSIVE_LOCKS_REQUIRED(!m_mutex) {
LOCK(m_mutex);
auto it = m_store.find(tx);
if (it != m_store.end()) {
return it->second.get();
}
auto newData = std::make_unique<TransactionExecutionData>(tx);
TransactionExecutionData* newDataPtr = newData.get();
m_store[tx] = std::move(newData);
return newDataPtr;
}
};
/** Signature hash sizes */
@ -265,6 +342,11 @@ public:
return false;
}
virtual bool CheckContract(int mode, int index, const std::vector<unsigned char>& pubkey, const std::vector<unsigned char>& data, const std::vector<unsigned char>& taptree, ScriptExecutionData& ScriptExecutionData, ScriptError* serror, TransactionExecutionData* tx_exec_data) const
{
return false;
}
virtual ~BaseSignatureChecker() = default;
};
@ -301,6 +383,7 @@ public:
bool CheckSchnorrSignature(std::span<const unsigned char> sig, std::span<const unsigned char> pubkey, SigVersion sigversion, ScriptExecutionData& execdata, ScriptError* serror = nullptr) const override;
bool CheckLockTime(const CScriptNum& nLockTime) const override;
bool CheckSequence(const CScriptNum& nSequence) const override;
bool CheckContract(int mode, int index, const std::vector<unsigned char>& pubkey, const std::vector<unsigned char>& data, const std::vector<unsigned char>& taptree, ScriptExecutionData& ScriptExecutionData, ScriptError* serror, TransactionExecutionData* tx_exec_data) const override;
};
using TransactionSignatureChecker = GenericTransactionSignatureChecker<CTransaction>;
@ -343,9 +426,9 @@ uint256 ComputeTapbranchHash(std::span<const unsigned char> a, std::span<const u
* Requires control block to have valid length (33 + k*32, with k in {0,1,..,128}). */
uint256 ComputeTaprootMerkleRoot(std::span<const unsigned char> control, const uint256& tapleaf_hash);
bool EvalScript(std::vector<std::vector<unsigned char> >& stack, const CScript& script, unsigned int flags, const BaseSignatureChecker& checker, SigVersion sigversion, ScriptExecutionData& execdata, ScriptError* error = nullptr);
bool EvalScript(std::vector<std::vector<unsigned char> >& stack, const CScript& script, unsigned int flags, const BaseSignatureChecker& checker, SigVersion sigversion, ScriptError* error = nullptr);
bool VerifyScript(const CScript& scriptSig, const CScript& scriptPubKey, const CScriptWitness* witness, unsigned int flags, const BaseSignatureChecker& checker, ScriptError* serror = nullptr);
bool EvalScript(std::vector<std::vector<unsigned char> >& stack, const CScript& script, unsigned int flags, const BaseSignatureChecker& checker, SigVersion sigversion, ScriptExecutionData& execdata, ScriptError* error = nullptr, TransactionExecutionData* tx_exec_data = nullptr);
bool EvalScript(std::vector<std::vector<unsigned char> >& stack, const CScript& script, unsigned int flags, const BaseSignatureChecker& checker, SigVersion sigversion, ScriptError* error = nullptr, TransactionExecutionData* tx_exec_data = nullptr);
bool VerifyScript(const CScript& scriptSig, const CScript& scriptPubKey, const CScriptWitness* witness, unsigned int flags, const BaseSignatureChecker& checker, ScriptError* serror = nullptr, TransactionExecutionData* tx_exec_data = nullptr);
size_t CountWitnessSigOps(const CScript& scriptSig, const CScript& scriptPubKey, const CScriptWitness* witness, unsigned int flags);

View file

@ -149,6 +149,8 @@ std::string GetOpName(opcodetype opcode)
// Opcode added by BIP 342 (Tapscript)
case OP_CHECKSIGADD : return "OP_CHECKSIGADD";
case OP_CHECKCONTRACTVERIFY : return "OP_CHECKCONTRACTVERIFY";
case OP_INVALIDOPCODE : return "OP_INVALIDOPCODE";
default:
@ -360,7 +362,7 @@ bool IsOpSuccess(const opcodetype& opcode)
return opcode == 80 || opcode == 98 || (opcode >= 126 && opcode <= 129) ||
(opcode >= 131 && opcode <= 134) || (opcode >= 137 && opcode <= 138) ||
(opcode >= 141 && opcode <= 142) || (opcode >= 149 && opcode <= 153) ||
(opcode >= 187 && opcode <= 254);
(opcode >= 188 && opcode <= 254);
}
bool CheckMinimalPush(const std::vector<unsigned char>& data, opcodetype opcode) {

View file

@ -209,6 +209,8 @@ enum opcodetype
// Opcode added by BIP 342 (Tapscript)
OP_CHECKSIGADD = 0xba,
OP_CHECKCONTRACTVERIFY = 0xbb,
OP_INVALIDOPCODE = 0xff,
};

View file

@ -111,6 +111,14 @@ std::string ScriptErrorString(const ScriptError serror)
return "OP_CHECKMULTISIG(VERIFY) is not available in tapscript";
case SCRIPT_ERR_TAPSCRIPT_MINIMALIF:
return "OP_IF/NOTIF argument must be minimal in tapscript";
case SCRIPT_ERR_CHECKCONTRACTVERIFY_WRONG_ARGS:
return "Invalid arguments for OP_CHECKCONTRACTVERIFY";
case SCRIPT_ERR_CHECKCONTRACTVERIFY_OUT_OF_BOUNDS:
return "Index of input/output out of bounds in OP_CHECKCONTRACTVERIFY";
case SCRIPT_ERR_CHECKCONTRACTVERIFY_MISMATCH:
return "Mismatching contract data or program";
case SCRIPT_ERR_CHECKCONTRACTVERIFY_WRONG_AMOUNT:
return "Incorrect amount for OP_CHECKCONTRACTVERIFY";
case SCRIPT_ERR_OP_CODESEPARATOR:
return "Using OP_CODESEPARATOR in non-witness script";
case SCRIPT_ERR_SIG_FINDANDDELETE:

View file

@ -78,6 +78,12 @@ typedef enum ScriptError_t
SCRIPT_ERR_TAPSCRIPT_CHECKMULTISIG,
SCRIPT_ERR_TAPSCRIPT_MINIMALIF,
/* CHECKCONTRACTVERIFY */
SCRIPT_ERR_CHECKCONTRACTVERIFY_WRONG_ARGS,
SCRIPT_ERR_CHECKCONTRACTVERIFY_OUT_OF_BOUNDS,
SCRIPT_ERR_CHECKCONTRACTVERIFY_MISMATCH,
SCRIPT_ERR_CHECKCONTRACTVERIFY_WRONG_AMOUNT,
/* Constant scriptCode */
SCRIPT_ERR_OP_CODESEPARATOR,
SCRIPT_ERR_SIG_FINDANDDELETE,

View file

@ -121,7 +121,9 @@ BOOST_AUTO_TEST_CASE(sign)
{
CScript sigSave = txTo[i].vin[0].scriptSig;
txTo[i].vin[0].scriptSig = txTo[j].vin[0].scriptSig;
bool sigOK = !CScriptCheck(txFrom.vout[txTo[i].vin[0].prevout.n], CTransaction(txTo[i]), signature_cache, 0, SCRIPT_VERIFY_P2SH | SCRIPT_VERIFY_STRICTENC, false, &txdata)().has_value();
auto imm_txTo = CTransaction(txTo[i]);
TransactionExecutionData tx_exec_data(&imm_txTo);
bool sigOK = !CScriptCheck(txFrom.vout[txTo[i].vin[0].prevout.n], imm_txTo, signature_cache, 0, SCRIPT_VERIFY_P2SH | SCRIPT_VERIFY_STRICTENC, false, &txdata, &tx_exec_data)().has_value();
if (i == j)
BOOST_CHECK_MESSAGE(sigOK, strprintf("VerifySignature %d %d", i, j));
else

View file

@ -1608,6 +1608,15 @@ static void AssetTest(const UniValue& test, SignatureCache& signature_cache)
// a subset of test_flags.
if (fin || ((flags & test_flags) == flags)) {
bool ret = VerifyScript(tx.vin[idx].scriptSig, prevouts[idx].scriptPubKey, &tx.vin[idx].scriptWitness, flags, txcheck, nullptr);
// HACK: Skip two failing tests due to 0xbb no longer being an op_success on regtest.
// This is of course just a workaround for the draft PR.
if (test["comment"].get_str().substr(0, 9) == "opsuccess" &&
tx.vin[idx].scriptWitness.stack.size() > 0 &&
(tx.vin[idx].scriptWitness.stack[0] == std::vector<unsigned char>{0xbb} ||
tx.vin[idx].scriptWitness.stack[0] == std::vector<unsigned char>{0x00, 0x63, 0xbb, 0x68})) {
continue;
}
BOOST_CHECK(ret);
}
}

View file

@ -70,6 +70,7 @@ static std::map<std::string, unsigned int> mapFlagNames = {
{std::string("DISCOURAGE_UPGRADABLE_PUBKEYTYPE"), (unsigned int)SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_PUBKEYTYPE},
{std::string("DISCOURAGE_OP_SUCCESS"), (unsigned int)SCRIPT_VERIFY_DISCOURAGE_OP_SUCCESS},
{std::string("DISCOURAGE_UPGRADABLE_TAPROOT_VERSION"), (unsigned int)SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_TAPROOT_VERSION},
{std::string("CHECKCONTRACTVERIFY"), (unsigned int)SCRIPT_VERIFY_CHECKCONTRACTVERIFY},
};
unsigned int ParseScriptFlags(std::string strFlags)
@ -567,6 +568,7 @@ BOOST_AUTO_TEST_CASE(test_big_witness_transaction)
// check all inputs concurrently, with the cache
PrecomputedTransactionData txdata(tx);
TransactionExecutionData tx_exec_data(&tx);
CCheckQueue<CScriptCheck> scriptcheckqueue(/*batch_size=*/128, /*worker_threads_num=*/20);
CCheckQueueControl<CScriptCheck> control(&scriptcheckqueue);
@ -584,7 +586,7 @@ BOOST_AUTO_TEST_CASE(test_big_witness_transaction)
for(uint32_t i = 0; i < mtx.vin.size(); i++) {
std::vector<CScriptCheck> vChecks;
vChecks.emplace_back(coins[tx.vin[i].prevout.n].out, tx, signature_cache, i, SCRIPT_VERIFY_P2SH | SCRIPT_VERIFY_WITNESS, false, &txdata);
vChecks.emplace_back(coins[tx.vin[i].prevout.n].out, tx, signature_cache, i, SCRIPT_VERIFY_P2SH | SCRIPT_VERIFY_WITNESS, false, &txdata, &tx_exec_data);
control.Add(std::move(vChecks));
}

View file

@ -24,6 +24,7 @@ bool CheckInputScripts(const CTransaction& tx, TxValidationState& state,
const CCoinsViewCache& inputs, unsigned int flags, bool cacheSigStore,
bool cacheFullScriptStore, PrecomputedTransactionData& txdata,
ValidationCache& validation_cache,
TransactionExecutionDataStore &tx_exec_store,
std::vector<CScriptCheck>* pvChecks) EXCLUSIVE_LOCKS_REQUIRED(cs_main);
BOOST_AUTO_TEST_SUITE(txvalidationcache_tests)
@ -142,7 +143,8 @@ static void ValidateCheckInputsForAllFlags(const CTransaction &tx, uint32_t fail
// WITNESS requires P2SH
test_flags |= SCRIPT_VERIFY_P2SH;
}
bool ret = CheckInputScripts(tx, state, &active_coins_tip, test_flags, true, add_to_cache, txdata, validation_cache, nullptr);
TransactionExecutionDataStore tx_exec_store;
bool ret = CheckInputScripts(tx, state, &active_coins_tip, test_flags, true, add_to_cache, txdata, validation_cache, tx_exec_store, nullptr);
// CheckInputScripts should succeed iff test_flags doesn't intersect with
// failing_flags
bool expected_return_value = !(test_flags & failing_flags);
@ -152,13 +154,15 @@ static void ValidateCheckInputsForAllFlags(const CTransaction &tx, uint32_t fail
if (ret && add_to_cache) {
// Check that we get a cache hit if the tx was valid
std::vector<CScriptCheck> scriptchecks;
BOOST_CHECK(CheckInputScripts(tx, state, &active_coins_tip, test_flags, true, add_to_cache, txdata, validation_cache, &scriptchecks));
TransactionExecutionDataStore tx_exec_store2;
BOOST_CHECK(CheckInputScripts(tx, state, &active_coins_tip, test_flags, true, add_to_cache, txdata, validation_cache, tx_exec_store2, &scriptchecks));
BOOST_CHECK(scriptchecks.empty());
} else {
// Check that we get script executions to check, if the transaction
// was invalid, or we didn't add to cache.
std::vector<CScriptCheck> scriptchecks;
BOOST_CHECK(CheckInputScripts(tx, state, &active_coins_tip, test_flags, true, add_to_cache, txdata, validation_cache, &scriptchecks));
TransactionExecutionDataStore tx_exec_store2;
BOOST_CHECK(CheckInputScripts(tx, state, &active_coins_tip, test_flags, true, add_to_cache, txdata, validation_cache, tx_exec_store2, &scriptchecks));
BOOST_CHECK_EQUAL(scriptchecks.size(), tx.vin.size());
}
}
@ -215,14 +219,16 @@ BOOST_FIXTURE_TEST_CASE(checkinputs_test, Dersig100Setup)
TxValidationState state;
PrecomputedTransactionData ptd_spend_tx;
TransactionExecutionDataStore tx_exec_store;
BOOST_CHECK(!CheckInputScripts(CTransaction(spend_tx), state, &m_node.chainman->ActiveChainstate().CoinsTip(), SCRIPT_VERIFY_P2SH | SCRIPT_VERIFY_DERSIG, true, true, ptd_spend_tx, m_node.chainman->m_validation_cache, nullptr));
BOOST_CHECK(!CheckInputScripts(CTransaction(spend_tx), state, &m_node.chainman->ActiveChainstate().CoinsTip(), SCRIPT_VERIFY_P2SH | SCRIPT_VERIFY_DERSIG, true, true, ptd_spend_tx, m_node.chainman->m_validation_cache, tx_exec_store, nullptr));
// If we call again asking for scriptchecks (as happens in
// ConnectBlock), we should add a script check object for this -- we're
// not caching invalidity (if that changes, delete this test case).
std::vector<CScriptCheck> scriptchecks;
BOOST_CHECK(CheckInputScripts(CTransaction(spend_tx), state, &m_node.chainman->ActiveChainstate().CoinsTip(), SCRIPT_VERIFY_P2SH | SCRIPT_VERIFY_DERSIG, true, true, ptd_spend_tx, m_node.chainman->m_validation_cache, &scriptchecks));
TransactionExecutionDataStore tx_exec_store2;
BOOST_CHECK(CheckInputScripts(CTransaction(spend_tx), state, &m_node.chainman->ActiveChainstate().CoinsTip(), SCRIPT_VERIFY_P2SH | SCRIPT_VERIFY_DERSIG, true, true, ptd_spend_tx, m_node.chainman->m_validation_cache, tx_exec_store2, &scriptchecks));
BOOST_CHECK_EQUAL(scriptchecks.size(), 1U);
// Test that CheckInputScripts returns true iff DERSIG-enforcing flags are
@ -284,7 +290,8 @@ BOOST_FIXTURE_TEST_CASE(checkinputs_test, Dersig100Setup)
invalid_with_cltv_tx.vin[0].scriptSig = CScript() << vchSig << 100;
TxValidationState state;
PrecomputedTransactionData txdata;
BOOST_CHECK(CheckInputScripts(CTransaction(invalid_with_cltv_tx), state, m_node.chainman->ActiveChainstate().CoinsTip(), SCRIPT_VERIFY_CHECKLOCKTIMEVERIFY, true, true, txdata, m_node.chainman->m_validation_cache, nullptr));
TransactionExecutionDataStore tx_exec_store;
BOOST_CHECK(CheckInputScripts(CTransaction(invalid_with_cltv_tx), state, m_node.chainman->ActiveChainstate().CoinsTip(), SCRIPT_VERIFY_CHECKLOCKTIMEVERIFY, true, true, txdata, m_node.chainman->m_validation_cache, tx_exec_store, nullptr));
}
// TEST CHECKSEQUENCEVERIFY
@ -312,7 +319,8 @@ BOOST_FIXTURE_TEST_CASE(checkinputs_test, Dersig100Setup)
invalid_with_csv_tx.vin[0].scriptSig = CScript() << vchSig << 100;
TxValidationState state;
PrecomputedTransactionData txdata;
BOOST_CHECK(CheckInputScripts(CTransaction(invalid_with_csv_tx), state, &m_node.chainman->ActiveChainstate().CoinsTip(), SCRIPT_VERIFY_CHECKSEQUENCEVERIFY, true, true, txdata, m_node.chainman->m_validation_cache, nullptr));
TransactionExecutionDataStore tx_exec_store;
BOOST_CHECK(CheckInputScripts(CTransaction(invalid_with_csv_tx), state, &m_node.chainman->ActiveChainstate().CoinsTip(), SCRIPT_VERIFY_CHECKSEQUENCEVERIFY, true, true, txdata, m_node.chainman->m_validation_cache, tx_exec_store, nullptr));
}
// TODO: add tests for remaining script flags
@ -373,13 +381,15 @@ BOOST_FIXTURE_TEST_CASE(checkinputs_test, Dersig100Setup)
TxValidationState state;
PrecomputedTransactionData txdata;
TransactionExecutionDataStore tx_exec_store;
// This transaction is now invalid under segwit, because of the second input.
BOOST_CHECK(!CheckInputScripts(CTransaction(tx), state, &m_node.chainman->ActiveChainstate().CoinsTip(), SCRIPT_VERIFY_P2SH | SCRIPT_VERIFY_WITNESS, true, true, txdata, m_node.chainman->m_validation_cache, nullptr));
BOOST_CHECK(!CheckInputScripts(CTransaction(tx), state, &m_node.chainman->ActiveChainstate().CoinsTip(), SCRIPT_VERIFY_P2SH | SCRIPT_VERIFY_WITNESS, true, true, txdata, m_node.chainman->m_validation_cache, tx_exec_store, nullptr));
std::vector<CScriptCheck> scriptchecks;
// Make sure this transaction was not cached (ie because the first
// input was valid)
BOOST_CHECK(CheckInputScripts(CTransaction(tx), state, &m_node.chainman->ActiveChainstate().CoinsTip(), SCRIPT_VERIFY_P2SH | SCRIPT_VERIFY_WITNESS, true, true, txdata, m_node.chainman->m_validation_cache, &scriptchecks));
TransactionExecutionDataStore tx_exec_store2;
BOOST_CHECK(CheckInputScripts(CTransaction(tx), state, &m_node.chainman->ActiveChainstate().CoinsTip(), SCRIPT_VERIFY_P2SH | SCRIPT_VERIFY_WITNESS, true, true, txdata, m_node.chainman->m_validation_cache, tx_exec_store2, &scriptchecks));
// Should get 2 script checks back -- caching is on a whole-transaction basis.
BOOST_CHECK_EQUAL(scriptchecks.size(), 2U);
}

View file

@ -140,6 +140,7 @@ bool CheckInputScripts(const CTransaction& tx, TxValidationState& state,
const CCoinsViewCache& inputs, unsigned int flags, bool cacheSigStore,
bool cacheFullScriptStore, PrecomputedTransactionData& txdata,
ValidationCache& validation_cache,
TransactionExecutionDataStore& tx_exec_store,
std::vector<CScriptCheck>* pvChecks = nullptr)
EXCLUSIVE_LOCKS_REQUIRED(cs_main);
@ -428,8 +429,10 @@ static bool CheckInputsFromMempoolAndCache(const CTransaction& tx, TxValidationS
}
}
TransactionExecutionDataStore tx_exec_store;
// Call CheckInputScripts() to cache signature and script validity against current tip consensus rules.
return CheckInputScripts(tx, state, view, flags, /* cacheSigStore= */ true, /* cacheFullScriptStore= */ true, txdata, validation_cache);
return CheckInputScripts(tx, state, view, flags, /* cacheSigStore= */ true, /* cacheFullScriptStore= */ true, txdata, validation_cache, tx_exec_store);
}
namespace {
@ -1235,13 +1238,16 @@ bool MemPoolAccept::PolicyScriptChecks(const ATMPArgs& args, Workspace& ws)
// Check input scripts and signatures.
// This is done last to help prevent CPU exhaustion denial-of-service attacks.
if (!CheckInputScripts(tx, state, m_view, scriptVerifyFlags, true, false, ws.m_precomputed_txdata, GetValidationCache())) {
TransactionExecutionDataStore tx_exec_store;
if (!CheckInputScripts(tx, state, m_view, scriptVerifyFlags, true, false, ws.m_precomputed_txdata, GetValidationCache(), tx_exec_store)) {
// SCRIPT_VERIFY_CLEANSTACK requires SCRIPT_VERIFY_WITNESS, so we
// need to turn both off, and compare against just turning off CLEANSTACK
// to see if the failure is specifically due to witness validation.
TxValidationState state_dummy; // Want reported failures to be from first CheckInputScripts
if (!tx.HasWitness() && CheckInputScripts(tx, state_dummy, m_view, scriptVerifyFlags & ~(SCRIPT_VERIFY_WITNESS | SCRIPT_VERIFY_CLEANSTACK), true, false, ws.m_precomputed_txdata, GetValidationCache()) &&
!CheckInputScripts(tx, state_dummy, m_view, scriptVerifyFlags & ~SCRIPT_VERIFY_CLEANSTACK, true, false, ws.m_precomputed_txdata, GetValidationCache())) {
TransactionExecutionDataStore tx_exec_store2;
TransactionExecutionDataStore tx_exec_store3;
if (!tx.HasWitness() && CheckInputScripts(tx, state_dummy, m_view, scriptVerifyFlags & ~(SCRIPT_VERIFY_WITNESS | SCRIPT_VERIFY_CLEANSTACK), true, false, ws.m_precomputed_txdata, GetValidationCache(), tx_exec_store2) &&
!CheckInputScripts(tx, state_dummy, m_view, scriptVerifyFlags & ~SCRIPT_VERIFY_CLEANSTACK, true, false, ws.m_precomputed_txdata, GetValidationCache(), tx_exec_store3)) {
// Only the witness is missing, so the transaction itself may be fine.
state.Invalid(TxValidationResult::TX_WITNESS_STRIPPED,
state.GetRejectReason(), state.GetDebugMessage());
@ -2117,7 +2123,7 @@ std::optional<std::pair<ScriptError, std::string>> CScriptCheck::operator()() {
const CScript &scriptSig = ptxTo->vin[nIn].scriptSig;
const CScriptWitness *witness = &ptxTo->vin[nIn].scriptWitness;
ScriptError error{SCRIPT_ERR_UNKNOWN_ERROR};
if (VerifyScript(scriptSig, m_tx_out.scriptPubKey, witness, nFlags, CachingTransactionSignatureChecker(ptxTo, nIn, m_tx_out.nValue, cacheStore, *m_signature_cache, *txdata), &error)) {
if (VerifyScript(scriptSig, m_tx_out.scriptPubKey, witness, nFlags, CachingTransactionSignatureChecker(ptxTo, nIn, m_tx_out.nValue, cacheStore, *m_signature_cache, *txdata), &error, m_tx_execdata)) {
return std::nullopt;
} else {
auto debug_str = strprintf("input %i of %s (wtxid %s), spending %s:%i", nIn, ptxTo->GetHash().ToString(), ptxTo->GetWitnessHash().ToString(), ptxTo->vin[nIn].prevout.hash.ToString(), ptxTo->vin[nIn].prevout.n);
@ -2164,6 +2170,7 @@ bool CheckInputScripts(const CTransaction& tx, TxValidationState& state,
const CCoinsViewCache& inputs, unsigned int flags, bool cacheSigStore,
bool cacheFullScriptStore, PrecomputedTransactionData& txdata,
ValidationCache& validation_cache,
TransactionExecutionDataStore& tx_exec_store,
std::vector<CScriptCheck>* pvChecks)
{
if (tx.IsCoinBase()) return true;
@ -2199,6 +2206,8 @@ bool CheckInputScripts(const CTransaction& tx, TxValidationState& state,
}
assert(txdata.m_spent_outputs.size() == tx.vin.size());
TransactionExecutionData *tx_exec_data = tx_exec_store.getOrCreate(&tx);
for (unsigned int i = 0; i < tx.vin.size(); i++) {
// We very carefully only pass in things to CScriptCheck which
@ -2208,7 +2217,7 @@ bool CheckInputScripts(const CTransaction& tx, TxValidationState& state,
// spent being checked as a part of CScriptCheck.
// Verify signature
CScriptCheck check(txdata.m_spent_outputs[i], tx, validation_cache.m_signature_cache, i, flags, cacheSigStore, &txdata);
CScriptCheck check(txdata.m_spent_outputs[i], tx, validation_cache.m_signature_cache, i, flags, cacheSigStore, &txdata, tx_exec_data);
if (pvChecks) {
pvChecks->emplace_back(std::move(check));
} else if (auto result = check(); result.has_value()) {
@ -2222,7 +2231,7 @@ bool CheckInputScripts(const CTransaction& tx, TxValidationState& state,
// non-upgraded nodes by banning CONSENSUS-failing
// data providers.
CScriptCheck check2(txdata.m_spent_outputs[i], tx, validation_cache.m_signature_cache, i,
flags & ~STANDARD_NOT_MANDATORY_VERIFY_FLAGS, cacheSigStore, &txdata);
flags & ~STANDARD_NOT_MANDATORY_VERIFY_FLAGS, cacheSigStore, &txdata, tx_exec_data);
auto mandatory_result = check2();
if (!mandatory_result.has_value()) {
return state.Invalid(TxValidationResult::TX_NOT_STANDARD, strprintf("non-mandatory-script-verify-flag (%s)", ScriptErrorString(result->first)), result->second);
@ -2630,6 +2639,8 @@ bool Chainstate::ConnectBlock(const CBlock& block, BlockValidationState& state,
CBlockUndo blockundo;
TransactionExecutionDataStore tx_exec_store;
// Precomputed transaction data pointers must not be invalidated
// until after `control` has run the script checks (potentially
// in multiple threads). Preallocate the vector size so a new allocation
@ -2698,7 +2709,7 @@ bool Chainstate::ConnectBlock(const CBlock& block, BlockValidationState& state,
std::vector<CScriptCheck> vChecks;
bool fCacheResults = fJustCheck; /* Don't cache results if we're actually connecting blocks (still consult the cache, though) */
TxValidationState tx_state;
if (fScriptChecks && !CheckInputScripts(tx, tx_state, view, flags, fCacheResults, fCacheResults, txsdata[i], m_chainman.m_validation_cache, parallel_script_checks ? &vChecks : nullptr)) {
if (fScriptChecks && !CheckInputScripts(tx, tx_state, view, flags, fCacheResults, fCacheResults, txsdata[i], m_chainman.m_validation_cache, tx_exec_store, parallel_script_checks ? &vChecks : nullptr)) {
// Any transaction validation failure in ConnectBlock is a block consensus failure
state.Invalid(BlockValidationResult::BLOCK_CONSENSUS,
tx_state.GetRejectReason(), tx_state.GetDebugMessage());

View file

@ -21,6 +21,7 @@
#include <policy/feerate.h>
#include <policy/packages.h>
#include <policy/policy.h>
#include <script/interpreter.h>
#include <script/script_error.h>
#include <script/sigcache.h>
#include <sync.h>
@ -337,10 +338,11 @@ private:
bool cacheStore;
PrecomputedTransactionData *txdata;
SignatureCache* m_signature_cache;
TransactionExecutionData *m_tx_execdata;
public:
CScriptCheck(const CTxOut& outIn, const CTransaction& txToIn, SignatureCache& signature_cache, unsigned int nInIn, unsigned int nFlagsIn, bool cacheIn, PrecomputedTransactionData* txdataIn) :
m_tx_out(outIn), ptxTo(&txToIn), nIn(nInIn), nFlags(nFlagsIn), cacheStore(cacheIn), txdata(txdataIn), m_signature_cache(&signature_cache) { }
CScriptCheck(const CTxOut& outIn, const CTransaction& txToIn, SignatureCache& signature_cache, unsigned int nInIn, unsigned int nFlagsIn, bool cacheIn, PrecomputedTransactionData* txdataIn, TransactionExecutionData* tx_execdata) :
m_tx_out(outIn), ptxTo(&txToIn), nIn(nInIn), nFlags(nFlagsIn), cacheStore(cacheIn), txdata(txdataIn), m_signature_cache(&signature_cache), m_tx_execdata(tx_execdata) { }
CScriptCheck(const CScriptCheck&) = delete;
CScriptCheck& operator=(const CScriptCheck&) = delete;

View file

@ -0,0 +1,731 @@
#!/usr/bin/env python3
# Copyright (c) 2025 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""
Test OP_CHECKCONTRACTVERIFY
"""
from dataclasses import dataclass
import hashlib
from typing import Any, Dict, List, Optional, Tuple, Union
from test_framework import script, key
from test_framework.test_framework import BitcoinTestFramework, TestNode
from test_framework.p2p import P2PInterface
from test_framework.wallet import MiniWallet, MiniWalletMode
from test_framework.script import (
CScript,
OP_CHECKCONTRACTVERIFY,
OP_RETURN,
OP_TRUE,
TaprootInfo,
)
from test_framework.messages import CTransaction, COutPoint, CTxInWitness, CTxOut, CTxIn
from test_framework.util import assert_equal, assert_raises_rpc_error
# Modes for OP_CHECKCONTRACTVERIFY
CCV_MODE_CHECK_INPUT: int = -1
CCV_MODE_CHECK_OUTPUT: int = 0
CCV_MODE_CHECK_OUTPUT_IGNORE_AMOUNT: int = 1
CCV_MODE_CHECK_OUTPUT_DEDUCT_AMOUNT: int = 2
# x-only public key with provably unknown private key defined in BIP-341
NUMS_KEY: bytes = bytes.fromhex(key.H_POINT)
# ======= Utility Classes & Functions =======
TapLeaf = Tuple[str, CScript]
TapTree = Union[TapLeaf, List["TapTree"]]
def flatten_scripts(tree: TapTree) -> List[TapLeaf]:
result = []
if isinstance(tree, list):
assert len(tree) == 2
result.extend(flatten_scripts(tree[0]))
result.extend(flatten_scripts(tree[1]))
else:
assert isinstance(tree, tuple) and len(tree) == 2
result.append(tree)
return result
class P2TR:
"""
A class representing a Pay-to-Taproot script.
"""
def __init__(self, internal_pubkey: bytes, scripts: TapTree):
assert len(internal_pubkey) == 32
self.internal_pubkey = internal_pubkey
self.scripts = scripts
self.tr_info = script.taproot_construct(internal_pubkey, [scripts])
def get_script(self, clause_name: str):
for name, clause_script in flatten_scripts(self.scripts):
if name == clause_name:
return clause_script
raise ValueError(f"Clause {clause_name} not found")
def get_tr_info(self) -> TaprootInfo:
return self.tr_info
def get_tx_out(self, value: int) -> CTxOut:
return CTxOut(
nValue=value,
scriptPubKey=self.get_tr_info().scriptPubKey
)
class AugmentedP2TR:
"""
An abstract class representing a Pay-to-Taproot script with some embedded data.
While the exact script can only be produced once the embedded data is known,
the scripts and the "naked internal key" are decided in advance.
"""
def __init__(self, naked_internal_pubkey: bytes):
assert len(naked_internal_pubkey) == 32
self.naked_internal_pubkey = naked_internal_pubkey
def get_scripts(self) -> TapTree:
raise NotImplementedError("This must be implemented in subclasses")
def get_script(self, clause_name: str):
for name, clause_script in flatten_scripts(self.get_scripts()):
if name == clause_name:
return clause_script
raise ValueError(f"Clause {clause_name} not found")
def get_taptree(self) -> bytes:
# use dummy data, since it doesn't affect the merkle root
return self.get_tr_info(b'').merkle_root
def get_tr_info(self, data: bytes) -> TaprootInfo:
if len(data) == 0:
internal_pubkey = self.naked_internal_pubkey
else:
data_hash = hashlib.sha256(
self.naked_internal_pubkey + data).digest()
internal_pubkey, _ = key.tweak_add_pubkey(
self.naked_internal_pubkey, data_hash)
return script.taproot_construct(internal_pubkey, [self.get_scripts()])
def get_tx_out(self, value: int, data: bytes) -> CTxOut:
return CTxOut(nValue=value, scriptPubKey=self.get_tr_info(data).scriptPubKey)
class PrivkeyPlaceholder:
"""
A placeholder for a witness element that will be replaced with the
corresponding Schnorr signature.
"""
def __init__(self, privkey: key.ECKey):
self.privkey = privkey
@dataclass
class CcvInput:
txid: str
vout_index: int
amount: int
contract: Union[P2TR, AugmentedP2TR]
data: Optional[bytes]
leaf_name: str
# excluding the control block and the script
wit_stack: List[Union[bytes, PrivkeyPlaceholder]]
nSequence: int = 0
def create_tx(
inputs: List[CcvInput],
outputs: List[CTxOut],
*,
version: int = 2,
nLockTime: int = 0
) -> CTransaction:
"""
Creates a transaction with the given inputs and outputs.
Inputs are spending UTXOs that might or might not have data
embedded with OP_CHECKCONTRACTVERIFY.
The witness is constructed by replacing each PrivkeyPlaceholder with a signature
constructed with its private key (using SIGHASH_DEFAULT), and then appending the
control block and the leaf script, per BIP-341.
"""
tx = CTransaction()
tx.version = version
tx.nLockTime = nLockTime
tx.vin = []
tx.wit.vtxinwit = []
in_txouts = []
for inp in inputs:
txin = CTxIn(COutPoint(int(inp.txid, 16), inp.vout_index),
nSequence=inp.nSequence)
tx.vin.append(txin)
# Retrieve leaf script & control block
if isinstance(inp.contract, AugmentedP2TR):
assert inp.data is not None
tr_info = inp.contract.get_tr_info(inp.data)
else:
assert isinstance(inp.contract, P2TR) and inp.data is None
tr_info = inp.contract.get_tr_info()
in_txouts.append(
CTxOut(nValue=inp.amount, scriptPubKey=tr_info.scriptPubKey))
leaf_script = tr_info.leaves[inp.leaf_name].script
control_block = tr_info.controlblock_for_script_spend(inp.leaf_name)
wit_stack = inp.wit_stack.copy()
wit_stack.extend([leaf_script, control_block])
wit = CTxInWitness()
wit.scriptWitness.stack = wit_stack
tx.wit.vtxinwit.append(wit)
tx.vout = outputs
# For each input, if any witness stack element is a PrivkeyPlaceholder, replace with the
# actual signature. This assumes signing with SIGHASH_DEFAULT.
for i in range(len(tx.vin)):
for j in range(len(tx.wit.vtxinwit[i].scriptWitness.stack)):
if isinstance(tx.wit.vtxinwit[i].scriptWitness.stack[j], PrivkeyPlaceholder):
contract = inputs[i].contract
clause = inputs[i].leaf_name
privkey: key.ECKey = tx.wit.vtxinwit[i].scriptWitness.stack[j].privkey
sigmsg = script.TaprootSignatureHash(
tx, in_txouts, input_index=i, hash_type=0,
scriptpath=True, leaf_script=contract.get_script(clause),
)
sig = key.sign_schnorr(privkey.get_bytes(), sigmsg)
tx.wit.vtxinwit[i].scriptWitness.stack[j] = sig
return tx
# ======= Contract definitions =======
class EmbedData(P2TR):
"""
An output that can only be spent to a `CompareWithEmbeddedData` output, with
its embedded data passed as the witness.
"""
def __init__(self, ignore_amount: bool = False):
super().__init__(
NUMS_KEY,
(
"forced",
CScript([
# witness: <data>
0, # index
0, # use NUMS as the naked pubkey
CompareWithEmbeddedData().get_taptree(), # output Merkle tree
CCV_MODE_CHECK_OUTPUT_IGNORE_AMOUNT if ignore_amount else 0, # mode
OP_CHECKCONTRACTVERIFY,
OP_TRUE
])
)
)
class CompareWithEmbeddedData(AugmentedP2TR):
"""
An output that can only be spent by passing the embedded data in the witness.
"""
def __init__(self):
super().__init__(NUMS_KEY)
def get_scripts(self) -> TapTree:
return (
"check_data",
CScript([
# witness: <data>
-1, # index: check current input
0, # use NUMS as the naked pubkey
-1, # use taptree of the current input
CCV_MODE_CHECK_INPUT, # check input
OP_CHECKCONTRACTVERIFY,
OP_TRUE
])
)
class SendToSelf(P2TR):
"""
A utxo that can only be spent by sending the entire amount to the same script.
The output index must match the input index.
"""
def __init__(self):
super().__init__(
NUMS_KEY,
("send_to_self", CScript([
# witness: <>
0, # no data tweaking
-1, # index: check current output
-1, # use internal key of the current input
-1, # use taptree of the current input
CCV_MODE_CHECK_OUTPUT, # all the amount must go to this output
OP_CHECKCONTRACTVERIFY,
OP_TRUE
]))
)
class SplitFunds(P2TR):
"""
An output that can only be spent by sending part of the fund to itself,
and all the remaining funds to the P2TR address with NUMS pubkey.
"""
def __init__(self):
super().__init__(
NUMS_KEY,
(
"split", CScript([
# witness: <>
0, # no data tweaking
0, # index
-1, # use internal key of the current input
-1, # use taptree of the current input
# check output, deduct amount from input
CCV_MODE_CHECK_OUTPUT_DEDUCT_AMOUNT,
OP_CHECKCONTRACTVERIFY,
0, # no data tweaking
1, # index
0, # NUMS pubkey
0, # no taptweak
# check output, all remaining amount must go to this output
CCV_MODE_CHECK_OUTPUT,
OP_CHECKCONTRACTVERIFY,
OP_TRUE
])
)
)
def ccv(mode=0, index=0, data=0, pk=0, tree=0):
return CScript([
data,
index,
pk,
tree,
mode,
OP_CHECKCONTRACTVERIFY,
])
class TestCCVSuccess(P2TR):
"""
A utxo with hardcoded parameters to test the OP_SUCCESS behavior for the mode.
"""
def __init__(self, **kwargs):
super().__init__(NUMS_KEY, [
("spend", CScript([
*ccv(**kwargs),
OP_RETURN # make sure that the script execution fails if it reaches here
]))
])
class TestCCVFail(P2TR):
"""
A utxo with hardcoded parameters to test the failure cases for invalid parameters.
"""
def __init__(self, **kwargs):
super().__init__(NUMS_KEY, [
("spend", CScript([
*ccv(**kwargs),
OP_TRUE # make sure that the script execution succeeds if it reaches here
]))
])
# ======= Tests =======
class CheckContractVerifyTest(BitcoinTestFramework):
def set_test_params(self):
self.num_nodes = 1
self.extra_args = [
[
# Use only one script thread to get the exact reject reason for testing
"-par=1",
# TODO: figure out changes to standardness rules
"-acceptnonstdtxn=1",
# TODO: remove when package relay submission becomes a thing.
"-minrelaytxfee=0",
"-blockmintxfee=0",
]
]
self.setup_clean_chain = True
def run_test(self):
wallet = MiniWallet(self.nodes[0], mode=MiniWalletMode.RAW_OP_TRUE)
node = self.nodes[0]
node.add_p2p_connection(P2PInterface())
# Generate some matured UTXOs to spend into vaults.
self.generate(wallet, 200)
self.test_ccv(node, wallet, data=b'\x42'*32, ignore_amount=False)
self.test_ccv(node, wallet, data=b'', ignore_amount=True)
self.test_ccv(node, wallet, data=b'\x42'*32, ignore_amount=True)
self.test_many_to_one(node, wallet)
self.test_send_to_self(node, wallet)
self.test_deduct_amount(node, wallet)
self.test_undefined_modes_opsuccess(node, wallet)
self.test_invalid_parameters(node, wallet)
def test_ccv(
self,
node: TestNode,
wallet: MiniWallet,
data,
ignore_amount: bool
):
assert_equal(node.getmempoolinfo()["size"], 0)
# tx1 tx2
# MiniWallet ==> S ==(data)==> T ==> OP_TRUE
# S: tr(NUMS, CheckOutputContract(T, data))
# T: tr(NUMS×data, CheckInputContract(data == 0x424242...))
T = CompareWithEmbeddedData()
S = EmbedData(ignore_amount=ignore_amount)
# Create UTXO for S
# If ignoring amount, the contract value (if any) can be used to pay for fees
# otherwise, we put 0 fees for the sake of the tests.
# In practice, package relay would likely be used to manage fees, when ready.
amount_sats = 100000
fees = 1000 if ignore_amount else 0
res = wallet.send_to(
from_node=node,
scriptPubKey=S.get_tr_info().scriptPubKey,
amount=amount_sats
)
tx1_txid = res['txid']
tx1_n = res['sent_vout']
# Create UTXO for T
tx2 = create_tx(
inputs=[CcvInput(tx1_txid, tx1_n, amount_sats,
S, None, "forced", [data])],
outputs=[T.get_tx_out(amount_sats - fees, data)]
)
if not ignore_amount:
# broadcast with insufficient output amount; this should fail
tx2.vout[0].nValue -= 1
self.assert_broadcast_tx(
tx2, err_msg='Incorrect amount for OP_CHECKCONTRACTVERIFY')
tx2.vout[0].nValue += 1
tx2_txid = self.assert_broadcast_tx(tx2, mine_all=True)
# create an output for the final spend
dest_ctr = P2TR(NUMS_KEY, [("true", CScript([OP_TRUE]))])
# try to spend with the wrong data
tx3_wrongdata = create_tx(
inputs=[CcvInput(tx2_txid, 0, amount_sats - fees, T,
data, "check_data", [b'\x43'*32])],
outputs=[dest_ctr.get_tx_out(amount_sats - 2 * fees)]
)
self.assert_broadcast_tx(
tx3_wrongdata, err_msg="Mismatching contract data or program")
# Broadcasting with correct data succeeds
tx3 = create_tx(
inputs=[CcvInput(tx2_txid, 0, amount_sats - fees,
T, data, "check_data", [data])],
outputs=[dest_ctr.get_tx_out(amount_sats - 2 * fees)]
)
self.assert_broadcast_tx(tx3, mine_all=True)
def test_many_to_one(
self,
node: TestNode,
wallet: MiniWallet
):
assert_equal(node.getmempoolinfo()["size"], 0)
# Creates 3 utxos with different amounts and all with the same EmbedData script.
# Spending them together to a single output, the total amount must be preserved.
# tx1,tx2,tx3 tx4
# MiniWallet ==> S1, S2, S3 ==> T ==> OP_TRUE
# S1: tr(NUMS, CheckOutputContract(T, data))
# S2: tr(NUMS, CheckOutputContract(T, data))
# S3: tr(NUMS, CheckOutputContract(T, data))
# T: tr(NUMS×data, CheckInputContract(data == 0x424242...))
data = b'\x42'*32
amounts_sats: List[int] = []
T = CompareWithEmbeddedData()
S = EmbedData()
tx_ids_and_n: List[Tuple[str, int]] = []
inputs = []
for i in range(3):
# Create UTXO for S[i]
amount_sats = 100000 * (i + 1)
amounts_sats.append(amount_sats)
res = wallet.send_to(
from_node=node,
scriptPubKey=S.get_tr_info().scriptPubKey,
amount=amount_sats
)
tx_ids_and_n.append((res['txid'], res['sent_vout']))
inputs.append(CcvInput(
res['txid'], res['sent_vout'], amount_sats, S, None, "forced", [data]
))
# Create UTXO for T
outputs = [T.get_tx_out(sum(amounts_sats), data)]
tx4 = create_tx(inputs, outputs)
# broadcast with insufficient output amount; this should fail
tx4.vout[0].nValue -= 1
self.assert_broadcast_tx(
tx4, err_msg='Incorrect amount for OP_CHECKCONTRACTVERIFY')
tx4.vout[0].nValue += 1
# correct amount succeeds
self.assert_broadcast_tx(tx4, mine_all=True)
def test_send_to_self(
self,
node: TestNode,
wallet: MiniWallet
):
assert_equal(node.getmempoolinfo()["size"], 0)
# Creates a utxo with the SendToSelf contract, and verifies that:
# - sending to a different scriptPubKey fails;
# - sending to an output with the same scriptPubKey works.
amount_sats = 10000
C = SendToSelf()
res = wallet.send_to(
from_node=node,
scriptPubKey=C.get_tr_info().scriptPubKey,
amount=amount_sats
)
tx_id, n = (res['txid'], res['sent_vout'])
# Create UTXO for C
tx2 = create_tx(
inputs=[CcvInput(tx_id, n, amount_sats, C,
None, "send_to_self", [])],
outputs=[C.get_tx_out(amount_sats)]
)
# broadcast with insufficient output amount; this should fail
tx2.vout[0].nValue -= 1
self.assert_broadcast_tx(
tx2, err_msg='Incorrect amount for OP_CHECKCONTRACTVERIFY')
tx2.vout[0].nValue += 1
# broadcast with incorrect output script; this should fail
correct_script = tx2.vout[0].scriptPubKey
tx2.vout[0].scriptPubKey = correct_script[:-1] + \
bytes([correct_script[-1] ^ 1])
self.assert_broadcast_tx(
tx2, err_msg="Mismatching contract data or program")
tx2.vout[0].scriptPubKey = correct_script
# correct amount succeeds
self.assert_broadcast_tx(tx2, mine_all=True)
def test_deduct_amount(
self,
node: TestNode,
wallet: MiniWallet
):
assert_equal(node.getmempoolinfo()["size"], 0)
# Tests the behavior of the CCV_MODE_CHECK_OUTPUT_DEDUCT_AMOUNT mode
# tx1 tx2
# MiniWallet ==> SplitFunds() ==> SplitFunds(), P2TR(NUMS)
S = SplitFunds()
amount_sats = 10000
recover_amount_sats = 3000 # the amount that goes back to the same script
res = wallet.send_to(
from_node=node,
scriptPubKey=S.get_tr_info().scriptPubKey,
amount=amount_sats
)
tx1_txid, tx1_n = (res['txid'], res['sent_vout'])
# Create UTXO for S
tx2 = create_tx(
inputs=[CcvInput(tx1_txid, tx1_n, amount_sats,
S, None, "split", [])],
outputs=[
S.get_tx_out(recover_amount_sats),
CTxOut(
nValue=amount_sats - recover_amount_sats - 1, # 1 sat short, this must fail
scriptPubKey=b'\x51\x20' + NUMS_KEY
)
]
)
self.assert_broadcast_tx(
tx2, err_msg='Incorrect amount for OP_CHECKCONTRACTVERIFY')
tx2.vout[1].nValue += 1 # correct amount
self.assert_broadcast_tx(tx2, mine_all=True)
def test_undefined_modes_opsuccess(
self,
node: TestNode,
wallet: MiniWallet
):
assert_equal(node.getmempoolinfo()["size"], 0)
# Tests that undefined modes immediately terminate the script successfully.
testcases = [
{"mode": -2},
{"mode": 3},
]
amount_sats = 10000
fees = 1000
for testcase in testcases:
C = TestCCVSuccess(**testcase)
res = wallet.send_to(
from_node=node,
scriptPubKey=C.get_tr_info().scriptPubKey,
amount=amount_sats
)
tx_id, n = (res['txid'], res['sent_vout'])
# Create UTXO for C
tx2 = create_tx(
inputs=[CcvInput(tx_id, n, amount_sats, C, None, "spend", [])],
outputs=[CTxOut(
nValue=amount_sats - fees,
scriptPubKey=P2TR(
NUMS_KEY, [("true", CScript([OP_TRUE]))]).tr_info.scriptPubKey
)]
)
# it should succeed
self.assert_broadcast_tx(tx2, mine_all=True)
def test_invalid_parameters(
self,
node: TestNode,
wallet: MiniWallet
):
assert_equal(node.getmempoolinfo()["size"], 0)
# Tests that invalid parameters, that cause the Script validation to fail
testcases: List[Dict[str, Any]] = [
{"pk": -2},
{"pk": 1},
{"pk": b'\x42'*31},
{"pk": b'\x42'*33},
{"pk": b'\x42'*64},
{"tree": -2},
{"tree": 1},
{"tree": b'\x42'*31},
{"tree": b'\x42'*33},
]
amount_sats = 10000
fees = 1000
for testcase in testcases:
C = TestCCVFail(**testcase)
res = wallet.send_to(
from_node=node,
scriptPubKey=C.get_tr_info().scriptPubKey,
amount=amount_sats
)
tx_id, n = (res['txid'], res['sent_vout'])
# Create UTXO for C
tx2 = create_tx(
inputs=[CcvInput(tx_id, n, amount_sats, C, None, "spend", [])],
outputs=[CTxOut(
nValue=amount_sats - fees,
scriptPubKey=P2TR(
NUMS_KEY, [("true", CScript([OP_TRUE]))]).tr_info.scriptPubKey
)]
)
# it should fail
self.assert_broadcast_tx(
tx2, err_msg='Invalid arguments for OP_CHECKCONTRACTVERIFY')
# taken from OP_VAULT PR's functional test
def assert_broadcast_tx(
self,
tx: CTransaction,
mine_all: bool = False,
err_msg: Optional[str] = None
) -> str:
"""
Broadcast a transaction and facilitate various assertions about how the
broadcast went.
"""
node = self.nodes[0]
txhex = tx.serialize().hex()
txid = tx.rehash()
if not err_msg:
assert_equal(node.sendrawtransaction(txhex), txid)
else:
assert_raises_rpc_error(-26, err_msg,
node.sendrawtransaction, txhex)
if mine_all:
self.generate(node, 1)
assert_equal(node.getmempoolinfo()["size"], 0)
return txid
if __name__ == "__main__":
CheckContractVerifyTest(__file__).main()

View file

@ -0,0 +1,480 @@
#!/usr/bin/env python3
# Copyright (c) 2025 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://opensource.org/licenses/mit-license.php.
"""
Tests a vault construction based on OP_CHECKCONTRACTVERIFY.
The vaults are functionally very similar to the ones defined in BIP-345, except that
the withdrawal transaction must send the entire amount to a single P2TR output.
"""
from typing import Optional
from test_framework import key, script
from test_framework.test_framework import BitcoinTestFramework, TestNode
from test_framework.p2p import P2PInterface
from test_framework.wallet import MiniWallet, MiniWalletMode
from test_framework.script import (
CScript,
OP_1, OP_CHECKCONTRACTVERIFY, OP_CHECKSEQUENCEVERIFY, OP_CHECKSIG, OP_DROP,
OP_DUP, OP_PICK, OP_SWAP, OP_TRUE
)
from test_framework.messages import CTransaction, CTxOut
from test_framework.util import assert_equal, assert_raises_rpc_error
from feature_checkcontractverify import (
P2TR, AugmentedP2TR, CCV_MODE_CHECK_INPUT, CCV_MODE_CHECK_OUTPUT, CCV_MODE_CHECK_OUTPUT_DEDUCT_AMOUNT,
CcvInput, PrivkeyPlaceholder, TapTree, create_tx, NUMS_KEY
)
unvault_privkey = key.ECKey()
unvault_privkey.set(
b'y\x88\xe19\x11_\xf6\x19L#\xb7t\x9c\xce\x1c\x08\xf6\xdf\x1a\x01W\xc8\xc1\x07\x8d\xb9\x14\xf1\x91\x89c\x8b', True)
unvault_pubkey_xonly = unvault_privkey.get_pubkey().get_bytes()[1:]
recover_privkey = key.ECKey()
recover_privkey.set(
b'\xdcg\xd3\x9f\xc5\x0c/\x82\x06\x01\\T\x8f\xd2\xb6\x90\xddJ\xe5:\xbc\xddJ\rV\x1c\xe2\x07\xb0\xdd7\x89', True)
recover_pubkey_xonly = recover_privkey.get_pubkey().get_bytes()[1:]
class Vault(P2TR):
"""
A UTXO that can be spent either:
- with the "recover" clause, sending it to a PT2R output that has recover_pk as the taproot key
- with the "trigger" clause, sending the entire amount to an Unvaulting output, after providing a 'withdrawal_pk'
- with the "trigger_and_revault" clause, sending part of the amount to an output with the same script as this Vault, and the rest
to an Unvaulting output, after providing a 'withdrawal_pk'
- with the alternate_pk using the keypath spend (if provided; the key is NUMS_KEY otherwise)
"""
def __init__(self, alternate_pk: Optional[bytes], spend_delay: int, recover_pk: bytes, unvault_pk: bytes, *, has_partial_revault=True, has_early_recover=True):
assert (alternate_pk is None or len(alternate_pk) == 32) and len(
recover_pk) == 32 and len(unvault_pk) == 32
self.alternate_pk = alternate_pk
self.spend_delay = spend_delay
self.recover_pk = recover_pk
self.unvault_pk = unvault_pk
unvaulting = Unvaulting(alternate_pk, spend_delay, recover_pk)
self.has_partial_revault = has_partial_revault
self.has_early_recover = has_early_recover
# witness: <sig> <withdrawal_pk> <out_i>
trigger = ("trigger",
CScript([
# data and index already on the stack
0 if alternate_pk is None else alternate_pk, # pk
unvaulting.get_taptree(), # taptree
CCV_MODE_CHECK_OUTPUT,
OP_CHECKCONTRACTVERIFY,
unvault_pk,
OP_CHECKSIG
])
)
# witness: <sig> <withdrawal_pk> <trigger_out_i> <revault_out_i>
trigger_and_revault = (
"trigger_and_revault",
CScript([
0, OP_SWAP, # no data tweak
-1, # current input's internal key
-1, # current input's taptweak
CCV_MODE_CHECK_OUTPUT_DEDUCT_AMOUNT, # revault output
OP_CHECKCONTRACTVERIFY,
# data and index already on the stack
0 if alternate_pk is None else alternate_pk, # pk
unvaulting.get_taptree(), # taptree
CCV_MODE_CHECK_OUTPUT,
OP_CHECKCONTRACTVERIFY,
unvault_pk,
OP_CHECKSIG
])
)
# witness: <out_i>
recover = (
"recover",
CScript([
0, # data
OP_SWAP, # <out_i> (from witness)
recover_pk, # pk
0, # taptree
CCV_MODE_CHECK_OUTPUT,
OP_CHECKCONTRACTVERIFY,
OP_TRUE
])
)
super().__init__(NUMS_KEY if alternate_pk is None else alternate_pk, [
trigger, [trigger_and_revault, recover]])
class Unvaulting(AugmentedP2TR):
"""
A UTXO that can be spent either:
- with the "recover" clause, sending it to a PT2R output that has recover_pk as the taproot key
- with the "withdraw" clause, after a relative timelock of spend_delay blocks, sending the entire amount to a P2TR output that has
the taproot key 'withdrawal_pk'
- with the alternate_pk using the keypath spend (if provided; the key is NUMS_KEY otherwise)
"""
def __init__(self, alternate_pk: Optional[bytes], spend_delay: int, recover_pk: bytes):
assert (alternate_pk is None or len(alternate_pk)
== 32) and len(recover_pk) == 32
self.alternate_pk = alternate_pk
self.spend_delay = spend_delay
self.recover_pk = recover_pk
super().__init__(NUMS_KEY if alternate_pk is None else alternate_pk)
def get_scripts(self) -> TapTree:
# witness: <withdrawal_pk>
withdrawal = (
"withdraw",
CScript([
OP_DUP,
-1,
0 if self.alternate_pk is None else self.alternate_pk,
-1,
CCV_MODE_CHECK_INPUT,
OP_CHECKCONTRACTVERIFY,
# Check timelock
self.spend_delay, OP_CHECKSEQUENCEVERIFY, OP_DROP,
# Check that the transaction output is as expected
0, # no data
0, # output index
2, OP_PICK, # withdrawal_pk
0, # no taptweak
CCV_MODE_CHECK_OUTPUT,
OP_CHECKCONTRACTVERIFY,
# withdrawal_pk is left on the stack on success
])
)
# witness: <out_i>
recover = (
"recover",
CScript([
0, # data
OP_SWAP, # <out_i> (from witness)
self.recover_pk, # pk
0, # taptree
CCV_MODE_CHECK_OUTPUT,
OP_CHECKCONTRACTVERIFY,
OP_TRUE
])
)
return [withdrawal, recover]
# We reuse these specs for all the tests
vault_contract = Vault(
alternate_pk=None,
spend_delay=10,
recover_pk=recover_pubkey_xonly,
unvault_pk=unvault_pubkey_xonly
)
unvault_contract = Unvaulting(
alternate_pk=None,
spend_delay=10,
recover_pk=recover_pubkey_xonly
)
class CheckContractVerifyVaultTest(BitcoinTestFramework):
def set_test_params(self):
self.num_nodes = 1
self.extra_args = [
[
# Use only one script thread to get the exact reject reason for testing
"-par=1",
# TODO: figure out changes to standardness rules
"-acceptnonstdtxn=1",
# TODO: remove when package relay submission becomes a thing.
"-minrelaytxfee=0",
"-blockmintxfee=0",
]
]
self.setup_clean_chain = True
def run_test(self):
wallet = MiniWallet(self.nodes[0], mode=MiniWalletMode.RAW_OP_TRUE)
node = self.nodes[0]
node.add_p2p_connection(P2PInterface())
# Generate some matured UTXOs to spend into vaults.
self.generate(wallet, 200)
# Run the original end-to-end Vault flow.
self.test_vault_e2e(node, wallet)
# Test spending two Vault outputs in one transaction.
self.test_trigger_and_revault(node, wallet)
# Test spending the Unvaulting output using the 'recover' clause.
self.test_unvault_recover(node, wallet)
def test_vault_e2e(
self,
node: TestNode,
wallet: MiniWallet,
):
"""
Demonstrates a simple Vault flow:
1) Create a Vault output
2) Trigger into Unvault output
3) Attempt early withdrawal (fail)
4) Wait spend_delay blocks
5) Attempt withdrawal to a wrong pubkey (fail)
6) Complete the withdrawal
"""
######################################
# Create the Vault UTXO
######################################
vault_amount = 50_000
vault_txout = CTxOut(
vault_amount, vault_contract.get_tr_info().scriptPubKey)
res = wallet.send_to(
from_node=node,
scriptPubKey=vault_txout.scriptPubKey,
amount=vault_txout.nValue
)
vault_txid, vault_vout = res["txid"], res["sent_vout"]
self.generate(node, 1)
######################################
# Step 2: Trigger → Unvault
######################################
withdrawal_pk = b'\x01' * 32
tx_trigger = create_tx(
inputs=[CcvInput(
vault_txid, vault_vout, vault_amount,
vault_contract,
None,
"trigger",
# [unvault_signature, unvault_pk, out_i]
[PrivkeyPlaceholder(unvault_privkey),
withdrawal_pk, script.bn2vch(0)]
)],
outputs=[
unvault_contract.get_tx_out(vault_amount, withdrawal_pk),
],
)
trigger_txid = self.assert_broadcast_tx(tx_trigger, mine_all=True)
######################################
# Step 3: Attempt early withdrawal (fail)
######################################
withdraw_amount = vault_amount
withdrawal_inputs = [CcvInput(
trigger_txid, 0, withdraw_amount,
unvault_contract,
withdrawal_pk,
"withdraw",
[withdrawal_pk],
nSequence=vault_contract.spend_delay
)]
tx_withdraw = create_tx(
inputs=withdrawal_inputs,
outputs=[
CTxOut(withdraw_amount, CScript([OP_1, withdrawal_pk]))
],
)
# This should fail, as the timelock is not satisfied yet
self.assert_broadcast_tx(tx_withdraw, err_msg="non-BIP68-final")
######################################
# Step 4: Wait spend_delay blocks
######################################
self.generate(node, vault_contract.spend_delay)
######################################
# Step 5: Attempt withdrawal to a wrong pubkey
######################################
tx_withdraw_wrong = create_tx(
inputs=withdrawal_inputs,
outputs=[
CTxOut(withdraw_amount, CScript([OP_1, b'\x02' * 32]))
],
)
self.assert_broadcast_tx(
tx_withdraw_wrong, err_msg="Mismatching contract data or program")
######################################
# Step 6: Complete the withdrawal
######################################
self.assert_broadcast_tx(tx_withdraw, mine_all=True)
def test_trigger_and_revault(self, node: TestNode, wallet: MiniWallet):
"""
Test creating two different Vault outputs and spending them together into one Unvaulting output.
One input uses the 'trigger' clause and the other uses the 'trigger_and_revault' clause.
"""
withdrawal_pk = b'\x01' * 32
# Create two Vault outputs with different amounts.
vault_amount1 = 40_000
vault_amount2 = 50_000
withdrawal_amount = 60_000
revault_amount = vault_amount1 + vault_amount2 - withdrawal_amount
res1 = wallet.send_to(
from_node=node,
scriptPubKey=vault_contract.get_tr_info().scriptPubKey,
amount=vault_amount1
)
vault1_txid, vault1_vout = res1["txid"], res1["sent_vout"]
res2 = wallet.send_to(
from_node=node,
scriptPubKey=vault_contract.get_tr_info().scriptPubKey,
amount=vault_amount2
)
vault2_txid, vault2_vout = res2["txid"], res2["sent_vout"]
self.generate(node, 1) # confirm both vault outputs
# Create a transaction that spends both vault outputs into a single Unvaulting output.
# For the first input, use the 'trigger' clause.
# For the second input, use the 'trigger_and_revault' clause.
tx_trigger = create_tx(
inputs=[
CcvInput(
vault1_txid, vault1_vout, vault_amount1,
vault_contract,
None,
"trigger",
[PrivkeyPlaceholder(unvault_privkey),
withdrawal_pk, script.bn2vch(0)]
),
CcvInput(
vault2_txid, vault2_vout, vault_amount2,
vault_contract,
None,
"trigger_and_revault",
[PrivkeyPlaceholder(unvault_privkey),
withdrawal_pk, script.bn2vch(0), script.bn2vch(1)]
)
],
outputs=[
unvault_contract.get_tx_out(withdrawal_amount, withdrawal_pk),
vault_contract.get_tx_out(revault_amount)
],
)
self.assert_broadcast_tx(tx_trigger, mine_all=True)
def test_unvault_recover(self, node: TestNode, wallet: MiniWallet):
"""
Test spending a Vault output to create an Unvaulting output and then
spending the Unvaulting output using the 'recover' clause.
"""
withdrawal_pk = b'\x01' * 32
vault_amount = 50_000
# Create the Vault output.
vault_txout = CTxOut(
vault_amount, vault_contract.get_tr_info().scriptPubKey)
res = wallet.send_to(
from_node=node,
scriptPubKey=vault_txout.scriptPubKey,
amount=vault_txout.nValue
)
vault_txid, vault_vout = res["txid"], res["sent_vout"]
self.generate(node, 1)
# Spend the Vault output using the 'trigger' clause to produce an Unvaulting output.
tx_trigger = create_tx(
inputs=[CcvInput(
vault_txid, vault_vout, vault_amount,
vault_contract,
None,
"trigger",
[PrivkeyPlaceholder(unvault_privkey),
withdrawal_pk, script.bn2vch(0)]
)],
outputs=[
unvault_contract.get_tx_out(vault_amount, withdrawal_pk)
],
)
unvault_txid = self.assert_broadcast_tx(tx_trigger, mine_all=True)
inputs = [CcvInput(
unvault_txid, 0, vault_amount,
unvault_contract,
withdrawal_pk,
"recover",
[script.bn2vch(0)]
)]
# Recovering to the wrong pubkey should fail.
tx_recover_wrong = create_tx(
inputs=inputs,
outputs=[
CTxOut(vault_amount, CScript([OP_1, NUMS_KEY]))
],
)
self.assert_broadcast_tx(
tx_recover_wrong, err_msg="Mismatching contract data or program")
# Now correctly spend the Unvaulting output using the 'recover' clause.
tx_recover = create_tx(
inputs=inputs,
outputs=[
CTxOut(vault_amount, CScript([OP_1, recover_pubkey_xonly]))
],
)
self.assert_broadcast_tx(tx_recover, mine_all=True)
# taken from OP_VAULT PR's functional test
def assert_broadcast_tx(
self,
tx: CTransaction,
mine_all: bool = False,
err_msg: Optional[str] = None
) -> str:
"""
Broadcast a transaction and facilitate various assertions about how the
broadcast went.
"""
node = self.nodes[0]
txhex = tx.serialize().hex()
txid = tx.rehash()
if not err_msg:
assert_equal(node.sendrawtransaction(txhex), txid)
else:
assert_raises_rpc_error(-26, err_msg,
node.sendrawtransaction, txhex)
if mine_all:
self.generate(node, 1)
assert_equal(node.getmempoolinfo()["size"], 0)
return txid
if __name__ == "__main__":
CheckContractVerifyVaultTest(__file__).main()

View file

@ -249,6 +249,8 @@ OP_NOP10 = CScriptOp(0xb9)
# BIP 342 opcodes (Tapscript)
OP_CHECKSIGADD = CScriptOp(0xba)
OP_CHECKCONTRACTVERIFY = CScriptOp(0xbb)
OP_INVALIDOPCODE = CScriptOp(0xff)
OPCODE_NAMES.update({
@ -364,6 +366,7 @@ OPCODE_NAMES.update({
OP_NOP9: 'OP_NOP9',
OP_NOP10: 'OP_NOP10',
OP_CHECKSIGADD: 'OP_CHECKSIGADD',
OP_CHECKCONTRACTVERIFY: 'OP_CHECKCONTRACTVERIFY',
OP_INVALIDOPCODE: 'OP_INVALIDOPCODE',
})
@ -901,7 +904,17 @@ def taproot_tree_helper(scripts):
# - tweak: the tweak (32 bytes)
# - leaves: a dict of name -> TaprootLeafInfo objects for all known leaves
# - merkle_root: the script tree's Merkle root, or bytes() if no leaves are present
TaprootInfo = namedtuple("TaprootInfo", "scriptPubKey,internal_pubkey,negflag,tweak,leaves,merkle_root,output_pubkey")
class TaprootInfo(namedtuple("TaprootInfo", "scriptPubKey,internal_pubkey,negflag,tweak,leaves,merkle_root,output_pubkey")):
def __hash__(self):
return hash(str(self))
def controlblock_for_script_spend(self, script_name: str) -> bytes:
leaf = self.leaves[script_name]
return (
bytes([leaf.version + self.negflag]) +
self.internal_pubkey +
leaf.merklebranch
)
# A TaprootLeafInfo object has the following fields:
# - script: the leaf script (CScript or bytes)
@ -936,4 +949,4 @@ def taproot_construct(pubkey, scripts=None, treat_internal_as_infinity=False):
return TaprootInfo(CScript([OP_1, tweaked]), pubkey, negated + 0, tweak, leaves, h, tweaked)
def is_op_success(o):
return o == 0x50 or o == 0x62 or o == 0x89 or o == 0x8a or o == 0x8d or o == 0x8e or (o >= 0x7e and o <= 0x81) or (o >= 0x83 and o <= 0x86) or (o >= 0x95 and o <= 0x99) or (o >= 0xbb and o <= 0xfe)
return o == 0x50 or o == 0x62 or o == 0x89 or o == 0x8a or o == 0x8d or o == 0x8e or (o >= 0x7e and o <= 0x81) or (o >= 0x83 and o <= 0x86) or (o >= 0x95 and o <= 0x99) or (o >= 0xbc and o <= 0xfe)

View file

@ -344,6 +344,8 @@ BASE_SCRIPTS = [
'wallet_timelock.py',
'p2p_permissions.py',
'feature_blocksdir.py',
'feature_checkcontractverify.py',
'feature_checkcontractverify_vaults.py',
'wallet_startup.py',
'feature_remove_pruned_files_on_startup.py',
'p2p_i2p_ports.py',