diff --git a/solana/solana-ibc/programs/solana-ibc/src/execution_context.rs b/solana/solana-ibc/programs/solana-ibc/src/execution_context.rs index 2bb68fb4..13212abc 100644 --- a/solana/solana-ibc/programs/solana-ibc/src/execution_context.rs +++ b/solana/solana-ibc/programs/solana-ibc/src/execution_context.rs @@ -52,17 +52,14 @@ impl ClientExecutionContext for SolanaIbcStorage<'_, '_> { let serialized_client_state = serde_json::to_string(&client_state).unwrap(); - let client_state_trie_key = &TrieKey::ClientState { - client_id: client_state_path.0.to_string(), - } - .to_vec(); + let client_state_trie_key = TrieKey::from(&client_state_path); let trie = self.trie.as_mut().unwrap(); msg!( "THis is serialized client state {}", &lib::hash::CryptoHash::digest(serialized_client_state.as_bytes()) ); trie.set( - client_state_trie_key, + &client_state_trie_key, &lib::hash::CryptoHash::digest(serialized_client_state.as_bytes()), ) .unwrap(); @@ -88,15 +85,10 @@ impl ClientExecutionContext for SolanaIbcStorage<'_, '_> { let serialized_consensus_state = serde_json::to_string(&consensus_state).unwrap(); - let consensus_state_trie_key = &TrieKey::ConsensusState { - client_id: consensus_state_path.client_id.to_string(), - height: consensus_state_path.height, - epoch: consensus_state_path.epoch, - } - .to_vec(); + let consensus_state_trie_key = TrieKey::from(&consensus_state_path); let trie = self.trie.as_mut().unwrap(); trie.set( - consensus_state_trie_key, + &consensus_state_trie_key, &lib::hash::CryptoHash::digest( serialized_consensus_state.as_bytes(), ), @@ -205,10 +197,10 @@ impl ExecutionContext for SolanaIbcStorage<'_, '_> { let serialized_connection_end = serde_json::to_string(&connection_end).unwrap(); - let connection_trie_key = &TrieKey::from(connection_path).to_vec(); + let connection_trie_key = TrieKey::from(connection_path); let trie = self.trie.as_mut().unwrap(); trie.set( - connection_trie_key, + &connection_trie_key, &lib::hash::CryptoHash::digest( serialized_connection_end.as_bytes(), ), @@ -254,10 +246,10 @@ impl ExecutionContext for SolanaIbcStorage<'_, '_> { commitment_path, commitment ); - let commitment_trie_key = &TrieKey::from(commitment_path).to_vec(); + let commitment_trie_key = TrieKey::from(commitment_path); let trie = self.trie.as_mut().unwrap(); trie.set( - commitment_trie_key, + &commitment_trie_key, &lib::hash::CryptoHash::digest(&commitment.into_vec()), ) .unwrap(); @@ -300,10 +292,10 @@ impl ExecutionContext for SolanaIbcStorage<'_, '_> { receipt_path, receipt ); - let receipt_trie_key = &TrieKey::from(receipt_path).to_vec(); + let receipt_trie_key = TrieKey::from(receipt_path); let trie = self.trie.as_mut().unwrap(); - trie.set(receipt_trie_key, &lib::hash::CryptoHash::DEFAULT).unwrap(); - trie.seal(receipt_trie_key).unwrap(); + trie.set(&receipt_trie_key, &lib::hash::CryptoHash::DEFAULT).unwrap(); + trie.seal(&receipt_trie_key).unwrap(); record_packet_sequence( &mut self.packet_receipt_sequence_sets, &receipt_path.port_id, @@ -323,10 +315,10 @@ impl ExecutionContext for SolanaIbcStorage<'_, '_> { ack_path, ack_commitment ); - let ack_commitment_trie_key = &TrieKey::from(ack_path).to_vec(); + let ack_commitment_trie_key = TrieKey::from(ack_path); let trie = self.trie.as_mut().unwrap(); trie.set( - ack_commitment_trie_key, + &ack_commitment_trie_key, &lib::hash::CryptoHash::digest(&ack_commitment.into_vec()), ) .unwrap(); @@ -368,10 +360,10 @@ impl ExecutionContext for SolanaIbcStorage<'_, '_> { )); let serialized_channel_end = borsh::to_vec(&channel_end).unwrap(); - let channel_end_trie_key = &TrieKey::from(channel_end_path).to_vec(); + let channel_end_trie_key = TrieKey::from(channel_end_path); let trie = self.trie.as_mut().unwrap(); trie.set( - channel_end_trie_key, + &channel_end_trie_key, &lib::hash::CryptoHash::digest(&serialized_channel_end), ) .unwrap(); @@ -396,13 +388,13 @@ impl ExecutionContext for SolanaIbcStorage<'_, '_> { let seq_send_key = (seq_send_path.0.to_string(), seq_send_path.1.to_string()); - let next_seq_send_trie_key = &TrieKey::from(seq_send_path).to_vec(); + let next_seq_send_trie_key = TrieKey::from(seq_send_path); let trie = self.trie.as_mut().unwrap(); let seq_in_u64: u64 = seq.into(); let seq_in_bytes = seq_in_u64.to_be_bytes(); trie.set( - next_seq_send_trie_key, + &next_seq_send_trie_key, &lib::hash::CryptoHash::digest(&seq_in_bytes), ) .unwrap(); @@ -423,13 +415,13 @@ impl ExecutionContext for SolanaIbcStorage<'_, '_> { ); let seq_recv_key = (seq_recv_path.0.to_string(), seq_recv_path.1.to_string()); - let next_seq_recv_trie_key = &TrieKey::from(seq_recv_path).to_vec(); + let next_seq_recv_trie_key = TrieKey::from(seq_recv_path); let trie = self.trie.as_mut().unwrap(); let seq_in_u64: u64 = seq.into(); let seq_in_bytes = seq_in_u64.to_be_bytes(); trie.set( - next_seq_recv_trie_key, + &next_seq_recv_trie_key, &lib::hash::CryptoHash::digest(&seq_in_bytes), ) .unwrap(); @@ -445,13 +437,13 @@ impl ExecutionContext for SolanaIbcStorage<'_, '_> { msg!("store_next_sequence_ack: path: {}, seq: {:?}", seq_ack_path, seq); let seq_ack_key = (seq_ack_path.0.to_string(), seq_ack_path.1.to_string()); - let next_seq_ack_trie_key = &TrieKey::from(seq_ack_path).to_vec(); + let next_seq_ack_trie_key = TrieKey::from(seq_ack_path); let trie = self.trie.as_mut().unwrap(); let seq_in_u64: u64 = seq.into(); let seq_in_bytes = seq_in_u64.to_be_bytes(); trie.set( - next_seq_ack_trie_key, + &next_seq_ack_trie_key, &lib::hash::CryptoHash::digest(&seq_in_bytes), ) .unwrap(); diff --git a/solana/solana-ibc/programs/solana-ibc/src/trie_key.rs b/solana/solana-ibc/programs/solana-ibc/src/trie_key.rs index 4e08f64b..f1fe368a 100644 --- a/solana/solana-ibc/programs/solana-ibc/src/trie_key.rs +++ b/solana/solana-ibc/programs/solana-ibc/src/trie_key.rs @@ -1,10 +1,9 @@ -use std::mem::size_of; - use ibc::core::ics04_channel::packet::Sequence; use ibc::core::ics24_host::identifier::{ChannelId, PortId}; use ibc::core::ics24_host::path::{ - AckPath, ChannelEndPath, CommitmentPath, ConnectionPath, ReceiptPath, - SeqAckPath, SeqRecvPath, SeqSendPath, + AckPath, ChannelEndPath, ClientConsensusStatePath, ClientStatePath, + CommitmentPath, ConnectionPath, ReceiptPath, SeqAckPath, SeqRecvPath, + SeqSendPath, }; // Note: We’re not using ChannelId::prefix() and ConnectionId::prefix() because @@ -12,253 +11,260 @@ use ibc::core::ics24_host::path::{ // include that hyphen. use super::{CHANNEL_ID_PREFIX, CONNECTION_ID_PREFIX}; -pub enum TrieKey { - ClientState { client_id: String }, - ConsensusState { client_id: String, epoch: u64, height: u64 }, - Connection { connection_id: u32 }, - ChannelEnd { port_id: String, channel_id: u32 }, - NextSequenceSend { port_id: String, channel_id: u32 }, - NextSequenceRecv { port_id: String, channel_id: u32 }, - NextSequenceAck { port_id: String, channel_id: u32 }, - Commitment { port_id: String, channel_id: u32, sequence: u64 }, - Receipts { port_id: String, channel_id: u32, sequence: u64 }, - Acks { port_id: String, channel_id: u32, sequence: u64 }, +/// A key used for indexing entries in the provable storage. +/// +/// The key is built from IBC storage paths. The first byte is discriminant +/// determining the type of path and the rest are concatenated components +/// encoded in binary. The key format can be visualised as the following enum: +/// +/// ```ignore +/// enum TrieKey { +/// ClientState { client_id: String }, +/// ConsensusState { client_id: String, epoch: u64, height: u64 }, +/// Connection { connection_id: u32 }, +/// ChannelEnd { port_id: String, channel_id: u32 }, +/// NextSequenceSend { port_id: String, channel_id: u32 }, +/// NextSequenceRecv { port_id: String, channel_id: u32 }, +/// NextSequenceAck { port_id: String, channel_id: u32 }, +/// Commitment { port_id: String, channel_id: u32, sequence: u64 }, +/// Receipts { port_id: String, channel_id: u32, sequence: u64 }, +/// Acks { port_id: String, channel_id: u32, sequence: u64 }, +/// } +/// ``` +/// +/// Integers are encoded using big-endian to guarantee dense encoding of +/// consecutive keys (i.e. sequence 10 is immediately followed by 11 which would +/// not be the case in little-endian encoding). This is also one reason why we +/// don’t just use Borsh encoding. +// TODO(mina86): Look into using lib::varint::Buffer or some kind of small vec +// to avoid heap allocations. +pub struct TrieKey(Vec); + +/// Constructs a new [`TrieKey`] by concatenating key components. +/// +/// The first argument to the macro is a [`Tag`] object. Remaining must +/// implement [`AsComponent`]. +macro_rules! new_key_impl { + ($tag:expr $(, $component:expr)*) => {{ + let len = 1 $(+ $component.key_len())*; + let mut key = Vec::with_capacity(len); + key.push(Tag::from($tag) as u8); + $($component.append_into(&mut key);)* + debug_assert_eq!(len, key.len()); + TrieKey(key) + }} } -#[repr(u8)] -enum TrieKeyWithoutFields { - ClientState = 1, - ConsensusState = 2, - Connection = 3, - ChannelEnd = 4, - NextSequenceSend = 5, - NextSequenceRecv = 6, - NextSequenceAck = 7, - Commitment = 8, - Receipts = 9, - Acks = 10, +impl TrieKey { + /// Constructs a new key for a `(port_id, channel_id)` path. + /// + /// Panics if `channel_id` is invalid. + fn from_channel_path( + tag: Tag, + port_id: &PortId, + channel_id: &ChannelId, + ) -> Self { + new_key_impl!(tag, port_id, channel_id) + } + + /// Constructs a new key for a `(port_id, channel_id, sequence)` path. + /// + /// Panics if `channel_id` is invalid. + fn from_sequence_path( + tag: Tag, + port_id: &PortId, + channel_id: &ChannelId, + sequence: Sequence, + ) -> Self { + new_key_impl!(tag, port_id, channel_id, sequence) + } } -/// Strips `prefix` from `data` and parses it to get `T`. Panics if data -/// doesn’t start with the prefix or parsing fails. -fn parse_sans_prefix( - prefix: &'static str, - data: &str, -) -> T { - data.strip_prefix(prefix) - .and_then(|id| id.parse().ok()) - .unwrap_or_else(|| panic!("invalid identifier: {data}")) +impl core::ops::Deref for TrieKey { + type Target = [u8]; + fn deref(&self) -> &[u8] { self.0.as_slice() } } -/// Constructs a `(port_id, channel_id)` for creation of a TrieKey. -/// -/// Panics if `channel_id` is invalid. -fn handle_port_channel( - port_id: &PortId, - channel_id: &ChannelId, -) -> (String, u32) { - let port_id = port_id.to_string(); - let channel_id = parse_sans_prefix(CHANNEL_ID_PREFIX, channel_id.as_str()); - (port_id, channel_id) +impl From<&ClientStatePath> for TrieKey { + fn from(path: &ClientStatePath) -> Self { + new_key_impl!(Tag::ClientState, path.0) + } } -/// Constructs a `(port_id, channel_id, sequence)` for creation of a TrieKey. -/// -/// Panics if `channel_id` is invalid. -fn handle_port_channel_seq( - port_id: &PortId, - channel_id: &ChannelId, - sequence: Sequence, -) -> (String, u32, u64) { - let (port_id, channel_id) = handle_port_channel(port_id, channel_id); - let sequence = u64::from(sequence); - (port_id, channel_id, sequence) +impl From<&ClientConsensusStatePath> for TrieKey { + fn from(path: &ClientConsensusStatePath) -> Self { + new_key_impl!( + Tag::ConsensusState, + path.client_id, + path.epoch, + path.height + ) + } } -impl From<&ReceiptPath> for TrieKey { - fn from(path: &ReceiptPath) -> Self { - let (port_id, channel_id, sequence) = handle_port_channel_seq( +impl From<&ConnectionPath> for TrieKey { + fn from(path: &ConnectionPath) -> Self { + new_key_impl!(Tag::Connection, path.0) + } +} + +impl From<&ChannelEndPath> for TrieKey { + fn from(path: &ChannelEndPath) -> Self { + Self::from_channel_path(Tag::ChannelEnd, &path.0, &path.1) + } +} + +impl From<&SeqSendPath> for TrieKey { + fn from(path: &SeqSendPath) -> Self { + Self::from_channel_path(Tag::NextSequenceSend, &path.0, &path.1) + } +} + +impl From<&SeqRecvPath> for TrieKey { + fn from(path: &SeqRecvPath) -> Self { + Self::from_channel_path(Tag::NextSequenceRecv, &path.0, &path.1) + } +} + +impl From<&SeqAckPath> for TrieKey { + fn from(path: &SeqAckPath) -> Self { + Self::from_channel_path(Tag::NextSequenceAck, &path.0, &path.1) + } +} + +impl From<&CommitmentPath> for TrieKey { + fn from(path: &CommitmentPath) -> Self { + Self::from_sequence_path( + Tag::Commitment, &path.port_id, &path.channel_id, path.sequence, - ); - Self::Receipts { port_id, channel_id, sequence } + ) } } -impl From<&AckPath> for TrieKey { - fn from(path: &AckPath) -> Self { - let (port_id, channel_id, sequence) = handle_port_channel_seq( +impl From<&ReceiptPath> for TrieKey { + fn from(path: &ReceiptPath) -> Self { + Self::from_sequence_path( + Tag::Receipt, &path.port_id, &path.channel_id, path.sequence, - ); - Self::Acks { port_id, channel_id, sequence } + ) } } -impl From<&CommitmentPath> for TrieKey { - fn from(path: &CommitmentPath) -> Self { - let (port_id, channel_id, sequence) = handle_port_channel_seq( +impl From<&AckPath> for TrieKey { + fn from(path: &AckPath) -> Self { + Self::from_sequence_path( + Tag::Ack, &path.port_id, &path.channel_id, path.sequence, - ); - Self::Commitment { port_id, channel_id, sequence } + ) } } -impl From<&SeqRecvPath> for TrieKey { - fn from(path: &SeqRecvPath) -> Self { - let (port_id, channel_id) = handle_port_channel(&path.0, &path.1); - Self::NextSequenceRecv { port_id, channel_id } - } +/// A discriminant used as the first byte of each trie key to create namespaces +/// for different objects stored in the trie. +#[repr(u8)] +enum Tag { + ClientState = 1, + ConsensusState = 2, + Connection = 3, + ChannelEnd = 4, + NextSequenceSend = 5, + NextSequenceRecv = 6, + NextSequenceAck = 7, + Commitment = 8, + Receipt = 9, + Ack = 10, } -impl From<&SeqSendPath> for TrieKey { - fn from(path: &SeqSendPath) -> Self { - let (port_id, channel_id) = handle_port_channel(&path.0, &path.1); - Self::NextSequenceSend { port_id, channel_id } +/// Component of a [`TrieKey`]. +/// +/// A `TrieKey` is constructed by concatenating a sequence of components. +trait AsComponent { + /// Returns length of the raw representation of the component. + fn key_len(&self) -> usize; + + /// Appends the component into a vector. + fn append_into(&self, dest: &mut Vec); +} + +// TODO(#35): Investigate weather we can impose restrictions on client +// identifiers, e.g. `client-`. +impl AsComponent for ibc::core::ics24_host::identifier::ClientId { + fn key_len(&self) -> usize { self.as_str().key_len() } + fn append_into(&self, dest: &mut Vec) { + self.as_str().append_into(dest) } } -impl From<&SeqAckPath> for TrieKey { - fn from(path: &SeqAckPath) -> Self { - let (port_id, channel_id) = handle_port_channel(&path.0, &path.1); - Self::NextSequenceAck { port_id, channel_id } +impl AsComponent for ibc::core::ics24_host::identifier::ConnectionId { + fn key_len(&self) -> usize { 0_u32.key_len() } + fn append_into(&self, dest: &mut Vec) { + parse_sans_prefix(CONNECTION_ID_PREFIX, self.as_str()).append_into(dest) } } -impl From<&ChannelEndPath> for TrieKey { - fn from(path: &ChannelEndPath) -> Self { - let (port_id, channel_id) = handle_port_channel(&path.0, &path.1); - Self::ChannelEnd { port_id, channel_id } +// TODO(#35): Investigate weather we can impose restrictions on port +// identifiers, e.g. `port-`. +impl AsComponent for ibc::core::ics24_host::identifier::PortId { + fn key_len(&self) -> usize { self.as_str().key_len() } + fn append_into(&self, dest: &mut Vec) { + self.as_str().append_into(dest) } } -impl From<&ConnectionPath> for TrieKey { - fn from(path: &ConnectionPath) -> Self { - Self::Connection { - connection_id: parse_sans_prefix( - CONNECTION_ID_PREFIX, - path.0.as_str(), - ), - } +impl AsComponent for ibc::core::ics24_host::identifier::ChannelId { + fn key_len(&self) -> usize { 0_u32.key_len() } + fn append_into(&self, dest: &mut Vec) { + parse_sans_prefix(CHANNEL_ID_PREFIX, self.as_str()).append_into(dest) } } +impl AsComponent for ibc::core::ics04_channel::packet::Sequence { + fn key_len(&self) -> usize { 0_u64.key_len() } + fn append_into(&self, dest: &mut Vec) { + u64::from(*self).append_into(dest) + } +} -impl TrieKey { - fn len(&self) -> usize { - size_of::() + - match self { - TrieKey::ClientState { client_id } => client_id.len(), - TrieKey::ConsensusState { - client_id, - epoch: _u64, - height: _, - } => client_id.len() + size_of::() + size_of::(), - TrieKey::Connection { connection_id: _ } => size_of::(), - TrieKey::ChannelEnd { port_id, channel_id: _ } => { - port_id.len() + size_of::() - } - TrieKey::NextSequenceSend { port_id, channel_id: _ } => { - port_id.len() + size_of::() - } - TrieKey::NextSequenceRecv { port_id, channel_id: _ } => { - port_id.len() + size_of::() - } - TrieKey::NextSequenceAck { port_id, channel_id: _ } => { - port_id.len() + size_of::() - } - TrieKey::Commitment { port_id, channel_id: _, sequence: _ } => { - port_id.len() + size_of::() + size_of::() - } - TrieKey::Receipts { port_id, channel_id: _, sequence: _ } => { - port_id.len() + size_of::() + size_of::() - } - TrieKey::Acks { port_id, channel_id: _, sequence: _ } => { - port_id.len() + size_of::() + size_of::() - } - } +impl AsComponent for str { + fn key_len(&self) -> usize { + assert!(self.len() <= usize::from(u8::MAX)); + 1 + self.len() } + fn append_into(&self, dest: &mut Vec) { + // TODO(#35): Perhaps it would be worth to compress the value. For + // identifiers longer than 32 bytes we could hash them to limit the + // length of the encoding to 33 bytes. And since we can assume the + // string is ASCII for shorter values we could pack each 8 bytes into 7 + // bytes (though this is probably not really worth it). + dest.push(self.len() as u8); + dest.extend(self.as_bytes()); + } +} - pub fn append_into(&self, buf: &mut Vec) { - let expected_len = self.len(); - let start_len = buf.len(); - buf.reserve(self.len()); - match self { - TrieKey::ClientState { client_id } => { - buf.push(TrieKeyWithoutFields::ClientState as u8); - buf.extend(client_id.as_bytes()); - } - TrieKey::ConsensusState { client_id, epoch, height } => { - buf.push(TrieKeyWithoutFields::ConsensusState as u8); - buf.extend(client_id.as_bytes()); - buf.push(TrieKeyWithoutFields::ConsensusState as u8); - buf.extend(height.to_be_bytes()); - buf.push(TrieKeyWithoutFields::ConsensusState as u8); - buf.extend(epoch.to_be_bytes()) - } - TrieKey::Connection { connection_id } => { - buf.push(TrieKeyWithoutFields::Connection as u8); - buf.extend(connection_id.to_be_bytes()) - } - TrieKey::ChannelEnd { port_id, channel_id } => { - buf.push(TrieKeyWithoutFields::ChannelEnd as u8); - buf.extend(port_id.as_bytes()); - buf.push(TrieKeyWithoutFields::ChannelEnd as u8); - buf.extend(channel_id.to_be_bytes()); - } - TrieKey::NextSequenceSend { port_id, channel_id } => { - buf.push(TrieKeyWithoutFields::NextSequenceSend as u8); - buf.extend(port_id.as_bytes()); - buf.push(TrieKeyWithoutFields::NextSequenceSend as u8); - buf.extend(channel_id.to_be_bytes()); - } - TrieKey::NextSequenceRecv { port_id, channel_id } => { - buf.push(TrieKeyWithoutFields::NextSequenceRecv as u8); - buf.extend(port_id.as_bytes()); - buf.push(TrieKeyWithoutFields::NextSequenceRecv as u8); - buf.extend(channel_id.to_be_bytes()); - } - TrieKey::NextSequenceAck { port_id, channel_id } => { - buf.push(TrieKeyWithoutFields::NextSequenceAck as u8); - buf.extend(port_id.as_bytes()); - buf.push(TrieKeyWithoutFields::NextSequenceAck as u8); - buf.extend(channel_id.to_be_bytes()); - } - TrieKey::Commitment { port_id, channel_id, sequence } => { - buf.push(TrieKeyWithoutFields::Commitment as u8); - buf.extend(port_id.as_bytes()); - buf.push(TrieKeyWithoutFields::Commitment as u8); - buf.extend(channel_id.to_be_bytes()); - buf.push(TrieKeyWithoutFields::Commitment as u8); - buf.extend(sequence.to_be_bytes()); - } - TrieKey::Receipts { port_id, channel_id, sequence } => { - buf.push(TrieKeyWithoutFields::Receipts as u8); - buf.extend(port_id.as_bytes()); - buf.push(TrieKeyWithoutFields::Receipts as u8); - buf.extend(channel_id.to_be_bytes()); - buf.push(TrieKeyWithoutFields::Receipts as u8); - buf.extend(sequence.to_be_bytes()); - } - TrieKey::Acks { port_id, channel_id, sequence } => { - buf.push(TrieKeyWithoutFields::Acks as u8); - buf.extend(port_id.as_bytes()); - buf.push(TrieKeyWithoutFields::Acks as u8); - buf.extend(channel_id.to_be_bytes()); - buf.push(TrieKeyWithoutFields::Acks as u8); - buf.extend(sequence.to_be_bytes()); - } - } - debug_assert_eq!(expected_len, buf.len() - start_len); +impl AsComponent for u32 { + fn key_len(&self) -> usize { core::mem::size_of_val(self) } + fn append_into(&self, dest: &mut Vec) { + dest.extend(&self.to_be_bytes()[..]); } +} - pub fn to_vec(&self) -> Vec { - let mut buf = Vec::with_capacity(self.len()); - self.append_into(&mut buf); - buf +impl AsComponent for u64 { + fn key_len(&self) -> usize { core::mem::size_of_val(self) } + fn append_into(&self, dest: &mut Vec) { + dest.extend(&self.to_be_bytes()[..]); } } + +/// Strips `prefix` from `data` and parses it to get `u32`. Panics if data +/// doesn’t start with the prefix or parsing fails. +fn parse_sans_prefix(prefix: &'static str, data: &str) -> u32 { + data.strip_prefix(prefix) + .and_then(|id| id.parse().ok()) + .unwrap_or_else(|| panic!("invalid identifier: {data}")) +}