Skip to content

Commit

Permalink
solana-ibc: use Borsh for consensus state serialisation (#95)
Browse files Browse the repository at this point in the history
Borsh is a deterministic encoding which is somewhat important in our
case where we store hash of the serialised representation in one
storage and the object in another.

Furthermore, Borsh is more space and time efficient than JSON so using
it has additional performance benefit.

Closes: #46
  • Loading branch information
mina86 authored Nov 16, 2023
1 parent 787efde commit d142c36
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 87 deletions.
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ borsh = { version = "0.10.3", default-features = false }
bytemuck = { version = "1.14", default-features = false }
derive_more = "0.99.17"
hex-literal = "0.4.1"
ibc = { version = "0.47.0", default-features = false, features = ["serde", "borsh"] }
ibc-proto = { version = "0.37.1", default-features = false, features = ["serde"] }
ibc = { version = "0.47.0", default-features = false, features = ["borsh", "serde"] }
ibc-proto = { version = "0.37.1", default-features = false }
pretty_assertions = "1.4.0"
rand = { version = "0.8.5" }
serde = "1"
Expand Down
60 changes: 34 additions & 26 deletions solana/solana-ibc/programs/solana-ibc/src/client_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ impl AnyClientStateTag {

impl AnyClientState {
/// Protobuf type URL for Tendermint client state used in Any message.
const TENDERMINT_TYPE: &'static str = ibc::clients::ics07_tendermint::client_state::TENDERMINT_CLIENT_STATE_TYPE_URL;
const TENDERMINT_TYPE: &'static str =
ibc::clients::ics07_tendermint::client_state::TENDERMINT_CLIENT_STATE_TYPE_URL;
#[cfg(any(test, feature = "mocks"))]
/// Protobuf type URL for Mock client state used in Any message.
const MOCK_TYPE: &'static str =
Expand Down Expand Up @@ -505,40 +506,47 @@ impl ibc::clients::ics07_tendermint::ValidationContext for IbcStorage<'_, '_> {
client_id: &ClientId,
height: &Height,
) -> Result<Option<Self::AnyConsensusState>, ContextError> {
use core::ops::Bound;
let height = (height.revision_number(), height.revision_height());
let min = (client_id.to_string(), height);
self.borrow()
.private
.consensus_states
.range((Bound::Excluded(min), Bound::Unbounded))
.next()
.map(|(_, encoded)| serde_json::from_str(encoded))
.transpose()
.map_err(|err| {
ContextError::ClientError(ClientError::ClientSpecific {
description: err.to_string(),
})
})
self.get_consensus_state(client_id, height, Direction::Next)
}

fn prev_consensus_state(
&self,
client_id: &ClientId,
height: &Height,
) -> Result<Option<Self::AnyConsensusState>, ContextError> {
self.get_consensus_state(client_id, height, Direction::Prev)
}
}

#[derive(Copy, Clone, PartialEq)]
enum Direction {
Next,
Prev,
}

impl IbcStorage<'_, '_> {
fn get_consensus_state(
&self,
client_id: &ClientId,
height: &Height,
dir: Direction,
) -> Result<Option<AnyConsensusState>, ContextError> {
let height = (height.revision_number(), height.revision_height());
self.borrow()
.private
.consensus_states
.range(..(client_id.to_string(), height))
.next_back()
.map(|(_, encoded)| serde_json::from_str(encoded))
let pivot = core::ops::Bound::Excluded((client_id.to_string(), height));
let range = if dir == Direction::Next {
(pivot, core::ops::Bound::Unbounded)
} else {
(core::ops::Bound::Unbounded, pivot)
};

let store = self.borrow();
let mut range = store.private.consensus_states.range(range);
if dir == Direction::Next { range.next() } else { range.next_back() }
.map(|(_, data)| borsh::BorshDeserialize::try_from_slice(data))
.transpose()
.map_err(|err| {
ContextError::ClientError(ClientError::ClientSpecific {
description: err.to_string(),
})
.map_err(|err| err.to_string())
.map_err(|description| {
ContextError::from(ClientError::ClientSpecific { description })
})
}
}
128 changes: 107 additions & 21 deletions solana/solana-ibc/programs/solana-ibc/src/consensus_state.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use anchor_lang::prelude::borsh;
use anchor_lang::prelude::borsh::maybestd::io;
use ibc::clients::ics07_tendermint::consensus_state::ConsensusState as TmConsensusState;
use ibc::core::ics02_client::consensus_state::ConsensusState;
use ibc::core::ics02_client::error::ClientError;
Expand All @@ -12,27 +14,97 @@ use ibc_proto::ibc::lightclients::tendermint::v1::ConsensusState as RawTmConsens
#[cfg(any(test, feature = "mocks"))]
use ibc_proto::ibc::mock::ConsensusState as RawMockConsensusState;
use ibc_proto::protobuf::Protobuf;
use serde::{Deserialize, Serialize};

const TENDERMINT_CONSENSUS_STATE_TYPE_URL: &str =
"/ibc.lightclients.tendermint.v1.ConsensusState";

#[derive(
Clone,
Debug,
PartialEq,
Serialize,
Deserialize,
derive_more::From,
derive_more::TryInto,
)]
#[serde(tag = "type")]
#[derive(Clone, Debug, PartialEq, derive_more::From, derive_more::TryInto)]
pub enum AnyConsensusState {
Tendermint(TmConsensusState),
#[cfg(any(test, feature = "mocks"))]
Mock(MockConsensusState),
}

/// Discriminants used when borsh-encoding [`AnyConsensusState`].
#[derive(Clone, Copy, PartialEq, Eq, strum::FromRepr)]
#[repr(u8)]
enum AnyConsensusStateTag {
Tendermint = 0,
#[cfg(any(test, feature = "mocks"))]
Mock = 255,
}

impl AnyConsensusStateTag {
/// Returns tag from protobuf type URL. Returns `None` if the type URL is
/// not recognised.
#[allow(dead_code)]
fn from_type_url(url: &str) -> Option<Self> {
match url {
AnyConsensusState::TENDERMINT_TYPE => Some(Self::Tendermint),
#[cfg(any(test, feature = "mocks"))]
AnyConsensusState::MOCK_TYPE => Some(Self::Mock),
_ => None,
}
}
}

impl AnyConsensusState {
/// Protobuf type URL for Tendermint client state used in Any message.
const TENDERMINT_TYPE: &'static str =
ibc::clients::ics07_tendermint::consensus_state::TENDERMINT_CONSENSUS_STATE_TYPE_URL;
#[cfg(any(test, feature = "mocks"))]
/// Protobuf type URL for Mock client state used in Any message.
const MOCK_TYPE: &'static str =
ibc::mock::consensus_state::MOCK_CONSENSUS_STATE_TYPE_URL;

/// Encodes the payload and returns discriminants that allow decoding the
/// value later.
///
/// Returns a `(tag, type, value)` triple where `tag` is discriminant
/// identifying variant of the enum, `type` is protobuf type URL
/// corresponding to the client state and `value` is the client state
/// encoded as protobuf.
///
/// `(tag, value)` is used when borsh-encoding and `(type, value)` is used
/// in Any protobuf message. To decode value [`Self::from_tagged`] can be
/// used potentially going through [`AnyConsensusStateTag::from_type_url`] if
/// necessary.
fn to_any(&self) -> (AnyConsensusStateTag, &str, Vec<u8>) {
match self {
AnyConsensusState::Tendermint(state) => (
AnyConsensusStateTag::Tendermint,
Self::TENDERMINT_TYPE,
Protobuf::<RawTmConsensusState>::encode_vec(state),
),
#[cfg(any(test, feature = "mocks"))]
AnyConsensusState::Mock(state) => (
AnyConsensusStateTag::Mock,
Self::MOCK_TYPE,
Protobuf::<RawMockConsensusState>::encode_vec(state),
),
}
}

/// Decodes protobuf corresponding to specified enum variant.
fn from_tagged(
tag: AnyConsensusStateTag,
value: Vec<u8>,
) -> Result<Self, ibc_proto::protobuf::Error> {
match tag {
AnyConsensusStateTag::Tendermint => {
Protobuf::<RawTmConsensusState>::decode_vec(&value)
.map(Self::Tendermint)
}
#[cfg(any(test, feature = "mocks"))]
AnyConsensusStateTag::Mock => {
Protobuf::<RawMockConsensusState>::decode_vec(&value)
.map(Self::Mock)
}
}
}
}


impl Protobuf<Any> for AnyConsensusState {}

impl TryFrom<Any> for AnyConsensusState {
Expand Down Expand Up @@ -64,17 +136,31 @@ impl TryFrom<Any> for AnyConsensusState {

impl From<AnyConsensusState> for Any {
fn from(value: AnyConsensusState) -> Self {
match value {
AnyConsensusState::Tendermint(value) => Any {
type_url: TENDERMINT_CONSENSUS_STATE_TYPE_URL.to_string(),
value: Protobuf::<RawTmConsensusState>::encode_vec(&value),
},
#[cfg(any(test, feature = "mocks"))]
AnyConsensusState::Mock(value) => Any {
type_url: MOCK_CONSENSUS_STATE_TYPE_URL.to_string(),
value: Protobuf::<RawMockConsensusState>::encode_vec(&value),
},
let (_, type_url, value) = value.to_any();
Any { type_url: type_url.into(), value }
}
}

impl borsh::BorshSerialize for AnyConsensusState {
fn serialize<W: io::Write>(&self, wr: &mut W) -> io::Result<()> {
let (tag, _, value) = self.to_any();
(tag as u8, value).serialize(wr)
}
}

impl borsh::BorshDeserialize for AnyConsensusState {
fn deserialize_reader<R: io::Read>(rd: &mut R) -> io::Result<Self> {
let (tag, value) = <(u8, Vec<u8>)>::deserialize_reader(rd)?;
let res = AnyConsensusStateTag::from_repr(tag)
.map(|tag| Self::from_tagged(tag, value));
match res {
None => Err(format!("invalid AnyConsensusState tag: {tag}")),
Some(Err(err)) => {
Err(format!("unable to decode AnyConsensusState: {err}"))
}
Some(Ok(value)) => Ok(value),
}
.map_err(|msg| io::Error::new(io::ErrorKind::InvalidData, msg))
}
}

Expand Down
29 changes: 10 additions & 19 deletions solana/solana-ibc/programs/solana-ibc/src/execution_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,28 +62,19 @@ impl ClientExecutionContext for IbcStorage<'_, '_> {
consensus_state_path,
consensus_state
);
let consensus_state_key = (
let mut store = self.borrow_mut();
let serialized = store_serialised_proof(
&mut store.provable,
&TrieKey::from(&consensus_state_path),
&consensus_state,
)?;
let key = (
consensus_state_path.client_id.to_string(),
(consensus_state_path.epoch, consensus_state_path.height),
);
let mut store = self.borrow_mut();
let serialized_consensus_state =
serde_json::to_string(&consensus_state).unwrap();

let consensus_state_trie_key = TrieKey::from(&consensus_state_path);
let trie = &mut store.provable;
trie.set(
&consensus_state_trie_key,
&CryptoHash::digest(serialized_consensus_state.as_bytes()),
)
.unwrap();

store
.private
.consensus_states
.insert(consensus_state_key, serialized_consensus_state);
store.private.height.0 = consensus_state_path.epoch;
store.private.height.1 = consensus_state_path.height;
store.private.consensus_states.insert(key, serialized);
store.private.height =
(consensus_state_path.epoch, consensus_state_path.height);
Ok(())
}

Expand Down
3 changes: 2 additions & 1 deletion solana/solana-ibc/programs/solana-ibc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ pub struct ChainWithVerifier<'info> {
trie: UncheckedAccount<'info>,

#[account(address = solana_program::sysvar::instructions::ID)]
/// CHECK:
ix_sysvar: AccountInfo<'info>,

system_program: Program<'info, System>,
Expand Down Expand Up @@ -227,7 +228,7 @@ pub struct Deliver<'info> {

/// The guest blockchain data.
#[account(init_if_needed, payer = sender, seeds = [CHAIN_SEED], bump, space = 10000)]
chain: Account<'info, chain::ChainData>,
chain: Box<Account<'info, chain::ChainData>>,

system_program: Program<'info, System>,
}
Expand Down
2 changes: 1 addition & 1 deletion solana/solana-ibc/programs/solana-ibc/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pub(crate) type InnerChannelId = String;
pub(crate) type InnerClient = Vec<u8>; // Serialized
pub(crate) type InnerConnectionEnd = Vec<u8>; // Serialized
pub(crate) type InnerChannelEnd = Vec<u8>; // Serialized
pub(crate) type InnerConsensusState = String; // Serialized
pub(crate) type InnerConsensusState = Vec<u8>; // Serialized

/// A triple of send, receive and acknowledge sequences.
#[derive(
Expand Down
37 changes: 20 additions & 17 deletions solana/solana-ibc/programs/solana-ibc/src/validation_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,27 +59,30 @@ impl ValidationContext for IbcStorage<'_, '_> {
&self,
client_cons_state_path: &ClientConsensusStatePath,
) -> Result<Self::AnyConsensusState> {
let consensus_state_key = &(
let key = &(
client_cons_state_path.client_id.to_string(),
(client_cons_state_path.epoch, client_cons_state_path.height),
);
let store = self.borrow();
match store.private.consensus_states.get(consensus_state_key) {
Some(data) => {
let result: Self::AnyConsensusState =
serde_json::from_str(data).unwrap();
Ok(result)
}
None => Err(ContextError::ClientError(
ClientError::ConsensusStateNotFound {
client_id: client_cons_state_path.client_id.clone(),
height: ibc::Height::new(
client_cons_state_path.epoch,
client_cons_state_path.height,
)?,
},
)),
let state = self
.borrow()
.private
.consensus_states
.get(key)
.map(|data| borsh::BorshDeserialize::try_from_slice(data));
match state {
Some(Ok(value)) => Ok(value),
Some(Err(err)) => Err(ClientError::ClientSpecific {
description: err.to_string(),
}),
None => Err(ClientError::ConsensusStateNotFound {
client_id: client_cons_state_path.client_id.clone(),
height: ibc::Height::new(
client_cons_state_path.epoch,
client_cons_state_path.height,
)?,
}),
}
.map_err(ibc::core::ContextError::from)
}

fn host_height(&self) -> Result<ibc::Height> {
Expand Down

0 comments on commit d142c36

Please sign in to comment.