// Copyright (c) 2025 The Limenka developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #ifndef LIMENKA_CRYPTO_BULLETPROOFS_H #define LIMENKA_CRYPTO_BULLETPROOFS_H #include #include #include #include // --------------------------------------------------------------------------- // Bulletproofs for confidential transactions (P2BPCT) // // Range proof proving that a Pedersen-committed amount lies in [0, 2^128) // without revealing the amount. Uses the secp256k1 curve with NUMS // generator points derived via try-and-increment hash-to-curve. // // Follows Bunz et al. "Bulletproofs: Short Proofs for Confidential // Transactions and More" (IEEE S&P 2018), single-party (m=1) range proof, // n = 128 bits. The verification equations match the dalek-cryptography // bulletproofs construction. // // Value unit is a wallet convention; consensus only sees a 128-bit scalar // in [0, 2^128). The limenka wallet commits attosats: satoshis * 10^18 // plus the sub-satoshi fraction. // --------------------------------------------------------------------------- // Number of bits in the range proof (128 bits = CAmount range). static constexpr size_t BP_BITS = 128; // Number of inner-product rounds: log2(BP_BITS). static constexpr size_t BP_ROUNDS = 7; // Size of a compressed secp256k1 public key (33 bytes). static constexpr size_t BP_POINT_SIZE = 33; // Size of a scalar (32 bytes). static constexpr size_t BP_SCALAR_SIZE = 32; // A Pedersen commitment C = v*G + r*H is a compressed EC point (33 bytes). using BPCommitment = std::vector; // A scalar (32 bytes, big-endian, mod secp256k1 order). using BPScalar = std::vector; // A bulletproof includes the range proof and the inner product argument. // Serialized format (matches dalek-cryptography bulletproofs): // A (33 bytes) — commitment to a_L, a_R // S (33 bytes) — commitment to s_L, s_R // T1 (33 bytes) — first t polynomial commitment // T2 (33 bytes) — second t polynomial commitment // t_hat (32 bytes) — t(x) inner product value // taux (32 bytes) — t(x) blinding factor // mu (32 bytes) — alpha + rho*x combined blinding // L[7] (33 bytes each) — IPA left points (rounds 7..1) // R[7] (33 bytes each) — IPA right points (rounds 7..1) // a (32 bytes) — final IPA scalar // b (32 bytes) — final IPA scalar // // Total: 4*33 + 3*32 + 14*33 + 2*32 = 754 bytes struct Bulletproof { std::vector A; // 33 bytes std::vector S; // 33 bytes std::vector T1; // 33 bytes std::vector T2; // 33 bytes std::vector t_hat; // 32 bytes std::vector taux; // 32 bytes std::vector mu; // 32 bytes std::vector L[BP_ROUNDS]; // 7 x 33 bytes std::vector R[BP_ROUNDS]; // 7 x 33 bytes std::vector a; // 32 bytes std::vector b; // 32 bytes }; // Initialize the NUMS generator points. Must be called once before any // verification or commitment operation. Thread-safe after first call. void InitBulletproofGenerators(); // Verify that all generators are valid compressed points, pairwise distinct, // and distinct from the secp256k1 base point G (the NUMS property). Returns // false if any generator is malformed or collides with another. bool VerifyBulletproofGenerators(); // Compute a Pedersen commitment C = r*G + v*H, where r is the blinding // (private key, in the secp256k1 base point G) and v is the amount (in a // NUMS value generator H). Returns false on failure. bool CommitAmount(__int128 amount, const BPScalar& blinding, BPCommitment& out); // Verify a bulletproof range proof. // commitment: Pedersen commitment C = v*G + r*H (33 bytes) // proof: the bulletproof proving that v in [0, 2^128) // Returns true if the proof is valid. bool VerifyBulletproof(const BPCommitment& commitment, const Bulletproof& proof); // Prove a range proof for a committed amount (wallet-side, not consensus). // Derives all blinding randomness deterministically from a 32-byte seed. // WALLET REQUIREMENT: `blinding` and `seed` MUST be fresh 32-byte values from // a CSPRNG for every output. Reusing a seed or blinding leaks the amount // (Pedersen commitments are only hiding under fresh uniform blinding). // Returns false if `blinding` or `seed` is not 32 bytes. bool ProveBulletproof(__int128 amount, const BPScalar& blinding, const BPScalar& seed, BPCommitment& commitment, Bulletproof& proof); // Verify the confidential transaction balance and kernel signature. // Checks sum(input) - sum(output) == fee*H and that `kernel_sig` (64-byte // BIP340 Schnorr) is valid over `kernel_msg` (32 bytes) with public key // equal to the kernel excess point. // 33-byte points absorbing blinding contributions the signer could not // know (stealth outputs); each offset is added to the excess key. bool VerifyCTBalance(const std::vector>& input_commitments, const std::vector>& output_commitments, __int128 fee, const std::vector& kernel_msg, const std::vector& kernel_sig, const std::vector<__int128>& transparent_inputs = {}); // Sign a confidential transaction kernel with the excess blinding factor // (wallet-side, not consensus). Returns a 64-byte BIP340 signature, or // false if `excess` is zero (the kernel key would be the identity - the // wallet must re-blind one of its outputs in that case). bool CreateCTKernelSig(const BPScalar& excess, const std::vector& kernel_msg, std::vector& sig); // Deserialize a bulletproof from a byte range. Returns false on malformed input. bool ParseBulletproof(std::span data, Bulletproof& proof); // Serialize a bulletproof into the canonical byte format (754 bytes). std::vector SerializeBulletproof(const Bulletproof& proof); #endif // LIMENKA_CRYPTO_BULLETPROOFS_H