Skip to content

Zk sdk#953

Open
saefstroem wants to merge 26 commits into
kaspanet:toccatafrom
saefstroem:zk-sdk
Open

Zk sdk#953
saefstroem wants to merge 26 commits into
kaspanet:toccatafrom
saefstroem:zk-sdk

Conversation

@saefstroem
Copy link
Copy Markdown
Collaborator

This PR implements the beginning of a ZK sdk. The goal with this SDK is to reduce the knowledge requirement factor when an actor needs to deal with UTXO based ZK systems that are largely non-existent in the same relative scale as others.

The sdk allows one to convert any R0 proof whether this is a SuccinctReceipt or Groth16Receipt into a native Kaspa script, reducing the workload for protocols to migrate onto Kaspa and barrier for adoption.

Easy conversion from risc0 proof to kaspa script!
@IzioDev
Copy link
Copy Markdown
Collaborator

IzioDev commented Apr 14, 2026

Nice! Should we start investigating wasm bridging for r0 script builder? Since the SDK will mostly (ark seems no-std compat) depends on types (and no native calls?), it should be doable, what do you think?

@saefstroem
Copy link
Copy Markdown
Collaborator Author

Nice! Should we start investigating wasm bridging for r0 script builder? Since the SDK will mostly (ark seems no-std compat) depends on types (and no native calls?), it should be doable, what do you think?

hey, yeah I think this is a good idea. let me check on this on my next pass on this

Comment on lines +14 to +21
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();
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

Comment on lines +31 to +41
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();
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

Comment on lines +32 to +48
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

Comment on lines +48 to +67
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

Comment on lines +48 to +67
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.

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

}
}

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

}
}

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.

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

Switch Groth16 handling to use a static R0 serialized uncompressed verifying key and r0-specific byte deserialization. Rename PointFromBytes::from_bytes to from_r0_bytes and update callers; replace runtime VK construction with R0_SERIALIZED_UNCOMPRESSED_VK and deserialize_uncompressed. Simplify digest/id conversions and remove legacy fixed-array helper. Add num-bigint parse error variant and adjust HashFnId::TryFrom to accept &str. Remove unused result module and clean up unused imports. Also add workspace entries for hex, bincode and num-bigint and add a test to validate upstream VK serialization parity.
@saefstroem
Copy link
Copy Markdown
Collaborator Author

There are some elements which I have identified to be missing, most importantly the generation of safe bounded Groth16 script by image id.

saefstroem added 11 commits May 5, 2026 16:03
Introduce a builder API and wiring to convert RISC0 receipts into Kaspa scripts. Added zk_to_script builder modules (bind, finalize, tag, proof with groth16 and succinct backends) and a stateful R0ScriptBuilder type. Implemented Groth16 support: serializing VK/proofs, static R0 VK bytes, append_locking_groth16 and append_spending_groth16 helpers, and digest-splitting for BN254 compatibility. Added succinct proof handling to pack seal, control proof, and journal into scripts. Updated groth16 locking logic to recompute claim/output digests and assemble precompile inputs. Expanded tests to exercise new builders and script assembly. Also modified rcpt.rs to emit a debug hex of a potential control id (println present).
Add new zk_to_script builder backends (commit/groth16, commit/succinct, and related proof modules), update builder module wiring and Groth16 VK handling, and adjust rcpt handling and tests (updated image/rcpt hex fixtures). Remove several legacy/unused riscv builder files (converter, bind, finalize, tag, some groth16 modules). Also update Cargo.lock to reflect added/updated dependencies required for risc0/ark groth16 integration and other dependency bumps. These changes enable risc0 groth16/succinct zk-to-script support and pin the corresponding crate versions.
Comment thread Cargo.toml Outdated
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants