Skip to content

Commit 501e3e4

Browse files
committed
add sovereign sdk integration
1 parent 877881d commit 501e3e4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+4998
-138
lines changed

rust/main/Cargo.lock

Lines changed: 214 additions & 104 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rust/main/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ members = [
1111
"chains/hyperlane-fuel",
1212
"chains/hyperlane-radix",
1313
"chains/hyperlane-sealevel",
14+
"chains/hyperlane-sovereign",
1415
"chains/hyperlane-starknet",
1516
"ethers-prometheus",
1617
"hyperlane-base",

rust/main/agents/scraper/migration/src/m20230309_000001_create_table_domain.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -544,7 +544,24 @@ const DOMAINS: &[RawDomain] = &[
544544
chain_id: 9913375,
545545
is_test_net: true,
546546
is_deprecated: false,
547-
}, // ---------- End: E2E tests chains ----------------
547+
},
548+
RawDomain {
549+
name: "sovrollup0",
550+
token: "SOV",
551+
domain: 50001,
552+
chain_id: 50001,
553+
is_test_net: true,
554+
is_deprecated: false,
555+
},
556+
RawDomain {
557+
name: "sovrollup1",
558+
token: "SOV",
559+
domain: 50002,
560+
chain_id: 50002,
561+
is_test_net: true,
562+
is_deprecated: false,
563+
},
564+
// ---------- End: E2E tests chains ----------------
548565
];
549566

550567
#[derive(DeriveMigrationName)]

rust/main/agents/validator/src/reorg_reporter.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ impl LatestCheckpointReorgReporter {
9797
origin: &HyperlaneDomain,
9898
) -> Vec<(Url, ValidatorSettings)> {
9999
use ChainConnectionConf::{
100-
Cosmos, CosmosNative, Ethereum, Fuel, Radix, Sealevel, Starknet,
100+
Cosmos, CosmosNative, Ethereum, Fuel, Radix, Sealevel, Sovereign, Starknet,
101101
};
102102

103103
let chain_conf = settings
@@ -149,6 +149,13 @@ impl LatestCheckpointReorgReporter {
149149
Radix(updated_conn)
150150
})
151151
}
152+
Sovereign(conn) => {
153+
Self::map_urls_to_connections(vec![conn.url.clone()], conn, |conn, url| {
154+
let mut updated_conn = conn.clone();
155+
updated_conn.url = url;
156+
Sovereign(updated_conn)
157+
})
158+
}
152159
};
153160

154161
chain_conn_confs
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
[package]
2+
name = "hyperlane-sovereign"
3+
version = "0.1.0"
4+
edition.workspace = true
5+
6+
[dependencies]
7+
hyperlane-core = { path = "../../hyperlane-core", features = ["async"] }
8+
hyperlane-operation-verifier = { path = "../../applications/hyperlane-operation-verifier" }
9+
hyperlane-warp-route = { path = "../../applications/hyperlane-warp-route" }
10+
11+
anyhow.workspace = true
12+
async-trait.workspace = true
13+
base64.workspace = true
14+
bech32.workspace = true
15+
bytes.workspace = true
16+
derive-new.workspace = true
17+
ethers.workspace = true
18+
futures.workspace = true
19+
k256.workspace = true
20+
reqwest.workspace = true
21+
serde.workspace = true
22+
serde_json.workspace = true
23+
sha2.workspace = true
24+
sha3.workspace = true
25+
tokio = { workspace = true, features = ["fs", "macros"] }
26+
tracing.workspace = true
27+
url.workspace = true
28+
hex.workspace = true
29+
num-traits.workspace = true
30+
31+
ed25519-dalek = "2.1.1"
32+
bs58 = { version = "0.5.1", default-features = false, features = [
33+
"std",
34+
"alloc",
35+
] }
36+
sov-universal-wallet = { git = "https://@github.com/Sovereign-Labs/sovereign-sdk.git", branch = "nightly", features = [
37+
"serde",
38+
] }
39+
tokio-tungstenite = "0.23"
40+
tokio-retry = "0.3.0"
41+
42+
[features]
43+
default = []
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
use std::io::Cursor;
2+
3+
use async_trait::async_trait;
4+
use derive_new::new;
5+
use hyperlane_core::{Decode, HyperlaneMessage, H256, U256};
6+
use hyperlane_operation_verifier::{
7+
ApplicationOperationVerifier, ApplicationOperationVerifierReport,
8+
};
9+
use hyperlane_warp_route::TokenMessage;
10+
11+
use crate::signers::sovereign::SOV_HEX_ADDRESS_LEADING_ZEROS;
12+
13+
const WARP_ROUTE_MARKER: &str = "/";
14+
15+
/// Application operation verifier for Sovereign
16+
#[derive(new)]
17+
pub struct SovereignApplicationOperationVerifier {}
18+
19+
#[async_trait]
20+
impl ApplicationOperationVerifier for SovereignApplicationOperationVerifier {
21+
async fn verify(
22+
&self,
23+
app_context: &Option<String>,
24+
message: &HyperlaneMessage,
25+
) -> Option<ApplicationOperationVerifierReport> {
26+
use ApplicationOperationVerifierReport::{MalformedMessage, ZeroAmount};
27+
tracing::trace!(
28+
?app_context,
29+
?message,
30+
"Sovereign application operation verifier",
31+
);
32+
33+
let context = match app_context {
34+
Some(c) => c,
35+
None => return None,
36+
};
37+
38+
if !context.contains(WARP_ROUTE_MARKER) {
39+
return None;
40+
}
41+
42+
// Starting from this point we assume that we are in a warp route context
43+
44+
let mut reader = Cursor::new(message.body.as_slice());
45+
let token_message = match TokenMessage::read_from(&mut reader) {
46+
Ok(m) => m,
47+
Err(_) => return Some(MalformedMessage(message.clone())),
48+
};
49+
50+
if !has_enough_leading_zeroes(token_message.recipient()) {
51+
return Some(MalformedMessage(message.clone()));
52+
}
53+
54+
if token_message.amount() == U256::zero() {
55+
return Some(ZeroAmount);
56+
}
57+
58+
None
59+
}
60+
}
61+
62+
// this uses the ed25519 addresses check as it has less leading zeros
63+
fn has_enough_leading_zeroes(address: H256) -> bool {
64+
address
65+
.as_bytes()
66+
.iter()
67+
.take(SOV_HEX_ADDRESS_LEADING_ZEROS)
68+
.all(|b| *b == 0)
69+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
use std::{ops::RangeInclusive, str::FromStr};
2+
3+
use async_trait::async_trait;
4+
use hyperlane_core::{ChainResult, Indexed, Indexer, LogMeta, SequenceAwareIndexer, H256, H512};
5+
use serde::Deserialize;
6+
7+
use crate::{indexer::SovIndexer, types::TxEvent, SovereignProvider};
8+
9+
/// Struct that retrieves delivery event data for a Sovereign Mailbox.
10+
#[derive(Debug, Clone)]
11+
pub struct SovereignDeliveryIndexer {
12+
provider: SovereignProvider,
13+
}
14+
15+
impl SovereignDeliveryIndexer {
16+
/// Create a new `SovereignDeliveryIndexer`.
17+
pub fn new(provider: SovereignProvider) -> ChainResult<Self> {
18+
Ok(SovereignDeliveryIndexer { provider })
19+
}
20+
}
21+
22+
#[derive(Debug, Clone, Deserialize)]
23+
struct ProcessEvent {
24+
process_id: ProcessEventInner,
25+
}
26+
27+
#[derive(Debug, Clone, Deserialize)]
28+
struct ProcessEventInner {
29+
id: String,
30+
}
31+
32+
#[async_trait]
33+
impl crate::indexer::SovIndexer<H256> for SovereignDeliveryIndexer {
34+
const EVENT_KEY: &'static str = "Mailbox/ProcessId";
35+
36+
fn provider(&self) -> &SovereignProvider {
37+
&self.provider
38+
}
39+
40+
async fn latest_sequence(&self, at_slot: Option<u64>) -> ChainResult<Option<u32>> {
41+
let sequence = self.provider().get_count(at_slot).await?;
42+
Ok(Some(sequence))
43+
}
44+
45+
fn decode_event(&self, event: &TxEvent) -> ChainResult<H256> {
46+
let evt: ProcessEvent = serde_json::from_value(event.value.clone())?;
47+
Ok(H256::from_str(&evt.process_id.id)?)
48+
}
49+
}
50+
51+
#[async_trait]
52+
impl SequenceAwareIndexer<H256> for SovereignDeliveryIndexer {
53+
async fn latest_sequence_count_and_tip(&self) -> ChainResult<(Option<u32>, u32)> {
54+
<Self as SovIndexer<H256>>::latest_sequence_count_and_tip(self).await
55+
}
56+
}
57+
58+
#[async_trait]
59+
impl Indexer<H256> for SovereignDeliveryIndexer {
60+
async fn fetch_logs_in_range(
61+
&self,
62+
range: RangeInclusive<u32>,
63+
) -> ChainResult<Vec<(Indexed<H256>, LogMeta)>> {
64+
<Self as SovIndexer<H256>>::fetch_logs_in_range(self, range).await
65+
}
66+
67+
async fn get_finalized_block_number(&self) -> ChainResult<u32> {
68+
<Self as SovIndexer<H256>>::get_finalized_block_number(self).await
69+
}
70+
71+
async fn fetch_logs_by_tx_hash(
72+
&self,
73+
tx_hash: H512,
74+
) -> ChainResult<Vec<(Indexed<H256>, LogMeta)>> {
75+
<Self as SovIndexer<H256>>::fetch_logs_by_tx_hash(self, tx_hash).await
76+
}
77+
}
78+
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
use std::fmt::Debug;
2+
use std::ops::RangeInclusive;
3+
4+
use async_trait::async_trait;
5+
use futures::stream::{self, FuturesOrdered, TryStreamExt};
6+
use hyperlane_core::{ChainResult, Indexed, Indexer, LogMeta, SequenceAwareIndexer, H256, H512};
7+
8+
use crate::types::{Tx, TxEvent};
9+
use crate::SovereignProvider;
10+
11+
// SovIndexer is a trait that contains default implementations for indexing
12+
// various different event types on the Sovereign chain to reduce code duplication in
13+
// e.g. SovereignMailboxIndexer, SovereignInterchainGasPaymasterIndexer, etc.
14+
#[async_trait]
15+
pub trait SovIndexer<T>: Indexer<T> + SequenceAwareIndexer<T>
16+
where
17+
T: Into<Indexed<T>> + Debug + Clone + Send,
18+
{
19+
const EVENT_KEY: &'static str;
20+
21+
fn provider(&self) -> &SovereignProvider;
22+
23+
fn decode_event(&self, event: &TxEvent) -> ChainResult<T>;
24+
25+
async fn latest_sequence(&self, at_slot: Option<u64>) -> ChainResult<Option<u32>>;
26+
27+
// Default implementation of Indexer<T>
28+
async fn fetch_logs_in_range(
29+
&self,
30+
range: RangeInclusive<u32>,
31+
) -> ChainResult<Vec<(Indexed<T>, LogMeta)>> {
32+
let logs = range
33+
.map(|slot_num| async move {
34+
let slot = self.provider().get_specified_slot(slot_num.into()).await?;
35+
ChainResult::Ok(stream::iter(
36+
slot.batches
37+
.into_iter()
38+
.flat_map(|batch| batch.txs)
39+
.map(move |tx| self.process_tx(&tx, slot.number, slot.hash)),
40+
))
41+
})
42+
.collect::<FuturesOrdered<_>>()
43+
.try_flatten()
44+
.try_collect::<Vec<_>>()
45+
.await?
46+
.concat();
47+
48+
Ok(logs)
49+
}
50+
51+
async fn get_finalized_block_number(&self) -> ChainResult<u32> {
52+
let latest_slot = self.provider().get_finalized_slot().await?;
53+
Ok(latest_slot.try_into().expect("Slot number overflowed u32"))
54+
}
55+
56+
/// Get the transaction by hash. Sovereign Tx hashes are represented as H256,
57+
/// so the input needs to be padded with 0's
58+
async fn fetch_logs_by_tx_hash(
59+
&self,
60+
tx_hash: H512,
61+
) -> ChainResult<Vec<(Indexed<T>, LogMeta)>> {
62+
if tx_hash.0[0..32] != [0; 32] {
63+
return Err(custom_err!(
64+
"Invalid sovereign transaction id, should have 32 bytes: {tx_hash:?}"
65+
));
66+
}
67+
let tx_hash = H256(tx_hash[32..].try_into().expect("Must be 32 bytes"));
68+
69+
let provider = self.provider();
70+
let tx = provider.get_tx_by_hash(tx_hash).await?;
71+
let batch = provider.get_batch(tx.batch_number).await?;
72+
let slot = provider.get_specified_slot(batch.slot_number).await?;
73+
74+
self.process_tx(&tx, slot.number, slot.hash)
75+
}
76+
77+
// Default implementation of SequenceAwareIndexer<T>
78+
async fn latest_sequence_count_and_tip(&self) -> ChainResult<(Option<u32>, u32)> {
79+
let finalized_slot = self.provider().get_finalized_slot().await?;
80+
let sequence = self.latest_sequence(Some(finalized_slot)).await?;
81+
82+
Ok((
83+
sequence,
84+
finalized_slot
85+
.try_into()
86+
.map_err(|_| custom_err!("Slot number overflowed"))?,
87+
))
88+
}
89+
90+
// Helper function to process a single transaction
91+
fn process_tx(&self, tx: &Tx, slot_num: u64, slot_hash: H256) -> ChainResult<Vec<(Indexed<T>, LogMeta)>> {
92+
tx.events
93+
.iter()
94+
.filter(|ev| ev.key == Self::EVENT_KEY)
95+
.map(|ev| self.process_event(tx, ev, slot_num, slot_hash))
96+
.collect()
97+
}
98+
99+
// Helper function to process a single event
100+
fn process_event(
101+
&self,
102+
tx: &Tx,
103+
event: &TxEvent,
104+
slot_num: u64,
105+
slot_hash: H256,
106+
) -> ChainResult<(Indexed<T>, LogMeta)> {
107+
let decoded_event = self.decode_event(event)?;
108+
109+
let meta = LogMeta {
110+
// NOTE: sovereign logs are emitted by modules, not contracts, so we use a dummy address
111+
address: H256::default(),
112+
block_number: slot_num,
113+
block_hash: slot_hash,
114+
transaction_id: tx.hash.into(),
115+
// NOTE: this diverges from the Ethereum behavior, as for sovereign, those numbers are
116+
// global, and for Ethereum, they are block local. However, deducing block-local numbers
117+
// for transactions fetched by hash would require pulling the whole slot data, which means
118+
// a lot of overhead
119+
transaction_index: tx.number,
120+
log_index: event.number.into(),
121+
};
122+
123+
Ok((decoded_event.into(), meta))
124+
}
125+
}

0 commit comments

Comments
 (0)