bulletproof_tests.cpp raw
1 // Copyright (c) 2025 The Limenka 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 <crypto/bulletproofs.h>
6 #include <crypto/bignum.h>
7 #include <random.h>
8 #include <script/interpreter.h>
9 #include <script/script.h>
10 #include <uint256.h>
11
12 #include <boost/test/unit_test.hpp>
13
14 BOOST_AUTO_TEST_SUITE(bulletproof_tests)
15
16 static BPScalar RandScalar(FastRandomContext& rng)
17 {
18 auto bytes = rng.randbytes(BP_SCALAR_SIZE);
19 return BPScalar(bytes.begin(), bytes.end());
20 }
21
22 // Serialize a proof into the canonical byte format.
23 static std::vector<uint8_t> SerializeProof(const Bulletproof& p)
24 {
25 std::vector<uint8_t> out;
26 auto append = [&](const std::vector<uint8_t>& v) { out.insert(out.end(), v.begin(), v.end()); };
27 append(p.A); append(p.S); append(p.T1); append(p.T2);
28 append(p.t_hat); append(p.taux); append(p.mu);
29 for (size_t i = 0; i < BP_ROUNDS; i++) { append(p.L[i]); append(p.R[i]); }
30 append(p.a); append(p.b);
31 return out;
32 }
33
34 BOOST_AUTO_TEST_CASE(commit_amount_homomorphic)
35 {
36 InitBulletproofGenerators();
37 // Generators must be valid, pairwise distinct, and NUMS (H != G).
38 BOOST_CHECK(VerifyBulletproofGenerators());
39
40 FastRandomContext rng{uint256{1}};
41 BPScalar r = RandScalar(rng);
42 BPCommitment c;
43 BOOST_CHECK(CommitAmount(123456789, r, c));
44 BOOST_CHECK_EQUAL(c.size(), size_t(BP_POINT_SIZE));
45 }
46
47 BOOST_AUTO_TEST_CASE(proof_roundtrip)
48 {
49 FastRandomContext rng{uint256{2}};
50 // Satoshi-scale and sub-satoshi-scale values (attosat convention:
51 // satoshis * 10^18 + fraction). 128-bit range exercises the top bits.
52 const __int128 SAT_SCALE = 1000000000000000000LL;
53 const std::vector<__int128> amounts = {
54 0, 1, 2, 50000000000ULL,
55 2100000000000000ULL,
56 (__int128)UINT64_MAX / 2, UINT64_MAX,
57 SAT_SCALE + 12345, // 1 sat + 12345 attosats
58 21000000LL * SAT_SCALE, // 21M satoshis
59 ((__int128)1 << 110) + 7, // high 128-bit range
60 (__int128)1 << 120, // near the top of the range
61 };
62 for (__int128 amount : amounts) {
63 BPScalar blinding = RandScalar(rng);
64 BPScalar seed = RandScalar(rng);
65 BPCommitment commitment;
66 Bulletproof proof;
67 BOOST_CHECK(ProveBulletproof(amount, blinding, seed, commitment, proof));
68 BOOST_CHECK(VerifyBulletproof(commitment, proof));
69 }
70 }
71
72 BOOST_AUTO_TEST_CASE(proof_rejects_tampering)
73 {
74 FastRandomContext rng{uint256{3}};
75 BPScalar blinding = RandScalar(rng);
76 BPScalar seed = RandScalar(rng);
77 BPCommitment commitment;
78 Bulletproof proof;
79 BOOST_REQUIRE(ProveBulletproof(123456789, blinding, seed, commitment, proof));
80
81 // Tamper with each scalar field and confirm rejection.
82 {
83 Bulletproof p2 = proof; p2.t_hat[0] ^= 0xff;
84 BOOST_CHECK(!VerifyBulletproof(commitment, p2));
85 }
86 {
87 Bulletproof p2 = proof; p2.taux[0] ^= 0xff;
88 BOOST_CHECK(!VerifyBulletproof(commitment, p2));
89 }
90 {
91 Bulletproof p2 = proof; p2.mu[0] ^= 0xff;
92 BOOST_CHECK(!VerifyBulletproof(commitment, p2));
93 }
94 {
95 Bulletproof p2 = proof; p2.a[0] ^= 0xff;
96 BOOST_CHECK(!VerifyBulletproof(commitment, p2));
97 }
98 {
99 Bulletproof p2 = proof; p2.b[0] ^= 0xff;
100 BOOST_CHECK(!VerifyBulletproof(commitment, p2));
101 }
102
103 // Tamper with a point field.
104 {
105 Bulletproof p2 = proof;
106 p2.A[0] ^= 0xff;
107 BOOST_CHECK(!VerifyBulletproof(commitment, p2));
108 }
109 {
110 Bulletproof p2 = proof;
111 p2.L[0][0] ^= 0xff;
112 BOOST_CHECK(!VerifyBulletproof(commitment, p2));
113 }
114 {
115 Bulletproof p2 = proof;
116 p2.R[5][32] ^= 0xff;
117 BOOST_CHECK(!VerifyBulletproof(commitment, p2));
118 }
119
120 // Wrong commitment must be rejected.
121 BPCommitment wrong = commitment;
122 wrong[0] ^= 0xff;
123 BOOST_CHECK(!VerifyBulletproof(wrong, proof));
124 }
125
126 BOOST_AUTO_TEST_CASE(parse_roundtrip)
127 {
128 FastRandomContext rng{uint256{4}};
129 BPScalar blinding = RandScalar(rng);
130 BPScalar seed = RandScalar(rng);
131 BPCommitment commitment;
132 Bulletproof proof;
133 BOOST_REQUIRE(ProveBulletproof(42, blinding, seed, commitment, proof));
134
135 auto bytes = SerializeProof(proof);
136 BOOST_CHECK_EQUAL(bytes.size(), size_t(754));
137
138 Bulletproof parsed;
139 BOOST_CHECK(ParseBulletproof(bytes, parsed));
140 BOOST_CHECK(VerifyBulletproof(commitment, parsed));
141
142 // Truncated data must fail to parse.
143 bytes.pop_back();
144 Bulletproof bad;
145 BOOST_CHECK(!ParseBulletproof(bytes, bad));
146 }
147
148 BOOST_AUTO_TEST_CASE(ct_balance)
149 {
150 FastRandomContext rng{uint256{5}};
151 // Single input/output with output blinding = 0, so the excess equals the
152 // input blinding (no scalar arithmetic needed in the test).
153 const __int128 SAT_SCALE = 1000000000000000000LL;
154 // (v_in, fee) pairs at raw scale and attosat scale (sub-satoshi fee).
155 const std::vector<std::pair<__int128, __int128>> cases = {
156 {1000000ULL, 5000},
157 {50000000000ULL, 5000},
158 {(__int128)UINT64_MAX / 2, 5000},
159 {5005 * SAT_SCALE + 400, 5000 * SAT_SCALE + 250},
160 {((__int128)1 << 100) + 123, 5000},
161 };
162 const std::vector<uint8_t> msg(32, 0xAB); // fixed 32-byte kernel message
163 for (const auto& [v_in, fee] : cases) {
164 __int128 v_out = v_in - fee;
165 BPScalar r_in = RandScalar(rng);
166 BPScalar zero(BP_SCALAR_SIZE, 0);
167 BPCommitment cin, cout;
168 BOOST_REQUIRE(CommitAmount(v_in, r_in, cin));
169 BOOST_REQUIRE(CommitAmount(v_out, zero, cout));
170
171 std::vector<uint8_t> sig;
172 BOOST_REQUIRE(CreateCTKernelSig(r_in, msg, sig));
173 BOOST_CHECK_EQUAL(sig.size(), size_t(64));
174
175 // Valid kernel signature verifies.
176 BOOST_CHECK(VerifyCTBalance({cin}, {cout}, fee, msg, sig));
177
178 // Wrong fee shifts the kernel point, so the signature must fail.
179 BOOST_CHECK(!VerifyCTBalance({cin}, {cout}, fee + 1, msg, sig));
180
181 // Tampered signature must fail.
182 sig[0] ^= 0xff;
183 BOOST_CHECK(!VerifyCTBalance({cin}, {cout}, fee, msg, sig));
184
185 // Wrong message must fail.
186 std::vector<uint8_t> sig2;
187 BOOST_REQUIRE(CreateCTKernelSig(r_in, msg, sig2));
188 std::vector<uint8_t> wrong_msg(32, 0xCD);
189 BOOST_CHECK(!VerifyCTBalance({cin}, {cout}, fee, wrong_msg, sig2));
190
191 // Missing output (amount not conserved) must fail: the kernel has a
192 // non-zero value component, whose discrete log the signer cannot know.
193 std::vector<uint8_t> sig3;
194 BOOST_REQUIRE(CreateCTKernelSig(r_in, msg, sig3));
195 BOOST_CHECK(!VerifyCTBalance({cin}, {}, fee, msg, sig3));
196 }
197 }
198
199 BOOST_AUTO_TEST_CASE(verify_p2bpct_script)
200 {
201 FastRandomContext rng{uint256{12}};
202 BPScalar blinding = RandScalar(rng);
203 BPScalar seed = RandScalar(rng);
204 BPCommitment commitment;
205 Bulletproof proof;
206 BOOST_REQUIRE(ProveBulletproof(12345, blinding, seed, commitment, proof));
207
208 // P2BPCT output: OP_4 <33-byte commitment>.
209 const CScript spk = CScript() << OP_4 << commitment;
210
211 // Spend witness: a single 688-byte bulletproof stack element.
212 const std::vector<uint8_t> proof_bytes = SerializeProof(proof);
213
214 CMutableTransaction tx;
215 tx.vin.resize(1);
216 tx.vout.resize(1);
217 tx.vout[0].nValue = 0;
218 MutableTransactionSignatureChecker checker(&tx, 0, 0, MissingDataBehavior::ASSERT_FAIL);
219 const unsigned int flags = SCRIPT_VERIFY_WITNESS | SCRIPT_VERIFY_P2SH | SCRIPT_VERIFY_P2BPCT;
220
221 // Valid proof verifies through the interpreter.
222 {
223 CScriptWitness witness;
224 witness.stack.push_back(proof_bytes);
225 ScriptError err;
226 BOOST_CHECK(VerifyScript(CScript(), spk, &witness, flags, checker, &err));
227 }
228
229 // Tampered proof is rejected.
230 {
231 std::vector<uint8_t> bad = proof_bytes;
232 bad[0] ^= 0xff;
233 CScriptWitness witness;
234 witness.stack.push_back(bad);
235 ScriptError err;
236 BOOST_CHECK(!VerifyScript(CScript(), spk, &witness, flags, checker, &err));
237 }
238
239 // Wrong stack shape is rejected.
240 {
241 CScriptWitness witness;
242 witness.stack.push_back(proof_bytes);
243 witness.stack.push_back(proof_bytes); // two elements, expected one
244 ScriptError err;
245 BOOST_CHECK(!VerifyScript(CScript(), spk, &witness, flags, checker, &err));
246 }
247 }
248
249 BOOST_AUTO_TEST_SUITE_END()
250