Compare commits

..

10 commits

11 changed files with 558 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
.guix-environment
build-aux/rust-environment
target

37
Cargo.toml Normal file
View file

@ -0,0 +1,37 @@
[package]
name = "mtcs-garage"
edition = { workspace = true }
version = { workspace = true }
[workspace.package]
version = "0.0.0"
edition = "2021"
[workspace]
resolver = "2"
members = ["crates/*"]
[workspace.dependencies]
ed25519-dalek-patched = { package = "ed25519-dalek", git = "https://github.com/sp1-patches/curve25519-dalek", branch = "patch-v4.1.1" }
sha2 = "0.10.8"
sp1-helper = { git = "https://github.com/succinctlabs/sp1.git" }
sp1-sdk = { git = "https://github.com/succinctlabs/sp1.git" }
sp1-zkvm = { git = "https://github.com/succinctlabs/sp1.git" }
[dependencies]
clap = { version = "4.5.4", features = ["derive"] }
ed25519-dalek = "2.1.1"
hex = { version = "0.4.3", features = ["serde"] }
rand = "0.8.5"
serde = "1.0.198"
serde_json = "1.0.116"
sha2 = { workspace = true }
[dev-dependencies]
assert_cmd = "2.0.14"
predicates = "3.1.0"
rstest = "0.19.0"
[[bin]]
name = "credit5000"
path = "src/main.rs"

View file

@ -0,0 +1,9 @@
[package]
name = "mtcs-client-guest"
version = { workspace = true }
edition = { workspace = true }
[dependencies]
ed25519-dalek-patched = { workspace = true }
mtcs-garage = { path = "../../" }
sp1-zkvm = { workspace = true }

View file

@ -0,0 +1,56 @@
//! Client side zkvm program.
#![no_main]
sp1_zkvm::entrypoint!(main);
use ed25519_dalek_patched::{VerifyingKey, Signature, SecretKey}
type InputLengh = u64;
type Secret = [u8: 32];
struct InputHead {
salt: [u8: 32],
from: [u8: 32],
val: u32,
to: [u8: 32],
hash: [u8: 32],
signature: [u8: 64],
public_key: [u8: 32],
root: [u8: 32],
}
// Input Head length in bytes.
// Could be turned into Phantom attribute or somethin?
const HL = 260;
pub fn main() {
let length = sp1_zkvm::io::read::<InputLengh>();
let input = sp1_zkvm::io::read::<InputHead>();
let secret = sp1_zkvm::io::read::<Secret>();
let mut my_slice = [0_u8; 32];
sp1_zkvm::io::read_slice(&mut my_slice);
let hashes = sp1_zkvm::io::read::<Vec<u8>>();
let pk = VerifyingKey::from_bytes(input.public_key).unwrap();
assert!(pk.verify_strict(
&input.from
.iter()
.chain(input.val.to_le_bytes().iter())
.chain(input.to.iter())
.cloned()
.collect(),
Signature::from_bytes(&input.signature),
).unwrap());
let mut h = Sha256::new();
let chunks = hashes.chunks(32);
chunks.fold(chunks.next().unwrap(), |acc, e| {
h.update();
assert!(*e == *h.finalize_reset());
});
assert!(input.root == *self.hashes.last().unwrap());
sp1_zkvm::io::commit_slice(&input.hash);
sp1_zkvm::io::commit_slice(&input.root);
}

View file

@ -0,0 +1,10 @@
[package]
version = { workspace = true }
name = "client-host"
edition = { workspace = true }
[dependencies]
sp1-sdk = { workspace = true }
[build-dependencies]
sp1-helper = { workspace = true }

View file

@ -0,0 +1,5 @@
use sp1_helper::build_program;
fn main() {
build_program("../client-guest")
}

View file

@ -0,0 +1,3 @@
[toolchain]
channel = "nightly-2024-04-20"
components = ["llvm-tools", "rustc-dev"]

View file

@ -0,0 +1,7 @@
//! A simple script to generate and verify the proof of a given program.
use sp1_sdk::{ProverClient, SP1Stdin};
const ELF: &[u8] = include_bytes!("../../../elf/riscv32im-succinct-zkvm-elf");
fn main() { }

284
src/lib.rs Normal file
View file

@ -0,0 +1,284 @@
use std::error::Error;
use dalek::{ed25519::signature::SignerMut, SigningKey};
use ed25519_dalek as dalek;
use ed25519_dalek::Verifier;
use rand;
use sha2::{digest::Digest, Sha256};
use serde::{Deserialize, Serialize};
/// TODO: Use hashes instead of public addresses.
/// TODO: Don't forget document endiannes.
/// TODO: Allow doubly signed obligations or implement abstract interfaces for Cert (see below).
/// Implement abstract Cert datatype to allow attestation of hyper-relations:
/// TODO: Use serializable trait to generalize over the subject field.
/// TODO: Allow per subject type verification trait. Hard not to go
/// full crypto-conditions but better not to complicate things too
/// much.
pub type Address = [u8; 32];
pub type Secret = [u8; 32];
#[derive(Copy, Clone, PartialEq, Debug)]
pub struct Obligation {
pub from: PseudoAnonID,
pub to: PseudoAnonID,
pub value: u32,
}
impl Obligation {
pub fn serialize(self) -> [u8; 68] {
let mut arr = [0; 68];
arr[..32].clone_from_slice(&self.from);
arr[32..36].clone_from_slice(&self.value.to_le_bytes());
arr[36..].clone_from_slice(&self.to);
arr
}
}
pub type Signature = [u8; 64];
pub type Salt = [u8; 32];
#[derive(Copy, Clone)]
pub struct AuthCert {
pub subject: Obligation,
pub signature: Signature,
}
impl AuthCert {
pub fn maybe_from_signature(
subject: &Obligation,
signature: &Signature,
pk: &Address,
) -> Result<Self, Box<dyn Error>> {
let pk = dalek::VerifyingKey::from_bytes(pk)?;
pk.verify(
&subject.serialize(),
&dalek::Signature::from_bytes(signature),
)?;
Ok(Self {
subject: *subject,
signature: *signature,
})
}
pub fn from_secret(subject: &Obligation, secret: &Secret) -> Self {
Self {
subject: subject.clone(),
signature: SigningKey::from_bytes(secret).sign(&subject.serialize()).into(),
}
}
}
pub type PseudoAnonID = [u8; 32];
fn pseudo_anon_id(a: &Address) -> PseudoAnonID {
let mut h = Sha256::new();
h.update(a);
let res: PseudoAnonID = h.finalize().into();
res
}
pub struct MembershipProof {
subjects: Vec<PseudoAnonID>,
root: [u8; 32],
hashes: Vec<[u8; 32]>,
}
// TODO: replace with real implementation
impl MembershipProof {
pub fn new(subjects: Vec<PseudoAnonID>) -> Self {
let mut n = subjects.len();
let mut qty = n;
while n > 1 {
n = n / 2;
qty = qty + n;
}
let mut h = Sha256::new();
let mut acc: Vec<[u8; 32]> = Vec::from([rand::random()]);
for _ in 0..qty {
h.update(acc.last().unwrap());
acc.push(h.finalize_reset().into());
}
Self {
subjects,
root: *acc.last().unwrap(),
hashes: acc,
}
}
pub fn verify(&self) -> bool {
let mut h = Sha256::new();
self.hashes[1..]
.iter()
.fold(Some(self.hashes[0]), |acc, e| match acc {
Some(x) => {
h.update(x);
if *e == *h.finalize_reset() {
Some(x)
} else {
None
}
}
None => None,
});
self.root == *self.hashes.last().unwrap()
}
}
pub struct Cert {
pub subject: Obligation,
pub authenticity: AuthCert,
pub membership: MembershipProof,
pub salt: Salt,
pub hash: [u8; 32],
}
impl Cert {
pub fn new(sub: Obligation, auth: AuthCert, mem: MembershipProof) -> Self {
let mut h = Sha256::new();
let salt: Salt = rand::random();
h.update(sub.serialize());
h.update(salt);
Self {
subject: sub,
authenticity: auth,
membership: mem,
salt: salt,
hash: h.finalize().into(),
}
}
pub fn zkvm_serialize(&self) -> Vec<u8> { // , pk: Address
// First 8 bytes are the length of the message,
// followed by 260 bytes of fields of defined length,
// the rest is membership proof.
(260 + 32 * self.membership.hashes.len())
.to_le_bytes() // 8 bytes; NOTE: Machine specific! (from usize)
.iter()
.chain(self.salt.iter()) // 32 bytes
.chain(self.subject.serialize().iter()) // 68 bytes
.chain(self.hash.iter()) // 32 bytes
.chain(self.salt.iter()) // 32 bytes
.chain(self.authenticity.signature.iter()) // 64 bytes
.chain(self.membership.root.iter()) // 32 bytes
.chain(self.membership.hashes.iter().flatten()) // tail
.cloned()
.collect::<Vec<u8>>()
}
}
#[cfg(test)]
mod tests {
use super::*;
use ed25519_dalek::Signer;
use rand::Rng;
fn make_address() -> Address {
rand::random()
}
#[allow(dead_code)]
struct TestAddr {
sk: dalek::SigningKey,
id: PseudoAnonID,
}
impl TestAddr {
pub fn new() -> Self {
let addr = make_address();
Self {
sk: dalek::SigningKey::from_bytes(&addr),
id: pseudo_anon_id(&addr),
}
}
}
#[allow(dead_code)]
struct TestObligation {
o: Obligation,
f: TestAddr,
t: TestAddr,
}
fn rand_obligation() -> TestObligation {
match (TestAddr::new(), TestAddr::new()) {
(f, t) => TestObligation {
o: Obligation {
from: f.id,
to: t.id,
value: rand::random(),
},
f,
t,
},
}
}
#[test]
fn serialize_obligation() {
assert_eq!(
Obligation {
from: [1; 32],
to: [2; 32],
value: 4_294_967_295, // Max u32 (255 255 255 255)
}
.serialize(),
[
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 255, 255, 255, 255, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2
]
);
}
#[test]
fn authenticity_cert_new() {
let tob = rand_obligation();
let sig = tob.f.sk.sign(&tob.o.serialize()[..]).to_bytes();
let cert = AuthCert::maybe_from_signature(&tob.o, &sig, &tob.f.sk.verifying_key().to_bytes());
assert!(cert.is_ok());
assert_eq!(cert.as_ref().unwrap().subject, tob.o);
assert_eq!(cert.as_ref().unwrap().signature, sig);
// Why this sometime fails??
// assert!(AuthCert::new(&tob.o, &[0; 64], &[0; 32]).is_err());
assert!(AuthCert::maybe_from_signature(&tob.o, &[0; 64], &[1; 32]).is_err());
assert!(AuthCert::maybe_from_signature(&tob.o, &sig, &tob.o.from).is_err());
assert!(AuthCert::maybe_from_signature(&tob.o, &sig, &tob.o.to).is_err());
}
#[test]
fn test_fake_proof() {
let n: usize = rand::thread_rng().gen_range(0..100);
let m = MembershipProof::new(
std::iter::from_fn(|| Some(make_address()))
.take(n)
.collect(),
);
assert_eq!(m.subjects.len(), n);
assert!(m.hashes.len() < 2 * m.subjects.len());
assert_eq!(*m.hashes.last().unwrap(), m.root);
assert!(m.verify());
}
#[test]
fn test_zkvm_serialize() {
let nodes = rand::thread_rng().gen_range(0..100);
let o = rand_obligation();
let mp = MembershipProof::new(
std::iter::from_fn(|| Some(make_address()))
.take(nodes)
.collect(),
);
let c = Cert {
subject: o.o,
authenticity: AuthCert {
subject: o.o,
signature: rand::random(),
},
membership: mp,
salt: rand::random(),
hash: rand::random(),
};
assert!(c.zkvm_serialize().len() > 260);
assert!(c.zkvm_serialize().len() < 260 + (nodes * 32) * 2);
}
}

110
src/main.rs Normal file
View file

@ -0,0 +1,110 @@
use clap::Parser;
// use sp1_sdk::{utils, ProverClient, SP1Stdin};
use hex;
use mtcs_garage::{AuthCert, Cert, MembershipProof, Obligation};
// const ELF: &[u8] = include_bytes!("../elf/riscv32im-succinct-zkvm-elf");
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
#[arg(short, long)]
secret: String,
#[arg(short, long)]
from: String,
#[arg(short, long)]
to: String,
#[arg(short, long)]
value: u32,
// #[arg(short, long)]
// cache: String,
}
fn main() {
let args = Args::parse();
let (from, to) = (
hex::decode(args.from).unwrap().try_into().unwrap(),
hex::decode(args.to).unwrap().try_into().unwrap(),
);
let obligation = Obligation { from, to, value: args.value };
let cert = Cert {
subject: obligation,
authenticity: AuthCert::from_secret(
&obligation,
&hex::decode(args.secret).unwrap().try_into().unwrap(),
),
hash: [0; 32],
salt: [0; 32],
membership: MembershipProof::new(vec![from, to])
};
// utils::setup_tracer();
// let mut stdin = SP1Stdin::new();
// stdin.write(&cert.zkvm_serialize());
// let client = ProverClient::new();
// let (pk, vk) = client.prover.setup(ELF);
// let proof = client.prove(&pk, stdin);
// match proof {
// Ok(p) => println!("coool"),
// Err(_) => println!("not cool"),
// }
}
// fn main() {
// let nodes = rand::thread_rng().gen_range(0..100);
// let o = rand_obligation();
// let mp = MembershipProof::new(
// std::iter::from_fn(|| Some(make_address()))
// .take(nodes)
// .collect(),
// );
// let c = Cert {
// subject: o.o,
// authenticity: AuthCert {
// subject: o.o,
// signature: rand::random(),
// },
// membership: mp,
// salt: rand::random(),
// hash: rand::random(),
// };
// utils::setup_tracer();
// let mut stdin = SP1Stdin::new();
// stdin.write(&data_str);
// stdin.write(&key);
// stdin.write(&initial_account_state);
// stdin.write(&transactions);
// let mut proof = SP1Prover::prove(JSON_ELF, stdin).expect("proving failed");
// // Read output.
// let val = proof.stdout.read::<String>();
// println!("Value of {} is {}", key, val);
// let account_state = proof.stdout.read::<Account>();
// println!(
// "Final account state: {}",
// serde_json::to_string(&account_state).unwrap()
// );
// // Verify proof.
// SP1Verifier::verify(JSON_ELF, &proof).expect("verification failed");
// // Save proof.
// proof
// .save("proof-with-io.json")
// .expect("saving proof failed");
// println!("successfully generated and verified proof for the program!")
// }

34
tests/test_cli.rs Normal file
View file

@ -0,0 +1,34 @@
use assert_cmd::prelude::*;
use predicates::prelude::*;
use rand;
use std::process::Command;
use rstest::*;
use mtcs_garage::*;
use hex;
#[fixture]
fn cmd() -> Command {
Command::cargo_bin("credit5000").unwrap()
}
#[rstest]
fn cli_showes_help(mut cmd: Command) -> Result<(), Box<dyn std::error::Error>> {
cmd.arg("--help");
cmd.assert().stdout(predicate::str::contains("Usage: credit5000"));
Ok(())
}
#[rstest]
fn cli_stuff(mut cmd: Command) -> Result<(), Box<dyn std::error::Error>> {
let o = cmd.args([
"--secret", &hex::encode(rand::random::<Secret>()),
"--from", &hex::encode(rand::random::<Address>()),
"--to", &hex::encode(rand::random::<Address>()),
"--value", "1000",
]);
cmd.assert().stdout(predicate::str::contains("coool"));
Ok(())
}