// 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. #include #include #include #include #include #include #include #include #include // --------------------------------------------------------------------------- // Curve constants // --------------------------------------------------------------------------- // secp256k1 group order. static BigNum CurveOrder() { static const uint8_t n_bytes[32] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xBA, 0xAE, 0xDC, 0xE6, 0xAF, 0x48, 0xA0, 0x3B, 0xBF, 0xD2, 0x5E, 0x8C, 0xD0, 0x36, 0x41, 0x41, }; static BigNum n(std::vector(n_bytes, n_bytes + 32), true); return n; } // secp256k1 base point G, compressed. static const std::vector& BasePointG() { static const std::vector g = { 0x02, 0x79, 0xBE, 0x66, 0x7E, 0xF9, 0xDC, 0xBB, 0xAC, 0x55, 0xA0, 0x62, 0x95, 0xCE, 0x87, 0x0B, 0x07, 0x02, 0x9B, 0xFC, 0xDB, 0x2D, 0xCE, 0x28, 0xD9, 0x59, 0xF2, 0x81, 0x5B, 0x16, 0xF8, 0x17, 0x98, }; return g; } // --------------------------------------------------------------------------- // Scalar arithmetic mod the curve order, using BigNum. // --------------------------------------------------------------------------- static BigNum SMod(const BigNum& x) { return x % CurveOrder(); } static BigNum SAdd(const BigNum& a, const BigNum& b) { return (a + b) % CurveOrder(); } static BigNum SMul(const BigNum& a, const BigNum& b) { return (a * b) % CurveOrder(); } static BigNum SNeg(const BigNum& a) { return a.is_zero() ? BigNum(0u) : CurveOrder() - a; } static BigNum SInv(const BigNum& a) { // Fermat: a^(n-2) mod n. return BigNum::mod_pow(a, CurveOrder() - BigNum(2u), CurveOrder()); } // 32-byte big-endian scalar. static std::vector ScalarToBytes(const BigNum& x) { auto le = x.to_bytes(BP_SCALAR_SIZE); std::vector be(BP_SCALAR_SIZE); for (size_t i = 0; i < BP_SCALAR_SIZE; i++) be[i] = le[BP_SCALAR_SIZE - 1 - i]; return be; } static BigNum ScalarFromBytes(const std::vector& be) { return SMod(BigNum(be, true)); } // Domain-separated hash to a scalar in [0, n). static BigNum HashToScalar(const char* label, const std::vector& data) { CSHA256 sha; sha.Write(reinterpret_cast(label), std::strlen(label)); sha.Write(data.data(), data.size()); uint8_t hash[CSHA256::OUTPUT_SIZE]; sha.Finalize(hash); return SMod(BigNum(std::vector(hash, hash + CSHA256::OUTPUT_SIZE), false)); } // --------------------------------------------------------------------------- // Point operations via secp256k1 // --------------------------------------------------------------------------- static secp256k1_context* GetContext() { static secp256k1_context* ctx = secp256k1_context_create(SECP256K1_CONTEXT_VERIFY); return ctx; } static bool ParsePoint(const std::vector& in, secp256k1_pubkey& out) { return secp256k1_ec_pubkey_parse(GetContext(), &out, in.data(), in.size()) == 1; } static std::vector SerializePoint(const secp256k1_pubkey& pub) { std::vector out(BP_POINT_SIZE); size_t len = BP_POINT_SIZE; secp256k1_ec_pubkey_serialize(GetContext(), out.data(), &len, &pub, SECP256K1_EC_COMPRESSED); out.resize(len); return out; } // Try-and-increment hash-to-curve, returning a NUMS point with even y. static bool HashToCurve(const std::string& label, std::vector& out) { for (uint32_t counter = 0; counter < 0x100000; counter++) { CSHA256 sha; sha.Write(reinterpret_cast(label.data()), label.size()); uint8_t cb[4] = {uint8_t(counter >> 24), uint8_t(counter >> 16), uint8_t(counter >> 8), uint8_t(counter)}; sha.Write(cb, 4); uint8_t hash[CSHA256::OUTPUT_SIZE]; sha.Finalize(hash); secp256k1_xonly_pubkey xonly; if (!secp256k1_xonly_pubkey_parse(GetContext(), &xonly, hash)) continue; // Convert to a full point (even y) with a zero tweak. secp256k1_pubkey pub; static const uint8_t zero[32] = {0}; if (!secp256k1_xonly_pubkey_tweak_add(GetContext(), &pub, &xonly, zero)) continue; out = SerializePoint(pub); return true; } return false; } // --------------------------------------------------------------------------- // Generators // --------------------------------------------------------------------------- static std::vector g_value; static std::vector> g_G_vec; static std::vector> g_H_vec; static std::once_flag g_generators_init; void InitBulletproofGenerators() { std::call_once(g_generators_init, []() { HashToCurve("bulletproofs v1 H", g_value); g_G_vec.resize(BP_BITS); g_H_vec.resize(BP_BITS); for (size_t i = 0; i < BP_BITS; i++) { HashToCurve("bulletproofs v1 G" + std::to_string(i), g_G_vec[i]); HashToCurve("bulletproofs v1 H" + std::to_string(i), g_H_vec[i]); } }); } bool VerifyBulletproofGenerators() { InitBulletproofGenerators(); std::vector> all; all.push_back(g_value); all.insert(all.end(), g_G_vec.begin(), g_G_vec.end()); all.insert(all.end(), g_H_vec.begin(), g_H_vec.end()); all.push_back(BasePointG()); for (size_t i = 0; i < all.size(); i++) { if (all[i].size() != BP_POINT_SIZE) return false; secp256k1_pubkey tmp; if (!secp256k1_ec_pubkey_parse(GetContext(), &tmp, all[i].data(), all[i].size())) return false; for (size_t j = i + 1; j < all.size(); j++) { if (all[i] == all[j]) return false; } } return true; } // --------------------------------------------------------------------------- // Commitment // --------------------------------------------------------------------------- bool CommitAmount(__int128 amount, const BPScalar& blinding, BPCommitment& out) { if (blinding.size() != BP_SCALAR_SIZE) return false; InitBulletproofGenerators(); // C = r*G + v*H (blinding r in base point G, value v in NUMS H). // amount as a full 128-bit BigNum (little-endian bytes). std::vector amount_bytes(16); for (int i = 0; i < 16; i++) amount_bytes[i] = uint8_t(static_cast(static_cast<__uint128_t>(amount) >> (8 * i))); BigNum v = SMod(BigNum(amount_bytes, false)); BigNum r = ScalarFromBytes(blinding); // Skip zero scalars (identity points). Require at least one non-zero // term so the commitment is never the point at infinity. std::vector pubs; std::vector ptrs; if (!v.is_zero()) { secp256k1_pubkey vH; if (!ParsePoint(g_value, vH)) return false; auto vs = ScalarToBytes(v); if (!secp256k1_ec_pubkey_tweak_mul(GetContext(), &vH, vs.data())) return false; pubs.push_back(vH); } if (!r.is_zero()) { secp256k1_pubkey rG; if (!ParsePoint(BasePointG(), rG)) return false; auto rs = ScalarToBytes(r); if (!secp256k1_ec_pubkey_tweak_mul(GetContext(), &rG, rs.data())) return false; pubs.push_back(rG); } if (pubs.empty()) return false; for (auto& p : pubs) ptrs.push_back(&p); secp256k1_pubkey sum; if (!secp256k1_ec_pubkey_combine(GetContext(), &sum, ptrs.data(), ptrs.size())) return false; out = SerializePoint(sum); return true; } // --------------------------------------------------------------------------- // Multiscalar check: sum of scalar*point == identity. // --------------------------------------------------------------------------- static bool MultiscalarEqualsIdentity( const std::vector>>& terms) { // Add a fixed offset point so the result is never the identity; the sum // is the identity iff result == offset. std::vector offset; if (!HashToCurve("bulletproofs v1 offset", offset)) return false; std::vector pubs; pubs.reserve(terms.size() + 1); std::vector ptrs; secp256k1_pubkey off; if (!ParsePoint(offset, off)) return false; pubs.push_back(off); for (const auto& [scalar, point] : terms) { BigNum s = SMod(scalar); if (s.is_zero()) continue; secp256k1_pubkey pub; if (!ParsePoint(point, pub)) return false; auto sb = ScalarToBytes(s); if (!secp256k1_ec_pubkey_tweak_mul(GetContext(), &pub, sb.data())) return false; pubs.push_back(pub); } for (auto& p : pubs) ptrs.push_back(&p); secp256k1_pubkey result; if (!secp256k1_ec_pubkey_combine(GetContext(), &result, ptrs.data(), ptrs.size())) return false; return SerializePoint(result) == offset; } // --------------------------------------------------------------------------- // Inner product: compute the folding challenges u_j and the s vector. // --------------------------------------------------------------------------- static bool ComputeIPAScalars( const Bulletproof& proof, const BigNum& w, std::vector& u_sq, std::vector& u_inv_sq, std::vector& s) { // Challenges u_6..u_1, each bound to the range proof via w. // proof.L[i] corresponds to round (6 - i), so L[0]=L_6 ... L[5]=L_1. std::vector u(BP_ROUNDS); { std::vector acc = ScalarToBytes(w); for (size_t j = 0; j < BP_ROUNDS; j++) { std::vector data = acc; data.insert(data.end(), proof.L[j].begin(), proof.L[j].end()); data.insert(data.end(), proof.R[j].begin(), proof.R[j].end()); BigNum uj = HashToScalar("bp-u", data); if (uj.is_zero()) uj = BigNum(1u); u[j] = uj; acc = ScalarToBytes(uj); } } u_sq.resize(BP_ROUNDS); u_inv_sq.resize(BP_ROUNDS); BigNum allinv(1u); for (size_t j = 0; j < BP_ROUNDS; j++) { BigNum inv = SInv(u[j]); u_sq[j] = SMul(u[j], u[j]); u_inv_sq[j] = SMul(inv, inv); allinv = SMul(allinv, inv); } // s[i] = s[i - 2^lg] * u_{lg+1}^2, s[0] = allinv. // u vector is stored as u[0..5] = u_6..u_1. s.resize(BP_BITS); s[0] = allinv; for (size_t i = 1; i < BP_BITS; i++) { size_t lg = 63 - __builtin_clzll((unsigned long long)i); size_t k = size_t(1) << lg; // u_{lg+1} corresponds to index (BP_ROUNDS - 1 - lg). size_t idx = (BP_ROUNDS - 1) - lg; s[i] = SMul(s[i - k], u_sq[idx]); } return true; } // --------------------------------------------------------------------------- // Range proof verification. // --------------------------------------------------------------------------- bool VerifyBulletproof(const BPCommitment& commitment, const Bulletproof& proof) { if (commitment.size() != BP_POINT_SIZE) return false; if (proof.A.size() != BP_POINT_SIZE || proof.S.size() != BP_POINT_SIZE) return false; if (proof.T1.size() != BP_POINT_SIZE || proof.T2.size() != BP_POINT_SIZE) return false; if (proof.t_hat.size() != BP_SCALAR_SIZE || proof.taux.size() != BP_SCALAR_SIZE) return false; if (proof.mu.size() != BP_SCALAR_SIZE) return false; if (proof.a.size() != BP_SCALAR_SIZE || proof.b.size() != BP_SCALAR_SIZE) return false; for (size_t i = 0; i < BP_ROUNDS; i++) { if (proof.L[i].size() != BP_POINT_SIZE || proof.R[i].size() != BP_POINT_SIZE) return false; } InitBulletproofGenerators(); // Validate that all points parse (and are not the identity). secp256k1_pubkey tmp; auto valid_point = [&](const std::vector& p) { return ParsePoint(p, tmp); }; if (!valid_point(commitment) || !valid_point(proof.A) || !valid_point(proof.S) || !valid_point(proof.T1) || !valid_point(proof.T2)) return false; for (size_t i = 0; i < BP_ROUNDS; i++) { if (!valid_point(proof.L[i]) || !valid_point(proof.R[i])) return false; } // Challenges. BigNum y, z, x, w, c; { std::vector data = commitment; data.insert(data.end(), proof.A.begin(), proof.A.end()); data.insert(data.end(), proof.S.begin(), proof.S.end()); y = HashToScalar("bp-y", data); if (y.is_zero()) y = BigNum(1u); { auto yb = ScalarToBytes(y); data.insert(data.end(), yb.begin(), yb.end()); } z = HashToScalar("bp-z", data); if (z.is_zero()) z = BigNum(1u); std::vector tx = proof.T1; tx.insert(tx.end(), proof.T2.begin(), proof.T2.end()); x = HashToScalar("bp-x", tx); if (x.is_zero()) x = BigNum(1u); std::vector td = proof.t_hat; td.insert(td.end(), proof.taux.begin(), proof.taux.end()); td.insert(td.end(), proof.mu.begin(), proof.mu.end()); w = HashToScalar("bp-w", td); if (w.is_zero()) w = BigNum(1u); c = HashToScalar("bp-c", ScalarToBytes(w)); if (c.is_zero()) c = BigNum(1u); } BigNum t_hat = ScalarFromBytes(proof.t_hat); BigNum taux = ScalarFromBytes(proof.taux); BigNum mu = ScalarFromBytes(proof.mu); BigNum a_scalar = ScalarFromBytes(proof.a); BigNum b_scalar = ScalarFromBytes(proof.b); // y^{-i} for i = 0..n-1, and y^i. std::vector y_pow(BP_BITS); // y^i std::vector y_inv_pow(BP_BITS); // y^{-i} { BigNum yp(1u), yip(1u), yinv = SInv(y); for (size_t i = 0; i < BP_BITS; i++) { y_pow[i] = yp; y_inv_pow[i] = yip; yp = SMul(yp, y); yip = SMul(yip, yinv); } } // delta(y,z) = (z - z^2) * <1, y^n> - z^3 * <1, 2^n> BigNum z2 = SMul(z, z); BigNum z3 = SMul(z2, z); BigNum sum_y(0u), sum_2(0u); { BigNum two(2u), twopow(1u); for (size_t i = 0; i < BP_BITS; i++) { sum_y = SAdd(sum_y, y_pow[i]); sum_2 = SAdd(sum_2, twopow); twopow = SMul(twopow, two); } } BigNum delta = SAdd(SMul(SAdd(z, SNeg(z2)), sum_y), SNeg(SMul(z3, sum_2))); // IPA folding scalars. std::vector u_sq, u_inv_sq, s_vec; if (!ComputeIPAScalars(proof, w, u_sq, u_inv_sq, s_vec)) return false; // Build the multiscalar terms: sum must equal identity. std::vector>> terms; // A, S, V, T1, T2 terms.emplace_back(BigNum(1u), proof.A); terms.emplace_back(x, proof.S); terms.emplace_back(SMul(c, z2), commitment); terms.emplace_back(SMul(c, x), proof.T1); terms.emplace_back(SMul(c, SMul(x, x)), proof.T2); // Value generator H (NUMS): w*(t_hat - a*b) + c*(delta - t_hat) { BigNum ab = SMul(a_scalar, b_scalar); BigNum gcoef = SAdd(SMul(w, SAdd(t_hat, SNeg(ab))), SMul(c, SAdd(delta, SNeg(t_hat)))); terms.emplace_back(gcoef, g_value); } // Blinding generator G (base): -mu - c*taux terms.emplace_back(SAdd(SNeg(mu), SNeg(SMul(c, taux))), BasePointG()); // G_vec: -z - a*s_i for (size_t i = 0; i < BP_BITS; i++) { BigNum coef = SAdd(SNeg(z), SNeg(SMul(a_scalar, s_vec[i]))); terms.emplace_back(coef, g_G_vec[i]); } // H_vec: z + y^{-i} * (z^2 * 2^i - b / s_i), where 1/s_i = s_{n-1-i}. { BigNum two(2u), twopow(1u); for (size_t i = 0; i < BP_BITS; i++) { BigNum binv = s_vec[BP_BITS - 1 - i]; // 1/s_i BigNum inner = SAdd(SMul(z2, twopow), SNeg(SMul(b_scalar, binv))); BigNum coef = SAdd(z, SMul(y_inv_pow[i], inner)); terms.emplace_back(coef, g_H_vec[i]); twopow = SMul(twopow, two); } } // L_j * u_j^2 and R_j * u_j^{-2} for (size_t j = 0; j < BP_ROUNDS; j++) { terms.emplace_back(u_sq[j], proof.L[j]); terms.emplace_back(u_inv_sq[j], proof.R[j]); } return MultiscalarEqualsIdentity(terms); } // --------------------------------------------------------------------------- // Parsing // --------------------------------------------------------------------------- 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) { if (kernel_msg.size() != BP_SCALAR_SIZE || kernel_sig.size() != 64) return false; InitBulletproofGenerators(); // kernel K = sum(C_in) - sum(C_out) - fee*H + sum(v_transparent_in)*H. // No offset terms: the balance equation is a plain Pedersen sum, so a // committed excess must equal the signer's key exactly. // Transparent mint inputs contribute their visible value with zero // blinding; their script signatures authorize the spend separately. std::vector>> terms; for (const auto& c : input_commitments) { if (c.size() != BP_POINT_SIZE) return false; terms.emplace_back(BigNum(1u), c); } for (const auto& c : output_commitments) { if (c.size() != BP_POINT_SIZE) return false; terms.emplace_back(SNeg(BigNum(1u)), c); } for (const __int128 v : transparent_inputs) { if (v < 0) return false; std::vector vb(16); for (int i = 0; i < 16; i++) vb[i] = uint8_t(static_cast(static_cast<__uint128_t>(v) >> (8 * i))); terms.emplace_back(SMod(BigNum(vb, false)), g_value); } { std::vector fb(16); for (int i = 0; i < 16; i++) fb[i] = uint8_t(static_cast(static_cast<__uint128_t>(fee) >> (8 * i))); terms.emplace_back(SNeg(SMod(BigNum(fb, false))), g_value); } // Compute K. It must be a valid, non-identity point (the excess key). std::vector pubs; std::vector ptrs; for (const auto& [scalar, point] : terms) { BigNum s = SMod(scalar); if (s.is_zero()) continue; secp256k1_pubkey pub; if (!ParsePoint(point, pub)) return false; auto sb = ScalarToBytes(s); if (!secp256k1_ec_pubkey_tweak_mul(GetContext(), &pub, sb.data())) return false; pubs.push_back(pub); } if (pubs.empty()) return false; for (auto& p : pubs) ptrs.push_back(&p); secp256k1_pubkey K; if (!secp256k1_ec_pubkey_combine(GetContext(), &K, ptrs.data(), ptrs.size())) return false; // Convert K to an x-only pubkey (even y) and verify the Schnorr signature. secp256k1_xonly_pubkey Kx; if (!secp256k1_xonly_pubkey_from_pubkey(GetContext(), &Kx, nullptr, &K)) return false; return secp256k1_schnorrsig_verify(GetContext(), kernel_sig.data(), kernel_msg.data(), BP_SCALAR_SIZE, &Kx) == 1; } bool CreateCTKernelSig(const BPScalar& excess, const std::vector& kernel_msg, std::vector& sig) { if (excess.size() != BP_SCALAR_SIZE || kernel_msg.size() != BP_SCALAR_SIZE) return false; // The excess is the private key; must be in [1, n-1]. BigNum e = ScalarFromBytes(excess); if (e.is_zero()) return false; secp256k1_keypair keypair; auto seckey = ScalarToBytes(e); if (!secp256k1_keypair_create(GetContext(), &keypair, seckey.data())) return false; sig.resize(64); static const uint8_t aux[32] = {0}; return secp256k1_schnorrsig_sign32(GetContext(), sig.data(), kernel_msg.data(), &keypair, aux) == 1; } std::vector SerializeBulletproof(const Bulletproof& proof) { std::vector out; auto append = [&](const std::vector& v) { out.insert(out.end(), v.begin(), v.end()); }; append(proof.A); append(proof.S); append(proof.T1); append(proof.T2); append(proof.t_hat); append(proof.taux); append(proof.mu); for (size_t i = 0; i < BP_ROUNDS; i++) { append(proof.L[i]); append(proof.R[i]); } append(proof.a); append(proof.b); return out; } bool ParseBulletproof(std::span data, Bulletproof& proof) { size_t offset = 0; auto read_vec = [&](size_t n, std::vector& out) { if (offset + n > data.size()) return false; out.assign(data.begin() + offset, data.begin() + offset + n); offset += n; return true; }; if (!read_vec(BP_POINT_SIZE, proof.A)) return false; if (!read_vec(BP_POINT_SIZE, proof.S)) return false; if (!read_vec(BP_POINT_SIZE, proof.T1)) return false; if (!read_vec(BP_POINT_SIZE, proof.T2)) return false; if (!read_vec(BP_SCALAR_SIZE, proof.t_hat)) return false; if (!read_vec(BP_SCALAR_SIZE, proof.taux)) return false; if (!read_vec(BP_SCALAR_SIZE, proof.mu)) return false; for (size_t i = 0; i < BP_ROUNDS; i++) { if (!read_vec(BP_POINT_SIZE, proof.L[i])) return false; if (!read_vec(BP_POINT_SIZE, proof.R[i])) return false; } if (!read_vec(BP_SCALAR_SIZE, proof.a)) return false; if (!read_vec(BP_SCALAR_SIZE, proof.b)) return false; return offset == data.size(); } // --------------------------------------------------------------------------- // Prover (wallet-side, not consensus) // --------------------------------------------------------------------------- static BigNum SeedScalar(const std::vector& seed, const char* label, uint32_t counter) { CSHA256 sha; sha.Write(seed.data(), seed.size()); sha.Write(reinterpret_cast(label), std::strlen(label)); uint8_t cb[4] = {uint8_t(counter >> 24), uint8_t(counter >> 16), uint8_t(counter >> 8), uint8_t(counter)}; sha.Write(cb, 4); uint8_t hash[CSHA256::OUTPUT_SIZE]; sha.Finalize(hash); return SMod(BigNum(std::vector(hash, hash + CSHA256::OUTPUT_SIZE), false)); } // Sum of scalar*point, skipping zero scalars. Returns false on malformed point. static bool MultiscalarMul(const std::vector>>& terms, std::vector& out) { std::vector pubs; std::vector ptrs; for (const auto& [scalar, point] : terms) { BigNum s = SMod(scalar); if (s.is_zero()) continue; secp256k1_pubkey pub; if (!ParsePoint(point, pub)) return false; auto sb = ScalarToBytes(s); if (!secp256k1_ec_pubkey_tweak_mul(GetContext(), &pub, sb.data())) return false; pubs.push_back(pub); } if (pubs.empty()) { out.clear(); return true; } for (auto& p : pubs) ptrs.push_back(&p); secp256k1_pubkey result; if (!secp256k1_ec_pubkey_combine(GetContext(), &result, ptrs.data(), ptrs.size())) return false; out = SerializePoint(result); return true; } bool ProveBulletproof(__int128 amount, const BPScalar& blinding, const BPScalar& seed, BPCommitment& commitment, Bulletproof& proof) { if (blinding.size() != BP_SCALAR_SIZE || seed.size() != BP_SCALAR_SIZE) return false; InitBulletproofGenerators(); BigNum gamma = ScalarFromBytes(blinding); // a_L = bits of v, a_R = a_L - 1. std::vector aL(BP_BITS), aR(BP_BITS); for (size_t i = 0; i < BP_BITS; i++) { BigNum b(uint32_t((static_cast(amount >> i)) & 1)); aL[i] = b; aR[i] = SAdd(b, SNeg(BigNum(1u))); } // Random scalars. BigNum alpha = SeedScalar(seed, "alpha", 0); BigNum rho = SeedScalar(seed, "rho", 0); BigNum tau1 = SeedScalar(seed, "tau1", 0); BigNum tau2 = SeedScalar(seed, "tau2", 0); std::vector sL(BP_BITS), sR(BP_BITS); for (size_t i = 0; i < BP_BITS; i++) { sL[i] = SeedScalar(seed, "sL", (uint32_t)i); sR[i] = SeedScalar(seed, "sR", (uint32_t)i); } // A = alpha*G + + ; S = rho*G + + . // (alpha, rho are blinding scalars, so they sit in the base point G.) std::vector>> A_terms, S_terms; A_terms.emplace_back(alpha, BasePointG()); S_terms.emplace_back(rho, BasePointG()); for (size_t i = 0; i < BP_BITS; i++) { A_terms.emplace_back(aL[i], g_G_vec[i]); A_terms.emplace_back(aR[i], g_H_vec[i]); S_terms.emplace_back(sL[i], g_G_vec[i]); S_terms.emplace_back(sR[i], g_H_vec[i]); } if (!MultiscalarMul(A_terms, proof.A)) return false; if (!MultiscalarMul(S_terms, proof.S)) return false; // Commitment C = v*G + gamma*H. if (!CommitAmount(amount, blinding, commitment)) return false; // Challenges y, z. BigNum y, z; { std::vector data = commitment; data.insert(data.end(), proof.A.begin(), proof.A.end()); data.insert(data.end(), proof.S.begin(), proof.S.end()); y = HashToScalar("bp-y", data); if (y.is_zero()) y = BigNum(1u); { auto yb = ScalarToBytes(y); data.insert(data.end(), yb.begin(), yb.end()); } z = HashToScalar("bp-z", data); if (z.is_zero()) z = BigNum(1u); } // y^i and y^{-i}. std::vector y_pow(BP_BITS), y_inv_pow(BP_BITS); { BigNum yp(1u), yip(1u), yinv = SInv(y); for (size_t i = 0; i < BP_BITS; i++) { y_pow[i] = yp; y_inv_pow[i] = yip; yp = SMul(yp, y); yip = SMul(yip, yinv); } } // l0 = aL - z, l1 = sL. // r0 = y^n o (aR + z) + z^2 * 2^n, r1 = y^n o sR. std::vector l0(BP_BITS), l1(BP_BITS), r0(BP_BITS), r1(BP_BITS); BigNum z2 = SMul(z, z); { BigNum two(2u), twopow(1u); for (size_t i = 0; i < BP_BITS; i++) { l0[i] = SAdd(aL[i], SNeg(z)); l1[i] = sL[i]; r0[i] = SAdd(SMul(y_pow[i], SAdd(aR[i], z)), SMul(z2, twopow)); r1[i] = SMul(y_pow[i], sR[i]); twopow = SMul(twopow, two); } } // t0 = , t2 = , t1 = - t0 - t2. BigNum t0(0u), t2(0u), t1(0u); { BigNum t01(0u); for (size_t i = 0; i < BP_BITS; i++) { t0 = SAdd(t0, SMul(l0[i], r0[i])); t2 = SAdd(t2, SMul(l1[i], r1[i])); t01 = SAdd(t01, SMul(SAdd(l0[i], l1[i]), SAdd(r0[i], r1[i]))); } t1 = SAdd(SAdd(t01, SNeg(t0)), SNeg(t2)); } // T1 = t1*H + tau1*G, T2 = t2*H + tau2*G (t in value gen H, tau in base G). { std::vector>> t1t, t2t; t1t.emplace_back(t1, g_value); t1t.emplace_back(tau1, BasePointG()); t2t.emplace_back(t2, g_value); t2t.emplace_back(tau2, BasePointG()); if (!MultiscalarMul(t1t, proof.T1)) return false; if (!MultiscalarMul(t2t, proof.T2)) return false; } // x = H(T1, T2). BigNum x; { std::vector tx = proof.T1; tx.insert(tx.end(), proof.T2.begin(), proof.T2.end()); x = HashToScalar("bp-x", tx); if (x.is_zero()) x = BigNum(1u); } // l = l0 + l1*x, r = r0 + r1*x, t_hat = . std::vector l(BP_BITS), r(BP_BITS); BigNum t_hat(0u); for (size_t i = 0; i < BP_BITS; i++) { l[i] = SAdd(l0[i], SMul(l1[i], x)); r[i] = SAdd(r0[i], SMul(r1[i], x)); t_hat = SAdd(t_hat, SMul(l[i], r[i])); } // tau_x = tau2*x^2 + tau1*x + z^2*gamma; mu = alpha + rho*x. BigNum x2 = SMul(x, x); BigNum tau_x = SAdd(SAdd(SMul(tau2, x2), SMul(tau1, x)), SMul(z2, gamma)); BigNum mu = SAdd(alpha, SMul(rho, x)); proof.t_hat = ScalarToBytes(t_hat); proof.taux = ScalarToBytes(tau_x); proof.mu = ScalarToBytes(mu); // w = H("bp-w", t_hat || tau_x || mu); Q = w * G. BigNum w; { std::vector td = proof.t_hat; td.insert(td.end(), proof.taux.begin(), proof.taux.end()); td.insert(td.end(), proof.mu.begin(), proof.mu.end()); w = HashToScalar("bp-w", td); if (w.is_zero()) w = BigNum(1u); } std::vector Q; { secp256k1_pubkey q; if (!ParsePoint(g_value, q)) return false; auto wb = ScalarToBytes(w); if (!secp256k1_ec_pubkey_tweak_mul(GetContext(), &q, wb.data())) return false; Q = SerializePoint(q); } // IPA over (G, y^{-n} o H) with vectors (l, r), Q, proving = t_hat. std::vector> Gc = g_G_vec; std::vector> Hc(BP_BITS); for (size_t i = 0; i < BP_BITS; i++) { secp256k1_pubkey h; if (!ParsePoint(g_H_vec[i], h)) return false; auto yib = ScalarToBytes(y_inv_pow[i]); if (!secp256k1_ec_pubkey_tweak_mul(GetContext(), &h, yib.data())) return false; Hc[i] = SerializePoint(h); } std::vector al = l, ar = r; std::vector acc = ScalarToBytes(w); size_t n = BP_BITS; for (size_t round = 0; round < BP_ROUNDS; round++) { size_t half = n / 2; BigNum cL(0u), cR(0u); for (size_t i = 0; i < half; i++) { cL = SAdd(cL, SMul(al[i], ar[i + half])); cR = SAdd(cR, SMul(al[i + half], ar[i])); } std::vector>> Lt, Rt; for (size_t i = 0; i < half; i++) { Lt.emplace_back(al[i], Gc[i + half]); Lt.emplace_back(ar[i + half], Hc[i]); Rt.emplace_back(al[i + half], Gc[i]); Rt.emplace_back(ar[i], Hc[i + half]); } Lt.emplace_back(cL, Q); Rt.emplace_back(cR, Q); if (!MultiscalarMul(Lt, proof.L[round])) return false; if (!MultiscalarMul(Rt, proof.R[round])) return false; std::vector data = acc; data.insert(data.end(), proof.L[round].begin(), proof.L[round].end()); data.insert(data.end(), proof.R[round].begin(), proof.R[round].end()); BigNum u = HashToScalar("bp-u", data); if (u.is_zero()) u = BigNum(1u); BigNum uinv = SInv(u); acc = ScalarToBytes(u); std::vector na(half), nb(half); std::vector> nG(half), nH(half); for (size_t i = 0; i < half; i++) { na[i] = SAdd(SMul(al[i], u), SMul(al[i + half], uinv)); nb[i] = SAdd(SMul(ar[i], uinv), SMul(ar[i + half], u)); } for (size_t i = 0; i < half; i++) { std::vector>> gt, ht; gt.emplace_back(uinv, Gc[i]); gt.emplace_back(u, Gc[i + half]); ht.emplace_back(u, Hc[i]); ht.emplace_back(uinv, Hc[i + half]); if (!MultiscalarMul(gt, nG[i])) return false; if (!MultiscalarMul(ht, nH[i])) return false; } al = std::move(na); ar = std::move(nb); Gc = std::move(nG); Hc = std::move(nH); n = half; } proof.a = ScalarToBytes(al[0]); proof.b = ScalarToBytes(ar[0]); return true; }