Skip to content

Commit a8ffa6e

Browse files
vaporifsrdtrkmariuszzakgjermundgarabacernicc
authored
feat(attester): solana adapter (#6)
* test(e2e): added a solana test suite (#574) * feat: solana nix tooling (#579) Co-authored-by: srdtrk <srdtrk@hotmail.com> * feat: solana tendermint light client core (#590) Co-authored-by: Mariusz Zak <mariuszzak21@gmail.com> Co-authored-by: srdtrk <srdtrk@hotmail.com> * feat: router core (#601) Co-authored-by: Gjermund Garaba <gjermund@garaba.net> * feat: solana tendermint mollusk initialize test (#614) * chore: ci router mollusk tests (#629) * chore: router pr comments + validation tests (#633) * feat: Add Solana fixture generation and fix update_client implementation (#615) * chore: router cpi tests (#642) * fix: solana ibc inconsistencies (#651) * chore: solana storage adr (#613) Co-authored-by: srdtrk <59252793+srdtrk@users.noreply.github.com> * test(e2e): initial Solana end-to-end tests (#648) Co-authored-by: Gjermund Garaba <gjermund@garaba.net> * chore: lock agave rust nix (#668) * add rust in solana devshell for lsp access (#673) * feat: adjust solana tests to use tendermint-light-client fixtures (#670) Co-authored-by: Gjermund Garaba <gjermund@garaba.net> Co-authored-by: Dmytro Onypko <vaporif@proton.me> Co-authored-by: srdtrk <59252793+srdtrk@users.noreply.github.com> Co-authored-by: srdtrk <srdtrk@hotmail.com> * ibc app & router updates (#669) * solana update from main (#683) Co-authored-by: Gjermund Garaba <gjermund@garaba.net> Co-authored-by: Mariusz Żak <mariuszzak21@gmail.com> Co-authored-by: srdtrk <59252793+srdtrk@users.noreply.github.com> Co-authored-by: srdtrk <srdtrk@hotmail.com> * chore: solana upgrade adr (#665) * feat: solana relayer implementation wip (#678) * chore(solana): merge `main` into `feat/solana` (#717) * feat: extend Solana → Cosmos IBC integration with e2e testing (#680) * chore(solana): nix idl generation (#761) * feat(solana): chunked header update + e2e solana->cosmos/cosmos->solana (#760) * feat(solana): chunked router instructions + working e2e (#766) * fix(solana): nix (#769) Co-authored-by: Dmytro Onypko <vaporif@proton.me> * fix(solana): idl generation (#774) * feat(solana): implement ics27-gmp ibc app (#757) * feat(solana): upgradability (#773) * chore(solana): Merge `main` into `feat/solana` (#778) * chore(solana): nix darwin fix (#781) * chore(solana): nix pull apple sdk 15 (#784) * feat(solana): remove txs field in relayer, encode txs, parallel relayer packets submission (#771) * chore: remove unused HeaderMetadata (#786) * chore(solana): simplify misbehaviour check (#787) * chore(solana): remove tendermint non chunked update (#789) * chore(solana): remove unused errors (#790) * test(solana): extend e2e test coverage for unhappy paths in GMP app (#782) * test(solana): add missing unit test coverage for membership verification of ics07-tendermint-light-client (#788) * chore(solana): reorganize SEED constants and PDA derivation for consistency (#792) * feat(solana): use confirmed commitment in relayer + parallelize e2e tests (#793) * chore(solana): docs + tests + cleanup (#800) * fix(solana): outstanding post review issues + tests (#811) * fix(solana): use seconds instead of nanos in non-membership (#812) * chore(solana): remaining accounts refactoring + unit tests edgecases (#813) * chore(solana): e2e tests for cleanup chunks (#814) * feat(solana): chunked submit misbehaviour (#816) * feat(solana): ics27-GMP improvements (#810) * chore(solana): add comments for universal app unreachable + constant generation for future (#817) * fix(solana): owner/checked validations (#819) * feat(solana): auto init space calulcation (#825) * perf(solana): optimize light client for mainnet headers (#826) * perf(solana): light client update (#830) * feat(solana): sendpacket sequence improvement (#824) * chore(crates): switch to tendermint repo (#834) * chore(solana): merge with main (#835) Co-authored-by: Gjermund Garaba <gjermund@garaba.net> Co-authored-by: Mariusz Żak <mariuszzak21@gmail.com> Co-authored-by: srdtrk <59252793+srdtrk@users.noreply.github.com> Co-authored-by: srdtrk <srdtrk@hotmail.com> Co-authored-by: Coder <161350311+MamunC0der@users.noreply.github.com> * feat(solana): ics23 hostfunctions (#837) * feat(solana): structured updateclient/relaypacket (#838) * add signer * lint go * solana adapter * unordered * commitment set * cleanup * condense * warnings * simplify * use commitment type * use pda fn --------- Co-authored-by: srdtrk <59252793+srdtrk@users.noreply.github.com> Co-authored-by: srdtrk <srdtrk@hotmail.com> Co-authored-by: Mariusz Zak <mariuszzak21@gmail.com> Co-authored-by: Gjermund Garaba <gjermund@garaba.net> Co-authored-by: Rok Černič <rok.cernic@gmail.com> Co-authored-by: Coder <161350311+MamunC0der@users.noreply.github.com>
1 parent 0ea7b07 commit a8ffa6e

File tree

6 files changed

+223
-29
lines changed

6 files changed

+223
-29
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,8 @@ ethereum-light-client-v1_2_0 = { package = "ethereum-light-client", git = "https
192192
tempfile = { version = "3.8.1", default-features = false }
193193

194194
# Solana dependencies
195-
solana-client = { version = "2.0" }
196-
solana-sdk = { version = "2.0" }
195+
solana-client = { version = "2.0", default-features = false }
196+
solana-sdk = { version = "2.0", default-features = false }
197197
solana-commitment-config = { version = "2.2.1", default-features = false }
198198
solana-transaction-status = { version = "2.0" }
199199
bincode = { version = "1.3" }

programs/ibc-attestor/Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ keywords.workspace = true
88

99

1010
[features]
11-
default = ["evm"]
12-
sol = ["dep:solana-client", "dep:solana-commitment-config", "dep:solana-sdk", ]
11+
default = ["sol"]
12+
sol = ["dep:solana-client", "dep:solana-commitment-config", "dep:solana-sdk", "dep:solana-ibc-types"]
1313
evm = [ "dep:alloy", "dep:alloy-rpc-client", "dep:alloy-network", "dep:alloy-provider" ]
1414
cosmos = [ "dep:alloy", "dep:ibc-eureka-utils" ]
1515

@@ -39,6 +39,7 @@ hex = { workspace = true, default-features = true }
3939
solana-client = { workspace = true, default-features = false, optional = true }
4040
solana-commitment-config = { workspace = true, default-features = false, optional = true }
4141
solana-sdk = { workspace = true, default-features = false, optional = true }
42+
solana-ibc-types = { path = "../solana/packages/solana-ibc-types", optional = true }
4243

4344
anyhow = { workspace = true, default-features = true, features = ["backtrace"] }
4445
thiserror = { workspace = true, default-features = false }
Lines changed: 213 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1+
use alloy_primitives::keccak256;
2+
use futures::{stream::FuturesUnordered, TryStreamExt};
3+
use ibc_eureka_solidity_types::msgs::IAttestorMsgs;
14
use serde::{Deserialize, Serialize};
25
use solana_client::nonblocking::rpc_client::RpcClient;
6+
use solana_commitment_config::{CommitmentConfig, CommitmentLevel};
7+
use solana_ibc_types::Commitment;
38
use solana_sdk::pubkey::Pubkey;
9+
use std::str::FromStr;
410

511
use crate::adapter::AttestationAdapter;
612
use crate::api::CommitmentType;
7-
use crate::cli::SolanaClientConfig;
813
use crate::AttestorError;
9-
use ibc_eureka_solidity_types::msgs::IAttestorMsgs;
14+
use crate::Packets;
1015

1116
/// Relevant chain peek options. For their Solana
1217
/// interpretation see [these docs](https://docs.arbitrum.io/for-devs/troubleshooting-building#how-many-block-numbers-must-we-wait-for-in-arbitrum-before-we-can-confidently-state-that-the-transaction-has-reached-finality)
@@ -22,38 +27,229 @@ pub struct AccountState {
2227
pub(super) data: Vec<u8>,
2328
}
2429

30+
#[derive(Clone, Debug, serde::Deserialize)]
31+
pub struct SolanaClientConfig {
32+
pub url: String,
33+
pub router_program_id: String,
34+
}
35+
2536
pub struct SolanaClient {
26-
_client: RpcClient,
27-
_account_key: Pubkey,
37+
client: RpcClient,
38+
router_program_id: Pubkey,
2839
}
2940

3041
impl SolanaClient {
31-
pub fn _from_config(_config: &SolanaClientConfig) -> Self {
32-
todo!()
42+
pub fn from_config(config: &SolanaClientConfig) -> Result<Self, AttestorError> {
43+
let client = RpcClient::new_with_commitment(
44+
config.url.clone(),
45+
CommitmentConfig {
46+
commitment: CommitmentLevel::Finalized,
47+
},
48+
);
49+
50+
let router_program_id = Pubkey::from_str(&config.router_program_id).map_err(|e| {
51+
AttestorError::ClientConfigError(format!(
52+
"Invalid router program ID {}: {}",
53+
config.router_program_id, e
54+
))
55+
})?;
56+
57+
Ok(Self {
58+
client,
59+
router_program_id,
60+
})
3361
}
3462

35-
#[allow(dead_code)]
36-
async fn get_account_info_by_slot_height(
63+
async fn get_timestamp_for_slot_at_height(&self, slot: u64) -> Result<u64, AttestorError> {
64+
self.verify_slot_is_finalized(slot).await?;
65+
66+
let block_time = self
67+
.client
68+
.get_block_time(slot)
69+
.await
70+
.map_err(|e| AttestorError::ClientError(e.to_string()))?;
71+
72+
Ok(block_time as u64)
73+
}
74+
75+
async fn verify_slot_is_finalized(&self, slot: u64) -> Result<(), AttestorError> {
76+
let current_finalized_slot = self
77+
.client
78+
.get_slot_with_commitment(CommitmentConfig::finalized())
79+
.await
80+
.map_err(|e| AttestorError::ClientError(e.to_string()))?;
81+
82+
if slot > current_finalized_slot {
83+
return Err(AttestorError::ClientError(format!(
84+
"Slot {} is not finalized yet. Current finalized slot: {}",
85+
slot, current_finalized_slot
86+
)));
87+
}
88+
89+
Ok(())
90+
}
91+
92+
async fn get_historical_packet_commitment(
3793
&self,
38-
_peek_kind: &PeekKind,
39-
) -> Result<AccountState, AttestorError> {
40-
todo!()
94+
client_id: &str,
95+
sequence: u64,
96+
slot: u64,
97+
commitment_type: CommitmentType,
98+
) -> Result<[u8; 32], AttestorError> {
99+
let (commitment_pda, _bump) = match commitment_type {
100+
CommitmentType::Packet => {
101+
Commitment::packet_commitment_pda(client_id, sequence, self.router_program_id)
102+
}
103+
CommitmentType::Ack => {
104+
Commitment::packet_ack_pda(client_id, sequence, self.router_program_id)
105+
}
106+
CommitmentType::Receipt => {
107+
Commitment::packet_receipt_pda(client_id, sequence, self.router_program_id)
108+
}
109+
};
110+
111+
let account = self
112+
.client
113+
.get_account_with_commitment(&commitment_pda, CommitmentConfig::finalized())
114+
.await
115+
.map_err(|e| {
116+
AttestorError::ClientError(format!(
117+
"Failed to get commitment account for client_id={}, sequence={}, slot={}: {}",
118+
client_id, sequence, slot, e
119+
))
120+
})?
121+
.value
122+
.ok_or_else(|| {
123+
AttestorError::ClientError(format!(
124+
"Commitment not found for client_id={}, sequence={} at slot={}",
125+
client_id, sequence, slot
126+
))
127+
})?;
128+
129+
let account_data_len = account.data.len();
130+
131+
// The account data should be a 32-byte commitment value
132+
// Skip the 8-byte anchor discriminator
133+
if account_data_len < 8 + 32 {
134+
return Err(AttestorError::ClientError(format!(
135+
"Invalid commitment account data length: got {account_data_len} bytes, expected at least 40",
136+
)));
137+
}
138+
139+
let mut commitment = [0u8; 32];
140+
commitment.copy_from_slice(&account.data[8..40]);
141+
142+
Ok(commitment)
41143
}
42144
}
43145

44146
impl AttestationAdapter for SolanaClient {
45147
async fn get_unsigned_state_attestation_at_height(
46148
&self,
47-
_height: u64,
149+
height: u64,
48150
) -> Result<IAttestorMsgs::StateAttestation, AttestorError> {
49-
todo!()
151+
let timestamp = self.get_timestamp_for_slot_at_height(height).await?;
152+
153+
Ok(IAttestorMsgs::StateAttestation { height, timestamp })
50154
}
51155
async fn get_unsigned_packet_attestation_at_height(
52156
&self,
53-
_packets: crate::Packets,
54-
_height: u64,
55-
_commitment_type: CommitmentType,
157+
packets: Packets,
158+
height: u64,
159+
commitment_type: CommitmentType,
56160
) -> Result<IAttestorMsgs::PacketAttestation, AttestorError> {
57-
todo!()
161+
tracing::debug!("Total Solana packets received: {}", packets.len());
162+
163+
let validated: Vec<_> = packets
164+
.into_iter()
165+
.map(|packet| async move {
166+
let sequence = packet.sequence;
167+
let client_id = packet.sourceClient.clone();
168+
169+
match commitment_type {
170+
CommitmentType::Packet => {
171+
let expected_commitment = packet.commitment();
172+
let commitment = self
173+
.get_historical_packet_commitment(&client_id, sequence, height, commitment_type)
174+
.await
175+
.map_err(|err| {
176+
tracing::error!(
177+
"Packet commitment failed: {:?}, error: {}; expected 0x{}",
178+
packet,
179+
err,
180+
hex::encode(&expected_commitment),
181+
);
182+
err
183+
})?;
184+
185+
if expected_commitment != commitment {
186+
return Err(AttestorError::InvalidCommitment {
187+
reason: format!(
188+
"Packet commitment mismatch for seq={}: expected 0x{}, got 0x{}",
189+
sequence,
190+
hex::encode(expected_commitment),
191+
hex::encode(commitment),
192+
),
193+
});
194+
}
195+
196+
let commitment_path = packet.commitment_path();
197+
let hashed_path = keccak256(commitment_path);
198+
Ok(IAttestorMsgs::PacketCompact {
199+
path: hashed_path,
200+
commitment: commitment.into(),
201+
})
202+
}
203+
204+
CommitmentType::Ack => {
205+
let commitment = self
206+
.get_historical_packet_commitment(&client_id, sequence, height, commitment_type)
207+
.await
208+
.map_err(|err| {
209+
tracing::error!("Ack commitment failed: {:?}, error: {}", packet, err);
210+
err
211+
})?;
212+
213+
let commitment_path = packet.ack_commitment_path();
214+
let hashed_path = keccak256(commitment_path);
215+
Ok(IAttestorMsgs::PacketCompact {
216+
path: hashed_path,
217+
commitment: commitment.into(),
218+
})
219+
}
220+
221+
CommitmentType::Receipt => {
222+
let commitment = self
223+
.get_historical_packet_commitment(&client_id, sequence, height, commitment_type)
224+
.await
225+
.unwrap_or([0u8; 32]);
226+
227+
if commitment != [0u8; 32] {
228+
return Err(AttestorError::InvalidCommitment {
229+
reason: format!(
230+
"Receipt commitment should be zero for seq={}: got 0x{}",
231+
sequence,
232+
hex::encode(commitment)
233+
),
234+
});
235+
}
236+
237+
let commitment_path = packet.receipt_commitment_path();
238+
let hashed_path = keccak256(commitment_path);
239+
Ok(IAttestorMsgs::PacketCompact {
240+
path: hashed_path,
241+
commitment: commitment.into(),
242+
})
243+
}
244+
}
245+
})
246+
.collect::<FuturesUnordered<_>>()
247+
.try_collect()
248+
.await?;
249+
250+
Ok(IAttestorMsgs::PacketAttestation {
251+
height,
252+
packets: validated,
253+
})
58254
}
59255
}

programs/ibc-attestor/src/cli/config.rs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ use crate::adapter::evm::EvmClientConfig;
1717
#[cfg(feature = "cosmos")]
1818
use crate::adapter::cosmos::CosmosClientConfig;
1919

20+
#[cfg(feature = "sol")]
21+
use crate::adapter::sol::SolanaClientConfig;
22+
2023
// Without LazyCell we can't use paths under the hood
2124
#[allow(
2225
clippy::borrow_interior_mutable_const,
@@ -115,10 +118,3 @@ impl ServerConfig {
115118
Level::from_str(&self.log_level).unwrap_or(Level::INFO)
116119
}
117120
}
118-
119-
#[cfg(feature = "sol")]
120-
#[derive(Clone, Debug, Deserialize)]
121-
pub struct SolanaClientConfig {
122-
pub url: String,
123-
pub account_key: String,
124-
}

programs/ibc-attestor/src/server.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ pub async fn run_evm_server(config: AttestorConfig) -> Result<(), anyhow::Error>
8383

8484
#[cfg(feature = "sol")]
8585
pub async fn run_solana_server(config: AttestorConfig) -> Result<(), anyhow::Error> {
86-
let sol = crate::adapter::sol::SolanaClient::_from_config(&config.solana);
86+
let sol = crate::adapter::sol::SolanaClient::from_config(&config.solana)?;
8787
run_server(sol, config).await
8888
}
8989

0 commit comments

Comments
 (0)