Merge 2aff9a36c3 into merged_master (Bitcoin PR bitcoin/bitcoin#30352)

This commit is contained in:
ivanlele 2026-03-11 09:41:57 +00:00
commit cd6ec40d0f
No known key found for this signature in database
24 changed files with 202 additions and 6 deletions

View file

@ -0,0 +1,10 @@
P2P and network changes
-----------------------
- Pay To Anchor(P2A) is a new standard witness output type for spending,
a newly recognised output template. This allows for key-less anchor
outputs, with compact spending conditions for additional efficiencies on
top of an equivalent `sh(OP_TRUE)` output, in addition to the txid stability
of the spending transaction.
N.B. propagation of this output spending on the network will be limited
until a sufficient number of nodes on the network adopt this upgrade.

View file

@ -100,6 +100,10 @@ bool ExtractDestination(const CScript& scriptPubKey, CTxDestination& addressRet)
addressRet = tap;
return true;
}
case TxoutType::ANCHOR: {
addressRet = PayToAnchor();
return true;
}
case TxoutType::WITNESS_UNKNOWN: {
addressRet = WitnessUnknown{vSolutions[0][0], vSolutions[1]};
return true;

View file

@ -9,6 +9,7 @@
#include <pubkey.h>
#include <script/script.h>
#include <uint256.h>
#include <util/check.h>
#include <util/hash_type.h>
#include <algorithm>
@ -148,6 +149,13 @@ public:
}
};
struct PayToAnchor : public WitnessUnknown
{
PayToAnchor() : WitnessUnknown(1, {0x4e, 0x73}) {
Assume(CScript::IsPayToAnchor(1, {0x4e, 0x73}));
};
};
/**
* A txout script categorized into standard templates.
* * CNoDestination: Optionally a script, no corresponding address.
@ -157,11 +165,12 @@ public:
* * WitnessV0ScriptHash: TxoutType::WITNESS_V0_SCRIPTHASH destination (P2WSH address)
* * WitnessV0KeyHash: TxoutType::WITNESS_V0_KEYHASH destination (P2WPKH address)
* * WitnessV1Taproot: TxoutType::WITNESS_V1_TAPROOT destination (P2TR address)
* * PayToAnchor: TxoutType::ANCHOR destination (P2A address)
* * WitnessUnknown: TxoutType::WITNESS_UNKNOWN destination (P2W??? address)
* * NullData: TxoutType::NULL_DATA destination (OP_RETURN) // ELEMENTS
* A CTxDestination is the internal data type encoded in a bitcoin address
*/
using CTxDestination = std::variant<CNoDestination, PubKeyDestination, PKHash, ScriptHash, WitnessV0ScriptHash, WitnessV0KeyHash, WitnessV1Taproot, WitnessUnknown, NullData>;
using CTxDestination = std::variant<CNoDestination, PubKeyDestination, PKHash, ScriptHash, WitnessV0ScriptHash, WitnessV0KeyHash, WitnessV1Taproot, PayToAnchor, WitnessUnknown, NullData>;
/** Check whether a CTxDestination corresponds to one with an address. */
bool IsValidDestination(const CTxDestination& dest);

View file

@ -282,6 +282,10 @@ CTxDestination DecodeDestination(const std::string& str, const CChainParams& par
return tap;
}
if (CScript::IsPayToAnchor(version, data)) {
return PayToAnchor();
}
if (version > 16) {
error_str = "Invalid Bech32 address witness version";
return CNoDestination();

View file

@ -249,6 +249,11 @@ bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs)
// get the scriptPubKey corresponding to this input:
CScript prevScript = prev.scriptPubKey;
// witness stuffing detected
if (prevScript.IsPayToAnchor()) {
return false;
}
bool p2sh = false;
if (prevScript.IsPayToScriptHash()) {
std::vector <std::vector<unsigned char> > stack;

View file

@ -630,6 +630,7 @@ static RPCHelpMan decodescript()
case TxoutType::SCRIPTHASH:
case TxoutType::WITNESS_UNKNOWN:
case TxoutType::WITNESS_V1_TAPROOT:
case TxoutType::ANCHOR:
// Should not be wrapped
return false;
} // no default case, so the compiler can warn about missing cases
@ -676,6 +677,7 @@ static RPCHelpMan decodescript()
case TxoutType::WITNESS_V0_KEYHASH:
case TxoutType::WITNESS_V0_SCRIPTHASH:
case TxoutType::WITNESS_V1_TAPROOT:
case TxoutType::ANCHOR:
// Should not be wrapped
return false;
} // no default case, so the compiler can warn about missing cases

View file

@ -333,6 +333,14 @@ public:
return obj;
}
UniValue operator()(const PayToAnchor& anchor) const
{
UniValue obj(UniValue::VOBJ);
obj.pushKV("isscript", true);
obj.pushKV("iswitness", true);
return obj;
}
UniValue operator()(const WitnessUnknown& id) const
{
UniValue obj(UniValue::VOBJ);

View file

@ -3313,6 +3313,8 @@ static bool VerifyWitnessProgram(const CScriptWitness& witness, int witversion,
}
return set_success(serror);
}
} else if (!is_p2sh && CScript::IsPayToAnchor(witversion, program)) {
return true;
} else {
if (flags & SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM) {
return set_error(serror, SCRIPT_ERR_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM);

View file

@ -318,6 +318,23 @@ bool CScript::IsPayToWitnessPubkeyHash() const
// END ELEMENTS
//
bool CScript::IsPayToAnchor() const
{
return (this->size() == 4 &&
(*this)[0] == OP_1 &&
(*this)[1] == 0x02 &&
(*this)[2] == 0x4e &&
(*this)[3] == 0x73);
}
bool CScript::IsPayToAnchor(int version, const std::vector<unsigned char>& program)
{
return version == 1 &&
program.size() == 2 &&
program[0] == 0x4e &&
program[1] == 0x73;
}
bool CScript::IsPayToScriptHash() const
{
// Extra-fast test for pay-to-script-hash CScripts:

View file

@ -597,6 +597,14 @@ public:
bool IsPayToPubkeyHash() const;
bool IsPayToWitnessPubkeyHash() const;
/*
* OP_1 <0x4e73>
*/
bool IsPayToAnchor() const;
/** Checks if output of IsWitnessProgram comes from a P2A output script
*/
static bool IsPayToAnchor(int version, const std::vector<unsigned char>& program);
bool IsPayToScriptHash() const;
bool IsPayToWitnessScriptHash() const;
bool IsWitnessProgram(int& version, std::vector<unsigned char>& program) const;

View file

@ -484,10 +484,10 @@ static bool SignStep(const SigningProvider& provider, const BaseSignatureCreator
// ELEMENTS
case TxoutType::FEE:
return false;
case TxoutType::OP_TRUE:
return Params().anyonecanspend_aremine;
case TxoutType::ANCHOR:
return true;
} // no default case, so the compiler can warn about missing cases
assert(false);
}

View file

@ -25,6 +25,7 @@ std::string GetTxnOutputType(TxoutType t)
case TxoutType::SCRIPTHASH: return "scripthash";
case TxoutType::MULTISIG: return "multisig";
case TxoutType::NULL_DATA: return "nulldata";
case TxoutType::ANCHOR: return "anchor";
case TxoutType::WITNESS_V0_KEYHASH: return "witness_v0_keyhash";
case TxoutType::WITNESS_V0_SCRIPTHASH: return "witness_v0_scripthash";
case TxoutType::WITNESS_V1_TAPROOT: return "witness_v1_taproot";
@ -179,6 +180,9 @@ TxoutType Solver(const CScript& scriptPubKey, std::vector<std::vector<unsigned c
vSolutionsRet.push_back(std::move(witnessprogram));
return TxoutType::WITNESS_V1_TAPROOT;
}
if (scriptPubKey.IsPayToAnchor()) {
return TxoutType::ANCHOR;
}
if (witnessversion != 0) {
vSolutionsRet.push_back(std::vector<unsigned char>{(unsigned char)witnessversion});
vSolutionsRet.push_back(std::move(witnessprogram));

View file

@ -22,6 +22,7 @@ template <typename C> class Span;
enum class TxoutType {
NONSTANDARD,
// 'standard' transaction types:
ANCHOR, //!< anyone can spend script
PUBKEY,
PUBKEYHASH,
SCRIPTHASH,

View file

@ -76,11 +76,13 @@ FUZZ_TARGET(script, .init = initialize_script)
assert(which_type == TxoutType::PUBKEY ||
which_type == TxoutType::NONSTANDARD ||
which_type == TxoutType::NULL_DATA ||
which_type == TxoutType::MULTISIG);
which_type == TxoutType::MULTISIG ||
which_type == TxoutType::ANCHOR);
}
if (which_type == TxoutType::NONSTANDARD ||
which_type == TxoutType::NULL_DATA ||
which_type == TxoutType::MULTISIG) {
which_type == TxoutType::MULTISIG ||
which_type == TxoutType::ANCHOR) {
assert(!extract_destination_ret);
}
@ -94,6 +96,7 @@ FUZZ_TARGET(script, .init = initialize_script)
(void)Solver(script, solutions);
(void)script.HasValidOps();
(void)script.IsPayToAnchor();
(void)script.IsPayToScriptHash();
(void)script.IsPayToWitnessScriptHash();
(void)script.IsPushOnly();

View file

@ -216,6 +216,9 @@ CTxDestination ConsumeTxDestination(FuzzedDataProvider& fuzzed_data_provider) no
[&] {
tx_destination = WitnessV1Taproot{XOnlyPubKey{ConsumeUInt256(fuzzed_data_provider)}};
},
[&] {
tx_destination = PayToAnchor{};
},
[&] {
std::vector<unsigned char> program{ConsumeRandomLengthByteVector(fuzzed_data_provider, /*max_length=*/40)};
if (program.size() < 2) {

View file

@ -128,6 +128,20 @@ BOOST_AUTO_TEST_CASE(script_standard_Solver_success)
BOOST_CHECK(solutions[0] == std::vector<unsigned char>{16});
BOOST_CHECK(solutions[1] == ToByteVector(uint256::ONE));
// TxoutType::ANCHOR
std::vector<unsigned char> anchor_bytes{0x4e, 0x73};
s.clear();
s << OP_1 << anchor_bytes;
BOOST_CHECK_EQUAL(Solver(s, solutions), TxoutType::ANCHOR);
BOOST_CHECK(solutions.empty());
// Sanity-check IsPayToAnchor
int version{-1};
std::vector<unsigned char> witness_program;
BOOST_CHECK(s.IsPayToAnchor());
BOOST_CHECK(s.IsWitnessProgram(version, witness_program));
BOOST_CHECK(CScript::IsPayToAnchor(version, witness_program));
// TxoutType::NONSTANDARD
s.clear();
s << OP_9 << OP_ADD << OP_11 << OP_EQUAL;
@ -186,6 +200,18 @@ BOOST_AUTO_TEST_CASE(script_standard_Solver_failure)
s.clear();
s << OP_0 << std::vector<unsigned char>(19, 0x01);
BOOST_CHECK_EQUAL(Solver(s, solutions), TxoutType::NONSTANDARD);
// TxoutType::ANCHOR but wrong witness version
s.clear();
s << OP_2 << std::vector<unsigned char>{0x4e, 0x73};
BOOST_CHECK(!s.IsPayToAnchor());
BOOST_CHECK_EQUAL(Solver(s, solutions), TxoutType::WITNESS_UNKNOWN);
// TxoutType::ANCHOR but wrong 2-byte data push
s.clear();
s << OP_1 << std::vector<unsigned char>{0xff, 0xff};
BOOST_CHECK(!s.IsPayToAnchor());
BOOST_CHECK_EQUAL(Solver(s, solutions), TxoutType::WITNESS_UNKNOWN);
}
BOOST_AUTO_TEST_CASE(script_standard_ExtractDestination)

View file

@ -1315,6 +1315,19 @@ BOOST_AUTO_TEST_CASE(sign_invalid_miniscript)
BOOST_CHECK(!SignSignature(keystore, CTransaction(prev), curr, 0, SIGHASH_ALL, sig_data));
}
/* P2A input should be considered signed. */
BOOST_AUTO_TEST_CASE(sign_paytoanchor)
{
FillableSigningProvider keystore;
SignatureData sig_data;
CMutableTransaction prev, curr;
prev.vout.emplace_back(CAsset(), 0, GetScriptForDestination(PayToAnchor{}));
curr.vin.emplace_back(COutPoint{prev.GetHash(), 0});
BOOST_CHECK(SignSignature(keystore, CTransaction(prev), curr, 0, SIGHASH_ALL, sig_data));
}
BOOST_AUTO_TEST_CASE(script_standard_push)
{
ScriptError err;

View file

@ -1043,6 +1043,14 @@ BOOST_AUTO_TEST_CASE(test_IsStandard)
t.vout[0].nValue = 239;
CheckIsNotStandard(t, "dust");
}
// Check anchor outputs
t.vout[0].scriptPubKey = CScript() << OP_1 << std::vector<unsigned char>{0x4e, 0x73};
BOOST_CHECK(t.vout[0].scriptPubKey.IsPayToAnchor());
t.vout[0].nValue = 240;
CheckIsStandard(t);
t.vout[0].nValue = 239;
CheckIsNotStandard(t, "dust");
}
BOOST_AUTO_TEST_SUITE_END()

View file

@ -519,6 +519,7 @@ public:
}
UniValue operator()(const WitnessV1Taproot& id) const { return UniValue(UniValue::VOBJ); }
UniValue operator()(const PayToAnchor& id) const { return UniValue(UniValue::VOBJ); }
UniValue operator()(const WitnessUnknown& id) const { return UniValue(UniValue::VOBJ); }
UniValue operator()(const NullData& id) const { return NullUniValue; }
};

View file

@ -930,6 +930,7 @@ static std::string RecurseImportData(const CScript& script, ImportData& import_d
case TxoutType::NONSTANDARD:
case TxoutType::WITNESS_UNKNOWN:
case TxoutType::WITNESS_V1_TAPROOT:
case TxoutType::ANCHOR:
return "unrecognized script";
// ELEMENTS
case TxoutType::OP_TRUE:

View file

@ -116,6 +116,7 @@ IsMineResult IsMineInner(const LegacyDataSPKM& keystore, const CScript& scriptPu
case TxoutType::WITNESS_UNKNOWN:
case TxoutType::WITNESS_V1_TAPROOT:
case TxoutType::FEE:
case TxoutType::ANCHOR:
break;
case TxoutType::PUBKEY:
keyID = CPubKey(vSolutions[0]).GetID();

View file

@ -15,7 +15,7 @@ from test_framework.messages import (
COutPoint,
CTransaction,
CTxIn,
# CTxInWitness,
CTxInWitness,
CTxOut,
CTxOutValue,
MAX_BLOCK_WEIGHT,
@ -38,6 +38,7 @@ from test_framework.script_util import (
keys_to_multisig_script,
# MIN_PADDING,
MIN_STANDARD_TX_NONWITNESS_SIZE,
PAY_TO_ANCHOR,
script_to_p2sh_script,
script_to_p2wsh_script,
)
@ -379,6 +380,58 @@ class MempoolAcceptanceTest(BitcoinTestFramework):
# CVE-2017-12842 does not affect elements transactions
assert_equal(MIN_STANDARD_TX_NONWITNESS_SIZE - 1, 64)
self.log.info('OP_1 <0x4e73> is able to be created and spent')
anchor_value = 10000
create_anchor_tx = self.wallet.send_to(from_node=node, scriptPubKey=PAY_TO_ANCHOR, amount=anchor_value)
self.generate(node, 1)
# First spend has non-empty witness, will be rejected to prevent third party wtxid malleability
anchor_nonempty_wit_spend = CTransaction()
anchor_nonempty_wit_spend.vin.append(CTxIn(COutPoint(int(create_anchor_tx["txid"], 16), create_anchor_tx["sent_vout"]), b""))
anchor_nonempty_wit_spend.vout.append(CTxOut(anchor_value - int(fee*COIN), script_to_p2wsh_script(CScript([OP_TRUE]))))
anchor_nonempty_wit_spend.vout.append(CTxOut(int(fee*COIN))) # ELEMENTS: explicit fee output
anchor_nonempty_wit_spend.wit.vtxinwit.append(CTxInWitness())
anchor_nonempty_wit_spend.wit.vtxinwit[0].scriptWitness.stack.append(b"f")
anchor_nonempty_wit_spend.rehash()
self.check_mempool_result(
result_expected=[{'txid': anchor_nonempty_wit_spend.rehash(), 'allowed': False, 'reject-reason': 'bad-witness-nonstandard'}],
rawtxs=[anchor_nonempty_wit_spend.serialize().hex()],
maxfeerate=0,
)
# Clear witness stuffing
anchor_spend = anchor_nonempty_wit_spend
anchor_spend.wit.vtxinwit[0].scriptWitness.stack = []
anchor_spend.rehash()
self.check_mempool_result(
result_expected=[{'txid': anchor_spend.rehash(), 'allowed': True, 'vsize': anchor_spend.get_vsize(), 'fees': { 'base': Decimal('0.00000700')}}],
rawtxs=[anchor_spend.serialize().hex()],
maxfeerate=0,
)
self.log.info('But cannot be spent if nested sh()')
nested_anchor_tx = self.wallet.create_self_transfer(sequence=SEQUENCE_FINAL)['tx']
nested_anchor_tx.vout[0].scriptPubKey = script_to_p2sh_script(PAY_TO_ANCHOR)
nested_anchor_tx.rehash()
self.generateblock(node, self.wallet.get_address(), [nested_anchor_tx.serialize().hex()])
nested_anchor_spend = CTransaction()
nested_anchor_spend.vin.append(CTxIn(COutPoint(nested_anchor_tx.sha256, 0), b""))
nested_anchor_spend.vin[0].scriptSig = CScript([bytes(PAY_TO_ANCHOR)])
nested_anchor_spend.vout.append(CTxOut(nested_anchor_tx.vout[0].nValue.getAmount() - int(fee*COIN), script_to_p2wsh_script(CScript([OP_TRUE]))))
nested_anchor_spend.vout.append(CTxOut(int(fee*COIN))) # ELEMENTS: explicit fee output
nested_anchor_spend.rehash()
self.check_mempool_result(
result_expected=[{'txid': nested_anchor_spend.rehash(), 'allowed': False, 'reject-reason': 'non-mandatory-script-verify-flag (Witness version reserved for soft-fork upgrades)'}],
rawtxs=[nested_anchor_spend.serialize().hex()],
maxfeerate=0,
)
# but is consensus-legal
self.generateblock(node, self.wallet.get_address(), [nested_anchor_spend.serialize().hex()])
self.log.info('Spending a confirmed bare multisig is okay')
address = self.wallet.get_address()
tx = tx_from_hex(raw_tx_reference)

View file

@ -187,6 +187,16 @@ class DecodeScriptTest(BitcoinTestFramework):
assert_equal('1 ' + xonly_public_key, rpc_result['asm'])
assert 'segwit' not in rpc_result
self.log.info("- P2A (anchor)")
# 1 <4e73>
witprog_hex = '4e73'
rpc_result = self.nodes[0].decodescript('5102' + witprog_hex)
assert_equal('anchor', rpc_result['type'])
# in the disassembly, the witness program is shown as single decimal due to its small size
witprog_as_decimal = int.from_bytes(bytes.fromhex(witprog_hex), 'little')
assert_equal(f'1 {witprog_as_decimal}', rpc_result['asm'])
assert_equal('ert1pfees5fana6', rpc_result['address'])
def decoderawtransaction_asm_sighashtype(self):
"""Test decoding scripts via RPC command "decoderawtransaction".

View file

@ -8,6 +8,7 @@ import unittest
from test_framework.script import (
CScript,
OP_0,
OP_1,
OP_15,
OP_16,
OP_CHECKMULTISIG,
@ -42,6 +43,8 @@ assert MIN_PADDING == 5
DUMMY_MIN_OP_RETURN_SCRIPT = CScript([OP_RETURN] + ([OP_0] * (MIN_PADDING - 1)))
assert len(DUMMY_MIN_OP_RETURN_SCRIPT) == MIN_PADDING
PAY_TO_ANCHOR = CScript([OP_1, bytes.fromhex("4e73")])
def key_to_p2pk_script(key):
key = check_key(key)
return CScript([key, OP_CHECKSIG])