Skip to content

Commit

Permalink
feat: env and bind built-ins (#390)
Browse files Browse the repository at this point in the history
Implement `env` and `bind` as primitives for env construction.

Extra:
* Adapt the microchain server to send only a slice of the transition
  proofs, chosen by the client. This is necessary for when the VM
  changes (as in this patch) and the client would no longer accept
  old proofs sent by the server. Then the client can reconsider what
  qualifies as the new genesis and verify proofs from there.
  • Loading branch information
arthurpaulino authored Nov 14, 2024
1 parent 5d0dbad commit 9afe288
Show file tree
Hide file tree
Showing 6 changed files with 327 additions and 22 deletions.
25 changes: 15 additions & 10 deletions src/core/cli/meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1381,26 +1381,32 @@ impl<C1: Chipset<F>, C2: Chipset<F>> MetaCmd<F, C1, C2> {
example: &["!(microchain-verify \"127.0.0.1:1234\" #c0x123 genesis current)"],
returns: "t",
run: |repl, args, _dir| {
let [&addr_expr, &id_expr, &genesis_state_expr, &current_state_expr] =
repl.take(args)?;
let [&addr_expr, &id_expr, &initial_state_expr, &final_state_expr] = repl.take(args)?;
let (addr, _) = repl.reduce_aux(&addr_expr)?;
if addr.tag != Tag::Str {
bail!("Address must be a string");
}
let (id, _) = repl.reduce_aux(&id_expr)?;
let (genesis_state, _) = repl.reduce_aux(&genesis_state_expr)?;
if genesis_state.tag != Tag::Cons {
let (initial_state, _) = repl.reduce_aux(&initial_state_expr)?;
if initial_state.tag != Tag::Cons {
bail!("Initial state must be a pair");
}
let (final_state, _) = repl.reduce_aux(&final_state_expr)?;
if final_state.tag != Tag::Cons {
bail!("Final state must be a pair");
}
let addr_str = repl.zstore.fetch_string(&addr);
let stream = &mut TcpStream::connect(addr_str)?;
write_data(stream, Request::GetProofs(id.digest))?;
write_data(
stream,
Request::GetProofs(id.digest, initial_state.digest, final_state.digest),
)?;
let Response::Proofs(proofs) = read_data(stream)? else {
bail!("Could not read proofs from server");
};
repl.memoize_dag(&genesis_state);
let (_, &(mut callable)) = repl.zstore.fetch_tuple11(&genesis_state);
let mut state = genesis_state;
repl.memoize_dag(&initial_state);
let (_, &(mut callable)) = repl.zstore.fetch_tuple11(&initial_state);
let mut state = initial_state;
let empty_env = repl.zstore.intern_empty_env();
for (i, proof) in proofs.into_iter().enumerate() {
let OpaqueChainProof {
Expand All @@ -1421,8 +1427,7 @@ impl<C1: Chipset<F>, C2: Chipset<F>> MetaCmd<F, C1, C2> {
callable = next_callable;
state = result;
}
let (current_state, _) = repl.reduce_aux(&current_state_expr)?;
if state != current_state {
if state != final_state {
bail!("Chain final state doesn't match target final state");
}
println!("Microchain verification succeeded");
Expand Down
136 changes: 130 additions & 6 deletions src/core/cli/microchain.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use anyhow::Result;
use clap::Args;
use p3_baby_bear::BabyBear;
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
use sphinx_core::stark::StarkGenericConfig;
use std::{
hash::Hash,
io::{Read, Write},
net::{TcpListener, TcpStream},
};
Expand Down Expand Up @@ -93,7 +95,7 @@ pub(crate) enum Request {
GetGenesis([F; DIGEST_SIZE]),
GetState([F; DIGEST_SIZE]),
Transition([F; DIGEST_SIZE], ChainProof),
GetProofs([F; DIGEST_SIZE]),
GetProofs([F; DIGEST_SIZE], [F; DIGEST_SIZE], [F; DIGEST_SIZE]),
}

#[derive(Serialize, Deserialize)]
Expand All @@ -107,6 +109,8 @@ pub(crate) enum Response {
NextCallableIsFlawed,
ProofVerificationFailed(String),
ProofAccepted,
NoProofForInitialState,
NoProofForFinalState,
Proofs(Vec<OpaqueChainProof>),
}

Expand Down Expand Up @@ -207,15 +211,15 @@ impl MicrochainArgs {
let callable_zptr = state.callable_data.zptr(&mut zstore);
let expr = zstore.intern_cons(callable_zptr, call_args);

// the result is a pair composed by the chain result and next callable
// the next state is a pair composed by the chain result and next callable
// provided by the client
let result =
let next_state =
zstore.intern_cons(next_chain_result_zptr, next_callable_zptr);

// and now the proof must verify, meaning that the user must have
// used the correct callable from the server state
let machine_proof =
crypto_proof.into_machine_proof(&expr, &empty_env, &result);
crypto_proof.into_machine_proof(&expr, &empty_env, &next_state);
let machine = new_machine(&toplevel);
let (_, vk) = machine.setup(&LairMachineProgram);
let challenger = &mut machine.config().challenger();
Expand Down Expand Up @@ -244,12 +248,95 @@ impl MicrochainArgs {
},
)?;

// update the proof index
let mut proof_index = load_proof_index(&id).unwrap_or_default();
let prev_chain_result_zptr = state.chain_result.zptr;
let prev_state =
zstore.intern_cons(prev_chain_result_zptr, callable_zptr);
proof_index.insert(
prev_state.digest,
next_state.digest,
proofs.len() - 1,
);
dump_proof_index(&id, &proof_index)?;

return_msg!(Response::ProofAccepted);
}
Request::GetProofs(id) => {
let Ok(proofs) = load_proofs(&id) else {
Request::GetProofs(id, initial_digest, final_digest) => {
let Ok(mut proofs) = load_proofs(&id) else {
return_msg!(Response::NoDataForId);
};
// let proof_index = load_proof_index(&id)?;
// let Some(initial_index) = proof_index.index_by_prev(&initial_digest) else {
// return_msg!(Response::NoProofForInitialState);
// };
// let Some(final_index) = proof_index.index_by_next(&final_digest) else {
// return_msg!(Response::NoProofForFinalState);
// };

// the following code snippet is only meant to support version transitioning
// and should be eliminated (in favor of the code above) once legacy microchains
// are dropped
let proof_index = load_proof_index(&id).unwrap_or_default();
let initial_index =
if let Some(index) = proof_index.index_by_prev(&initial_digest) {
index
} else {
let (_, genesis_state) = load_genesis(&id)?;
let genesis_result_zptr = genesis_state.chain_result.zptr;
let genesis_callable_zptr =
genesis_state.callable_data.zptr(&mut zstore);
let genesis_zptr = zstore
.intern_cons(genesis_result_zptr, genesis_callable_zptr);
if genesis_zptr.digest == initial_digest {
0
} else {
let mut index = None;
for (i, proof) in proofs.iter().enumerate() {
let OpaqueChainProof {
next_chain_result,
next_callable,
..
} = proof;
let next_state = zstore
.intern_cons(*next_chain_result, *next_callable);
if next_state.digest == initial_digest {
index = Some(i + 1);
break;
}
}
let Some(index) = index else {
return_msg!(Response::NoProofForInitialState);
};
index
}
};
let final_index =
if let Some(index) = proof_index.index_by_next(&final_digest) {
index
} else {
let mut index = None;
for (i, proof) in proofs.iter().enumerate() {
let OpaqueChainProof {
next_chain_result,
next_callable,
..
} = proof;
let next_state =
zstore.intern_cons(*next_chain_result, *next_callable);
if next_state.digest == final_digest {
index = Some(i);
break;
}
}
let Some(index) = index else {
return_msg!(Response::NoProofForFinalState);
};
index
};

proofs.truncate(final_index + 1);
proofs.drain(..initial_index);
return_msg!(Response::Proofs(proofs));
}
}
Expand All @@ -262,6 +349,35 @@ impl MicrochainArgs {
}
}

/// Holds indices of proofs in a sequence of state transitions. The index of a
/// proof can be looked up by the digest of the previous state or by the digest
/// of the next state.
#[derive(Serialize, Deserialize, Default)]
struct ProofIndex<F: Hash + Eq> {
prev_map: FxHashMap<[F; DIGEST_SIZE], usize>,
next_map: FxHashMap<[F; DIGEST_SIZE], usize>,
}

impl<F: Hash + Eq> ProofIndex<F> {
fn insert(
&mut self,
prev_digest: [F; DIGEST_SIZE],
next_digest: [F; DIGEST_SIZE],
index: usize,
) {
self.prev_map.insert(prev_digest, index);
self.next_map.insert(next_digest, index);
}

fn index_by_prev(&self, digest: &[F]) -> Option<usize> {
self.prev_map.get(digest).copied()
}

fn index_by_next(&self, digest: &[F]) -> Option<usize> {
self.next_map.get(digest).copied()
}
}

fn dump_microchain_data<T: Serialize + ?Sized>(id: &[F], name: &str, data: &T) -> Result<()> {
let hash = format!("{:x}", field_elts_to_biguint(id));
let dir = microchains_dir()?.join(hash);
Expand All @@ -282,6 +398,10 @@ fn dump_state(id: &[F], state: &ChainState) -> Result<()> {
dump_microchain_data(id, "state", state)
}

fn dump_proof_index(id: &[F], proof_index: &ProofIndex<F>) -> Result<()> {
dump_microchain_data(id, "proof_index", proof_index)
}

fn load_microchain_data<T: for<'a> Deserialize<'a>>(id: &[F], name: &str) -> Result<T> {
let hash = format!("{:x}", field_elts_to_biguint(id));
let bytes = std::fs::read(microchains_dir()?.join(hash).join(name))?;
Expand All @@ -301,6 +421,10 @@ fn load_state(id: &[F]) -> Result<ChainState> {
load_microchain_data(id, "state")
}

fn load_proof_index(id: &[F]) -> Result<ProofIndex<F>> {
load_microchain_data(id, "proof_index")
}

pub(crate) fn read_data<T: for<'a> Deserialize<'a>>(stream: &mut TcpStream) -> Result<T> {
let mut size_bytes = [0; 8];
stream.read_exact(&mut size_bytes)?;
Expand Down
Loading

0 comments on commit 9afe288

Please sign in to comment.