Skip to content

Commit 4a9b55e

Browse files
committed
policy: add checks for exact annex padding of simplicity programs
1 parent 5bbc40f commit 4a9b55e

3 files changed

Lines changed: 117 additions & 0 deletions

File tree

src/policy/policy.cpp

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@
1414
#include <span.h>
1515
#include <chainparams.h> // Peg-out enforcement
1616

17+
extern "C" {
18+
#include <simplicity/bounded.h>
19+
#include <simplicity/elements/cost.h>
20+
#include <simplicity/errorCodes.h>
21+
}
22+
1723
// ELEMENTS:
1824
CAsset policyAsset;
1925

@@ -274,6 +280,7 @@ bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs)
274280
// Check policy limits for Taproot spends:
275281
// - MAX_STANDARD_TAPSCRIPT_STACK_ITEM_SIZE limit for stack item size
276282
// - No annexes
283+
// ELEMENTS: allow annexes for simplicity transactions
277284
if (witnessversion == 1 && witnessprogram.size() == WITNESS_V1_TAPROOT_SIZE && !p2sh) {
278285
// Missing witness; invalid by consensus rules
279286
if (i >= tx.witness.vtxinwit.size()) {
@@ -282,8 +289,16 @@ bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs)
282289
// Taproot spend (non-P2SH-wrapped, version 1, witness program size 32; see BIP 341)
283290
Span stack{tx.witness.vtxinwit[i].scriptWitness.stack};
284291
if (stack.size() >= 2 && !stack.back().empty() && stack.back()[0] == ANNEX_TAG) {
292+
SpanPopBack(stack); // Ignore annex
293+
const auto& control_block = SpanPopBack(stack);
294+
if ((control_block[0] & TAPROOT_LEAF_MASK) == TAPROOT_LEAF_TAPSIMPLICITY) {
295+
// Annexes are allowed for Simplicity spends
296+
// checks for zero padding and exact size are done in ExactAnnexPadding
297+
return true;
298+
} else {
285299
// Annexes are nonstandard as long as no semantics are defined for them.
286300
return false;
301+
}
287302
}
288303
if (stack.size() >= 2) {
289304
// Script path spend (2 or more stack elements after removing optional annex)
@@ -340,3 +355,92 @@ int64_t GetVirtualTransactionInputSize(const CTransaction& tx, const size_t nIn,
340355
{
341356
return GetVirtualTransactionSize(GetTransactionInputWeight(tx, nIn), nSigOpCost, bytes_per_sigop);
342357
}
358+
359+
bool ExactAnnexPadding(const CTransaction& tx, const CCoinsViewCache& mapInputs, std::string& reason)
360+
{
361+
if (tx.IsCoinBase())
362+
return true;
363+
364+
for (unsigned int i = 0; i < tx.vin.size(); i++)
365+
{
366+
if (tx.witness.vtxinwit.size() <= i || tx.witness.vtxinwit[i].scriptWitness.IsNull())
367+
continue;
368+
369+
const CTxOut &prev = tx.vin[i].m_is_pegin ? GetPeginOutputFromWitness(tx.witness.vtxinwit[i].m_pegin_witness) : mapInputs.AccessCoin(tx.vin[i].prevout).out;
370+
371+
CScript prevScript = prev.scriptPubKey;
372+
373+
// Skip P2SH
374+
if (prevScript.IsPayToScriptHash())
375+
continue;
376+
377+
int witnessversion = 0;
378+
std::vector<unsigned char> witnessprogram;
379+
if (!prevScript.IsWitnessProgram(witnessversion, witnessprogram))
380+
continue;
381+
382+
// Only check taproot v1 spends
383+
if (witnessversion != 1 || witnessprogram.size() != WITNESS_V1_TAPROOT_SIZE)
384+
continue;
385+
386+
Span stack{tx.witness.vtxinwit[i].scriptWitness.stack};
387+
388+
// Check for annex
389+
if (stack.size() < 2 || stack.back().empty() || stack.back()[0] != ANNEX_TAG)
390+
continue;
391+
392+
const auto& annex = SpanPopBack(stack);
393+
394+
// Need at least 2 more elements (control block + script) for script path spend
395+
if (stack.size() < 2)
396+
continue;
397+
398+
const auto& control_block = SpanPopBack(stack);
399+
if (control_block.empty())
400+
continue;
401+
402+
// Only check Simplicity spends (leaf version 0xbe)
403+
if ((control_block[0] & TAPROOT_LEAF_MASK) != TAPROOT_LEAF_TAPSIMPLICITY)
404+
continue;
405+
406+
// All annex bytes after 0x50 tag must be zero
407+
std::vector<unsigned char> zero_padding(annex.size(), 0);
408+
zero_padding[0] = ANNEX_TAG;
409+
if (annex != zero_padding) {
410+
reason = "bad-annex-nonzero-padding";
411+
return false;
412+
}
413+
414+
// Annex padding must provide exactly the right budget for the program's cost bound.
415+
// The program is the second-to-last stack item (after popping CMR script and control block).
416+
if (stack.size() < 1)
417+
continue;
418+
419+
SpanPopBack(stack); // drop the CMR
420+
if (stack.size() < 1)
421+
continue;
422+
423+
const auto& simplicity_program = SpanPopBack(stack);
424+
425+
// Compute the program's cost bound (in milli weight units)
426+
simplicity_err error;
427+
ubounded cost_bound;
428+
if (!simplicity_elements_computeCostBound(&error, &cost_bound, simplicity_program.data(), simplicity_program.size())) {
429+
reason = "bad-annex-compute-cost";
430+
return false;
431+
}
432+
433+
// Budget = serialized witness size + VALIDATION_WEIGHT_OFFSET
434+
// cost_bound is in milliWU, budget is in WU
435+
// For exact padding: budget must equal ceil(cost_bound / 1000)
436+
int64_t required_budget = ((int64_t)cost_bound + 999) / 1000;
437+
int64_t actual_budget = ::GetSerializeSize(tx.witness.vtxinwit[i].scriptWitness.stack, PROTOCOL_VERSION) + VALIDATION_WEIGHT_OFFSET;
438+
439+
if (actual_budget != required_budget) {
440+
reason = "bad-annex-padding-size";
441+
return false;
442+
}
443+
}
444+
445+
return true;
446+
}

src/policy/policy.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,13 @@ bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs)
134134
*/
135135
bool IsIssuanceInMoneyRange(const CTransaction& tx);
136136

137+
/**
138+
* Check that Simplicity transactions have exact annex padding:
139+
* - Annex bytes (after 0x50 tag) must all be zero
140+
* - Annex must be exactly the right size for the program's cost bound
141+
*/
142+
bool ExactAnnexPadding(const CTransaction& tx, const CCoinsViewCache& mapInputs, std::string& reason);
143+
137144
/** Compute the virtual transaction size (weight reinterpreted as bytes). */
138145
int64_t GetVirtualTransactionSize(int64_t nWeight, int64_t nSigOpCost, unsigned int bytes_per_sigop);
139146
int64_t GetVirtualTransactionSize(const CTransaction& tx, int64_t nSigOpCost, unsigned int bytes_per_sigop);

src/validation.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1016,6 +1016,12 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
10161016
if (tx.HasWitness() && fRequireStandard && !IsWitnessStandard(tx, m_view))
10171017
return state.Invalid(TxValidationResult::TX_WITNESS_MUTATED, "bad-witness-nonstandard");
10181018

1019+
// Check Simplicity transactions for exact annex padding
1020+
std::string annex_reason;
1021+
if (tx.HasWitness() && fRequireStandard && !ExactAnnexPadding(tx, m_view, annex_reason)) {
1022+
return state.Invalid(TxValidationResult::TX_NOT_STANDARD, annex_reason);
1023+
}
1024+
10191025
int64_t nSigOpsCost = GetTransactionSigOpCost(tx, m_view, STANDARD_SCRIPT_VERIFY_FLAGS);
10201026

10211027
// We only consider policyAsset

0 commit comments

Comments
 (0)