simplicity: allow exact annex padding in policy

This commit is contained in:
Byron Hambly 2026-03-25 15:10:28 +02:00
parent af697df271
commit 27c0cf5949
No known key found for this signature in database
GPG key ID: DE8F6EA20A661697
7 changed files with 48 additions and 9 deletions

View file

@ -307,6 +307,7 @@ 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
// ELEMENTS: allow annexes for simplicity transactions
if (witnessversion == 1 && witnessprogram.size() == WITNESS_V1_TAPROOT_SIZE && !p2sh) {
// Missing witness; invalid by consensus rules
if (i >= tx.witness.vtxinwit.size()) {
@ -315,8 +316,14 @@ bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs)
// Taproot spend (non-P2SH-wrapped, version 1, witness program size 32; see BIP 341)
Span stack{tx.witness.vtxinwit[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;
SpanPopBack(stack); // drop the annex
const auto& control_block = SpanPopBack(stack);
// Annexes are allowed for Simplicity spends only
// checks for zero padding and exact size are done in CheckSimplicity
if (control_block.empty() || (control_block[0] & TAPROOT_LEAF_MASK) != TAPROOT_LEAF_TAPSIMPLICITY) {
// Annexes are nonstandard as long as no semantics are defined for them.
return false;
}
}
if (stack.size() >= 2) {
// Script path spend (2 or more stack elements after removing optional annex)

View file

@ -129,7 +129,9 @@ static constexpr unsigned int STANDARD_SCRIPT_VERIFY_FLAGS{MANDATORY_SCRIPT_VERI
SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_TAPROOT_VERSION |
SCRIPT_VERIFY_DISCOURAGE_OP_SUCCESS |
SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_PUBKEYTYPE |
SCRIPT_VERIFY_SIMPLICITY};
SCRIPT_VERIFY_SIMPLICITY |
SCRIPT_VERIFY_ANNEX_PADDING};
/** For convenience, standard but not mandatory verify flags. */
static constexpr unsigned int STANDARD_NOT_MANDATORY_VERIFY_FLAGS{STANDARD_SCRIPT_VERIFY_FLAGS & ~MANDATORY_SCRIPT_VERIFY_FLAGS};

View file

@ -3094,14 +3094,14 @@ uint32_t GenericTransactionSignatureChecker<T>::GetnIn() const
}
template <class T>
bool GenericTransactionSignatureChecker<T>::CheckSimplicity(const valtype& program, const valtype& witness, const rawElementsTapEnv& simplicityRawTap, int64_t budget, ScriptError* serror) const
bool GenericTransactionSignatureChecker<T>::CheckSimplicity(const valtype& program, const valtype& witness, const rawElementsTapEnv& simplicityRawTap, int64_t minCost, int64_t budget, ScriptError* serror) const
{
simplicity_err error;
elementsTapEnv* simplicityTapEnv = simplicity_elements_mallocTapEnv(&simplicityRawTap);
assert(txdata->m_simplicity_tx_data);
assert(simplicityTapEnv);
if (!simplicity_elements_execSimplicity(&error, nullptr, txdata->m_simplicity_tx_data.get(), nIn, simplicityTapEnv, txdata->m_hash_genesis_block.data(), 0, budget, nullptr, program.data(), program.size(), witness.data(), witness.size())) {
if (!simplicity_elements_execSimplicity(&error, nullptr, txdata->m_simplicity_tx_data.get(), nIn, simplicityTapEnv, txdata->m_hash_genesis_block.data(), minCost, budget, nullptr, program.data(), program.size(), witness.data(), witness.size())) {
assert(!"simplicity_elements_execSimplicity internal error");
}
simplicity_elements_freeTapEnv(simplicityTapEnv);
@ -3262,9 +3262,11 @@ static bool VerifyWitnessProgram(const CScriptWitness& witness, int witversion,
// BIP341 Taproot: 32-byte non-P2SH witness v1 program (which encodes a P2C-tweaked pubkey)
if (!(flags & SCRIPT_VERIFY_TAPROOT)) return set_success(serror);
if (stack.size() == 0) return set_error(serror, SCRIPT_ERR_WITNESS_PROGRAM_WITNESS_EMPTY);
valtype annex;
if (stack.size() >= 2 && !stack.back().empty() && stack.back()[0] == ANNEX_TAG) {
// Drop annex (this is non-standard; see IsWitnessStandard)
const valtype& annex = SpanPopBack(stack);
// ELEMENTS: store the annex for CheckSimplicity
annex = SpanPopBack(stack);
execdata.m_annex_hash = (HashWriter{} << annex).GetSHA256();
execdata.m_annex_present = true;
} else {
@ -3306,7 +3308,27 @@ static bool VerifyWitnessProgram(const CScriptWitness& witness, int witversion,
simplicityRawTap.controlBlock = control.data();
simplicityRawTap.pathLen = (control.size() - TAPROOT_CONTROL_BASE_SIZE) / TAPROOT_CONTROL_NODE_SIZE;
simplicityRawTap.scriptCMR = script.data();
return checker.CheckSimplicity(simplicity_program, simplicity_witness, simplicityRawTap, budget, serror);
// If there is no annex, or we are in consensus checking mode, minCost is set to 0 which effectively disables any overweight cost checks.
int64_t minCost = 0;
if ((flags & SCRIPT_VERIFY_ANNEX_PADDING) && annex.size() > 0) {
valtype zero_padding(annex.size(), 0);
zero_padding[0] = ANNEX_TAG;
if (annex != zero_padding) {
return set_error(serror, SCRIPT_ERR_SIMPLICITY_PADDING_NONZERO);
}
// Compute what the budget would have been without the padding.
// budget includes the padding cost, so subtracting this stack item won't underflow.
minCost = budget - ::GetSerializeSize(annex);
// If the annex exists and is empty (i.e. its size is 1), then the only way the annex could be smaller is by eliminating it entirely.
// So we use the above computed value for minCost. Note: in that case the minCost is 2 WU less than the budget.
// If the annex exists and is non-empty, then we add to minCost the value of an annex that contains one fewer byte.
if (zero_padding.size() > 1) {
zero_padding.pop_back();
minCost += ::GetSerializeSize(zero_padding);
}
}
return checker.CheckSimplicity(simplicity_program, simplicity_witness, simplicityRawTap, minCost, budget, serror);
}
if (flags & SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_TAPROOT_VERSION) {
return set_error(serror, SCRIPT_ERR_DISCOURAGE_UPGRADABLE_TAPROOT_VERSION);

View file

@ -161,6 +161,10 @@ enum : uint32_t {
//
SCRIPT_VERIFY_SIMPLICITY = (1U << 23),
// Check exact annex padding policy for simplicity spends
//
SCRIPT_VERIFY_ANNEX_PADDING = (1U << 24),
// Constants to point to the highest flag in use. Add new flags above this line.
//
SCRIPT_VERIFY_END_MARKER
@ -345,7 +349,7 @@ public:
return std::numeric_limits<uint32_t>::max();
}
virtual bool CheckSimplicity(const std::vector<unsigned char>& witness, const std::vector<unsigned char>& program, const rawElementsTapEnv& simplicityRawTap, int64_t budget, ScriptError* serror) const
virtual bool CheckSimplicity(const std::vector<unsigned char>& witness, const std::vector<unsigned char>& program, const rawElementsTapEnv& simplicityRawTap, int64_t minCost, int64_t budget, ScriptError* serror) const
{
return false;
}
@ -393,7 +397,7 @@ public:
const PrecomputedTransactionData* GetPrecomputedTransactionData() const override;
uint32_t GetnIn() const override;
bool CheckSimplicity(const std::vector<unsigned char>& program, const std::vector<unsigned char>& witness, const rawElementsTapEnv& simplicityRawTap, int64_t budget, ScriptError* serror) const override;
bool CheckSimplicity(const std::vector<unsigned char>& program, const std::vector<unsigned char>& witness, const rawElementsTapEnv& simplicityRawTap, int64_t minCost, int64_t budget, ScriptError* serror) const override;
};
using TransactionSignatureChecker = GenericTransactionSignatureChecker<CTransaction>;

View file

@ -134,6 +134,8 @@ std::string ScriptErrorString(const ScriptError serror)
return "EC scalar mult verify fail";
case SCRIPT_ERR_SIMPLICITY_WRONG_LENGTH:
return "Simplicity witness has incorrect length";
case SCRIPT_ERR_SIMPLICITY_PADDING_NONZERO:
return "Simplicity annex padding must be all zeros";
case SCRIPT_ERR_SIMPLICITY_DATA_OUT_OF_RANGE:
return SIMPLICITY_ERR_MSG(SIMPLICITY_ERR_DATA_OUT_OF_RANGE);
case SCRIPT_ERR_SIMPLICITY_DATA_OUT_OF_ORDER:

View file

@ -99,6 +99,7 @@ typedef enum ScriptError_t
/* Elements: Simplicity related errors */
SCRIPT_ERR_SIMPLICITY_WRONG_LENGTH,
SCRIPT_ERR_SIMPLICITY_PADDING_NONZERO,
SCRIPT_ERR_SIMPLICITY_NOT_YET_IMPLEMENTED,
SCRIPT_ERR_SIMPLICITY_DATA_OUT_OF_RANGE,
SCRIPT_ERR_SIMPLICITY_DATA_OUT_OF_ORDER,

View file

@ -71,6 +71,7 @@ static std::map<std::string, unsigned int> mapFlagNames = {
{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("SIMPLICITY"), (unsigned int)SCRIPT_VERIFY_SIMPLICITY},
{std::string("ANNEX_PADDING"), (unsigned int)SCRIPT_VERIFY_ANNEX_PADDING},
};
unsigned int ParseScriptFlags(std::string strFlags)