Skip to content

aptos light client and verification #3903

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
34 changes: 34 additions & 0 deletions Cargo.lock

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

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -70,6 +70,7 @@ members = [
"lib/scroll-verifier",
"lib/tendermint-verifier",

"lib/aptos-light-client-types",
"lib/arbitrum-light-client-types",
"lib/berachain-light-client-types",
"lib/cometbls-light-client-types",
@@ -86,6 +87,7 @@ members = [

"cosmwasm/deployer",

"cosmwasm/ibc-union/lightclient/aptos",
"cosmwasm/ibc-union/lightclient/arbitrum",
"cosmwasm/ibc-union/lightclient/berachain",
"cosmwasm/ibc-union/lightclient/cometbls",
@@ -281,6 +283,7 @@ reconnecting-jsonrpc-ws-client = { path = "lib/reconnecting-jsonrpc-ws-client",
ibc-classic-spec = { path = "lib/ibc-classic-spec", default-features = false }
ibc-union-spec = { path = "lib/ibc-union-spec", default-features = false }

aptos-light-client-types = { path = "lib/aptos-light-client-types", default-features = false }
movement-light-client-types = { path = "lib/movement-light-client-types", default-features = false }

cosmos-sdk-event = { path = "lib/cosmos-sdk-event", default-features = false }
37 changes: 37 additions & 0 deletions cosmwasm/ibc-union/lightclient/aptos/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
[package]
name = "aptos-light-client"
version = "0.0.0"

authors = { workspace = true }
edition = { workspace = true }
license-file = { workspace = true }
publish = { workspace = true }
repository = { workspace = true }

[lints]
workspace = true

[dependencies]
aptos-light-client-types = { workspace = true, features = ["ethabi", "serde", "bincode"] }
aptos-verifier = { workspace = true }
bcs = { workspace = true }
cosmwasm-std = { workspace = true, features = ["abort"] }
hex-literal = { workspace = true }
ibc-union-light-client = { workspace = true }
ibc-union-msg = { workspace = true }
rlp = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde-utils = { workspace = true }
thiserror = { workspace = true }
unionlabs = { workspace = true }
unionlabs-cosmwasm-upgradable = { workspace = true }

[dev-dependencies]
aptos-crypto = { workspace = true }
aptos-types = { workspace = true }

[lib]
crate-type = ["cdylib", "rlib"]

[features]
library = []
216 changes: 216 additions & 0 deletions cosmwasm/ibc-union/lightclient/aptos/src/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
use aptos_light_client_types::{
client_state::ClientState, consensus_state::ConsensusState, header::Header,
};
use cosmwasm_std::Empty;
use ibc_union_light_client::IbcClientError;
use ibc_union_msg::lightclient::{Status, VerifyCreationResponseEvent};
use unionlabs::{
aptos::{
account::AccountAddress, storage_proof::StorageProof, transaction_info::TransactionInfo,
},
encoding::Bincode,
primitives::H256,
};

use crate::error::Error;

pub enum AptosLightClient {}

impl ibc_union_light_client::IbcClient for AptosLightClient {
type Error = Error;

type CustomQuery = Empty;

type Header = Header;

type Misbehaviour = Header;

type ClientState = ClientState;

type ConsensusState = ConsensusState;

type StorageProof = StorageProof;

type Encoding = Bincode;

fn verify_membership(
ctx: ibc_union_light_client::IbcClientCtx<Self>,
height: u64,
key: Vec<u8>,
storage_proof: Self::StorageProof,
value: Vec<u8>,
) -> Result<(), ibc_union_light_client::IbcClientError<Self>> {
let client_state = ctx.read_self_client_state()?;
let consensus_state = ctx.read_self_consensus_state(height)?;
verify_membership(
&key,
consensus_state.state_root,
client_state.table_handle,
storage_proof,
&value,
)
.map_err(Into::into)
}

fn verify_non_membership(
_ctx: ibc_union_light_client::IbcClientCtx<Self>,
_height: u64,
_key: Vec<u8>,
_storage_proof: Self::StorageProof,
) -> Result<(), ibc_union_light_client::IbcClientError<Self>> {
unimplemented!()
}

fn get_timestamp(consensus_state: &Self::ConsensusState) -> u64 {
consensus_state.timestamp
}

fn get_latest_height(client_state: &Self::ClientState) -> u64 {
client_state.latest_block_num
}

fn get_counterparty_chain_id(client_state: &Self::ClientState) -> String {
client_state.chain_id.clone()
}

fn status(client_state: &Self::ClientState) -> Status {
if client_state.frozen_height.height() != 0 {
Status::Frozen
} else {
Status::Active
}
}

fn verify_creation(
_client_state: &Self::ClientState,
_consensus_state: &Self::ConsensusState,
) -> Result<Option<Vec<VerifyCreationResponseEvent>>, IbcClientError<AptosLightClient>> {
Ok(None)
}

fn verify_header(
_ctx: ibc_union_light_client::IbcClientCtx<Self>,
_header: Self::Header,
_caller: cosmwasm_std::Addr,
) -> Result<
(u64, Self::ClientState, Self::ConsensusState),
ibc_union_light_client::IbcClientError<Self>,
> {
unimplemented!()
}

fn misbehaviour(
_ctx: ibc_union_light_client::IbcClientCtx<Self>,
_misbehaviour: Self::Misbehaviour,
) -> Result<Self::ClientState, ibc_union_light_client::IbcClientError<Self>> {
unimplemented!()
}
}

pub fn update_state(
mut client_state: ClientState,
header: Header,
) -> Result<(u64, ClientState, ConsensusState), Error> {
let TransactionInfo::V0(tx_info) = header.tx_proof.transaction_info;

let consensus_state = ConsensusState {
state_root: H256::new(*tx_info.state_checkpoint_hash.unwrap().get()), // TODO(aeryz): we always need this, no need to make this not an option
timestamp: header
.state_proof
.latest_ledger_info()
.commit_info
.timestamp_usecs,
};

if header.new_height > client_state.latest_block_num {
client_state.latest_block_num = header.new_height;
}

Ok((header.new_height, client_state, consensus_state))
}

// #[cfg(feature = "union-aptos")]
pub fn verify_membership(
path: &[u8],
state_root: H256,
table_handle: AccountAddress,
proof: StorageProof,
value: &[u8],
) -> Result<(), Error> {
let Some(proof_value) = &proof.state_value else {
return Err(Error::MembershipProofWithoutValue);
};

// `aptos_std::table` stores the value as bcs encoded
let given_value = bcs::to_bytes(&value).expect("cannot fail");
if proof_value.data() != given_value {
return Err(Error::ProofValueMismatch(
proof_value.data().to_vec(),
given_value,
));
}

let Some(proof_leaf) = proof.proof.leaf.as_ref() else {
return Err(Error::MembershipProofWithoutValue);
};

if aptos_verifier::hash_state_value(proof_value) != *proof_leaf.value_hash.get() {
return Err(Error::ProofValueHashMismatch);
}

let key =
aptos_verifier::hash_table_key(&bcs::to_bytes(path).expect("cannot fail"), &table_handle);

if key != *proof_leaf.key.get() {
return Err(Error::ProofKeyMismatch);
}

Ok(aptos_verifier::verify_membership(
proof.proof,
state_root.into(),
)?)
}

#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename = "StateValue")]
enum PersistedStateValue {
V0(Vec<u8>),
WithMetadata {
data: Vec<u8>,
metadata: PersistedStateValueMetadata,
},
}

#[derive(serde::Deserialize, serde::Serialize)]
#[serde(rename = "StateValueMetadata")]
pub enum PersistedStateValueMetadata {
V0 {
deposit: u64,
creation_time_usecs: u64,
},
V1 {
slot_deposit: u64,
bytes_deposit: u64,
creation_time_usecs: u64,
},
}

#[cfg(test)]
mod tests {
use hex_literal::hex;
use unionlabs::{
encoding::{DecodeAs, Proto},
ibc::core::channel::channel::Channel,
};

#[test]
fn test_proto() {
let channel_end = hex!(
"6d080110011a470a457761736d2e756e696f6e3134686a32746176713866706573647778786375343472747933686839307668756a7276636d73746c347a723374786d6676773973336539666532220c636f6e6e656374696f6e2d302a1075637330302d70696e67706f6e672d31"
);
println!(
"end 1: {:?}",
Channel::decode_as::<Proto>(&channel_end).unwrap()
);
}
}
33 changes: 33 additions & 0 deletions cosmwasm/ibc-union/lightclient/aptos/src/contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use cosmwasm_std::{entry_point, Binary, Deps, DepsMut, Env, Response, StdResult};
use ibc_union_light_client::{
msg::{InitMsg, QueryMsg},
IbcClientError,
};
use unionlabs_cosmwasm_upgradable::UpgradeMsg;

use crate::client::AptosLightClient;

#[entry_point]
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
ibc_union_light_client::query::<AptosLightClient>(deps, env, msg).map_err(Into::into)
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct MigrateMsg {}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn migrate(
deps: DepsMut,
_env: Env,
msg: UpgradeMsg<InitMsg, MigrateMsg>,
) -> Result<Response, IbcClientError<AptosLightClient>> {
msg.run(
deps,
|deps, init_msg| {
let res = ibc_union_light_client::init(deps, init_msg)?;

Ok((res, None))
},
|_deps, _migrate_msg, _current_version| Ok((Response::default(), None)),
)
}
41 changes: 41 additions & 0 deletions cosmwasm/ibc-union/lightclient/aptos/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use cosmwasm_std::StdError;
use ibc_union_light_client::IbcClientError;
use unionlabs::ibc::core::client::height::Height;

use crate::client::AptosLightClient;

#[derive(thiserror::Error, Debug, PartialEq)]
pub enum Error {
#[error("header verification failure ({0})")]
HeaderVerification(#[from] aptos_verifier::Error),
#[error("invalid state_proof storage proof")]
InvalidStateProof,
#[error("empty ibc path")]
EmptyIbcPath,
#[error("consensus state not found ({0})")]
ConsensusStateNotFound(Height),
#[error("membership proof with no value")]
MembershipProofWithoutValue,
#[error("proof value {proof_value} doesn't match the given value {given})", proof_value = serde_utils::to_hex(.0), given = serde_utils::to_hex(.1))]
ProofValueMismatch(Vec<u8>, Vec<u8>),
#[error("proof value hash doesn't match the calculated one")]
ProofValueHashMismatch,
#[error("proof key hash doesn't match the calculated one")]
ProofKeyMismatch,
#[error("invalid ibc path {0}")]
InvalidIbcPath(String),
#[error(transparent)]
StdError(#[from] StdError),
}

impl From<Error> for StdError {
fn from(value: Error) -> Self {
StdError::generic_err(value.to_string())
}
}

impl From<Error> for IbcClientError<AptosLightClient> {
fn from(value: Error) -> Self {
IbcClientError::ClientSpecific(value)
}
}
93 changes: 93 additions & 0 deletions cosmwasm/ibc-union/lightclient/aptos/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
pub mod client;
#[cfg(any(test, not(feature = "library")))]
pub mod contract;
pub mod error;

// use aptos_types::{proof::TransactionInfoWithProof, state_proof::StateProof};
// use aptos_crypto::{hash::CryptoHash, HashValue};
// use sparse_merkle_proof::*;

#[test]
fn merkle_proof() {
// use hash_value::HashValue;
// use hex_literal::hex;
// use sparse_merkle_proof::SparseMerkleProof;
// use state_proof::StateProof;
// use transaction_proof::TransactionInfoWithProof;
// // let internal = SparseMerkleInternalNode::new(HashValue::zero(), HashValue::zero()).hash();
// // let mine = sparse_merkle_proof::SparseMerkleInternalNode::new(
// // sparse_merkle_proof::HashValue::zero(),
// // sparse_merkle_proof::HashValue::zero(),
// // )
// // .hash();

// // let internal = SparseMerkleLeafNode::new(HashValue::zero(), HashValue::zero()).hash();
// // let mine = sparse_merkle_proof::SparseMerkleLeafNode::new(
// // sparse_merkle_proof::HashValue::zero(),
// // sparse_merkle_proof::HashValue::zero(),
// // )
// // .hash();

// // println!("internal == mine: {}", internal.as_ref() == mine.as_ref());

// let proof: SparseMerkleProof = serde_json::from_str(
// r#"{"leaf":{"key":"f2d067d8ef7e97deb231d46f40f9f30200e6f1dad495d33e2a7911825a97ad14","value_hash":"40414333f8109f8cb971c67c9eca3c0049e21e6c5e28551f1a4975c96ab15212"},"siblings":["fafdceaec25fd64517ce3745992467dfac306a5ce59e63255da5b9f58d1417ea","4480c449082954642653a4570c7cb2ea2114d79b61621b94f095f25d640b6e27","0fc055434d70262945d428a5eda3d8396aa960c65ee8c4e79bd20638a95e7a31","731e28eb6655e01b8714aa72f76a0f468c330b46eb9c21816a88e56840896f24","b120265e60289e6e44216efd4f3fba86a8de645d3eb7912ff09812024c639b2f","d90e0a63c7c3cf7ed000841a85f981d8c6bec4c23353822204c7c9e9c5dee4db","30b21a8a3bf202b5fe18e415c299fd3b9985462a6292fffdabd5b32fbc27ba30","5350415253455f4d45524b4c455f504c414345484f4c4445525f484153480000","5b9096922002407577b4e46e6466aadb15cbb9521fd0e9847d474398ea3736e2","5350415253455f4d45524b4c455f504c414345484f4c4445525f484153480000","884f8b72a832aa718c6590d0bfeb1ec85b546611b6c07f8df563827974ad8134"]}"#,
// ).unwrap();

// let txs_with_proof: TransactionInfoWithProof = serde_json::from_str(
// r#"{"ledger_info_to_transaction_info_proof":{"siblings":["38ebb945a351a6701658fe7f5398133ad62777754e3ea44834da0d1e75a87e10","cdb6d7b047d18fdf13f27f13cf1077bc03b1e93b283feeecec55e31176a1f328","835449fd22e856b1f0fdb76d1ff3e493b7c0f8f43b9f66690b4d4d90a0c424ac","af625d6b7281a633d6bdf5cc144186e1ff094e9953165fd1516ab16544776631","d00d20a6fb6874e4c36e5690a4069b94a41f1ae197ae8769d2207f668f016c1c","3c929e62e334cb0ca8dbfd955899aa2bb09e6cc2ce053261689bb69d31c133f4","819a3f1ed1827d33e60b91da7f44736b4c583faec52b8867f45a97c1803b2b66","ea3756c694f6ed5782c91640e5e821604fa39cc55ff85691949d5c93f5c9fb95"],"phantom":null},"transaction_info":{"V0":{"gas_used":0,"status":"Success","transaction_hash":"e77d9016e431a2d367c513ebaf1bc39e291dc9589728e9bf1495fc573cb085ca","event_root_hash":"414343554d554c41544f525f504c414345484f4c4445525f4841534800000000","state_change_hash":"afb6e14fe47d850fd0a7395bcfb997ffacf4715e0f895cc162c218e4a7564bc6","state_checkpoint_hash":"02388da3aee85236d64e272fec0b1a6fcd4962986327971faef9ee2951a4ad6a","state_cemetery_hash":null}}}"#,
// ).unwrap();

// let state_proof: StateProof = serde_json::from_str(
// r#"{"latest_li_w_sigs":{"V0":{"ledger_info":{"commit_info":{"epoch":1,"round":0,"id":"0bb023a37e8dbe213f277bab59579e8a75ff6c646d6a5e5384789526128043b3","executed_state_id":"b4f2928670ff96185bc02ed57168c18443b6735524b580114aff5dc262f6ff3c","version":181,"timestamp_usecs":1723755828375426,"next_epoch_state":{"epoch":1,"verifier":{"validator_infos":[{"address":"d1126ce48bd65fb72190dbd9a6eaa65ba973f1e1664ac0cfba4db1d071fd0c36","public_key":"0x86fb211f41a07c6399ccc6ab3a8fe568fb0f574ce1b811896c44c6da4f267d543c6cac9fb8f4e9b92a3b809eefb91cbd","voting_power":100000000}]}}},"consensus_data_hash":"0000000000000000000000000000000000000000000000000000000000000000"},"signatures":{"validator_bitmask":{"inner":[]},"sig":null}}},"epoch_changes":{"ledger_info_with_sigs":[],"more":false}}"#,
// ).unwrap();

// println!("{}", hex::encode(state_proof.hash()));

// println!("{}", txs_with_proof.transaction_info.hash());

// for i in 0..20 {
// let res = txs_with_proof.ledger_info_to_transaction_info_proof.verify(
// state_proof
// .latest_ledger_info()
// .commit_info
// .executed_state_id,
// txs_with_proof.transaction_info.hash(),
// i,
// );

// println!("Res: {res:?}");
// }

// let key = aptos_types::state_store::state_key::StateKey::table_item(
// &TableHandle(AccountAddress::new(hex!(
// "be769b7536776eb353a61aa4d26de32ee16844d89d8cc2ede29732e3d19407ea"
// ))),
// &vec![04, 0x41, 0x42, 0x43, 0x44],
// );

// println!("{}", key.hash());

// proof.verify_by_hash(
// HashValue(hex!(
// "02388da3aee85236d64e272fec0b1a6fcd4962986327971faef9ee2951a4ad6a"
// )),
// HashValue(hex!(
// "f2d067d8ef7e97deb231d46f40f9f30200e6f1dad495d33e2a7911825a97ad14"
// )),
// Some(HashValue(hex!(
// "40414333f8109f8cb971c67c9eca3c0049e21e6c5e28551f1a4975c96ab15212"
// ))),
// );

// // Membership verification
// // 1. SparseMerkleProof is verified against the tx `state_checkpoint_hash`.
// // 2. Tx data is verifier against `state_proof.latest_ledger_info().commit_info.executed_state_id` (which is saved at `update_state`)

// // Header verification
// // 1. Check if the l1 has the state root at that height.
// // 2. Verify `state_proof` hash against the `l1.state_root`
// // 3. Save `state_root.latest_ledger_info().commit_info.executed_state_id` for membership verification. (this is the merkle root of txs)

// println!("Res: {res:?}");
}
26 changes: 26 additions & 0 deletions lib/aptos-light-client-types/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[package]
name = "aptos-light-client-types"
version = "0.0.0"

authors = { workspace = true }
edition = { workspace = true }
license-file = { workspace = true }
publish = { workspace = true }
repository = { workspace = true }

[lints]
workspace = true

[dependencies]
alloy = { workspace = true, features = ["sol-types"], optional = true }
bincode = { workspace = true, features = ["alloc", "derive"], optional = true }
serde = { workspace = true, optional = true, features = ["derive"] }
unionlabs = { workspace = true }

[features]
bincode = ["dep:bincode", "unionlabs/bincode"]
ethabi = ["unionlabs/ethabi", "dep:alloy"]
serde = ["dep:serde"]

[dev-dependencies]
hex-literal = { workspace = true }
12 changes: 12 additions & 0 deletions lib/aptos-light-client-types/src/client_state.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use unionlabs::{aptos::account::AccountAddress, ibc::core::client::height::Height};

#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))]
pub struct ClientState {
pub chain_id: String,
pub ibc_contract_address: AccountAddress,
pub table_handle: AccountAddress,
pub frozen_height: Height,
pub latest_block_num: u64,
}
43 changes: 43 additions & 0 deletions lib/aptos-light-client-types/src/consensus_state.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use unionlabs::primitives::H256;

#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ConsensusState {
pub state_root: H256,
pub timestamp: u64,
}

#[cfg(feature = "ethabi")]
pub mod ethabi {
use alloy::sol_types::SolValue;
use unionlabs::impl_ethabi_via_try_from_into;

use super::*;

impl_ethabi_via_try_from_into!(ConsensusState => SolConsensusState);

alloy::sol! {
struct SolConsensusState {
bytes32 state_root;
uint64 timestamp;
}
}

impl From<ConsensusState> for SolConsensusState {
fn from(value: ConsensusState) -> Self {
Self {
state_root: value.state_root.get().into(),
timestamp: value.timestamp,
}
}
}

impl From<SolConsensusState> for ConsensusState {
fn from(value: SolConsensusState) -> Self {
Self {
state_root: H256::new(value.state_root.0),
timestamp: value.timestamp,
}
}
}
}
15 changes: 15 additions & 0 deletions lib/aptos-light-client-types/src/header.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use unionlabs::{
aptos::{state_proof::StateProof, transaction_proof::TransactionInfoWithProof},
ibc::core::client::height::Height,
};

#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))]
pub struct Header {
pub trusted_height: Height,
pub state_proof: StateProof,
pub tx_index: u64,
pub tx_proof: TransactionInfoWithProof,
pub new_height: u64,
}
5 changes: 5 additions & 0 deletions lib/aptos-light-client-types/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pub mod client_state;
pub mod consensus_state;
pub mod header;

pub use crate::{client_state::ClientState, consensus_state::ConsensusState, header::Header};
4 changes: 4 additions & 0 deletions lib/aptos-verifier/Cargo.toml
Original file line number Diff line number Diff line change
@@ -17,3 +17,7 @@ hex-literal = { workspace = true }
sha3 = { workspace = true }
thiserror = { workspace = true }
unionlabs = { workspace = true }

[dev-dependencies]
cosmwasm-crypto = { version = "2.1" }
serde = { workspace = true }
2 changes: 2 additions & 0 deletions lib/aptos-verifier/src/error.rs
Original file line number Diff line number Diff line change
@@ -10,6 +10,8 @@ pub enum Error {
MaxSiblingsExceeded(usize),
#[error("storage verification error")]
StorageVerification(#[from] StorageVerificationError),
#[error("the epoch order must be incrementing sequentially and one by one")]
InvalidEpochOrder,
}

#[derive(Debug, Clone, PartialEq, thiserror::Error)]
209 changes: 204 additions & 5 deletions lib/aptos-verifier/src/lib.rs
Original file line number Diff line number Diff line change
@@ -11,20 +11,104 @@ use sha3::{Digest, Sha3_256};
use unionlabs::{
aptos::{
account::AccountAddress,
ledger_info::{LedgerInfo, LedgerInfoWithSignatures, LedgerInfoWithV0},
sparse_merkle_proof::{SparseMerkleLeafNode, SparseMerkleProof},
state_proof::StateProof,
storage_proof::StateValue,
transaction_info::TransactionInfo,
transaction_proof::TransactionInfoWithProof,
validator_verifier::ValidatorVerifier,
},
primitives::H256,
BytesBitIterator,
primitives::{encoding::HexPrefixed, H256, H384, H768},
BytesBitIteratorBE,
};

pub(crate) const MAX_ACCUMULATOR_PROOF_DEPTH: usize = 63;
// "SPARSE_MERKLE_PLACEHOLDER_HASH"
pub(crate) const SPARSE_MERKLE_PLACEHOLDER_HASH: [u8; 32] =
hex!("00005350415253455F4D45524B4C455F504C414345484F4C4445525F48415348");

pub trait BlsVerify {
fn verify_signature<'pk>(
&self,
public_keys: impl IntoIterator<Item = &'pk H384>,
msg: &[u8],
signature: H768,
) -> Result<(), Error>;
}

/// Given a `trusted_state`, verify a state transition using `state_proof`
///
/// * `current_validator_verifier`: The validator verifier for the current(trusted) epoch.
/// * `trusted_state`: Currently trusted `LedgerInfo`. Note that if there's any epoch change, it **MUST** start
/// from the current epoch + 1.
/// * `state_proof`: Proof of state transition. Note that the function expects epoch changes to be in an ascending
/// order respective to the epoch number.
/// * `bls_verifier`: BLS verifier
pub fn verify_state_proof<V: BlsVerify>(
current_validator_verifier: &ValidatorVerifier,
trusted_state: &LedgerInfo,
state_proof: &StateProof,
bls_verifier: &V,
) -> Result<(), Error> {
let mut current_epoch = trusted_state.commit_info.epoch;
// TODO(aeryz): does this info exist on every block info?
let mut current_next_verifier = trusted_state.commit_info.next_epoch_state.as_ref().unwrap();

for li in &state_proof.epoch_changes.ledger_info_with_sigs {
let LedgerInfoWithSignatures::V0(li) = li;
if current_epoch + 1 != li.ledger_info.commit_info.epoch {
println!("{} {}", current_epoch, li.ledger_info.commit_info.epoch);
return Err(Error::InvalidEpochOrder);
}

verify_ledger_info(&current_next_verifier.verifier, li, bls_verifier)?;

current_epoch += 1;
current_next_verifier = li
.ledger_info
.commit_info
.next_epoch_state
.as_ref()
.unwrap();
}

let LedgerInfoWithSignatures::V0(li) = &state_proof.latest_li_w_sigs;

if li.ledger_info.commit_info.epoch == current_epoch {
verify_ledger_info(current_validator_verifier, li, bls_verifier)
} else {
verify_ledger_info(&current_next_verifier.verifier, li, bls_verifier)
}
}

pub fn verify_ledger_info<V: BlsVerify>(
validator_verifier: &ValidatorVerifier,
ledger_info: &LedgerInfoWithV0,
bls_verifier: &V,
) -> Result<(), Error> {
// Self::check_num_of_voters(self.len() as u16, multi_signature.get_signers_bitvec())?;
let (pub_keys, _) = BytesBitIteratorBE::new(&ledger_info.signatures.validator_bitmask.inner)
.enumerate()
.filter(|(_, is_true)| *is_true)
.map(|(i, _)| {
let validator = validator_verifier.validator_infos.get(i).unwrap();
(
H384::<HexPrefixed>::new(
validator.public_key.pubkey.as_slice().try_into().unwrap(),
),
validator.address,
)
})
.collect::<(Vec<_>, Vec<_>)>();

// self.check_voting_power(authors.iter(), true)?;
bls_verifier.verify_signature(
&pub_keys,
&hash_ledger_info(&ledger_info.ledger_info),
ledger_info.signatures.sig.unwrap(),
)
}
/// Verifies an element whose hash is `element_hash` and version is `element_version` exists in
/// the accumulator whose root hash is `expected_root_hash` using the provided proof.
pub fn verify_tx_state(
@@ -130,7 +214,7 @@ pub fn verify_existence_proof(
.iter()
.rev()
.zip(
BytesBitIterator::new(&element_key)
BytesBitIteratorBE::new(&element_key)
.rev()
.skip(256 - proof.siblings.len()),
)
@@ -174,16 +258,26 @@ pub fn hash_table_key(key: &[u8], table_handle: &AccountAddress) -> [u8; 32] {
.into()
}

fn hash_ledger_info(ledger_info: &LedgerInfo) -> Vec<u8> {
let mut buf = Sha3_256::new()
.chain_update("APTOS::LedgerInfo")
.finalize()
.to_vec();
bcs::serialize_into(&mut buf, ledger_info).expect("expected to be able to serialize");
buf
}

fn hash_tx_info(tx_info: &TransactionInfo) -> [u8; 32] {
let mut state = Sha3_256::new();
state.update(
Sha3_256::new()
.chain_update("APTOS::TransactionInfo")
.finalize(),
);
bcs::serialize_into(&mut state, tx_info).expect("expected to be able to serialize");
let mut buf = vec![];
bcs::serialize_into(&mut buf, tx_info).expect("expected to be able to serialize");

state.finalize().into()
state.chain_update(buf).finalize().into()
}

fn hash_sparse_merkle_leaf_node(leaf: &SparseMerkleLeafNode) -> [u8; 32] {
@@ -235,3 +329,108 @@ impl SparseMerkleInternalNode {
state.finalize().into()
}
}

#[cfg(test)]
mod tests {
use cosmwasm_crypto::HashFunction;
use unionlabs::{
aptos::state_proof::StateProof,
encoding::{DecodeAs, Json},
};

use super::*;

struct BlsVerifier;

pub const BLS12_381_G1_GENERATOR: [u8; 48] = [
151, 241, 211, 167, 49, 151, 215, 148, 38, 149, 99, 140, 79, 169, 172, 15, 195, 104, 140,
79, 151, 116, 185, 5, 161, 78, 58, 63, 23, 27, 172, 88, 108, 85, 232, 63, 249, 122, 26,
239, 251, 58, 240, 10, 219, 34, 198, 187,
];

impl BlsVerify for BlsVerifier {
fn verify_signature<'pk>(
&self,
public_keys: impl IntoIterator<Item = &'pk H384>,
msg: &[u8],
signature: H768,
) -> Result<(), Error> {
let pubkeys = public_keys
.into_iter()
.flat_map(|x| *x)
.collect::<Vec<u8>>();

let pubkey = cosmwasm_crypto::bls12_381_aggregate_g1(&pubkeys).unwrap();

pub const DST_POP_G2: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_";
let hashed_msg =
cosmwasm_crypto::bls12_381_hash_to_g2(HashFunction::Sha256, msg, DST_POP_G2);

let valid = cosmwasm_crypto::bls12_381_pairing_equality(
&BLS12_381_G1_GENERATOR,
signature.as_ref(),
&pubkey,
&hashed_msg,
)
.unwrap();

if valid {
Ok(())
} else {
panic!("invalid signature");
}
}
}

#[derive(serde::Deserialize)]
struct StateProofResponse {
state_proof: StateProof,
}

#[test]
fn test_aptos_full_verification() {
let state_proof_response = r#"{"state_proof":{"latest_li_w_sigs":{"V0":{"ledger_info":{"commit_info":{"epoch":119,"round":16,"id":"922f07af4849dec266bda670b62925b11b5565f9bfc7179efbe9da7a53058724","executed_state_id":"b6b1c56ae0c373ccd07329e5e75ac39b81253407f37bf8a7914c1bac5be08d87","version":13266,"timestamp_usecs":1740872921079217,"next_epoch_state":null},"consensus_data_hash":"0000000000000000000000000000000000000000000000000000000000000000"},"signatures":{"validator_bitmask":{"inner":[128]},"sig":"0xaff12e7c24499a4b8122451f1eff007fd99e1412aa3717c4e9e7fb3bebc6e5191e961c69840851f51cc3707499ed1ebf049ee47d3cbc5fe1ac9f58ee124d26b3039c1bf3fa360c9cccd590f20afbb5f058c23631cd39689543f71d3998344681"}}},"epoch_changes":{"ledger_info_with_sigs":[{"V0":{"ledger_info":{"commit_info":{"epoch":116,"round":58,"id":"312707bf0195fcc81771e572e275def4ce8fa37c234d2c4a378a260606c7afe2","executed_state_id":"0741c0bafc105474169e9bf4e0be1f23a4f846cb03c20984ca843d2720b3f7bf","version":13007,"timestamp_usecs":1740871950517277,"next_epoch_state":{"epoch":117,"verifier":{"validator_infos":[{"address":"d2982978cec002dfb4b3b7921de20b828e82e20ad5600d8989cd58150bb2a15c","public_key":"0x8f31062af1a3eddcaebd429a413c89b4dd7059c00a5355311a88ac9a0691868a40c236397456433e7a98a5f32ef3c43b","voting_power":10}]}}},"consensus_data_hash":"0000000000000000000000000000000000000000000000000000000000000000"},"signatures":{"validator_bitmask":{"inner":[128]},"sig":"0xb90b90e62ef1d13e2315346a11914485cb9ef57607541c2cb51ca96b5700f73764752c36b40fa138cde1063c2e7580ff17dd5dee04939c5f9cbac82b57bb23a97ca69bbc222b1a51bd24fb05318724d8f73400092381a656cbd6c2ac3853f87b"}}},{"V0":{"ledger_info":{"commit_info":{"epoch":117,"round":59,"id":"edb6d1cbb9e05e64a5e61453f941a8746ac51ea0b37277c84da791eb5772903c","executed_state_id":"6e22c941807bd825e192f7d32845b2e885ca446bfddecbec9d760fb5668c830b","version":13125,"timestamp_usecs":1740872013560533,"next_epoch_state":{"epoch":118,"verifier":{"validator_infos":[{"address":"d2982978cec002dfb4b3b7921de20b828e82e20ad5600d8989cd58150bb2a15c","public_key":"0x8f31062af1a3eddcaebd429a413c89b4dd7059c00a5355311a88ac9a0691868a40c236397456433e7a98a5f32ef3c43b","voting_power":10}]}}},"consensus_data_hash":"0000000000000000000000000000000000000000000000000000000000000000"},"signatures":{"validator_bitmask":{"inner":[128]},"sig":"0xae4180f98d41ba4adccc25f1f28d598e9909199fdc61a95f85796f175612a1051138ee3005b378982018582a8b799dc003021c03a72eb3f237d88de1cdf72e2afe95aa3b36cdeb48b197c8a310bf2fb2c60e381fce4a98cbd32571dc27bc9622"}}},{"V0":{"ledger_info":{"commit_info":{"epoch":118,"round":53,"id":"028253e6c5d782817d86019ef756965a257df7e5d1810ca975c5228ba228f0b3","executed_state_id":"444b8fc3c16d5ade2ee42be83d89c075716a2d34c0065432d45b4db3db6aa1d3","version":13233,"timestamp_usecs":1740872906587940,"next_epoch_state":{"epoch":119,"verifier":{"validator_infos":[{"address":"d2982978cec002dfb4b3b7921de20b828e82e20ad5600d8989cd58150bb2a15c","public_key":"0x8f31062af1a3eddcaebd429a413c89b4dd7059c00a5355311a88ac9a0691868a40c236397456433e7a98a5f32ef3c43b","voting_power":10}]}}},"consensus_data_hash":"0000000000000000000000000000000000000000000000000000000000000000"},"signatures":{"validator_bitmask":{"inner":[128]},"sig":"0xa7c4d1ee4a05f51848c314cd6a13686cea1e3affb0bfeb7498a1d57ec671228f1bf6807efd52d42a74af6133978c562013354227ea3db6bbbb20f79e940307b144d2cc08b7281685a9ac2e890aa8f497cf514ef13ebc9e3998eac3e87ed7e8d5"}}}],"more":false}},"tx_proof":{"ledger_info_to_transaction_info_proof":{"siblings":["a4140f7e236a14395e65d95295b9535d407aff135bf963ef0ed3ed7337aa72af","47b822eb4b68fef10ce704b310935444dcc89864aeecbb7ef8071f0b0efe9f8d","3a96143203411f9d28c8307fc5cab7356d6ba46334fdc11f4808b12941966bf7","5556b583f5ab11a8d436d50319e44e3d9f625bbb22381a4936503cfd2720d552","cce2679849ffe8474343d45f41f4528d19f941a0fa136175a1e0b2d1a8aa3a5e","a78d9e3fd2325fe5a7f6d4f2635ec5627b8b301539b2942eeb81073a64375e95","8c5e8355ca3f4a9024f0a780bcfa2c1c21099ebf87a7ab6d4290ab4ea8c0cdeb","496ac967737b55026d557a6ccd4a124719285a6c6f6fd0cd24b7ff07e90090a1","8d5c5c1b03cdd95d8cb3f19e329030ba3d8f1f25fb4937c914016074383ed6f5","87797c1f2cf51bac9b422c01e7a08e39e303f734004f618ce9c7433f31679556","a6c04e9e97a5a74e2dad96d2aa3a253f32e91d51860da9b879a6471b5f3e9409","d80171fe08006e630363ac73b42961cfa95d89e4f0d0e39f1a941f5b8595a87b","d182842f579a375d0dce4859fb85f60480fd8dfbc495305caf6d0c66d3e4ab72","9df97297843ca0b9ffccf3ff3a09d08aea2fd18f21a5cc26ad86a0b4ca4493ba"]},"transaction_info":{"V0":{"gas_used":0,"status":"Success","transaction_hash":"bd6bf5bbfea15b145526fc3f050d1723533da31a6163cc9f5da410a2eb0e58c0","event_root_hash":"414343554d554c41544f525f504c414345484f4c4445525f4841534800000000","state_change_hash":"afb6e14fe47d850fd0a7395bcfb997ffacf4715e0f895cc162c218e4a7564bc6","state_checkpoint_hash":"5985147d09cefc857cbf837010c4465c4f0940440270a1ef36c3abda36bf23af","state_cemetery_hash":null}}},"tx_index":7117}"#;
let mut state_proof_response =
StateProofResponse::decode_as::<Json>(state_proof_response.as_bytes()).unwrap();

let initial_state = state_proof_response
.state_proof
.epoch_changes
.ledger_info_with_sigs
.iter()
.skip(1)
.next()
.cloned()
.unwrap();

let current_validator_verifier = state_proof_response
.state_proof
.epoch_changes
.ledger_info_with_sigs
.first()
.unwrap()
.ledger_info()
.commit_info
.next_epoch_state
.as_ref()
.unwrap()
.clone();

state_proof_response
.state_proof
.epoch_changes
.ledger_info_with_sigs = state_proof_response
.state_proof
.epoch_changes
.ledger_info_with_sigs[2..]
.to_vec();

verify_state_proof(
&current_validator_verifier.verifier,
initial_state.ledger_info(),
&state_proof_response.state_proof,
&BlsVerifier,
)
.unwrap();
}
}
6 changes: 3 additions & 3 deletions lib/ethereum-sync-protocol/src/lib.rs
Original file line number Diff line number Diff line change
@@ -19,7 +19,7 @@ use typenum::Unsigned;
use unionlabs::{
ensure,
primitives::{H256, H384, H768},
BytesBitIterator,
BytesBitIteratorLE,
};

use crate::{
@@ -73,7 +73,7 @@ pub fn validate_light_client_update<C: ChainSpec, V: BlsVerify>(
) -> Result<(), Error> {
// verify that the sync committee has sufficient participants
let sync_aggregate = &update.sync_aggregate;
let set_bits = BytesBitIterator::new(&sync_aggregate.sync_committee_bits)
let set_bits = BytesBitIteratorLE::new(&sync_aggregate.sync_committee_bits)
.filter(|included| *included)
.count();
ensure(
@@ -209,7 +209,7 @@ pub fn validate_light_client_update<C: ChainSpec, V: BlsVerify>(

// It's not mandatory for all of the members of the sync committee to participate. So we are extracting the
// public keys of the ones who participated.
let participant_pubkeys = BytesBitIterator::new(&sync_aggregate.sync_committee_bits)
let participant_pubkeys = BytesBitIteratorLE::new(&sync_aggregate.sync_committee_bits)
.zip(sync_committee.pubkeys.iter())
.filter_map(|(included, pubkey)| if included { Some(pubkey) } else { None })
.collect::<Vec<_>>();
14 changes: 14 additions & 0 deletions lib/unionlabs/src/aptos/ledger_info.rs
Original file line number Diff line number Diff line change
@@ -14,6 +14,20 @@ pub enum LedgerInfoWithSignatures {
V0(LedgerInfoWithV0),
}

impl LedgerInfoWithSignatures {
#[must_use]
pub fn ledger_info(&self) -> &LedgerInfo {
let Self::V0(ledger_info) = self;
&ledger_info.ledger_info
}

#[must_use]
pub fn signatures(&self) -> &AggregateSignature {
let Self::V0(ledger_info) = self;
&ledger_info.signatures
}
}

/// The validator node returns this structure which includes signatures
/// from validators that confirm the state. The client needs to only pass back
/// the `LedgerInfo` element since the validator node doesn't need to know the signatures
37 changes: 32 additions & 5 deletions lib/unionlabs/src/lib.rs
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@ extern crate alloc;
use core::{
fmt::{self, Debug, Display},
iter,
marker::PhantomData,
ptr::addr_of,
str::FromStr,
};
@@ -260,17 +261,43 @@ impl<T: core::error::Error> Display for ErrorReporter<T> {
}
}

pub trait BitIndex {
/// Get the 'i'th bit
fn index_of(i: usize) -> usize;
}

pub struct LittleEndianBitIndex;

impl BitIndex for LittleEndianBitIndex {
fn index_of(i: usize) -> usize {
i % 8
}
}

pub struct BigEndianBitIndex;

impl BitIndex for BigEndianBitIndex {
fn index_of(i: usize) -> usize {
7 - i % 8
}
}

pub type BytesBitIteratorLE<'a> = BytesBitIterator<'a, LittleEndianBitIndex>;
pub type BytesBitIteratorBE<'a> = BytesBitIterator<'a, BigEndianBitIndex>;

#[must_use = "constructing an iterator has no effect"]
pub struct BytesBitIterator<'a> {
pub struct BytesBitIterator<'a, E: BitIndex> {
bz: &'a [u8],
pos: core::ops::Range<usize>,
_marker: PhantomData<E>,
}

impl<'a> BytesBitIterator<'a> {
impl<'a, E: BitIndex> BytesBitIterator<'a, E> {
pub fn new(bz: &'a impl AsRef<[u8]>) -> Self {
BytesBitIterator {
bz: bz.as_ref(),
pos: (0..bz.as_ref().len() * 8),
_marker: PhantomData,
}
}

@@ -279,12 +306,12 @@ impl<'a> BytesBitIterator<'a> {
// debug_assert_eq!(self.hash_bytes.len(), Hash::LENGTH); // invariant
// debug_assert_lt!(index, Hash::LENGTH_IN_BITS); // assumed precondition
let pos = index / 8;
let bit = index % 8;
let bit = E::index_of(index);
(self.bz[pos] >> bit) & 1 != 0
}
}

impl core::iter::Iterator for BytesBitIterator<'_> {
impl<E: BitIndex> core::iter::Iterator for BytesBitIterator<'_, E> {
type Item = bool;

fn next(&mut self) -> Option<Self::Item> {
@@ -296,7 +323,7 @@ impl core::iter::Iterator for BytesBitIterator<'_> {
}
}

impl core::iter::DoubleEndedIterator for BytesBitIterator<'_> {
impl<E: BitIndex> core::iter::DoubleEndedIterator for BytesBitIterator<'_, E> {
fn next_back(&mut self) -> Option<Self::Item> {
self.pos.next_back().map(|x| self.get_bit(x))
}