Skip to content

ics-cf-solana light-client: header verification & client update; protobuf impl #510

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 21 commits into
base: hyperspace-solana
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
4f7e924
Implement header verification & client update
vmarkushin Aug 5, 2024
c1942e9
Fix doc type
vmarkushin Aug 8, 2024
f8b6ab9
Add `PreCheckedShreds` type that provides some guarantees for the rec…
vmarkushin Aug 8, 2024
999e51d
Check for misbehavior when an update from the past received
vmarkushin Aug 8, 2024
e7efb3f
Remove unnecessary checks of Shreds that were already done by `PreChe…
vmarkushin Aug 8, 2024
311cf7d
Remove usage of `PK` generic parameter
vmarkushin Aug 8, 2024
b5d9b0c
Add some todos
vmarkushin Aug 8, 2024
37afb2f
Make shreds verification code look nicer
vmarkushin Aug 8, 2024
b172be3
Fix iterator loop
vmarkushin Aug 8, 2024
65b5bd0
Rewrite Header proto decoding using iterators
vmarkushin Aug 8, 2024
673fd8a
Optimize collection
vmarkushin Aug 8, 2024
eb250e9
Update light-clients/icsxx-cf-solana/src/client.rs
vmarkushin Aug 12, 2024
8787431
Add a TODO
vmarkushin Aug 12, 2024
687e9b3
Feature-gate CfClient in pallet-ibc
vmarkushin Aug 12, 2024
1f97d39
Fix tests compilation and add a test for decoding shreds
vmarkushin Aug 12, 2024
976d8d9
Refactor and optimize `TryFrom<Vec<Shred>>`` for `PreCheckedShreds`
vmarkushin Aug 12, 2024
d3964b5
Add check for all the shreds being data in PreCheckedShreds
vmarkushin Aug 12, 2024
1ceff4e
Extract check for misbehaviour for header method
vmarkushin Aug 12, 2024
f014eb5
Use u64 instead of Duration and Timestamp
vmarkushin Aug 12, 2024
0738a5b
Merge remote-tracking branch 'origin/vmarkushin/cf-solana-client-upda…
vmarkushin Aug 12, 2024
dcaea3c
Merge branch 'refs/heads/hyperspace-solana' into vmarkushin/cf-solana…
vmarkushin Aug 12, 2024
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
52 changes: 17 additions & 35 deletions light-clients/icsxx-cf-solana/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use solana_sdk::{clock::Slot, pubkey::Pubkey};
use std::time::Duration;

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ClientState<PK> {
pub struct ClientState {
/// Highest available guest block height.
pub latest_height: Slot,

Expand All @@ -27,14 +27,12 @@ pub struct ClientState<PK> {

/// Chain's slot duration.
pub slot_duration: Duration,

_ph: core::marker::PhantomData<PK>,
}

// super::wrap!(cf_guest_upstream::ClientState<PK> as ClientState);
// super::wrap!(impl<PK> proto for ClientState);
// super::wrap!(cf_guest_upstream::ClientState as ClientState);
// super::wrap!(impl proto for ClientState);

impl<PK> ClientState<PK> {
impl ClientState {
pub(crate) fn timestamp_for_slot(&self, slot: Slot) -> Timestamp {
Timestamp::from_nanoseconds(
self.genesis_time.nanoseconds() + (slot as u64 * self.slot_duration.as_nanos() as u64),
Expand All @@ -43,7 +41,7 @@ impl<PK> ClientState<PK> {
}
}

impl<PK: guestchain::PubKey> ClientState<PK> {
impl ClientState {
pub fn new(
latest_height: Slot,
trusting_period_ns: u64,
Expand All @@ -59,11 +57,10 @@ impl<PK: guestchain::PubKey> ClientState<PK> {
current_leader: current_validator,
genesis_time,
slot_duration,
_ph: core::marker::PhantomData,
}
}

pub fn update_unchecked(self, header: Header<PK>) -> Self {
pub fn update_unchecked(self, header: Header) -> Self {
Self { latest_height: header.slot(), ..self }
}

Expand Down Expand Up @@ -119,14 +116,10 @@ impl<PK: guestchain::PubKey> ClientState<PK> {
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct UpgradeOptions {}

impl<PK> ibc::core::ics02_client::client_state::ClientState for ClientState<PK>
where
PK: guestchain::PubKey + Send + Sync,
PK::Signature: Send + Sync,
{
impl ibc::core::ics02_client::client_state::ClientState for ClientState {
type UpgradeOptions = UpgradeOptions;

type ClientDef = CfSolanaClient<PK>;
type ClientDef = CfSolanaClient;

fn chain_id(&self) -> ibc::core::ics24_host::identifier::ChainId {
ibc::core::ics24_host::identifier::ChainId::new(String::from("Solana"), 0)
Expand Down Expand Up @@ -167,14 +160,14 @@ where
}
}

impl<PK: guestchain::PubKey> From<ClientState<PK>> for proto::ClientState {
fn from(state: ClientState<PK>) -> Self {
impl From<ClientState> for proto::ClientState {
fn from(state: ClientState) -> Self {
Self::from(&state)
}
}

impl<PK: guestchain::PubKey> From<&ClientState<PK>> for proto::ClientState {
fn from(state: &ClientState<PK>) -> Self {
impl From<&ClientState> for proto::ClientState {
fn from(state: &ClientState) -> Self {
Self {
latest_height: state.latest_height.into(),
trusting_period_ns: state.trusting_period_ns,
Expand All @@ -186,14 +179,14 @@ impl<PK: guestchain::PubKey> From<&ClientState<PK>> for proto::ClientState {
}
}

impl<PK: guestchain::PubKey> TryFrom<proto::ClientState> for ClientState<PK> {
impl TryFrom<proto::ClientState> for ClientState {
type Error = BadMessage;
fn try_from(msg: proto::ClientState) -> Result<Self, Self::Error> {
Self::try_from(&msg)
}
}

impl<PK: guestchain::PubKey> TryFrom<&proto::ClientState> for ClientState<PK> {
impl TryFrom<&proto::ClientState> for ClientState {
type Error = BadMessage;

fn try_from(msg: &proto::ClientState) -> Result<Self, Self::Error> {
Expand All @@ -209,15 +202,13 @@ impl<PK: guestchain::PubKey> TryFrom<&proto::ClientState> for ClientState<PK> {
current_leader,
genesis_time,
slot_duration,
_ph: core::marker::PhantomData,
})
}
}

proto_utils::define_wrapper! {
proto: crate::proto::ClientState,
wrapper: ClientState<PK> where
PK: guestchain::PubKey = guestchain::validators::MockPubKey,
wrapper: ClientState,
}

#[cfg(test)]
Expand All @@ -239,17 +230,8 @@ mod tests {
&ANY_MESSAGE[38..]
}

const GENESIS_HASH: CryptoHash = CryptoHash([
243, 148, 241, 41, 122, 49, 51, 253, 97, 145, 113, 22, 234, 164, 193, 183, 185, 48, 160,
186, 69, 72, 144, 156, 126, 229, 103, 131, 220, 174, 140, 165,
]);
const EPOCH_COMMITMENT: CryptoHash = CryptoHash([
86, 12, 131, 131, 127, 125, 82, 54, 32, 207, 121, 149, 204, 11, 121, 102, 180, 211, 111,
54, 0, 207, 247, 125, 195, 57, 10, 10, 80, 84, 86, 152,
]);

fn check(state: ClientState<MockPubKey>) {
let want = ClientState::<MockPubKey>::new(5.into(), 64000000000000, false);
fn check(state: ClientState) {
let want = ClientState::new(5.into(), 64000000000000, false);
assert_eq!(want, state);
}

Expand Down
136 changes: 64 additions & 72 deletions light-clients/icsxx-cf-solana/src/client_def.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
use core::str::FromStr;

use guestchain::Signature;

use crate::alloc::string::ToString;
use alloc::vec::Vec;
use guestchain::{PubKey, Verifier};
use core::str::FromStr;
use ibc::{
core::{
ics02_client::{
Expand All @@ -18,28 +14,23 @@ use ibc::{
protobuf::Protobuf,
};
use prost::Message;
use std::num::NonZeroU64;

use crate::{error::Error, ClientMessage, ClientState, ConsensusState as ClientConsensusState};

type Result<T = (), E = ibc::core::ics02_client::error::Error> = ::core::result::Result<T, E>;

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CfSolanaClient<PK>(core::marker::PhantomData<PK>);
pub struct CfSolanaClient;

impl<PK: PubKey> Default for CfSolanaClient<PK> {
impl Default for CfSolanaClient {
fn default() -> Self {
Self(core::marker::PhantomData)
Self
}
}

impl<PK> ClientDef for CfSolanaClient<PK>
where
PK: PubKey + Send + Sync,
PK::Signature: Send + Sync,
{
type ClientMessage = ClientMessage<PK>;
type ClientState = ClientState<PK>;
impl ClientDef for CfSolanaClient {
type ClientMessage = ClientMessage;
type ClientState = ClientState;
type ConsensusState = ClientConsensusState;

fn verify_client_message<Ctx: ReaderContext>(
Expand All @@ -53,38 +44,23 @@ where
ClientMessage::Header(header) => {
// The client can't be updated if no shreds were received
let shreds = header.shreds;
if shreds.is_empty() {
return Err(Ics02ClientError::implementation_specific(
"no shreds received".to_string(),
));
}
let slot = shreds.slot();

let slot = shreds.first().unwrap().slot();

// All shreds' slots should be the same
shreds.iter().skip(1).all(|s| s.slot() == slot).then(|| ()).ok_or_else(|| {
Ics02ClientError::implementation_specific(
"shreds have different slots".to_string(),
)
})?;

if slot <= client_state.latest_height().revision_height {
return Err(Ics02ClientError::implementation_specific(
"slot is not greater than latest height".to_string(),
));
}
// TODO: verify that the header is within trusting period

let leader = client_state.leader_for_slot(slot);

// Verify all shreds
shreds
.iter()
.map(|shred| shred.sanitize().and_then(|_| shred.verify_with_root(&leader)))
.collect::<Result<Vec<_>, _>>()
.map_err(|e| {
.try_for_each(|shred| {
shred.sanitize()?;
let _root = shred.verify_with_root(&leader)?;
Ok(())
})
.map_err(|err: Error| {
Ics02ClientError::implementation_specific(alloc::format!(
"shred verification failed: {}",
e
"shred verification failed: {err}",
))
})?;
Ok(())
Expand All @@ -107,13 +83,8 @@ where
ClientMessage::Header(header) => header,
_ => unreachable!("02-client will check for Header before calling update_state; qed"),
};
let hash = header.hash();
let slot = header.slot();
let timestamp = client_state.timestamp_for_slot(slot);
let nanos = NonZeroU64::try_from(timestamp.nanoseconds()).map_err(|e| {
Ics02ClientError::implementation_specific(alloc::format!("invalid timestamp: {}", e))
})?;
let header_consensus_state = ClientConsensusState::new(&hash, nanos);
let header_consensus_state =
ClientConsensusState::from_header_and_client_state(&header, &client_state)?;
let cs = Ctx::AnyConsensusState::wrap(&header_consensus_state).ok_or_else(|| {
Error::UnknownConsensusStateType { description: "Ctx::AnyConsensusState".to_string() }
})?;
Expand All @@ -130,12 +101,47 @@ where

fn check_for_misbehaviour<Ctx: ReaderContext>(
&self,
_ctx: &Ctx,
_client_id: ibc::core::ics24_host::identifier::ClientId,
_client_state: Self::ClientState,
_client_msg: Self::ClientMessage,
ctx: &Ctx,
client_id: ibc::core::ics24_host::identifier::ClientId,
client_state: Self::ClientState,
client_msg: Self::ClientMessage,
) -> Result<bool, Ics02ClientError> {
todo!("check_for_misbehaviour")
match client_msg {
ClientMessage::Header(header) => {
// The client can't be updated if no shreds were received
let height = header.height();

// If we received an update from the past...
if height <= client_state.latest_height() {
// ...and we have the consensus state for that height, we need to check if
// they're the same, otherwise we have a misbehaviour.
if let Ok(existing_consensus_state) = ctx.consensus_state(&client_id, height) {
let header_consensus_state =
ClientConsensusState::from_header_and_client_state(
&header,
&client_state,
)?;
let new_consensus_state = Ctx::AnyConsensusState::wrap(
&header_consensus_state,
)
.ok_or_else(|| Error::UnknownConsensusStateType {
description: "Ctx::AnyConsensusState".to_string(),
})?;

// The consensus state is different, so we have a misbehaviour.
if existing_consensus_state != new_consensus_state {
return Ok(true);
}
}
}

Ok(false)
},
ClientMessage::Misbehaviour(_) =>
return Err(Ics02ClientError::implementation_specific(
"misbehaviour not supported".to_string(),
)),
}
}

fn verify_upgrade_and_update_state<Ctx: ReaderContext>(
Expand Down Expand Up @@ -265,7 +271,7 @@ where
commitment: ibc::core::ics04_channel::commitment::PacketCommitment,
) -> Result<(), Ics02ClientError> {
client_state.verify_height(client_id, height)?;
verify_delay_passed::<Ctx, PK>(ctx, height, connection_end)?;
verify_delay_passed::<Ctx>(ctx, height, connection_end)?;

let path = ibc_core_host_types::path::CommitmentPath {
port_id: convert(port_id),
Expand All @@ -291,7 +297,7 @@ where
) -> Result<(), Ics02ClientError> {
// client state height = consensus state height
client_state.verify_height(client_id, height)?;
verify_delay_passed::<Ctx, PK>(ctx, height, connection_end)?;
verify_delay_passed::<Ctx>(ctx, height, connection_end)?;

let path = ibc_core_host_types::path::AckPath {
port_id: convert(port_id),
Expand All @@ -315,7 +321,7 @@ where
sequence: ibc::core::ics04_channel::packet::Sequence,
) -> Result<(), Ics02ClientError> {
client_state.verify_height(client_id, height)?;
verify_delay_passed::<Ctx, PK>(ctx, height, connection_end)?;
verify_delay_passed::<Ctx>(ctx, height, connection_end)?;

let path = ibc_core_host_types::path::SeqRecvPath(convert(port_id), convert(channel_id));
let mut seq_bytes = Vec::new();
Expand All @@ -337,7 +343,7 @@ where
sequence: ibc::core::ics04_channel::packet::Sequence,
) -> Result<(), Ics02ClientError> {
client_state.verify_height(client_id, height)?;
verify_delay_passed::<Ctx, PK>(ctx, height, connection_end)?;
verify_delay_passed::<Ctx>(ctx, height, connection_end)?;

let path = ibc_core_host_types::path::ReceiptPath {
port_id: convert(port_id),
Expand All @@ -348,7 +354,7 @@ where
}
}

fn verify_delay_passed<Ctx: ReaderContext, PK: PubKey>(
fn verify_delay_passed<Ctx: ReaderContext>(
ctx: &Ctx,
height: ibc::Height,
connection_end: &ibc::core::ics03_connection::connection::ConnectionEnd,
Expand All @@ -368,7 +374,7 @@ fn verify_delay_passed<Ctx: ReaderContext, PK: PubKey>(
let delay_period_height = ctx.block_delay(delay_period_time);
let delay_period_time_u64 = u64::try_from(delay_period_time.as_nanos()).unwrap();

ClientState::<PK>::verify_delay_passed(
ClientState::verify_delay_passed(
current_timestamp,
current_height,
processed_time.nanoseconds(),
Expand All @@ -379,20 +385,6 @@ fn verify_delay_passed<Ctx: ReaderContext, PK: PubKey>(
.map_err(|e| e.into())
}

impl<PK: PubKey> Verifier<PK> for CfSolanaClient<PK> {
fn verify(&self, message: &[u8], pubkey: &PK, signature: &PK::Signature) -> bool {
(|| {
let pubkey = pubkey.as_bytes();
let pubkey = ed25519_consensus::VerificationKey::try_from(&pubkey[..]).ok()?;
let signature = signature.as_bytes();
let sig = ed25519_consensus::Signature::try_from(&signature[..]).ok()?;
pubkey.verify(&sig, message).ok()?;
Some(())
})()
.is_some()
}
}

// Helper wrappers

fn verify(
Expand Down
Loading