diff --git a/crates/core/executor/src/air.rs b/crates/core/executor/src/air.rs index 8c75dd9d46..6f83a3f8aa 100644 --- a/crates/core/executor/src/air.rs +++ b/crates/core/executor/src/air.rs @@ -190,6 +190,14 @@ pub enum RiscvAirId { /// The ALU x0 chip (all ALU ops with rd = x0). #[subenum(CoreAirId)] AluX0 = 61, + /// The septic curve add assign chip. + SepticAddAssign = 62, + /// The septic curve double assign chip. + SepticDoubleAssign = 63, + /// The septic curve scalar mul assign chip. + SepticScalarMulAssign = 64, + /// The septic curve Schnorr verify chip (Shamir's trick). + SepticVerify = 65, } impl RiscvAirId { @@ -274,6 +282,10 @@ impl RiscvAirId { | RiscvAirId::Bn254Fp2AddSubAssign | RiscvAirId::Bn254Fp2MulAssign | RiscvAirId::Poseidon2 + | RiscvAirId::SepticAddAssign + | RiscvAirId::SepticDoubleAssign + | RiscvAirId::SepticScalarMulAssign + | RiscvAirId::SepticVerify ) } diff --git a/crates/core/executor/src/artifacts/rv64im_costs.json b/crates/core/executor/src/artifacts/rv64im_costs.json index e9c3460688..97f3fa6707 100644 --- a/crates/core/executor/src/artifacts/rv64im_costs.json +++ b/crates/core/executor/src/artifacts/rv64im_costs.json @@ -60,5 +60,9 @@ "SyscallCore": 10, "Bls12381FpOpAssign": 450, "ShaCompress": 206, - "LoadDouble": 39 + "LoadDouble": 39, + "SepticAddAssign": 1599, + "SepticDoubleAssign": 1591, + "SepticScalarMulAssign": 519792, + "SepticVerify": 609219 } \ No newline at end of file diff --git a/crates/core/executor/src/minimal/ecall.rs b/crates/core/executor/src/minimal/ecall.rs index acdde77292..252c807b64 100644 --- a/crates/core/executor/src/minimal/ecall.rs +++ b/crates/core/executor/src/minimal/ecall.rs @@ -7,6 +7,7 @@ use super::{ fptower::{fp2_addsub_syscall, fp2_mul_syscall, fp_op_syscall}, keccak::keccak_permute, poseidon2::poseidon2, + septic::{septic_add, septic_double, septic_scalar_mul, septic_verify}, sha256::{sha256_compress, sha256_extend}, uint256::uint256_mul, uint256_ops::uint256_ops, @@ -152,6 +153,10 @@ pub fn ecall_handler(ctx: &mut impl SyscallContext, code: SyscallCode) -> u64 { uint256_ops(ctx, arg1, arg2) }, SyscallCode::POSEIDON2 => unsafe { poseidon2(ctx, arg1, arg2) }, + SyscallCode::SEPTIC_ADD => unsafe { septic_add(ctx, arg1, arg2) }, + SyscallCode::SEPTIC_DOUBLE => unsafe { septic_double(ctx, arg1, arg2) }, + SyscallCode::SEPTIC_SCALAR_MUL => unsafe { septic_scalar_mul(ctx, arg1, arg2) }, + SyscallCode::SEPTIC_VERIFY => unsafe { septic_verify(ctx, arg1, arg2) }, SyscallCode::HALT => { ctx.set_exit_code(arg1 as u32); None diff --git a/crates/core/executor/src/minimal/precompiles/mod.rs b/crates/core/executor/src/minimal/precompiles/mod.rs index 4bfb94abc4..78ac37447b 100644 --- a/crates/core/executor/src/minimal/precompiles/mod.rs +++ b/crates/core/executor/src/minimal/precompiles/mod.rs @@ -4,6 +4,7 @@ pub mod edwards; pub mod fptower; pub mod keccak; pub mod poseidon2; +pub mod septic; pub mod sha256; pub mod uint256; pub mod uint256_ops; diff --git a/crates/core/executor/src/minimal/precompiles/septic.rs b/crates/core/executor/src/minimal/precompiles/septic.rs new file mode 100644 index 0000000000..a1949500a3 --- /dev/null +++ b/crates/core/executor/src/minimal/precompiles/septic.rs @@ -0,0 +1,274 @@ +use slop_algebra::{AbstractField, PrimeField32}; +use sp1_hypercube::{ + septic_curve::SepticCurve, + septic_digest::{CURVE_CUMULATIVE_SUM_START_X, CURVE_CUMULATIVE_SUM_START_Y}, + septic_extension::SepticExtension, +}; +use sp1_jit::SyscallContext; +use sp1_primitives::SP1Field; + +/// Number of u64 words used to hold a septic curve point in memory (14 u32 words = 7 u64 words). +const SEPTIC_POINT_U64_WORDS: usize = 7; + +/// Number of u64 words used to hold a 256-bit scalar (8 u32 words = 4 u64 words). +const SEPTIC_SCALAR_U64_WORDS: usize = 4; + +/// Scalar bit length (256 bits = 4 u64 words). +const SEPTIC_SCALAR_BITS: usize = SEPTIC_SCALAR_U64_WORDS * 64; + +/// The standard generator point for the septic curve, matching +/// `CURVE_CUMULATIVE_SUM_START` from `sp1_hypercube::septic_digest`. +fn septic_generator() -> SepticCurve { + let mut x = [SP1Field::zero(); 7]; + let mut y = [SP1Field::zero(); 7]; + for i in 0..7 { + x[i] = SP1Field::from_canonical_u32(CURVE_CUMULATIVE_SUM_START_X[i]); + y[i] = SP1Field::from_canonical_u32(CURVE_CUMULATIVE_SUM_START_Y[i]); + } + SepticCurve { x: SepticExtension(x), y: SepticExtension(y) } +} + +/// Shamir's trick: compute `s*G + e*A` with shared doublings (MSB-first). +/// +/// Runs ~381 EC operations instead of ~651 for two independent scalar mults: one +/// precomputed `G+A`, then at each bit position a shared double plus (at most) +/// a conditional add. Returns `(result, set)` where `set = false` indicates +/// both scalars were zero and the caller should emit the zero sentinel point. +fn shamirs_trick( + g: SepticCurve, + a: SepticCurve, + s: &[u64; SEPTIC_SCALAR_U64_WORDS], + e: &[u64; SEPTIC_SCALAR_U64_WORDS], +) -> (SepticCurve, bool) { + let g_plus_a = g.add_incomplete(a); + + let mut highest: Option = None; + for pos in 0..SEPTIC_SCALAR_BITS { + let word = pos / 64; + let bit = pos % 64; + if ((s[word] | e[word]) >> bit) & 1 == 1 { + highest = Some(pos); + } + } + + let Some(highest) = highest else { + return ( + SepticCurve { + x: SepticExtension([SP1Field::zero(); 7]), + y: SepticExtension([SP1Field::zero(); 7]), + }, + false, + ); + }; + + let mut result = g; + let mut result_set = false; + + for pos in (0..=highest).rev() { + if result_set { + result = result.double(); + } + + let word = pos / 64; + let bit = pos % 64; + let s_bit = (s[word] >> bit) & 1 == 1; + let e_bit = (e[word] >> bit) & 1 == 1; + + let to_add = match (s_bit, e_bit) { + (true, true) => Some(g_plus_a), + (true, false) => Some(g), + (false, true) => Some(a), + (false, false) => None, + }; + + if let Some(p) = to_add { + if result_set { + result = result.add_incomplete(p); + } else { + result = p; + result_set = true; + } + } + } + + (result, result_set) +} + +/// Execute a septic curve Schnorr-style verify syscall. +/// +/// Reads a 15-u64 buffer laid out as `[A(7), s(4), e(4)]`, computes +/// `s*G + e*A` via Shamir's trick in one syscall (`G` is the hardcoded +/// generator above), then writes the 7-u64 result back over the `A` slot. +/// +/// `s = 0 && e = 0` writes the all-zero sentinel point, matching the guest +/// API's handling of identity. +pub(crate) unsafe fn septic_verify( + ctx: &mut impl SyscallContext, + arg1: u64, + _arg2: u64, +) -> Option { + let buf_ptr = arg1; + if !buf_ptr.is_multiple_of(8) { + panic!(); + } + + let a_point = u64_words_to_septic_point(ctx.mr_slice_unsafe(buf_ptr, SEPTIC_POINT_U64_WORDS)); + let scalars_ptr = buf_ptr + (SEPTIC_POINT_U64_WORDS as u64) * 8; + let scalar_words: Vec = + ctx.mr_slice(scalars_ptr, 2 * SEPTIC_SCALAR_U64_WORDS).into_iter().copied().collect(); + + let mut s = [0u64; SEPTIC_SCALAR_U64_WORDS]; + let mut e = [0u64; SEPTIC_SCALAR_U64_WORDS]; + s.copy_from_slice(&scalar_words[..SEPTIC_SCALAR_U64_WORDS]); + e.copy_from_slice(&scalar_words[SEPTIC_SCALAR_U64_WORDS..]); + + let g_point = septic_generator(); + let (result, result_set) = shamirs_trick(g_point, a_point, &s, &e); + + let result_words = if result_set { + septic_point_to_u64_words(&result) + } else { + [0u64; SEPTIC_POINT_U64_WORDS] + }; + + ctx.bump_memory_clk(); + ctx.mw_slice(buf_ptr, &result_words); + + None +} + +fn u64_words_to_septic_point<'a>( + words: impl IntoIterator, +) -> SepticCurve { + let mut elems = [SP1Field::zero(); 14]; + for (i, w) in words.into_iter().enumerate() { + elems[2 * i] = SP1Field::from_canonical_u32(*w as u32); + elems[2 * i + 1] = SP1Field::from_canonical_u32((*w >> 32) as u32); + } + SepticCurve { + x: SepticExtension([elems[0], elems[1], elems[2], elems[3], elems[4], elems[5], elems[6]]), + y: SepticExtension([ + elems[7], elems[8], elems[9], elems[10], elems[11], elems[12], elems[13], + ]), + } +} + +fn septic_point_to_u64_words(point: &SepticCurve) -> [u64; SEPTIC_POINT_U64_WORDS] { + let mut elems = [0u32; 14]; + for i in 0..7 { + elems[i] = point.x.0[i].as_canonical_u32(); + elems[7 + i] = point.y.0[i].as_canonical_u32(); + } + let mut out = [0u64; SEPTIC_POINT_U64_WORDS]; + for i in 0..SEPTIC_POINT_U64_WORDS { + out[i] = (elems[2 * i] as u64) | ((elems[2 * i + 1] as u64) << 32); + } + out +} + +/// Execute a septic curve add assign syscall. +pub(crate) unsafe fn septic_add( + ctx: &mut impl SyscallContext, + arg1: u64, + arg2: u64, +) -> Option { + let p_ptr = arg1; + if !p_ptr.is_multiple_of(8) { + panic!(); + } + let q_ptr = arg2; + if !q_ptr.is_multiple_of(8) { + panic!(); + } + + let p_point = u64_words_to_septic_point(ctx.mr_slice_unsafe(p_ptr, SEPTIC_POINT_U64_WORDS)); + let q_point = u64_words_to_septic_point(ctx.mr_slice(q_ptr, SEPTIC_POINT_U64_WORDS)); + + let result = p_point.add_incomplete(q_point); + let result_words = septic_point_to_u64_words(&result); + + ctx.bump_memory_clk(); + ctx.mw_slice(p_ptr, &result_words); + + None +} + +/// Execute a septic curve double assign syscall. +pub(crate) unsafe fn septic_double( + ctx: &mut impl SyscallContext, + arg1: u64, + _arg2: u64, +) -> Option { + let p_ptr = arg1; + if !p_ptr.is_multiple_of(8) { + panic!(); + } + + let p_point = u64_words_to_septic_point(ctx.mr_slice_unsafe(p_ptr, SEPTIC_POINT_U64_WORDS)); + let result = p_point.double(); + let result_words = septic_point_to_u64_words(&result); + + ctx.mw_slice(p_ptr, &result_words); + + None +} + +/// Execute a septic curve scalar multiplication syscall. +/// +/// Performs the entire double-and-add loop in one syscall: reads the point at `arg1` +/// and the 256-bit little-endian scalar at `arg2`, then writes `scalar * P` back to +/// `arg1`. The scalar is stored as 4 u64 words (8 u32 words / 32 bytes). +/// +/// The septic curve has no native identity element, so we keep a sentinel flag and +/// only invoke `add_incomplete` once we've accumulated a non-identity running sum. +/// `scalar = 0` produces the all-zero sentinel point used by the guest API. +pub(crate) unsafe fn septic_scalar_mul( + ctx: &mut impl SyscallContext, + arg1: u64, + arg2: u64, +) -> Option { + let p_ptr = arg1; + if !p_ptr.is_multiple_of(8) { + panic!(); + } + let scalar_ptr = arg2; + if !scalar_ptr.is_multiple_of(8) { + panic!(); + } + + let p_point = u64_words_to_septic_point(ctx.mr_slice_unsafe(p_ptr, SEPTIC_POINT_U64_WORDS)); + let scalar_words: Vec = + ctx.mr_slice(scalar_ptr, SEPTIC_SCALAR_U64_WORDS).into_iter().copied().collect(); + + let mut result = SepticCurve { + x: SepticExtension([SP1Field::zero(); 7]), + y: SepticExtension([SP1Field::zero(); 7]), + }; + let mut result_set = false; + let mut temp = p_point; + + for word in &scalar_words { + for bit in 0..64 { + if (word >> bit) & 1 == 1 { + if result_set { + result = result.add_incomplete(temp); + } else { + result = temp; + result_set = true; + } + } + temp = temp.double(); + } + } + + let result_words = if result_set { + septic_point_to_u64_words(&result) + } else { + [0u64; SEPTIC_POINT_U64_WORDS] + }; + + ctx.bump_memory_clk(); + ctx.mw_slice(p_ptr, &result_words); + + None +} diff --git a/crates/core/executor/src/syscall_code.rs b/crates/core/executor/src/syscall_code.rs index 265019e7e5..7a09670969 100644 --- a/crates/core/executor/src/syscall_code.rs +++ b/crates/core/executor/src/syscall_code.rs @@ -173,6 +173,18 @@ pub enum SyscallCode { /// Executes the `POSEIDON2` syscall. POSEIDON2 = 0x00_00_01_33, + + /// Executes the `SEPTIC_ADD` precompile. + SEPTIC_ADD = 0x00_00_01_34, + + /// Executes the `SEPTIC_DOUBLE` precompile. + SEPTIC_DOUBLE = 0x00_00_01_35, + + /// Executes the `SEPTIC_SCALAR_MUL` precompile. + SEPTIC_SCALAR_MUL = 0x00_00_01_36, + + /// Executes the `SEPTIC_VERIFY` precompile (Shamir's trick: `s*G + e*A`). + SEPTIC_VERIFY = 0x00_00_01_37, } impl SyscallCode { @@ -224,6 +236,10 @@ impl SyscallCode { #[allow(clippy::mistyped_literal_suffixes)] 0x00_00_01_32 => SyscallCode::MPROTECT, 0x00_00_01_33 => SyscallCode::POSEIDON2, + 0x00_00_01_34 => SyscallCode::SEPTIC_ADD, + 0x00_00_01_35 => SyscallCode::SEPTIC_DOUBLE, + 0x00_00_01_36 => SyscallCode::SEPTIC_SCALAR_MUL, + 0x00_00_01_37 => SyscallCode::SEPTIC_VERIFY, _ => panic!("invalid syscall number: {value}"), } } @@ -321,6 +337,10 @@ impl SyscallCode { RiscvAirId::Uint256Ops } SyscallCode::POSEIDON2 => RiscvAirId::Poseidon2, + SyscallCode::SEPTIC_ADD => RiscvAirId::SepticAddAssign, + SyscallCode::SEPTIC_DOUBLE => RiscvAirId::SepticDoubleAssign, + SyscallCode::SEPTIC_SCALAR_MUL => RiscvAirId::SepticScalarMulAssign, + SyscallCode::SEPTIC_VERIFY => RiscvAirId::SepticVerify, SyscallCode::MPROTECT | SyscallCode::U256XU2048_MUL | SyscallCode::SECP256K1_DECOMPRESS @@ -379,6 +399,10 @@ impl SyscallCode { SyscallCode::U256XU2048_MUL => 72, SyscallCode::MPROTECT => 0, SyscallCode::POSEIDON2 => 8, + SyscallCode::SEPTIC_ADD => 14, + SyscallCode::SEPTIC_DOUBLE => 14, + SyscallCode::SEPTIC_SCALAR_MUL => 14, + SyscallCode::SEPTIC_VERIFY => 14, _ => 0, } } @@ -427,6 +451,10 @@ impl SyscallCode { SyscallCode::U256XU2048_MUL => 4 * 2, SyscallCode::MPROTECT => 1, SyscallCode::POSEIDON2 => 2, + SyscallCode::SEPTIC_ADD => 2 * 2, + SyscallCode::SEPTIC_DOUBLE => 2, + SyscallCode::SEPTIC_SCALAR_MUL => 2 * 2, + SyscallCode::SEPTIC_VERIFY => 2, _ => 0, } } diff --git a/crates/core/executor/src/vm/gas.rs b/crates/core/executor/src/vm/gas.rs index faebc98d64..90b995d090 100644 --- a/crates/core/executor/src/vm/gas.rs +++ b/crates/core/executor/src/vm/gas.rs @@ -337,6 +337,14 @@ pub fn get_complexity_mapping() -> EnumMap { // System operations mapping[RiscvAirId::Poseidon2] = 497; + // Septic curve operations + mapping[RiscvAirId::SepticAddAssign] = 918; + mapping[RiscvAirId::SepticDoubleAssign] = 904; + // Approximation: ~217 doublings + ~108 additions per 217-bit scalar. + mapping[RiscvAirId::SepticScalarMulAssign] = 295240; + // Approximation: ~217 doublings + ~163 additions (Shamir's trick) + 1 precompute. + mapping[RiscvAirId::SepticVerify] = 346234; + // RISC-V instruction costs mapping[RiscvAirId::DivRem] = 348; mapping[RiscvAirId::Add] = 16; diff --git a/crates/core/executor/src/vm/syscall.rs b/crates/core/executor/src/vm/syscall.rs index 01a8a0d524..e8d8d44358 100644 --- a/crates/core/executor/src/vm/syscall.rs +++ b/crates/core/executor/src/vm/syscall.rs @@ -224,6 +224,12 @@ pub(crate) fn sp1_ecall_handler<'a, RT: SyscallRuntime<'a>>( precompiles::fptower::fp_op::<_, Bn254BaseField>(rt, code, args1, args2) } SyscallCode::POSEIDON2 => poseidon2::poseidon2(rt, code, args1, args2), + SyscallCode::SEPTIC_ADD => precompiles::septic::septic_add(rt, code, args1, args2), + SyscallCode::SEPTIC_DOUBLE => precompiles::septic::septic_double(rt, code, args1, args2), + SyscallCode::SEPTIC_SCALAR_MUL => { + precompiles::septic::septic_scalar_mul(rt, code, args1, args2) + } + SyscallCode::SEPTIC_VERIFY => precompiles::septic::septic_verify(rt, code, args1, args2), SyscallCode::SECP256K1_DECOMPRESS | SyscallCode::BLS12381_DECOMPRESS | SyscallCode::SECP256R1_DECOMPRESS diff --git a/crates/core/executor/src/vm/syscall/precompiles/mod.rs b/crates/core/executor/src/vm/syscall/precompiles/mod.rs index 730b86023d..4a2f874dad 100644 --- a/crates/core/executor/src/vm/syscall/precompiles/mod.rs +++ b/crates/core/executor/src/vm/syscall/precompiles/mod.rs @@ -1,5 +1,6 @@ pub mod edwards; pub mod fptower; pub mod keccak256; +pub mod septic; pub mod sha256; pub mod weierstrass; diff --git a/crates/core/executor/src/vm/syscall/precompiles/septic.rs b/crates/core/executor/src/vm/syscall/precompiles/septic.rs new file mode 100644 index 0000000000..3d2e8c397e --- /dev/null +++ b/crates/core/executor/src/vm/syscall/precompiles/septic.rs @@ -0,0 +1,87 @@ +//! Septic curve precompile dispatch for the full VM executor. +//! +//! For the POC, the full VM executor path is only required to compile; trace +//! generation for a Septic AIR is not yet implemented. The minimal executor +//! handles the actual computation used by the mock prover. + +use crate::{vm::syscall::SyscallRuntime, SyscallCode}; + +const SEPTIC_POINT_U64_WORDS: usize = 7; +const SEPTIC_SCALAR_U64_WORDS: usize = 4; + +pub(crate) fn septic_add<'a, RT: SyscallRuntime<'a>>( + rt: &mut RT, + _syscall_code: SyscallCode, + arg1: u64, + arg2: u64, +) -> Option { + let p_ptr = arg1; + assert!(p_ptr.is_multiple_of(8), "p_ptr must be 8-byte aligned"); + let q_ptr = arg2; + assert!(q_ptr.is_multiple_of(8), "q_ptr must be 8-byte aligned"); + + let _p = rt.mr_slice_unsafe(SEPTIC_POINT_U64_WORDS); + let _q = rt.mr_slice(q_ptr, SEPTIC_POINT_U64_WORDS); + + rt.increment_clk(); + + let _w = rt.mw_slice(p_ptr, SEPTIC_POINT_U64_WORDS); + + None +} + +pub(crate) fn septic_double<'a, RT: SyscallRuntime<'a>>( + rt: &mut RT, + _syscall_code: SyscallCode, + arg1: u64, + _arg2: u64, +) -> Option { + let p_ptr = arg1; + assert!(p_ptr.is_multiple_of(8), "p_ptr must be 8-byte aligned"); + + let _p = rt.mr_slice_unsafe(SEPTIC_POINT_U64_WORDS); + let _w = rt.mw_slice(p_ptr, SEPTIC_POINT_U64_WORDS); + + None +} + +pub(crate) fn septic_scalar_mul<'a, RT: SyscallRuntime<'a>>( + rt: &mut RT, + _syscall_code: SyscallCode, + arg1: u64, + arg2: u64, +) -> Option { + let p_ptr = arg1; + assert!(p_ptr.is_multiple_of(8), "p_ptr must be 8-byte aligned"); + let scalar_ptr = arg2; + assert!(scalar_ptr.is_multiple_of(8), "scalar_ptr must be 8-byte aligned"); + + let _p = rt.mr_slice_unsafe(SEPTIC_POINT_U64_WORDS); + let _scalar = rt.mr_slice(scalar_ptr, SEPTIC_SCALAR_U64_WORDS); + + rt.increment_clk(); + + let _w = rt.mw_slice(p_ptr, SEPTIC_POINT_U64_WORDS); + + None +} + +pub(crate) fn septic_verify<'a, RT: SyscallRuntime<'a>>( + rt: &mut RT, + _syscall_code: SyscallCode, + arg1: u64, + _arg2: u64, +) -> Option { + let buf_ptr = arg1; + assert!(buf_ptr.is_multiple_of(8), "buf_ptr must be 8-byte aligned"); + + let _a = rt.mr_slice_unsafe(SEPTIC_POINT_U64_WORDS); + let scalars_ptr = buf_ptr + (SEPTIC_POINT_U64_WORDS as u64) * 8; + let _scalars = rt.mr_slice(scalars_ptr, 2 * SEPTIC_SCALAR_U64_WORDS); + + rt.increment_clk(); + + let _w = rt.mw_slice(buf_ptr, SEPTIC_POINT_U64_WORDS); + + None +} diff --git a/crates/test-artifacts/programs/Cargo.lock b/crates/test-artifacts/programs/Cargo.lock index 7b81d1be6b..0b54ef0996 100644 --- a/crates/test-artifacts/programs/Cargo.lock +++ b/crates/test-artifacts/programs/Cargo.lock @@ -365,7 +365,7 @@ version = "1.1.0" dependencies = [ "common-test-utils", "sp1-curves", - "sp1-lib 6.1.0", + "sp1-lib 6.0.2", "sp1-zkvm", ] @@ -408,7 +408,7 @@ name = "bls12381-mul-test" version = "1.1.0" dependencies = [ "sp1-derive", - "sp1-lib 6.1.0", + "sp1-lib 6.0.2", "sp1-zkvm", ] @@ -431,7 +431,7 @@ version = "1.1.0" dependencies = [ "common-test-utils", "sp1-curves", - "sp1-lib 6.1.0", + "sp1-lib 6.0.2", "sp1-zkvm", ] @@ -474,7 +474,7 @@ name = "bn254-mul-test" version = "1.1.0" dependencies = [ "sp1-derive", - "sp1-lib 6.1.0", + "sp1-lib 6.0.2", "sp1-zkvm", ] @@ -545,7 +545,7 @@ name = "common-test-utils" version = "1.1.0" dependencies = [ "num-bigint 0.4.6", - "sp1-lib 6.1.0", + "sp1-lib 6.0.2", ] [[package]] @@ -684,7 +684,7 @@ dependencies = [ "digest 0.10.7", "fiat-crypto", "rustc_version 0.4.1", - "sp1-lib 6.1.0", + "sp1-lib 6.0.2", "subtle", "zeroize", ] @@ -2544,7 +2544,7 @@ version = "1.1.0" dependencies = [ "common-test-utils", "sp1-curves", - "sp1-lib 6.1.0", + "sp1-lib 6.0.2", "sp1-zkvm", ] @@ -2580,7 +2580,7 @@ dependencies = [ "num", "p256", "sp1-curves", - "sp1-lib 6.1.0", + "sp1-lib 6.0.2", "sp1-zkvm", ] @@ -2601,7 +2601,7 @@ dependencies = [ "num", "p256", "sp1-curves", - "sp1-lib 6.1.0", + "sp1-lib 6.0.2", "sp1-zkvm", ] @@ -2814,7 +2814,7 @@ checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "slop-algebra" -version = "6.1.0" +version = "6.0.2" dependencies = [ "itertools 0.14.0", "p3-field", @@ -2823,7 +2823,7 @@ dependencies = [ [[package]] name = "slop-bn254" -version = "6.1.0" +version = "6.0.2" dependencies = [ "ff 0.13.1", "p3-bn254-fr", @@ -2837,7 +2837,7 @@ dependencies = [ [[package]] name = "slop-challenger" -version = "6.1.0" +version = "6.0.2" dependencies = [ "futures", "p3-challenger", @@ -2848,7 +2848,7 @@ dependencies = [ [[package]] name = "slop-koala-bear" -version = "6.1.0" +version = "6.0.2" dependencies = [ "lazy_static", "p3-koala-bear", @@ -2861,21 +2861,21 @@ dependencies = [ [[package]] name = "slop-poseidon2" -version = "6.1.0" +version = "6.0.2" dependencies = [ "p3-poseidon2", ] [[package]] name = "slop-primitives" -version = "6.1.0" +version = "6.0.2" dependencies = [ "slop-algebra", ] [[package]] name = "slop-symmetric" -version = "6.1.0" +version = "6.0.2" dependencies = [ "p3-symmetric", ] @@ -2892,7 +2892,7 @@ dependencies = [ [[package]] name = "sp1-curves" -version = "6.1.0" +version = "6.0.2" dependencies = [ "cfg-if", "dashu", @@ -2911,7 +2911,7 @@ dependencies = [ [[package]] name = "sp1-derive" -version = "6.1.0" +version = "6.0.2" dependencies = [ "proc-macro2", "quote", @@ -2930,7 +2930,7 @@ dependencies = [ [[package]] name = "sp1-lib" -version = "6.1.0" +version = "6.0.2" dependencies = [ "bincode", "elliptic-curve", @@ -2940,7 +2940,7 @@ dependencies = [ [[package]] name = "sp1-primitives" -version = "6.1.0" +version = "6.0.2" dependencies = [ "bincode", "blake3", @@ -2962,7 +2962,7 @@ dependencies = [ [[package]] name = "sp1-zkvm" -version = "6.1.0" +version = "6.0.2" dependencies = [ "cfg-if", "critical-section", @@ -2974,7 +2974,7 @@ dependencies = [ "rand 0.8.5", "sha2 0.10.9", "slop-algebra", - "sp1-lib 6.1.0", + "sp1-lib 6.0.2", "sp1-primitives", ] @@ -3248,7 +3248,7 @@ source = "git+https://github.com/sp1-patches/tiny-keccak?tag=patch-2.0.2-sp1-6.0 dependencies = [ "cfg-if", "crunchy", - "sp1-lib 6.1.0", + "sp1-lib 6.0.2", ] [[package]] diff --git a/crates/zkvm/entrypoint/src/syscalls/mod.rs b/crates/zkvm/entrypoint/src/syscalls/mod.rs index f129008715..4ea388edbc 100644 --- a/crates/zkvm/entrypoint/src/syscalls/mod.rs +++ b/crates/zkvm/entrypoint/src/syscalls/mod.rs @@ -12,6 +12,7 @@ mod mprotect; mod poseidon2; mod secp256k1; mod secp256r1; +mod septic; mod sha_compress; mod sha_extend; mod sys; @@ -39,6 +40,7 @@ pub use mprotect::*; pub use poseidon2::*; pub use secp256k1::*; pub use secp256r1::*; +pub use septic::*; pub use sha_compress::*; pub use sha_extend::*; pub use sys::*; @@ -181,3 +183,19 @@ pub const MPROTECT: u32 = 0x00_00_01_32; /// Executes the `POSEIDON2` permutation syscall. pub const POSEIDON2: u32 = 0x00_00_01_33; + +/// Executes the `SEPTIC_ADD` precompile. +#[allow(clippy::mistyped_literal_suffixes)] +pub const SEPTIC_ADD: u32 = 0x00_00_01_34; + +/// Executes the `SEPTIC_DOUBLE` precompile. +#[allow(clippy::mistyped_literal_suffixes)] +pub const SEPTIC_DOUBLE: u32 = 0x00_00_01_35; + +/// Executes the `SEPTIC_SCALAR_MUL` precompile. +#[allow(clippy::mistyped_literal_suffixes)] +pub const SEPTIC_SCALAR_MUL: u32 = 0x00_00_01_36; + +/// Executes the `SEPTIC_VERIFY` precompile (Shamir's trick: `s*G + e*A`). +#[allow(clippy::mistyped_literal_suffixes)] +pub const SEPTIC_VERIFY: u32 = 0x00_00_01_37; diff --git a/crates/zkvm/entrypoint/src/syscalls/septic.rs b/crates/zkvm/entrypoint/src/syscalls/septic.rs new file mode 100644 index 0000000000..038852fb8f --- /dev/null +++ b/crates/zkvm/entrypoint/src/syscalls/septic.rs @@ -0,0 +1,111 @@ +#[cfg(target_os = "zkvm")] +use core::arch::asm; + +/// Adds two septic curve points. +/// +/// The result is stored in the first point. +/// +/// Each point is laid out as 7 contiguous u64 words representing 14 KoalaBear +/// field elements: `[x0, x1, x2, x3, x4, x5, x6, y0, y1, y2, y3, y4, y5, y6]`, +/// packed two per u64 (little-endian). +/// +/// ### Safety +/// +/// The caller must ensure that `p` and `q` are valid pointers aligned to 8 bytes +/// and that `p != q`. The points must satisfy the incomplete weierstrass addition +/// preconditions (use `syscall_septic_double` for `P + P`). +#[allow(unused_variables)] +#[no_mangle] +pub extern "C" fn syscall_septic_add(p: *mut [u64; 7], q: *const [u64; 7]) { + #[cfg(target_os = "zkvm")] + unsafe { + asm!( + "ecall", + in("t0") crate::syscalls::SEPTIC_ADD, + in("a0") p, + in("a1") q + ); + } + + #[cfg(not(target_os = "zkvm"))] + unreachable!() +} + +/// Doubles a septic curve point. +/// +/// The result is stored in-place in the supplied buffer. +/// +/// ### Safety +/// +/// The caller must ensure that `p` is a valid pointer aligned to 8 bytes. +#[allow(unused_variables)] +#[no_mangle] +pub extern "C" fn syscall_septic_double(p: *mut [u64; 7]) { + #[cfg(target_os = "zkvm")] + unsafe { + asm!( + "ecall", + in("t0") crate::syscalls::SEPTIC_DOUBLE, + in("a0") p, + in("a1") 0 + ); + } + + #[cfg(not(target_os = "zkvm"))] + unreachable!() +} + +/// Scalar multiplication on the septic curve. The result is stored in-place +/// (`p = scalar * p`). +/// +/// `scalar` is interpreted as a 256-bit little-endian integer packed as 4 u64 +/// words (8 u32 words). The septic curve group order is 217 bits, so the top +/// bits should be zero. +/// +/// ### Safety +/// +/// The caller must ensure that `p` and `scalar` are valid pointers aligned to +/// 8 bytes. +#[allow(unused_variables)] +#[no_mangle] +pub extern "C" fn syscall_septic_scalar_mul(p: *mut [u64; 7], scalar: *const [u64; 4]) { + #[cfg(target_os = "zkvm")] + unsafe { + asm!( + "ecall", + in("t0") crate::syscalls::SEPTIC_SCALAR_MUL, + in("a0") p, + in("a1") scalar + ); + } + + #[cfg(not(target_os = "zkvm"))] + unreachable!() +} + +/// Schnorr verification helper: computes `s*G + e*A` using Shamir's trick in +/// a single syscall, where `G` is the hardcoded septic curve generator. +/// +/// `buf` is laid out as 15 contiguous u64 words: `[A(7), s(4), e(4)]` — +/// the pubkey point followed by the two little-endian 256-bit scalars. The +/// result point (`s*G + e*A`) overwrites the first 7 u64 words (the `A` slot). +/// +/// ### Safety +/// +/// The caller must ensure that `buf` is a valid pointer aligned to 8 bytes. +#[allow(unused_variables)] +#[no_mangle] +pub extern "C" fn syscall_septic_verify(buf: *mut [u64; 15]) { + #[cfg(target_os = "zkvm")] + unsafe { + asm!( + "ecall", + in("t0") crate::syscalls::SEPTIC_VERIFY, + in("a0") buf, + in("a1") 0 + ); + } + + #[cfg(not(target_os = "zkvm"))] + unreachable!() +} diff --git a/crates/zkvm/lib/src/lib.rs b/crates/zkvm/lib/src/lib.rs index 4d213120d4..7b3714e2aa 100644 --- a/crates/zkvm/lib/src/lib.rs +++ b/crates/zkvm/lib/src/lib.rs @@ -15,6 +15,7 @@ pub mod mprotect; pub mod poseidon2; pub mod secp256k1; pub mod secp256r1; +pub mod septic; pub mod unconstrained; pub mod utils; @@ -179,6 +180,23 @@ extern "C" { /// Executes the Poseidon2 permutation on the given state buffer in-place. pub fn syscall_poseidon2(inout: &mut crate::poseidon2::Poseidon2State); + + /// Executes a septic curve addition on the given points. + pub fn syscall_septic_add(p: *mut [u64; 7], q: *const [u64; 7]); + + /// Executes a septic curve doubling on the given point. + pub fn syscall_septic_double(p: *mut [u64; 7]); + + /// Executes a septic curve scalar multiplication on the given point and scalar. + /// The result is written back to `p`. The scalar is a 256-bit little-endian + /// integer (the upper bits should be zero for valid scalars on the 217-bit + /// group order). + pub fn syscall_septic_scalar_mul(p: *mut [u64; 7], scalar: *const [u64; 4]); + + /// Computes `s*G + e*A` on the septic curve using Shamir's trick, where `G` + /// is the standard hardcoded generator. The 15-u64 buffer is laid out as + /// `[A(7), s(4), e(4)]`; the result overwrites the first 7 u64 words. + pub fn syscall_septic_verify(buf: *mut [u64; 15]); } #[repr(C)] diff --git a/crates/zkvm/lib/src/septic.rs b/crates/zkvm/lib/src/septic.rs new file mode 100644 index 0000000000..1588232f40 --- /dev/null +++ b/crates/zkvm/lib/src/septic.rs @@ -0,0 +1,147 @@ +//! Higher-level guest API for SP1's septic curve precompile. +//! +//! The septic curve is `y^2 = x^3 + 45x + 41z^3` over `F_{p^7} = F_p[z]/(z^7 - 3z - 5)`, +//! where `p` is the KoalaBear prime. Each point is represented as 14 KoalaBear +//! field elements `[x0..x6, y0..y6]`, packed into 7 u64 words (two u32s per u64, +//! little-endian) for 8-byte alignment. + +use crate::{ + syscall_septic_add, syscall_septic_double, syscall_septic_scalar_mul, syscall_septic_verify, +}; + +/// A septic curve point. +/// +/// The `data` field stores 7 u64 words representing 14 KoalaBear field elements: +/// `[x0..x6, y0..y6]`, two field elements per u64 (little-endian). +#[derive(Clone, Copy, Debug)] +#[repr(C, align(8))] +pub struct SepticPoint { + pub data: [u64; 7], +} + +impl SepticPoint { + /// Construct a `SepticPoint` from 7-element x and y coordinate arrays. + pub fn new(x: [u32; 7], y: [u32; 7]) -> Self { + let mut elems = [0u32; 14]; + elems[..7].copy_from_slice(&x); + elems[7..].copy_from_slice(&y); + let mut data = [0u64; 7]; + for i in 0..7 { + data[i] = (elems[2 * i] as u64) | ((elems[2 * i + 1] as u64) << 32); + } + SepticPoint { data } + } + + /// Return the unpacked 14 field elements `[x0..x6, y0..y6]`. + pub fn limbs(&self) -> [u32; 14] { + let mut out = [0u32; 14]; + for i in 0..7 { + out[2 * i] = self.data[i] as u32; + out[2 * i + 1] = (self.data[i] >> 32) as u32; + } + out + } + + /// x-coordinate as 7 KoalaBear limbs. + pub fn x(&self) -> [u32; 7] { + let l = self.limbs(); + [l[0], l[1], l[2], l[3], l[4], l[5], l[6]] + } + + /// y-coordinate as 7 KoalaBear limbs. + pub fn y(&self) -> [u32; 7] { + let l = self.limbs(); + [l[7], l[8], l[9], l[10], l[11], l[12], l[13]] + } + + /// Point addition: `self + other` (incomplete — assumes `self != other`). + pub fn add(&self, other: &SepticPoint) -> SepticPoint { + let mut result = *self; + unsafe { + syscall_septic_add(&mut result.data as *mut [u64; 7], &other.data as *const [u64; 7]); + } + result + } + + /// Point doubling: `2 * self`. + pub fn double(&self) -> SepticPoint { + let mut result = *self; + unsafe { + syscall_septic_double(&mut result.data as *mut [u64; 7]); + } + result + } + + /// Scalar multiplication via double-and-add. + /// + /// `scalar` is little-endian limbs. Iterates each bit, accumulating the + /// running sum. Returns the identity-like all-zero point if `scalar == 0`. + pub fn scalar_mul(&self, scalar: &[u32]) -> SepticPoint { + let mut result_set = false; + let mut result = SepticPoint { data: [0u64; 7] }; + let mut temp = *self; + + for limb in scalar { + for bit in 0..32 { + if (limb >> bit) & 1 == 1 { + if !result_set { + result = temp; + result_set = true; + } else { + result = result.add(&temp); + } + } + temp = temp.double(); + } + } + result + } + + /// Scalar multiplication via the `SEPTIC_SCALAR_MUL` precompile (single syscall). + /// + /// `scalar` is 8 little-endian u32 limbs, packed into 4 u64 words for the + /// syscall. Compared to [`Self::scalar_mul`] this performs the entire + /// double-and-add loop inside the executor, avoiding ~325 individual syscalls. + pub fn scalar_mul_single(&self, scalar: &[u32; 8]) -> SepticPoint { + let mut result = *self; + let mut scalar_packed = [0u64; 4]; + for i in 0..4 { + scalar_packed[i] = (scalar[2 * i] as u64) | ((scalar[2 * i + 1] as u64) << 32); + } + unsafe { + syscall_septic_scalar_mul( + &mut result.data as *mut [u64; 7], + &scalar_packed as *const [u64; 4], + ); + } + result + } +} + +/// Schnorr verification helper: compute `s * G + e * A` via the `SEPTIC_VERIFY` +/// precompile (single syscall), where `G` is the hardcoded septic curve +/// generator. Uses Shamir's trick inside the executor, so it costs ~381 EC +/// operations vs. ~651 for two independent `scalar_mul_single` calls. +/// +/// The caller compares the returned point against `R` to complete the Schnorr +/// verification equation. +pub fn schnorr_compute(a: &SepticPoint, s: &[u32; 8], e: &[u32; 8]) -> SepticPoint { + let mut buf = [0u64; 15]; + + buf[0..7].copy_from_slice(&a.data); + + for i in 0..4 { + buf[7 + i] = (s[2 * i] as u64) | ((s[2 * i + 1] as u64) << 32); + } + for i in 0..4 { + buf[11 + i] = (e[2 * i] as u64) | ((e[2 * i + 1] as u64) << 32); + } + + unsafe { + syscall_septic_verify(&mut buf as *mut [u64; 15]); + } + + let mut result_data = [0u64; 7]; + result_data.copy_from_slice(&buf[0..7]); + SepticPoint { data: result_data } +}