Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

receipt support #15

Merged
merged 2 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions crates/sn-trie-proofs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ A comprehensive transaction/receipt inclusion proofs handler for [Starknet trie]
Add dependency `sn-trie-proofs` to your project:

```
sn-trie-proofs = { version= "0.1.1" }
sn-trie-proofs = { git = "https://github.com/HerodotusDev/trie-proofs.git" }
```

## Usage
Expand All @@ -33,9 +33,26 @@ async fn test_build_tx_tree_from_block_3() {
let membership: Membership = handler.verify_proof(0, proof).unwrap();

assert!(membership.is_member());
}
```

- **Transaction Receipt Trie Handler**

let proof = handler.get_proof(1).unwrap();
let membership: Membership = handler.verify_proof(1, proof).unwrap();
Currently we only supporting receipt trie after 0.13.2 version.

```rust
#[tokio::test]
async fn test_build_tx_tree_from_block_4() {
let mut handler = TxReceiptsMptHandler::new(PATHFINDER_URL).unwrap();
// # 0.13.2
let block_number = 99708;
handler
.build_tx_receipts_tree_from_block(block_number)
.await
.unwrap();

let proof = handler.get_proof(0).unwrap();
let membership: Membership = handler.verify_proof(0, proof).unwrap();

assert!(membership.is_member());
}
Expand Down
3 changes: 3 additions & 0 deletions crates/sn-trie-proofs/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,7 @@ pub enum SnTrieError {

#[error("Verification error")]
VerificationError,

#[error("Unsupported protocol")]
UnsupportedProtocol,
}
2 changes: 2 additions & 0 deletions crates/sn-trie-proofs/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
pub mod error;
pub mod rpc;
pub mod tx_hash;
pub mod tx_receipt_hash;
pub mod tx_receipt_trie;
pub mod tx_trie;

pub use error::SnTrieError;
69 changes: 65 additions & 4 deletions crates/sn-trie-proofs/src/rpc.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use serde_json::{json, Value};
use starknet_types_core::felt::Felt;
use starknet_types_rpc::BlockWithTxs;
use starknet_types_rpc::{BlockWithReceipts, BlockWithTxs};

use crate::error::SnTrieError;

Expand Down Expand Up @@ -39,11 +39,40 @@ impl<'a> RpcProvider<'a> {
.clone();

let get_proof_output: BlockWithTxs<Felt> = serde_json::from_value(response_json).unwrap();
let gateway = GatewayProvider::new(self.gateway_url.to_string());
let gateway = GatewayProvider::new(self.gateway_url);
let transaction_commitment = gateway.get_tx_commit(block_number).await.unwrap();

Ok((get_proof_output, transaction_commitment))
}

pub(crate) async fn get_block_transactions_receipts(
&self,
block_number: u64,
) -> Result<(BlockWithReceipts<Felt>, Vec<u64>, String), SnTrieError> {
let request = json!({
"jsonrpc": "2.0",
"id": "0",
"method": "starknet_getBlockWithReceipts",
"params": {
"block_id": {"block_number": block_number},
}
});

let url = self.url;
let provider = reqwest::Client::new();
let response = provider.post(url).json(&request).send().await.unwrap();
let response_json =
serde_json::from_str::<serde_json::Value>(&response.text().await.unwrap()).unwrap()
["result"]
.clone();

let get_proof_output: BlockWithReceipts<Felt> =
serde_json::from_value(response_json).unwrap();
let gateway = GatewayProvider::new(self.gateway_url);
let (l1_gas_vec, receipt_commitment) = gateway.get_l1_gas(block_number).await.unwrap();

Ok((get_proof_output, l1_gas_vec, receipt_commitment))
}
}

pub const GATEWAY_URL: &str = "https://alpha-sepolia.starknet.io";
Expand All @@ -53,8 +82,10 @@ pub struct GatewayProvider {
}

impl GatewayProvider {
pub fn new(base_url: String) -> Self {
Self { base_url }
pub fn new(base_url: impl AsRef<str>) -> Self {
Self {
base_url: base_url.as_ref().to_string(),
}
}

async fn get_tx_commit(&self, block_number: u64) -> Result<String, SnTrieError> {
Expand All @@ -74,4 +105,34 @@ impl GatewayProvider {
Err(SnTrieError::GatewayError(response.status().as_u16()))
}
}

/// Note: This method is only available after 0.13.2
async fn get_l1_gas(&self, block_number: u64) -> Result<(Vec<u64>, String), SnTrieError> {
let url = format!(
"{}/feeder_gateway/get_block?blockNumber={}",
self.base_url, block_number
);

let client = reqwest::Client::new();
let response = client.get(&url).send().await.unwrap();

if response.status().is_success() {
let block_data: Value = response.json().await.unwrap();

println!("{:?}", block_data);
let receipt_commitment = block_data["receipt_commitment"].to_string();
let transaction_receipts = block_data["transaction_receipts"].as_array().unwrap();

let gas_consumed: Vec<u64> = transaction_receipts
.iter()
.filter_map(|receipt| {
receipt["execution_resources"]["total_gas_consumed"]["l1_gas"].as_u64()
})
.collect();

Ok((gas_consumed, receipt_commitment))
} else {
Err(SnTrieError::GatewayError(response.status().as_u16()))
}
}
}
62 changes: 62 additions & 0 deletions crates/sn-trie-proofs/src/tx_receipt_hash.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use starknet_types_core::{
felt::Felt,
hash::{Poseidon, StarkHash},
};
use starknet_types_rpc::{MsgToL1, TransactionAndReceipt};

pub fn calculate_receipt_hash(receipt: &TransactionAndReceipt<Felt>, l1_gas: u64) -> Felt {
let mut hash_chains = vec![];
let common_properties = match &receipt.receipt {
starknet_types_rpc::TxnReceipt::Declare(tx_receipt) => {
&tx_receipt.common_receipt_properties
}
starknet_types_rpc::TxnReceipt::Deploy(tx_receipt) => &tx_receipt.common_receipt_properties,
starknet_types_rpc::TxnReceipt::DeployAccount(tx_receipt) => {
&tx_receipt.common_receipt_properties
}
starknet_types_rpc::TxnReceipt::Invoke(tx_receipt) => &tx_receipt.common_receipt_properties,
starknet_types_rpc::TxnReceipt::L1Handler(tx_receipt) => {
&tx_receipt.common_receipt_properties
}
};

hash_chains.push(common_properties.transaction_hash);
hash_chains.push(common_properties.actual_fee.amount);
hash_chains.push(calculate_messages_sent_hash(
&common_properties.messages_sent,
));
// TODO: calculate_revert_reason_hash
hash_chains.push(Felt::ZERO);

// chain_gas_consumed
hash_chains.push(Felt::ZERO);
hash_chains.push(l1_gas.into());
hash_chains.push(
common_properties
.execution_resources
.data_availability
.l1_data_gas
.into(),
);

Poseidon::hash_array(&hash_chains)
}

pub fn calculate_messages_sent_hash(messages: &Vec<MsgToL1<Felt>>) -> Felt {
let mut hash_chains = vec![];
hash_chains.push(Felt::from(messages.len() as u64));
for message in messages {
hash_chains.push(message.from_address);
hash_chains.push(message.to_address);
hash_chains.push(Felt::from(message.payload.len()));
for p in message.payload.clone() {
hash_chains.push(p);
}
}
Poseidon::hash_array(&hash_chains)
}

// Returns starknet-keccak of the revert reason ASCII string, or 0 if the transaction succeeded.
// pub fn calculate_revert_reason_hash(execution_status: String, revert_reason: String) -> Felt {

// }
183 changes: 183 additions & 0 deletions crates/sn-trie-proofs/src/tx_receipt_trie.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
use crate::error::SnTrieError;
use crate::tx_receipt_hash::calculate_receipt_hash;
use sn_merkle_trie::conversion::from_u64_to_bits;
use sn_merkle_trie::node::TrieNode;
use sn_merkle_trie::transaction::TransactionMerkleTree;
use sn_merkle_trie::{Membership, MerkleTree};
use starknet_types_core::hash::{Poseidon, StarkHash};
use starknet_types_core::{felt::Felt, hash::Pedersen};

use super::rpc::RpcProvider;
use super::rpc::GATEWAY_URL;

/// Note: only implemented after 0.13.2 version
pub struct TxReceiptsMptHandler<'a> {
provider: RpcProvider<'a>,
trie: Option<TxReceiptsMpt>,
}

pub struct TxReceiptsMpt {
pub trie: TransactionMerkleTree,
pub elements: Vec<Felt>,
root: Felt,
root_idx: u64,
}

impl<'a> TxReceiptsMptHandler<'a> {
pub fn new(rpc_url: &'a str) -> Result<Self, SnTrieError> {
let provider = RpcProvider::new(rpc_url, GATEWAY_URL);
Ok(Self {
provider,
trie: None,
})
}

/// Build
pub async fn build_tx_receipts_tree_from_block(
&mut self,
block_number: u64,
) -> Result<(), SnTrieError> {
let (txs, vec_l1_gas, expected_commit) = self
.provider
.get_block_transactions_receipts(block_number)
.await
.expect("rpc fetch failed");
let protocol = txs.block_header.starknet_version;

if protocol.as_str() < "0.13.2" {
return Err(SnTrieError::UnsupportedProtocol);
}

let tx_final_hashes: Vec<Felt> = txs
.transactions
.iter()
.zip(vec_l1_gas.iter())
.map(|(t, &l1_gas)| calculate_receipt_hash(t, l1_gas))
.collect();

self.build_trie(tx_final_hashes, &expected_commit, &protocol)?;
Ok(())
}

pub fn build_trie(
&mut self,
txs: Vec<Felt>,
expected_commit: &str,
protocol: &str,
) -> Result<(), SnTrieError> {
let trie = if protocol >= "0.13.2" {
self.build_trie_generic::<Poseidon>(txs, expected_commit)?
} else {
self.build_trie_generic::<Pedersen>(txs, expected_commit)?
};

self.trie = Some(trie);
Ok(())
}

fn build_trie_generic<H: StarkHash + 'static>(
&self,
txs: Vec<Felt>,
expected_commit: &str,
) -> Result<TxReceiptsMpt, SnTrieError> {
let mut tree = if std::any::TypeId::of::<H>() == std::any::TypeId::of::<Poseidon>() {
TransactionMerkleTree::Poseidon(MerkleTree::default())
} else {
TransactionMerkleTree::Pedersen(MerkleTree::default())
};

for (idx, hash) in txs.clone().into_iter().enumerate() {
let idx: u64 = idx.try_into().unwrap();
let key = from_u64_to_bits(idx);
tree.set(key, hash).expect("set failed");
}

let (root, root_idx) = tree.commit().expect("commit failed");

let cleaned_expected_commit = expected_commit.trim_matches('"').to_string();
assert_eq!(cleaned_expected_commit, root.to_hex_string());
if cleaned_expected_commit != root.to_hex_string() {
return Err(SnTrieError::InvalidCommitment);
}

Ok(TxReceiptsMpt {
trie: tree,
elements: txs,
root,
root_idx,
})
}

pub fn get_proof(&self, tx_index: u64) -> Result<Vec<TrieNode>, SnTrieError> {
let trie = self.trie.as_ref().ok_or(SnTrieError::TrieNotFound)?;
let root_idx = trie.root_idx;
let proof = trie
.trie
.get_proof(root_idx, from_u64_to_bits(tx_index))
.unwrap()
.ok_or(SnTrieError::TrieNotFound)?;
Ok(proof)
}

pub fn verify_proof(
&self,
tx_index: u64,
proof: Vec<TrieNode>,
) -> Result<Membership, SnTrieError> {
let trie = self.trie.as_ref().ok_or(SnTrieError::TrieNotFound)?;
let value = trie
.elements
.get(tx_index as usize)
.ok_or(SnTrieError::InvalidTxIndex)?;

let result = trie
.trie
.verify_proof(trie.root, &from_u64_to_bits(tx_index), *value, &proof)
.ok_or(SnTrieError::VerificationError)?;
Ok(result)
}

pub fn get_root_idx(&self) -> Result<u64, SnTrieError> {
let trie = self.trie.as_ref().ok_or(SnTrieError::TrieNotFound)?;
let root_idx = trie.root_idx;
Ok(root_idx)
}
}

#[cfg(test)]
mod tests {
use super::*;

const PATHFINDER_URL: &str = "https://pathfinder.sepolia.iosis.tech/";

#[tokio::test]
async fn test_build_tx_tree_from_block_4() {
let mut handler = TxReceiptsMptHandler::new(PATHFINDER_URL).unwrap();
// # 0.13.2
let block_number = 99708;
handler
.build_tx_receipts_tree_from_block(block_number)
.await
.unwrap();

let proof = handler.get_proof(0).unwrap();
let membership: Membership = handler.verify_proof(0, proof).unwrap();

assert!(membership.is_member());

let proof = handler.get_proof(1).unwrap();
let membership: Membership = handler.verify_proof(1, proof).unwrap();

assert!(membership.is_member());

let proof = handler.get_proof(2).unwrap();
let membership: Membership = handler.verify_proof(2, proof).unwrap();

assert!(membership.is_member());

let proof = handler.get_proof(3).unwrap();
let membership: Membership = handler.verify_proof(3, proof).unwrap();

assert!(membership.is_member());
}
}