Skip to content

Commit

Permalink
Merge bitcoin#30352: policy: Add PayToAnchor(P2A), OP_1 <0x4e73> as…
Browse files Browse the repository at this point in the history
… a standard output script for spending

75648ce test: add P2A ProduceSignature coverage (Greg Sanders)
7998ce6 Add release note for P2A output feature (Greg Sanders)
71c9b02 test: add P2A coverage for decodescript (Greg Sanders)
1349e9e test: Add anchor mempool acceptance test (Greg Sanders)
9d89209 policy: stop 3rd party wtxid malleability of anchor spend (Greg Sanders)
b60aaf8 policy: make anchor spend standard (Greg Sanders)
455fca8 policy: Add OP_1 <0x4e73> as a standard output type (Greg Sanders)

Pull request description:

  This is a sub-feature taken out of the original proposal for ephemeral anchors bitcoin#30239

  This PR makes *spending* of `OP_1 <0x4e73>` (i.e. `bc1pfeessrawgf`) standard. Creation of this output type is already standard.

  Any future witness output types are considered relay-standard to create, but not to spend. This preserves upgrade hooks, such as a completely new output type for a softfork such as BIP341.  It also gives us a bit of room to use a new output type for policy uses.

  This particular sized witness program has no other known use-cases (https://bitcoin.stackexchange.com/a/110664/17078), s it affords insufficient cryptographic security for a secure commitment to data, such as a script or a public key. This makes this type of output "keyless", or unauthenticated.

  As a witness program, the `scriptSig` of the input MUST be blank, by BIP141. This helps ensure txid-stability of the spending transaction, which may be required for smart contracting wallets. If we do not use segwit, a miner can simply insert an `OP_NOP` in the `scriptSig` without effecting the result of program execution.

  An additional relay restriction is to disallow non-empty witness data, which an adversary may use to penalize the "honest" transactor when RBF'ing the transaction due to the incremental fee requirement of RBF rules.

  The intended use-case for this output type is to "anchor" the transaction with a spending child to bring exogenous CPFP fees into the transaction package, encouraging the inclusion of the package in a block. The minimal size of creation and spending of this output makes it an attractive contrast to outputs like `p2sh(OP_TRUE)` and `p2wsh(OP_TRUE)` which
  are significantly larger in vbyte terms.

  Combined with TRUC transactions which limits the size of child transactions significantly, this is an attractive option for presigned transactions that need to be fee-bumped after the fact.

ACKs for top commit:
  sdaftuar:
    utACK 75648ce
  theStack:
    re-ACK 75648ce
  ismaelsadeeq:
    re-ACK 75648ce via [diff](https://github.com/bitcoin/bitcoin/compare/e7ce6dc070c0319cbb868d41cadd836b2e6ca9db..75648cea5a9032b3d388cbebacb94d908e08924e)
  glozow:
    ACK 75648ce
  tdb3:
    ACK 75648ce

Tree-SHA512: d529de23d20857e6cdb40fa611d0446b49989eaafed06c28280e8fd1897f1ed8d89a4eabbec1bbf8df3d319910066c3dbbba5a70a87ff0b2967d5205db32ad1e
  • Loading branch information
glozow committed Aug 2, 2024
2 parents ec8b38c + 75648ce commit 2aff9a3
Show file tree
Hide file tree
Showing 24 changed files with 200 additions and 3 deletions.
10 changes: 10 additions & 0 deletions doc/release-notes-30352.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 4 additions & 0 deletions src/addresstype.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,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;
Expand Down
11 changes: 10 additions & 1 deletion src/addresstype.h
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -116,6 +117,13 @@ struct WitnessUnknown
}
};

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.
Expand All @@ -125,10 +133,11 @@ struct WitnessUnknown
* * 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)
* A CTxDestination is the internal data type encoded in a bitcoin address
*/
using CTxDestination = std::variant<CNoDestination, PubKeyDestination, PKHash, ScriptHash, WitnessV0ScriptHash, WitnessV0KeyHash, WitnessV1Taproot, WitnessUnknown>;
using CTxDestination = std::variant<CNoDestination, PubKeyDestination, PKHash, ScriptHash, WitnessV0ScriptHash, WitnessV0KeyHash, WitnessV1Taproot, PayToAnchor, WitnessUnknown>;

/** Check whether a CTxDestination corresponds to one with an address. */
bool IsValidDestination(const CTxDestination& dest);
Expand Down
4 changes: 4 additions & 0 deletions src/key_io.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,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();
Expand Down
5 changes: 5 additions & 0 deletions src/policy/policy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,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;
Expand Down
2 changes: 2 additions & 0 deletions src/rpc/rawtransaction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,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
Expand Down Expand Up @@ -599,6 +600,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
Expand Down
8 changes: 8 additions & 0 deletions src/rpc/util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,14 @@ class DescribeAddressVisitor
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);
Expand Down
2 changes: 2 additions & 0 deletions src/script/interpreter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1943,6 +1943,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);
Expand Down
17 changes: 17 additions & 0 deletions src/script/script.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,23 @@ unsigned int CScript::GetSigOpCount(const CScript& scriptSig) const
return subscript.GetSigOpCount(true);
}

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:
Expand Down
8 changes: 8 additions & 0 deletions src/script/script.h
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,14 @@ class CScript : public CScriptBase
*/
unsigned int GetSigOpCount(const CScript& scriptSig) 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;
Expand Down
3 changes: 3 additions & 0 deletions src/script/sign.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,9 @@ static bool SignStep(const SigningProvider& provider, const BaseSignatureCreator

case TxoutType::WITNESS_V1_TAPROOT:
return SignTaproot(provider, creator, WitnessV1Taproot(XOnlyPubKey{vSolutions[0]}), sigdata, ret);

case TxoutType::ANCHOR:
return true;
} // no default case, so the compiler can warn about missing cases
assert(false);
}
Expand Down
4 changes: 4 additions & 0 deletions src/script/solver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,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";
Expand Down Expand Up @@ -165,6 +166,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));
Expand Down
1 change: 1 addition & 0 deletions src/script/solver.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ template <typename C> class Span;
enum class TxoutType {
NONSTANDARD,
// 'standard' transaction types:
ANCHOR, //!< anyone can spend script
PUBKEY,
PUBKEYHASH,
SCRIPTHASH,
Expand Down
7 changes: 5 additions & 2 deletions src/test/fuzz/script.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand All @@ -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();
Expand Down
3 changes: 3 additions & 0 deletions src/test/fuzz/util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,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) {
Expand Down
26 changes: 26 additions & 0 deletions src/test/script_standard_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down
13 changes: 13 additions & 0 deletions src/test/script_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1277,6 +1277,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(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;
Expand Down
8 changes: 8 additions & 0 deletions src/test/transaction_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1026,6 +1026,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()
1 change: 1 addition & 0 deletions src/wallet/rpc/addresses.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,7 @@ class DescribeWalletAddressVisitor
}

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); }
};

Expand Down
1 change: 1 addition & 0 deletions src/wallet/rpc/backup.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -910,6 +910,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";
} // no default case, so the compiler can warn about missing cases
NONFATAL_UNREACHABLE();
Expand Down
1 change: 1 addition & 0 deletions src/wallet/scriptpubkeyman.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ IsMineResult IsMineInner(const LegacyDataSPKM& keystore, const CScript& scriptPu
case TxoutType::NULL_DATA:
case TxoutType::WITNESS_UNKNOWN:
case TxoutType::WITNESS_V1_TAPROOT:
case TxoutType::ANCHOR:
break;
case TxoutType::PUBKEY:
keyID = CPubKey(vSolutions[0]).GetID();
Expand Down
51 changes: 51 additions & 0 deletions test/functional/mempool_accept.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
keys_to_multisig_script,
MIN_PADDING,
MIN_STANDARD_TX_NONWITNESS_SIZE,
PAY_TO_ANCHOR,
script_to_p2sh_script,
script_to_p2wsh_script,
)
Expand Down Expand Up @@ -389,6 +390,56 @@ def run_test(self):
maxfeerate=0,
)

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.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 - int(fee*COIN), script_to_p2wsh_script(CScript([OP_TRUE]))))
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)
Expand Down
Loading

0 comments on commit 2aff9a3

Please sign in to comment.