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
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

55 changes: 42 additions & 13 deletions circuits/crates/bitcoin-light-client-circuit/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ use commit_chain::{
};
use header_chain::{
BitcoinMerkleTree, BlockHeaderCircuitOutput, CircuitBlockHeader, CircuitTransaction,
HeaderChainCircuitInput, MMRHost, SPV, verify_merkle_proof,
HeaderChainCircuitInput, MMRHost, MMRInclusionProof, SPV, verify_merkle_proof,
};
use state_chain::{
StateChainCircuitInput, StateChainCircuitOutput, verify_sequencer_commit, withdrawal_leaf,
};
use state_chain::{StateChainCircuitInput, StateChainCircuitOutput, verify_sequencer_commit};
use zkm_primitives::io::ZKMPublicValues;

use bitcoin::{Transaction, secp256k1::XOnlyPublicKey};
Expand Down Expand Up @@ -380,6 +382,8 @@ pub fn propose_longest_chain(
included_watchtowers: U256, //pis
graph_id: [u8; GRAPH_ID_SIZE], // pis
operator_genesis_sequencer_commit_txid: [u8; 32], // pis
gateway_address: [u8; 20], // bound by the constant
withdrawal_proof: MMRInclusionProof,

watchtower_challenge_init_txid: [u8; 32],
watchtower_challenge_init_txn: Option<Transaction>,
Expand Down Expand Up @@ -561,14 +565,14 @@ pub fn propose_longest_chain(
.expect("watchtower challenge defeats operator chain");
}

let mut is_found = false;
for withdrawal in &state_chain_output.chain_state.withdrawals {
if withdrawal.2.contains(&graph_id) {
is_found = true;
break;
}
}
assert!(is_found, "Graph id {graph_id:?} is not included in current state chain");
// The Gateway named by the constant processed a withdrawal for this graph.
assert!(
state_chain_output
.chain_state
.withdrawals
.verify_proof(withdrawal_leaf(&gateway_address, &graph_id), &withdrawal_proof),
"Graph id {graph_id:?} has no withdrawal from the Gateway in the state chain"
);

// (operator_total_work, included_watchtowers, graph_id, operator_genesis_sequencer_commit_txid, btc_best_block_hash)
//(hash_operator_inputs(graph_id, operator_genesis_sequencer_commit_txid), btc_best_block_hash)
Expand All @@ -580,6 +584,7 @@ pub fn propose_longest_chain(
let constant = hash_operator_constant(
graph_id,
operator_genesis_sequencer_commit_txid,
gateway_address,
watchtower_challenge_init_txid,
graph_watchtower_xonly_public_keys,
);
Expand All @@ -601,16 +606,21 @@ pub fn propose_longest_chain(
(operator_committed_blockhash, constant, included_watchtowers.to_le_bytes::<32>())
}

/// The graph constant committed by the Operator proof and Connector D. It names the
/// Gateway whose withdrawal the proof must show, so a withdrawal for the same graph id
/// recorded by another contract cannot satisfy it.
pub fn hash_operator_constant(
graph_id: [u8; GRAPH_ID_SIZE],
operator_genesis_sequencer_commit_txid: [u8; 32],
gateway_address: [u8; 20],
watchtower_challenge_init_txid: [u8; 32],
watchtower_xonly_public_keys: &[[u8; 32]],
) -> [u8; 32] {
let mut engine = sha256::HashEngine::default();
engine.input(b"bitvm/operator-constant/v3");
engine.input(b"bitvm/operator-constant/v4");
engine.input(&graph_id);
engine.input(&operator_genesis_sequencer_commit_txid);
engine.input(&gateway_address);
engine.input(&watchtower_challenge_init_txid);
engine.input(&(watchtower_xonly_public_keys.len() as u16).to_be_bytes());
for key in watchtower_xonly_public_keys {
Expand Down Expand Up @@ -1036,9 +1046,11 @@ mod tests {
let genesis_txid = [2u8; 32];
let watchtower_keys = [[3u8; 32], [4u8; 32]];
let challenge_init_txid = [5u8; 32];
let mut input = b"bitvm/operator-constant/v3".to_vec();
let gateway = [6u8; 20];
let mut input = b"bitvm/operator-constant/v4".to_vec();
input.extend_from_slice(&graph_id);
input.extend_from_slice(&genesis_txid);
input.extend_from_slice(&gateway);
input.extend_from_slice(&challenge_init_txid);
input.extend_from_slice(&(watchtower_keys.len() as u16).to_be_bytes());
for key in &watchtower_keys {
Expand All @@ -1047,18 +1059,35 @@ mod tests {
let expected = bitcoin::hashes::sha256::Hash::hash(&input);

assert_eq!(
hash_operator_constant(graph_id, genesis_txid, challenge_init_txid, &watchtower_keys),
hash_operator_constant(
graph_id,
genesis_txid,
gateway,
challenge_init_txid,
&watchtower_keys
),
*expected.as_byte_array()
);
assert_ne!(
hash_operator_constant(
graph_id,
genesis_txid,
gateway,
challenge_init_txid,
&[watchtower_keys[1], watchtower_keys[0]],
),
*expected.as_byte_array()
);
assert_ne!(
hash_operator_constant(
graph_id,
genesis_txid,
[7u8; 20],
challenge_init_txid,
&watchtower_keys
),
*expected.as_byte_array()
);
}

#[test]
Expand Down
136 changes: 132 additions & 4 deletions circuits/crates/state-chain/src/state_chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,62 @@ use alloy_primitives::{Address, B256, U256};
use bincode::Options as BincodeOptions;
use guest_executor::executor::EthClientExecutor;
use guest_executor::io::EthClientExecutorInput;
use header_chain::MMRGuest;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::sync::Arc;
use tendermint_light_client_verifier::types::LightBlock;

// Contract address, base slot and key, the expected value of the slot is hardcoded to 1.
type WithdrawalSlot = (Address, [u8; 32], Vec<[u8; 16]>);
pub type WithdrawalSlot = (Address, [u8; 32], Vec<[u8; 16]>);

/// Base storage slot of `Gateway.withdrawDataMap`, the only slot a withdrawal may
/// be read from. `forge inspect src/GatewayDebug.sol:GatewayDebug storage-layout`.
pub const WITHDRAW_DATA_BASE_SLOT: [u8; 32] = {
let mut slot = [0u8; 32];
slot[31] = 16;
slot
};

/// Leaf of the withdrawal accumulator: the Gateway that processed the withdrawal
/// and the graph it was processed for. The slot is fixed by [`WITHDRAW_DATA_BASE_SLOT`].
pub fn withdrawal_leaf(gateway: &[u8; 20], graph_id: &[u8; 16]) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(b"bitvm/withdrawal-leaf/v1");
hasher.update(gateway);
hasher.update(graph_id);
hasher.finalize().into()
}

/// One withdrawal in accumulator order, as the host replays it to build inclusion proofs.
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
pub struct WithdrawalRecord {
pub block_number: u64,
pub gateway: [u8; 20],
pub graph_id: [u8; 16],
}

impl WithdrawalRecord {
pub fn leaf(&self) -> [u8; 32] {
withdrawal_leaf(&self.gateway, &self.graph_id)
}
}

/// The withdrawals of `block` in the order the circuit appends them.
pub fn block_withdrawal_records(block: &CircuitStateBlock) -> Vec<WithdrawalRecord> {
let block_number = block.evm_block.current_block.number;
block
.withdrawals
.iter()
.flat_map(|(gateway, _, graph_ids)| {
graph_ids.iter().map(move |graph_id| WithdrawalRecord {
block_number,
gateway: gateway.into_array(),
graph_id: *graph_id,
})
})
.collect()
}

/// The input proof of the commit chain circuit.
/// The proof can be either None (implying the beginning) or a Succinct proof.
Expand All @@ -37,7 +87,8 @@ pub struct StateChainState {
pub genesis_evm_block_hash: [u8; 32],
pub latest_evm_block_hash: [u8; 32],
pub latest_cosmos_block: Vec<u8>,
pub withdrawals: Vec<WithdrawalSlot>,
/// Every withdrawal processed since genesis, one [`withdrawal_leaf`] each.
pub withdrawals: MMRGuest,
}

#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)]
Expand Down Expand Up @@ -94,7 +145,7 @@ impl StateChainState {
latest_evm_block_hash: genesis_evm_block_hash,
genesis_evm_block_hash,
latest_cosmos_block,
withdrawals: vec![],
withdrawals: MMRGuest::new(),
}
}

Expand Down Expand Up @@ -135,7 +186,9 @@ impl StateChainState {
&block.cosmos_txns,
&data_hash,
);
self.withdrawals.extend_from_slice(&block.withdrawals);
for record in block_withdrawal_records(&block) {
self.withdrawals.append(record.leaf());
}
}
self.evm_block_height += 1;
self.latest_evm_block_hash = current_block_hash;
Expand All @@ -158,6 +211,7 @@ pub fn execute_el_block_and_check_withdraw_tx(

let mut storage_info = vec![];
withdrawals.iter().for_each(|withdrawal| {
assert_eq!(withdrawal.1, WITHDRAW_DATA_BASE_SLOT, "withdrawal is not in withdrawDataMap");
for graph_id in &withdrawal.2 {
let mut data = [0u8; 32 * 2];
data[0..16].copy_from_slice(graph_id);
Expand Down Expand Up @@ -192,4 +246,78 @@ mod tests {
.unwrap();
chain_state.apply_blocks(blocks2);
}

#[test]
fn withdrawals_accumulate_in_block_order_and_prove_their_gateway() {
let blocks: Vec<CircuitStateBlock> =
serde_json::from_slice(include_bytes!("../../../data/state-chain/10346758-10.blocks"))
.unwrap();
let block_hash: [u8; 32] = blocks[0].evm_block.current_block.hash_slow().into();
let block_height = blocks[0].evm_block.current_block.header.number;
let mut chain_state =
StateChainState::new(block_height, block_hash, blocks[0].cosmos_block.clone());
assert_eq!(chain_state.withdrawals.size, 0);

// The fixture has no withdrawals; a block-level unit check covers the leaf order.
let gateway = Address::repeat_byte(0x11);
let other_gateway = Address::repeat_byte(0x22);
let mut block = blocks[1].clone();
block.withdrawals = vec![
(gateway, WITHDRAW_DATA_BASE_SLOT, vec![[1; 16], [2; 16]]),
(other_gateway, WITHDRAW_DATA_BASE_SLOT, vec![[1; 16]]),
];
let records = block_withdrawal_records(&block);
assert_eq!(
records,
vec![
WithdrawalRecord {
block_number: block.evm_block.current_block.number,
gateway: gateway.into_array(),
graph_id: [1; 16]
},
WithdrawalRecord {
block_number: block.evm_block.current_block.number,
gateway: gateway.into_array(),
graph_id: [2; 16]
},
WithdrawalRecord {
block_number: block.evm_block.current_block.number,
gateway: other_gateway.into_array(),
graph_id: [1; 16]
},
]
);
let mut host = header_chain::MMRHost::new();
for record in &records {
chain_state.withdrawals.append(record.leaf());
host.append(record.leaf());
}
assert_eq!(chain_state.withdrawals.size, 3);
let (leaf, proof) = host.generate_proof(1);
assert_eq!(leaf, withdrawal_leaf(&gateway.into_array(), &[2; 16]));
assert!(chain_state.withdrawals.verify_proof(leaf, &proof));
// The same graph id under another Gateway is a different leaf.
assert!(
!chain_state
.withdrawals
.verify_proof(withdrawal_leaf(&other_gateway.into_array(), &[2; 16]), &proof)
);
assert_ne!(
withdrawal_leaf(&gateway.into_array(), &[1; 16]),
withdrawal_leaf(&other_gateway.into_array(), &[1; 16])
);
}

#[test]
fn withdrawals_outside_withdraw_data_map_are_rejected() {
let blocks: Vec<CircuitStateBlock> =
serde_json::from_slice(include_bytes!("../../../data/state-chain/10346758-10.blocks"))
.unwrap();
let mut block = blocks[1].clone();
block.withdrawals = vec![(Address::repeat_byte(0x11), [0; 32], vec![[1; 16]])];
let result = std::panic::catch_unwind(|| {
execute_el_block_and_check_withdraw_tx(&block.withdrawals, block.evm_block.clone())
});
assert!(result.is_err());
}
}
6 changes: 5 additions & 1 deletion circuits/operator-proof/guest/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ use alloy_primitives::U256;
use bitcoin::Transaction;
use bitcoin_light_client_circuit::IndexedWatchtowerChallenge;
use commit_chain::CommitChainCircuitInput;
use header_chain::{HeaderChainCircuitInput, SPV};
use header_chain::{HeaderChainCircuitInput, MMRInclusionProof, SPV};
use state_chain::StateChainCircuitInput;

pub fn main() {
// calculate operator public input: https://github.com/ProjectZKM/Ziren/blob/main/crates/sdk/src/utils.rs#L42
let included_watchtowers: U256 = zkm_zkvm::io::read::<U256>();
let graph_id: [u8; 16] = zkm_zkvm::io::read::<[u8; 16]>();
let operator_genesis_sequencer_commit_txid: [u8; 32] = zkm_zkvm::io::read();
let gateway_address: [u8; 20] = zkm_zkvm::io::read();
let withdrawal_proof: MMRInclusionProof = zkm_zkvm::io::read();

// https://github.com/KSlashh/BitVM/blob/v2/goat/src/transactions/watchtower_challenge.rs#L128
let watchtower_challenge_init_txid: [u8; 32] = zkm_zkvm::io::read();
Expand All @@ -30,6 +32,8 @@ pub fn main() {
included_watchtowers,
graph_id,
operator_genesis_sequencer_commit_txid,
gateway_address,
withdrawal_proof,
watchtower_challenge_init_txid,
watchtower_challenge_init_txn,
watchtower_challenges,
Expand Down
Loading
Loading