Skip to content

Commit 878ea82

Browse files
committed
qa: unit tests to sanity check OP_TEMPLATEHASH
Sanity check the template hash by using it to commit to the transaction that must spend an output. Malleating committed fields must lead to a consensus failure, and changing non-committed fields is fine. We also add the option to generate test vectors from this unit test.
1 parent 48a3294 commit 878ea82

File tree

2 files changed

+325
-0
lines changed

2 files changed

+325
-0
lines changed

src/test/script_tests.cpp

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include <common/system.h>
99
#include <core_io.h>
1010
#include <key.h>
11+
#include <policy/policy.h>
1112
#include <rpc/util.h>
1213
#include <script/script.h>
1314
#include <script/script_error.h>
@@ -18,6 +19,7 @@
1819
#include <streams.h>
1920
#include <test/util/json.h>
2021
#include <test/util/random.h>
22+
#include <test/util/script.h>
2123
#include <test/util/setup_common.h>
2224
#include <test/util/transaction_utils.h>
2325
#include <util/fs.h>
@@ -1747,4 +1749,288 @@ BOOST_AUTO_TEST_CASE(compute_tapleaf)
17471749
BOOST_CHECK_EQUAL(ComputeTapleafHash(0xc2, std::span(script)), tlc2);
17481750
}
17491751

1752+
/** A test vector for OP_TEMPLATEHASH. */
1753+
struct TemplateHashTestCase
1754+
{
1755+
//! The transaction being validated.
1756+
const CTransaction spending_tx;
1757+
//! The outputs spent by the transaction being validated.
1758+
const std::vector<CTxOut> spent_outputs;
1759+
//! The index of the transaction input being validated.
1760+
const uint32_t input_index;
1761+
//! Whether script validation is expected to succeed.
1762+
const bool valid;
1763+
//! Description of the test vector.
1764+
const std::string comment;
1765+
1766+
explicit TemplateHashTestCase(std::vector<CTxOut> spent_txos, CTransaction tx, uint32_t idx, bool val, std::string com):
1767+
spending_tx{std::move(tx)}, spent_outputs{std::move(spent_txos)}, input_index{idx}, valid{val}, comment{std::move(com)} {}
1768+
1769+
UniValue GetJson() const
1770+
{
1771+
UniValue json{UniValue::VOBJ}, spent_txos{UniValue::VARR};
1772+
for (const auto& txo: spent_outputs) {
1773+
DataStream ssTxo;
1774+
ssTxo << txo;
1775+
spent_txos.push_back(HexStr(ssTxo));
1776+
}
1777+
json.pushKV("spent_outputs", std::move(spent_txos));
1778+
json.pushKV("spending_tx", EncodeHexTx(spending_tx));
1779+
json.pushKV("input_index", input_index);
1780+
json.pushKV("valid", valid);
1781+
json.pushKV("comment", comment);
1782+
return json;
1783+
}
1784+
};
1785+
1786+
/** Shorthand for making a copy of a vector. **/
1787+
template<typename T>
1788+
static std::vector<T> Clone(std::vector<T>& vec)
1789+
{
1790+
return std::vector<T>{vec};
1791+
}
1792+
1793+
/** Run script validation for provided tx and input index. Check it succeeds/fails according to provided validity status. **/
1794+
static void CheckTemplateMatch(const CMutableTransaction& tx, std::vector<CTxOut> spent_outputs, unsigned in_index,
1795+
bool is_valid, std::vector<TemplateHashTestCase>& cases, std::string comment)
1796+
{
1797+
Assert(tx.vin.size() == spent_outputs.size() && in_index < tx.vin.size());
1798+
constexpr unsigned FLAGS{MANDATORY_SCRIPT_VERIFY_FLAGS | SCRIPT_VERIFY_TEMPLATEHASH};
1799+
constexpr CAmount dummy_am{0}; // We never check signatures.
1800+
constexpr auto dummy_mdb{MissingDataBehavior::ASSERT_FAIL};
1801+
const auto spent_spk{spent_outputs[in_index].scriptPubKey};
1802+
1803+
// Record for test vector generation.
1804+
cases.emplace_back(spent_outputs, CTransaction{tx}, in_index, is_valid, comment);
1805+
1806+
// Perform script validation with the provided inputs.
1807+
PrecomputedTransactionData precomp;
1808+
precomp.Init(tx, std::move(spent_outputs));
1809+
const auto checker{GenericTransactionSignatureChecker(&tx, in_index, dummy_am, precomp, dummy_mdb)};
1810+
ScriptError err;
1811+
bool res{VerifyScript(tx.vin[in_index].scriptSig, spent_spk, &tx.vin[in_index].scriptWitness, FLAGS, checker, &err)};
1812+
1813+
// Check script validation result, if not valid make sure the failure is the one expected for `<hash> OP_TEMPLATEHASH OP_EQUAL`.
1814+
BOOST_CHECK_MESSAGE(res == is_valid, std::string{"Script validation unexpectedly "} + (res ? "succeeded" : "failed") + ": " + comment);
1815+
if (!is_valid) {
1816+
BOOST_CHECK_MESSAGE(err == ScriptError::SCRIPT_ERR_EVAL_FALSE, std::string{"Unexpected error for '"} + comment + "': " + ScriptErrorString(err));
1817+
}
1818+
}
1819+
1820+
/** Sanity check next-transaction commitments using OP_TEMPLATEHASH. */
1821+
BOOST_AUTO_TEST_CASE(templatehash)
1822+
{
1823+
// Record the various cases exercised in this test to optionally generate vectors at the end.
1824+
std::vector<TemplateHashTestCase> test_cases;
1825+
1826+
// The transaction whose template hash is to be checked.
1827+
CMutableTransaction tx;
1828+
tx.vin = {
1829+
CTxIn{*Txid::FromHex("0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9"), 21},
1830+
CTxIn{*Txid::FromHex("f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"), 12}
1831+
};
1832+
tx.vout.emplace_back(424242, ScriptFromHex("001482074bdf6ce32b071dd120a17cf99cbc01ad3080"));
1833+
1834+
// Construct the script to be spent, checking the (valid) hash of this transaction.
1835+
const uint256 vanilla_hash{GetTemplateHash(tx, 0)};
1836+
const auto leaf_script{CScript() << vanilla_hash << OP_TEMPLATEHASH << OP_EQUAL};
1837+
TaprootBuilder builder;
1838+
builder.Add(0, leaf_script, TAPROOT_LEAF_TAPSCRIPT);
1839+
builder.Finalize(XOnlyPubKey::NUMS_H);
1840+
const CScript spent_spk{GetScriptForDestination(builder.GetOutput())};
1841+
1842+
// Outputs spent by the transaction whose template hash is to be checked.
1843+
CTxOut dummy_txo{1'085'986, ScriptFromHex("76a914079ded3e3befdab0757fe0e8842aeffc0ff2160288ac")};
1844+
std::vector<CTxOut> spent_outputs{{CTxOut{424243, spent_spk}, dummy_txo}};
1845+
1846+
// Construct the witness data for the transaction.
1847+
const auto spend_data{builder.GetSpendData()};
1848+
const auto control_blocks{spend_data.scripts.begin()->second};
1849+
const auto& cb{*control_blocks.begin()};
1850+
tx.vin[0].scriptWitness.stack.emplace_back(leaf_script.begin(), leaf_script.end());
1851+
tx.vin[0].scriptWitness.stack.emplace_back(cb.begin(), cb.end());
1852+
1853+
// Script validation must pass when we use the right hash for the right input.
1854+
{
1855+
CheckTemplateMatch(tx, Clone(spent_outputs), 0, /*is_valid=*/true, test_cases, std::string{"Template hash matches. Input index matches."});
1856+
}
1857+
1858+
// Script validation must pass when we use the right hash for the wrong input.
1859+
{
1860+
// Swap the two inputs and corresponding spent utxos.
1861+
CMutableTransaction tx2{tx};
1862+
std::swap(tx2.vin[0], tx2.vin[1]);
1863+
auto spent_outputs2{spent_outputs};
1864+
std::swap(spent_outputs2[0], spent_outputs2[1]);
1865+
1866+
CheckTemplateMatch(tx2, std::move(spent_outputs2), 1, /*is_valid=*/false, test_cases, std::string{"Template hash matches. Input index mismatches."});
1867+
}
1868+
1869+
// Script validation must fail if any committed field is malleated in the transaction.
1870+
// Version:
1871+
{
1872+
CMutableTransaction tx2{tx};
1873+
tx2.version = 42;
1874+
CheckTemplateMatch(tx2, Clone(spent_outputs), 0, /*is_valid=*/false, test_cases, std::string{"Template hash mismatches: incorrect transaction version."});
1875+
}
1876+
// Locktime:
1877+
{
1878+
CMutableTransaction tx2{tx};
1879+
tx2.nLockTime = 42;
1880+
CheckTemplateMatch(tx2, Clone(spent_outputs), 0, /*is_valid=*/false, test_cases, std::string{"Template hash mismatches: incorrect transaction locktime."});
1881+
}
1882+
// Output value:
1883+
{
1884+
CMutableTransaction tx2{tx};
1885+
tx2.vout[0].nValue++;
1886+
CheckTemplateMatch(tx2, Clone(spent_outputs), 0, /*is_valid=*/false, test_cases, std::string{"Template hash mismatches: incorrect output value."});
1887+
}
1888+
// Output script:
1889+
{
1890+
CMutableTransaction tx2{tx};
1891+
tx2.vout[0].scriptPubKey = ScriptFromHex("001482074bdf6ce32b071dd120a17cf99cbc01ad3081");
1892+
CheckTemplateMatch(tx2, Clone(spent_outputs), 0, /*is_valid=*/false, test_cases, std::string{"Template hash mismatches: incorrect output script."});
1893+
}
1894+
// This input's sequence:
1895+
{
1896+
CMutableTransaction tx2{tx};
1897+
tx2.vin[0].nSequence = 42;
1898+
CheckTemplateMatch(tx2, Clone(spent_outputs), 0, /*is_valid=*/false, test_cases, std::string{"Template hash mismatches: incorrect sequence in spending input."});
1899+
}
1900+
// Another input's sequence:
1901+
{
1902+
CMutableTransaction tx2{tx};
1903+
tx2.vin[1].nSequence = 42;
1904+
CheckTemplateMatch(tx2, Clone(spent_outputs), 0, /*is_valid=*/false, test_cases, std::string{"Template hash mismatches: incorrect sequence in another input."});
1905+
}
1906+
// This input's annex:
1907+
{
1908+
CMutableTransaction tx2{tx};
1909+
tx2.vin[0].scriptWitness.stack.push_back({ANNEX_TAG, 0});
1910+
CheckTemplateMatch(tx2, Clone(spent_outputs), 0, /*is_valid=*/false, test_cases, std::string{"Template hash mismatches: spending input contains annex but none was committed."});
1911+
}
1912+
1913+
// Script validation must succeed if a non-committed transaction field is malleated.
1914+
// Another input's annex:
1915+
{
1916+
CMutableTransaction tx2{tx};
1917+
tx2.vin[1].scriptWitness.stack.push_back({ANNEX_TAG, 'd', 'u', 'm', 'm', 'y'});
1918+
CheckTemplateMatch(tx2, Clone(spent_outputs), 0, /*is_valid=*/true, test_cases, std::string{"Template hash matches with malleated annex for another input."});
1919+
}
1920+
// Another input's scriptsig:
1921+
{
1922+
CMutableTransaction tx2{tx};
1923+
tx2.vin[1].scriptSig.resize(1);
1924+
CheckTemplateMatch(tx2, Clone(spent_outputs), 0, /*is_valid=*/true, test_cases, std::string{"Template hash matches with malleated scriptSig for another input."});
1925+
}
1926+
// This input's prevout:
1927+
COutPoint dummy_op{*Txid::FromHex("27c4d937dca276fb2b61e579902e8a876fd5b5abc17590410ced02d5a9f8e483"), 42};
1928+
{
1929+
CMutableTransaction tx2{tx};
1930+
tx2.vin[0].prevout = dummy_op;
1931+
CheckTemplateMatch(tx2, Clone(spent_outputs), 0, /*is_valid=*/true, test_cases, std::string{"Template hash matches with malleated prevout for spending input."});
1932+
}
1933+
// Another input's prevout:
1934+
{
1935+
CMutableTransaction tx2{tx};
1936+
tx2.vin[1].prevout = dummy_op;
1937+
CheckTemplateMatch(tx2, Clone(spent_outputs), 0, /*is_valid=*/true, test_cases, std::string{"Template hash matches with malleated prevout for another input."});
1938+
}
1939+
// Spent output's value:
1940+
{
1941+
std::vector<CTxOut> spent_outputs2(spent_outputs);
1942+
++spent_outputs2[0].nValue;
1943+
CheckTemplateMatch(tx, std::move(spent_outputs2), 0, /*is_valid=*/true, test_cases, std::string{"Template hash matches with malleated value for corresponding spent output."});
1944+
}
1945+
// Other spent output's value:
1946+
{
1947+
std::vector<CTxOut> spent_outputs2(spent_outputs);
1948+
++spent_outputs2[1].nValue;
1949+
CheckTemplateMatch(tx, std::move(spent_outputs2), 0, /*is_valid=*/true, test_cases, std::string{"Template hash matches with malleated value for other spent output."});
1950+
}
1951+
// Other spent output's scriptpubkey:
1952+
{
1953+
std::vector<CTxOut> spent_outputs2(spent_outputs);
1954+
spent_outputs2[1].scriptPubKey = ScriptFromHex("0014266a4832c001885db26e853ef1d1dde840f7dbaf");
1955+
CheckTemplateMatch(tx, std::move(spent_outputs2), 0, /*is_valid=*/true, test_cases, std::string{"Template hash matches with malleated scriptpubkey for other spent output."});
1956+
}
1957+
1958+
// Same transaction but different hash
1959+
{
1960+
CMutableTransaction tx2{tx};
1961+
auto spent_outputs2{spent_outputs};
1962+
1963+
// Re-build the scriptpubkey with a leaf script containing the check against a dummy hash instead.
1964+
std::vector<uint8_t> dummy_hash(32);
1965+
const auto leaf_script2{CScript() << dummy_hash << OP_TEMPLATEHASH << OP_EQUAL};
1966+
TaprootBuilder builder2;
1967+
builder2.Add(0, leaf_script2, TAPROOT_LEAF_TAPSCRIPT);
1968+
builder2.Finalize(XOnlyPubKey::NUMS_H);
1969+
spent_outputs2[0].scriptPubKey = GetScriptForDestination(builder2.GetOutput());
1970+
1971+
// Re-build the spending transaction's witness.
1972+
const auto spend_data{builder2.GetSpendData()};
1973+
const auto control_blocks{spend_data.scripts.begin()->second};
1974+
const auto& cb{*control_blocks.begin()};
1975+
tx2.vin[0].scriptWitness.stack.clear();
1976+
tx2.vin[0].scriptWitness.stack.emplace_back(leaf_script2.begin(), leaf_script2.end());
1977+
tx2.vin[0].scriptWitness.stack.emplace_back(cb.begin(), cb.end());
1978+
1979+
// Verification must fail, the hash does not correspond to this transaction.
1980+
CheckTemplateMatch(tx2, std::move(spent_outputs2), 0, /*is_valid=*/false, test_cases, std::string{"Template hash mismatches: spending a script with a different committed hash."});
1981+
}
1982+
1983+
// We can also commit to a transaction which contains an annex.
1984+
{
1985+
CMutableTransaction tx2{tx};
1986+
auto spent_outputs2{spent_outputs};
1987+
1988+
// Set an annex to the transaction.
1989+
std::vector<uint8_t> annex{ANNEX_TAG, 'd', 'a', 't', 'a'};
1990+
tx2.vin[0].scriptWitness.stack.push_back(annex);
1991+
1992+
// Sanity check this transaction isn't valid according to our previous commitment (but don't
1993+
// record it, as this error already was above).
1994+
std::vector<TemplateHashTestCase> dummy_cases;
1995+
CheckTemplateMatch(tx2, Clone(spent_outputs2), 0, /*is_valid=*/false, dummy_cases, std::string{});
1996+
1997+
// Re-build the scriptpubkey with a leaf script containing the template hash committing to the annex.
1998+
const uint256 hash_with_annex{GetTemplateHash(tx2, 0)};
1999+
const auto leaf_script2{CScript() << hash_with_annex << OP_TEMPLATEHASH << OP_EQUAL};
2000+
TaprootBuilder builder2;
2001+
builder2.Add(0, leaf_script2, TAPROOT_LEAF_TAPSCRIPT);
2002+
builder2.Finalize(XOnlyPubKey::NUMS_H);
2003+
spent_outputs2[0].scriptPubKey = GetScriptForDestination(builder2.GetOutput());
2004+
2005+
// Re-build the spending transaction's witness.
2006+
const auto spend_data{builder2.GetSpendData()};
2007+
const auto control_blocks{spend_data.scripts.begin()->second};
2008+
const auto& cb{*control_blocks.begin()};
2009+
tx2.vin[0].scriptWitness.stack.clear();
2010+
tx2.vin[0].scriptWitness.stack.emplace_back(leaf_script2.begin(), leaf_script2.end());
2011+
tx2.vin[0].scriptWitness.stack.emplace_back(cb.begin(), cb.end());
2012+
tx2.vin[0].scriptWitness.stack.emplace_back(annex.begin(), annex.end());
2013+
2014+
// Verification must pass if the commitment is for the transaction with this annex.
2015+
CheckTemplateMatch(tx2, Clone(spent_outputs2), 0, /*is_valid=*/true, test_cases, std::string{"Template hash matches in the presence of an annex."});
2016+
2017+
// But must fail for a transaction with another annex.
2018+
std::vector<uint8_t> other_annex{ANNEX_TAG, 'J', 'P', 'E', 'G'};
2019+
tx2.vin[0].scriptWitness.stack.back() = other_annex;
2020+
CheckTemplateMatch(tx2, std::move(spent_outputs2), 0, /*is_valid=*/false, test_cases, std::string{"Template hash mismatches: spending with a different annex than that committed."});
2021+
}
2022+
2023+
// Optionally dump test vectors as JSON. Uncomment UPDATE_JSON_TESTS at the top of this file to use.
2024+
#ifdef UPDATE_JSON_TESTS
2025+
UniValue json_cases{UniValue::VARR};
2026+
for (const auto& test_case: test_cases) {
2027+
json_cases.push_back(test_case.GetJson());
2028+
}
2029+
const auto json_str{JSONPrettyPrint(json_cases)};
2030+
FILE* file = fsbridge::fopen("templatehash_tests.json.gen", "w");
2031+
fputs(json_str.c_str(), file);
2032+
fclose(file);
2033+
#endif
2034+
}
2035+
17502036
BOOST_AUTO_TEST_SUITE_END()

src/test/util/script.h

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
#define BITCOIN_TEST_UTIL_SCRIPT_H
77

88
#include <crypto/sha256.h>
9+
#include <script/interpreter.h>
910
#include <script/script.h>
11+
#include <util/check.h>
1012

1113
static const std::vector<uint8_t> WITNESS_STACK_ELEM_OP_TRUE{uint8_t{OP_TRUE}};
1214
static const CScript P2WSH_OP_TRUE{
@@ -33,4 +35,41 @@ static const std::vector<std::vector<uint8_t>> P2WSH_EMPTY_TWO_STACK{{static_cas
3335
/** Flags that are not forbidden by an assert in script validation */
3436
bool IsValidFlagCombination(unsigned flags);
3537

38+
/** Helper to compute the template hash of a transaction as computed by OP_TEMPLATEHASH. */
39+
template<class T>
40+
uint256 GetTemplateHash(const T& tx, unsigned int in_index, std::vector<uint8_t>* out_annex = nullptr)
41+
{
42+
Assert(in_index < tx.vin.size());
43+
44+
// Initialize the precomputed fields used in computing the template hash.
45+
PrecomputedTransactionData precomp;
46+
{
47+
std::vector<CTxOut> dummy_spent(tx.vin.size());
48+
precomp.Init(tx, std::move(dummy_spent), /*force=*/true);
49+
}
50+
assert(precomp.m_bip341_taproot_ready);
51+
52+
// Detect the presence of an annex at the specified input.
53+
ScriptExecutionData execdata;
54+
execdata.m_annex_present = false;
55+
const auto& stack{tx.vin[in_index].scriptWitness.stack};
56+
if (!stack.empty()) {
57+
const auto& top_elem{tx.vin[in_index].scriptWitness.stack.back()};
58+
execdata.m_annex_present = !top_elem.empty() && top_elem[0] == ANNEX_TAG;
59+
if (execdata.m_annex_present) {
60+
execdata.m_annex_hash = (HashWriter{} << top_elem).GetSHA256();
61+
if (out_annex) {
62+
*out_annex = top_elem;
63+
}
64+
}
65+
}
66+
execdata.m_annex_init = true;
67+
68+
// Compute the template hash.
69+
const CAmount dummy_am{0};
70+
const auto dummy_mdb{MissingDataBehavior::ASSERT_FAIL};
71+
const auto checker{GenericTransactionSignatureChecker(&tx, in_index, dummy_am, precomp, dummy_mdb)};
72+
return checker.GetTemplateHash(execdata);
73+
}
74+
3675
#endif // BITCOIN_TEST_UTIL_SCRIPT_H

0 commit comments

Comments
 (0)