forked from axelarnetwork/axelar-amplifier
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add CallContract starknet event parsing logic (#19)
- Loading branch information
Showing
5 changed files
with
538 additions
and
372 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,282 @@ | ||
use std::num::TryFromIntError; | ||
|
||
use ethers::types::H256; | ||
use starknet_core::types::ValueOutOfRangeError; | ||
use starknet_core::utils::{parse_cairo_short_string, ParseCairoShortStringError}; | ||
use thiserror::Error; | ||
|
||
use crate::starknet::events::EventType; | ||
use crate::starknet::types::byte_array::{ByteArray, ByteArrayError}; | ||
use crate::types::Hash; | ||
|
||
/// This is the event emitted by the gateway cairo contract on Starknet, | ||
/// when the call_contract method is called from a third party. | ||
#[derive(Debug, PartialEq)] | ||
pub struct ContractCallEvent { | ||
pub destination_address: String, | ||
pub destination_chain: String, | ||
pub source_address: String, | ||
pub payload_hash: Hash, | ||
} | ||
|
||
/// An error, representing failure to convert/parse a starknet event | ||
/// to some specific event. | ||
#[derive(Error, Debug)] | ||
pub enum ContractCallError { | ||
#[error("Invalid ContractCall event: {0}")] | ||
InvalidEvent(String), | ||
#[error("Cairo short string parse error: {0}")] | ||
Cairo(#[from] ParseCairoShortStringError), | ||
#[error("FieldElement operation errored with out of range: {0}")] | ||
FeltOutOfRange(#[from] ValueOutOfRangeError), | ||
#[error("Failed int conversion: {0}")] | ||
TryFromConversion(#[from] TryFromIntError), | ||
#[error("Event data/keys array index is out of bounds")] | ||
OutOfBound, | ||
#[error("ByteArray type error: {0}")] | ||
ByteArray(#[from] ByteArrayError), | ||
} | ||
|
||
impl TryFrom<starknet_core::types::Event> for ContractCallEvent { | ||
type Error = ContractCallError; | ||
|
||
fn try_from(starknet_event: starknet_core::types::Event) -> Result<Self, Self::Error> { | ||
if starknet_event.keys.len() != 2 { | ||
return Err(ContractCallError::InvalidEvent( | ||
"ContractCall should have exactly 2 event keys - event_type and destination_chain" | ||
.to_owned(), | ||
)); | ||
} | ||
|
||
// first key is always the event type | ||
let event_type_felt = starknet_event.keys[0]; | ||
if !matches!( | ||
EventType::parse(event_type_felt), | ||
Some(EventType::ContractCall) | ||
) { | ||
return Err(ContractCallError::InvalidEvent( | ||
"not a ContractCall event".to_owned(), | ||
)); | ||
} | ||
|
||
// destination_chain is the second key in the event keys list (the first key | ||
// defined from the event) | ||
// | ||
// This field, should not exceed 252 bits (a felt's length) | ||
let destination_chain = parse_cairo_short_string(&starknet_event.keys[1])?; | ||
|
||
// source_address represents the original caller of the `call_contract` gateway | ||
// method. It is the first field in data, by the order defined in the | ||
// event. | ||
// | ||
// TODO: Not sure if `064x` is the correct formatting. Maybe we should calculate | ||
// the pedersen hash of the felt as described here, to get the actual address, | ||
// although I'm not sure that we can do it as described here: | ||
// https://docs.starknet.io/documentation/architecture_and_concepts/Smart_Contracts/contract-address/ | ||
let source_address = format!("0x{:064x}", starknet_event.data[0]); | ||
|
||
// destination_contract_address (ByteArray) is composed of FieldElements | ||
// from the second element to elemet X. | ||
let destination_address_chunks_count_felt = starknet_event.data[1]; | ||
let da_chunks_count: usize = u8::try_from(destination_address_chunks_count_felt)?.into(); | ||
|
||
// It's + 3, because we need to offset the 0th element, pending_word and | ||
// pending_word_count, in addition to all chunks (da_chunks_count_usize) | ||
let da_elements_start_index: usize = 1; | ||
let da_elements_end_index: usize = da_chunks_count.wrapping_add(3); | ||
let destination_address_byte_array: ByteArray = ByteArray::try_from( | ||
starknet_event | ||
.data | ||
.get(da_elements_start_index..=da_elements_end_index) | ||
.ok_or(ContractCallError::OutOfBound)? | ||
.to_vec(), | ||
)?; | ||
let destination_address = destination_address_byte_array.try_to_string()?; | ||
|
||
// payload_hash is a keccak256, which is a combination of two felts (chunks) | ||
// - first felt contains the 128 least significat bits (LSB) | ||
// - second felt contains the 128 most significat bits (MSG) | ||
let ph_chunk1_index: usize = da_elements_end_index.wrapping_add(1); | ||
let ph_chunk2_index: usize = ph_chunk1_index.wrapping_add(1); | ||
let mut payload_hash = [0; 32]; | ||
let lsb: [u8; 32] = starknet_event | ||
.data | ||
.get(ph_chunk1_index) | ||
.ok_or(ContractCallError::InvalidEvent( | ||
"payload_hash chunk 1 out of range".to_owned(), | ||
))? | ||
.to_bytes_be(); | ||
let msb: [u8; 32] = starknet_event | ||
.data | ||
.get(ph_chunk2_index) | ||
.ok_or(ContractCallError::InvalidEvent( | ||
"payload_hash chunk 2 out of range".to_owned(), | ||
))? | ||
.to_bytes_be(); | ||
|
||
// most significat bits, go before least significant bits for u256 construction | ||
// check - https://docs.starknet.io/documentation/architecture_and_concepts/Smart_Contracts/serialization_of_Cairo_types/#serialization_in_u256_values | ||
payload_hash[..16].copy_from_slice(&msb[16..]); | ||
payload_hash[16..].copy_from_slice(&lsb[16..]); | ||
|
||
Ok(ContractCallEvent { | ||
destination_address, | ||
destination_chain, | ||
source_address, | ||
payload_hash: H256::from_slice(&payload_hash), | ||
}) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use std::str::FromStr; | ||
|
||
use ethers::types::H256; | ||
use starknet_core::types::FieldElement; | ||
use starknet_core::utils::starknet_keccak; | ||
|
||
use super::ContractCallEvent; | ||
use crate::starknet::events::contract_call::ContractCallError; | ||
|
||
#[test] | ||
fn destination_address_chunks_offset_out_of_range() { | ||
let mut starknet_event = get_dummy_event(); | ||
// longer chunk, which offsets the destination_address byte array out of range | ||
starknet_event.data[1] = FieldElement::from_str( | ||
"0x0000000000000000000000000000000000000000000000000000000000000001", | ||
) | ||
.unwrap(); | ||
|
||
let event = ContractCallEvent::try_from(starknet_event).unwrap_err(); | ||
assert!(matches!(event, ContractCallError::ByteArray(_))); | ||
} | ||
|
||
#[test] | ||
fn destination_address_chunks_count_too_long() { | ||
let mut starknet_event = get_dummy_event(); | ||
// too long for u32 | ||
starknet_event.data[1] = FieldElement::MAX; | ||
|
||
let event = ContractCallEvent::try_from(starknet_event).unwrap_err(); | ||
assert!(matches!(event, ContractCallError::FeltOutOfRange(_))); | ||
} | ||
|
||
#[test] | ||
fn invalid_dest_chain() { | ||
let mut starknet_event = get_dummy_event(); | ||
// too long for Cairo long string too long | ||
starknet_event.keys[1] = FieldElement::MAX; | ||
|
||
let event = ContractCallEvent::try_from(starknet_event).unwrap_err(); | ||
assert!(matches!(event, ContractCallError::Cairo(_))); | ||
} | ||
|
||
#[test] | ||
fn more_than_2_keys() { | ||
// the payload is the word "hello" | ||
let mut starknet_event = get_dummy_event(); | ||
starknet_event | ||
.keys | ||
.push(starknet_keccak("additional_element".as_bytes())); | ||
|
||
let event = ContractCallEvent::try_from(starknet_event).unwrap_err(); | ||
assert!(matches!(event, ContractCallError::InvalidEvent(_))); | ||
} | ||
|
||
#[test] | ||
fn wrong_event_type() { | ||
// the payload is the word "hello" | ||
let mut starknet_event = get_dummy_event(); | ||
starknet_event.keys[0] = starknet_keccak("NOTContractCall".as_bytes()); | ||
|
||
let event = ContractCallEvent::try_from(starknet_event).unwrap_err(); | ||
assert!(matches!(event, ContractCallError::InvalidEvent(_))); | ||
} | ||
|
||
#[test] | ||
fn valid_call_contract_event() { | ||
// the payload is the word "hello" | ||
let starknet_event = get_dummy_event(); | ||
|
||
let event = ContractCallEvent::try_from(starknet_event).unwrap(); | ||
assert_eq!( | ||
event, | ||
ContractCallEvent { | ||
destination_address: String::from("hello"), | ||
destination_chain: String::from("destination_chain"), | ||
source_address: String::from( | ||
"0x00b3ff441a68610b30fd5e2abbf3a1548eb6ba6f3559f2862bf2dc757e5828ca" | ||
), | ||
payload_hash: H256::from_slice(&[ | ||
28u8, 138, 255, 149, 6, 133, 194, 237, 75, 195, 23, 79, 52, 114, 40, 123, 86, | ||
217, 81, 123, 156, 148, 129, 39, 49, 154, 9, 167, 163, 109, 234, 200 | ||
]) | ||
} | ||
); | ||
} | ||
|
||
fn get_dummy_event() -> starknet_core::types::Event { | ||
starknet_core::types::Event { | ||
// I think it's a pedersen hash, but we don't use it, so any value should do | ||
from_address: starknet_keccak("some_from_address".as_bytes()), | ||
keys: vec![ | ||
starknet_keccak("ContractCall".as_bytes()), | ||
FieldElement::from_str( | ||
"0x00000000000000000000000000000064657374696e6174696f6e5f636861696e", | ||
) | ||
.unwrap(), | ||
], | ||
data: vec![ | ||
FieldElement::from_str( | ||
"0xb3ff441a68610b30fd5e2abbf3a1548eb6ba6f3559f2862bf2dc757e5828ca", | ||
) | ||
.unwrap(), | ||
FieldElement::from_str( | ||
"0x0000000000000000000000000000000000000000000000000000000000000000", | ||
) | ||
.unwrap(), | ||
FieldElement::from_str( | ||
"0x00000000000000000000000000000000000000000000000000000068656c6c6f", | ||
) | ||
.unwrap(), | ||
FieldElement::from_str( | ||
"0x0000000000000000000000000000000000000000000000000000000000000005", | ||
) | ||
.unwrap(), | ||
FieldElement::from_str( | ||
"0x0000000000000000000000000000000056d9517b9c948127319a09a7a36deac8", | ||
) | ||
.unwrap(), | ||
FieldElement::from_str( | ||
"0x000000000000000000000000000000001c8aff950685c2ed4bc3174f3472287b", | ||
) | ||
.unwrap(), | ||
FieldElement::from_str( | ||
"0x0000000000000000000000000000000000000000000000000000000000000005", | ||
) | ||
.unwrap(), | ||
FieldElement::from_str( | ||
"0x0000000000000000000000000000000000000000000000000000000000000068", | ||
) | ||
.unwrap(), | ||
FieldElement::from_str( | ||
"0x0000000000000000000000000000000000000000000000000000000000000065", | ||
) | ||
.unwrap(), | ||
FieldElement::from_str( | ||
"0x000000000000000000000000000000000000000000000000000000000000006c", | ||
) | ||
.unwrap(), | ||
FieldElement::from_str( | ||
"0x000000000000000000000000000000000000000000000000000000000000006c", | ||
) | ||
.unwrap(), | ||
FieldElement::from_str( | ||
"0x000000000000000000000000000000000000000000000000000000000000006f", | ||
) | ||
.unwrap(), | ||
], | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
use std::sync::OnceLock; | ||
|
||
use starknet_core::types::FieldElement; | ||
use starknet_core::utils::starknet_keccak; | ||
|
||
pub mod contract_call; | ||
|
||
// Since a keccak hash over a string is a deterministic operation, | ||
// we can use `OnceLock` to eliminate useless hash calculations. | ||
static CALL_CONTRACT_FELT: OnceLock<FieldElement> = OnceLock::new(); | ||
|
||
/// All Axelar event types supported by starknet | ||
#[derive(Debug)] | ||
pub enum EventType { | ||
ContractCall, | ||
} | ||
|
||
impl EventType { | ||
fn parse(event_type_felt: FieldElement) -> Option<Self> { | ||
let contract_call_type = | ||
CALL_CONTRACT_FELT.get_or_init(|| starknet_keccak("ContractCall".as_bytes())); | ||
|
||
if event_type_felt == *contract_call_type { | ||
Some(EventType::ContractCall) | ||
} else { | ||
None | ||
} | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod event_type_tests { | ||
use starknet_core::utils::starknet_keccak; | ||
|
||
use crate::starknet::events::EventType; | ||
|
||
#[test] | ||
fn parse_contract_call() { | ||
let contract_call_felt = starknet_keccak("ContractCall".as_bytes()); | ||
assert!(matches!( | ||
EventType::parse(contract_call_felt), | ||
Some(EventType::ContractCall) | ||
)); | ||
} | ||
|
||
#[test] | ||
fn parse_unknown_event() { | ||
let contract_call_felt = starknet_keccak("UnknownEvent".as_bytes()); | ||
assert!(EventType::parse(contract_call_felt).is_none()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
pub mod events; | ||
pub mod types; |
Oops, something went wrong.