Skip to content
Draft
17 changes: 17 additions & 0 deletions arm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ hex-literal = "0.4"
lazy_static = { version = "1.5.0", optional = true }
bytemuck = { version = "1.12", features = ["derive"] }
thiserror = "2.0.6"
borsh = { version = "1.5", default-features = false, features = ["derive"], optional = true }
sha2 = { version = "0.10", optional = true }
solana-program = { version = "2.1", optional = true }
solana-secp256k1 = { version = "0.1", optional = true }
dashu = { version = "0.4", optional = true }

[dev-dependencies]
k256 = { version = "=0.13.3", features = ["arithmetic", "ecdsa", "std"], default-features = false }

[features]
default = ["transaction", "prove", "k256"]
Expand All @@ -43,3 +51,12 @@ bonsai = ["zkvm", "risc0-zkvm/bonsai"]
cuda = ["zkvm", "risc0-zkvm/cuda"]
aggregation = ["aggregation_circuit", "transaction", "zkvm", "k256"]
aggregation_circuit = []
borsh = ["dep:borsh"]
solana = [
"transaction",
"borsh",
"dep:sha2",
"dep:solana-program",
"dep:solana-secp256k1",
"dep:dashu",
]
4 changes: 4 additions & 0 deletions arm/src/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ use crate::{action_tree::MerkleTree, logic_proof::LogicVerifier, Digest};

/// An action consists of compliance units and logic verifier inputs.
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[cfg_attr(
feature = "borsh",
derive(borsh::BorshSerialize, borsh::BorshDeserialize)
)]
pub struct Action {
/// The compliance units in this action.
pub compliance_units: Vec<ComplianceUnit>,
Expand Down
44 changes: 31 additions & 13 deletions arm/src/compliance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,19 @@
/// Size hard-coded to two resources per unit
const COMPLIANCE_INSTANCE_SIZE: usize = 56;

#[cfg(any(feature = "k256", feature = "zkvm"))]
#[cfg(any(feature = "k256", feature = "zkvm", feature = "solana"))]
use crate::error::ArmError;
use crate::utils::bytes_to_words;
use crate::{constants::EMPTY_HASH_BYTES, Digest};
use serde_with::serde_as;

#[cfg(feature = "zkvm")]
use crate::utils::words_to_bytes;
#[cfg(all(feature = "zkvm", feature = "k256"))]
#[cfg(feature = "k256")]
use crate::utils::bytes_to_words;
#[cfg(feature = "k256")]
use crate::{merkle_path::MerklePath, nullifier_key::NullifierKey, resource::Resource};
#[cfg(feature = "k256")]
use k256::{elliptic_curve::sec1::FromEncodedPoint, EncodedPoint, ProjectivePoint};
#[cfg(all(feature = "zkvm", feature = "k256"))]
use k256::{
elliptic_curve::{sec1::ToEncodedPoint, Field, PrimeField},
Scalar,
elliptic_curve::{sec1::FromEncodedPoint, sec1::ToEncodedPoint, Field, PrimeField},
EncodedPoint, ProjectivePoint, Scalar,
};
#[cfg(all(feature = "zkvm", feature = "k256"))]
use rand::rngs::OsRng;
Expand All @@ -30,6 +27,10 @@ pub fn initial_root() -> Digest {

/// The compliance instance contains all public inputs to the compliance proof.
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
#[cfg_attr(
feature = "borsh",
derive(borsh::BorshSerialize, borsh::BorshDeserialize)
)]
pub struct ComplianceInstance {
/// The nullifier of the consumed resource.
pub consumed_nullifier: Digest,
Expand All @@ -51,6 +52,10 @@ pub struct ComplianceInstance {
/// serialization(used in the aggregation circuit).
#[serde_as]
#[derive(serde::Serialize, serde::Deserialize)]
#[cfg_attr(
feature = "borsh",
derive(borsh::BorshSerialize, borsh::BorshDeserialize)
)]
pub struct ComplianceInstanceWords {
/// The compliance instance as an array of u32 words.
#[serde_as(as = "[_; COMPLIANCE_INSTANCE_SIZE]")]
Expand Down Expand Up @@ -87,24 +92,34 @@ impl ComplianceInstance {
impl ComplianceInstance {
/// Serializes this instance to journal bytes (risc0 serde format).
pub fn to_journal(&self) -> Result<Vec<u8>, ArmError> {
use crate::utils::words_to_bytes;
let words =
risc0_zkvm::serde::to_vec(self).map_err(|_| ArmError::InstanceSerializationFailed)?;
Ok(words_to_bytes(&words).to_vec())
}
}

#[cfg(all(feature = "solana", not(feature = "zkvm")))]
impl ComplianceInstance {
/// Serializes this instance to journal bytes (borsh format for Solana).
pub fn to_journal(&self) -> Result<Vec<u8>, ArmError> {
borsh::to_vec(self).map_err(|_| ArmError::InstanceSerializationFailed)
}
}

impl ComplianceInstanceWords {
/// Creates a ComplianceInstanceWords from a byte slice.
pub fn from_bytes(instance_bytes: &[u8]) -> Result<Self, ArmError> {
let u32_words: [u32; COMPLIANCE_INSTANCE_SIZE] = bytes_to_words(instance_bytes)
.try_into()
.map_err(|_| ArmError::InstanceSerializationFailed)?;
let u32_words: [u32; COMPLIANCE_INSTANCE_SIZE] =
crate::utils::bytes_to_words(instance_bytes)
.try_into()
.map_err(|_| ArmError::InstanceSerializationFailed)?;
Ok(ComplianceInstanceWords { u32_words })
}
}

/// The compliance witness contains all private inputs to the compliance proof.
#[cfg(all(feature = "zkvm", feature = "k256"))]
#[cfg(feature = "k256")]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct ComplianceWitness {
/// The consumed resource
Expand Down Expand Up @@ -161,7 +176,10 @@ impl ComplianceWitness {
ephemeral_root: initial_root(),
}
}
}

#[cfg(feature = "k256")]
impl ComplianceWitness {
/// Compliance constraints
pub fn constrain(&self) -> Result<ComplianceInstance, ArmError> {
let consumed_cm = self.consumed_commitment();
Expand Down
4 changes: 4 additions & 0 deletions arm/src/compliance_unit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ use crate::{
/// A compliance unit consists of a compliance proof and its corresponding instance.
/// The vk is a constant in the compliance unit, so we don't place it here.
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[cfg_attr(
feature = "borsh",
derive(borsh::BorshSerialize, borsh::BorshDeserialize)
)]
pub struct ComplianceUnit {
/// The compliance proof (optional, would be absent when aggregation is enabled).
pub proof: Option<Vec<u8>>,
Expand Down
47 changes: 44 additions & 3 deletions arm/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,59 @@

use hex_literal::hex;

/// Compliance verification key bytes.
/// Compliance verification key bytes (default circuit, risc0 serde).
#[cfg(not(feature = "solana"))]
pub const COMPLIANCE_VK_BYTES: [u8; 32] =
hex!("919e13001cd3319be5a5a7cb189203be083674acb3fff23d05aae9c3ed86314d");

/// Padding logic verification key bytes.
/// Compliance verification key bytes (Borsh-serializing circuit).
#[cfg(feature = "solana")]
pub const COMPLIANCE_VK_BYTES: [u8; 32] =
hex!("600a952bc393092b6f47daa8259eb541133aaa5ebe90bce837b25e00eb53969d");

/// Padding logic verification key bytes (default circuit, risc0 serde).
#[cfg(not(feature = "solana"))]
pub const PADDING_LOGIC_VK_BYTES: [u8; 32] =
hex!("21fcc2fc2c07f9753405d3070f2488c67389f7d797b6f6e20a9f2529fe4a0bff");

/// Padding logic verification key bytes (Borsh-serializing circuit).
#[cfg(feature = "solana")]
pub const PADDING_LOGIC_VK_BYTES: [u8; 32] =
hex!("9f28d8464937c5cad995819b37c89dbba1035c711fe810ab7a8872874cd776fc");

/// Batch aggregation verification key bytes (default circuit, risc0 serde).
#[cfg(not(feature = "solana"))]
pub const BATCH_AGGREGATION_VK_BYTES: [u8; 32] =
hex!("5ca0cbd4d5c267f42e0883b1ae7a28689d792230d9c4c61ca4f5df56aaf5fede");

/// Batch aggregation verification key bytes (Borsh-serializing circuit).
#[cfg(feature = "solana")]
pub const BATCH_AGGREGATION_VK_BYTES: [u8; 32] =
hex!("584aa83eda1588b1cda9d94ac1b51e504f6ce6b940e662a76073c1df43cf5a05");

/// Hash of the empty string (used for PADDING_LEAF and INITIAL_ROOT).
pub const EMPTY_HASH_BYTES: [u8; 32] =
hex!("cc1d2f838445db7aec431df9ee8a871f40e7aa5e064fc056633ef8c60fab7b06");

/// EMPTY_HASH as little-endian u32 words (for constructing `Digest` in const context).
pub const EMPTY_HASH_WORDS: [u32; 8] = bytes_to_words_const(EMPTY_HASH_BYTES);

/// Convert a 32-byte array to 8 little-endian u32 words at compile time.
pub const fn bytes_to_words_const(bytes: [u8; 32]) -> [u32; 8] {
let mut words = [0u32; 8];
let mut i = 0;
while i < 8 {
words[i] = u32::from_le_bytes([
bytes[i * 4],
bytes[i * 4 + 1],
bytes[i * 4 + 2],
bytes[i * 4 + 3],
]);
i += 1;
}
words
}

/// Compliance proving key / compliance guest ELF binary.
#[cfg(feature = "zkvm")]
pub const COMPLIANCE_PK: &[u8] = include_bytes!("../elfs/compliance-guest.bin");
Expand Down Expand Up @@ -44,5 +85,5 @@ lazy_static! {
lazy_static! {
/// Batch aggregation verification key / Batch aggregation image id.
pub static ref BATCH_AGGREGATION_VK: crate::Digest =
crate::Digest::try_from(hex!("5ca0cbd4d5c267f42e0883b1ae7a28689d792230d9c4c61ca4f5df56aaf5fede").as_slice()).unwrap();
crate::Digest::try_from(BATCH_AGGREGATION_VK_BYTES.as_slice()).unwrap();
}
52 changes: 47 additions & 5 deletions arm/src/delta_proof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ use k256::{
use serde::{Deserialize, Serialize};

use crate::error::ArmError;
use sha3::{Digest, Keccak256};

#[cfg(not(feature = "solana"))]
use sha3::{Digest, Keccak256 as MessageHasher};

#[cfg(feature = "solana")]
use sha2::{Digest, Sha256 as MessageHasher};

/// The delta proof consists of an ECDSA signature and a recovery ID.
#[derive(Clone, Debug, PartialEq, Eq)]
Expand Down Expand Up @@ -36,8 +41,7 @@ pub struct DeltaInstance {
impl DeltaProof {
/// Generates a delta proof by signing the given message with the provided witness.
pub fn prove(message: &[u8], witness: &DeltaWitness) -> Result<DeltaProof, ArmError> {
// Hash the message using Keccak256
let mut digest = Keccak256::new();
let mut digest = MessageHasher::new();
digest.update(message);

// Sign the hashed message using RFC6979
Expand Down Expand Up @@ -72,8 +76,7 @@ impl DeltaProof {
return Err(ArmError::InvalidDeltaProof);
}

// Hash the message using Keccak256
let mut digest = Keccak256::new();
let mut digest = MessageHasher::new();
digest.update(message);

// Verify the signature
Expand Down Expand Up @@ -216,6 +219,45 @@ impl<'de> Deserialize<'de> for DeltaWitness {
}
}

#[cfg(feature = "borsh")]
mod borsh_impl {
use super::{DeltaProof, DeltaWitness};
use borsh::io::{Error, ErrorKind, Read, Result, Write};
use borsh::{BorshDeserialize, BorshSerialize};

impl BorshSerialize for DeltaProof {
fn serialize<W: Write>(&self, writer: &mut W) -> Result<()> {
let bytes = self.to_bytes();
writer.write_all(&bytes)
}
}

impl BorshDeserialize for DeltaProof {
fn deserialize_reader<R: Read>(reader: &mut R) -> Result<Self> {
let mut bytes = [0u8; 65];
reader.read_exact(&mut bytes)?;
DeltaProof::from_bytes(&bytes)
.map_err(|e| Error::new(ErrorKind::InvalidData, format!("{:?}", e)))
}
}

impl BorshSerialize for DeltaWitness {
fn serialize<W: Write>(&self, writer: &mut W) -> Result<()> {
let bytes = self.to_bytes();
writer.write_all(&bytes)
}
}

impl BorshDeserialize for DeltaWitness {
fn deserialize_reader<R: Read>(reader: &mut R) -> Result<Self> {
let mut bytes = [0u8; 32];
reader.read_exact(&mut bytes)?;
DeltaWitness::from_bytes(&bytes)
.map_err(|e| Error::new(ErrorKind::InvalidData, format!("{:?}", e)))
}
}
}

#[test]
fn test_delta_proof() {
use k256::elliptic_curve::rand_core::OsRng;
Expand Down
25 changes: 25 additions & 0 deletions arm/src/delta_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,37 @@ mod placeholder {

/// Opaque 65-byte delta proof (signature + recovery ID).
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(
feature = "borsh",
derive(borsh::BorshSerialize, borsh::BorshDeserialize)
)]
pub struct DeltaProof(pub [u8; 65]);

/// Opaque 32-byte delta witness (signing key).
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(
feature = "borsh",
derive(borsh::BorshSerialize, borsh::BorshDeserialize)
)]
pub struct DeltaWitness(pub [u8; 32]);

impl DeltaProof {
/// Returns the length of the proof bytes.
pub fn len(&self) -> usize {
self.0.len()
}

/// Returns true if the proof is empty (always false for valid proofs).
pub fn is_empty(&self) -> bool {
false
}

/// Returns the proof bytes as a slice.
pub fn as_slice(&self) -> &[u8] {
&self.0
}
}

impl DeltaWitness {
/// Panics - composition requires k256.
pub fn compose(&self, _other: &DeltaWitness) -> DeltaWitness {
Expand Down
Loading