From dfe57764c4464cfa72ab1edda82782cf4c548815 Mon Sep 17 00:00:00 2001 From: Arthur Paulino Date: Tue, 17 Sep 2024 12:24:27 -0300 Subject: [PATCH] feat: micro-chain simulator Implement the following meta commands: * `micro-chain-serve` to start a chain server * `micro-chain-get` for a client to get the current server state * `micro-chain-transition` for a client to send a chain proof to the server This is a minimal implementation for a demo and doesn't assume too much from the actual final micro-chain system, on which we will have more info later. --- src/lurk/cli/lurk_data.rs | 7 +- src/lurk/cli/meta.rs | 254 +++++++++++++++++++++++++++++++++++--- src/lurk/cli/proofs.rs | 14 ++- src/lurk/cli/repl.rs | 19 ++- src/lurk/cli/zdag.rs | 14 +++ src/lurk/state.rs | 5 +- 6 files changed, 280 insertions(+), 33 deletions(-) diff --git a/src/lurk/cli/lurk_data.rs b/src/lurk/cli/lurk_data.rs index e2d10950..31fd9479 100644 --- a/src/lurk/cli/lurk_data.rs +++ b/src/lurk/cli/lurk_data.rs @@ -10,7 +10,7 @@ use super::zdag::ZDag; #[derive(Serialize, Deserialize)] pub(crate) struct LurkData { - zptr: ZPtr, + pub(crate) zptr: ZPtr, zdag: ZDag, } @@ -31,4 +31,9 @@ impl LurkData { zdag.populate_zstore(zstore); zptr } + + #[inline] + pub(crate) fn has_opaque_data(&self) -> bool { + self.zdag.has_opaque_data(&self.zptr) + } } diff --git a/src/lurk/cli/meta.rs b/src/lurk/cli/meta.rs index 07335a6d..52ee6b58 100644 --- a/src/lurk/cli/meta.rs +++ b/src/lurk/cli/meta.rs @@ -4,7 +4,12 @@ use itertools::Itertools; use p3_baby_bear::BabyBear; use p3_field::PrimeField32; use rustc_hash::FxHashMap; +use serde::{Deserialize, Serialize}; use sphinx_core::stark::StarkGenericConfig; +use std::{ + io::{Read, Write}, + net::{TcpListener, TcpStream}, +}; use crate::{ lair::{chipset::Chipset, lair_chip::LairMachineProgram}, @@ -12,7 +17,7 @@ use crate::{ big_num::field_elts_to_biguint, cli::{ paths::{commits_dir, proofs_dir}, - proofs::IOProof, + proofs::{get_verifier_version, IOProof}, repl::Repl, }, package::{Package, SymbolRef}, @@ -22,7 +27,12 @@ use crate::{ }, }; -use super::{comm_data::CommData, debug::debug_mode, lurk_data::LurkData, proofs::ProtocolProof}; +use super::{ + comm_data::CommData, + debug::debug_mode, + lurk_data::LurkData, + proofs::{ChainProof, ProtocolProof}, +}; #[allow(dead_code)] #[allow(clippy::type_complexity)] @@ -165,7 +175,7 @@ impl> MetaCmd { run: |repl, args, _path| { if args != repl.zstore.nil() { let expr = *repl.peek1(args)?; - let result = repl.handle_non_meta(&expr); + let result = repl.handle_non_meta(&expr, None); debug_mode(&repl.format_debug_data())?; result.map(|_| ()) } else { @@ -419,7 +429,7 @@ impl> MetaCmd { }, }; - fn call(repl: &mut Repl, call_expr: &ZPtr) -> Result> { + fn call(repl: &mut Repl, call_expr: &ZPtr, env: Option>) -> Result> { if call_expr == repl.zstore.nil() { bail!("Missing callable object"); } @@ -434,7 +444,7 @@ impl> MetaCmd { } _ => (), } - repl.handle_non_meta(call_expr) + repl.handle_non_meta(call_expr, env) } const CALL: Self = Self { @@ -447,7 +457,7 @@ impl> MetaCmd { "!(call #0x83420bafb3cb56870b10b498607c0a6314b0ea331328bbb232c74078abb5dc 0)", ], run: |repl, args, _path| { - Self::call(repl, args)?; + Self::call(repl, args, None)?; Ok(()) }, }; @@ -486,7 +496,7 @@ impl> MetaCmd { "!(chain #0x5a34ed7712c5fd2f324feb0e1764b27bac9259c4b663e4601e678939a9363d 1)", ], run: |repl, args, _path| { - let cons = Self::call(repl, args)?; + let cons = Self::call(repl, args, None)?; Self::persist_chain_comm(repl, &cons) }, }; @@ -506,6 +516,26 @@ impl> MetaCmd { } } + fn transition_call( + repl: &mut Repl, + current_state_expr: &ZPtr, + call_args: ZPtr, + env: Option>, + ) -> Result> { + let (current_state, _) = repl.reduce_aux(current_state_expr)?; + if current_state.tag != Tag::Cons { + bail!("Current state must reduce to a pair"); + } + // reduce `call_args` elements + let list_sym = repl.zstore.intern_symbol(&builtin_sym("list")); + let list_expr = repl.zstore.intern_cons(list_sym, call_args); + let (call_args, _) = repl.reduce_aux(&list_expr)?; + repl.memoize_dag(current_state.tag, ¤t_state.digest); + let (_, &callable) = repl.zstore.fetch_tuple2(¤t_state); + let call_expr = repl.zstore.intern_cons(callable, call_args); + Self::call(repl, &call_expr, env) + } + const TRANSITION: Self = Self { name: "transition", summary: "Chains a callable object and binds the next state to a variable", @@ -513,20 +543,13 @@ impl> MetaCmd { info: &["It has the same side effects of the `chain` meta command."], example: &["!(transition new-state old-state input0)"], run: |repl, args, _path| { - let (&new_state_sym, rest) = repl.car_cdr(args); + let (&next_state_sym, rest) = repl.car_cdr(args); + Self::validate_binding_var(repl, &next_state_sym)?; let (¤t_state_expr, &call_args) = repl.car_cdr(rest); - Self::validate_binding_var(repl, &new_state_sym)?; - let (current_state, _) = repl.reduce_aux(¤t_state_expr)?; - if current_state.tag != Tag::Cons { - bail!("Current state must reduce to a pair"); - } - repl.memoize_dag(current_state.tag, ¤t_state.digest); - let (_, &callable) = repl.zstore.fetch_tuple2(¤t_state); - let call_expr = repl.zstore.intern_cons(callable, call_args); - let cons = Self::call(repl, &call_expr)?; + let cons = Self::transition_call(repl, ¤t_state_expr, call_args, None)?; Self::persist_chain_comm(repl, &cons)?; - println!("{}", repl.fmt(&new_state_sym)); - repl.env = repl.zstore.intern_env(new_state_sym, cons, repl.env); + println!("{}", repl.fmt(&next_state_sym)); + repl.env = repl.zstore.intern_env(next_state_sym, cons, repl.env); Ok(()) }, }; @@ -726,6 +749,7 @@ impl> MetaCmd { let description = get_prop("description", |val| val.tag == Tag::Str, empty_str)?; let protocol = repl.zstore.intern_list([vars, body, lang, description]); + println!("{}", repl.fmt(&name)); repl.env = repl.zstore.intern_env(name, protocol, repl.env); Ok(()) }, @@ -790,7 +814,7 @@ impl> MetaCmd { run: |repl, args, _path| { if args != repl.zstore.nil() { let expr = *repl.peek1(args)?; - repl.handle_non_meta(&expr)?; + repl.handle_non_meta(&expr, None)?; } repl.prove_last_reduction()?; Ok(()) @@ -1035,6 +1059,9 @@ impl> MetaCmd { let protocol_proof_bytes = std::fs::read(path_str)?; let ProtocolProof { crypto_proof, args } = bincode::deserialize(&protocol_proof_bytes)?; + if args.has_opaque_data() { + bail!("Arguments can't have opaque data"); + } let args = args.populate_zstore(&mut repl.zstore); let (args_vec_reduced, None) = repl.zstore.fetch_list(&args) else { bail!("Arguments must be a list"); @@ -1075,6 +1102,190 @@ impl> MetaCmd { Ok(()) }, }; + + const MICRO_CHAIN_SERVE: Self = Self { + name: "micro-chain-serve", + summary: + "Starts a server to manage state transitions by receiving proofs of chained callables.", + format: "!(micro-chain-serve )", + info: &["The initial state must follow the format of chain outputs."], + example: &["!(micro-chain-serve \"127.0.0.1:1234\" (some-callable init-arg0 init-arg1))"], + run: |repl, args, _path| { + let (addr, &state_expr) = repl.peek2(args)?; + if addr.tag != Tag::Str { + bail!("Address must be a string"); + } + let addr_str = repl.zstore.fetch_string(addr); + let (mut state, _) = repl.reduce_aux(&state_expr)?; + if state.tag != Tag::Cons { + bail!("Initial state must be a pair"); + } + + let listener = TcpListener::bind(&addr_str)?; + println!("Listening at {addr_str}"); + let empty_env = repl.zstore.intern_empty_env(); + for stream in listener.incoming() { + match stream { + Ok(mut stream) => match read_data::(&mut stream)? { + Request::Get => { + repl.memoize_dag(Tag::Cons, &state.digest); + let lurk_data = LurkData::new(state, &repl.zstore); + write_data(&mut stream, lurk_data)?; + } + Request::Transition(chain_proof) => { + let ChainProof { + crypto_proof, + call_args, + next_state, + } = chain_proof; + if next_state.zptr.tag != Tag::Cons { + write_data(&mut stream, Response::NextStateNotCons)?; + continue; + } + if next_state.has_opaque_data() { + write_data(&mut stream, Response::NextStateIsOpaque)?; + continue; + } + let (_, callable) = repl.zstore.fetch_tuple2(&state); + let expr = repl.zstore.intern_cons(*callable, call_args); + let machine_proof = crypto_proof.into_machine_proof( + &expr, + &empty_env, + &next_state.zptr, + ); + let machine = repl.stark_machine(); + let (_, vk) = machine.setup(&LairMachineProgram); + let challenger = &mut machine.config().challenger(); + if machine.verify(&vk, &machine_proof, challenger).is_err() { + write_data( + &mut stream, + Response::ProofVerificationFailed( + get_verifier_version().to_string(), + ), + )?; + continue; + } + write_data(&mut stream, Response::ProofAccepted)?; + state = next_state.zptr; + } + }, + Err(e) => { + eprintln!("Connection failed: {e}"); + } + } + } + Ok(()) + }, + }; + + const MICRO_CHAIN_GET: Self = Self { + name: "micro-chain-get", + summary: "Binds the current state from a micro-chain server to a symbol", + format: "!(micro-chain-get )", + info: &[], + example: &["!(micro-chain-get \"127.0.0.1:1234\" state0)"], + run: |repl, args, _path| { + let (addr, &state_sym) = repl.peek2(args)?; + if addr.tag != Tag::Str { + bail!("Address must be a string"); + } + Self::validate_binding_var(repl, &state_sym)?; + let addr_str = repl.zstore.fetch_string(addr); + let stream = &mut TcpStream::connect(addr_str)?; + write_data(stream, Request::Get)?; + let lurk_data: LurkData = read_data(stream)?; + let state = lurk_data.populate_zstore(&mut repl.zstore); + println!("{}", repl.fmt(&state_sym)); + repl.env = repl.zstore.intern_env(state_sym, state, repl.env); + Ok(()) + }, + }; + + const MICRO_CHAIN_TRANSITION: Self = Self { + name: "micro-chain-transition", + summary: + "Proves a state transition via chaining and sends the proof to a micro-chain server", + format: "!(micro-chain-transition )", + info: &["The transition is successful iff the proof is accepted by the server"], + example: &["!(micro-chain-transition \"127.0.0.1:1234\" state1 state0 arg0 arg1)"], + run: |repl, args, _path| { + let (&addr, rest) = repl.car_cdr(args); + if addr.tag != Tag::Str { + bail!("Address must be a string"); + } + let (&next_state_sym, rest) = repl.car_cdr(rest); + Self::validate_binding_var(repl, &next_state_sym)?; + let (¤t_state_expr, &call_args) = repl.car_cdr(rest); + let empty_env = repl.zstore.intern_empty_env(); + let state = + Self::transition_call(repl, ¤t_state_expr, call_args, Some(empty_env))?; + let proof_key = repl.prove_last_reduction()?; + let io_proof = Self::load_io_proof(&proof_key)?; + let crypto_proof = io_proof.crypto_proof; + let next_state = LurkData::new(state, &repl.zstore); + let chain_proof = ChainProof { + crypto_proof, + call_args, + next_state, + }; + let addr_str = repl.zstore.fetch_string(&addr); + let stream = &mut TcpStream::connect(addr_str)?; + write_data(stream, Request::Transition(chain_proof))?; + match read_data::(stream)? { + Response::ProofAccepted => { + println!("Proof accepted by the server"); + println!("{}", repl.fmt(&next_state_sym)); + repl.env = repl.zstore.intern_env(next_state_sym, state, repl.env); + } + Response::NextStateNotCons => bail!("Next state is not a pair"), + Response::NextStateIsOpaque => bail!("Next state can't be opaque"), + Response::ProofVerificationFailed(verifier_version) => { + let mut msg = "Proof verification failed".to_string(); + if verifier_version != get_verifier_version() { + msg.push_str( + "\nWarning: proof was created for a different verifier version", + ); + } + bail!(msg); + } + Response::State(_) => unreachable!(), + } + Ok(()) + }, + }; +} + +#[derive(Serialize, Deserialize)] +enum Request { + Get, + Transition(ChainProof), +} + +#[derive(Serialize, Deserialize)] +enum Response { + State(LurkData), + ProofAccepted, + NextStateNotCons, + NextStateIsOpaque, + ProofVerificationFailed(String), +} + +fn read_data Deserialize<'a>>(stream: &mut TcpStream) -> Result { + let mut size_bytes = [0; 8]; + stream.read_exact(&mut size_bytes)?; + let size = usize::from_le_bytes(size_bytes); + let mut data_buffer = vec![0; size]; + stream.read_exact(&mut data_buffer)?; + let data = bincode::deserialize(&data_buffer)?; + Ok(data) +} + +fn write_data(stream: &mut TcpStream, data: T) -> Result<()> { + let data_bytes = bincode::serialize(&data)?; + stream.write_all(&data_bytes.len().to_le_bytes())?; + stream.write_all(&data_bytes)?; + stream.flush()?; + Ok(()) } fn copy_inner<'a, T: Copy + 'a, I: IntoIterator>(xs: I) -> Vec { @@ -1113,6 +1324,9 @@ pub(crate) fn meta_cmds>() -> MetaCmdsMap { MetaCmd::DEFPROTOCOL, MetaCmd::PROVE_PROTOCOL, MetaCmd::VERIFY_PROTOCOL, + MetaCmd::MICRO_CHAIN_SERVE, + MetaCmd::MICRO_CHAIN_GET, + MetaCmd::MICRO_CHAIN_TRANSITION, MetaCmd::HELP, ] .map(|mc| (mc.name, mc)) diff --git a/src/lurk/cli/proofs.rs b/src/lurk/cli/proofs.rs index ffec1353..42eacacb 100644 --- a/src/lurk/cli/proofs.rs +++ b/src/lurk/cli/proofs.rs @@ -36,6 +36,11 @@ pub(crate) struct CryptoProof { type F = BabyBear; +#[inline] +pub(crate) fn get_verifier_version() -> &'static str { + env!("VERGEN_GIT_SHA") +} + impl CryptoProof { #[inline] pub(crate) fn into_machine_proof( @@ -73,7 +78,7 @@ impl CryptoProof { #[inline] pub(crate) fn has_same_verifier_version(&self) -> bool { - self.verifier_version == env!("VERGEN_GIT_SHA") + self.verifier_version == get_verifier_version() } } @@ -190,3 +195,10 @@ impl ProtocolProof { } } } + +#[derive(Serialize, Deserialize)] +pub(crate) struct ChainProof { + pub(crate) crypto_proof: CryptoProof, + pub(crate) call_args: ZPtr, + pub(crate) next_state: LurkData, +} diff --git a/src/lurk/cli/repl.rs b/src/lurk/cli/repl.rs index 2568aead..5dca81da 100644 --- a/src/lurk/cli/repl.rs +++ b/src/lurk/cli/repl.rs @@ -485,14 +485,13 @@ impl> Repl { result_data.map(|data| ZPtr::from_flat_data(&data)) } - #[inline] - pub(crate) fn reduce(&mut self, expr: &ZPtr) -> Result> { - let env = self.env; - self.reduce_with_env(expr, &env) - } - - pub(crate) fn handle_non_meta(&mut self, expr: &ZPtr) -> Result> { - let result = self.reduce(expr)?; + pub(crate) fn handle_non_meta( + &mut self, + expr: &ZPtr, + env: Option>, + ) -> Result> { + let env = env.unwrap_or(self.env); + let result = self.reduce_with_env(expr, &env)?; self.memoize_dag(result.tag, &result.digest); let iterations = self.queries.func_queries[self.eval_idx].len(); println!( @@ -550,7 +549,7 @@ impl> Repl { if is_meta { self.handle_meta(&zptr, file_dir)?; } else { - let result = self.handle_non_meta(&zptr)?; + let result = self.handle_non_meta(&zptr, None)?; if result.tag == Tag::Err { // error out when loading a file bail!("Reduction error: {}", self.fmt(&result)); @@ -615,7 +614,7 @@ impl> Repl { { eprintln!("!Error: {e}"); } - } else if let Err(e) = self.handle_non_meta(&zptr) { + } else if let Err(e) = self.handle_non_meta(&zptr, None) { eprintln!("Error: {e}"); } line = rest.to_string(); diff --git a/src/lurk/cli/zdag.rs b/src/lurk/cli/zdag.rs index 98708257..b796050a 100644 --- a/src/lurk/cli/zdag.rs +++ b/src/lurk/cli/zdag.rs @@ -84,4 +84,18 @@ impl ZDag { zstore.dag.insert(zptr, zptr_type); } } + + /// Whether there's data missing in the ZDag or not + pub(crate) fn has_opaque_data(&self, zptr: &ZPtr) -> bool { + match self.0.get(zptr) { + None => true, + Some(ZPtrType::Atom) => false, + Some(ZPtrType::Tuple2(a, b) | ZPtrType::Compact10(a, b)) => { + self.has_opaque_data(a) || self.has_opaque_data(b) + } + Some(ZPtrType::Compact110(a, b, c)) => { + self.has_opaque_data(a) || self.has_opaque_data(b) || self.has_opaque_data(c) + } + } + } } diff --git a/src/lurk/state.rs b/src/lurk/state.rs index bc0ba093..51f185d9 100644 --- a/src/lurk/state.rs +++ b/src/lurk/state.rs @@ -304,7 +304,7 @@ pub(crate) const BUILTIN_SYMBOLS: [&str; 38] = [ "breakpoint", ]; -const META_SYMBOLS: [&str; 30] = [ +const META_SYMBOLS: [&str; 33] = [ "def", "defrec", "load", @@ -335,4 +335,7 @@ const META_SYMBOLS: [&str; 30] = [ "defprotocol", "prove-protocol", "verify-protocol", + "micro-chain-serve", + "micro-chain-get", + "micro-chain-transition", ];