// Copyright 2022-2024 Protocol Labs
// SPDX-License-Identifier: Apache-2.0, MIT
// The IPC actors have bindings in `recall_ipc_actors_abis`.
// Here we define stable IDs for them, so we can deploy the
// Solidity contracts during genesis.
use anyhow::Context;
use ethers::core::abi::Tokenize;
use ethers::core::types as et;
use ethers::core::utils::keccak256;
use fvm_shared::address::Error as AddressError;
use fvm_shared::address::Payload;
use lazy_static::lazy_static;
use recall_fendermint_vm_genesis::{Power, Validator};
use recall_ipc_actors_abis as ia;
pub use recall_ipc_actors_abis::checkpointing_facet::BottomUpCheckpoint;
use recall_ipc_api::subnet_id::SubnetID;
use recall_merkle_tree_rs::{
core::{process_proof, Hash},
format::Raw,
standard::{standard_leaf_hash, LeafType, StandardMerkleTree},
};
use crate::{
diamond::{EthContract, EthContractMap, EthFacet},
eam::{EthAddress, EAM_ACTOR_ID},
};
define_id!(GATEWAY { id: 64 });
define_id!(SUBNETREGISTRY { id: 65 });
lazy_static! {
/// Contracts deployed at genesis with well-known IDs.
pub static ref IPC_CONTRACTS: EthContractMap = {
[
(
gateway::CONTRACT_NAME,
EthContract {
actor_id: GATEWAY_ACTOR_ID,
abi: ia::gateway_diamond::GATEWAYDIAMOND_ABI.to_owned(),
facets: vec![
EthFacet {
name: "GatewayGetterFacet",
abi: ia::gateway_getter_facet::GATEWAYGETTERFACET_ABI.to_owned(),
},
EthFacet {
name: "GatewayManagerFacet",
abi: ia::gateway_manager_facet::GATEWAYMANAGERFACET_ABI.to_owned(),
},
EthFacet {
name: "TopDownFinalityFacet",
abi: ia::top_down_finality_facet::TOPDOWNFINALITYFACET_ABI.to_owned(),
},
EthFacet {
name: "CheckpointingFacet",
abi: ia::checkpointing_facet::CHECKPOINTINGFACET_ABI.to_owned(),
},
EthFacet {
name: "GatewayMessengerFacet",
abi: ia::gateway_messenger_facet::GATEWAYMESSENGERFACET_ABI.to_owned(),
},
EthFacet {
name: "XnetMessagingFacet",
abi: ia::xnet_messaging_facet::XNETMESSAGINGFACET_ABI.to_owned(),
},
EthFacet {
name: "DiamondLoupeFacet",
abi: ia::diamond_loupe_facet::DIAMONDLOUPEFACET_ABI.to_owned(),
},
EthFacet {
name: "DiamondCutFacet",
abi: ia::diamond_cut_facet::DIAMONDCUTFACET_ABI.to_owned(),
},
EthFacet {
name: "OwnershipFacet",
abi: ia::ownership_facet::OWNERSHIPFACET_ABI.to_owned(),
},
],
},
),
(
registry::CONTRACT_NAME,
EthContract {
actor_id: SUBNETREGISTRY_ACTOR_ID,
abi: ia::subnet_registry_diamond::SUBNETREGISTRYDIAMOND_ABI.to_owned(),
facets: vec![
// The registry incorporates the SubnetActor facets, although these aren't expected differently in the constructor.
EthFacet {
name: "SubnetActorGetterFacet",
abi: ia::subnet_actor_getter_facet::SUBNETACTORGETTERFACET_ABI.to_owned(),
},
EthFacet {
name: "SubnetActorManagerFacet",
abi: ia::subnet_actor_manager_facet::SUBNETACTORMANAGERFACET_ABI.to_owned(),
},
EthFacet {
name: "SubnetActorRewardFacet",
abi: ia::subnet_actor_reward_facet::SUBNETACTORREWARDFACET_ABI.to_owned(),
},
EthFacet {
name: "SubnetActorCheckpointingFacet",
abi: ia::subnet_actor_checkpointing_facet::SUBNETACTORCHECKPOINTINGFACET_ABI.to_owned(),
},
EthFacet {
name: "SubnetActorPauseFacet",
abi: ia::subnet_actor_pause_facet::SUBNETACTORPAUSEFACET_ABI.to_owned(),
},
EthFacet {
name: "DiamondLoupeFacet",
abi: ia::diamond_loupe_facet::DIAMONDLOUPEFACET_ABI.to_owned(),
},
EthFacet {
name: "DiamondCutFacet",
abi: ia::diamond_cut_facet::DIAMONDCUTFACET_ABI.to_owned(),
},
EthFacet {
name: "OwnershipFacet",
abi: ia::ownership_facet::OWNERSHIPFACET_ABI.to_owned(),
},
EthFacet {
name: "SubnetActorActivityFacet",
abi: ia::subnet_actor_activity_facet::SUBNETACTORACTIVITYFACET_ABI
.to_owned(),
},
// ========== IF YOU WANT TO ADD FACET FOR SUBNET, APPEND HERE ==========
// The registry has its own facets:
// https://github.com/consensus-shipyard/ipc-solidity-actors/blob/b01a2dffe367745f55111a65536a3f6fea9165f5/scripts/deploy-registry.template.ts#L58-L67
EthFacet {
name: "RegisterSubnetFacet",
abi: ia::register_subnet_facet::REGISTERSUBNETFACET_ABI
.to_owned(),
},
EthFacet {
name: "SubnetGetterFacet",
abi: ia::subnet_getter_facet::SUBNETGETTERFACET_ABI.to_owned(),
},
],
},
),
]
.into_iter()
.collect()
};
/// Contracts that need to be deployed afresh for each subnet.
///
/// See [deploy-sa-diamond.ts](https://github.com/consensus-shipyard/ipc-solidity-actors/blob/dev/scripts/deploy-sa-diamond.ts)
///
/// But it turns out that the [SubnetRegistry](https://github.com/consensus-shipyard/ipc-solidity-actors/blob/3b0f3528b79e53e3c90f15016a40892122938ef0/src/SubnetRegistry.sol#L67)
/// actor has this `SubnetActorDiamond` and its facets baked into it, and able to deploy without further ado.
pub static ref SUBNET_CONTRACTS: EthContractMap = {
[
(
subnet::CONTRACT_NAME,
EthContract {
actor_id: 0,
abi: ia::subnet_actor_diamond::SUBNETACTORDIAMOND_ABI.to_owned(),
facets: vec![
EthFacet {
name: "SubnetActorGetterFacet",
abi: ia::subnet_actor_getter_facet::SUBNETACTORGETTERFACET_ABI.to_owned(),
},
EthFacet {
name: "SubnetActorManagerFacet",
abi: ia::subnet_actor_manager_facet::SUBNETACTORMANAGERFACET_ABI.to_owned(),
},
EthFacet {
name: "SubnetActorRewardFacet",
abi: ia::subnet_actor_reward_facet::SUBNETACTORREWARDFACET_ABI.to_owned(),
},
EthFacet {
name: "SubnetActorCheckpointingFacet",
abi: ia::subnet_actor_checkpointing_facet::SUBNETACTORCHECKPOINTINGFACET_ABI.to_owned(),
},
EthFacet {
name: "SubnetActorPauseFacet",
abi: ia::subnet_actor_pause_facet::SUBNETACTORPAUSEFACET_ABI.to_owned(),
},
EthFacet {
name: "DiamondLoupeFacet",
abi: ia::diamond_loupe_facet::DIAMONDLOUPEFACET_ABI.to_owned(),
},
EthFacet {
name: "DiamondCutFacet",
abi: ia::diamond_cut_facet::DIAMONDCUTFACET_ABI.to_owned(),
},
EthFacet {
name: "OwnershipFacet",
abi: ia::ownership_facet::OWNERSHIPFACET_ABI.to_owned(),
},
],
},
),
]
.into_iter()
.collect()
};
/// ABI types of the Merkle tree which contains validator addresses and their voting power.
pub static ref VALIDATOR_TREE_FIELDS: Vec<String> =
vec!["address".to_owned(), "uint256".to_owned()];
}
/// Construct a Merkle tree from the power table in a format which can be validated by
/// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/MerkleProof.sol
///
/// The reference implementation is https://github.com/OpenZeppelin/merkle-tree/
pub struct ValidatorMerkleTree {
tree: StandardMerkleTree<Raw>,
}
impl ValidatorMerkleTree {
pub fn new(validators: &[Validator<Power>]) -> anyhow::Result<Self> {
// Using the 20 byte address for keys because that's what the Solidity library returns
// when recovering a public key from a signature.
let values = validators
.iter()
.map(Self::validator_to_vec)
.collect::<anyhow::Result<Vec<_>>>()?;
let tree = StandardMerkleTree::of(&values, &VALIDATOR_TREE_FIELDS)
.context("failed to construct Merkle tree")?;
Ok(Self { tree })
}
pub fn root_hash(&self) -> Hash {
self.tree.root()
}
/// Create a Merkle proof for a validator.
pub fn prove(&self, validator: &Validator<Power>) -> anyhow::Result<Vec<Hash>> {
let v = Self::validator_to_vec(validator)?;
let proof = self
.tree
.get_proof(LeafType::LeafBytes(v))
.context("failed to produce Merkle proof")?;
Ok(proof)
}
/// Validate a proof against a known root hash.
pub fn validate(
validator: &Validator<Power>,
root: &Hash,
proof: &[Hash],
) -> anyhow::Result<bool> {
let v = Self::validator_to_vec(validator)?;
let h = standard_leaf_hash(v, &VALIDATOR_TREE_FIELDS)?;
let r = process_proof(&h, proof).context("failed to process Merkle proof")?;
Ok(*root == r)
}
/// Convert a validator to what we can pass to the tree.
fn validator_to_vec(validator: &Validator<Power>) -> anyhow::Result<Vec<String>> {
let addr = EthAddress::from(validator.public_key.0);
let addr = et::Address::from_slice(&addr.0);
let addr = format!("{addr:?}");
let power = et::U256::from(validator.power.0);
let power = power.to_string();
Ok(vec![addr, power])
}
}
/// Decompose a subnet ID into a root ID and a route of Ethereum addresses
pub fn subnet_id_to_eth(subnet_id: &SubnetID) -> Result<(u64, Vec<et::Address>), AddressError> {
// Every step along the way in the subnet ID we have an Ethereum address.
let mut route = Vec::new();
for addr in subnet_id.children() {
let addr = match addr.payload() {
Payload::ID(id) => EthAddress::from_id(*id),
Payload::Delegated(da)
if da.namespace() == EAM_ACTOR_ID && da.subaddress().len() == 20 =>
{
EthAddress(da.subaddress().try_into().expect("checked length"))
}
_ => return Err(AddressError::InvalidPayload),
};
route.push(et::H160::from(addr.0))
}
Ok((subnet_id.root_id(), route))
}
/// Hash some value in the same way we'd hash it in Solidity.
///
/// Be careful that if we have to hash a single struct,
/// Solidity's `abi.encode` function will treat it as a tuple,
/// so it has to be passed as a tuple in Rust. Vectors are fine.
pub fn abi_hash<T: Tokenize>(value: T) -> [u8; 32] {
keccak256(ethers::abi::encode(&value.into_tokens()))
}
/// Types where we need to match the way we sign them in Solidity and Rust.
pub trait AbiHash {
/// Hash the item the way we would in Solidity.
fn abi_hash(self) -> [u8; 32];
}
macro_rules! abi_hash {
(struct $name:ty) => {
// Structs have to be hashed as a tuple.
impl AbiHash for $name {
fn abi_hash(self) -> [u8; 32] {
abi_hash((self,))
}
}
};
(Vec < $name:ty >) => {
// Vectors can be hashed as-is
impl AbiHash for Vec<$name> {
fn abi_hash(self) -> [u8; 32] {
abi_hash(self)
}
}
};
}
abi_hash!(struct recall_ipc_actors_abis::checkpointing_facet::BottomUpCheckpoint);
abi_hash!(struct recall_ipc_actors_abis::subnet_actor_checkpointing_facet::BottomUpCheckpoint);
abi_hash!(Vec<recall_ipc_actors_abis::gateway_getter_facet::IpcEnvelope>);
abi_hash!(Vec<recall_ipc_actors_abis::subnet_actor_checkpointing_facet::IpcEnvelope>);
abi_hash!(Vec<recall_ipc_actors_abis::subnet_actor_getter_facet::IpcEnvelope>);
pub mod gateway {
use super::subnet_id_to_eth;
use ethers::contract::{EthAbiCodec, EthAbiType};
use ethers::core::types::{Bytes, H160, U256};
use fvm_shared::address::Error as AddressError;
use fvm_shared::econ::TokenAmount;
use recall_fendermint_vm_genesis::ipc::GatewayParams;
use recall_fendermint_vm_genesis::{Collateral, Validator};
use recall_ipc_actors_abis::gateway_diamond::SubnetID as GatewaySubnetID;
pub use recall_ipc_actors_abis::gateway_getter_facet::Validator as GatewayValidator;
use crate::eam::EthAddress;
pub const CONTRACT_NAME: &str = "GatewayDiamond";
pub const METHOD_INVOKE_CONTRACT: u64 = crate::evm::Method::InvokeContract as u64;
// Constructor parameters aren't generated as part of the Rust bindings.
// TODO: Remove these once https://github.com/gakonst/ethers-rs/pull/2631 is merged.
/// Container type `ConstructorParameters`.
///
/// See [GatewayDiamond.sol](https://github.com/consensus-shipyard/ipc/blob/bc3512fc7c4b0dfcdaac89f297f99cafae68f097/contracts/src/GatewayDiamond.sol#L28-L36)
#[derive(Clone, EthAbiType, EthAbiCodec, Default, Debug, PartialEq, Eq, Hash)]
pub struct ConstructorParameters {
pub bottom_up_check_period: U256,
pub active_validators_limit: u16,
pub majority_percentage: u8,
pub network_name: GatewaySubnetID,
pub validators: Vec<GatewayValidator>,
}
impl ConstructorParameters {
pub fn new(
params: GatewayParams,
validators: Vec<Validator<Collateral>>,
) -> anyhow::Result<Self> {
// Every validator has an Ethereum address.
let validators = validators
.into_iter()
.map(|v| {
let pk = v.public_key.0.serialize();
let addr = EthAddress::new_secp256k1(&pk)?;
let collateral = tokens_to_u256(v.power.0);
Ok(GatewayValidator {
addr: H160::from(addr.0),
weight: collateral,
metadata: Bytes::from(pk),
})
})
.collect::<Result<Vec<_>, AddressError>>()?;
let (root, route) = subnet_id_to_eth(¶ms.subnet_id)?;
Ok(Self {
bottom_up_check_period: U256::from(params.bottom_up_check_period),
active_validators_limit: params.active_validators_limit,
majority_percentage: params.majority_percentage,
network_name: GatewaySubnetID { root, route },
validators,
})
}
}
fn tokens_to_u256(value: TokenAmount) -> U256 {
// XXX: Ignoring any error resulting from larger fee than what fits into U256. This is in genesis after all.
U256::from_big_endian(&value.atto().to_bytes_be().1)
}
#[cfg(test)]
mod tests {
use ethers::core::types::{Selector, U256};
use ethers_core::{
abi::Tokenize,
types::{Bytes, H160},
};
use fvm_shared::{bigint::BigInt, econ::TokenAmount};
use recall_ipc_actors_abis::gateway_diamond::SubnetID as GatewaySubnetID;
use recall_ipc_actors_abis::gateway_getter_facet::Validator as GatewayValidator;
use std::str::FromStr;
use crate::ipc::tests::{check_param_types, constructor_param_types};
use super::{tokens_to_u256, ConstructorParameters};
#[test]
fn tokenize_constructor_params() {
let cp = ConstructorParameters {
network_name: GatewaySubnetID {
root: 0,
route: Vec::new(),
},
bottom_up_check_period: U256::from(100),
majority_percentage: 67,
validators: vec![GatewayValidator {
addr: H160::zero(),
weight: U256::zero(),
metadata: Bytes::new(),
}],
active_validators_limit: 100,
};
// It looks like if we pass just the record then it will be passed as 5 tokens,
// but the constructor only expects one parameter, and it has to be a tuple.
let cp = (Vec::<Selector>::new(), cp);
let tokens = cp.into_tokens();
let cons = recall_ipc_actors_abis::gateway_diamond::GATEWAYDIAMOND_ABI
.constructor()
.expect("Gateway has a constructor");
let param_types = constructor_param_types(cons);
check_param_types(&tokens, ¶m_types).unwrap();
cons.encode_input(vec![], &tokens)
.expect("should encode constructor input");
}
#[test]
#[should_panic]
fn max_fee_exceeded() {
let mut value = BigInt::from_str(&U256::MAX.to_string()).unwrap();
value += 1;
let value = TokenAmount::from_atto(value);
let _ = tokens_to_u256(value);
}
}
}
pub mod registry {
use ethers::contract::{EthAbiCodec, EthAbiType};
use ethers::core::types::Address;
type FunctionSelector = [u8; 4];
pub const CONTRACT_NAME: &str = "SubnetRegistryDiamond";
/// Container type `ConstructorParameters`.
///
/// See [SubnetRegistry.sol](https://github.com/consensus-shipyard/ipc/blob/62f0d64fea993196cd3f148498c25a108b0069c8/contracts/src/SubnetRegistryDiamond.sol#L16-L28)
#[derive(Clone, EthAbiType, EthAbiCodec, Default, Debug, PartialEq, Eq, Hash)]
pub struct ConstructorParameters {
pub gateway: Address,
pub getter_facet: Address,
pub manager_facet: Address,
pub rewarder_facet: Address,
pub pauser_facet: Address,
pub checkpointer_facet: Address,
pub diamond_cut_facet: Address,
pub diamond_loupe_facet: Address,
pub ownership_facet: Address,
pub activity_facet: Address,
pub subnet_getter_selectors: Vec<FunctionSelector>,
pub subnet_manager_selectors: Vec<FunctionSelector>,
pub subnet_rewarder_selectors: Vec<FunctionSelector>,
pub subnet_pauser_selectors: Vec<FunctionSelector>,
pub subnet_checkpointer_selectors: Vec<FunctionSelector>,
pub subnet_actor_diamond_cut_selectors: Vec<FunctionSelector>,
pub subnet_actor_diamond_loupe_selectors: Vec<FunctionSelector>,
pub subnet_actor_ownership_selectors: Vec<FunctionSelector>,
pub subnet_actor_activity_selectors: Vec<FunctionSelector>,
pub creation_privileges: u8, // 0 = Unrestricted, 1 = Owner.
}
}
pub mod subnet {
use crate::revert_errors;
use recall_ipc_actors_abis::checkpointing_facet::CheckpointingFacetErrors;
use recall_ipc_actors_abis::gateway_manager_facet::GatewayManagerFacetErrors;
use recall_ipc_actors_abis::subnet_actor_checkpointing_facet::SubnetActorCheckpointingFacetErrors;
use recall_ipc_actors_abis::subnet_actor_manager_facet::SubnetActorManagerFacetErrors;
use recall_ipc_actors_abis::subnet_actor_pause_facet::SubnetActorPauseFacetErrors;
use recall_ipc_actors_abis::subnet_actor_reward_facet::SubnetActorRewardFacetErrors;
use recall_ipc_actors_abis::top_down_finality_facet::TopDownFinalityFacetErrors;
pub const CONTRACT_NAME: &str = "SubnetActorDiamond";
// The subnet actor has its own errors, but it also invokes the gateway, which might revert for its own reasons.
revert_errors! {
SubnetActorErrors {
SubnetActorManagerFacetErrors,
SubnetActorRewardFacetErrors,
SubnetActorPauseFacetErrors,
SubnetActorCheckpointingFacetErrors,
GatewayManagerFacetErrors,
CheckpointingFacetErrors,
TopDownFinalityFacetErrors
}
}
#[cfg(test)]
mod tests {
use ethers::abi::{AbiType, Tokenize};
use ethers::core::types::Bytes;
use recall_ipc_actors_abis::subnet_actor_checkpointing_facet::{
BottomUpCheckpoint, SubnetID,
};
#[test]
fn checkpoint_abi() {
// Some random checkpoint printed in a test that failed because the Rust ABI was different then the Solidity ABI.
let checkpoint = BottomUpCheckpoint {
subnet_id: SubnetID {
root: 12378393254986206693,
route: vec![
"0x7b11cf9ca8ccee13bb3d003c97af5c18434067a9",
"0x3d9019b8bf3bfd5e979ddc3b2761be54af867c47",
]
.into_iter()
.map(|h| h.parse().unwrap())
.collect(),
},
block_height: ethers::types::U256::from(21),
block_hash: [
107, 115, 111, 52, 42, 179, 77, 154, 254, 66, 52, 169, 43, 219, 25, 12, 53,
178, 232, 216, 34, 217, 96, 27, 0, 185, 215, 8, 155, 25, 15, 1,
],
next_configuration_number: 1,
msgs: vec![],
activity: Default::default(),
};
let param_type = BottomUpCheckpoint::param_type();
// Captured value of `abi.encode` in Solidity.
let expected_abi: Bytes = "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000156b736f342ab34d9afe4234a92bdb190c35b2e8d822d9601b00b9d7089b190f01000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000001a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000abc8e314f58b4de5000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000007b11cf9ca8ccee13bb3d003c97af5c18434067a90000000000000000000000003d9019b8bf3bfd5e979ddc3b2761be54af867c470000000000000000000000000000000000000000000000000000000000000000".parse().unwrap();
// XXX: It doesn't work with `decode_whole`.
let expected_tokens =
ethers::abi::decode(&[param_type], &expected_abi).expect("invalid Solidity ABI");
// The data needs to be wrapped into a tuple.
let observed_tokens = (checkpoint,).into_tokens();
let observed_abi: Bytes = ethers::abi::encode(&observed_tokens).into();
assert_eq!(observed_tokens, expected_tokens);
assert_eq!(observed_abi, expected_abi);
}
}
}
#[cfg(test)]
mod tests {
use anyhow::bail;
use ethers_core::abi::{Constructor, ParamType, Token};
use quickcheck_macros::quickcheck;
use recall_fendermint_vm_genesis::{Power, Validator};
use super::ValidatorMerkleTree;
/// Check all tokens against expected parameters; return any offending one.
///
/// Based on [Tokens::types_check]
pub fn check_param_types(tokens: &[Token], param_types: &[ParamType]) -> anyhow::Result<()> {
if param_types.len() != tokens.len() {
bail!(
"different number of parameters; expected {}, got {}",
param_types.len(),
tokens.len()
);
}
for (i, (pt, t)) in param_types.iter().zip(tokens).enumerate() {
if !t.type_check(pt) {
bail!("parameter {i} didn't type check: expected {pt:?}, got {t:?}");
}
}
Ok(())
}
/// Returns all input params of given constructor.
///
/// Based on [Constructor::param_types]
pub fn constructor_param_types(cons: &Constructor) -> Vec<ParamType> {
cons.inputs.iter().map(|p| p.kind.clone()).collect()
}
#[quickcheck]
fn merkleize_validators(validators: Vec<Validator<Power>>) {
if validators.is_empty() {
return;
}
let tree = ValidatorMerkleTree::new(&validators).expect("failed to create tree");
let root = tree.root_hash();
let validator = validators.first().unwrap();
let proof = tree.prove(validator).expect("failed to prove");
assert!(ValidatorMerkleTree::validate(validator, &root, &proof).expect("failed to validate"))
}
}