Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 23 additions & 0 deletions arm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,20 @@ 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 +57,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
2 changes: 1 addition & 1 deletion arm/src/action_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ impl MerkleTree {
) {
if cur_layer.len() > 1 {
let sibling = {
let is_sibling_left = !position.is_multiple_of(2);
let is_sibling_left = position % 2 != 0;
let sibling_value = if is_sibling_left {
cur_layer[position - 1]
} else {
Expand Down
32 changes: 26 additions & 6 deletions arm/src/compliance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@
const COMPLIANCE_INSTANCE_SIZE: usize = 56;

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"))]
use crate::utils::bytes_to_words;
#[cfg(all(feature = "zkvm", feature = "k256"))]
use crate::{merkle_path::MerklePath, nullifier_key::NullifierKey, resource::Resource};
#[cfg(feature = "k256")]
Expand All @@ -29,6 +28,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 @@ -50,6 +53,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 @@ -86,18 +93,28 @@ 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 })
}
}
Expand Down Expand Up @@ -160,7 +177,10 @@ impl ComplianceWitness {
ephemeral_root: initial_root(),
}
}
}

#[cfg(all(feature = "zkvm", 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 @@ -18,6 +18,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();
}
108 changes: 92 additions & 16 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 @@ -209,25 +212,98 @@ impl<'de> Deserialize<'de> for DeltaWitness {
where
D: serde::Deserializer<'de>,
{
let bytes = <[u8; 32]>::deserialize(deserializer)?;
let bytes: Vec<u8> = Vec::deserialize(deserializer)?;
if bytes.len() != 32 {
return Err(serde::de::Error::custom(
"Invalid byte length for DeltaWitness",
));
}
DeltaWitness::from_bytes(&bytes).map_err(|e| {
serde::de::Error::custom(format!("Failed to deserialize DeltaWitness: {:?}", e))
})
}
}

#[test]
fn test_delta_proof() {
#[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)))
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use k256::elliptic_curve::rand_core::OsRng;

let mut rng = OsRng;
let signing_key = SigningKey::random(&mut rng);
let verifying_key = VerifyingKey::from(&signing_key);
#[test]
fn test_delta_proof() {
let mut rng = OsRng;
let signing_key = SigningKey::random(&mut rng);
let verifying_key = VerifyingKey::from(&signing_key);

let message = b"Hello, world!";
let witness = DeltaWitness { signing_key };
let proof = DeltaProof::prove(message, &witness).unwrap();
let instance = DeltaInstance { verifying_key };
let message = b"Hello, world!";
let witness = DeltaWitness { signing_key };
let proof = DeltaProof::prove(message, &witness).unwrap();
let instance = DeltaInstance { verifying_key };

DeltaProof::verify(message, &proof, instance).unwrap();
DeltaProof::verify(message, &proof, instance).unwrap();
}

/// DeltaProof: serialize then deserialize via bincode must round-trip.
#[test]
fn delta_proof_bincode_roundtrip() {
let mut rng = OsRng;
let signing_key = SigningKey::random(&mut rng);
let witness = DeltaWitness { signing_key };
let proof = DeltaProof::prove(b"roundtrip", &witness).unwrap();

let encoded = bincode::serialize(&proof).unwrap();
let decoded: DeltaProof = bincode::deserialize(&encoded).unwrap();
assert_eq!(proof, decoded);
}

/// DeltaWitness: serialize then deserialize via bincode must round-trip.
#[test]
fn delta_witness_bincode_roundtrip() {
let mut rng = OsRng;
let signing_key = SigningKey::random(&mut rng);
let witness = DeltaWitness { signing_key };

let encoded = bincode::serialize(&witness).unwrap();
let decoded: DeltaWitness = bincode::deserialize(&encoded).unwrap();
assert_eq!(witness, decoded);
}
}
Loading