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