mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-04-29 06:49:38 -04:00
Compare commits
8 commits
c49666bfb4
...
e092c32873
Author | SHA1 | Date | |
---|---|---|---|
|
e092c32873 | ||
|
c5e44a0435 | ||
|
32d55e28af | ||
|
1fca268986 | ||
|
c1c2fe5663 | ||
|
77c8c9ebb8 | ||
|
2c1deefedc | ||
|
66153cca78 |
22 changed files with 1652 additions and 45 deletions
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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{};
|
||||
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -209,6 +209,8 @@ enum opcodetype
|
|||
// Opcode added by BIP 342 (Tapscript)
|
||||
OP_CHECKSIGADD = 0xba,
|
||||
|
||||
OP_CHECKCONTRACTVERIFY = 0xbb,
|
||||
|
||||
OP_INVALIDOPCODE = 0xff,
|
||||
};
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
|
|
731
test/functional/feature_checkcontractverify.py
Executable file
731
test/functional/feature_checkcontractverify.py
Executable 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()
|
480
test/functional/feature_checkcontractverify_vaults.py
Executable file
480
test/functional/feature_checkcontractverify_vaults.py
Executable 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()
|
|
@ -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)
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -87,7 +87,7 @@ class BackwardsCompatibilityTest(BitcoinTestFramework):
|
|||
# 0.21.x and 22.x would both produce bad derivation paths when topping up an inactive hd chain
|
||||
# Make sure that this is being automatically cleaned up by migration
|
||||
node_master = self.nodes[1]
|
||||
node_v22 = self.nodes[self.num_nodes - 5]
|
||||
node_v22 = self.nodes[self.num_nodes - 3]
|
||||
wallet_name = "bad_deriv_path"
|
||||
node_v22.createwallet(wallet_name=wallet_name, descriptors=False)
|
||||
bad_deriv_wallet = node_v22.get_wallet_rpc(wallet_name)
|
||||
|
|
Loading…
Add table
Reference in a new issue