Skip to content
Draft
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1aeac7c
Preserve SpendKey derivation while clearing secrets
reubenyap Aug 27, 2026
0179de5
Add versioned Spark spend proofs
reubenyap Aug 27, 2026
195d354
Expose Spark V2 transaction construction
reubenyap Aug 27, 2026
ad5292c
Validate decrypted Spark memo payloads
reubenyap Aug 27, 2026
ce6af7e
Use full-width Grootle binding scalars
reubenyap Aug 27, 2026
9509b28
Derive Spark Name proof messages by version
reubenyap Aug 27, 2026
46ee784
Reject unbound V1 extension commitments
reubenyap Aug 28, 2026
d8be8d1
Validate decoded Spark V2 cover-set IDs
reubenyap Aug 28, 2026
9c2ad80
Make Spark V2 guard tests exact
reubenyap Aug 28, 2026
245ad5f
Use standard exceptions for scalar failures
reubenyap Aug 28, 2026
82ec145
Honor caller-provided Spark spend limits
reubenyap Aug 28, 2026
e4e2f79
Fail invalid test runner invocations
reubenyap Aug 28, 2026
23205c2
Make Spark guard tests hit intended failures
reubenyap Aug 28, 2026
f278dbb
Keep Spark parameter sets independent
reubenyap Aug 28, 2026
4df5334
Reject malformed Spark recipient addresses
reubenyap Aug 28, 2026
e8af1df
Return only valid Spark recovery data
reubenyap Aug 28, 2026
b8d10c6
Validate Grootle multiexponent dimensions
reubenyap Aug 28, 2026
cb41264
Handle cryptographic provider failures safely
reubenyap Aug 28, 2026
af893a1
Clean temporary Spark secret material
reubenyap Aug 28, 2026
778d135
Avoid redundant Spark transaction copies
reubenyap Aug 28, 2026
ad4fcea
Support endian conversion on Windows
reubenyap Aug 28, 2026
4fd3c83
Make native test builds reproducible
reubenyap Aug 28, 2026
1d57c4f
Avoid duplicate pull request test runs
reubenyap Aug 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions build
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#!/bin/bash
set -e

if [ -z "$1" ]; then
echo "Please supply a name for the directory to hold the execuatables"
exit
Expand Down
12 changes: 10 additions & 2 deletions include/spark.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const uint32_t DEFAULT_SPARK_NCOUNT = 1;
#define OP_SPARKSPEND 0xd3
// Transaction type (nType)
#define TRANSACTION_SPARK 9
#define TRANSACTION_SPARK_V2 11
// Spark spend version (nVersion)
#define SPARK_TX_VERSION 3
//Diversifier for spark change address,
Expand Down Expand Up @@ -74,17 +75,24 @@ void createSparkSpendTransaction(
const std::map<uint64_t, uint256>& idAndBlockHashes_all,
const uint256& txHashSig,
std::size_t additionalTxSize,
spark::SpendTransactionVersion version,
const uint256& extensionCommitment,
CAmount &fee,
std::vector<uint8_t>& serializedSpend,
std::vector<std::vector<unsigned char>>& outputScripts,
std::vector<CSparkMintMeta>& spentCoinsOut);

void GetSparkNameScript(spark::CSparkNameTxData &sparkNameData,
Scalar m,
const uint256& ownershipDigest,
spark::SpendTransactionVersion version,
const spark::SpendKey& spendKey,
const spark::IncomingViewKey& incomingViewKey,
std::vector<unsigned char>& outputScript);

size_t getSparkNameTxDataSize(const spark::CSparkNameTxData &sparkNameData);
uint256 getSparkNameCommitment(const spark::CSparkNameTxData &sparkNameData);
Scalar getSparkNameOwnershipMessage(
const uint256& digest,
spark::SpendTransactionVersion version);

#endif // SPARK_H
#endif // SPARK_H
3 changes: 3 additions & 0 deletions run_all_tests
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/bin/bash
set -e

if [ -z "$1" ];then
echo "Please supply a name for the directory to hold the execuatables"
Expand All @@ -12,6 +13,8 @@ fi

echo Running Spark Tests
./$1/spark_tests
echo Running Spark Name Tests
./$1/spark_name_tests
echo Running Address Tests
./$1/address_tests
echo Running Ownership Tests
Expand Down
185 changes: 177 additions & 8 deletions src/chaum.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,21 @@ Chaum::Chaum(const GroupElement& F_, const GroupElement& G_, const GroupElement&
F(F_), G(G_), H(H_), U(U_) {
}

Scalar Chaum::challenge(
namespace {

std::vector<unsigned char> EncodeUint64LE(uint64_t value)
{
std::vector<unsigned char> encoded(8);
for (std::size_t i = 0; i < encoded.size(); ++i) {
encoded[i] = static_cast<unsigned char>(value & 0xff);
value >>= 8;
}
return encoded;
}

} // namespace

Scalar Chaum::challenge_v1(
const Scalar& mu,
const std::vector<GroupElement>& S,
const std::vector<GroupElement>& T,
Expand All @@ -28,14 +42,14 @@ Scalar Chaum::challenge(
return transcript.challenge("c");
}

void Chaum::prove(
void Chaum::prove_v1(
const Scalar& mu,
const std::vector<Scalar>& x,
const std::vector<Scalar>& y,
const std::vector<Scalar>& z,
const std::vector<GroupElement>& S,
const std::vector<GroupElement>& T,
ChaumProof& proof
ChaumProofV1& proof
) {
// Check statement validity
std::size_t n = x.size();
Expand Down Expand Up @@ -66,7 +80,7 @@ void Chaum::prove(
proof.A2[i] = T[i]*r[i] + G*s[i];
}

Scalar c = challenge(mu, S, T, proof.A1, proof.A2);
Scalar c = challenge_v1(mu, S, T, proof.A1, proof.A2);

proof.t1.resize(n);
proof.t3 = t;
Expand All @@ -82,24 +96,27 @@ void Chaum::prove(
}
}

bool Chaum::verify(
bool Chaum::verify_v1(
const Scalar& mu,
const std::vector<GroupElement>& S,
const std::vector<GroupElement>& T,
ChaumProof& proof
ChaumProofV1& proof
) {
// Check proof semantics
std::size_t n = S.size();
if (!(T.size() == n && proof.A2.size() == n && proof.t1.size() == n)) {
if (n == 0 || !(T.size() == n && proof.A2.size() == n && proof.t1.size() == n)) {
throw std::invalid_argument("Bad Chaum semantics!");
}
for (std::size_t i = 0; i < n; i++) {
if (S[i].isInfinity()) {
throw std::invalid_argument("Bad Chaum input!");
}
if (T[i].isInfinity()) {
throw std::invalid_argument("Bad Chaum input!");
}
}

Scalar c = challenge(mu, S, T, proof.A1, proof.A2);
Scalar c = challenge_v1(mu, S, T, proof.A1, proof.A2);
if (c.isZero()) {
throw std::invalid_argument("Unexpected challenge!");
}
Expand Down Expand Up @@ -178,4 +195,156 @@ bool Chaum::verify(
return multiexp.get_multiple().isInfinity();
}

bool Chaum::verify_single_input(
const Scalar& mu,
const std::vector<GroupElement>& S,
const std::vector<GroupElement>& T,
ChaumProofV1& proof
) {
if (S.size() != 1 || T.size() != 1) {
return false;
}
if (proof.A2.size() != 1 || proof.t1.size() != 1 ||
S[0].isInfinity() || T[0].isInfinity()) {
throw std::invalid_argument("Bad Chaum single-input semantics!");
}

const Scalar c = challenge_v1(mu, S, T, proof.A1, proof.A2);
if (c.isZero()) {
throw std::invalid_argument("Unexpected challenge!");
}

const GroupElement firstLeft = proof.A1 + S[0]*c;
const GroupElement firstRight =
F*proof.t1[0] + G*proof.t2 + H*proof.t3;
if (firstLeft != firstRight) {
return false;
}

const GroupElement secondLeft = proof.A2[0] + U*c;
const GroupElement secondRight = T[0]*proof.t1[0] + G*proof.t2;
return secondLeft == secondRight;
}

Scalar Chaum::challenge_v2(
const Scalar& mu,
const ChaumV2Context& context,
const std::vector<GroupElement>& S,
const std::vector<GroupElement>& T,
const std::vector<GroupElement>& A1,
const std::vector<GroupElement>& A2
) {
Transcript transcript(LABEL_TRANSCRIPT_CHAUM_V2);
transcript.add("version", std::vector<unsigned char>{2});
transcript.add("input_count", EncodeUint64LE(S.size()));
transcript.add("F", F);
transcript.add("G", G);
transcript.add("H", H);
transcript.add("U", U);
transcript.add("mu", mu);
transcript.add("S", S);
transcript.add("T", T);
transcript.add("A1", A1);
transcript.add("A2", A2);
transcript.add(
"cover_set_references",
context.serialized_cover_set_references);
transcript.add("outputs", context.serialized_outputs);
transcript.add("fee", EncodeUint64LE(context.fee));
transcript.add("transparent_value", EncodeUint64LE(context.transparent_value));
transcript.add(
"extension_commitment",
std::vector<unsigned char>(
context.extension_commitment.begin(),
context.extension_commitment.end()));
return transcript.challenge("c");
}

void Chaum::prove_v2(
const Scalar& mu,
const ChaumV2Context& context,
const std::vector<Scalar>& x,
const std::vector<Scalar>& y,
const std::vector<Scalar>& z,
const std::vector<GroupElement>& S,
const std::vector<GroupElement>& T,
ChaumProofV2& proof
) {
const std::size_t n = x.size();
if (n == 0 || n > MAX_CHAUM_V2_INPUTS ||
y.size() != n || z.size() != n || S.size() != n || T.size() != n) {
throw std::invalid_argument("Bad Chaum V2 statement");
}
for (std::size_t i = 0; i < n; ++i) {
if (S[i].isInfinity() || T[i].isInfinity() ||
F*x[i] + G*y[i] + H*z[i] != S[i] ||
T[i]*x[i] + G*y[i] != U) {
throw std::invalid_argument("Bad Chaum V2 statement");
}
}

std::vector<Scalar> r(n), s(n), t(n);
proof.A1.resize(n);
proof.A2.resize(n);
for (std::size_t i = 0; i < n; ++i) {
r[i].randomize();
s[i].randomize();
t[i].randomize();
proof.A1[i] = F*r[i] + G*s[i] + H*t[i];
proof.A2[i] = T[i]*r[i] + G*s[i];
}

const Scalar c = challenge_v2(mu, context, S, T, proof.A1, proof.A2);
if (c.isZero()) {
throw std::invalid_argument("Unexpected Chaum V2 challenge");
}

proof.t1.resize(n);
proof.t2.resize(n);
proof.t3.resize(n);
for (std::size_t i = 0; i < n; ++i) {
proof.t1[i] = r[i] + c*x[i];
proof.t2[i] = s[i] + c*y[i];
proof.t3[i] = t[i] + c*z[i];
}
}

bool Chaum::verify_v2(
const Scalar& mu,
const ChaumV2Context& context,
const std::vector<GroupElement>& S,
const std::vector<GroupElement>& T,
const ChaumProofV2& proof
) {
const std::size_t n = S.size();
if (n == 0 || n > MAX_CHAUM_V2_INPUTS || T.size() != n ||
proof.A1.size() != n || proof.A2.size() != n ||
proof.t1.size() != n || proof.t2.size() != n || proof.t3.size() != n) {
return false;
}
for (std::size_t i = 0; i < n; ++i) {
if (S[i].isInfinity() || T[i].isInfinity() ||
proof.A1[i].isInfinity() || proof.A2[i].isInfinity()) {
return false;
}
}

const Scalar c = challenge_v2(mu, context, S, T, proof.A1, proof.A2);
if (c.isZero()) {
return false;
}

// Check all 2*n equations directly. This correctness-oriented verifier
// deliberately avoids probabilistic batching until independently derived
// coefficients and the final transcript have received cryptographic review.
for (std::size_t i = 0; i < n; ++i) {
if (proof.A1[i] + S[i]*c !=
F*proof.t1[i] + G*proof.t2[i] + H*proof.t3[i] ||
proof.A2[i] + U*c != T[i]*proof.t1[i] + G*proof.t2[i]) {
return false;
}
}
return true;
}

}
52 changes: 47 additions & 5 deletions src/chaum.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,80 @@
#define FIRO_LIBSPARK_CHAUM_H

#include "chaum_proof.h"
#include "../bitcoin/uint256.h"
#include "../secp256k1/include/MultiExponent.h"

namespace spark {

struct ChaumV2Context {
uint64_t fee = 0;
uint64_t transparent_value = 0;
std::vector<std::vector<unsigned char>> serialized_outputs;
uint256 extension_commitment{};
std::vector<unsigned char> serialized_cover_set_references;
};

class Chaum {
public:
Chaum(const GroupElement& F, const GroupElement& G, const GroupElement& H, const GroupElement& U);

void prove(
void prove_v1(
const Scalar& mu,
const std::vector<Scalar>& x,
const std::vector<Scalar>& y,
const std::vector<Scalar>& z,
const std::vector<GroupElement>& S,
const std::vector<GroupElement>& T,
ChaumProof& proof
ChaumProofV1& proof
);
// Original verification retained for pre-activation historical blocks.
bool verify_v1(
const Scalar& mu,
const std::vector<GroupElement>& S,
const std::vector<GroupElement>& T,
ChaumProofV1& proof
);
bool verify(
bool verify_single_input(
const Scalar& mu,
const std::vector<GroupElement>& S,
const std::vector<GroupElement>& T,
ChaumProof& proof
ChaumProofV1& proof
);

void prove_v2(
const Scalar& mu,
const ChaumV2Context& context,
const std::vector<Scalar>& x,
const std::vector<Scalar>& y,
const std::vector<Scalar>& z,
const std::vector<GroupElement>& S,
const std::vector<GroupElement>& T,
ChaumProofV2& proof
);
bool verify_v2(
const Scalar& mu,
const ChaumV2Context& context,
const std::vector<GroupElement>& S,
const std::vector<GroupElement>& T,
const ChaumProofV2& proof
);

private:
Scalar challenge(
Scalar challenge_v1(
const Scalar& mu,
const std::vector<GroupElement>& S,
const std::vector<GroupElement>& T,
const GroupElement& A1,
const std::vector<GroupElement>& A2
);
Scalar challenge_v2(
const Scalar& mu,
const ChaumV2Context& context,
const std::vector<GroupElement>& S,
const std::vector<GroupElement>& T,
const std::vector<GroupElement>& A1,
const std::vector<GroupElement>& A2
);
const GroupElement& F;
const GroupElement& G;
const GroupElement& H;
Expand Down
Loading