Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,7 @@ void SetupServerArgs(ArgsManager& argsman)
argsman.AddArg("-mempoolfullrbf", strprintf("Accept transaction replace-by-fee without requiring replaceability signaling (default: %u)", DEFAULT_MEMPOOL_FULL_RBF), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY);
argsman.AddArg("-permitbaremultisig", strprintf("Relay non-P2SH multisig (default: %u)", DEFAULT_PERMIT_BAREMULTISIG), ArgsManager::ALLOW_ANY,
OptionsCategory::NODE_RELAY);
argsman.AddArg("-annexcarrier", strprintf("Relay and mine transactions with annex data, up to %u bytes of payload (default: %u)", MAX_ANNEX_DATA, DEFAULT_ACCEPT_ANNEXDATA), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY);
argsman.AddArg("-minrelaytxfee=<amt>", strprintf("Fees (in %s/kvB) smaller than this are considered zero fee for relaying, mining and transaction creation (default: %s)",
CURRENCY_UNIT, FormatMoney(DEFAULT_MIN_RELAY_TX_FEE)), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY);
argsman.AddArg("-whitelistforcerelay", strprintf("Add 'forcerelay' permission to whitelisted inbound peers with default permissions. This will relay transactions even if the transactions were already in the mempool. (default: %d)", DEFAULT_WHITELISTFORCERELAY), ArgsManager::ALLOW_ANY, OptionsCategory::NODE_RELAY);
Expand Down
1 change: 1 addition & 0 deletions src/kernel/mempool_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ struct MemPoolOptions {
* If nullopt, any size is nonstandard.
*/
std::optional<unsigned> max_datacarrier_bytes{DEFAULT_ACCEPT_DATACARRIER ? std::optional{MAX_OP_RETURN_RELAY} : std::nullopt};
bool annex_datacarrier{DEFAULT_ACCEPT_ANNEXDATA};
bool permit_bare_multisig{DEFAULT_PERMIT_BAREMULTISIG};
bool require_standard{true};
bool full_rbf{DEFAULT_MEMPOOL_FULL_RBF};
Expand Down
2 changes: 2 additions & 0 deletions src/node/mempool_args.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ std::optional<bilingual_str> ApplyArgsManOptions(const ArgsManager& argsman, con
mempool_opts.max_datacarrier_bytes = std::nullopt;
}

mempool_opts.annex_datacarrier = argsman.GetBoolArg("-annexcarrier", DEFAULT_ACCEPT_ANNEXDATA);

mempool_opts.require_standard = !argsman.GetBoolArg("-acceptnonstdtxn", !chainparams.RequireStandard());
if (!chainparams.IsTestChain() && !mempool_opts.require_standard) {
return strprintf(Untranslated("acceptnonstdtxn is not currently supported for %s chain"), chainparams.NetworkIDString());
Expand Down
20 changes: 16 additions & 4 deletions src/policy/policy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,15 @@ bool AreInputsStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs)
return true;
}

bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs)
bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs, bool allow_annex_data)
{
// For now allow 0 to MAX_ANNEX_DATA bytes push to allow drop-in replacement via BIP PR#1381
// but we allow it spread over any number of inputs based on this budget:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Payload budget spread over any number of inputs might not work well for non-interactive combination of unordered pair of inputs/outputs under SIGHASH_SINGLE|SIGHASH_ANYONECANPAY, and such batching might still exist with eltoo LN, no ?

Copy link
Author

@instagibbs instagibbs Mar 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are larger issues with annex budgeting we have to solve I think due to the deficiency in sighashes over the annex. Let's continue thinking on this,

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if you want to do a non-interactive combination of inputs/outputs it's best to do it by having multiple transactions -- ie Ai to Ao, Bi to Bo, Ci to Co1 and Co2, would be three transactions each paying 0 fees and each with an ephemeral anchor output, than a fourth transaction D which pays fees for all of them and spends all their ephemeral anchors. That minimises malleability so you can spend "Ao" without having to worry about its txid changing due to a fee bump, and I think reduces the complexity of dealing with partial rbfs etc. Presuming things work out that way, each of Ai, Bi, Ci naturally get their own budget for annex data.

// 1(witness stack element size) + 1(annex marker) + 1(payload len) + MAX_ANNEX_DATA(payload)
size_t annex_bytes_left{1 + 1 + 1 + MAX_ANNEX_DATA};

if (!allow_annex_data) annex_bytes_left = 0;

if (tx.IsCoinBase())
return true; // Coinbases are skipped

Expand Down Expand Up @@ -266,13 +273,18 @@ bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs)

// Check policy limits for Taproot spends:
// - MAX_STANDARD_TAPSCRIPT_STACK_ITEM_SIZE limit for stack item size
// - No annexes
// - Limited annex format
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the 0x50 <1 byte data len> start to be used by more experimental applications on inquisition, of course we might have conflicts with assigning consensus semantic for taproot annex, maybe annexcarrier could be its own annex "tag" by introducing a marker value before the len for a v0.2 of annexcarrier. I definitely can see a "annexcarrier" generic data payload where there is no meaning attached to the record itself, for client-reserved value (like nSequence was used sometimes in the past)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per bitcoin/bips#1381 0x50 n [n-bytes] (where n is a single byte equal or less than 0x7e) is interpreted as an annex entry with type 0 and contents of n-bytes, so this is effectively just reserving type 0 for data that doesn't have any additional consensus meaning (though may be accessible via a future OP_PUSHANNEX). Per that spec, to extend it to other types (which may have additional consensus meaning), you'd use a multibyte n where the first byte has the high bit set, and to extend to longer than 126 bytes you'd say 0x50 0x7F [n-0x7F] [n-bytes].

if (witnessversion == 1 && witnessprogram.size() == WITNESS_V1_TAPROOT_SIZE && !p2sh) {
// Taproot spend (non-P2SH-wrapped, version 1, witness program size 32; see BIP 341)
Span stack{tx.vin[i].scriptWitness.stack};
if (stack.size() >= 2 && !stack.back().empty() && stack.back()[0] == ANNEX_TAG) {
// Annexes are nonstandard as long as no semantics are defined for them.
return false;
const auto& annex = stack.back();

if (annex.size() >= annex_bytes_left) return false;
annex_bytes_left -= annex.size() + 1; // bip141 witness stack element size included

// limit annex format to allow for future expansion
if (annex.size() < 2 || annex.size() != static_cast<size_t>(annex[1]) + 2) return false;
}
if (stack.size() >= 2) {
// Script path spend (2 or more stack elements after removing optional annex)
Expand Down
4 changes: 2 additions & 2 deletions src/policy/policy.h
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,9 @@ bool AreInputsStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs)
* 3600bytes witnessScript size, 80bytes per witness stack element, 100 witness stack elements
* These limits are adequate for multisignatures up to n-of-100 using OP_CHECKSIG, OP_ADD, and OP_EQUAL.
*
* Also enforce a maximum stack item size limit and no annexes for tapscript spends.
* Also enforce a maximum stack item size limit and limited annexes for tapscript spends.
*/
bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs);
bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs, bool allow_annex_data);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it would be better to pull the annex check out of here, into its own function that takes a set of flags like STANDARD_SCRIPT_VERIFY_FLAGS. Maybe something to consider if/when more things are added to the annex.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Future Work(TM), agreed


/** Compute the virtual transaction size (weight reinterpreted as bytes). */
int64_t GetVirtualTransactionSize(int64_t nWeight, int64_t nSigOpCost, unsigned int bytes_per_sigop);
Expand Down
12 changes: 12 additions & 0 deletions src/script/standard.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include <variant>

static const bool DEFAULT_ACCEPT_DATACARRIER = true;
static const bool DEFAULT_ACCEPT_ANNEXDATA = true;

class CKeyID;
class CScript;
Expand All @@ -38,6 +39,17 @@ class CScriptID : public BaseHash<uint160>
*/
static const unsigned int MAX_OP_RETURN_RELAY = 83;

/**
* Allows up to one annex data to be embedded in a transaction, counting towards the
* same data carrier limit as OP_RETURN values
*/
extern bool accept_annex_data;

/**
* Static maximum of annex data per transaction
*/
static const unsigned int MAX_ANNEX_DATA = 126;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it wasn't for this limitation, inscriptions could also use the annex and with that become more chain-space efficient because there's no commit/reveal scheme needed anymore?

Copy link

@joostjager joostjager Mar 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it may be worth to make this policy change so that you can at least have a single large data block too. There are other applications besides inscriptions that require more than 126 bytes.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This limitation is specifically in place so I don't have to rely on a more complex encoding ala #9 to start experimentation with useful size annexes. It's not a principled limitation with respect to DoS concerns or anything.

Copy link

@joostjager joostjager Mar 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TLV is nice, but also it doesn't seem quite clear how any of this is going to be validated, if ever. The basic case of just storing data though is immediately useful. What would you think of using

0x50 0x00 <len bytes>

for this basic case, and reserve other values of the second byte for a future tlv format? This is also more compact for lengths > 126, because the length itself doesn't need to be encoded.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For larger sizes it can be a bit annoying, since currently taproot checksigs don't cover other inputs annexes. Limiting total annex to 126 bytes means a griefing counter-party can only do so much griefing.

I'm agnostic to what the specific format is; I'm looking to experiment with small sizes.


/**
* Mandatory script verification flags that all new blocks must comply with for
* them to be valid. (but old blocks may not comply with) Currently just P2SH,
Expand Down
2 changes: 1 addition & 1 deletion src/test/fuzz/coins_view.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ FUZZ_TARGET_INIT(coins_view, initialize_coins_view)
(void)GetTransactionSigOpCost(transaction, coins_view_cache, flags);
},
[&] {
(void)IsWitnessStandard(CTransaction{random_mutable_transaction}, coins_view_cache);
(void)IsWitnessStandard(CTransaction{random_mutable_transaction}, coins_view_cache, /* allow_annex_data= */ true);
});
}
}
2 changes: 1 addition & 1 deletion src/test/fuzz/transaction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ FUZZ_TARGET_INIT(transaction, initialize_transaction)
CCoinsView coins_view;
const CCoinsViewCache coins_view_cache(&coins_view);
(void)AreInputsStandard(tx, coins_view_cache);
(void)IsWitnessStandard(tx, coins_view_cache);
(void)IsWitnessStandard(tx, coins_view_cache, /* allow_annex_data= */true);

UniValue u(UniValue::VOBJ);
TxToUniv(tx, /*block_hash=*/uint256::ZERO, /*entry=*/u);
Expand Down
1 change: 1 addition & 0 deletions src/txmempool.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,7 @@ CTxMemPool::CTxMemPool(const Options& opts)
m_dust_relay_feerate{opts.dust_relay_feerate},
m_permit_bare_multisig{opts.permit_bare_multisig},
m_max_datacarrier_bytes{opts.max_datacarrier_bytes},
m_annex_datacarrier{opts.annex_datacarrier},
m_require_standard{opts.require_standard},
m_full_rbf{opts.full_rbf},
m_limits{opts.limits}
Expand Down
1 change: 1 addition & 0 deletions src/txmempool.h
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,7 @@ class CTxMemPool
const CFeeRate m_dust_relay_feerate;
const bool m_permit_bare_multisig;
const std::optional<unsigned> m_max_datacarrier_bytes;
const bool m_annex_datacarrier;
const bool m_require_standard;
const bool m_full_rbf;

Expand Down
2 changes: 1 addition & 1 deletion src/validation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -809,7 +809,7 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
}

// Check for non-standard witnesses.
if (tx.HasWitness() && m_pool.m_require_standard && !IsWitnessStandard(tx, m_view)) {
if (tx.HasWitness() && m_pool.m_require_standard && !IsWitnessStandard(tx, m_view, m_pool.m_annex_datacarrier)) {
return state.Invalid(TxValidationResult::TX_WITNESS_MUTATED, "bad-witness-nonstandard");
}

Expand Down
7 changes: 4 additions & 3 deletions test/functional/feature_taproot.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
CTxIn,
CTxInWitness,
CTxOut,
MAX_ANNEX_DATA,
SEQUENCE_FINAL,
)
from test_framework.script import (
Expand Down Expand Up @@ -680,9 +681,9 @@ def spenders_taproot_active():

# == Tests for signature hashing ==

# Run all tests once with no annex, and once with a valid random annex.
for annex in [None, lambda _: bytes([ANNEX_TAG]) + random_bytes(random.randrange(0, 250))]:
# Non-empty annex is non-standard
# Run all tests once with no annex, and once with a valid yet non-standard random annex.
for annex in [None, lambda _: bytes([ANNEX_TAG, MAX_ANNEX_DATA + 1]) + random_bytes(random.randrange(0, 250))]:
# Non-empty annex is non-standard unless it's a specific format we avoid making in above lambda
no_annex = annex is None

# Sighash mutation tests (test all sighash combinations)
Expand Down
31 changes: 30 additions & 1 deletion test/functional/mempool_datacarrier.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"""Test datacarrier functionality"""
from test_framework.messages import (
CTxOut,
MAX_ANNEX_DATA,
MAX_OP_RETURN_RELAY,
)
from test_framework.script import (
Expand All @@ -25,7 +26,7 @@ def set_test_params(self):
self.num_nodes = 3
self.extra_args = [
[],
["-datacarrier=0"],
["-datacarrier=0", "-annexcarrier=0"],
["-datacarrier=1", f"-datacarriersize={MAX_OP_RETURN_RELAY - 1}"]
]

Expand All @@ -42,6 +43,20 @@ def test_null_data_transaction(self, node: TestNode, data: bytes, success: bool)
else:
assert_raises_rpc_error(-26, "scriptpubkey", self.wallet.sendrawtransaction, from_node=node, tx_hex=tx_hex)

def test_annex_data_transaction(self, node: TestNode, data: bytes, success: bool) -> None:
tx = self.wallet.create_self_transfer(fee_rate=0)["tx"]
tx.wit.vtxinwit[0].scriptWitness.stack = tx.wit.vtxinwit[0].scriptWitness.stack + [bytes.fromhex("50") + len(data).to_bytes(1, 'little') + bytes.fromhex("ff"*len(data))]
tx.vout[0].nValue -= tx.get_vsize() # simply pay 1sat/vbyte fee

tx_hex = tx.serialize().hex()

if success:
self.wallet.sendrawtransaction(from_node=node, tx_hex=tx_hex)
assert tx.rehash() in node.getrawmempool(True), f'{tx_hex} not in mempool'
else:
assert_raises_rpc_error(-26, "bad-witness-nonstandard", self.wallet.sendrawtransaction, from_node=node, tx_hex=tx_hex)


def run_test(self):
self.wallet = MiniWallet(self.nodes[0])
self.wallet.rescan_utxos()
Expand All @@ -51,6 +66,20 @@ def run_test(self):
too_long_data = random_bytes(MAX_OP_RETURN_RELAY - 2)
small_data = random_bytes(MAX_OP_RETURN_RELAY - 4)

small_annex = random_bytes(0)
large_annex = random_bytes(MAX_ANNEX_DATA)
oversize_annex = random_bytes(127)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe MAX_ANNEX_DATA + 1 here, rather than a fixed number, as if the max was raised this would no longer be oversized?


self.log.info("Testing annex data transaction with default -annexcarrier value.")
self.test_annex_data_transaction(node=self.nodes[0], data=small_annex, success=True)
self.test_annex_data_transaction(node=self.nodes[0], data=large_annex, success=True)
self.test_annex_data_transaction(node=self.nodes[0], data=oversize_annex, success=False)

self.log.info("Testing annex data transaction with false -annexcarrier value.")
self.test_annex_data_transaction(node=self.nodes[1], data=small_annex, success=False)
self.test_annex_data_transaction(node=self.nodes[1], data=large_annex, success=False)
self.test_annex_data_transaction(node=self.nodes[1], data=oversize_annex, success=False)

self.log.info("Testing null data transaction with default -datacarrier and -datacarriersize values.")
self.test_null_data_transaction(node=self.nodes[0], data=default_size_data, success=True)

Expand Down
3 changes: 3 additions & 0 deletions test/functional/test_framework/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@
# Default setting for -datacarriersize. 80 bytes of data, +1 for OP_RETURN, +2 for the pushdata opcodes.
MAX_OP_RETURN_RELAY = 83

# Static max data for -annexcarrier
MAX_ANNEX_DATA = 126

DEFAULT_MEMPOOL_EXPIRY_HOURS = 336 # hours

def sha256(s):
Expand Down