Skip to content

Commit 09de430

Browse files
committed
fuzz: add second simplicity fuzz test
The first fuzztest takes a Simplicity program and a transaction and directly calls the Simplicity interpreter with some context cobbled together from the transaction. It also tries messing with the budget and computes AMRs to check that the AMR-check works, even though on the blockchain AMRs are never used. It also attempts mangling programs to directly fuzz the parser, type inference and CMR checking. THIS test, on the other hand, takes a transaction, looks for Simplicity programs (or witnesses which look like Simplicity programs), computes their CMRs to produce a correct corresponding scriptPubKey, creates scriptchecks, and executes them. This should do an end-to-end coverage of the whole Simplicity consensus logic, including all the new branches in interpreter.cpp. To produce seeds for this, I have a a local fuzz target which uses rust-simplicity and rust-elements to produce programs, deep Taproot trees, and transactions. I run this to get high coverage, then dump the resulting complete transactions to disk, where they can be used as seeds for this test.
1 parent 788200a commit 09de430

File tree

3 files changed

+246
-9
lines changed

3 files changed

+246
-9
lines changed

src/Makefile.test.include

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,7 @@ test_fuzz_fuzz_SOURCES = \
315315
test/fuzz/signet.cpp \
316316
test/fuzz/simplicity_compute_amr.c \
317317
test/fuzz/simplicity.cpp \
318+
test/fuzz/simplicity_tx.cpp \
318319
test/fuzz/socks5.cpp \
319320
test/fuzz/span.cpp \
320321
test/fuzz/spanparsing.cpp \

src/test/fuzz/simplicity.cpp

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@ extern "C" {
1818
#include <string>
1919
#include <vector>
2020

21-
uint256 GENESIS_HASH;
22-
23-
CConfidentialAsset INPUT_ASSET_UNCONF{};
24-
CConfidentialAsset INPUT_ASSET_CONF{};
25-
CConfidentialValue INPUT_VALUE_UNCONF{};
26-
CConfidentialValue INPUT_VALUE_CONF{};
27-
CScript TAPROOT_SCRIPT_PUB_KEY{};
28-
std::vector<unsigned char> TAPROOT_CONTROL{};
29-
std::vector<unsigned char> TAPROOT_ANNEX(99, 0x50);
21+
static uint256 GENESIS_HASH;
22+
23+
static CConfidentialAsset INPUT_ASSET_UNCONF{};
24+
static CConfidentialAsset INPUT_ASSET_CONF{};
25+
static CConfidentialValue INPUT_VALUE_UNCONF{};
26+
static CConfidentialValue INPUT_VALUE_CONF{};
27+
static CScript TAPROOT_SCRIPT_PUB_KEY{};
28+
static std::vector<unsigned char> TAPROOT_CONTROL{};
29+
static std::vector<unsigned char> TAPROOT_ANNEX(99, 0x50);
3030
//CMutableTransaction MTX_TEMPLATE{};
3131

3232
// Defined in simplicity_compute_amr.c

src/test/fuzz/simplicity_tx.cpp

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
// Copyright (c) 2020 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#include <cstdio>
6+
#include <span.h>
7+
#include <primitives/transaction.h>
8+
#include <script/sigcache.h>
9+
#include <validation.h>
10+
extern "C" {
11+
#include <simplicity/cmr.h>
12+
#include <simplicity/elements/env.h>
13+
#include <simplicity/elements/exec.h>
14+
}
15+
#include <test/fuzz/FuzzedDataProvider.h>
16+
#include <test/fuzz/fuzz.h>
17+
#include <test/fuzz/util.h>
18+
19+
#include <cstdint>
20+
#include <optional>
21+
#include <string>
22+
#include <vector>
23+
24+
static uint256 GENESIS_HASH;
25+
26+
static CConfidentialAsset INPUT_ASSET_UNCONF{};
27+
static CConfidentialAsset INPUT_ASSET_CONF{};
28+
static CConfidentialValue INPUT_VALUE_UNCONF{};
29+
static CConfidentialValue INPUT_VALUE_CONF{};
30+
31+
const unsigned int VERIFY_FLAGS = SCRIPT_VERIFY_NONE
32+
| SCRIPT_VERIFY_P2SH
33+
| SCRIPT_VERIFY_WITNESS
34+
| SCRIPT_VERIFY_DERSIG
35+
| SCRIPT_VERIFY_CHECKLOCKTIMEVERIFY
36+
| SCRIPT_VERIFY_CHECKSEQUENCEVERIFY
37+
| SCRIPT_VERIFY_TAPROOT
38+
| SCRIPT_VERIFY_NULLDUMMY
39+
| SCRIPT_SIGHASH_RANGEPROOF
40+
| SCRIPT_VERIFY_SIMPLICITY;
41+
42+
void initialize_simplicity_tx()
43+
{
44+
g_con_elementsmode = true;
45+
// Copied from init.cpp AppInitMain
46+
InitSignatureCache();
47+
InitScriptExecutionCache();
48+
InitRangeproofCache();
49+
InitSurjectionproofCache();
50+
51+
GENESIS_HASH = uint256S("0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206");
52+
53+
INPUT_VALUE_UNCONF.SetToAmount(12345678);
54+
INPUT_VALUE_CONF.vchCommitment = {
55+
0x08,
56+
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
57+
0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
58+
0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,
59+
0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38,
60+
};
61+
62+
INPUT_ASSET_UNCONF.vchCommitment = INPUT_VALUE_CONF.vchCommitment;
63+
INPUT_ASSET_UNCONF.vchCommitment[0] = 0x01;
64+
INPUT_ASSET_CONF.vchCommitment = INPUT_VALUE_CONF.vchCommitment;
65+
INPUT_ASSET_CONF.vchCommitment[0] = 0x0a;
66+
}
67+
68+
void write_u32(FILE *fh, uint32_t val) {
69+
unsigned char buf[4];
70+
71+
val = htole32(val);
72+
memcpy(buf, &val, 4);
73+
assert(fwrite(buf, 1, 4, fh) == 4);
74+
}
75+
76+
FUZZ_TARGET_INIT(simplicity_tx, initialize_simplicity_tx)
77+
{
78+
simplicity_err error;
79+
80+
// 1. (no-op) run through Rust code
81+
//
82+
// 2. Construct transaction.
83+
CMutableTransaction mtx;
84+
{
85+
CDataStream txds{buffer, SER_NETWORK, INIT_PROTO_VERSION};
86+
try {
87+
txds >> mtx;
88+
} catch (const std::ios_base::failure&) {
89+
return;
90+
}
91+
mtx.witness.vtxoutwit.resize(mtx.vout.size());
92+
93+
// If no inputs have witnesses, all the code below should continue to work -- we
94+
// should be able to call `PrecomputedTransactionData::Init` on a legacy transaction
95+
// without any trouble. In this case it will set txdata.m_simplicity_tx_data to
96+
// NULL, and we won't be able to go any further, but there should be no crashes
97+
// or memory issues.
98+
if (!mtx.witness.vtxinwit.empty()) {
99+
mtx.witness.vtxinwit.resize(mtx.vin.size());
100+
// This is an assertion in the Simplicity interpreter. It is guaranteed
101+
// to hold for anything on the network since (even if validatepegin is off)
102+
// pegins are validated for well-formedness long before the script interpreter
103+
// is invoked. But in this code we just call the interpreter directly without
104+
// these checks.
105+
for (unsigned i = 0; i < mtx.vin.size(); i++) {
106+
if (mtx.vin[i].m_is_pegin && (mtx.witness.vtxinwit[i].m_pegin_witness.stack.size() < 4 || mtx.witness.vtxinwit[i].m_pegin_witness.stack[2].size() != 32)) {
107+
return;
108+
}
109+
}
110+
}
111+
112+
// We use the first vin as a "random oracle" rather than reading more from
113+
// the fuzzer, because we want our fuzz seeds to have as simple a structure
114+
// as possible. This means we must reject 0-input transactions, which are
115+
// invalid on-chain anyway.
116+
if (mtx.vin.size() == 0) {
117+
return;
118+
}
119+
}
120+
const auto& random_bytes = mtx.vin[0].prevout.hash;
121+
122+
// 3. Construct `nIn` and `spent_outs` arrays.
123+
bool expect_simplicity = false;
124+
std::vector<unsigned char[32]> cmrs;
125+
std::vector<CTxOut> spent_outs{};
126+
for (unsigned int i = 0; i < mtx.vin.size(); i++) {
127+
// Null asset or value would assert in the interpreter, and are impossible
128+
// to hit in real transactions. Nonces are not included in the UTXO set and
129+
// therefore don't matter.
130+
CConfidentialValue value = i & 1 ? INPUT_VALUE_CONF : INPUT_VALUE_UNCONF;
131+
CConfidentialAsset asset = i & 2 ? INPUT_ASSET_CONF : INPUT_ASSET_UNCONF;
132+
CScript scriptPubKey;
133+
if (i < random_bytes.size()) {
134+
if (i & 1 && random_bytes.data()[i] & 1) {
135+
value.vchCommitment[0] ^= 1;
136+
}
137+
if (i & 2 && random_bytes.data()[i] & 2) {
138+
asset.vchCommitment[0] ^= 1;
139+
}
140+
}
141+
142+
// Check for size 4: a Simplicity program will always have a witness, program,
143+
// CMR, control block and (maybe) annex, in that order. If the annex is present,
144+
// then checking for size 4 doesn't guarantee that a witness is present, but
145+
// that is ok at this point. (In fact, it is a useful thing to check.)
146+
if (i < mtx.witness.vtxinwit.size()) {
147+
auto& current = mtx.witness.vtxinwit[i].scriptWitness.stack;
148+
if (current.size() >= 4) {
149+
size_t top = current.size();
150+
if (!current[top - 1].empty() && current[top - 1][0] == 0x50) {
151+
--top;
152+
}
153+
const auto& control = current[top - 1];
154+
const auto& program = current[top - 3];
155+
156+
if (control.size() >= TAPROOT_CONTROL_BASE_SIZE && (control[0] & 0xfe) == 0xbe) {
157+
// The fuzzer won't be able to produce a valid CMR on its own, so we compute it
158+
// and jam it into the witness stack. But we do require the fuzzer give us a
159+
// place to put it, so we don't have to resize the stack (and so that actual
160+
// valid transactions will work with this code).
161+
// Compute CMR and do some sanity checks on it (and the program)
162+
std::vector<unsigned char> cmr(32, 0);
163+
assert(cmr.size() == 32); // fuck C++
164+
assert(simplicity_computeCmr(&error, cmr.data(), program.data(), program.size()));
165+
if (error == SIMPLICITY_NO_ERROR) {
166+
const XOnlyPubKey internal{Span{control}.subspan(1, TAPROOT_CONTROL_BASE_SIZE - 1)};
167+
168+
const CScript leaf_script{cmr.begin(), cmr.end()};
169+
const uint256 tapleaf_hash = ComputeTapleafHash(0xbe, leaf_script);
170+
uint256 merkle_root = ComputeTaprootMerkleRoot(control, tapleaf_hash);
171+
auto ret = internal.CreateTapTweak(&merkle_root);
172+
if (ret.has_value()) {
173+
expect_simplicity = true;
174+
//assert(0); // useful for searching for a nontrivial fuzz target
175+
// Just drop the parity; it needs to match the one in the control block,
176+
// but we want to test that logic, so we allow them not to match.
177+
const XOnlyPubKey output_key = ret->first;
178+
if (current[top - 2].size() == 32) {
179+
// FIXME remove this check when we stop using Rust
180+
assert(memcmp(current[top - 2].data(), cmr.data(), 32) == 0);
181+
}
182+
// If we made it here, success (aside from parity maybe)
183+
current[top - 2] = std::move(cmr);
184+
scriptPubKey = CScript() << OP_1 << ToByteVector(output_key);
185+
}
186+
}
187+
}
188+
}
189+
}
190+
// For scripts that we're not using, set them to various witness programs to try to
191+
// trick the interpreter into treating them as taproot or simplicity outputs. It
192+
// should fail but shouldn't crash or anything.
193+
//
194+
// We don't cover all cases, so this may result in the empty scriptpubkey -- this is
195+
// impossible on-chain but it shouldn't hurt anything.
196+
if (scriptPubKey.empty()) {
197+
if (i < random_bytes.size()) {
198+
switch(random_bytes.data()[i] >> 6) {
199+
case 0:
200+
scriptPubKey << OP_TRUE;
201+
break;
202+
case 1:
203+
scriptPubKey << OP_0 << std::vector<unsigned char>(20, 0xab);
204+
break;
205+
case 2:
206+
scriptPubKey << OP_0 << std::vector<unsigned char>(32, 0xcd);
207+
break;
208+
case 3:
209+
scriptPubKey << OP_1 << std::vector<unsigned char>(32, 0xef);
210+
break;
211+
}
212+
}
213+
}
214+
215+
spent_outs.push_back(CTxOut{asset, value, scriptPubKey});
216+
}
217+
assert(spent_outs.size() == mtx.vin.size());
218+
219+
// 4. Test via scriptcheck
220+
PrecomputedTransactionData txdata{GENESIS_HASH};
221+
std::vector<CTxOut> spent_outs_copy{spent_outs};
222+
txdata.Init(mtx, std::move(spent_outs_copy));
223+
if (expect_simplicity) {
224+
// The converse of this is not true -- if !expect_simplicity, it's still possible
225+
// that we will allocate Simplicity data. The check for whether to do this is very
226+
// lax: is this a 34-byte scriptPubKey that starts with OP_1 and does it have a
227+
// nonempty witness.
228+
assert(txdata.m_simplicity_tx_data != NULL);
229+
}
230+
231+
const CTransaction tx{mtx};
232+
for (unsigned i = 0; i < tx.vin.size(); i++) {
233+
CScriptCheck check{txdata.m_spent_outputs[i], tx, i, VERIFY_FLAGS, false /* cache */, &txdata};
234+
check();
235+
}
236+
}

0 commit comments

Comments
 (0)