Skip to content

Commit 56ee2b4

Browse files
kariyclaude
andauthored
fix(persistent-tee): use Poseidon L1->L2 message hash for Starknet settlement (#77)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 8a5dc79 commit 56ee2b4

1 file changed

Lines changed: 33 additions & 15 deletions

File tree

bin/persistent-tee/src/settlement.rs

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ use anyhow::Result;
66
use cainome::cairo_serde::{CairoSerde, ContractAddress};
77
use katana_tee_client::{OnchainProof, StarknetCalldata};
88
use piltover::{MessageToAppchain, MessageToStarknet, PiltoverInput, TEEInput};
9-
use sha3::{Digest, Keccak256};
109
use starknet::{
1110
accounts::{Account, ExecutionEncoding, SingleOwnerAccount},
1211
core::types::{BlockId, BlockTag, Call, Felt, FunctionCall, TransactionReceipt},
1312
macros::selector,
1413
providers::{jsonrpc::HttpTransport, JsonRpcClient, Provider},
1514
signers::{LocalWallet, SigningKey},
1615
};
16+
use starknet_types_core::hash::{Poseidon, StarkHash};
1717
use tokio::sync::mpsc::{Receiver, Sender};
1818
use tracing::{debug, error};
1919
use url::Url;
@@ -27,22 +27,40 @@ use saya_core::{
2727

2828
const POLLING_INTERVAL: Duration = Duration::from_secs(2);
2929

30-
/// Computes the Starknet L1→L2 message hash using keccak256.
30+
/// Computes the L1→L2 message hash for a Starknet-settled appchain.
3131
///
32-
/// Matches the Ethereum StarknetMessaging.sol formula:
33-
/// `keccak256(abi.encodePacked(from_address, to_address, nonce, selector, payload.length, payload))`
34-
/// where `from_address` is a 20-byte Ethereum address (lower 20 bytes of the felt252).
32+
/// `saya-tee` always settles to a Starknet piltover core, so the canonical L1→L2
33+
/// message hash is Poseidon-based (`hash::compute_message_hash_sn_to_appc` in
34+
/// piltover), not the Ethereum `keccak256` `StarknetMessaging.sol` formula. Katana
35+
/// commits to this Poseidon hash in the appchain attestation's `messages_commitment`
36+
/// (it stores the L1-handler `message_hash` produced by its Starknet messaging
37+
/// collector). Using keccak here makes piltover's recomputed commitment mismatch and
38+
/// every block that consumes an L1→L2 message reverts with `'tee: invalid messages'`.
39+
///
40+
/// Katana hashes over the L1-handler CALLDATA, which is `[from_address, ...payload]`:
41+
///
42+
/// ```text
43+
/// Poseidon([from_address, to_address, nonce, selector, calldata.len(), ...calldata])
44+
/// where calldata = [from_address, ...payload]
45+
/// ```
46+
///
47+
/// See katana `crates/messaging/src/stream/collector/starknet.rs`
48+
/// (`compute_starknet_to_appchain_message_hash`).
3549
fn compute_l1_to_l2_msg_hash(msg: &L1ToL2Message) -> Felt {
36-
let mut hasher = Keccak256::new();
37-
hasher.update(&msg.from_address.to_bytes_be()[12..]);
38-
hasher.update(msg.to_address.to_bytes_be());
39-
hasher.update(msg.nonce.to_bytes_be());
40-
hasher.update(msg.selector.to_bytes_be());
41-
hasher.update(Felt::from(msg.payload.len() as u64).to_bytes_be());
42-
for p in &msg.payload {
43-
hasher.update(p.to_bytes_be());
44-
}
45-
Felt::from_bytes_be(&hasher.finalize().into())
50+
let mut calldata: Vec<Felt> = Vec::with_capacity(msg.payload.len() + 1);
51+
calldata.push(msg.from_address);
52+
calldata.extend(msg.payload.iter().copied());
53+
54+
let mut buf: Vec<Felt> = vec![
55+
msg.from_address,
56+
msg.to_address,
57+
msg.nonce,
58+
msg.selector,
59+
Felt::from(calldata.len() as u64),
60+
];
61+
buf.extend(calldata);
62+
63+
Poseidon::hash_array(&buf)
4664
}
4765

4866
fn messages_to_starknet(msgs: &[L2ToL1Message]) -> Vec<MessageToStarknet> {

0 commit comments

Comments
 (0)