mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-05-31 00:31:02 +00:00
Adds Snowbridge to Rococo runtime (#2522)
# Description Adds Snowbridge to the Rococo bridge hub runtime. Includes config changes required in Rococo asset hub. --------- Co-authored-by: Alistair Singh <alistair.singh7@gmail.com> Co-authored-by: ron <yrong1997@gmail.com> Co-authored-by: Vincent Geddes <vincent.geddes@hey.com> Co-authored-by: claravanstaden <Cats 4 life!>
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
[package]
|
||||
name = "snowbridge-beacon-primitives"
|
||||
description = "Snowbridge Beacon Primitives"
|
||||
version = "0.0.1"
|
||||
authors = ["Snowfork <contact@snowfork.com>"]
|
||||
edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0.188", optional = true, features = ["derive"] }
|
||||
hex = { version = "0.4", default-features = false }
|
||||
codec = { package = "parity-scale-codec", version = "3.6.1", default-features = false }
|
||||
scale-info = { version = "2.9.0", default-features = false, features = ["derive"] }
|
||||
rlp = { version = "0.5", default-features = false }
|
||||
|
||||
frame-support = { path = "../../../../../substrate/frame/support", default-features = false }
|
||||
frame-system = { path = "../../../../../substrate/frame/system", default-features = false }
|
||||
sp-runtime = { path = "../../../../../substrate/primitives/runtime", default-features = false }
|
||||
sp-core = { path = "../../../../../substrate/primitives/core", default-features = false }
|
||||
sp-std = { path = "../../../../../substrate/primitives/std", default-features = false }
|
||||
sp-io = { path = "../../../../../substrate/primitives/io", default-features = false }
|
||||
|
||||
ssz_rs = { version = "0.9.0", default-features = false }
|
||||
ssz_rs_derive = { version = "0.9.0", default-features = false }
|
||||
byte-slice-cast = { version = "1.2.1", default-features = false }
|
||||
|
||||
snowbridge-ethereum = { path = "../../primitives/ethereum", default-features = false }
|
||||
static_assertions = { version = "1.1.0" }
|
||||
milagro_bls = { git = "https://github.com/snowfork/milagro_bls", default-features = false, rev = "a6d66e4eb89015e352fb1c9f7b661ecdbb5b2176" }
|
||||
|
||||
[dev-dependencies]
|
||||
hex-literal = { version = "0.4.1" }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"byte-slice-cast/std",
|
||||
"codec/std",
|
||||
"frame-support/std",
|
||||
"frame-system/std",
|
||||
"hex/std",
|
||||
"milagro_bls/std",
|
||||
"rlp/std",
|
||||
"scale-info/std",
|
||||
"serde",
|
||||
"snowbridge-ethereum/std",
|
||||
"sp-core/std",
|
||||
"sp-io/std",
|
||||
"sp-runtime/std",
|
||||
"sp-std/std",
|
||||
"ssz_rs/std",
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use sp_std::{convert::TryInto, prelude::*};
|
||||
use ssz_rs::{Bitvector, Deserialize};
|
||||
|
||||
pub fn decompress_sync_committee_bits<
|
||||
const SYNC_COMMITTEE_SIZE: usize,
|
||||
const SYNC_COMMITTEE_BITS_SIZE: usize,
|
||||
>(
|
||||
input: [u8; SYNC_COMMITTEE_BITS_SIZE],
|
||||
) -> [u8; SYNC_COMMITTEE_SIZE] {
|
||||
Bitvector::<{ SYNC_COMMITTEE_SIZE }>::deserialize(&input)
|
||||
.expect("checked statically; qed")
|
||||
.iter()
|
||||
.map(|bit| u8::from(bit == true))
|
||||
.collect::<Vec<u8>>()
|
||||
.try_into()
|
||||
.expect("checked statically; qed")
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use crate::{PublicKey, Signature};
|
||||
use codec::{Decode, Encode};
|
||||
use frame_support::{ensure, PalletError};
|
||||
pub use milagro_bls::{
|
||||
AggregatePublicKey, AggregateSignature, PublicKey as PublicKeyPrepared,
|
||||
Signature as SignaturePrepared,
|
||||
};
|
||||
use scale_info::TypeInfo;
|
||||
use sp_core::H256;
|
||||
use sp_runtime::RuntimeDebug;
|
||||
use sp_std::prelude::*;
|
||||
|
||||
#[derive(Copy, Clone, Encode, Decode, Eq, PartialEq, TypeInfo, RuntimeDebug, PalletError)]
|
||||
pub enum BlsError {
|
||||
InvalidSignature,
|
||||
InvalidPublicKey,
|
||||
InvalidAggregatePublicKeys,
|
||||
SignatureVerificationFailed,
|
||||
}
|
||||
|
||||
/// fast_aggregate_verify optimized with aggregate key subtracting absent ones.
|
||||
pub fn fast_aggregate_verify(
|
||||
aggregate_pubkey: &PublicKeyPrepared,
|
||||
absent_pubkeys: &Vec<PublicKeyPrepared>,
|
||||
message: H256,
|
||||
signature: &Signature,
|
||||
) -> Result<(), BlsError> {
|
||||
let agg_sig = prepare_aggregate_signature(signature)?;
|
||||
let agg_key = prepare_aggregate_pubkey_from_absent(aggregate_pubkey, absent_pubkeys)?;
|
||||
fast_aggregate_verify_pre_aggregated(agg_sig, agg_key, message)
|
||||
}
|
||||
|
||||
/// Decompress one public key into a point in G1.
|
||||
pub fn prepare_milagro_pubkey(pubkey: &PublicKey) -> Result<PublicKeyPrepared, BlsError> {
|
||||
PublicKeyPrepared::from_bytes_unchecked(&pubkey.0).map_err(|_| BlsError::InvalidPublicKey)
|
||||
}
|
||||
|
||||
/// Prepare for G1 public keys.
|
||||
pub fn prepare_g1_pubkeys(pubkeys: &[PublicKey]) -> Result<Vec<PublicKeyPrepared>, BlsError> {
|
||||
pubkeys
|
||||
.iter()
|
||||
// Deserialize one public key from compressed bytes
|
||||
.map(prepare_milagro_pubkey)
|
||||
.collect::<Result<Vec<PublicKeyPrepared>, BlsError>>()
|
||||
}
|
||||
|
||||
/// Prepare for G1 AggregatePublicKey.
|
||||
pub fn prepare_aggregate_pubkey(
|
||||
pubkeys: &[PublicKeyPrepared],
|
||||
) -> Result<AggregatePublicKey, BlsError> {
|
||||
AggregatePublicKey::into_aggregate(pubkeys).map_err(|_| BlsError::InvalidPublicKey)
|
||||
}
|
||||
|
||||
/// Prepare for G1 AggregatePublicKey.
|
||||
pub fn prepare_aggregate_pubkey_from_absent(
|
||||
aggregate_key: &PublicKeyPrepared,
|
||||
absent_pubkeys: &Vec<PublicKeyPrepared>,
|
||||
) -> Result<AggregatePublicKey, BlsError> {
|
||||
let mut aggregate_pubkey = AggregatePublicKey::from_public_key(aggregate_key);
|
||||
if !absent_pubkeys.is_empty() {
|
||||
let absent_aggregate_key = prepare_aggregate_pubkey(absent_pubkeys)?;
|
||||
aggregate_pubkey.point.sub(&absent_aggregate_key.point);
|
||||
}
|
||||
Ok(AggregatePublicKey { point: aggregate_pubkey.point })
|
||||
}
|
||||
|
||||
/// Prepare for G2 AggregateSignature, normally more expensive than G1 operation.
|
||||
pub fn prepare_aggregate_signature(signature: &Signature) -> Result<AggregateSignature, BlsError> {
|
||||
Ok(AggregateSignature::from_signature(
|
||||
&SignaturePrepared::from_bytes(&signature.0).map_err(|_| BlsError::InvalidSignature)?,
|
||||
))
|
||||
}
|
||||
|
||||
/// fast_aggregate_verify_pre_aggregated which is the most expensive call in beacon light client.
|
||||
pub fn fast_aggregate_verify_pre_aggregated(
|
||||
agg_sig: AggregateSignature,
|
||||
aggregate_key: AggregatePublicKey,
|
||||
message: H256,
|
||||
) -> Result<(), BlsError> {
|
||||
ensure!(
|
||||
agg_sig.fast_aggregate_verify_pre_aggregated(&message[..], &aggregate_key),
|
||||
BlsError::SignatureVerificationFailed
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
pub const MAX_PROOF_SIZE: u32 = 20;
|
||||
|
||||
pub const FEE_RECIPIENT_SIZE: usize = 20;
|
||||
pub const EXTRA_DATA_SIZE: usize = 32;
|
||||
pub const LOGS_BLOOM_SIZE: usize = 256;
|
||||
|
||||
pub const PUBKEY_SIZE: usize = 48;
|
||||
pub const SIGNATURE_SIZE: usize = 96;
|
||||
@@ -0,0 +1,31 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
pub mod bits;
|
||||
pub mod bls;
|
||||
pub mod config;
|
||||
pub mod merkle_proof;
|
||||
pub mod receipt;
|
||||
pub mod ssz;
|
||||
pub mod types;
|
||||
pub mod updates;
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
mod serde_utils;
|
||||
|
||||
pub use types::{
|
||||
BeaconHeader, CompactBeaconState, CompactExecutionHeader, ExecutionHeaderState,
|
||||
ExecutionPayloadHeader, FinalizedHeaderState, Fork, ForkData, ForkVersion, ForkVersions, Mode,
|
||||
PublicKey, Signature, SigningData, SyncAggregate, SyncCommittee, SyncCommitteePrepared,
|
||||
};
|
||||
pub use updates::{CheckpointUpdate, ExecutionHeaderUpdate, NextSyncCommitteeUpdate, Update};
|
||||
|
||||
pub use bits::decompress_sync_committee_bits;
|
||||
pub use bls::{
|
||||
fast_aggregate_verify, prepare_aggregate_pubkey, prepare_aggregate_pubkey_from_absent,
|
||||
prepare_aggregate_signature, prepare_g1_pubkeys, AggregatePublicKey, AggregateSignature,
|
||||
BlsError, PublicKeyPrepared, SignaturePrepared,
|
||||
};
|
||||
pub use merkle_proof::verify_merkle_branch;
|
||||
pub use receipt::verify_receipt_proof;
|
||||
@@ -0,0 +1,58 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use sp_core::H256;
|
||||
use sp_io::hashing::sha2_256;
|
||||
|
||||
/// Specified by <https://github.com/ethereum/consensus-specs/blob/fe9c1a8cbf0c2da8a4f349efdcd77dd7ac8445c4/specs/phase0/beacon-chain.md?plain=1#L742>
|
||||
/// with improvements from <https://github.com/ethereum/consensus-specs/blob/dev/ssz/merkle-proofs.md>
|
||||
pub fn verify_merkle_branch(
|
||||
leaf: H256,
|
||||
branch: &[H256],
|
||||
index: usize,
|
||||
depth: usize,
|
||||
root: H256,
|
||||
) -> bool {
|
||||
// verify the proof length
|
||||
if branch.len() != depth {
|
||||
return false
|
||||
}
|
||||
// verify the computed merkle root
|
||||
root == compute_merkle_root(leaf, branch, index)
|
||||
}
|
||||
|
||||
fn compute_merkle_root(leaf: H256, proof: &[H256], index: usize) -> H256 {
|
||||
let mut value: [u8; 32] = leaf.into();
|
||||
for (i, node) in proof.iter().enumerate() {
|
||||
let mut data = [0u8; 64];
|
||||
if generalized_index_bit(index, i) {
|
||||
// right node
|
||||
data[0..32].copy_from_slice(node.as_bytes());
|
||||
data[32..64].copy_from_slice(&value);
|
||||
value = sha2_256(&data);
|
||||
} else {
|
||||
// left node
|
||||
data[0..32].copy_from_slice(&value);
|
||||
data[32..64].copy_from_slice(node.as_bytes());
|
||||
value = sha2_256(&data);
|
||||
}
|
||||
}
|
||||
value.into()
|
||||
}
|
||||
|
||||
/// Spec: <https://github.com/ethereum/consensus-specs/blob/fe9c1a8cbf0c2da8a4f349efdcd77dd7ac8445c4/ssz/merkle-proofs.md#get_generalized_index_bit>
|
||||
fn generalized_index_bit(index: usize, position: usize) -> bool {
|
||||
index & (1 << position) > 0
|
||||
}
|
||||
|
||||
/// Spec: <https://github.com/ethereum/consensus-specs/blob/fe9c1a8cbf0c2da8a4f349efdcd77dd7ac8445c4/specs/altair/light-client/sync-protocol.md#get_subtree_index>
|
||||
pub const fn subtree_index(generalized_index: usize) -> usize {
|
||||
generalized_index % (1 << generalized_index_length(generalized_index))
|
||||
}
|
||||
|
||||
/// Spec: <https://github.com/ethereum/consensus-specs/blob/fe9c1a8cbf0c2da8a4f349efdcd77dd7ac8445c4/ssz/merkle-proofs.md#get_generalized_index_length>
|
||||
pub const fn generalized_index_length(generalized_index: usize) -> usize {
|
||||
match generalized_index.checked_ilog2() {
|
||||
Some(v) => v as usize,
|
||||
None => panic!("checked statically; qed"),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use sp_core::H256;
|
||||
use sp_io::hashing::keccak_256;
|
||||
use sp_std::prelude::*;
|
||||
|
||||
use snowbridge_ethereum::{mpt, Receipt};
|
||||
|
||||
pub fn verify_receipt_proof(
|
||||
receipts_root: H256,
|
||||
proof: &[Vec<u8>],
|
||||
) -> Option<Result<Receipt, rlp::DecoderError>> {
|
||||
match apply_merkle_proof(proof) {
|
||||
Some((root, data)) if root == receipts_root => Some(rlp::decode(&data)),
|
||||
Some((_, _)) => None,
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_merkle_proof(proof: &[Vec<u8>]) -> Option<(H256, Vec<u8>)> {
|
||||
let mut iter = proof.iter().rev();
|
||||
let first_bytes = match iter.next() {
|
||||
Some(b) => b,
|
||||
None => return None,
|
||||
};
|
||||
let item_to_prove: mpt::ShortNode = rlp::decode(first_bytes).ok()?;
|
||||
|
||||
let final_hash: Option<[u8; 32]> = iter.try_fold(keccak_256(first_bytes), |acc, x| {
|
||||
let node: Box<dyn mpt::Node> = x.as_slice().try_into().ok()?;
|
||||
if (*node).contains_hash(acc.into()) {
|
||||
return Some(keccak_256(x))
|
||||
}
|
||||
None
|
||||
});
|
||||
|
||||
final_hash.map(|hash| (hash.into(), item_to_prove.value))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use hex_literal::hex;
|
||||
|
||||
#[test]
|
||||
fn test_verify_receipt_proof() {
|
||||
let root: H256 =
|
||||
hex!("fd5e397a84884641f53c496804f24b5276cbb8c5c9cfc2342246be8e3ce5ad02").into();
|
||||
|
||||
// Valid proof
|
||||
let proof_receipt5 = vec!(
|
||||
hex!("f90131a0b5ba404eb5a6a88e56579f4d37ef9813b5ad7f86f0823ff3b407ac5a6bb465eca0398ead2655e78e03c127ce22c5830e90f18b1601ec055f938336c084feb915a9a026d322c26e46c50942c1aabde50e36df5cde572aed650ce73ea3182c6e90a02ca00600a356135f4db1db0d9842264cdff2652676f881669e91e316c0b6dd783011a0837f1deb4075336da320388c1edfffc56c448a43f4a5ba031300d32a7b509fc5a01c3ac82fd65b4aba7f9afaf604d9c82ec7e2deb573a091ae235751bc5c0c288da05d454159d9071b0f68b6e0503d290f23ac7602c1db0c569dee4605d8f5298f09a00bbed10350ec954448df795f6fd46e3faefc800ede061b3840eedc6e2b07a74da0acb02d26a3650f2064c14a435fdf1f668d8655daf455ebdf671713a7c089b3898080808080808080").to_vec(),
|
||||
hex!("f901f180a00046a08d4f0bdbdc6b31903086ce323182bce6725e7d9415f7ff91ee8f4820bda0e7cd26ad5f3d2771e4b5ab788e268a14a10209f94ee918eb6c829d21d3d11c1da00d4a56d9e9a6751874fd86c7e3cb1c6ad5a848da62751325f478978a00ea966ea064b81920c8f04a8a1e21f53a8280e739fbb7b00b2ab92493ca3f610b70e8ac85a0b1040ed4c55a73178b76abb16f946ce5bebd6b93ab873c83327df54047d12c27a0de6485e9ac58dc6e2b04b4bb38f562684f0b1a2ee586cc11079e7d9a9dc40b32a0d394f4d3532c3124a65fa36e69147e04fd20453a72ee9c50660f17e13ce9df48a066501003fc3e3478efd2803cd0eded6bbe9243ca01ba754d6327071ddbcbc649a0b2684e518f325fee39fc8ea81b68f3f5c785be00d087f3bed8857ae2ee8da26ea071060a5c52042e8d7ce21092f8ecf06053beb9a0b773a6f91a30c4220aa276b2a0fc22436632574ccf6043d0986dede27ea94c9ca9a3bb5ec03ce776a4ddef24a9a05a8a1d6698c4e7d8cc3a2506cb9b12ea9a079c9c7099bc919dc804033cc556e4a0170c468b0716fd36d161f0bf05875f15756a2976de92f9efe7716320509d79c9a0182f909a90cab169f3efb62387f9cccdd61440acc4deec42f68a4f7ca58075c7a055cf0e9202ac75689b76318f1171f3a44465eddc06aae0713bfb6b34fdd27b7980").to_vec(),
|
||||
hex!("f904de20b904daf904d701830652f0b9010004200000000000000000000080020000000000010000000000010000000000000000000000000000000000000000000002000000080000000000000000200000000000000000000000000008000000220000000000400010000000000000000000000000000000000000000000000000000000000000040000000010000100000000000800000000004000000000000000000000000000080000004000000000020000000000020000000000000000000000000000000000000000000004000000000002000000000100000000000000000000000000001000000002000020000010200000000000010000000000000000000000000000000000000010000000f903ccf89b9421130f34829b4c343142047a28ce96ec07814b15f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa00000000000000000000000007d843005c7433c16b27ff939cb37471541561ebda0000000000000000000000000e9c1281aae66801fa35ec404d5f2aea393ff6988a000000000000000000000000000000000000000000000000000000005d09b7380f89b9421130f34829b4c343142047a28ce96ec07814b15f863a08c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925a00000000000000000000000007d843005c7433c16b27ff939cb37471541561ebda00000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488da0ffffffffffffffffffffffffffffffffffffffffffffffffffffffcc840c6920f89b94c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa0000000000000000000000000e9c1281aae66801fa35ec404d5f2aea393ff6988a00000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488da000000000000000000000000000000000000000000000000003e973b5a5d1078ef87994e9c1281aae66801fa35ec404d5f2aea393ff6988e1a01c411e9a96e071241c2f21f7726b17ae89e3cab4c78be50e062b03a9fffbbad1b840000000000000000000000000000000000000000000000000000001f1420ad1d40000000000000000000000000000000000000000000000014ad400879d159a38f8fc94e9c1281aae66801fa35ec404d5f2aea393ff6988f863a0d78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822a00000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488da00000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488db88000000000000000000000000000000000000000000000000000000005d415f3320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003e973b5a5d1078ef87a94c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f842a07fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65a00000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488da000000000000000000000000000000000000000000000000003e973b5a5d1078e").to_vec(),
|
||||
);
|
||||
assert!(verify_receipt_proof(root, &proof_receipt5).is_some());
|
||||
|
||||
// Various invalid proofs
|
||||
let proof_empty: Vec<Vec<u8>> = vec![];
|
||||
let proof_missing_full_node = vec![proof_receipt5[0].clone(), proof_receipt5[2].clone()];
|
||||
let proof_missing_short_node1 = vec![proof_receipt5[0].clone(), proof_receipt5[1].clone()];
|
||||
let proof_missing_short_node2 = vec![proof_receipt5[0].clone()];
|
||||
let proof_invalid_encoding = vec![proof_receipt5[2][2..].to_vec()];
|
||||
let proof_no_full_node = vec![proof_receipt5[2].clone(), proof_receipt5[2].clone()];
|
||||
assert!(verify_receipt_proof(root, &proof_empty).is_none());
|
||||
assert!(verify_receipt_proof(root, &proof_missing_full_node).is_none());
|
||||
|
||||
assert_eq!(
|
||||
verify_receipt_proof(root, &proof_missing_short_node1),
|
||||
Some(Err(rlp::DecoderError::Custom("Unsupported receipt type")))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
verify_receipt_proof(root, &proof_missing_short_node2),
|
||||
Some(Err(rlp::DecoderError::Custom("Unsupported receipt type")))
|
||||
);
|
||||
|
||||
assert!(verify_receipt_proof(root, &proof_invalid_encoding).is_none());
|
||||
assert!(verify_receipt_proof(root, &proof_no_full_node).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_receipt_proof_with_intermediate_short_node() {
|
||||
let root: H256 =
|
||||
hex!("d128e3a57142d2bf15bc0cbcac7ad54f40750d571b5c3097e425882c10c9ba66").into();
|
||||
|
||||
let proof_receipt263 = vec![
|
||||
hex!("f90131a00d3cb8d3f57ac1c0e12918a2ebe0cafed8c273577b9dd73e7ed1079b403ef494a0678b9835b834f8a287c0dd33a8fca9146e456ca688555ed4ec1361a2180b778da0fe42da181a46677a043b3d9d4b8bb05a6a17b7b5c010c17e7c1d31cfb7c4f911a0c89f0e2c53241cdb578e1f2b4caf6ba36e00500bdc57fecd66b84a6a58394c19a086c3c1fae5a0575940b5d38e111c469d07883106c26856f3ef608469a2081f13a06c5992ff00aab6226a70a032fd2f571ba22f797321f45e2daa73020d638d21b0a050861e9503ef68728f6c90a44f7fe1bceb2a9bdab6957bbe7136166bd849561ea006aa6eaca8a07e57176e9aa41e6a09edfb7678d1a112404e0ec779d7e567e82ea0bb0b430d303ba21b0af11c487b8a218bd75db54c98940b3f11bad8ff47cad3ef8080808080808080").to_vec(),
|
||||
hex!("f871a0246de222036ee6a03329b0105da0a6b3f916fc95a9ed5a403a581a0c4d74242ca0ac108a49a88b57a05ac34a108b39f1e45f6f167f2b9fbc8d52fb58e2e5a6af1ea0fcfe07ac2ccd3c28b6eab68d1bce112f6f6dbd9023e4ec3c05b96615aa803d798080808080808080808080808080").to_vec(),
|
||||
hex!("e4820001a04fff54398cad4d05ea6abfd8b0f3b4fe14c04d7ff5f5211c5b927d9cf72ac1d8").to_vec(),
|
||||
hex!("f851a096d010643ca2d47412ca66898286b5f2412963b9ec051b33e570d575914c9c5ca028cd24c652989542fe89479ec6388eac4592432242af5ba97563b3ac7c71c019808080808080808080808080808080").to_vec(),
|
||||
hex!("f90211a0bb35a84c5b1dcb78ec9d32614912c696e62df77bebf9ab326ee55b5d3acdde46a01084b30dac8df0accfcd0fd6330b7f6fc72a4651246d0694be9162151686a620a03eed50afdce7909d784c6157c445a444c806b5f23d31f3b63786f600c84a95b2a0af5232f1df6c6d41879804d081abe867002abe26ba3e5f8e0254a83a54769831a0607915fb13dd5da594256389a45007a67a7f7a86e95d38d8462792b6c98a722ea00e1260fda1730f2738c650ce2bfba83857bc10f8fb119ebc4fb39acba24e6fbaa0d11de17e417327457812675ca3b84ae8e1b64827abfe01420953697c8313d5b1a05fcaf2f7a88f76336a0c32ffc78acb87ae2005454bd25d658035331be3173b46a03f94f4952ab9e650f83cfd0e7f367b1bcc493aacf39a06f16c4a2e1b5605da48a0bdb4ec79785ca8ae22d60f1bbd42d707b4d7ec4aff231a3ebab755e315b35053a043a67c3f2bcef37c8f47a673adcb7061007a553696d1092408601c11b2e6846aa0c519d5af48cae87c7f4538845417c9735813bee892a6fe2dda79f5c414e8576aa0f7058256e09589501d7c231d739e61c84a850e139690989d24fda6058b432e98a081a52faab520978cb19ce14400dba0cd5bcdc4e5a3c0740678aa8f97ee0e5c56a0bcecc61cadeae52518e3b68a48af4b11603dfd9d99d99d7985efa6d2de44f904a02cba4accfc6f39bc5adb6d4440eb6358b4a5103ef93298e4e694f1f940f8b48280").to_vec(),
|
||||
hex!("f901ae20b901aaf901a70183bb444eb9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000001000000000000000000000000000100000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000010000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000080000000000000000000000000000000000000000000000002000000000000000000081000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000f89df89b94dac17f958d2ee523a2206206994597c13d831ec7f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa00000000000000000000000002e514404ff6823f1b46a8318a709251db414e5e1a000000000000000000000000055021c55847c00d764357a352e5803237d328954a0000000000000000000000000000000000000000000000000000000000201c370").to_vec(),
|
||||
];
|
||||
assert!(verify_receipt_proof(root, &proof_receipt263).is_some());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use sp_core::U256;
|
||||
|
||||
use core::fmt::Formatter;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
// helper to deserialize arbitrary arrays like [T; N]
|
||||
pub mod arrays {
|
||||
use std::{convert::TryInto, marker::PhantomData};
|
||||
|
||||
use serde::{
|
||||
de::{SeqAccess, Visitor},
|
||||
ser::SerializeTuple,
|
||||
Deserialize, Deserializer, Serialize, Serializer,
|
||||
};
|
||||
|
||||
pub fn serialize<S: Serializer, T: Serialize, const N: usize>(
|
||||
data: &[T; N],
|
||||
ser: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
let mut s = ser.serialize_tuple(N)?;
|
||||
for item in data {
|
||||
s.serialize_element(item)?;
|
||||
}
|
||||
s.end()
|
||||
}
|
||||
|
||||
struct ArrayVisitor<T, const N: usize>(PhantomData<T>);
|
||||
|
||||
impl<'de, T, const N: usize> Visitor<'de> for ArrayVisitor<T, N>
|
||||
where
|
||||
T: Deserialize<'de>,
|
||||
{
|
||||
type Value = [T; N];
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str(&format!("an array of length {}", N))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: SeqAccess<'de>,
|
||||
{
|
||||
// can be optimized using MaybeUninit
|
||||
let mut data = Vec::with_capacity(N);
|
||||
for _ in 0..N {
|
||||
match (seq.next_element())? {
|
||||
Some(val) => data.push(val),
|
||||
None => return Err(serde::de::Error::invalid_length(N, &self)),
|
||||
}
|
||||
}
|
||||
match data.try_into() {
|
||||
Ok(arr) => Ok(arr),
|
||||
Err(_) => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D, T, const N: usize>(deserializer: D) -> Result<[T; N], D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
T: Deserialize<'de>,
|
||||
{
|
||||
deserializer.deserialize_tuple(N, ArrayVisitor::<T, N>(PhantomData))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_hex_to_bytes<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
|
||||
let str_without_0x = match s.strip_prefix("0x") {
|
||||
Some(val) => val,
|
||||
None => &s,
|
||||
};
|
||||
|
||||
let hex_bytes = match hex::decode(str_without_0x) {
|
||||
Ok(bytes) => bytes,
|
||||
Err(e) => return Err(serde::de::Error::custom(e.to_string())),
|
||||
};
|
||||
|
||||
Ok(hex_bytes)
|
||||
}
|
||||
|
||||
pub(crate) fn from_int_to_u256<'de, D>(deserializer: D) -> Result<U256, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let number = u128::deserialize(deserializer)?;
|
||||
|
||||
Ok(U256::from(number))
|
||||
}
|
||||
|
||||
pub struct HexVisitor<const LENGTH: usize>();
|
||||
|
||||
impl<'de, const LENGTH: usize> serde::de::Visitor<'de> for HexVisitor<LENGTH> {
|
||||
type Value = [u8; LENGTH];
|
||||
|
||||
fn expecting(&self, formatter: &mut Formatter) -> sp_std::fmt::Result {
|
||||
formatter.write_str("a hex string with an '0x' prefix")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
let stripped = match v.strip_prefix("0x") {
|
||||
Some(stripped) => stripped,
|
||||
None => v,
|
||||
};
|
||||
|
||||
let decoded = match hex::decode(stripped) {
|
||||
Ok(decoded) => decoded,
|
||||
Err(e) => return Err(serde::de::Error::custom(e.to_string())),
|
||||
};
|
||||
if decoded.len() != LENGTH {
|
||||
return Err(serde::de::Error::custom("publickey expected to be 48 characters"))
|
||||
}
|
||||
|
||||
let data: Self::Value = decoded
|
||||
.try_into()
|
||||
.map_err(|_e| serde::de::Error::custom("hex data has unexpected length"))?;
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use crate::{
|
||||
config::{EXTRA_DATA_SIZE, FEE_RECIPIENT_SIZE, LOGS_BLOOM_SIZE, PUBKEY_SIZE, SIGNATURE_SIZE},
|
||||
types::{
|
||||
BeaconHeader, ExecutionPayloadHeader, ForkData, SigningData, SyncAggregate, SyncCommittee,
|
||||
},
|
||||
};
|
||||
use byte_slice_cast::AsByteSlice;
|
||||
use sp_core::H256;
|
||||
use sp_std::{vec, vec::Vec};
|
||||
use ssz_rs::{
|
||||
prelude::{List, Vector},
|
||||
Bitvector, Deserialize, DeserializeError, SimpleSerialize, SimpleSerializeError, Sized, U256,
|
||||
};
|
||||
use ssz_rs_derive::SimpleSerialize as SimpleSerializeDerive;
|
||||
|
||||
#[derive(Default, SimpleSerializeDerive, Clone, Debug)]
|
||||
pub struct SSZBeaconBlockHeader {
|
||||
pub slot: u64,
|
||||
pub proposer_index: u64,
|
||||
pub parent_root: [u8; 32],
|
||||
pub state_root: [u8; 32],
|
||||
pub body_root: [u8; 32],
|
||||
}
|
||||
|
||||
impl From<BeaconHeader> for SSZBeaconBlockHeader {
|
||||
fn from(beacon_header: BeaconHeader) -> Self {
|
||||
SSZBeaconBlockHeader {
|
||||
slot: beacon_header.slot,
|
||||
proposer_index: beacon_header.proposer_index,
|
||||
parent_root: beacon_header.parent_root.to_fixed_bytes(),
|
||||
state_root: beacon_header.state_root.to_fixed_bytes(),
|
||||
body_root: beacon_header.body_root.to_fixed_bytes(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, SimpleSerializeDerive, Clone)]
|
||||
pub struct SSZSyncCommittee<const COMMITTEE_SIZE: usize> {
|
||||
pub pubkeys: Vector<Vector<u8, PUBKEY_SIZE>, COMMITTEE_SIZE>,
|
||||
pub aggregate_pubkey: Vector<u8, PUBKEY_SIZE>,
|
||||
}
|
||||
|
||||
impl<const COMMITTEE_SIZE: usize> From<SyncCommittee<COMMITTEE_SIZE>>
|
||||
for SSZSyncCommittee<COMMITTEE_SIZE>
|
||||
{
|
||||
fn from(sync_committee: SyncCommittee<COMMITTEE_SIZE>) -> Self {
|
||||
let mut pubkeys_vec = Vec::new();
|
||||
|
||||
for pubkey in sync_committee.pubkeys.iter() {
|
||||
// The only thing that can go wrong in the conversion from vec to Vector (ssz type) is
|
||||
// that the Vector size is 0, or that the given data to create the Vector from does not
|
||||
// match the expected size N. Because these sizes are statically checked (i.e.
|
||||
// PublicKey's size is 48, and const PUBKEY_SIZE is 48, it is impossible for "try_from"
|
||||
// to return an error condition.
|
||||
let conv_pubkey = Vector::<u8, PUBKEY_SIZE>::try_from(pubkey.0.to_vec())
|
||||
.expect("checked statically; qed");
|
||||
|
||||
pubkeys_vec.push(conv_pubkey);
|
||||
}
|
||||
|
||||
let pubkeys = Vector::<Vector<u8, PUBKEY_SIZE>, { COMMITTEE_SIZE }>::try_from(pubkeys_vec)
|
||||
.expect("checked statically; qed");
|
||||
|
||||
let aggregate_pubkey =
|
||||
Vector::<u8, PUBKEY_SIZE>::try_from(sync_committee.aggregate_pubkey.0.to_vec())
|
||||
.expect("checked statically; qed");
|
||||
|
||||
SSZSyncCommittee { pubkeys, aggregate_pubkey }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, SimpleSerializeDerive, Clone)]
|
||||
pub struct SSZSyncAggregate<const COMMITTEE_SIZE: usize> {
|
||||
pub sync_committee_bits: Bitvector<COMMITTEE_SIZE>,
|
||||
pub sync_committee_signature: Vector<u8, SIGNATURE_SIZE>,
|
||||
}
|
||||
|
||||
impl<const COMMITTEE_SIZE: usize, const COMMITTEE_BITS_SIZE: usize>
|
||||
From<SyncAggregate<COMMITTEE_SIZE, COMMITTEE_BITS_SIZE>> for SSZSyncAggregate<COMMITTEE_SIZE>
|
||||
{
|
||||
fn from(sync_aggregate: SyncAggregate<COMMITTEE_SIZE, COMMITTEE_BITS_SIZE>) -> Self {
|
||||
SSZSyncAggregate {
|
||||
sync_committee_bits: Bitvector::<COMMITTEE_SIZE>::deserialize(
|
||||
&sync_aggregate.sync_committee_bits,
|
||||
)
|
||||
.expect("checked statically; qed"),
|
||||
sync_committee_signature: Vector::<u8, SIGNATURE_SIZE>::try_from(
|
||||
sync_aggregate.sync_committee_signature.0.to_vec(),
|
||||
)
|
||||
.expect("checked statically; qed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, SimpleSerializeDerive, Clone)]
|
||||
pub struct SSZForkData {
|
||||
pub current_version: [u8; 4],
|
||||
pub genesis_validators_root: [u8; 32],
|
||||
}
|
||||
|
||||
impl From<ForkData> for SSZForkData {
|
||||
fn from(fork_data: ForkData) -> Self {
|
||||
SSZForkData {
|
||||
current_version: fork_data.current_version,
|
||||
genesis_validators_root: fork_data.genesis_validators_root,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, SimpleSerializeDerive, Clone)]
|
||||
pub struct SSZSigningData {
|
||||
pub object_root: [u8; 32],
|
||||
pub domain: [u8; 32],
|
||||
}
|
||||
|
||||
impl From<SigningData> for SSZSigningData {
|
||||
fn from(signing_data: SigningData) -> Self {
|
||||
SSZSigningData {
|
||||
object_root: signing_data.object_root.into(),
|
||||
domain: signing_data.domain.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, SimpleSerializeDerive, Clone, Debug)]
|
||||
pub struct SSZExecutionPayloadHeader {
|
||||
pub parent_hash: [u8; 32],
|
||||
pub fee_recipient: Vector<u8, FEE_RECIPIENT_SIZE>,
|
||||
pub state_root: [u8; 32],
|
||||
pub receipts_root: [u8; 32],
|
||||
pub logs_bloom: Vector<u8, LOGS_BLOOM_SIZE>,
|
||||
pub prev_randao: [u8; 32],
|
||||
pub block_number: u64,
|
||||
pub gas_limit: u64,
|
||||
pub gas_used: u64,
|
||||
pub timestamp: u64,
|
||||
pub extra_data: List<u8, EXTRA_DATA_SIZE>,
|
||||
pub base_fee_per_gas: U256,
|
||||
pub block_hash: [u8; 32],
|
||||
pub transactions_root: [u8; 32],
|
||||
pub withdrawals_root: [u8; 32],
|
||||
}
|
||||
|
||||
impl TryFrom<ExecutionPayloadHeader> for SSZExecutionPayloadHeader {
|
||||
type Error = SimpleSerializeError;
|
||||
|
||||
fn try_from(payload: ExecutionPayloadHeader) -> Result<Self, Self::Error> {
|
||||
Ok(SSZExecutionPayloadHeader {
|
||||
parent_hash: payload.parent_hash.to_fixed_bytes(),
|
||||
fee_recipient: Vector::<u8, FEE_RECIPIENT_SIZE>::try_from(
|
||||
payload.fee_recipient.to_fixed_bytes().to_vec(),
|
||||
)
|
||||
.expect("checked statically; qed"),
|
||||
state_root: payload.state_root.to_fixed_bytes(),
|
||||
receipts_root: payload.receipts_root.to_fixed_bytes(),
|
||||
// Logs bloom bytes size is not constrained, so here we do need to check the try_from
|
||||
// error
|
||||
logs_bloom: Vector::<u8, LOGS_BLOOM_SIZE>::try_from(payload.logs_bloom)
|
||||
.map_err(|(_, err)| err)?,
|
||||
prev_randao: payload.prev_randao.to_fixed_bytes(),
|
||||
block_number: payload.block_number,
|
||||
gas_limit: payload.gas_limit,
|
||||
gas_used: payload.gas_used,
|
||||
timestamp: payload.timestamp,
|
||||
// Extra data bytes size is not constrained, so here we do need to check the try_from
|
||||
// error
|
||||
extra_data: List::<u8, EXTRA_DATA_SIZE>::try_from(payload.extra_data)
|
||||
.map_err(|(_, err)| err)?,
|
||||
base_fee_per_gas: U256::from_bytes_le(
|
||||
payload
|
||||
.base_fee_per_gas
|
||||
.as_byte_slice()
|
||||
.try_into()
|
||||
.expect("checked in prep; qed"),
|
||||
),
|
||||
block_hash: payload.block_hash.to_fixed_bytes(),
|
||||
transactions_root: payload.transactions_root.to_fixed_bytes(),
|
||||
withdrawals_root: payload.withdrawals_root.to_fixed_bytes(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hash_tree_root<T: SimpleSerialize>(mut object: T) -> Result<H256, SimpleSerializeError> {
|
||||
match object.hash_tree_root() {
|
||||
Ok(node) => {
|
||||
let fixed_bytes: [u8; 32] =
|
||||
node.as_ref().try_into().expect("Node is a newtype over [u8; 32]; qed");
|
||||
Ok(fixed_bytes.into())
|
||||
},
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,512 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use codec::{Decode, Encode, MaxEncodedLen};
|
||||
use frame_support::{CloneNoBound, PartialEqNoBound, RuntimeDebugNoBound};
|
||||
use scale_info::TypeInfo;
|
||||
use sp_core::{H160, H256, U256};
|
||||
use sp_runtime::RuntimeDebug;
|
||||
use sp_std::{boxed::Box, prelude::*};
|
||||
|
||||
use crate::config::{PUBKEY_SIZE, SIGNATURE_SIZE};
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
use crate::serde_utils::HexVisitor;
|
||||
|
||||
use crate::ssz::{
|
||||
hash_tree_root, SSZBeaconBlockHeader, SSZExecutionPayloadHeader, SSZForkData, SSZSigningData,
|
||||
SSZSyncAggregate, SSZSyncCommittee,
|
||||
};
|
||||
use ssz_rs::SimpleSerializeError;
|
||||
|
||||
pub use crate::bits::decompress_sync_committee_bits;
|
||||
|
||||
use crate::bls::{prepare_g1_pubkeys, prepare_milagro_pubkey, BlsError};
|
||||
use milagro_bls::PublicKey as PublicKeyPrepared;
|
||||
|
||||
pub type ValidatorIndex = u64;
|
||||
pub type ForkVersion = [u8; 4];
|
||||
|
||||
#[derive(Clone, Encode, Decode, PartialEq, RuntimeDebug, TypeInfo)]
|
||||
pub struct ForkVersions {
|
||||
pub genesis: Fork,
|
||||
pub altair: Fork,
|
||||
pub bellatrix: Fork,
|
||||
pub capella: Fork,
|
||||
}
|
||||
|
||||
#[derive(Clone, Encode, Decode, PartialEq, RuntimeDebug, TypeInfo)]
|
||||
pub struct Fork {
|
||||
pub version: [u8; 4],
|
||||
pub epoch: u64,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Encode, Decode, PartialEq, RuntimeDebug, TypeInfo)]
|
||||
pub struct PublicKey(pub [u8; PUBKEY_SIZE]);
|
||||
|
||||
impl Default for PublicKey {
|
||||
fn default() -> Self {
|
||||
PublicKey([0u8; PUBKEY_SIZE])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[u8; PUBKEY_SIZE]> for PublicKey {
|
||||
fn from(v: [u8; PUBKEY_SIZE]) -> Self {
|
||||
Self(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl MaxEncodedLen for PublicKey {
|
||||
fn max_encoded_len() -> usize {
|
||||
PUBKEY_SIZE
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl<'de> Deserialize<'de> for PublicKey {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_str(HexVisitor::<PUBKEY_SIZE>()).map(|v| v.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl Serialize for PublicKey {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_bytes(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Encode, Decode, PartialEq, RuntimeDebug, TypeInfo)]
|
||||
pub struct Signature(pub [u8; SIGNATURE_SIZE]);
|
||||
|
||||
impl Default for Signature {
|
||||
fn default() -> Self {
|
||||
Signature([0u8; SIGNATURE_SIZE])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[u8; SIGNATURE_SIZE]> for Signature {
|
||||
fn from(v: [u8; SIGNATURE_SIZE]) -> Self {
|
||||
Self(v)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl<'de> Deserialize<'de> for Signature {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_str(HexVisitor::<SIGNATURE_SIZE>()).map(|v| v.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default, Encode, Decode, TypeInfo, MaxEncodedLen)]
|
||||
pub struct ExecutionHeaderState {
|
||||
pub beacon_block_root: H256,
|
||||
pub beacon_slot: u64,
|
||||
pub block_hash: H256,
|
||||
pub block_number: u64,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default, Encode, Decode, TypeInfo, MaxEncodedLen)]
|
||||
pub struct FinalizedHeaderState {
|
||||
pub beacon_block_root: H256,
|
||||
pub beacon_slot: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Encode, Decode, PartialEq, RuntimeDebug)]
|
||||
pub struct ForkData {
|
||||
// 1 or 0 bit, indicates whether a sync committee participated in a vote
|
||||
pub current_version: [u8; 4],
|
||||
pub genesis_validators_root: [u8; 32],
|
||||
}
|
||||
|
||||
impl ForkData {
|
||||
pub fn hash_tree_root(&self) -> Result<H256, SimpleSerializeError> {
|
||||
hash_tree_root::<SSZForkData>(self.clone().into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Encode, Decode, PartialEq, RuntimeDebug)]
|
||||
pub struct SigningData {
|
||||
pub object_root: H256,
|
||||
pub domain: H256,
|
||||
}
|
||||
|
||||
impl SigningData {
|
||||
pub fn hash_tree_root(&self) -> Result<H256, SimpleSerializeError> {
|
||||
hash_tree_root::<SSZSigningData>(self.clone().into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Sync committee as it is stored in the runtime storage.
|
||||
#[derive(
|
||||
Encode, Decode, PartialEqNoBound, CloneNoBound, RuntimeDebugNoBound, TypeInfo, MaxEncodedLen,
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "std",
|
||||
derive(Serialize, Deserialize),
|
||||
serde(deny_unknown_fields, bound(serialize = ""), bound(deserialize = ""))
|
||||
)]
|
||||
#[codec(mel_bound())]
|
||||
pub struct SyncCommittee<const COMMITTEE_SIZE: usize> {
|
||||
#[cfg_attr(feature = "std", serde(with = "crate::serde_utils::arrays"))]
|
||||
pub pubkeys: [PublicKey; COMMITTEE_SIZE],
|
||||
pub aggregate_pubkey: PublicKey,
|
||||
}
|
||||
|
||||
impl<const COMMITTEE_SIZE: usize> Default for SyncCommittee<COMMITTEE_SIZE> {
|
||||
fn default() -> Self {
|
||||
SyncCommittee {
|
||||
pubkeys: [Default::default(); COMMITTEE_SIZE],
|
||||
aggregate_pubkey: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<const COMMITTEE_SIZE: usize> SyncCommittee<COMMITTEE_SIZE> {
|
||||
pub fn hash_tree_root(&self) -> Result<H256, SimpleSerializeError> {
|
||||
hash_tree_root::<SSZSyncCommittee<COMMITTEE_SIZE>>(self.clone().into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Prepared G1 public key of sync committee as it is stored in the runtime storage.
|
||||
#[derive(Clone, PartialEq, Eq, Encode, Decode, TypeInfo, MaxEncodedLen)]
|
||||
pub struct SyncCommitteePrepared<const COMMITTEE_SIZE: usize> {
|
||||
pub root: H256,
|
||||
pub pubkeys: Box<[PublicKeyPrepared; COMMITTEE_SIZE]>,
|
||||
pub aggregate_pubkey: PublicKeyPrepared,
|
||||
}
|
||||
|
||||
impl<const COMMITTEE_SIZE: usize> Default for SyncCommitteePrepared<COMMITTEE_SIZE> {
|
||||
fn default() -> Self {
|
||||
SyncCommitteePrepared {
|
||||
root: H256::default(),
|
||||
pubkeys: Box::new([PublicKeyPrepared::default(); COMMITTEE_SIZE]),
|
||||
aggregate_pubkey: PublicKeyPrepared::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<const COMMITTEE_SIZE: usize> TryFrom<&SyncCommittee<COMMITTEE_SIZE>>
|
||||
for SyncCommitteePrepared<COMMITTEE_SIZE>
|
||||
{
|
||||
type Error = BlsError;
|
||||
|
||||
fn try_from(sync_committee: &SyncCommittee<COMMITTEE_SIZE>) -> Result<Self, Self::Error> {
|
||||
let g1_pubkeys = prepare_g1_pubkeys(&sync_committee.pubkeys)?;
|
||||
let sync_committee_root = sync_committee.hash_tree_root().expect("checked statically; qed");
|
||||
|
||||
Ok(SyncCommitteePrepared::<COMMITTEE_SIZE> {
|
||||
pubkeys: g1_pubkeys.try_into().expect("checked statically; qed"),
|
||||
aggregate_pubkey: prepare_milagro_pubkey(&sync_committee.aggregate_pubkey)?,
|
||||
root: sync_committee_root,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Beacon block header as it is stored in the runtime storage. The block root is the
|
||||
/// Merkleization of a BeaconHeader.
|
||||
#[derive(
|
||||
Copy, Clone, Default, Encode, Decode, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen,
|
||||
)]
|
||||
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
|
||||
pub struct BeaconHeader {
|
||||
// The slot for which this block is created. Must be greater than the slot of the block defined
|
||||
// by parent root.
|
||||
pub slot: u64,
|
||||
// The index of the validator that proposed the block.
|
||||
pub proposer_index: ValidatorIndex,
|
||||
// The block root of the parent block, forming a block chain.
|
||||
pub parent_root: H256,
|
||||
// The hash root of the post state of running the state transition through this block.
|
||||
pub state_root: H256,
|
||||
// The hash root of the beacon block body
|
||||
pub body_root: H256,
|
||||
}
|
||||
|
||||
impl BeaconHeader {
|
||||
pub fn hash_tree_root(&self) -> Result<H256, SimpleSerializeError> {
|
||||
hash_tree_root::<SSZBeaconBlockHeader>((*self).into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, CloneNoBound, PartialEqNoBound, RuntimeDebugNoBound, TypeInfo)]
|
||||
#[cfg_attr(
|
||||
feature = "std",
|
||||
derive(Deserialize),
|
||||
serde(
|
||||
try_from = "IntermediateSyncAggregate",
|
||||
deny_unknown_fields,
|
||||
bound(serialize = ""),
|
||||
bound(deserialize = "")
|
||||
)
|
||||
)]
|
||||
#[codec(mel_bound())]
|
||||
pub struct SyncAggregate<const COMMITTEE_SIZE: usize, const COMMITTEE_BITS_SIZE: usize> {
|
||||
pub sync_committee_bits: [u8; COMMITTEE_BITS_SIZE],
|
||||
pub sync_committee_signature: Signature,
|
||||
}
|
||||
|
||||
impl<const COMMITTEE_SIZE: usize, const COMMITTEE_BITS_SIZE: usize> Default
|
||||
for SyncAggregate<COMMITTEE_SIZE, COMMITTEE_BITS_SIZE>
|
||||
{
|
||||
fn default() -> Self {
|
||||
SyncAggregate {
|
||||
sync_committee_bits: [0; COMMITTEE_BITS_SIZE],
|
||||
sync_committee_signature: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<const COMMITTEE_SIZE: usize, const COMMITTEE_BITS_SIZE: usize>
|
||||
SyncAggregate<COMMITTEE_SIZE, COMMITTEE_BITS_SIZE>
|
||||
{
|
||||
pub fn hash_tree_root(&self) -> Result<H256, SimpleSerializeError> {
|
||||
hash_tree_root::<SSZSyncAggregate<COMMITTEE_SIZE>>(self.clone().into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Serde deserialization helper for SyncAggregate
|
||||
#[cfg(feature = "std")]
|
||||
#[derive(Deserialize)]
|
||||
struct IntermediateSyncAggregate {
|
||||
#[cfg_attr(feature = "std", serde(deserialize_with = "crate::serde_utils::from_hex_to_bytes"))]
|
||||
pub sync_committee_bits: Vec<u8>,
|
||||
pub sync_committee_signature: Signature,
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl<const COMMITTEE_SIZE: usize, const COMMITTEE_BITS_SIZE: usize>
|
||||
TryFrom<IntermediateSyncAggregate> for SyncAggregate<COMMITTEE_SIZE, COMMITTEE_BITS_SIZE>
|
||||
{
|
||||
type Error = String;
|
||||
|
||||
fn try_from(other: IntermediateSyncAggregate) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
sync_committee_bits: other
|
||||
.sync_committee_bits
|
||||
.try_into()
|
||||
.map_err(|_| "unexpected length".to_owned())?,
|
||||
sync_committee_signature: other.sync_committee_signature,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// ExecutionPayloadHeader
|
||||
/// <https://github.com/ethereum/annotated-spec/blob/master/capella/beacon-chain.md#executionpayloadheader>
|
||||
#[derive(
|
||||
Default, Encode, Decode, CloneNoBound, PartialEqNoBound, RuntimeDebugNoBound, TypeInfo,
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "std",
|
||||
derive(Deserialize),
|
||||
serde(deny_unknown_fields, bound(serialize = ""), bound(deserialize = ""))
|
||||
)]
|
||||
#[codec(mel_bound())]
|
||||
pub struct ExecutionPayloadHeader {
|
||||
pub parent_hash: H256,
|
||||
pub fee_recipient: H160,
|
||||
pub state_root: H256,
|
||||
pub receipts_root: H256,
|
||||
#[cfg_attr(feature = "std", serde(deserialize_with = "crate::serde_utils::from_hex_to_bytes"))]
|
||||
pub logs_bloom: Vec<u8>,
|
||||
pub prev_randao: H256,
|
||||
pub block_number: u64,
|
||||
pub gas_limit: u64,
|
||||
pub gas_used: u64,
|
||||
pub timestamp: u64,
|
||||
#[cfg_attr(feature = "std", serde(deserialize_with = "crate::serde_utils::from_hex_to_bytes"))]
|
||||
pub extra_data: Vec<u8>,
|
||||
#[cfg_attr(feature = "std", serde(deserialize_with = "crate::serde_utils::from_int_to_u256"))]
|
||||
pub base_fee_per_gas: U256,
|
||||
pub block_hash: H256,
|
||||
pub transactions_root: H256,
|
||||
pub withdrawals_root: H256,
|
||||
}
|
||||
|
||||
impl ExecutionPayloadHeader {
|
||||
pub fn hash_tree_root(&self) -> Result<H256, SimpleSerializeError> {
|
||||
hash_tree_root::<SSZExecutionPayloadHeader>(self.clone().try_into()?)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Default,
|
||||
Encode,
|
||||
Decode,
|
||||
CloneNoBound,
|
||||
PartialEqNoBound,
|
||||
RuntimeDebugNoBound,
|
||||
TypeInfo,
|
||||
MaxEncodedLen,
|
||||
)]
|
||||
pub struct CompactExecutionHeader {
|
||||
pub parent_hash: H256,
|
||||
#[codec(compact)]
|
||||
pub block_number: u64,
|
||||
pub state_root: H256,
|
||||
pub receipts_root: H256,
|
||||
}
|
||||
|
||||
impl From<ExecutionPayloadHeader> for CompactExecutionHeader {
|
||||
fn from(execution_payload: ExecutionPayloadHeader) -> Self {
|
||||
Self {
|
||||
parent_hash: execution_payload.parent_hash,
|
||||
block_number: execution_payload.block_number,
|
||||
state_root: execution_payload.state_root,
|
||||
receipts_root: execution_payload.receipts_root,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Default,
|
||||
Encode,
|
||||
Decode,
|
||||
Copy,
|
||||
Clone,
|
||||
PartialEqNoBound,
|
||||
RuntimeDebugNoBound,
|
||||
TypeInfo,
|
||||
MaxEncodedLen,
|
||||
)]
|
||||
pub struct CompactBeaconState {
|
||||
#[codec(compact)]
|
||||
pub slot: u64,
|
||||
pub block_roots_root: H256,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use hex_literal::hex;
|
||||
|
||||
#[test]
|
||||
pub fn test_hash_beacon_header1() {
|
||||
let hash_root = BeaconHeader {
|
||||
slot: 3,
|
||||
proposer_index: 2,
|
||||
parent_root: hex!("796ea53efb534eab7777809cc5ee2d84e7f25024b9d0c4d7e5bcaab657e4bdbd")
|
||||
.into(),
|
||||
state_root: hex!("ba3ff080912be5c9c158b2e962c1b39a91bc0615762ba6fa2ecacafa94e9ae0a")
|
||||
.into(),
|
||||
body_root: hex!("a18d7fcefbb74a177c959160e0ee89c23546482154e6831237710414465dcae5")
|
||||
.into(),
|
||||
}
|
||||
.hash_tree_root();
|
||||
|
||||
assert!(hash_root.is_ok());
|
||||
assert_eq!(
|
||||
hash_root.unwrap(),
|
||||
hex!("7d42595818709e805dd2fa710a2d2c1f62576ef1ab7273941ac9130fb94b91f7").into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_hash_beacon_header2() {
|
||||
let hash_root = BeaconHeader {
|
||||
slot: 3476424,
|
||||
proposer_index: 314905,
|
||||
parent_root: hex!("c069d7b49cffd2b815b0fb8007eb9ca91202ea548df6f3db60000f29b2489f28")
|
||||
.into(),
|
||||
state_root: hex!("444d293e4533501ee508ad608783a7d677c3c566f001313e8a02ce08adf590a3")
|
||||
.into(),
|
||||
body_root: hex!("6508a0241047f21ba88f05d05b15534156ab6a6f8e029a9a5423da429834e04a")
|
||||
.into(),
|
||||
}
|
||||
.hash_tree_root();
|
||||
|
||||
assert!(hash_root.is_ok());
|
||||
assert_eq!(
|
||||
hash_root.unwrap(),
|
||||
hex!("0aa41166ff01e58e111ac8c42309a738ab453cf8d7285ed8477b1c484acb123e").into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_hash_fork_data() {
|
||||
let hash_root = ForkData {
|
||||
current_version: hex!("83f38a34"),
|
||||
genesis_validators_root: hex!(
|
||||
"22370bbbb358800f5711a10ea9845284272d8493bed0348cab87b8ab1e127930"
|
||||
),
|
||||
}
|
||||
.hash_tree_root();
|
||||
|
||||
assert!(hash_root.is_ok());
|
||||
assert_eq!(
|
||||
hash_root.unwrap(),
|
||||
hex!("57c12c4246bc7152b174b51920506bf943eff9c7ffa50b9533708e9cc1f680fc").into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_hash_signing_data() {
|
||||
let hash_root = SigningData {
|
||||
object_root: hex!("63654cbe64fc07853f1198c165dd3d49c54fc53bc417989bbcc66da15f850c54")
|
||||
.into(),
|
||||
domain: hex!("037da907d1c3a03c0091b2254e1480d9b1783476e228ab29adaaa8f133e08f7a").into(),
|
||||
}
|
||||
.hash_tree_root();
|
||||
|
||||
assert!(hash_root.is_ok());
|
||||
assert_eq!(
|
||||
hash_root.unwrap(),
|
||||
hex!("b9eb2caf2d691b183c2d57f322afe505c078cd08101324f61c3641714789a54e").into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_hash_sync_aggregate() {
|
||||
let hash_root = SyncAggregate::<512, 64>{
|
||||
sync_committee_bits: hex!("cefffffefffffff767fffbedffffeffffeeffdffffdebffffff7f7dbdf7fffdffffbffcfffdff79dfffbbfefff2ffffff7ddeff7ffffc98ff7fbfffffffffff7"),
|
||||
sync_committee_signature: hex!("8af1a8577bba419fe054ee49b16ed28e081dda6d3ba41651634685e890992a0b675e20f8d9f2ec137fe9eb50e838aa6117f9f5410e2e1024c4b4f0e098e55144843ce90b7acde52fe7b94f2a1037342c951dc59f501c92acf7ed944cb6d2b5f7").into(),
|
||||
}.hash_tree_root();
|
||||
|
||||
assert!(hash_root.is_ok());
|
||||
assert_eq!(
|
||||
hash_root.unwrap(),
|
||||
hex!("e6dcad4f60ce9ff8a587b110facbaf94721f06cd810b6d8bf6cffa641272808d").into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_hash_execution_payload() {
|
||||
let hash_root =
|
||||
ExecutionPayloadHeader{
|
||||
parent_hash: hex!("eadee5ab098dde64e9fd02ae5858064bad67064070679625b09f8d82dec183f7").into(),
|
||||
fee_recipient: hex!("f97e180c050e5ab072211ad2c213eb5aee4df134").into(),
|
||||
state_root: hex!("564fa064c2a324c2b5978d7fdfc5d4224d4f421a45388af1ed405a399c845dff").into(),
|
||||
receipts_root: hex!("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421").into(),
|
||||
logs_bloom: hex!("00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000").to_vec(),
|
||||
prev_randao: hex!("6bf538bdfbdf1c96ff528726a40658a91d0bda0f1351448c4c4f3604db2a0ccf").into(),
|
||||
block_number: 477434,
|
||||
gas_limit: 8154925,
|
||||
gas_used: 0,
|
||||
timestamp: 1652816940,
|
||||
extra_data: vec![],
|
||||
base_fee_per_gas: U256::from(7_i16),
|
||||
block_hash: hex!("cd8df91b4503adb8f2f1c7a4f60e07a1f1a2cbdfa2a95bceba581f3ff65c1968").into(),
|
||||
transactions_root: hex!("7ffe241ea60187fdb0187bfa22de35d1f9bed7ab061d9401fd47e34a54fbede1").into(),
|
||||
withdrawals_root: hex!("28ba1834a3a7b657460ce79fa3a1d909ab8828fd557659d4d0554a9bdbc0ec30").into(),
|
||||
}.hash_tree_root();
|
||||
assert!(hash_root.is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
/// Operating modes for beacon client
|
||||
#[derive(Encode, Decode, Copy, Clone, PartialEq, RuntimeDebug, TypeInfo)]
|
||||
pub enum Mode {
|
||||
Active,
|
||||
Blocked,
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use codec::{Decode, Encode};
|
||||
use frame_support::{CloneNoBound, PartialEqNoBound, RuntimeDebugNoBound};
|
||||
use scale_info::TypeInfo;
|
||||
use sp_core::H256;
|
||||
use sp_std::prelude::*;
|
||||
|
||||
use crate::types::{BeaconHeader, ExecutionPayloadHeader, SyncAggregate, SyncCommittee};
|
||||
|
||||
#[derive(Encode, Decode, CloneNoBound, PartialEqNoBound, RuntimeDebugNoBound, TypeInfo)]
|
||||
#[cfg_attr(
|
||||
feature = "std",
|
||||
derive(serde::Serialize, serde::Deserialize),
|
||||
serde(deny_unknown_fields, bound(serialize = ""), bound(deserialize = ""))
|
||||
)]
|
||||
pub struct CheckpointUpdate<const COMMITTEE_SIZE: usize> {
|
||||
pub header: BeaconHeader,
|
||||
pub current_sync_committee: SyncCommittee<COMMITTEE_SIZE>,
|
||||
pub current_sync_committee_branch: Vec<H256>,
|
||||
pub validators_root: H256,
|
||||
pub block_roots_root: H256,
|
||||
pub block_roots_branch: Vec<H256>,
|
||||
}
|
||||
|
||||
impl<const COMMITTEE_SIZE: usize> Default for CheckpointUpdate<COMMITTEE_SIZE> {
|
||||
fn default() -> Self {
|
||||
CheckpointUpdate {
|
||||
header: Default::default(),
|
||||
current_sync_committee: Default::default(),
|
||||
current_sync_committee_branch: Default::default(),
|
||||
validators_root: Default::default(),
|
||||
block_roots_root: Default::default(),
|
||||
block_roots_branch: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Default, Encode, Decode, CloneNoBound, PartialEqNoBound, RuntimeDebugNoBound, TypeInfo,
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "std",
|
||||
derive(serde::Deserialize),
|
||||
serde(deny_unknown_fields, bound(serialize = ""), bound(deserialize = ""))
|
||||
)]
|
||||
pub struct Update<const COMMITTEE_SIZE: usize, const COMMITTEE_BITS_SIZE: usize> {
|
||||
/// A recent header attesting to the finalized header, using its `state_root`.
|
||||
pub attested_header: BeaconHeader,
|
||||
/// The signing data that the sync committee produced for this attested header, including
|
||||
/// who participated in the vote and the resulting signature.
|
||||
pub sync_aggregate: SyncAggregate<COMMITTEE_SIZE, COMMITTEE_BITS_SIZE>,
|
||||
/// The slot at which the sync aggregate can be found, typically attested_header.slot + 1, if
|
||||
/// the next slot block was not missed.
|
||||
pub signature_slot: u64,
|
||||
/// The next sync committee for the next sync committee period, if present.
|
||||
pub next_sync_committee_update: Option<NextSyncCommitteeUpdate<COMMITTEE_SIZE>>,
|
||||
/// The latest finalized header.
|
||||
pub finalized_header: BeaconHeader,
|
||||
/// The merkle proof testifying to the finalized header, using the `attested_header.state_root`
|
||||
/// as tree root.
|
||||
pub finality_branch: Vec<H256>,
|
||||
/// The finalized_header's `block_roots` root in the beacon state, used for ancestry proofs.
|
||||
pub block_roots_root: H256,
|
||||
/// The merkle path to prove the `block_roots_root` value.
|
||||
pub block_roots_branch: Vec<H256>,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Default, Encode, Decode, CloneNoBound, PartialEqNoBound, RuntimeDebugNoBound, TypeInfo,
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "std",
|
||||
derive(serde::Deserialize),
|
||||
serde(deny_unknown_fields, bound(serialize = ""), bound(deserialize = ""))
|
||||
)]
|
||||
pub struct NextSyncCommitteeUpdate<const COMMITTEE_SIZE: usize> {
|
||||
pub next_sync_committee: SyncCommittee<COMMITTEE_SIZE>,
|
||||
pub next_sync_committee_branch: Vec<H256>,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, CloneNoBound, PartialEqNoBound, RuntimeDebugNoBound, TypeInfo)]
|
||||
#[cfg_attr(
|
||||
feature = "std",
|
||||
derive(serde::Deserialize),
|
||||
serde(deny_unknown_fields, bound(serialize = ""), bound(deserialize = ""))
|
||||
)]
|
||||
pub struct ExecutionHeaderUpdate {
|
||||
/// Header for the beacon block containing the execution payload
|
||||
pub header: BeaconHeader,
|
||||
/// Proof that `header` is an ancestor of a finalized header
|
||||
pub ancestry_proof: Option<AncestryProof>,
|
||||
/// Execution header to be imported
|
||||
pub execution_header: ExecutionPayloadHeader,
|
||||
/// Merkle proof that execution payload is contained within `header`
|
||||
pub execution_branch: Vec<H256>,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, CloneNoBound, PartialEqNoBound, RuntimeDebugNoBound, TypeInfo)]
|
||||
#[cfg_attr(
|
||||
feature = "std",
|
||||
derive(serde::Deserialize),
|
||||
serde(deny_unknown_fields, bound(serialize = ""), bound(deserialize = ""))
|
||||
)]
|
||||
pub struct AncestryProof {
|
||||
/// Merkle proof that `header` is an ancestor of `finalized_header`
|
||||
pub header_branch: Vec<H256>,
|
||||
/// Root of a finalized block that has already been imported into the light client
|
||||
pub finalized_block_root: H256,
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
[package]
|
||||
name = "snowbridge-core"
|
||||
description = "Snowbridge Core"
|
||||
version = "0.1.1"
|
||||
authors = ["Snowfork <contact@snowfork.com>"]
|
||||
edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0.188", optional = true, features = ["alloc", "derive"], default-features = false }
|
||||
codec = { package = "parity-scale-codec", version = "3.6.1", default-features = false }
|
||||
scale-info = { version = "2.9.0", default-features = false, features = ["derive"] }
|
||||
hex-literal = { version = "0.4.1" }
|
||||
|
||||
polkadot-parachain-primitives = { path = "../../../../../polkadot/parachain", default-features = false }
|
||||
xcm = { package = "staging-xcm", path = "../../../../../polkadot/xcm", default-features = false }
|
||||
xcm-builder = { package = "staging-xcm-builder", path = "../../../../../polkadot/xcm/xcm-builder", default-features = false }
|
||||
|
||||
frame-support = { path = "../../../../../substrate/frame/support", default-features = false }
|
||||
frame-system = { path = "../../../../../substrate/frame/system", default-features = false }
|
||||
sp-runtime = { path = "../../../../../substrate/primitives/runtime", default-features = false }
|
||||
sp-std = { path = "../../../../../substrate/primitives/std", default-features = false }
|
||||
sp-io = { path = "../../../../../substrate/primitives/io", default-features = false }
|
||||
sp-core = { path = "../../../../../substrate/primitives/core", default-features = false }
|
||||
sp-arithmetic = { path = "../../../../../substrate/primitives/arithmetic", default-features = false }
|
||||
|
||||
snowbridge-beacon-primitives = { path = "../../primitives/beacon", default-features = false }
|
||||
|
||||
ethabi = { git = "https://github.com/Snowfork/ethabi-decode.git", package = "ethabi-decode", branch = "master", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
hex = { version = "0.4.3" }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"codec/std",
|
||||
"ethabi/std",
|
||||
"frame-support/std",
|
||||
"frame-system/std",
|
||||
"polkadot-parachain-primitives/std",
|
||||
"scale-info/std",
|
||||
"serde/std",
|
||||
"snowbridge-beacon-primitives/std",
|
||||
"sp-arithmetic/std",
|
||||
"sp-core/std",
|
||||
"sp-io/std",
|
||||
"sp-runtime/std",
|
||||
"sp-std/std",
|
||||
"xcm-builder/std",
|
||||
"xcm/std",
|
||||
]
|
||||
serde = ["dep:serde", "scale-info/serde"]
|
||||
runtime-benchmarks = [
|
||||
"frame-support/runtime-benchmarks",
|
||||
"frame-system/runtime-benchmarks",
|
||||
"polkadot-parachain-primitives/runtime-benchmarks",
|
||||
"sp-runtime/runtime-benchmarks",
|
||||
"xcm-builder/runtime-benchmarks",
|
||||
]
|
||||
@@ -0,0 +1,74 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
//! Types for representing inbound messages
|
||||
|
||||
use codec::{Decode, Encode};
|
||||
use frame_support::PalletError;
|
||||
use scale_info::TypeInfo;
|
||||
use sp_core::{H160, H256};
|
||||
use sp_runtime::RuntimeDebug;
|
||||
use sp_std::vec::Vec;
|
||||
|
||||
/// A trait for verifying inbound messages from Ethereum.
|
||||
pub trait Verifier {
|
||||
fn verify(event: &Log, proof: &Proof) -> Result<(), VerificationError>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Encode, Decode, RuntimeDebug, PalletError, TypeInfo)]
|
||||
#[cfg_attr(feature = "std", derive(PartialEq))]
|
||||
pub enum VerificationError {
|
||||
/// Execution header is missing
|
||||
HeaderNotFound,
|
||||
/// Event log was not found in the verified transaction receipt
|
||||
LogNotFound,
|
||||
/// Event log has an invalid format
|
||||
InvalidLog,
|
||||
/// Unable to verify the transaction receipt with the provided proof
|
||||
InvalidProof,
|
||||
}
|
||||
|
||||
pub type MessageNonce = u64;
|
||||
|
||||
/// A bridge message from the Gateway contract on Ethereum
|
||||
#[derive(Clone, Encode, Decode, PartialEq, RuntimeDebug, TypeInfo)]
|
||||
pub struct Message {
|
||||
/// Event log emitted by Gateway contract
|
||||
pub event_log: Log,
|
||||
/// Inclusion proof for a transaction receipt containing the event log
|
||||
pub proof: Proof,
|
||||
}
|
||||
|
||||
const MAX_TOPICS: usize = 4;
|
||||
|
||||
#[derive(Clone, RuntimeDebug)]
|
||||
pub enum LogValidationError {
|
||||
TooManyTopics,
|
||||
}
|
||||
|
||||
/// Event log
|
||||
#[derive(Clone, Encode, Decode, PartialEq, RuntimeDebug, TypeInfo)]
|
||||
pub struct Log {
|
||||
pub address: H160,
|
||||
pub topics: Vec<H256>,
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Log {
|
||||
pub fn validate(&self) -> Result<(), LogValidationError> {
|
||||
if self.topics.len() > MAX_TOPICS {
|
||||
return Err(LogValidationError::TooManyTopics)
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Inclusion proof for a transaction receipt
|
||||
#[derive(Clone, Encode, Decode, PartialEq, RuntimeDebug, TypeInfo)]
|
||||
pub struct Proof {
|
||||
// The block hash of the block in which the receipt was included.
|
||||
pub block_hash: H256,
|
||||
// The index of the transaction (and receipt) within the block.
|
||||
pub tx_index: u32,
|
||||
// Proof keys and values (receipts tree)
|
||||
pub data: (Vec<Vec<u8>>, Vec<Vec<u8>>),
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
//! # Core
|
||||
//!
|
||||
//! Common traits and types
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub mod inbound;
|
||||
pub mod operating_mode;
|
||||
pub mod outbound;
|
||||
pub mod pricing;
|
||||
pub mod ringbuffer;
|
||||
|
||||
pub use polkadot_parachain_primitives::primitives::{
|
||||
Id as ParaId, IsSystem, Sibling as SiblingParaId,
|
||||
};
|
||||
pub use ringbuffer::{RingBufferMap, RingBufferMapImpl};
|
||||
pub use sp_core::U256;
|
||||
|
||||
use codec::{Decode, Encode, MaxEncodedLen};
|
||||
use frame_support::traits::Contains;
|
||||
use hex_literal::hex;
|
||||
use scale_info::TypeInfo;
|
||||
use sp_core::H256;
|
||||
use sp_io::hashing::keccak_256;
|
||||
use sp_runtime::{traits::AccountIdConversion, RuntimeDebug};
|
||||
use sp_std::prelude::*;
|
||||
use xcm::prelude::{
|
||||
Junction::Parachain,
|
||||
Junctions::{Here, X1},
|
||||
MultiLocation,
|
||||
};
|
||||
use xcm_builder::{DescribeAllTerminal, DescribeFamily, DescribeLocation, HashedDescription};
|
||||
|
||||
/// The ID of an agent contract
|
||||
pub type AgentId = H256;
|
||||
pub use operating_mode::BasicOperatingMode;
|
||||
|
||||
pub use pricing::{PricingParameters, Rewards};
|
||||
|
||||
pub fn sibling_sovereign_account<T>(para_id: ParaId) -> T::AccountId
|
||||
where
|
||||
T: frame_system::Config,
|
||||
{
|
||||
SiblingParaId::from(para_id).into_account_truncating()
|
||||
}
|
||||
|
||||
pub fn sibling_sovereign_account_raw(para_id: ParaId) -> [u8; 32] {
|
||||
SiblingParaId::from(para_id).into_account_truncating()
|
||||
}
|
||||
|
||||
pub struct AllowSiblingsOnly;
|
||||
impl Contains<MultiLocation> for AllowSiblingsOnly {
|
||||
fn contains(location: &MultiLocation) -> bool {
|
||||
matches!(location, MultiLocation { parents: 1, interior: X1(Parachain(_)) })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gwei(x: u128) -> U256 {
|
||||
U256::from(1_000_000_000u128).saturating_mul(x.into())
|
||||
}
|
||||
|
||||
pub fn meth(x: u128) -> U256 {
|
||||
U256::from(1_000_000_000_000_000u128).saturating_mul(x.into())
|
||||
}
|
||||
|
||||
pub fn eth(x: u128) -> U256 {
|
||||
U256::from(1_000_000_000_000_000_000u128).saturating_mul(x.into())
|
||||
}
|
||||
|
||||
pub const ROC: u128 = 1_000_000_000_000;
|
||||
|
||||
/// Identifier for a message channel
|
||||
#[derive(
|
||||
Clone, Copy, Encode, Decode, PartialEq, Eq, Default, RuntimeDebug, MaxEncodedLen, TypeInfo,
|
||||
)]
|
||||
pub struct ChannelId([u8; 32]);
|
||||
|
||||
/// Deterministically derive a ChannelId for a sibling parachain
|
||||
/// Generator: keccak256("para" + big_endian_bytes(para_id))
|
||||
///
|
||||
/// The equivalent generator on the Solidity side is in
|
||||
/// contracts/src/Types.sol:into().
|
||||
fn derive_channel_id_for_sibling(para_id: ParaId) -> ChannelId {
|
||||
let para_id: u32 = para_id.into();
|
||||
let para_id_bytes: [u8; 4] = para_id.to_be_bytes();
|
||||
let prefix: [u8; 4] = *b"para";
|
||||
let preimage: Vec<u8> = prefix.into_iter().chain(para_id_bytes).collect();
|
||||
keccak_256(&preimage).into()
|
||||
}
|
||||
|
||||
impl ChannelId {
|
||||
pub const fn new(id: [u8; 32]) -> Self {
|
||||
ChannelId(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ParaId> for ChannelId {
|
||||
fn from(value: ParaId) -> Self {
|
||||
derive_channel_id_for_sibling(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[u8; 32]> for ChannelId {
|
||||
fn from(value: [u8; 32]) -> Self {
|
||||
ChannelId(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ChannelId> for [u8; 32] {
|
||||
fn from(value: ChannelId) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a [u8; 32]> for ChannelId {
|
||||
fn from(value: &'a [u8; 32]) -> Self {
|
||||
ChannelId(*value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<H256> for ChannelId {
|
||||
fn from(value: H256) -> Self {
|
||||
ChannelId(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for ChannelId {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Encode, Decode, RuntimeDebug, MaxEncodedLen, TypeInfo)]
|
||||
pub struct Channel {
|
||||
/// ID of the agent contract deployed on Ethereum
|
||||
pub agent_id: AgentId,
|
||||
/// ID of the parachain who will receive or send messages using this channel
|
||||
pub para_id: ParaId,
|
||||
}
|
||||
|
||||
pub trait StaticLookup {
|
||||
/// Type to lookup from.
|
||||
type Source;
|
||||
/// Type to lookup into.
|
||||
type Target;
|
||||
/// Attempt a lookup.
|
||||
fn lookup(s: Self::Source) -> Option<Self::Target>;
|
||||
}
|
||||
|
||||
/// Channel for high-priority governance commands
|
||||
pub const PRIMARY_GOVERNANCE_CHANNEL: ChannelId =
|
||||
ChannelId::new(hex!("0000000000000000000000000000000000000000000000000000000000000001"));
|
||||
|
||||
/// Channel for lower-priority governance commands
|
||||
pub const SECONDARY_GOVERNANCE_CHANNEL: ChannelId =
|
||||
ChannelId::new(hex!("0000000000000000000000000000000000000000000000000000000000000002"));
|
||||
|
||||
pub struct DescribeHere;
|
||||
impl DescribeLocation for DescribeHere {
|
||||
fn describe_location(l: &MultiLocation) -> Option<Vec<u8>> {
|
||||
match (l.parents, l.interior) {
|
||||
(0, Here) => Some(Vec::<u8>::new().encode()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an AgentId from a MultiLocation. An AgentId is a unique mapping to a Agent contract on
|
||||
/// Ethereum which acts as the sovereign account for the MultiLocation.
|
||||
pub type AgentIdOf = HashedDescription<H256, (DescribeHere, DescribeFamily<DescribeAllTerminal>)>;
|
||||
@@ -0,0 +1,25 @@
|
||||
use codec::{Decode, Encode, MaxEncodedLen};
|
||||
use scale_info::TypeInfo;
|
||||
use sp_runtime::RuntimeDebug;
|
||||
|
||||
/// Basic operating modes for a bridges module (Normal/Halted).
|
||||
#[derive(Encode, Decode, Clone, Copy, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum BasicOperatingMode {
|
||||
/// Normal mode, when all operations are allowed.
|
||||
Normal,
|
||||
/// The pallet is halted. All non-governance operations are disabled.
|
||||
Halted,
|
||||
}
|
||||
|
||||
impl Default for BasicOperatingMode {
|
||||
fn default() -> Self {
|
||||
Self::Normal
|
||||
}
|
||||
}
|
||||
|
||||
impl BasicOperatingMode {
|
||||
pub fn is_halted(&self) -> bool {
|
||||
*self == BasicOperatingMode::Halted
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
use codec::{Decode, Encode};
|
||||
use frame_support::PalletError;
|
||||
use scale_info::TypeInfo;
|
||||
use sp_arithmetic::traits::{BaseArithmetic, Unsigned};
|
||||
use sp_core::{RuntimeDebug, H256};
|
||||
pub use v1::{AgentExecuteCommand, Command, Initializer, Message, OperatingMode, QueuedMessage};
|
||||
|
||||
/// Enqueued outbound messages need to be versioned to prevent data corruption
|
||||
/// or loss after forkless runtime upgrades
|
||||
#[derive(Encode, Decode, TypeInfo, Clone, RuntimeDebug)]
|
||||
#[cfg_attr(feature = "std", derive(PartialEq))]
|
||||
pub enum VersionedQueuedMessage {
|
||||
V1(QueuedMessage),
|
||||
}
|
||||
|
||||
impl TryFrom<VersionedQueuedMessage> for QueuedMessage {
|
||||
type Error = ();
|
||||
fn try_from(x: VersionedQueuedMessage) -> Result<Self, Self::Error> {
|
||||
use VersionedQueuedMessage::*;
|
||||
match x {
|
||||
V1(x) => Ok(x),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Into<QueuedMessage>> From<T> for VersionedQueuedMessage {
|
||||
fn from(x: T) -> Self {
|
||||
VersionedQueuedMessage::V1(x.into())
|
||||
}
|
||||
}
|
||||
|
||||
mod v1 {
|
||||
use crate::{pricing::UD60x18, ChannelId};
|
||||
use codec::{Decode, Encode};
|
||||
use ethabi::Token;
|
||||
use scale_info::TypeInfo;
|
||||
use sp_core::{RuntimeDebug, H160, H256, U256};
|
||||
use sp_std::{borrow::ToOwned, vec, vec::Vec};
|
||||
|
||||
/// A message which can be accepted by implementations of `/[`SendMessage`\]`
|
||||
#[derive(Encode, Decode, TypeInfo, Clone, RuntimeDebug)]
|
||||
#[cfg_attr(feature = "std", derive(PartialEq))]
|
||||
pub struct Message {
|
||||
/// ID for this message. One will be automatically generated if not provided.
|
||||
///
|
||||
/// When this message is created from an XCM message, the ID should be extracted
|
||||
/// from the `SetTopic` instruction.
|
||||
///
|
||||
/// The ID plays no role in bridge consensus, and is purely meant for message tracing.
|
||||
pub id: Option<H256>,
|
||||
/// The message channel ID
|
||||
pub channel_id: ChannelId,
|
||||
/// The stable ID for a receiving gateway contract
|
||||
pub command: Command,
|
||||
}
|
||||
|
||||
/// The operating mode of Channels and Gateway contract on Ethereum.
|
||||
#[derive(Copy, Clone, Encode, Decode, PartialEq, Eq, RuntimeDebug, TypeInfo)]
|
||||
pub enum OperatingMode {
|
||||
/// Normal operations. Allow sending and receiving messages.
|
||||
Normal,
|
||||
/// Reject outbound messages. This allows receiving governance messages but does now allow
|
||||
/// enqueuing of new messages from the Ethereum side. This can be used to close off an
|
||||
/// deprecated channel or pause the bridge for upgrade operations.
|
||||
RejectingOutboundMessages,
|
||||
}
|
||||
|
||||
/// A command which is executable by the Gateway contract on Ethereum
|
||||
#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo)]
|
||||
#[cfg_attr(feature = "std", derive(PartialEq))]
|
||||
pub enum Command {
|
||||
/// Execute a sub-command within an agent for a consensus system in Polkadot
|
||||
AgentExecute {
|
||||
/// The ID of the agent
|
||||
agent_id: H256,
|
||||
/// The sub-command to be executed
|
||||
command: AgentExecuteCommand,
|
||||
},
|
||||
/// Upgrade the Gateway contract
|
||||
Upgrade {
|
||||
/// Address of the new implementation contract
|
||||
impl_address: H160,
|
||||
/// Codehash of the implementation contract
|
||||
impl_code_hash: H256,
|
||||
/// Optionally invoke an initializer in the implementation contract
|
||||
initializer: Option<Initializer>,
|
||||
},
|
||||
/// Create an agent representing a consensus system on Polkadot
|
||||
CreateAgent {
|
||||
/// The ID of the agent, derived from the `MultiLocation` of the consensus system on
|
||||
/// Polkadot
|
||||
agent_id: H256,
|
||||
},
|
||||
/// Create bidirectional messaging channel to a parachain
|
||||
CreateChannel {
|
||||
/// The ID of the channel
|
||||
channel_id: ChannelId,
|
||||
/// The agent ID of the parachain
|
||||
agent_id: H256,
|
||||
/// Initial operating mode
|
||||
mode: OperatingMode,
|
||||
},
|
||||
/// Update the configuration of a channel
|
||||
UpdateChannel {
|
||||
/// The ID of the channel
|
||||
channel_id: ChannelId,
|
||||
/// The new operating mode
|
||||
mode: OperatingMode,
|
||||
},
|
||||
/// Set the global operating mode of the Gateway contract
|
||||
SetOperatingMode {
|
||||
/// The new operating mode
|
||||
mode: OperatingMode,
|
||||
},
|
||||
/// Transfer ether from an agent contract to a recipient account
|
||||
TransferNativeFromAgent {
|
||||
/// The agent ID
|
||||
agent_id: H256,
|
||||
/// The recipient of the ether
|
||||
recipient: H160,
|
||||
/// The amount to transfer
|
||||
amount: u128,
|
||||
},
|
||||
/// Set token fees of the Gateway contract
|
||||
SetTokenTransferFees {
|
||||
/// The fee(DOT) for the cost of creating asset on AssetHub
|
||||
create_asset_xcm: u128,
|
||||
/// The fee(DOT) for the cost of sending asset on AssetHub
|
||||
transfer_asset_xcm: u128,
|
||||
/// The fee(Ether) for register token to discourage spamming
|
||||
register_token: U256,
|
||||
},
|
||||
/// Set pricing parameters
|
||||
SetPricingParameters {
|
||||
// ETH/DOT exchange rate
|
||||
exchange_rate: UD60x18,
|
||||
// Cost of delivering a message from Ethereum to BridgeHub, in ROC/KSM/DOT
|
||||
delivery_cost: u128,
|
||||
},
|
||||
}
|
||||
|
||||
impl Command {
|
||||
/// Compute the enum variant index
|
||||
pub fn index(&self) -> u8 {
|
||||
match self {
|
||||
Command::AgentExecute { .. } => 0,
|
||||
Command::Upgrade { .. } => 1,
|
||||
Command::CreateAgent { .. } => 2,
|
||||
Command::CreateChannel { .. } => 3,
|
||||
Command::UpdateChannel { .. } => 4,
|
||||
Command::SetOperatingMode { .. } => 5,
|
||||
Command::TransferNativeFromAgent { .. } => 6,
|
||||
Command::SetTokenTransferFees { .. } => 7,
|
||||
Command::SetPricingParameters { .. } => 8,
|
||||
}
|
||||
}
|
||||
|
||||
/// ABI-encode the Command.
|
||||
pub fn abi_encode(&self) -> Vec<u8> {
|
||||
match self {
|
||||
Command::AgentExecute { agent_id, command } =>
|
||||
ethabi::encode(&[Token::Tuple(vec![
|
||||
Token::FixedBytes(agent_id.as_bytes().to_owned()),
|
||||
Token::Bytes(command.abi_encode()),
|
||||
])]),
|
||||
Command::Upgrade { impl_address, impl_code_hash, initializer, .. } =>
|
||||
ethabi::encode(&[Token::Tuple(vec![
|
||||
Token::Address(*impl_address),
|
||||
Token::FixedBytes(impl_code_hash.as_bytes().to_owned()),
|
||||
initializer
|
||||
.clone()
|
||||
.map_or(Token::Bytes(vec![]), |i| Token::Bytes(i.params)),
|
||||
])]),
|
||||
Command::CreateAgent { agent_id } =>
|
||||
ethabi::encode(&[Token::Tuple(vec![Token::FixedBytes(
|
||||
agent_id.as_bytes().to_owned(),
|
||||
)])]),
|
||||
Command::CreateChannel { channel_id, agent_id, mode } =>
|
||||
ethabi::encode(&[Token::Tuple(vec![
|
||||
Token::FixedBytes(channel_id.as_ref().to_owned()),
|
||||
Token::FixedBytes(agent_id.as_bytes().to_owned()),
|
||||
Token::Uint(U256::from((*mode) as u64)),
|
||||
])]),
|
||||
Command::UpdateChannel { channel_id, mode } =>
|
||||
ethabi::encode(&[Token::Tuple(vec![
|
||||
Token::FixedBytes(channel_id.as_ref().to_owned()),
|
||||
Token::Uint(U256::from((*mode) as u64)),
|
||||
])]),
|
||||
Command::SetOperatingMode { mode } =>
|
||||
ethabi::encode(&[Token::Tuple(vec![Token::Uint(U256::from((*mode) as u64))])]),
|
||||
Command::TransferNativeFromAgent { agent_id, recipient, amount } =>
|
||||
ethabi::encode(&[Token::Tuple(vec![
|
||||
Token::FixedBytes(agent_id.as_bytes().to_owned()),
|
||||
Token::Address(*recipient),
|
||||
Token::Uint(U256::from(*amount)),
|
||||
])]),
|
||||
Command::SetTokenTransferFees {
|
||||
create_asset_xcm,
|
||||
transfer_asset_xcm,
|
||||
register_token,
|
||||
} => ethabi::encode(&[Token::Tuple(vec![
|
||||
Token::Uint(U256::from(*create_asset_xcm)),
|
||||
Token::Uint(U256::from(*transfer_asset_xcm)),
|
||||
Token::Uint(*register_token),
|
||||
])]),
|
||||
Command::SetPricingParameters { exchange_rate, delivery_cost } =>
|
||||
ethabi::encode(&[Token::Tuple(vec![
|
||||
Token::Uint(exchange_rate.clone().into_inner()),
|
||||
Token::Uint(U256::from(*delivery_cost)),
|
||||
])]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Representation of a call to the initializer of an implementation contract.
|
||||
/// The initializer has the following ABI signature: `initialize(bytes)`.
|
||||
#[derive(Clone, Encode, Decode, PartialEq, RuntimeDebug, TypeInfo)]
|
||||
pub struct Initializer {
|
||||
/// ABI-encoded params of type `bytes` to pass to the initializer
|
||||
pub params: Vec<u8>,
|
||||
/// The initializer is allowed to consume this much gas at most.
|
||||
pub maximum_required_gas: u64,
|
||||
}
|
||||
|
||||
/// A Sub-command executable within an agent
|
||||
#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo)]
|
||||
#[cfg_attr(feature = "std", derive(PartialEq))]
|
||||
pub enum AgentExecuteCommand {
|
||||
/// Transfer ERC20 tokens
|
||||
TransferToken {
|
||||
/// Address of the ERC20 token
|
||||
token: H160,
|
||||
/// The recipient of the tokens
|
||||
recipient: H160,
|
||||
/// The amount of tokens to transfer
|
||||
amount: u128,
|
||||
},
|
||||
}
|
||||
|
||||
impl AgentExecuteCommand {
|
||||
fn index(&self) -> u8 {
|
||||
match self {
|
||||
AgentExecuteCommand::TransferToken { .. } => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// ABI-encode the sub-command
|
||||
pub fn abi_encode(&self) -> Vec<u8> {
|
||||
match self {
|
||||
AgentExecuteCommand::TransferToken { token, recipient, amount } =>
|
||||
ethabi::encode(&[
|
||||
Token::Uint(self.index().into()),
|
||||
Token::Bytes(ethabi::encode(&[
|
||||
Token::Address(*token),
|
||||
Token::Address(*recipient),
|
||||
Token::Uint(U256::from(*amount)),
|
||||
])),
|
||||
]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Message which is awaiting processing in the MessageQueue pallet
|
||||
#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo)]
|
||||
#[cfg_attr(feature = "std", derive(PartialEq))]
|
||||
pub struct QueuedMessage {
|
||||
/// Message ID
|
||||
pub id: H256,
|
||||
/// Channel ID
|
||||
pub channel_id: ChannelId,
|
||||
/// Command to execute in the Gateway contract
|
||||
pub command: Command,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "std", derive(PartialEq, Debug))]
|
||||
/// Fee for delivering message
|
||||
pub struct Fee<Balance>
|
||||
where
|
||||
Balance: BaseArithmetic + Unsigned + Copy,
|
||||
{
|
||||
/// Fee to cover cost of processing the message locally
|
||||
pub local: Balance,
|
||||
/// Fee to cover cost processing the message remotely
|
||||
pub remote: Balance,
|
||||
}
|
||||
|
||||
impl<Balance> Fee<Balance>
|
||||
where
|
||||
Balance: BaseArithmetic + Unsigned + Copy,
|
||||
{
|
||||
pub fn total(&self) -> Balance {
|
||||
self.local.saturating_add(self.remote)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Balance> From<(Balance, Balance)> for Fee<Balance>
|
||||
where
|
||||
Balance: BaseArithmetic + Unsigned + Copy,
|
||||
{
|
||||
fn from((local, remote): (Balance, Balance)) -> Self {
|
||||
Self { local, remote }
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for sending messages to Ethereum
|
||||
pub trait SendMessage: SendMessageFeeProvider {
|
||||
type Ticket: Clone + Encode + Decode;
|
||||
|
||||
/// Validate an outbound message and return a tuple:
|
||||
/// 1. Ticket for submitting the message
|
||||
/// 2. Delivery fee
|
||||
fn validate(
|
||||
message: &Message,
|
||||
) -> Result<(Self::Ticket, Fee<<Self as SendMessageFeeProvider>::Balance>), SendError>;
|
||||
|
||||
/// Submit the message ticket for eventual delivery to Ethereum
|
||||
fn deliver(ticket: Self::Ticket) -> Result<H256, SendError>;
|
||||
}
|
||||
|
||||
pub trait Ticket: Encode + Decode + Clone {
|
||||
fn message_id(&self) -> H256;
|
||||
}
|
||||
|
||||
/// A trait for getting the local costs associated with sending a message.
|
||||
pub trait SendMessageFeeProvider {
|
||||
type Balance: BaseArithmetic + Unsigned + Copy;
|
||||
|
||||
/// The local component of the message processing fees in native currency
|
||||
fn local_fee() -> Self::Balance;
|
||||
}
|
||||
|
||||
/// Reasons why sending to Ethereum could not be initiated
|
||||
#[derive(Copy, Clone, Encode, Decode, PartialEq, Eq, RuntimeDebug, PalletError, TypeInfo)]
|
||||
pub enum SendError {
|
||||
/// Message is too large to be safely executed on Ethereum
|
||||
MessageTooLarge,
|
||||
/// The bridge has been halted for maintenance
|
||||
Halted,
|
||||
/// Invalid Channel
|
||||
InvalidChannel,
|
||||
}
|
||||
|
||||
pub trait GasMeter {
|
||||
/// All the gas used for submitting a message to Ethereum, minus the cost of dispatching
|
||||
/// the command within the message
|
||||
const MAXIMUM_BASE_GAS: u64;
|
||||
|
||||
fn maximum_gas_used_at_most(command: &Command) -> u64 {
|
||||
Self::MAXIMUM_BASE_GAS + Self::maximum_dispatch_gas_used_at_most(command)
|
||||
}
|
||||
|
||||
/// Measures the maximum amount of gas a command payload will require to dispatch, AFTER
|
||||
/// validation & verification.
|
||||
fn maximum_dispatch_gas_used_at_most(command: &Command) -> u64;
|
||||
}
|
||||
|
||||
/// A meter that assigns a constant amount of gas for the execution of a command
|
||||
///
|
||||
/// The gas figures are extracted from this report:
|
||||
/// > forge test --match-path test/Gateway.t.sol --gas-report
|
||||
///
|
||||
/// A healthy buffer is added on top of these figures to account for:
|
||||
/// * The EIP-150 63/64 rule
|
||||
/// * Future EVM upgrades that may increase gas cost
|
||||
pub struct ConstantGasMeter;
|
||||
|
||||
impl GasMeter for ConstantGasMeter {
|
||||
// The base transaction cost, which includes:
|
||||
// 21_000 transaction cost, roughly worst case 64_000 for calldata, and 100_000
|
||||
// for message verification
|
||||
const MAXIMUM_BASE_GAS: u64 = 185_000;
|
||||
|
||||
fn maximum_dispatch_gas_used_at_most(command: &Command) -> u64 {
|
||||
match command {
|
||||
Command::CreateAgent { .. } => 275_000,
|
||||
Command::CreateChannel { .. } => 100_000,
|
||||
Command::UpdateChannel { .. } => 50_000,
|
||||
Command::TransferNativeFromAgent { .. } => 60_000,
|
||||
Command::SetOperatingMode { .. } => 40_000,
|
||||
Command::AgentExecute { command, .. } => match command {
|
||||
// Execute IERC20.transferFrom
|
||||
//
|
||||
// Worst-case assumptions are important:
|
||||
// * No gas refund for clearing storage slot of source account in ERC20 contract
|
||||
// * Assume dest account in ERC20 contract does not yet have a storage slot
|
||||
// * ERC20.transferFrom possibly does other business logic besides updating balances
|
||||
AgentExecuteCommand::TransferToken { .. } => 100_000,
|
||||
},
|
||||
Command::Upgrade { initializer, .. } => {
|
||||
let initializer_max_gas = match *initializer {
|
||||
Some(Initializer { maximum_required_gas, .. }) => maximum_required_gas,
|
||||
None => 0,
|
||||
};
|
||||
// total maximum gas must also include the gas used for updating the proxy before
|
||||
// the the initializer is called.
|
||||
50_000 + initializer_max_gas
|
||||
},
|
||||
Command::SetTokenTransferFees { .. } => 60_000,
|
||||
Command::SetPricingParameters { .. } => 60_000,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GasMeter for () {
|
||||
const MAXIMUM_BASE_GAS: u64 = 1;
|
||||
|
||||
fn maximum_dispatch_gas_used_at_most(_: &Command) -> u64 {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
pub const ETHER_DECIMALS: u8 = 18;
|
||||
@@ -0,0 +1,67 @@
|
||||
use codec::{Decode, Encode, MaxEncodedLen};
|
||||
use scale_info::TypeInfo;
|
||||
use sp_arithmetic::traits::{BaseArithmetic, Unsigned, Zero};
|
||||
use sp_core::U256;
|
||||
use sp_runtime::{FixedU128, RuntimeDebug};
|
||||
use sp_std::prelude::*;
|
||||
|
||||
#[derive(Clone, Encode, Decode, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)]
|
||||
pub struct PricingParameters<Balance> {
|
||||
/// ETH/DOT exchange rate
|
||||
pub exchange_rate: FixedU128,
|
||||
/// Relayer rewards
|
||||
pub rewards: Rewards<Balance>,
|
||||
/// Ether (wei) fee per gas unit
|
||||
pub fee_per_gas: U256,
|
||||
}
|
||||
|
||||
#[derive(Clone, Encode, Decode, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)]
|
||||
pub struct Rewards<Balance> {
|
||||
/// Local reward in DOT
|
||||
pub local: Balance,
|
||||
/// Remote reward in ETH (wei)
|
||||
pub remote: U256,
|
||||
}
|
||||
|
||||
#[derive(RuntimeDebug)]
|
||||
pub struct InvalidPricingParameters;
|
||||
|
||||
impl<Balance> PricingParameters<Balance>
|
||||
where
|
||||
Balance: BaseArithmetic + Unsigned + Copy,
|
||||
{
|
||||
pub fn validate(&self) -> Result<(), InvalidPricingParameters> {
|
||||
if self.exchange_rate == FixedU128::zero() {
|
||||
return Err(InvalidPricingParameters)
|
||||
}
|
||||
if self.fee_per_gas == U256::zero() {
|
||||
return Err(InvalidPricingParameters)
|
||||
}
|
||||
if self.rewards.local.is_zero() {
|
||||
return Err(InvalidPricingParameters)
|
||||
}
|
||||
if self.rewards.remote.is_zero() {
|
||||
return Err(InvalidPricingParameters)
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Holder for fixed point number implemented in <https://github.com/PaulRBerg/prb-math>
|
||||
#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo)]
|
||||
#[cfg_attr(feature = "std", derive(PartialEq))]
|
||||
pub struct UD60x18(U256);
|
||||
|
||||
impl From<FixedU128> for UD60x18 {
|
||||
fn from(value: FixedU128) -> Self {
|
||||
// Both FixedU128 and UD60x18 have 18 decimal places
|
||||
let inner: u128 = value.into_inner();
|
||||
UD60x18(inner.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl UD60x18 {
|
||||
pub fn into_inner(self) -> U256 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use codec::FullCodec;
|
||||
use core::{cmp::Ord, marker::PhantomData, ops::Add};
|
||||
use frame_support::storage::{types::QueryKindTrait, StorageMap, StorageValue};
|
||||
use sp_core::{Get, GetDefault};
|
||||
use sp_runtime::traits::{One, Zero};
|
||||
|
||||
/// Trait object presenting the ringbuffer interface.
|
||||
pub trait RingBufferMap<Key, Value, QueryKind>
|
||||
where
|
||||
Key: FullCodec,
|
||||
Value: FullCodec,
|
||||
QueryKind: QueryKindTrait<Value, GetDefault>,
|
||||
{
|
||||
/// Insert a map entry.
|
||||
fn insert(k: Key, v: Value);
|
||||
|
||||
/// Check if map contains a key
|
||||
fn contains_key(k: Key) -> bool;
|
||||
|
||||
/// Get the value of the key
|
||||
fn get(k: Key) -> QueryKind::Query;
|
||||
}
|
||||
|
||||
pub struct RingBufferMapImpl<Index, B, CurrentIndex, Intermediate, M, QueryKind>(
|
||||
PhantomData<(Index, B, CurrentIndex, Intermediate, M, QueryKind)>,
|
||||
);
|
||||
|
||||
/// Ringbuffer implementation based on `RingBufferTransient`
|
||||
impl<Key, Value, Index, B, CurrentIndex, Intermediate, M, QueryKind>
|
||||
RingBufferMap<Key, Value, QueryKind>
|
||||
for RingBufferMapImpl<Index, B, CurrentIndex, Intermediate, M, QueryKind>
|
||||
where
|
||||
Key: FullCodec + Clone,
|
||||
Value: FullCodec,
|
||||
Index: Ord + One + Zero + Add<Output = Index> + Copy + FullCodec + Eq,
|
||||
B: Get<Index>,
|
||||
CurrentIndex: StorageValue<Index, Query = Index>,
|
||||
Intermediate: StorageMap<Index, Key, Query = Key>,
|
||||
M: StorageMap<Key, Value, Query = QueryKind::Query>,
|
||||
QueryKind: QueryKindTrait<Value, GetDefault>,
|
||||
{
|
||||
/// Insert a map entry.
|
||||
fn insert(k: Key, v: Value) {
|
||||
let bound = B::get();
|
||||
let mut current_index = CurrentIndex::get();
|
||||
|
||||
// Adding one here as bound denotes number of items but our index starts with zero.
|
||||
if (current_index + Index::one()) >= bound {
|
||||
current_index = Index::zero();
|
||||
} else {
|
||||
current_index = current_index + Index::one();
|
||||
}
|
||||
|
||||
// Deleting earlier entry if it exists
|
||||
if Intermediate::contains_key(current_index) {
|
||||
let older_key = Intermediate::get(current_index);
|
||||
M::remove(older_key);
|
||||
}
|
||||
|
||||
Intermediate::insert(current_index, k.clone());
|
||||
CurrentIndex::set(current_index);
|
||||
M::insert(k, v);
|
||||
}
|
||||
|
||||
/// Check if map contains a key
|
||||
fn contains_key(k: Key) -> bool {
|
||||
M::contains_key(k)
|
||||
}
|
||||
|
||||
/// Get the value associated with key
|
||||
fn get(k: Key) -> M::Query {
|
||||
M::get(k)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
use crate::{ChannelId, ParaId};
|
||||
use hex_literal::hex;
|
||||
|
||||
const EXPECT_CHANNEL_ID: [u8; 32] =
|
||||
hex!("c173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539");
|
||||
|
||||
// The Solidity equivalent code is tested in Gateway.t.sol:testDeriveChannelID
|
||||
#[test]
|
||||
fn generate_channel_id() {
|
||||
let para_id: ParaId = 1000.into();
|
||||
let channel_id: ChannelId = para_id.into();
|
||||
assert_eq!(channel_id, EXPECT_CHANNEL_ID.into());
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,14 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use frame_support::traits::Contains;
|
||||
use snowbridge_core::AllowSiblingsOnly;
|
||||
use xcm::prelude::{Junction::Parachain, Junctions::X1, MultiLocation};
|
||||
|
||||
#[test]
|
||||
fn allow_siblings_predicate_only_allows_siblings() {
|
||||
let sibling = MultiLocation::new(1, X1(Parachain(1000)));
|
||||
let child = MultiLocation::new(0, X1(Parachain(1000)));
|
||||
assert!(AllowSiblingsOnly::contains(&sibling), "Sibling returns true.");
|
||||
assert!(!AllowSiblingsOnly::contains(&child), "Child returns false.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
[target.wasm32-unknown-unknown]
|
||||
runner = 'wasm-bindgen-test-runner'
|
||||
@@ -0,0 +1,51 @@
|
||||
[package]
|
||||
name = "snowbridge-ethereum"
|
||||
description = "Snowbridge Ethereum"
|
||||
version = "0.1.0"
|
||||
authors = ["Snowfork <contact@snowfork.com>"]
|
||||
edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0.188", optional = true, features = ["derive"] }
|
||||
serde-big-array = { version = "0.3.2", optional = true, features = ["const-generics"] }
|
||||
codec = { package = "parity-scale-codec", version = "3.6.1", default-features = false, features = ["derive"] }
|
||||
scale-info = { version = "2.9.0", default-features = false, features = ["derive"] }
|
||||
ethbloom = { version = "0.13.0", default-features = false }
|
||||
ethereum-types = { version = "0.14.1", default-features = false, features = ["codec", "rlp", "serialize"] }
|
||||
hex = { package = "rustc-hex", version = "2.1.0", default-features = false }
|
||||
hex-literal = { version = "0.4.1", default-features = false }
|
||||
parity-bytes = { version = "0.1.2", default-features = false }
|
||||
rlp = { version = "0.5.2", default-features = false }
|
||||
|
||||
sp-io = { path = "../../../../../substrate/primitives/io", default-features = false }
|
||||
sp-std = { path = "../../../../../substrate/primitives/std", default-features = false }
|
||||
sp-core = { path = "../../../../../substrate/primitives/core", default-features = false }
|
||||
sp-runtime = { path = "../../../../../substrate/primitives/runtime", default-features = false }
|
||||
|
||||
ethabi = { git = "https://github.com/snowfork/ethabi-decode.git", package = "ethabi-decode", branch = "master", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.19"
|
||||
rand = "0.8.5"
|
||||
serde_json = "1.0.96"
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
expensive_tests = []
|
||||
std = [
|
||||
"codec/std",
|
||||
"ethabi/std",
|
||||
"ethbloom/std",
|
||||
"ethereum-types/std",
|
||||
"hex/std",
|
||||
"parity-bytes/std",
|
||||
"rlp/std",
|
||||
"scale-info/std",
|
||||
"serde",
|
||||
"serde-big-array",
|
||||
"sp-core/std",
|
||||
"sp-io/std",
|
||||
"sp-runtime/std",
|
||||
"sp-std/std",
|
||||
]
|
||||
@@ -0,0 +1,414 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use codec::{Decode, Encode};
|
||||
use ethbloom::Bloom as EthBloom;
|
||||
use hex_literal::hex;
|
||||
use parity_bytes::Bytes;
|
||||
use rlp::RlpStream;
|
||||
use scale_info::TypeInfo;
|
||||
use sp_io::hashing::keccak_256;
|
||||
use sp_runtime::RuntimeDebug;
|
||||
use sp_std::{convert::TryInto, prelude::*};
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
use serde_big_array::BigArray;
|
||||
|
||||
use ethereum_types::{Address, H256, H64, U256};
|
||||
|
||||
use crate::{mpt, receipt};
|
||||
|
||||
/// Complete block header id.
|
||||
#[derive(Clone, Copy, Default, Encode, Decode, PartialEq, RuntimeDebug, TypeInfo)]
|
||||
pub struct HeaderId {
|
||||
/// Header number.
|
||||
pub number: u64,
|
||||
/// Header hash.
|
||||
pub hash: H256,
|
||||
}
|
||||
|
||||
const EMPTY_OMMERS_HASH: [u8; 32] =
|
||||
hex!("1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347");
|
||||
|
||||
/// An Ethereum block header.
|
||||
#[derive(Clone, Default, Encode, Decode, PartialEq, RuntimeDebug, TypeInfo)]
|
||||
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
|
||||
pub struct Header {
|
||||
/// Parent block hash.
|
||||
pub parent_hash: H256,
|
||||
/// Block timestamp.
|
||||
pub timestamp: u64,
|
||||
/// Block number.
|
||||
pub number: u64,
|
||||
/// Block author.
|
||||
pub author: Address,
|
||||
|
||||
/// Transactions root.
|
||||
pub transactions_root: H256,
|
||||
/// Block ommers hash.
|
||||
pub ommers_hash: H256,
|
||||
/// Block extra data.
|
||||
pub extra_data: Bytes,
|
||||
|
||||
/// State root.
|
||||
pub state_root: H256,
|
||||
/// Block receipts root.
|
||||
pub receipts_root: H256,
|
||||
/// Block bloom.
|
||||
pub logs_bloom: Bloom,
|
||||
/// Gas used for contracts execution.
|
||||
pub gas_used: U256,
|
||||
/// Block gas limit.
|
||||
pub gas_limit: U256,
|
||||
|
||||
/// Block difficulty.
|
||||
pub difficulty: U256,
|
||||
/// Vector of post-RLP-encoded fields.
|
||||
pub seal: Vec<Bytes>,
|
||||
|
||||
// Base fee per gas (EIP-1559), only in headers from the London hardfork onwards.
|
||||
pub base_fee: Option<U256>,
|
||||
}
|
||||
|
||||
impl Header {
|
||||
/// Compute hash of this header (keccak of the RLP with seal).
|
||||
pub fn compute_hash(&self) -> H256 {
|
||||
keccak_256(&self.rlp(true)).into()
|
||||
}
|
||||
|
||||
/// Compute hash of the truncated header i.e. excluding seal.
|
||||
pub fn compute_partial_hash(&self) -> H256 {
|
||||
keccak_256(&self.rlp(false)).into()
|
||||
}
|
||||
|
||||
pub fn check_receipt_proof(
|
||||
&self,
|
||||
proof: &[Vec<u8>],
|
||||
) -> Option<Result<receipt::Receipt, rlp::DecoderError>> {
|
||||
match self.apply_merkle_proof(proof) {
|
||||
Some((root, data)) if root == self.receipts_root => Some(rlp::decode(&data)),
|
||||
Some((_, _)) => None,
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_merkle_proof(&self, proof: &[Vec<u8>]) -> Option<(H256, Vec<u8>)> {
|
||||
let mut iter = proof.iter().rev();
|
||||
let first_bytes = match iter.next() {
|
||||
Some(b) => b,
|
||||
None => return None,
|
||||
};
|
||||
let item_to_prove: mpt::ShortNode = rlp::decode(first_bytes).ok()?;
|
||||
|
||||
let final_hash: Option<[u8; 32]> = iter.try_fold(keccak_256(first_bytes), |acc, x| {
|
||||
let node: Box<dyn mpt::Node> = x.as_slice().try_into().ok()?;
|
||||
if (*node).contains_hash(acc.into()) {
|
||||
return Some(keccak_256(x))
|
||||
}
|
||||
None
|
||||
});
|
||||
|
||||
final_hash.map(|hash| (hash.into(), item_to_prove.value))
|
||||
}
|
||||
|
||||
pub fn mix_hash(&self) -> Option<H256> {
|
||||
let bytes: Bytes = self.decoded_seal_field(0, 32)?;
|
||||
let size = bytes.len();
|
||||
let mut mix_hash = [0u8; 32];
|
||||
for i in 0..size {
|
||||
mix_hash[31 - i] = bytes[size - 1 - i];
|
||||
}
|
||||
Some(mix_hash.into())
|
||||
}
|
||||
|
||||
pub fn nonce(&self) -> Option<H64> {
|
||||
let bytes: Bytes = self.decoded_seal_field(1, 8)?;
|
||||
let size = bytes.len();
|
||||
let mut nonce = [0u8; 8];
|
||||
for i in 0..size {
|
||||
nonce[7 - i] = bytes[size - 1 - i];
|
||||
}
|
||||
Some(nonce.into())
|
||||
}
|
||||
|
||||
pub fn has_ommers(&self) -> bool {
|
||||
self.ommers_hash != EMPTY_OMMERS_HASH.into()
|
||||
}
|
||||
|
||||
fn decoded_seal_field(&self, index: usize, max_len: usize) -> Option<Bytes> {
|
||||
let bytes: Bytes = rlp::decode(self.seal.get(index)?).ok()?;
|
||||
if bytes.len() > max_len {
|
||||
return None
|
||||
}
|
||||
Some(bytes)
|
||||
}
|
||||
|
||||
/// Returns header RLP with or without seals.
|
||||
/// For EIP-1559 baseFee addition refer to:
|
||||
/// <https://github.com/openethereum/openethereum/blob/193b25a22d5ff07759c6431129e95235510516f9/crates/ethcore/types/src/header.rs#L341>
|
||||
fn rlp(&self, with_seal: bool) -> Bytes {
|
||||
let mut s = RlpStream::new();
|
||||
|
||||
let stream_length_without_seal = if self.base_fee.is_some() { 14 } else { 13 };
|
||||
|
||||
if with_seal {
|
||||
s.begin_list(stream_length_without_seal + self.seal.len());
|
||||
} else {
|
||||
s.begin_list(stream_length_without_seal);
|
||||
}
|
||||
|
||||
s.append(&self.parent_hash);
|
||||
s.append(&self.ommers_hash);
|
||||
s.append(&self.author);
|
||||
s.append(&self.state_root);
|
||||
s.append(&self.transactions_root);
|
||||
s.append(&self.receipts_root);
|
||||
s.append(&EthBloom::from(self.logs_bloom.0));
|
||||
s.append(&self.difficulty);
|
||||
s.append(&self.number);
|
||||
s.append(&self.gas_limit);
|
||||
s.append(&self.gas_used);
|
||||
s.append(&self.timestamp);
|
||||
s.append(&self.extra_data);
|
||||
|
||||
if with_seal {
|
||||
for b in &self.seal {
|
||||
s.append_raw(b, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(base_fee) = self.base_fee {
|
||||
s.append(&base_fee);
|
||||
}
|
||||
|
||||
s.out().to_vec()
|
||||
}
|
||||
}
|
||||
|
||||
/// Logs bloom.
|
||||
#[derive(Clone, Debug, Encode, Decode, TypeInfo)]
|
||||
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
|
||||
pub struct Bloom(#[cfg_attr(feature = "std", serde(with = "BigArray"))] [u8; 256]);
|
||||
|
||||
impl<'a> From<&'a [u8; 256]> for Bloom {
|
||||
fn from(buffer: &'a [u8; 256]) -> Bloom {
|
||||
Bloom(*buffer)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<Bloom> for Bloom {
|
||||
fn eq(&self, other: &Bloom) -> bool {
|
||||
self.0.iter().zip(other.0.iter()).all(|(l, r)| l == r)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Bloom {
|
||||
fn default() -> Self {
|
||||
Bloom([0; 256])
|
||||
}
|
||||
}
|
||||
|
||||
impl rlp::Decodable for Bloom {
|
||||
fn decode(rlp: &rlp::Rlp) -> Result<Self, rlp::DecoderError> {
|
||||
let v: Vec<u8> = rlp.as_val()?;
|
||||
match v.len() {
|
||||
256 => {
|
||||
let mut bytes = [0u8; 256];
|
||||
bytes.copy_from_slice(&v);
|
||||
Ok(Self(bytes))
|
||||
},
|
||||
_ => Err(rlp::DecoderError::Custom("Expected 256 bytes")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn bloom_decode_rlp() {
|
||||
let raw_bloom = hex!(
|
||||
"
|
||||
b901000420000000000000000000008002000000000001000000000001000000000000000000
|
||||
0000000000000000000000000002000000080000000000000000200000000000000000000000
|
||||
0000080000002200000000004000100000000000000000000000000000000000000000000000
|
||||
0000000000000004000000001000010000000000080000000000400000000000000000000000
|
||||
0000080000004000000000020000000000020000000000000000000000000000000000000000
|
||||
0000040000000000020000000001000000000000000000000000000010000000020000200000
|
||||
10200000000000010000000000000000000000000000000000000010000000
|
||||
"
|
||||
);
|
||||
let expected_bytes = &raw_bloom[3..];
|
||||
let bloom: Bloom = rlp::decode(&raw_bloom).unwrap();
|
||||
assert_eq!(bloom.0, expected_bytes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_compute_hash_poa() {
|
||||
// PoA header
|
||||
let header = Header {
|
||||
parent_hash: Default::default(),
|
||||
timestamp: 0,
|
||||
number: 0,
|
||||
author: Default::default(),
|
||||
transactions_root: hex!(
|
||||
"56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"
|
||||
)
|
||||
.into(),
|
||||
ommers_hash: hex!("1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347")
|
||||
.into(),
|
||||
extra_data: vec![],
|
||||
state_root: hex!("eccf6b74c2bcbe115c71116a23fe963c54406010c244d9650526028ad3e32cce")
|
||||
.into(),
|
||||
receipts_root: hex!("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421")
|
||||
.into(),
|
||||
logs_bloom: Default::default(),
|
||||
gas_used: Default::default(),
|
||||
gas_limit: 0x222222.into(),
|
||||
difficulty: 0x20000.into(),
|
||||
seal: vec![vec![0x80], {
|
||||
let mut vec = vec![0xb8, 0x41];
|
||||
vec.resize(67, 0);
|
||||
vec
|
||||
}],
|
||||
base_fee: None,
|
||||
};
|
||||
assert_eq!(
|
||||
header.compute_hash().as_bytes(),
|
||||
hex!("9ff57c7fa155853586382022f0982b71c51fa313a0942f8c456300896643e890"),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_compute_hash_pow() {
|
||||
// <https://etherscan.io/block/11090290>
|
||||
let nonce = hex!("6935bbe7b63c4f8e").to_vec();
|
||||
let mix_hash =
|
||||
hex!("be3adfb0087be62b28b716e2cdf3c79329df5caa04c9eee035d35b5d52102815").to_vec();
|
||||
let header = Header {
|
||||
parent_hash: hex!("bede0bddd6f32c895fc505ffe0c39d9bde58e9a5272f31a3dee448b796edcbe3")
|
||||
.into(),
|
||||
timestamp: 1603160977,
|
||||
number: 11090290,
|
||||
author: hex!("ea674fdde714fd979de3edf0f56aa9716b898ec8").into(),
|
||||
transactions_root: hex!(
|
||||
"56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"
|
||||
)
|
||||
.into(),
|
||||
ommers_hash: hex!("1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347")
|
||||
.into(),
|
||||
extra_data: hex!("65746865726d696e652d61736961312d33").to_vec(),
|
||||
state_root: hex!("7dcb8aca872b712bad81df34a89d4efedc293566ffc3eeeb5cbcafcc703e42c9")
|
||||
.into(),
|
||||
receipts_root: hex!("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421")
|
||||
.into(),
|
||||
logs_bloom: Default::default(),
|
||||
gas_used: 0.into(),
|
||||
gas_limit: 0xbe8c19.into(),
|
||||
difficulty: 0xbc140caa61087i64.into(),
|
||||
seal: vec![rlp::encode(&mix_hash).to_vec(), rlp::encode(&nonce).to_vec()],
|
||||
base_fee: None,
|
||||
};
|
||||
assert_eq!(
|
||||
header.compute_hash().as_bytes(),
|
||||
hex!("0f9bdc91c2e0140acb873330742bda8c8181fa3add91fe7ae046251679cedef7"),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_pow_seal_fields_extracted_correctly() {
|
||||
let nonce: H64 = hex!("6935bbe7b63c4f8e").into();
|
||||
let mix_hash: H256 =
|
||||
hex!("be3adfb0087be62b28b716e2cdf3c79329df5caa04c9eee035d35b5d52102815").into();
|
||||
let header = Header {
|
||||
seal: vec![
|
||||
rlp::encode(&mix_hash.0.to_vec()).to_vec(),
|
||||
rlp::encode(&nonce.0.to_vec()).to_vec(),
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(header.nonce().unwrap(), nonce);
|
||||
assert_eq!(header.mix_hash().unwrap(), mix_hash);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_pow_seal_fields_return_none_for_invalid_values() {
|
||||
let nonce = hex!("696935bbe7b63c4f8e").to_vec();
|
||||
let mix_hash =
|
||||
hex!("bebe3adfb0087be62b28b716e2cdf3c79329df5caa04c9eee035d35b5d52102815").to_vec();
|
||||
let mut header = Header {
|
||||
seal: vec![rlp::encode(&mix_hash).to_vec(), rlp::encode(&nonce).to_vec()],
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(header.nonce(), None);
|
||||
assert_eq!(header.mix_hash(), None);
|
||||
|
||||
header.seal = Vec::new();
|
||||
assert_eq!(header.nonce(), None);
|
||||
assert_eq!(header.mix_hash(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_check_receipt_proof() {
|
||||
let header = Header {
|
||||
receipts_root: hex!("fd5e397a84884641f53c496804f24b5276cbb8c5c9cfc2342246be8e3ce5ad02")
|
||||
.into(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Valid proof
|
||||
let proof_receipt5 = vec!(
|
||||
hex!("f90131a0b5ba404eb5a6a88e56579f4d37ef9813b5ad7f86f0823ff3b407ac5a6bb465eca0398ead2655e78e03c127ce22c5830e90f18b1601ec055f938336c084feb915a9a026d322c26e46c50942c1aabde50e36df5cde572aed650ce73ea3182c6e90a02ca00600a356135f4db1db0d9842264cdff2652676f881669e91e316c0b6dd783011a0837f1deb4075336da320388c1edfffc56c448a43f4a5ba031300d32a7b509fc5a01c3ac82fd65b4aba7f9afaf604d9c82ec7e2deb573a091ae235751bc5c0c288da05d454159d9071b0f68b6e0503d290f23ac7602c1db0c569dee4605d8f5298f09a00bbed10350ec954448df795f6fd46e3faefc800ede061b3840eedc6e2b07a74da0acb02d26a3650f2064c14a435fdf1f668d8655daf455ebdf671713a7c089b3898080808080808080").to_vec(),
|
||||
hex!("f901f180a00046a08d4f0bdbdc6b31903086ce323182bce6725e7d9415f7ff91ee8f4820bda0e7cd26ad5f3d2771e4b5ab788e268a14a10209f94ee918eb6c829d21d3d11c1da00d4a56d9e9a6751874fd86c7e3cb1c6ad5a848da62751325f478978a00ea966ea064b81920c8f04a8a1e21f53a8280e739fbb7b00b2ab92493ca3f610b70e8ac85a0b1040ed4c55a73178b76abb16f946ce5bebd6b93ab873c83327df54047d12c27a0de6485e9ac58dc6e2b04b4bb38f562684f0b1a2ee586cc11079e7d9a9dc40b32a0d394f4d3532c3124a65fa36e69147e04fd20453a72ee9c50660f17e13ce9df48a066501003fc3e3478efd2803cd0eded6bbe9243ca01ba754d6327071ddbcbc649a0b2684e518f325fee39fc8ea81b68f3f5c785be00d087f3bed8857ae2ee8da26ea071060a5c52042e8d7ce21092f8ecf06053beb9a0b773a6f91a30c4220aa276b2a0fc22436632574ccf6043d0986dede27ea94c9ca9a3bb5ec03ce776a4ddef24a9a05a8a1d6698c4e7d8cc3a2506cb9b12ea9a079c9c7099bc919dc804033cc556e4a0170c468b0716fd36d161f0bf05875f15756a2976de92f9efe7716320509d79c9a0182f909a90cab169f3efb62387f9cccdd61440acc4deec42f68a4f7ca58075c7a055cf0e9202ac75689b76318f1171f3a44465eddc06aae0713bfb6b34fdd27b7980").to_vec(),
|
||||
hex!("f904de20b904daf904d701830652f0b9010004200000000000000000000080020000000000010000000000010000000000000000000000000000000000000000000002000000080000000000000000200000000000000000000000000008000000220000000000400010000000000000000000000000000000000000000000000000000000000000040000000010000100000000000800000000004000000000000000000000000000080000004000000000020000000000020000000000000000000000000000000000000000000004000000000002000000000100000000000000000000000000001000000002000020000010200000000000010000000000000000000000000000000000000010000000f903ccf89b9421130f34829b4c343142047a28ce96ec07814b15f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa00000000000000000000000007d843005c7433c16b27ff939cb37471541561ebda0000000000000000000000000e9c1281aae66801fa35ec404d5f2aea393ff6988a000000000000000000000000000000000000000000000000000000005d09b7380f89b9421130f34829b4c343142047a28ce96ec07814b15f863a08c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925a00000000000000000000000007d843005c7433c16b27ff939cb37471541561ebda00000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488da0ffffffffffffffffffffffffffffffffffffffffffffffffffffffcc840c6920f89b94c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa0000000000000000000000000e9c1281aae66801fa35ec404d5f2aea393ff6988a00000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488da000000000000000000000000000000000000000000000000003e973b5a5d1078ef87994e9c1281aae66801fa35ec404d5f2aea393ff6988e1a01c411e9a96e071241c2f21f7726b17ae89e3cab4c78be50e062b03a9fffbbad1b840000000000000000000000000000000000000000000000000000001f1420ad1d40000000000000000000000000000000000000000000000014ad400879d159a38f8fc94e9c1281aae66801fa35ec404d5f2aea393ff6988f863a0d78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822a00000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488da00000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488db88000000000000000000000000000000000000000000000000000000005d415f3320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003e973b5a5d1078ef87a94c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f842a07fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65a00000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488da000000000000000000000000000000000000000000000000003e973b5a5d1078e").to_vec(),
|
||||
);
|
||||
assert!(header.check_receipt_proof(&proof_receipt5).is_some());
|
||||
|
||||
// Various invalid proofs
|
||||
let proof_empty: Vec<Vec<u8>> = vec![];
|
||||
let proof_missing_full_node = vec![proof_receipt5[0].clone(), proof_receipt5[2].clone()];
|
||||
let proof_missing_short_node1 = vec![proof_receipt5[0].clone(), proof_receipt5[1].clone()];
|
||||
let proof_missing_short_node2 = vec![proof_receipt5[0].clone()];
|
||||
let proof_invalid_encoding = vec![proof_receipt5[2][2..].to_vec()];
|
||||
let proof_no_full_node = vec![proof_receipt5[2].clone(), proof_receipt5[2].clone()];
|
||||
assert!(header.check_receipt_proof(&proof_empty).is_none());
|
||||
assert!(header.check_receipt_proof(&proof_missing_full_node).is_none());
|
||||
|
||||
assert_eq!(
|
||||
header.check_receipt_proof(&proof_missing_short_node1),
|
||||
Some(Err(rlp::DecoderError::Custom("Unsupported receipt type")))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
header.check_receipt_proof(&proof_missing_short_node2),
|
||||
Some(Err(rlp::DecoderError::Custom("Unsupported receipt type")))
|
||||
);
|
||||
|
||||
assert!(header.check_receipt_proof(&proof_invalid_encoding).is_none());
|
||||
assert!(header.check_receipt_proof(&proof_no_full_node).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_check_receipt_proof_with_intermediate_short_node() {
|
||||
let header = Header {
|
||||
receipts_root: hex!("d128e3a57142d2bf15bc0cbcac7ad54f40750d571b5c3097e425882c10c9ba66")
|
||||
.into(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let proof_receipt263 = vec![
|
||||
hex!("f90131a00d3cb8d3f57ac1c0e12918a2ebe0cafed8c273577b9dd73e7ed1079b403ef494a0678b9835b834f8a287c0dd33a8fca9146e456ca688555ed4ec1361a2180b778da0fe42da181a46677a043b3d9d4b8bb05a6a17b7b5c010c17e7c1d31cfb7c4f911a0c89f0e2c53241cdb578e1f2b4caf6ba36e00500bdc57fecd66b84a6a58394c19a086c3c1fae5a0575940b5d38e111c469d07883106c26856f3ef608469a2081f13a06c5992ff00aab6226a70a032fd2f571ba22f797321f45e2daa73020d638d21b0a050861e9503ef68728f6c90a44f7fe1bceb2a9bdab6957bbe7136166bd849561ea006aa6eaca8a07e57176e9aa41e6a09edfb7678d1a112404e0ec779d7e567e82ea0bb0b430d303ba21b0af11c487b8a218bd75db54c98940b3f11bad8ff47cad3ef8080808080808080").to_vec(),
|
||||
hex!("f871a0246de222036ee6a03329b0105da0a6b3f916fc95a9ed5a403a581a0c4d74242ca0ac108a49a88b57a05ac34a108b39f1e45f6f167f2b9fbc8d52fb58e2e5a6af1ea0fcfe07ac2ccd3c28b6eab68d1bce112f6f6dbd9023e4ec3c05b96615aa803d798080808080808080808080808080").to_vec(),
|
||||
hex!("e4820001a04fff54398cad4d05ea6abfd8b0f3b4fe14c04d7ff5f5211c5b927d9cf72ac1d8").to_vec(),
|
||||
hex!("f851a096d010643ca2d47412ca66898286b5f2412963b9ec051b33e570d575914c9c5ca028cd24c652989542fe89479ec6388eac4592432242af5ba97563b3ac7c71c019808080808080808080808080808080").to_vec(),
|
||||
hex!("f90211a0bb35a84c5b1dcb78ec9d32614912c696e62df77bebf9ab326ee55b5d3acdde46a01084b30dac8df0accfcd0fd6330b7f6fc72a4651246d0694be9162151686a620a03eed50afdce7909d784c6157c445a444c806b5f23d31f3b63786f600c84a95b2a0af5232f1df6c6d41879804d081abe867002abe26ba3e5f8e0254a83a54769831a0607915fb13dd5da594256389a45007a67a7f7a86e95d38d8462792b6c98a722ea00e1260fda1730f2738c650ce2bfba83857bc10f8fb119ebc4fb39acba24e6fbaa0d11de17e417327457812675ca3b84ae8e1b64827abfe01420953697c8313d5b1a05fcaf2f7a88f76336a0c32ffc78acb87ae2005454bd25d658035331be3173b46a03f94f4952ab9e650f83cfd0e7f367b1bcc493aacf39a06f16c4a2e1b5605da48a0bdb4ec79785ca8ae22d60f1bbd42d707b4d7ec4aff231a3ebab755e315b35053a043a67c3f2bcef37c8f47a673adcb7061007a553696d1092408601c11b2e6846aa0c519d5af48cae87c7f4538845417c9735813bee892a6fe2dda79f5c414e8576aa0f7058256e09589501d7c231d739e61c84a850e139690989d24fda6058b432e98a081a52faab520978cb19ce14400dba0cd5bcdc4e5a3c0740678aa8f97ee0e5c56a0bcecc61cadeae52518e3b68a48af4b11603dfd9d99d99d7985efa6d2de44f904a02cba4accfc6f39bc5adb6d4440eb6358b4a5103ef93298e4e694f1f940f8b48280").to_vec(),
|
||||
hex!("f901ae20b901aaf901a70183bb444eb9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000001000000000000000000000000000100000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000010000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000080000000000000000000000000000000000000000000000002000000000000000000081000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000f89df89b94dac17f958d2ee523a2206206994597c13d831ec7f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa00000000000000000000000002e514404ff6823f1b46a8318a709251db414e5e1a000000000000000000000000055021c55847c00d764357a352e5803237d328954a0000000000000000000000000000000000000000000000000000000000201c370").to_vec(),
|
||||
];
|
||||
assert!(header.check_receipt_proof(&proof_receipt263).is_some());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
pub mod header;
|
||||
pub mod log;
|
||||
pub mod mpt;
|
||||
pub mod receipt;
|
||||
|
||||
pub use ethereum_types::{Address, H160, H256, H64, U256};
|
||||
|
||||
pub use header::{Bloom, Header, HeaderId};
|
||||
pub use log::Log;
|
||||
pub use receipt::Receipt;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DecodeError {
|
||||
// Unexpected RLP data
|
||||
InvalidRLP(rlp::DecoderError),
|
||||
// Data does not match expected ABI
|
||||
InvalidABI(ethabi::Error),
|
||||
// Invalid message payload
|
||||
InvalidPayload,
|
||||
}
|
||||
|
||||
impl From<rlp::DecoderError> for DecodeError {
|
||||
fn from(err: rlp::DecoderError) -> Self {
|
||||
DecodeError::InvalidRLP(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ethabi::Error> for DecodeError {
|
||||
fn from(err: ethabi::Error) -> Self {
|
||||
DecodeError::InvalidABI(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use codec::{Decode, Encode};
|
||||
use ethereum_types::{H160, H256};
|
||||
use sp_std::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, Encode, Decode, PartialEq, Eq)]
|
||||
pub struct Log {
|
||||
pub address: H160,
|
||||
pub topics: Vec<H256>,
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl rlp::Decodable for Log {
|
||||
/// We need to implement rlp::Decodable manually as the derive macro RlpDecodable
|
||||
/// didn't seem to generate the correct code for parsing our logs.
|
||||
fn decode(rlp: &rlp::Rlp) -> Result<Self, rlp::DecoderError> {
|
||||
let mut iter = rlp.iter();
|
||||
|
||||
let address: H160 = match iter.next() {
|
||||
Some(data) => data.as_val()?,
|
||||
None => return Err(rlp::DecoderError::Custom("Expected log address")),
|
||||
};
|
||||
|
||||
let topics: Vec<H256> = match iter.next() {
|
||||
Some(data) => data.as_list()?,
|
||||
None => return Err(rlp::DecoderError::Custom("Expected log topics")),
|
||||
};
|
||||
|
||||
let data: Vec<u8> = match iter.next() {
|
||||
Some(data) => data.data()?.to_vec(),
|
||||
None => return Err(rlp::DecoderError::Custom("Expected log data")),
|
||||
};
|
||||
|
||||
Ok(Self { address, topics, data })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::Log;
|
||||
use hex_literal::hex;
|
||||
|
||||
const RAW_LOG: [u8; 605] = hex!(
|
||||
"
|
||||
f9025a941cfd66659d44cfe2e627c5742ba7477a3284cffae1a0266413be5700ce8dd5ac6b9a7dfb
|
||||
abe99b3e45cae9a68ac2757858710b401a38b9022000000000000000000000000000000000000000
|
||||
00000000000000000000000060000000000000000000000000000000000000000000000000000000
|
||||
00000000c00000000000000000000000000000000000000000000000000000000000000100000000
|
||||
00000000000000000000000000000000000000000000000000000000283163466436363635394434
|
||||
34636665324536323763353734324261373437376133323834634666410000000000000000000000
|
||||
00000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||
000000000773656e6445544800000000000000000000000000000000000000000000000000000000
|
||||
00000000000000000000000000000000000000000000000000000001000000000000000000000000
|
||||
00cffeaaf7681c89285d65cfbe808b80e50269657300000000000000000000000000000000000000
|
||||
000000000000000000000000a0000000000000000000000000000000000000000000000000000000
|
||||
0000000000000000000000000000000000000000000000000000000000000000000000000a000000
|
||||
00000000000000000000000000000000000000000000000000000000020000000000000000000000
|
||||
00000000000000000000000000000000000000002f3146524d4d3850456957585961783772705336
|
||||
5834585a5831614141785357783143724b5479725659685632346667000000000000000000000000
|
||||
0000000000
|
||||
"
|
||||
);
|
||||
|
||||
#[test]
|
||||
fn decode_log() {
|
||||
let log: Log = rlp::decode(&RAW_LOG).unwrap();
|
||||
assert_eq!(log.address.as_bytes(), hex!["1cfd66659d44cfe2e627c5742ba7477a3284cffa"]);
|
||||
assert_eq!(
|
||||
log.topics[0].as_bytes(),
|
||||
hex!["266413be5700ce8dd5ac6b9a7dfbabe99b3e45cae9a68ac2757858710b401a38"]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
//! Helper types to work with Ethereum's Merkle Patricia Trie nodes
|
||||
|
||||
use ethereum_types::H256;
|
||||
use sp_std::{convert::TryFrom, prelude::*};
|
||||
|
||||
pub trait Node {
|
||||
fn contains_hash(&self, hash: H256) -> bool;
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for Box<dyn Node> {
|
||||
type Error = rlp::DecoderError;
|
||||
|
||||
fn try_from(bytes: &[u8]) -> Result<Box<dyn Node>, Self::Error> {
|
||||
let rlp = rlp::Rlp::new(bytes);
|
||||
match rlp.item_count()? {
|
||||
2 => {
|
||||
let node: ShortNode = rlp.as_val()?;
|
||||
Ok(Box::new(node))
|
||||
},
|
||||
17 => {
|
||||
let node: FullNode = rlp.as_val()?;
|
||||
Ok(Box::new(node))
|
||||
},
|
||||
_ => Err(rlp::DecoderError::Custom("Invalid number of list elements")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Intermediate trie node with children (refers to node with same name in Geth).
|
||||
/// This struct only handles the proof representation, i.e. a child is either empty
|
||||
/// or a 32-byte hash of its subtree.
|
||||
pub struct FullNode {
|
||||
pub children: Vec<Option<H256>>,
|
||||
}
|
||||
|
||||
impl rlp::Decodable for FullNode {
|
||||
fn decode(rlp: &rlp::Rlp) -> Result<Self, rlp::DecoderError> {
|
||||
let children: Vec<Option<H256>> = rlp
|
||||
.iter()
|
||||
.map(|item| {
|
||||
let v: Vec<u8> = item.as_val()?;
|
||||
match v.len() {
|
||||
0 => Ok(None),
|
||||
32 => {
|
||||
let mut bytes = [0u8; 32];
|
||||
bytes.copy_from_slice(&v);
|
||||
Ok(Some(bytes.into()))
|
||||
},
|
||||
_ => Err(rlp::DecoderError::Custom("Expected 32-byte hash or empty child")),
|
||||
}
|
||||
})
|
||||
.collect::<Result<_, rlp::DecoderError>>()?;
|
||||
|
||||
Ok(Self { children })
|
||||
}
|
||||
}
|
||||
|
||||
impl Node for FullNode {
|
||||
fn contains_hash(&self, hash: H256) -> bool {
|
||||
self.children.iter().any(|h| Some(hash) == *h)
|
||||
}
|
||||
}
|
||||
|
||||
/// Trie node where `value` is either the RLP-encoded item we're
|
||||
/// proving or an intermediate hash (refers to node with same name in Geth)
|
||||
/// Proof verification should return `value`. `key` is an implementation
|
||||
/// detail of the trie.
|
||||
pub struct ShortNode {
|
||||
pub key: Vec<u8>,
|
||||
pub value: Vec<u8>,
|
||||
}
|
||||
|
||||
impl rlp::Decodable for ShortNode {
|
||||
fn decode(rlp: &rlp::Rlp) -> Result<Self, rlp::DecoderError> {
|
||||
let mut iter = rlp.iter();
|
||||
|
||||
let key: Vec<u8> = match iter.next() {
|
||||
Some(data) => data.as_val()?,
|
||||
None => return Err(rlp::DecoderError::Custom("Expected key bytes")),
|
||||
};
|
||||
|
||||
let value: Vec<u8> = match iter.next() {
|
||||
Some(data) => data.as_val()?,
|
||||
None => return Err(rlp::DecoderError::Custom("Expected value bytes")),
|
||||
};
|
||||
|
||||
Ok(Self { key, value })
|
||||
}
|
||||
}
|
||||
|
||||
impl Node for ShortNode {
|
||||
fn contains_hash(&self, hash: H256) -> bool {
|
||||
self.value == hash.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use hex_literal::hex;
|
||||
|
||||
const RAW_PROOF: [&[u8]; 3] = [
|
||||
&hex!("f90131a0b5ba404eb5a6a88e56579f4d37ef9813b5ad7f86f0823ff3b407ac5a6bb465eca0398ead2655e78e03c127ce22c5830e90f18b1601ec055f938336c084feb915a9a026d322c26e46c50942c1aabde50e36df5cde572aed650ce73ea3182c6e90a02ca00600a356135f4db1db0d9842264cdff2652676f881669e91e316c0b6dd783011a0837f1deb4075336da320388c1edfffc56c448a43f4a5ba031300d32a7b509fc5a01c3ac82fd65b4aba7f9afaf604d9c82ec7e2deb573a091ae235751bc5c0c288da05d454159d9071b0f68b6e0503d290f23ac7602c1db0c569dee4605d8f5298f09a00bbed10350ec954448df795f6fd46e3faefc800ede061b3840eedc6e2b07a74da0acb02d26a3650f2064c14a435fdf1f668d8655daf455ebdf671713a7c089b3898080808080808080"),
|
||||
&hex!("f901f180a00046a08d4f0bdbdc6b31903086ce323182bce6725e7d9415f7ff91ee8f4820bda0e7cd26ad5f3d2771e4b5ab788e268a14a10209f94ee918eb6c829d21d3d11c1da00d4a56d9e9a6751874fd86c7e3cb1c6ad5a848da62751325f478978a00ea966ea064b81920c8f04a8a1e21f53a8280e739fbb7b00b2ab92493ca3f610b70e8ac85a0b1040ed4c55a73178b76abb16f946ce5bebd6b93ab873c83327df54047d12c27a0de6485e9ac58dc6e2b04b4bb38f562684f0b1a2ee586cc11079e7d9a9dc40b32a0d394f4d3532c3124a65fa36e69147e04fd20453a72ee9c50660f17e13ce9df48a066501003fc3e3478efd2803cd0eded6bbe9243ca01ba754d6327071ddbcbc649a0b2684e518f325fee39fc8ea81b68f3f5c785be00d087f3bed8857ae2ee8da26ea071060a5c52042e8d7ce21092f8ecf06053beb9a0b773a6f91a30c4220aa276b2a0fc22436632574ccf6043d0986dede27ea94c9ca9a3bb5ec03ce776a4ddef24a9a05a8a1d6698c4e7d8cc3a2506cb9b12ea9a079c9c7099bc919dc804033cc556e4a0170c468b0716fd36d161f0bf05875f15756a2976de92f9efe7716320509d79c9a0182f909a90cab169f3efb62387f9cccdd61440acc4deec42f68a4f7ca58075c7a055cf0e9202ac75689b76318f1171f3a44465eddc06aae0713bfb6b34fdd27b7980"),
|
||||
&hex!("f904de20b904daf904d701830652f0b9010004200000000000000000000080020000000000010000000000010000000000000000000000000000000000000000000002000000080000000000000000200000000000000000000000000008000000220000000000400010000000000000000000000000000000000000000000000000000000000000040000000010000100000000000800000000004000000000000000000000000000080000004000000000020000000000020000000000000000000000000000000000000000000004000000000002000000000100000000000000000000000000001000000002000020000010200000000000010000000000000000000000000000000000000010000000f903ccf89b9421130f34829b4c343142047a28ce96ec07814b15f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa00000000000000000000000007d843005c7433c16b27ff939cb37471541561ebda0000000000000000000000000e9c1281aae66801fa35ec404d5f2aea393ff6988a000000000000000000000000000000000000000000000000000000005d09b7380f89b9421130f34829b4c343142047a28ce96ec07814b15f863a08c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925a00000000000000000000000007d843005c7433c16b27ff939cb37471541561ebda00000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488da0ffffffffffffffffffffffffffffffffffffffffffffffffffffffcc840c6920f89b94c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa0000000000000000000000000e9c1281aae66801fa35ec404d5f2aea393ff6988a00000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488da000000000000000000000000000000000000000000000000003e973b5a5d1078ef87994e9c1281aae66801fa35ec404d5f2aea393ff6988e1a01c411e9a96e071241c2f21f7726b17ae89e3cab4c78be50e062b03a9fffbbad1b840000000000000000000000000000000000000000000000000000001f1420ad1d40000000000000000000000000000000000000000000000014ad400879d159a38f8fc94e9c1281aae66801fa35ec404d5f2aea393ff6988f863a0d78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822a00000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488da00000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488db88000000000000000000000000000000000000000000000000000000005d415f3320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003e973b5a5d1078ef87a94c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f842a07fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65a00000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488da000000000000000000000000000000000000000000000000003e973b5a5d1078e"),
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn decode_full_node() {
|
||||
let node1: FullNode = rlp::decode(RAW_PROOF[0]).unwrap();
|
||||
let node2: FullNode = rlp::decode(RAW_PROOF[1]).unwrap();
|
||||
assert_eq!(node1.children.len(), 17);
|
||||
assert_eq!(node2.children.len(), 17);
|
||||
assert_eq!(node1.children.iter().filter(|c| c.is_none()).count(), 8);
|
||||
assert_eq!(node2.children.iter().filter(|c| c.is_none()).count(), 2);
|
||||
|
||||
let result: Result<FullNode, rlp::DecoderError> = rlp::decode(RAW_PROOF[2]);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_short_node() {
|
||||
// key + item value
|
||||
let node: ShortNode = rlp::decode(RAW_PROOF[2]).unwrap();
|
||||
assert_eq!(node.key, vec![32]);
|
||||
assert!(!node.value.is_empty());
|
||||
|
||||
// key + item hash
|
||||
let node: ShortNode = rlp::decode(&hex!(
|
||||
"e4820001a04fff54398cad4d05ea6abfd8b0f3b4fe14c04d7ff5f5211c5b927d9cf72ac1d8"
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(node.key, vec![0, 1]);
|
||||
assert_eq!(
|
||||
node.value,
|
||||
hex!("4fff54398cad4d05ea6abfd8b0f3b4fe14c04d7ff5f5211c5b927d9cf72ac1d8").to_vec()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use crate::{Bloom, Log};
|
||||
use codec::{Decode, Encode};
|
||||
use sp_runtime::RuntimeDebug;
|
||||
use sp_std::prelude::*;
|
||||
|
||||
#[derive(Clone, Default, Encode, Decode, PartialEq, RuntimeDebug)]
|
||||
pub struct Receipt {
|
||||
pub post_state_or_status: Vec<u8>,
|
||||
pub cumulative_gas_used: u64,
|
||||
pub bloom: Bloom,
|
||||
pub logs: Vec<Log>,
|
||||
}
|
||||
|
||||
impl Receipt {
|
||||
pub fn contains_log(&self, log: &Log) -> bool {
|
||||
self.logs.iter().any(|l| l == log)
|
||||
}
|
||||
|
||||
fn decode_list(rlp: &rlp::Rlp) -> Result<Self, rlp::DecoderError> {
|
||||
let mut iter = rlp.iter();
|
||||
|
||||
let post_state_or_status: Vec<u8> = match iter.next() {
|
||||
Some(data) => data.as_val()?,
|
||||
None => return Err(rlp::DecoderError::Custom("Expected receipt post state or status")),
|
||||
};
|
||||
|
||||
let cumulative_gas_used: u64 = match iter.next() {
|
||||
Some(data) => data.as_val()?,
|
||||
None => return Err(rlp::DecoderError::Custom("Expected receipt cumulative gas used")),
|
||||
};
|
||||
|
||||
let bloom: Bloom = match iter.next() {
|
||||
Some(data) => data.as_val()?,
|
||||
None => return Err(rlp::DecoderError::Custom("Expected receipt bloom")),
|
||||
};
|
||||
|
||||
let logs: Vec<Log> = match iter.next() {
|
||||
Some(data) => data.as_list()?,
|
||||
None => return Err(rlp::DecoderError::Custom("Expected receipt logs")),
|
||||
};
|
||||
|
||||
Ok(Self { post_state_or_status, cumulative_gas_used, bloom, logs })
|
||||
}
|
||||
}
|
||||
|
||||
impl rlp::Decodable for Receipt {
|
||||
fn decode(rlp: &rlp::Rlp) -> Result<Self, rlp::DecoderError> {
|
||||
if rlp.is_data() {
|
||||
// Typed receipt
|
||||
let data = rlp.as_raw();
|
||||
match data[0] {
|
||||
// 1 = EIP-2930, 2 = EIP-1559
|
||||
1 | 2 => {
|
||||
let receipt_rlp = &rlp::Rlp::new(&data[1..]);
|
||||
if !receipt_rlp.is_list() {
|
||||
return Err(rlp::DecoderError::RlpExpectedToBeList)
|
||||
}
|
||||
Self::decode_list(&rlp::Rlp::new(&data[1..]))
|
||||
},
|
||||
_ => Err(rlp::DecoderError::Custom("Unsupported receipt type")),
|
||||
}
|
||||
} else if rlp.is_list() {
|
||||
// Legacy receipt
|
||||
Self::decode_list(rlp)
|
||||
} else {
|
||||
Err(rlp::DecoderError::RlpExpectedToBeList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::Receipt;
|
||||
use hex_literal::hex;
|
||||
|
||||
const RAW_RECEIPT: [u8; 1242] = hex!(
|
||||
"
|
||||
f904d701830652f0b901000420000000000000000000008002000000000001000000000001000000
|
||||
00000000000000000000000000000000000000020000000800000000000000002000000000000000
|
||||
00000000000008000000220000000000400010000000000000000000000000000000000000000000
|
||||
00000000000000000004000000001000010000000000080000000000400000000000000000000000
|
||||
00000800000040000000000200000000000200000000000000000000000000000000000000000000
|
||||
04000000000002000000000100000000000000000000000000001000000002000020000010200000
|
||||
000000010000000000000000000000000000000000000010000000f903ccf89b9421130f34829b4c
|
||||
343142047a28ce96ec07814b15f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a116
|
||||
28f55a4df523b3efa00000000000000000000000007d843005c7433c16b27ff939cb37471541561e
|
||||
bda0000000000000000000000000e9c1281aae66801fa35ec404d5f2aea393ff6988a00000000000
|
||||
0000000000000000000000000000000000000000000005d09b7380f89b9421130f34829b4c343142
|
||||
047a28ce96ec07814b15f863a08c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200a
|
||||
c8c7c3b925a00000000000000000000000007d843005c7433c16b27ff939cb37471541561ebda000
|
||||
00000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488da0ffffffffffffffff
|
||||
ffffffffffffffffffffffffffffffffffffffcc840c6920f89b94c02aaa39b223fe8d0a0e5c4f27
|
||||
ead9083c756cc2f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523
|
||||
b3efa0000000000000000000000000e9c1281aae66801fa35ec404d5f2aea393ff6988a000000000
|
||||
00000000000000007a250d5630b4cf539739df2c5dacb4c659f2488da00000000000000000000000
|
||||
0000000000000000000000000003e973b5a5d1078ef87994e9c1281aae66801fa35ec404d5f2aea3
|
||||
93ff6988e1a01c411e9a96e071241c2f21f7726b17ae89e3cab4c78be50e062b03a9fffbbad1b840
|
||||
000000000000000000000000000000000000000000000000000001f1420ad1d40000000000000000
|
||||
000000000000000000000000000000014ad400879d159a38f8fc94e9c1281aae66801fa35ec404d5
|
||||
f2aea393ff6988f863a0d78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159
|
||||
d822a00000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488da000000000
|
||||
00000000000000007a250d5630b4cf539739df2c5dacb4c659f2488db88000000000000000000000
|
||||
000000000000000000000000000000000005d415f332000000000000000000000000000000000000
|
||||
00000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||
00000000000000000000000000000000000000000000000000000000000003e973b5a5d1078ef87a
|
||||
94c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f842a07fcf532c15f0a6db0bd6d0e038bea71d
|
||||
30d808c7d98cb3bf7268a95bf5081b65a00000000000000000000000007a250d5630b4cf539739df
|
||||
2c5dacb4c659f2488da000000000000000000000000000000000000000000000000003e973b5a5d1
|
||||
078e
|
||||
"
|
||||
);
|
||||
|
||||
#[test]
|
||||
fn decode_legacy_receipt() {
|
||||
let receipt: Receipt = rlp::decode(&RAW_RECEIPT).unwrap();
|
||||
assert_eq!(receipt.post_state_or_status, vec!(1));
|
||||
assert_eq!(receipt.cumulative_gas_used, 414448);
|
||||
assert_eq!(
|
||||
receipt.bloom,
|
||||
(&hex!(
|
||||
"
|
||||
042000000000000000000000800200000000000100000000000100000000000000000000
|
||||
000000000000000000000000020000000800000000000000002000000000000000000000
|
||||
000000080000002200000000004000100000000000000000000000000000000000000000
|
||||
000000000000000000000400000000100001000000000008000000000040000000000000
|
||||
000000000000000800000040000000000200000000000200000000000000000000000000
|
||||
000000000000000000040000000000020000000001000000000000000000000000000010
|
||||
000000020000200000102000000000000100000000000000000000000000000000000000
|
||||
10000000
|
||||
"
|
||||
))
|
||||
.into(),
|
||||
);
|
||||
assert_eq!(receipt.logs.len(), 6);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
[package]
|
||||
name = "snowbridge-router-primitives"
|
||||
description = "Snowbridge Router Primitives"
|
||||
version = "0.1.1"
|
||||
authors = ["Snowfork <contact@snowfork.com>"]
|
||||
edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0.188", optional = true, features = ["derive"] }
|
||||
codec = { package = "parity-scale-codec", version = "3.6.1", default-features = false }
|
||||
scale-info = { version = "2.9.0", default-features = false, features = ["derive"] }
|
||||
log = { version = "0.4.20", default-features = false }
|
||||
|
||||
frame-support = { path = "../../../../../substrate/frame/support", default-features = false }
|
||||
frame-system = { path = "../../../../../substrate/frame/system", default-features = false }
|
||||
sp-core = { path = "../../../../../substrate/primitives/core", default-features = false }
|
||||
sp-io = { path = "../../../../../substrate/primitives/io", default-features = false }
|
||||
sp-runtime = { path = "../../../../../substrate/primitives/runtime", default-features = false }
|
||||
sp-std = { path = "../../../../../substrate/primitives/std", default-features = false }
|
||||
|
||||
xcm = { package = "staging-xcm", path = "../../../../../polkadot/xcm", default-features = false }
|
||||
xcm-builder = { package = "staging-xcm-builder", path = "../../../../../polkadot/xcm/xcm-builder", default-features = false }
|
||||
xcm-executor = { package = "staging-xcm-executor", path = "../../../../../polkadot/xcm/xcm-executor", default-features = false }
|
||||
|
||||
snowbridge-core = { path = "../../primitives/core", default-features = false }
|
||||
|
||||
ethabi = { git = "https://github.com/Snowfork/ethabi-decode.git", package = "ethabi-decode", branch = "master", default-features = false }
|
||||
|
||||
hex-literal = { version = "0.4.1" }
|
||||
|
||||
[dev-dependencies]
|
||||
hex = { package = "rustc-hex", version = "2.1.0" }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"codec/std",
|
||||
"ethabi/std",
|
||||
"frame-support/std",
|
||||
"frame-system/std",
|
||||
"log/std",
|
||||
"scale-info/std",
|
||||
"serde",
|
||||
"snowbridge-core/std",
|
||||
"sp-core/std",
|
||||
"sp-io/std",
|
||||
"sp-runtime/std",
|
||||
"sp-std/std",
|
||||
"xcm-builder/std",
|
||||
"xcm-executor/std",
|
||||
"xcm/std",
|
||||
]
|
||||
runtime-benchmarks = [
|
||||
"frame-support/runtime-benchmarks",
|
||||
"frame-system/runtime-benchmarks",
|
||||
"snowbridge-core/runtime-benchmarks",
|
||||
"sp-runtime/runtime-benchmarks",
|
||||
"xcm-builder/runtime-benchmarks",
|
||||
"xcm-executor/runtime-benchmarks",
|
||||
]
|
||||
@@ -0,0 +1,320 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
//! Converts messages from Ethereum to XCM messages
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use codec::{Decode, Encode};
|
||||
use core::marker::PhantomData;
|
||||
use frame_support::{traits::tokens::Balance as BalanceT, weights::Weight, PalletError};
|
||||
use scale_info::TypeInfo;
|
||||
use sp_core::{Get, RuntimeDebug, H160};
|
||||
use sp_io::hashing::blake2_256;
|
||||
use sp_runtime::MultiAddress;
|
||||
use sp_std::prelude::*;
|
||||
use xcm::prelude::{Junction::AccountKey20, *};
|
||||
use xcm_executor::traits::ConvertLocation;
|
||||
|
||||
const MINIMUM_DEPOSIT: u128 = 1;
|
||||
|
||||
/// Messages from Ethereum are versioned. This is because in future,
|
||||
/// we may want to evolve the protocol so that the ethereum side sends XCM messages directly.
|
||||
/// Instead having BridgeHub transcode the messages into XCM.
|
||||
#[derive(Clone, Encode, Decode, RuntimeDebug)]
|
||||
pub enum VersionedMessage {
|
||||
V1(MessageV1),
|
||||
}
|
||||
|
||||
/// For V1, the ethereum side sends messages which are transcoded into XCM. These messages are
|
||||
/// self-contained, in that they can be transcoded using only information in the message.
|
||||
#[derive(Clone, Encode, Decode, RuntimeDebug)]
|
||||
pub struct MessageV1 {
|
||||
/// EIP-155 chain id of the origin Ethereum network
|
||||
pub chain_id: u64,
|
||||
/// The command originating from the Gateway contract
|
||||
pub command: Command,
|
||||
}
|
||||
|
||||
#[derive(Clone, Encode, Decode, RuntimeDebug)]
|
||||
pub enum Command {
|
||||
/// Register a wrapped token on the AssetHub `ForeignAssets` pallet
|
||||
RegisterToken {
|
||||
/// The address of the ERC20 token to be bridged over to AssetHub
|
||||
token: H160,
|
||||
/// XCM execution fee on AssetHub
|
||||
fee: u128,
|
||||
},
|
||||
/// Send a token to AssetHub or another parachain
|
||||
SendToken {
|
||||
/// The address of the ERC20 token to be bridged over to AssetHub
|
||||
token: H160,
|
||||
/// The destination for the transfer
|
||||
destination: Destination,
|
||||
/// Amount to transfer
|
||||
amount: u128,
|
||||
/// XCM execution fee on AssetHub
|
||||
fee: u128,
|
||||
},
|
||||
}
|
||||
|
||||
/// Destination for bridged tokens
|
||||
#[derive(Clone, Encode, Decode, RuntimeDebug)]
|
||||
pub enum Destination {
|
||||
/// The funds will be deposited into account `id` on AssetHub
|
||||
AccountId32 { id: [u8; 32] },
|
||||
/// The funds will deposited into the sovereign account of destination parachain `para_id` on
|
||||
/// AssetHub, Account `id` on the destination parachain will receive the funds via a
|
||||
/// reserve-backed transfer. See <https://github.com/paritytech/xcm-format#depositreserveasset>
|
||||
ForeignAccountId32 {
|
||||
para_id: u32,
|
||||
id: [u8; 32],
|
||||
/// XCM execution fee on final destination
|
||||
fee: u128,
|
||||
},
|
||||
/// The funds will deposited into the sovereign account of destination parachain `para_id` on
|
||||
/// AssetHub, Account `id` on the destination parachain will receive the funds via a
|
||||
/// reserve-backed transfer. See <https://github.com/paritytech/xcm-format#depositreserveasset>
|
||||
ForeignAccountId20 {
|
||||
para_id: u32,
|
||||
id: [u8; 20],
|
||||
/// XCM execution fee on final destination
|
||||
fee: u128,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct MessageToXcm<
|
||||
CreateAssetCall,
|
||||
CreateAssetDeposit,
|
||||
InboundQueuePalletInstance,
|
||||
AccountId,
|
||||
Balance,
|
||||
> where
|
||||
CreateAssetCall: Get<CallIndex>,
|
||||
CreateAssetDeposit: Get<u128>,
|
||||
Balance: BalanceT,
|
||||
{
|
||||
_phantom: PhantomData<(
|
||||
CreateAssetCall,
|
||||
CreateAssetDeposit,
|
||||
InboundQueuePalletInstance,
|
||||
AccountId,
|
||||
Balance,
|
||||
)>,
|
||||
}
|
||||
|
||||
/// Reason why a message conversion failed.
|
||||
#[derive(Copy, Clone, TypeInfo, PalletError, Encode, Decode, RuntimeDebug)]
|
||||
pub enum ConvertMessageError {
|
||||
/// The message version is not supported for conversion.
|
||||
UnsupportedVersion,
|
||||
}
|
||||
|
||||
/// convert the inbound message to xcm which will be forwarded to the destination chain
|
||||
pub trait ConvertMessage {
|
||||
type Balance: BalanceT + From<u128>;
|
||||
type AccountId;
|
||||
/// Converts a versioned message into an XCM message and an optional topicID
|
||||
fn convert(message: VersionedMessage) -> Result<(Xcm<()>, Self::Balance), ConvertMessageError>;
|
||||
}
|
||||
|
||||
pub type CallIndex = [u8; 2];
|
||||
|
||||
impl<CreateAssetCall, CreateAssetDeposit, InboundQueuePalletInstance, AccountId, Balance>
|
||||
ConvertMessage
|
||||
for MessageToXcm<
|
||||
CreateAssetCall,
|
||||
CreateAssetDeposit,
|
||||
InboundQueuePalletInstance,
|
||||
AccountId,
|
||||
Balance,
|
||||
> where
|
||||
CreateAssetCall: Get<CallIndex>,
|
||||
CreateAssetDeposit: Get<u128>,
|
||||
InboundQueuePalletInstance: Get<u8>,
|
||||
Balance: BalanceT + From<u128>,
|
||||
AccountId: Into<[u8; 32]>,
|
||||
{
|
||||
type Balance = Balance;
|
||||
type AccountId = AccountId;
|
||||
|
||||
fn convert(message: VersionedMessage) -> Result<(Xcm<()>, Self::Balance), ConvertMessageError> {
|
||||
use Command::*;
|
||||
use VersionedMessage::*;
|
||||
match message {
|
||||
V1(MessageV1 { chain_id, command: RegisterToken { token, fee } }) =>
|
||||
Ok(Self::convert_register_token(chain_id, token, fee)),
|
||||
V1(MessageV1 { chain_id, command: SendToken { token, destination, amount, fee } }) =>
|
||||
Ok(Self::convert_send_token(chain_id, token, destination, amount, fee)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<CreateAssetCall, CreateAssetDeposit, InboundQueuePalletInstance, AccountId, Balance>
|
||||
MessageToXcm<CreateAssetCall, CreateAssetDeposit, InboundQueuePalletInstance, AccountId, Balance>
|
||||
where
|
||||
CreateAssetCall: Get<CallIndex>,
|
||||
CreateAssetDeposit: Get<u128>,
|
||||
InboundQueuePalletInstance: Get<u8>,
|
||||
Balance: BalanceT + From<u128>,
|
||||
AccountId: Into<[u8; 32]>,
|
||||
{
|
||||
fn convert_register_token(chain_id: u64, token: H160, fee: u128) -> (Xcm<()>, Balance) {
|
||||
let network = Ethereum { chain_id };
|
||||
let xcm_fee: MultiAsset = (MultiLocation::parent(), fee).into();
|
||||
let deposit: MultiAsset = (MultiLocation::parent(), CreateAssetDeposit::get()).into();
|
||||
|
||||
let total_amount = fee + CreateAssetDeposit::get();
|
||||
let total: MultiAsset = (MultiLocation::parent(), total_amount).into();
|
||||
|
||||
let bridge_location: MultiLocation = (Parent, Parent, GlobalConsensus(network)).into();
|
||||
|
||||
let owner = GlobalConsensusEthereumConvertsFor::<[u8; 32]>::from_chain_id(&chain_id);
|
||||
let asset_id = Self::convert_token_address(network, token);
|
||||
let create_call_index: [u8; 2] = CreateAssetCall::get();
|
||||
let inbound_queue_pallet_index = InboundQueuePalletInstance::get();
|
||||
|
||||
let xcm: Xcm<()> = vec![
|
||||
// Teleport required fees.
|
||||
ReceiveTeleportedAsset(total.into()),
|
||||
// Pay for execution.
|
||||
BuyExecution { fees: xcm_fee, weight_limit: Unlimited },
|
||||
// Fund the snowbridge sovereign with the required deposit for creation.
|
||||
DepositAsset { assets: Definite(deposit.into()), beneficiary: bridge_location },
|
||||
// Only our inbound-queue pallet is allowed to invoke `UniversalOrigin`
|
||||
DescendOrigin(X1(PalletInstance(inbound_queue_pallet_index))),
|
||||
// Change origin to the bridge.
|
||||
UniversalOrigin(GlobalConsensus(network)),
|
||||
// Call create_asset on foreign assets pallet.
|
||||
Transact {
|
||||
origin_kind: OriginKind::Xcm,
|
||||
require_weight_at_most: Weight::from_parts(400_000_000, 8_000),
|
||||
call: (
|
||||
create_call_index,
|
||||
asset_id,
|
||||
MultiAddress::<[u8; 32], ()>::Id(owner),
|
||||
MINIMUM_DEPOSIT,
|
||||
)
|
||||
.encode()
|
||||
.into(),
|
||||
},
|
||||
RefundSurplus,
|
||||
// Clear the origin so that remaining assets in holding
|
||||
// are claimable by the physical origin (BridgeHub)
|
||||
ClearOrigin,
|
||||
]
|
||||
.into();
|
||||
|
||||
(xcm, total_amount.into())
|
||||
}
|
||||
|
||||
fn convert_send_token(
|
||||
chain_id: u64,
|
||||
token: H160,
|
||||
destination: Destination,
|
||||
amount: u128,
|
||||
asset_hub_fee: u128,
|
||||
) -> (Xcm<()>, Balance) {
|
||||
let network = Ethereum { chain_id };
|
||||
let asset_hub_fee_asset: MultiAsset = (MultiLocation::parent(), asset_hub_fee).into();
|
||||
let asset: MultiAsset = (Self::convert_token_address(network, token), amount).into();
|
||||
|
||||
let (dest_para_id, beneficiary, dest_para_fee) = match destination {
|
||||
// Final destination is a 32-byte account on AssetHub
|
||||
Destination::AccountId32 { id } => (
|
||||
None,
|
||||
MultiLocation { parents: 0, interior: X1(AccountId32 { network: None, id }) },
|
||||
0,
|
||||
),
|
||||
// Final destination is a 32-byte account on a sibling of AssetHub
|
||||
Destination::ForeignAccountId32 { para_id, id, fee } => (
|
||||
Some(para_id),
|
||||
MultiLocation { parents: 0, interior: X1(AccountId32 { network: None, id }) },
|
||||
// Total fee needs to cover execution on AssetHub and Sibling
|
||||
fee,
|
||||
),
|
||||
// Final destination is a 20-byte account on a sibling of AssetHub
|
||||
Destination::ForeignAccountId20 { para_id, id, fee } => (
|
||||
Some(para_id),
|
||||
MultiLocation { parents: 0, interior: X1(AccountKey20 { network: None, key: id }) },
|
||||
// Total fee needs to cover execution on AssetHub and Sibling
|
||||
fee,
|
||||
),
|
||||
};
|
||||
|
||||
let total_fees = asset_hub_fee.saturating_add(dest_para_fee);
|
||||
let total_fee_asset: MultiAsset = (MultiLocation::parent(), total_fees).into();
|
||||
let inbound_queue_pallet_index = InboundQueuePalletInstance::get();
|
||||
|
||||
let mut instructions = vec![
|
||||
ReceiveTeleportedAsset(total_fee_asset.into()),
|
||||
BuyExecution { fees: asset_hub_fee_asset, weight_limit: Unlimited },
|
||||
DescendOrigin(X1(PalletInstance(inbound_queue_pallet_index))),
|
||||
UniversalOrigin(GlobalConsensus(network)),
|
||||
ReserveAssetDeposited(asset.clone().into()),
|
||||
ClearOrigin,
|
||||
];
|
||||
|
||||
match dest_para_id {
|
||||
Some(dest_para_id) => {
|
||||
let dest_para_fee_asset: MultiAsset =
|
||||
(MultiLocation::parent(), dest_para_fee).into();
|
||||
|
||||
instructions.extend(vec![
|
||||
// Perform a deposit reserve to send to destination chain.
|
||||
DepositReserveAsset {
|
||||
assets: Definite(vec![dest_para_fee_asset.clone(), asset.clone()].into()),
|
||||
dest: MultiLocation { parents: 1, interior: X1(Parachain(dest_para_id)) },
|
||||
xcm: vec![
|
||||
// Buy execution on target.
|
||||
BuyExecution { fees: dest_para_fee_asset, weight_limit: Unlimited },
|
||||
// Deposit asset to beneficiary.
|
||||
DepositAsset { assets: Definite(asset.into()), beneficiary },
|
||||
]
|
||||
.into(),
|
||||
},
|
||||
]);
|
||||
},
|
||||
None => {
|
||||
instructions.extend(vec![
|
||||
// Deposit asset to beneficiary.
|
||||
DepositAsset { assets: Definite(asset.into()), beneficiary },
|
||||
]);
|
||||
},
|
||||
}
|
||||
|
||||
(instructions.into(), total_fees.into())
|
||||
}
|
||||
|
||||
// Convert ERC20 token address to a Multilocation that can be understood by Assets Hub.
|
||||
fn convert_token_address(network: NetworkId, token: H160) -> MultiLocation {
|
||||
MultiLocation {
|
||||
parents: 2,
|
||||
interior: X2(
|
||||
GlobalConsensus(network),
|
||||
AccountKey20 { network: None, key: token.into() },
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GlobalConsensusEthereumConvertsFor<AccountId>(PhantomData<AccountId>);
|
||||
impl<AccountId> ConvertLocation<AccountId> for GlobalConsensusEthereumConvertsFor<AccountId>
|
||||
where
|
||||
AccountId: From<[u8; 32]> + Clone,
|
||||
{
|
||||
fn convert_location(location: &MultiLocation) -> Option<AccountId> {
|
||||
if let MultiLocation { interior: X1(GlobalConsensus(Ethereum { chain_id })), .. } = location
|
||||
{
|
||||
Some(Self::from_chain_id(chain_id).into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<AccountId> GlobalConsensusEthereumConvertsFor<AccountId> {
|
||||
pub fn from_chain_id(chain_id: &u64) -> [u8; 32] {
|
||||
(b"ethereum-chain", chain_id).using_encoded(blake2_256)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
use super::GlobalConsensusEthereumConvertsFor;
|
||||
use crate::inbound::CallIndex;
|
||||
use frame_support::parameter_types;
|
||||
use hex_literal::hex;
|
||||
use xcm::v3::prelude::*;
|
||||
use xcm_executor::traits::ConvertLocation;
|
||||
|
||||
const NETWORK: NetworkId = Ethereum { chain_id: 11155111 };
|
||||
|
||||
parameter_types! {
|
||||
pub EthereumNetwork: NetworkId = NETWORK;
|
||||
|
||||
pub const CreateAssetCall: CallIndex = [1, 1];
|
||||
pub const CreateAssetExecutionFee: u128 = 123;
|
||||
pub const CreateAssetDeposit: u128 = 891;
|
||||
pub const SendTokenExecutionFee: u128 = 592;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_contract_location_with_network_converts_successfully() {
|
||||
let expected_account: [u8; 32] =
|
||||
hex!("ce796ae65569a670d0c1cc1ac12515a3ce21b5fbf729d63d7b289baad070139d");
|
||||
let contract_location = MultiLocation { parents: 2, interior: X1(GlobalConsensus(NETWORK)) };
|
||||
|
||||
let account =
|
||||
GlobalConsensusEthereumConvertsFor::<[u8; 32]>::convert_location(&contract_location)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(account, expected_account);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_contract_location_with_incorrect_location_fails_convert() {
|
||||
let contract_location =
|
||||
MultiLocation { parents: 2, interior: X2(GlobalConsensus(Polkadot), Parachain(1000)) };
|
||||
|
||||
assert_eq!(
|
||||
GlobalConsensusEthereumConvertsFor::<[u8; 32]>::convert_location(&contract_location),
|
||||
None,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
pub mod inbound;
|
||||
pub mod outbound;
|
||||
@@ -0,0 +1,282 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
//! Converts XCM messages into simpler commands that can be processed by the Gateway contract
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use core::slice::Iter;
|
||||
|
||||
use codec::{Decode, Encode};
|
||||
|
||||
use frame_support::{ensure, traits::Get};
|
||||
use snowbridge_core::{
|
||||
outbound::{AgentExecuteCommand, Command, Message, SendMessage},
|
||||
ChannelId, ParaId,
|
||||
};
|
||||
use sp_core::{H160, H256};
|
||||
use sp_std::{iter::Peekable, marker::PhantomData, prelude::*};
|
||||
use xcm::v3::prelude::*;
|
||||
use xcm_executor::traits::{ConvertLocation, ExportXcm};
|
||||
|
||||
pub struct EthereumBlobExporter<
|
||||
UniversalLocation,
|
||||
EthereumNetwork,
|
||||
OutboundQueue,
|
||||
AgentHashedDescription,
|
||||
>(PhantomData<(UniversalLocation, EthereumNetwork, OutboundQueue, AgentHashedDescription)>);
|
||||
|
||||
impl<UniversalLocation, EthereumNetwork, OutboundQueue, AgentHashedDescription> ExportXcm
|
||||
for EthereumBlobExporter<UniversalLocation, EthereumNetwork, OutboundQueue, AgentHashedDescription>
|
||||
where
|
||||
UniversalLocation: Get<InteriorMultiLocation>,
|
||||
EthereumNetwork: Get<NetworkId>,
|
||||
OutboundQueue: SendMessage<Balance = u128>,
|
||||
AgentHashedDescription: ConvertLocation<H256>,
|
||||
{
|
||||
type Ticket = (Vec<u8>, XcmHash);
|
||||
|
||||
fn validate(
|
||||
network: NetworkId,
|
||||
_channel: u32,
|
||||
universal_source: &mut Option<InteriorMultiLocation>,
|
||||
destination: &mut Option<InteriorMultiLocation>,
|
||||
message: &mut Option<Xcm<()>>,
|
||||
) -> SendResult<Self::Ticket> {
|
||||
let expected_network = EthereumNetwork::get();
|
||||
let universal_location = UniversalLocation::get();
|
||||
|
||||
if network != expected_network {
|
||||
log::trace!(target: "xcm::ethereum_blob_exporter", "skipped due to unmatched bridge network {network:?}.");
|
||||
return Err(SendError::NotApplicable)
|
||||
}
|
||||
|
||||
let dest = destination.take().ok_or(SendError::MissingArgument)?;
|
||||
if dest != Here {
|
||||
log::trace!(target: "xcm::ethereum_blob_exporter", "skipped due to unmatched remote destination {dest:?}.");
|
||||
return Err(SendError::NotApplicable)
|
||||
}
|
||||
|
||||
let (local_net, local_sub) = universal_source
|
||||
.take()
|
||||
.ok_or_else(|| {
|
||||
log::error!(target: "xcm::ethereum_blob_exporter", "universal source not provided.");
|
||||
SendError::MissingArgument
|
||||
})?
|
||||
.split_global()
|
||||
.map_err(|()| {
|
||||
log::error!(target: "xcm::ethereum_blob_exporter", "could not get global consensus from universal source '{universal_source:?}'.");
|
||||
SendError::Unroutable
|
||||
})?;
|
||||
|
||||
if Ok(local_net) != universal_location.global_consensus() {
|
||||
log::trace!(target: "xcm::ethereum_blob_exporter", "skipped due to unmatched relay network {local_net:?}.");
|
||||
return Err(SendError::NotApplicable)
|
||||
}
|
||||
|
||||
let para_id = match local_sub {
|
||||
X1(Parachain(para_id)) => para_id,
|
||||
_ => {
|
||||
log::error!(target: "xcm::ethereum_blob_exporter", "could not get parachain id from universal source '{local_sub:?}'.");
|
||||
return Err(SendError::MissingArgument)
|
||||
},
|
||||
};
|
||||
|
||||
let message = message.take().ok_or_else(|| {
|
||||
log::error!(target: "xcm::ethereum_blob_exporter", "xcm message not provided.");
|
||||
SendError::MissingArgument
|
||||
})?;
|
||||
|
||||
let mut converter = XcmConverter::new(&message, &expected_network);
|
||||
let (agent_execute_command, message_id) = converter.convert().map_err(|err|{
|
||||
log::error!(target: "xcm::ethereum_blob_exporter", "unroutable due to pattern matching error '{err:?}'.");
|
||||
SendError::Unroutable
|
||||
})?;
|
||||
|
||||
let source_location: MultiLocation = MultiLocation { parents: 1, interior: local_sub };
|
||||
let agent_id = match AgentHashedDescription::convert_location(&source_location) {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
log::error!(target: "xcm::ethereum_blob_exporter", "unroutable due to not being able to create agent id. '{source_location:?}'");
|
||||
return Err(SendError::Unroutable)
|
||||
},
|
||||
};
|
||||
|
||||
let channel_id: ChannelId = ParaId::from(para_id).into();
|
||||
|
||||
let outbound_message = Message {
|
||||
id: Some(message_id.into()),
|
||||
channel_id,
|
||||
command: Command::AgentExecute { agent_id, command: agent_execute_command },
|
||||
};
|
||||
|
||||
// validate the message
|
||||
let (ticket, fee) = OutboundQueue::validate(&outbound_message).map_err(|err| {
|
||||
log::error!(target: "xcm::ethereum_blob_exporter", "OutboundQueue validation of message failed. {err:?}");
|
||||
SendError::Unroutable
|
||||
})?;
|
||||
|
||||
// convert fee to MultiAsset
|
||||
let fee = MultiAsset::from((MultiLocation::parent(), fee.total())).into();
|
||||
|
||||
Ok(((ticket.encode(), message_id), fee))
|
||||
}
|
||||
|
||||
fn deliver(blob: (Vec<u8>, XcmHash)) -> Result<XcmHash, SendError> {
|
||||
let ticket: OutboundQueue::Ticket = OutboundQueue::Ticket::decode(&mut blob.0.as_ref())
|
||||
.map_err(|_| {
|
||||
log::trace!(target: "xcm::ethereum_blob_exporter", "undeliverable due to decoding error");
|
||||
SendError::NotApplicable
|
||||
})?;
|
||||
|
||||
let message_id = OutboundQueue::deliver(ticket).map_err(|_| {
|
||||
log::error!(target: "xcm::ethereum_blob_exporter", "OutboundQueue submit of message failed");
|
||||
SendError::Transport("other transport error")
|
||||
})?;
|
||||
|
||||
log::info!(target: "xcm::ethereum_blob_exporter", "message delivered {message_id:#?}.");
|
||||
Ok(message_id.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors that can be thrown to the pattern matching step.
|
||||
#[derive(PartialEq, Debug)]
|
||||
enum XcmConverterError {
|
||||
UnexpectedEndOfXcm,
|
||||
EndOfXcmMessageExpected,
|
||||
WithdrawAssetExpected,
|
||||
DepositAssetExpected,
|
||||
NoReserveAssets,
|
||||
FilterDoesNotConsumeAllAssets,
|
||||
TooManyAssets,
|
||||
ZeroAssetTransfer,
|
||||
BeneficiaryResolutionFailed,
|
||||
AssetResolutionFailed,
|
||||
InvalidFeeAsset,
|
||||
SetTopicExpected,
|
||||
}
|
||||
|
||||
macro_rules! match_expression {
|
||||
($expression:expr, $(|)? $( $pattern:pat_param )|+ $( if $guard: expr )?, $value:expr $(,)?) => {
|
||||
match $expression {
|
||||
$( $pattern )|+ $( if $guard )? => Some($value),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
struct XcmConverter<'a, Call> {
|
||||
iter: Peekable<Iter<'a, Instruction<Call>>>,
|
||||
ethereum_network: &'a NetworkId,
|
||||
}
|
||||
impl<'a, Call> XcmConverter<'a, Call> {
|
||||
fn new(message: &'a Xcm<Call>, ethereum_network: &'a NetworkId) -> Self {
|
||||
Self { iter: message.inner().iter().peekable(), ethereum_network }
|
||||
}
|
||||
|
||||
fn convert(&mut self) -> Result<(AgentExecuteCommand, [u8; 32]), XcmConverterError> {
|
||||
// Get withdraw/deposit and make native tokens create message.
|
||||
let result = self.native_tokens_unlock_message()?;
|
||||
|
||||
// All xcm instructions must be consumed before exit.
|
||||
if self.next().is_ok() {
|
||||
return Err(XcmConverterError::EndOfXcmMessageExpected)
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn native_tokens_unlock_message(
|
||||
&mut self,
|
||||
) -> Result<(AgentExecuteCommand, [u8; 32]), XcmConverterError> {
|
||||
use XcmConverterError::*;
|
||||
|
||||
// Get the reserve assets from WithdrawAsset.
|
||||
let reserve_assets =
|
||||
match_expression!(self.next()?, WithdrawAsset(reserve_assets), reserve_assets)
|
||||
.ok_or(WithdrawAssetExpected)?;
|
||||
|
||||
// Check if clear origin exists and skip over it.
|
||||
if match_expression!(self.peek(), Ok(ClearOrigin), ()).is_some() {
|
||||
let _ = self.next();
|
||||
}
|
||||
|
||||
// Get the fee asset item from BuyExecution or continue parsing.
|
||||
let fee_asset = match_expression!(self.peek(), Ok(BuyExecution { fees, .. }), fees);
|
||||
if fee_asset.is_some() {
|
||||
let _ = self.next();
|
||||
}
|
||||
|
||||
let (deposit_assets, beneficiary) = match_expression!(
|
||||
self.next()?,
|
||||
DepositAsset { assets, beneficiary },
|
||||
(assets, beneficiary)
|
||||
)
|
||||
.ok_or(DepositAssetExpected)?;
|
||||
|
||||
// assert that the beneficiary is AccountKey20.
|
||||
let recipient = match_expression!(
|
||||
beneficiary,
|
||||
MultiLocation { parents: 0, interior: X1(AccountKey20 { network, key }) }
|
||||
if self.network_matches(network),
|
||||
H160(*key)
|
||||
)
|
||||
.ok_or(BeneficiaryResolutionFailed)?;
|
||||
|
||||
// Make sure there are reserved assets.
|
||||
if reserve_assets.len() == 0 {
|
||||
return Err(NoReserveAssets)
|
||||
}
|
||||
|
||||
// Check the the deposit asset filter matches what was reserved.
|
||||
if reserve_assets.inner().iter().any(|asset| !deposit_assets.matches(asset)) {
|
||||
return Err(FilterDoesNotConsumeAllAssets)
|
||||
}
|
||||
|
||||
// We only support a single asset at a time.
|
||||
ensure!(reserve_assets.len() == 1, TooManyAssets);
|
||||
let reserve_asset = reserve_assets.get(0).ok_or(AssetResolutionFailed)?;
|
||||
|
||||
// If there was a fee specified verify it.
|
||||
if let Some(fee_asset) = fee_asset {
|
||||
// The fee asset must be the same as the reserve asset.
|
||||
if fee_asset.id != reserve_asset.id || fee_asset.fun > reserve_asset.fun {
|
||||
return Err(InvalidFeeAsset)
|
||||
}
|
||||
}
|
||||
|
||||
let (token, amount) = match_expression!(
|
||||
reserve_asset,
|
||||
MultiAsset {
|
||||
id: Concrete(MultiLocation { parents: 0, interior: X1(AccountKey20 { network , key })}),
|
||||
fun: Fungible(amount)
|
||||
} if self.network_matches(network),
|
||||
(H160(*key), *amount)
|
||||
)
|
||||
.ok_or(AssetResolutionFailed)?;
|
||||
|
||||
// transfer amount must be greater than 0.
|
||||
ensure!(amount > 0, ZeroAssetTransfer);
|
||||
|
||||
// Check if there is a SetTopic and skip over it if found.
|
||||
let topic_id = match_expression!(self.next()?, SetTopic(id), id).ok_or(SetTopicExpected)?;
|
||||
|
||||
Ok((AgentExecuteCommand::TransferToken { token, recipient, amount }, *topic_id))
|
||||
}
|
||||
|
||||
fn next(&mut self) -> Result<&'a Instruction<Call>, XcmConverterError> {
|
||||
self.iter.next().ok_or(XcmConverterError::UnexpectedEndOfXcm)
|
||||
}
|
||||
|
||||
fn peek(&mut self) -> Result<&&'a Instruction<Call>, XcmConverterError> {
|
||||
self.iter.peek().ok_or(XcmConverterError::UnexpectedEndOfXcm)
|
||||
}
|
||||
|
||||
fn network_matches(&self, network: &Option<NetworkId>) -> bool {
|
||||
if let Some(network) = network {
|
||||
network == self.ethereum_network
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user