diff --git a/src/Makefile.am b/src/Makefile.am index 38b77b65c0..0cde9d514d 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -675,7 +675,9 @@ libspark_a_SOURCES = \ libspark/f4grumble.h \ libspark/f4grumble.cpp \ libspark/bech32.h \ - libspark/bech32.cpp + libspark/bech32.cpp \ + libspark/claim.h \ + libspark/claim.cpp liblelantus_a_CPPFLAGS = $(AM_CPPFLAGS) $(BITCOIN_INCLUDES) liblelantus_a_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS) diff --git a/src/libspark/claim.cpp b/src/libspark/claim.cpp new file mode 100644 index 0000000000..26ee93a9e4 --- /dev/null +++ b/src/libspark/claim.cpp @@ -0,0 +1,184 @@ +#include "claim.h" +#include "transcript.h" + +namespace spark { + +Claim::Claim(const GroupElement& F_, const GroupElement& G_, const GroupElement& H_, const GroupElement& U_): + F(F_), G(G_), H(H_), U(U_) { +} + +Scalar Claim::challenge( + const Scalar& mu, + const std::vector& identifier, + const std::vector& message, + const std::vector& S, + const std::vector& T, + const GroupElement& A1, + const std::vector& A2 +) { + Transcript transcript(LABEL_TRANSCRIPT_CLAIM); + transcript.add("F", F); + transcript.add("G", G); + transcript.add("H", H); + transcript.add("U", U); + transcript.add("mu", mu); + transcript.add("identifier", identifier); + transcript.add("message", message); + transcript.add("S", S); + transcript.add("T", T); + transcript.add("A1", A1); + transcript.add("A2", A2); + + return transcript.challenge("c"); +} + +void Claim::prove( + const Scalar& mu, + const std::vector& identifier, + const std::vector& message, + const std::vector& x, + const std::vector& y, + const std::vector& z, + const std::vector& S, + const std::vector& T, + ChaumProof& proof +) { + // Check statement validity + std::size_t n = x.size(); + if (!(y.size() == n && z.size() == n && S.size() == n && T.size() == n)) { + throw std::invalid_argument("Bad claim statement!"); + } + for (std::size_t i = 0; i < n; i++) { + if (!(F*x[i] + G*y[i] + H*z[i] == S[i] && T[i]*x[i] + G*y[i] == U)) { + throw std::invalid_argument("Bad claim statement!"); + } + } + + std::vector r; + r.resize(n); + std::vector s; + s.resize(n); + for (std::size_t i = 0; i < n; i++) { + r[i].randomize(); + s[i].randomize(); + } + Scalar t; + t.randomize(); + + proof.A1 = H*t; + proof.A2.resize(n); + for (std::size_t i = 0; i < n; i++) { + proof.A1 += F*r[i] + G*s[i]; + proof.A2[i] = T[i]*r[i] + G*s[i]; + } + + Scalar c = challenge(mu, identifier, message, S, T, proof.A1, proof.A2); + + proof.t1.resize(n); + proof.t3 = t; + Scalar c_power(c); + for (std::size_t i = 0; i < n; i++) { + if (c_power.isZero()) { + throw std::invalid_argument("Unexpected challenge!"); + } + proof.t1[i] = r[i] + c_power*x[i]; + proof.t2 += s[i] + c_power*y[i]; + proof.t3 += c_power*z[i]; + c_power *= c; + } +} + +bool Claim::verify( + const Scalar& mu, + const std::vector& identifier, + const std::vector& message, + const std::vector& S, + const std::vector& T, + const ChaumProof& proof +) { + // Check proof semantics + std::size_t n = S.size(); + if (!(T.size() == n && proof.A2.size() == n && proof.t1.size() == n)) { + throw std::invalid_argument("Bad claim semantics!"); + } + + Scalar c = challenge(mu, identifier, message, S, T, proof.A1, proof.A2); + if (c.isZero()) { + throw std::invalid_argument("Unexpected challenge!"); + } + std::vector c_powers; + c_powers.emplace_back(c); + for (std::size_t i = 1; i < n; i++) { + c_powers.emplace_back(c_powers[i-1]*c); + if (c_powers[i].isZero()) { + throw std::invalid_argument("Unexpected challenge!"); + } + } + + // Weight the verification equations + Scalar w; + while (w.isZero()) { + w.randomize(); + } + + std::vector scalars; + std::vector points; + scalars.reserve(3*n + 5); + points.reserve(3*n + 5); + + // F + Scalar F_scalar; + for (std::size_t i = 0; i < n; i++) { + F_scalar -= proof.t1[i]; + } + scalars.emplace_back(F_scalar); + points.emplace_back(F); + + // G + scalars.emplace_back(proof.t2.negate() - w*proof.t2); + points.emplace_back(G); + + // H + scalars.emplace_back(proof.t3.negate()); + points.emplace_back(H); + + // U + Scalar U_scalar; + for (std::size_t i = 0; i < n; i++) { + U_scalar += c_powers[i]; + } + U_scalar *= w; + scalars.emplace_back(U_scalar); + points.emplace_back(U); + + // A1 + scalars.emplace_back(Scalar((uint64_t) 1)); + points.emplace_back(proof.A1); + + // {A2} + GroupElement A2_sum = proof.A2[0]; + for (std::size_t i = 1; i < n; i++) { + A2_sum += proof.A2[i]; + } + scalars.emplace_back(w); + points.emplace_back(A2_sum); + + // {S} + for (std::size_t i = 0; i < n; i++) { + scalars.emplace_back(c_powers[i]); + points.emplace_back(S[i]); + } + + // {T} + for (std::size_t i = 0; i < n; i++) { + scalars.emplace_back(w.negate()*proof.t1[i]); + points.emplace_back(T[i]); + } + + secp_primitives::MultiExponent multiexp(points, scalars); + // merged equalities and doing check in one multiexponentation, + // for weighting we use random w + return multiexp.get_multiple().isInfinity(); +} + +} diff --git a/src/libspark/claim.h b/src/libspark/claim.h new file mode 100644 index 0000000000..2416248209 --- /dev/null +++ b/src/libspark/claim.h @@ -0,0 +1,52 @@ +#ifndef FIRO_LIBSPARK_CLAIM_H +#define FIRO_LIBSPARK_CLAIM_H + +#include "chaum_proof.h" +#include + +namespace spark { + +// A claim proof, which is used to assert control of the consumed coins in a spend transaction +class Claim { +public: + Claim(const GroupElement& F, const GroupElement& G, const GroupElement& H, const GroupElement& U); + + void prove( + const Scalar& mu, + const std::vector& identifier, + const std::vector& message, + const std::vector& x, + const std::vector& y, + const std::vector& z, + const std::vector& S, + const std::vector& T, + ChaumProof& proof + ); + bool verify( + const Scalar& mu, + const std::vector& identifier, + const std::vector& message, + const std::vector& S, + const std::vector& T, + const ChaumProof& proof + ); + +private: + Scalar challenge( + const Scalar& mu, + const std::vector& identifier, + const std::vector& message, + const std::vector& S, + const std::vector& T, + const GroupElement& A1, + const std::vector& A2 + ); + const GroupElement& F; + const GroupElement& G; + const GroupElement& H; + const GroupElement& U; +}; + +} + +#endif diff --git a/src/libspark/spend_transaction.cpp b/src/libspark/spend_transaction.cpp index d98dc67a15..465f7a90a6 100644 --- a/src/libspark/spend_transaction.cpp +++ b/src/libspark/spend_transaction.cpp @@ -459,4 +459,129 @@ const std::map& SpendTransaction::getBlockHashes() { return set_id_blockHash; } -} \ No newline at end of file +// Generate a claim on a given spend transaction. +// +// A claim is essentially just a Chaum authorizing proof that is bound to a transaction identifier and a message. +// +// To generate the claim, you must provide the same full view and spend keys that were used to generate the spend transaction, as well as the same input coin data. +// You must also provide an identifier that the verifier can use to uniquely identify the spend transaction when it verifies the claim. +// You can also provide an arbitrary message to sign, which can be useful to avoid replays or otherwise bind the claim to some particular context. +void SpendTransaction::proveClaim( + const FullViewKey& full_view_key, + const SpendKey& spend_key, + const SpendTransaction& transaction, + const std::vector& inputs, + const std::vector& identifier, + const std::vector& message, + ChaumProof& claim +) { + // The number of inputs + const std::size_t w = inputs.size(); + + // Check that the input coin data corresponds to the prover statement data we'll get from the spend transaction + // This ensures we don't try to use incorrect data to build the proof! + if (transaction.S1.size() != w || transaction.T.size() != w) { + throw std::invalid_argument("Bad claim input coin data!"); + } + for (std::size_t u = 0; u < w; u++) { + GroupElement S1 = transaction.params->get_F()*inputs[u].s + + transaction.params->get_H().inverse()*SparkUtils::hash_ser1(inputs[u].s, full_view_key.get_D()) + + full_view_key.get_D(); + if (S1 != transaction.S1[u]) { + throw std::invalid_argument("Bad claim input coin data!"); + } + if (inputs[u].T != transaction.T[u]) { + throw std::invalid_argument("Bad claim input coin data!"); + } + } + + // Build the prover witness + std::vector claim_x, claim_y, claim_z; + for (std::size_t u = 0; u < w; u++) { + claim_x.emplace_back(inputs[u].s); + claim_y.emplace_back(spend_key.get_r()); + claim_z.emplace_back(SparkUtils::hash_ser1(inputs[u].s, full_view_key.get_D()).negate()); + } + + // Compute the binding hash + Scalar mu = hash_bind( + hash_bind_inner( + transaction.cover_set_representations, + transaction.S1, + transaction.C1, + transaction.T, + transaction.grootle_proofs, + transaction.balance_proof, + transaction.range_proof + ), + transaction.out_coins, + transaction.f + transaction.vout + ); + + // Produce the claim, binding in the identifier and message + Claim claim_prover( + transaction.params->get_F(), + transaction.params->get_G(), + transaction.params->get_H(), + transaction.params->get_U() + ); + claim_prover.prove( + mu, + identifier, + message, + claim_x, + claim_y, + claim_z, + transaction.S1, + transaction.T, + claim + ); +} + +// Verify a claim on a given spend transaction. +// +// The verifier must have used the identifier to determine its own view of the spend transaction. +// It must not accept a spend transaction from the prover that it has not checked against its own view of the ledger. +// The spend transaction must also have been previously verified. +// +// If verification subsequently succeeds, it means the same spend key was used to produce the claim as was used to produce the spend transaction, and that the provided message was bound to the claim. +// It does _not_ say anything else about the spend transaction! +bool SpendTransaction::verifyClaim( + const SpendTransaction& transaction, + const std::vector& identifier, + const std::vector& message, + const ChaumProof& claim +) { + // Compute the binding hash + Scalar mu = hash_bind( + hash_bind_inner( + transaction.cover_set_representations, + transaction.S1, + transaction.C1, + transaction.T, + transaction.grootle_proofs, + transaction.balance_proof, + transaction.range_proof + ), + transaction.out_coins, + transaction.f + transaction.vout + ); + + // Verify the claim, binding in the identifier and message + Claim claim_verifier( + transaction.params->get_F(), + transaction.params->get_G(), + transaction.params->get_H(), + transaction.params->get_U() + ); + return claim_verifier.verify( + mu, + identifier, + message, + transaction.S1, + transaction.T, + claim + ); +} + +} diff --git a/src/libspark/spend_transaction.h b/src/libspark/spend_transaction.h index 24da12bea2..124fa12e32 100644 --- a/src/libspark/spend_transaction.h +++ b/src/libspark/spend_transaction.h @@ -7,6 +7,7 @@ #include "grootle.h" #include "bpplus.h" #include "chaum.h" +#include "claim.h" namespace spark { @@ -59,6 +60,9 @@ class SpendTransaction { static bool verify(const Params* params, const std::vector& transactions, const std::unordered_map>& cover_sets); static bool verify(const SpendTransaction& transaction, const std::unordered_map>& cover_sets); + + static void proveClaim(const FullViewKey& full_view_key, const SpendKey& spend_key, const SpendTransaction& transaction, const std::vector& inputs, const std::vector& identifier, const std::vector& message, ChaumProof& claim); + static bool verifyClaim(const SpendTransaction& transaction, const std::vector& identifier, const std::vector& message, const ChaumProof& claim); static std::vector hash_bind_inner( const std::map>& cover_set_representations, diff --git a/src/libspark/util.h b/src/libspark/util.h index 015c67e074..ff3c469416 100644 --- a/src/libspark/util.h +++ b/src/libspark/util.h @@ -30,6 +30,7 @@ const std::string LABEL_TRANSCRIPT_BPPLUS = "BULLETPROOF_PLUS_V1"; const std::string LABEL_TRANSCRIPT_CHAUM = "CHAUM_V1"; const std::string LABEL_TRANSCRIPT_GROOTLE = "GROOTLE_V1"; const std::string LABEL_TRANSCRIPT_SCHNORR = "SCHNORR_V1"; +const std::string LABEL_TRANSCRIPT_CLAIM = "CLAIM_V1"; // Generator labels const std::string LABEL_GENERATOR_F = "F";