Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
66922dc
merge + initial scaffolding
saefstroem Apr 11, 2026
b9dbe17
Implement zk sdk
saefstroem Apr 12, 2026
156ca8b
Update mod.rs
saefstroem Apr 12, 2026
5ea0fb5
Merge branch 'covpp-reset2' into zk-sdk
saefstroem Apr 20, 2026
d25305c
Update sdk
saefstroem Apr 20, 2026
d771435
Update sdk
saefstroem Apr 20, 2026
f893dbd
Remove unneeded deps
saefstroem Apr 20, 2026
260c9cb
Update Cargo.lock
saefstroem Apr 20, 2026
1cdebb7
Update error.rs
saefstroem Apr 20, 2026
3209ce8
risc0: use serialized VK and r0 byte parsing
saefstroem Apr 25, 2026
d235c6b
Merge branch 'covpp-reset2' into zk-sdk
saefstroem May 1, 2026
43f9657
Add comments + clippy
saefstroem May 1, 2026
1e005c7
Add R0 zk_to_script builders and groth16 support
saefstroem May 5, 2026
786814a
Merge branch 'covpp-reset2' into zk-sdk
saefstroem May 5, 2026
e062651
Merge remote-tracking branch 'upstream/toccata' into zk-sdk
saefstroem May 5, 2026
88527d2
Update zk builder and Cargo.lock for risc0/groth16
saefstroem May 5, 2026
b1cadf6
clippy
saefstroem May 5, 2026
41de812
Add wasm
saefstroem May 5, 2026
3bad7a7
Updated test examples
saefstroem May 5, 2026
538eb42
Update js test scripts to use compute budget and new tx version
saefstroem May 5, 2026
5798082
fmt
saefstroem May 5, 2026
373f780
revert devnet change
saefstroem May 5, 2026
0e11699
remove
saefstroem May 5, 2026
a136c51
Update all js examples
saefstroem May 6, 2026
10acbd7
fix toml
saefstroem May 6, 2026
b603891
Merge branch 'toccata' into zk-sdk
saefstroem May 13, 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
590 changes: 553 additions & 37 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ license = "ISC"
repository = "https://github.com/kaspanet/rusty-kaspa"
edition = "2024"
include = [
"src/**/*.rs",
"src/*risc0-zkvm.workspace*/*.rs",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this intentional or a leftover? why does that path need special treatment here?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right must be a leftover, fixing.

"src/**/*.s",
"src/**/*.r",
"proto/**/*.proto",
Expand Down Expand Up @@ -227,6 +227,7 @@ md-5 = "0.10.6"
num = "0.4.1"
num_cpus = "1.16.0"
num-traits = "0.2.17"
num-bigint = "0.4.6"
once_cell = { version = "1.18.0", default-features = false }
pad = "0.1.6"
parking_lot = "0.12.1"
Expand All @@ -246,6 +247,7 @@ risc0-circuit-keccak = { version="4.0.3", default-features=false }
risc0-zkp = "3.0.3"
risc0-core = "3.0.0"
risc0-binfmt = "3.0.3"
risc0-zkvm = "3.0.3"
ripemd = { version = "0.1.3", default-features = false }
rlimit = "0.10.1"
rocksdb = "0.24.0"
Expand Down
3 changes: 3 additions & 0 deletions crypto/txscript/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ risc0-binfmt.workspace = true
risc0-circuit-recursion.workspace = true
risc0-core.workspace = true
risc0-zkp.workspace = true
risc0-zkvm.workspace = true
risc0-groth16.workspace = true
secp256k1.workspace = true
serde-wasm-bindgen.workspace = true
serde.workspace = true
Expand All @@ -55,6 +57,7 @@ thiserror.workspace = true
wasm-bindgen.workspace = true
workflow-wasm.workspace = true


[build-dependencies]
cc = { workspace = true }

Expand Down
1 change: 1 addition & 0 deletions crypto/txscript/src/zk_precompiles/error.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use kaspa_txscript_errors::TxScriptError;
use risc0_zkvm::PrunedValueError;
use thiserror::Error;

#[derive(Debug, Error)]
Expand Down
10 changes: 10 additions & 0 deletions crypto/txscript/src/zk_precompiles/fields/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ impl Fr {
}
}

impl TryInto<Vec<u8>> for Fr {
type Error = FieldsError;

fn try_into(self) -> Result<Vec<u8>, Self::Error> {
let mut bytes = Vec::new();
self.0.serialize_uncompressed(&mut bytes).map_err(|e| FieldsError::ArkSerialization(e))?;
Ok(bytes)
}
}

impl TryFrom<&[u8]> for Fr {
type Error = FieldsError;

Expand Down
2 changes: 2 additions & 0 deletions crypto/txscript/src/zk_precompiles/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
mod error;
mod fields;
pub mod groth16;
mod points;
mod result;
pub mod risc0;
pub mod tags;
pub mod tests;
Expand Down
11 changes: 11 additions & 0 deletions crypto/txscript/src/zk_precompiles/points/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use thiserror::Error;

#[derive(Debug, Error)]
pub enum PointError {
#[error("Malformed G1 field element")]
MalformedG1,
#[error("Malformed G2 field element")]
MalformedG2,
#[error("Ark deserialization error: {0}")]
ArkDeserialization(#[from] ark_serialize::SerializationError),
}
57 changes: 57 additions & 0 deletions crypto/txscript/src/zk_precompiles/points/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
mod error;
use ark_bn254::{G1Affine, G2Affine};
use ark_serialize::CanonicalDeserialize;

pub trait PointFromBytes<'input>: Sized {
type Input: ?Sized;
fn from_bytes(bytes: &'input Self::Input) -> Result<Self, PointError>;
}
pub use error::PointError;
pub struct G1(pub G1Affine);
pub struct G2(pub G2Affine);

impl<'input> PointFromBytes<'input> for G1 {
type Input = Vec<Vec<u8>>;

/// Deserialize an element over the G1 group from bytes in big-endian format
fn from_bytes(bytes: &Self::Input) -> Result<G1, PointError> {
if bytes.len() != 2 {
return Err(PointError::MalformedG1);
}
let g1_affine: Vec<u8> = bytes[0].iter().rev().chain(bytes[1].iter().rev()).cloned().collect();
Comment on lines +14 to +21
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why shout it be 2 dimensional reversed vectors?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

r0 stores in BE order whilst ark expects LE, but I agree here that the fn name should be more specific to r0, not generic from_bytes


Ok(G1(G1Affine::deserialize_uncompressed(&*g1_affine)?))
}
}

impl<'input> PointFromBytes<'input> for G2 {
type Input = Vec<Vec<Vec<u8>>>;

fn from_bytes(bytes: &Self::Input) -> Result<G2, PointError> {
if bytes.len() != 2 || bytes[0].len() != 2 || bytes[1].len() != 2 {
return Err(PointError::MalformedG2);
}
let g2_affine: Vec<u8> = bytes[0][1]
.iter()
.rev()
.chain(bytes[0][0].iter().rev())
.chain(bytes[1][1].iter().rev())
.chain(bytes[1][0].iter().rev())
.cloned()
.collect();
Comment on lines +31 to +41
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why shout it be 2 dimensional reversed vectors?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see above


Ok(G2(G2Affine::deserialize_uncompressed(&*g2_affine)?))
}
}

impl Into<G1Affine> for G1 {
fn into(self) -> G1Affine {
self.0
}
}

impl Into<G2Affine> for G2 {
fn into(self) -> G2Affine {
self.0
}
}
3 changes: 3 additions & 0 deletions crypto/txscript/src/zk_precompiles/result.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
use crate::zk_precompiles::error::ZkIntegrityError;

pub type Result<T> = std::result::Result<T, ZkIntegrityError>;
Empty file.
22 changes: 22 additions & 0 deletions crypto/txscript/src/zk_precompiles/risc0/error.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use kaspa_txscript_errors::TxScriptError;
use risc0_zkp::verify::VerificationError;
use risc0_zkvm::PrunedValueError;
use thiserror::Error;

use crate::zk_precompiles::fields::error::FieldsError;

#[derive(Debug, Error)]
pub enum R0Error {
#[error("Std io error: {0}")]
Expand All @@ -28,4 +31,23 @@ pub enum R0Error {
Merkle,
#[error("Invalid BabyBearElem in seal")]
SealHasInvalidBabyBearElem,
#[error("Script builder error: {0}")]
ScriptBuilder(#[from] crate::script_builder::ScriptBuilderError),

#[error("Fields error: {0}")]
Fields(#[from] FieldsError),

#[error("Seal decoding error: {0}")]
SealDecoding(String),

#[error("Bincode VK serialization failed")]
BincodeVkSerialization,

#[error("Point error: {0}")]
Point(#[from] crate::zk_precompiles::points::PointError),

#[error("Ark serialization error: {0}")]
ArkSerialization(#[from] ark_serialize::SerializationError),
//#[error("Parse bigint error: {0}")]
//ParseBigInt(#[from] num_bigint::ParseBigIntError),
}
3 changes: 3 additions & 0 deletions crypto/txscript/src/zk_precompiles/risc0/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@ use crate::{
},
};
use kaspa_txscript_errors::TxScriptError;
use risc0_circuit_recursion::control_id;
use risc0_core::{field::Elem, field::baby_bear::BabyBearElem};
use risc0_zkp::core::digest::DIGEST_BYTES;
pub use risc0_zkp::core::digest::Digest;
mod error;
pub mod merkle;
pub mod rcpt;
pub mod receipt_claim;
mod result;
pub mod zk_to_script;

pub struct R0SuccinctPrecompile;
pub use error::R0Error;
Expand Down
14 changes: 13 additions & 1 deletion crypto/txscript/src/zk_precompiles/risc0/rcpt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,19 @@ impl TryFrom<u8> for HashFnId {
}
}

impl TryFrom<&String> for HashFnId {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

&str

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fromStr seems a better trait candidate than tryfrom

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why what if you pass incorrect string? then everything will panic

type Error = R0Error;

fn try_from(value: &String) -> Result<Self, Self::Error> {
match value.as_str() {
"blake2b" => Ok(HashFnId::Blake2b),
"poseidon2" => Ok(HashFnId::Poseidon2),
"sha-256" => Ok(HashFnId::Sha256),
_ => Err(R0Error::InvalidHashFnId(value.as_bytes().get(0).copied().unwrap_or(255))),
}
}
}

impl From<HashFnId> for u8 {
fn from(value: HashFnId) -> Self {
value as u8
Expand All @@ -70,7 +83,6 @@ pub struct SuccinctReceipt {
/// The control ID of this receipt, identifying the recursion program that was run (e.g. lift,
/// join, or resolve).
control_id: Digest,

/// Claim containing information about the computation that this receipt proves.
///
/// The standard claim type is [ReceiptClaim][crate::ReceiptClaim], which represents a RISC-V
Expand Down
3 changes: 3 additions & 0 deletions crypto/txscript/src/zk_precompiles/risc0/result.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
use crate::zk_precompiles::risc0::R0Error;

pub type Result<T> = std::result::Result<T, R0Error>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
pub mod vk;
use super::super::result::Result;
use crate::{
opcodes::codes::OpZkPrecompile,
script_builder::ScriptBuilder,
zk_precompiles::{
fields::Fr,
points::{G1, G2, PointFromBytes},
risc0::{
R0Error,
zk_to_script::{R0ScriptBuilder, groth16::vk::try_verifying_key},
},
tags::ZkTag,
},
};
use ark_bn254::{Bn254, Config};
use ark_ec::bn::Bn;
use ark_groth16::{Proof, VerifyingKey};
use ark_serialize::{CanonicalDeserialize, CanonicalSerialize};
use risc0_binfmt::Digestible;
use risc0_groth16::Seal;
use risc0_zkvm::{Digest, Groth16Receipt, Groth16ReceiptVerifierParameters, MaybePruned, SuccinctReceipt, sha};
fn split_digest_bytes(d: Digest) -> ([u8; 32], [u8; 32]) {
let bytes = d.as_bytes();
let mut lo = [0u8; 32];
let mut hi = [0u8; 32];
lo[..16].copy_from_slice(&bytes[..16]);
hi[..16].copy_from_slice(&bytes[16..32]);
(lo, hi)
}

fn to_fixed_array(input: &[u8]) -> [u8; 32] {
let mut fixed_array = [0u8; 32];
let start = core::cmp::max(32, input.len()) - core::cmp::min(32, input.len());
fixed_array[start..].copy_from_slice(&input[input.len().saturating_sub(32)..]);
fixed_array
}
impl R0ScriptBuilder {
/// Converts a Groth16Receipt into a Kaspa script.
/// This script unlocks the UTXO if the verification of the receipt
/// succeeds.
pub fn from_groth<Claim: Digestible + Clone>(receipt: &Groth16Receipt<Claim>) -> Result<ScriptBuilder> {
let mut params = Groth16ReceiptVerifierParameters::default();
let seal = &receipt.seal;
let digested_claim = receipt.claim.digest::<sha::Impl>();
let (a0, a1) = split_digest_bytes(params.control_root);
let (c0, c1) = split_digest_bytes(digested_claim);
let id_bn254 = to_fixed_array(params.bn254_control_id.as_bytes());
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

digest impls Into into [u8;32]. method seems redundant

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fair point

let seal = Seal::decode(seal).map_err(|e| R0Error::SealDecoding(e.to_string()))?;
let verifying_key = try_verifying_key()?;

let g1 = G1::from_bytes(&seal.a)?;
let g1_c = G1::from_bytes(&seal.c)?;
let g2 = G2::from_bytes(&seal.b)?;
let mut encoded_proof = Vec::new();
let proof: Proof<ark_ec::bn::Bn<ark_bn254::Config>> = Proof::<Bn254> { a: g1.0, b: g2.0, c: g1_c.0 };
proof.serialize_compressed(&mut encoded_proof)?;
// Serialize with serde_ark feature which under the hood is just
// uncompressed serialization.
// Re-serialize then deserialize to get the inner ark VK
let mut serialized_vk = Vec::new();
verifying_key.serialize_compressed(&mut serialized_vk).map_err(|_| R0Error::BincodeVkSerialization)?;
let mut builder = ScriptBuilder::new();
builder.add_data(&id_bn254)?;
builder.add_data(&c1)?;
builder.add_data(&c0)?;
builder.add_data(&a1)?;
builder.add_data(&a0)?;
builder.add_i64(5)?;
builder.add_data(&encoded_proof)?;
builder.add_data(&serialized_vk)?;
builder.add_data(&[ZkTag::Groth16 as u8])?;
builder.add_op(OpZkPrecompile)?;
Ok(builder)
}
}
// build_zk_script(&[seal, claim, hashfn, control_index, control_digests, journal, image_id, vec![stark_tag]]).unwrap()
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
use std::str::FromStr;

use ark_bn254::Bn254;
use num_bigint::BigInt;

use crate::zk_precompiles::{
points::{G1, G2, PointFromBytes},
risc0::{R0Error, zk_to_script::groth16::to_fixed_array},
};

// Constants from: risc0-ethereum/contracts/src/groth16/Groth16Verifier.sol
// When running a new ceremony, update them by running cargo xtask bootstrap-groth16
// after updating the new Groth16Verifier.sol on the risc0-ethereum repo.
const ALPHA_X: &str = "20491192805390485299153009773594534940189261866228447918068658471970481763042";
const ALPHA_Y: &str = "9383485363053290200918347156157836566562967994039712273449902621266178545958";
const BETA_X1: &str = "4252822878758300859123897981450591353533073413197771768651442665752259397132";
const BETA_X2: &str = "6375614351688725206403948262868962793625744043794305715222011528459656738731";
const BETA_Y1: &str = "21847035105528745403288232691147584728191162732299865338377159692350059136679";
const BETA_Y2: &str = "10505242626370262277552901082094356697409835680220590971873171140371331206856";
const GAMMA_X1: &str = "11559732032986387107991004021392285783925812861821192530917403151452391805634";
const GAMMA_X2: &str = "10857046999023057135944570762232829481370756359578518086990519993285655852781";
const GAMMA_Y1: &str = "4082367875863433681332203403145435568316851327593401208105741076214120093531";
const GAMMA_Y2: &str = "8495653923123431417604973247489272438418190587263600148770280649306958101930";
const DELTA_X1: &str = "1668323501672964604911431804142266013250380587483576094566949227275849579036";
const DELTA_X2: &str = "12043754404802191763554326994664886008979042643626290185762540825416902247219";
const DELTA_Y1: &str = "7710631539206257456743780535472368339139328733484942210876916214502466455394";
const DELTA_Y2: &str = "13740680757317479711909903993315946540841369848973133181051452051592786724563";

const IC0_X: &str = "8446592859352799428420270221449902464741693648963397251242447530457567083492";
const IC0_Y: &str = "1064796367193003797175961162477173481551615790032213185848276823815288302804";
const IC1_X: &str = "3179835575189816632597428042194253779818690147323192973511715175294048485951";
const IC1_Y: &str = "20895841676865356752879376687052266198216014795822152491318012491767775979074";
const IC2_X: &str = "5332723250224941161709478398807683311971555792614491788690328996478511465287";
const IC2_Y: &str = "21199491073419440416471372042641226693637837098357067793586556692319371762571";
const IC3_X: &str = "12457994489566736295787256452575216703923664299075106359829199968023158780583";
const IC3_Y: &str = "19706766271952591897761291684837117091856807401404423804318744964752784280790";
const IC4_X: &str = "19617808913178163826953378459323299110911217259216006187355745713323154132237";
const IC4_Y: &str = "21663537384585072695701846972542344484111393047775983928357046779215877070466";
const IC5_X: &str = "6834578911681792552110317589222010969491336870276623105249474534788043166867";
const IC5_Y: &str = "15060583660288623605191393599883223885678013570733629274538391874953353488393";

/// Convert a decimal U256 string to a 32-byte big-endian Vec.
fn from_u256(value: &str) -> Result<Vec<u8>, R0Error> {
let bytes = BigInt::from_str(value)?.to_bytes_be().1;
Ok(to_fixed_array(&bytes).to_vec())
}

pub fn try_verifying_key() -> Result<ark_groth16::VerifyingKey<Bn254>, R0Error> {
let alpha_g1 = G1::from_bytes(&vec![from_u256(ALPHA_X)?, from_u256(ALPHA_Y)?])?.0;
let beta_g2 =
G2::from_bytes(&vec![vec![from_u256(BETA_X1)?, from_u256(BETA_X2)?], vec![from_u256(BETA_Y1)?, from_u256(BETA_Y2)?]])?.0;
let gamma_g2 =
G2::from_bytes(&vec![vec![from_u256(GAMMA_X1)?, from_u256(GAMMA_X2)?], vec![from_u256(GAMMA_Y1)?, from_u256(GAMMA_Y2)?]])?.0;
let delta_g2 =
G2::from_bytes(&vec![vec![from_u256(DELTA_X1)?, from_u256(DELTA_X2)?], vec![from_u256(DELTA_Y1)?, from_u256(DELTA_Y2)?]])?.0;

let gamma_abc_g1 = vec![
G1::from_bytes(&vec![from_u256(IC0_X)?, from_u256(IC0_Y)?])?.0,
G1::from_bytes(&vec![from_u256(IC1_X)?, from_u256(IC1_Y)?])?.0,
G1::from_bytes(&vec![from_u256(IC2_X)?, from_u256(IC2_Y)?])?.0,
G1::from_bytes(&vec![from_u256(IC3_X)?, from_u256(IC3_Y)?])?.0,
G1::from_bytes(&vec![from_u256(IC4_X)?, from_u256(IC4_Y)?])?.0,
G1::from_bytes(&vec![from_u256(IC5_X)?, from_u256(IC5_Y)?])?.0,
];

Ok(ark_groth16::VerifyingKey::<Bn254> { alpha_g1, beta_g2, gamma_g2, delta_g2, gamma_abc_g1 })
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the key is constant, why is it calculated in runtime. why does it require vector allocations and conversions from string

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, let me fix this

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is no test that ensures its the same calculatuon of verifying key as upstream crate has

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no way to extract inner verifying key from r0 repo its only public to the crate see: pub struct VerifyingKey(pub(crate) VerifyingKey<Bn<Config>>) in risc0_groth16::verifier

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an indirect test, there is an r0 proof that verifies using this key.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But ill make it better

4 changes: 4 additions & 0 deletions crypto/txscript/src/zk_precompiles/risc0/zk_to_script/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
mod groth16;
mod succinct;

pub struct R0ScriptBuilder;
Loading
Loading