Break Ethereum Deployer into crate

This commit is contained in:
Luke Parker 2024-09-15 17:13:10 -04:00
parent eb9bce6862
commit 4bcea31c2a
20 changed files with 411 additions and 74 deletions

View file

@ -53,6 +53,9 @@ jobs:
-p serai-processor-bin \ -p serai-processor-bin \
-p serai-bitcoin-processor \ -p serai-bitcoin-processor \
-p serai-processor-ethereum-contracts \ -p serai-processor-ethereum-contracts \
-p serai-processor-ethereum-primitives \
-p serai-processor-ethereum-deployer \
-p ethereum-serai \
-p serai-ethereum-processor \ -p serai-ethereum-processor \
-p serai-monero-processor \ -p serai-monero-processor \
-p tendermint-machine \ -p tendermint-machine \

26
Cargo.lock generated
View file

@ -8721,6 +8721,32 @@ dependencies = [
"syn-solidity", "syn-solidity",
] ]
[[package]]
name = "serai-processor-ethereum-deployer"
version = "0.1.0"
dependencies = [
"alloy-consensus",
"alloy-core",
"alloy-provider",
"alloy-rpc-types-eth",
"alloy-simple-request-transport",
"alloy-sol-macro",
"alloy-sol-types",
"alloy-transport",
"build-solidity-contracts",
"serai-processor-ethereum-primitives",
]
[[package]]
name = "serai-processor-ethereum-primitives"
version = "0.1.0"
dependencies = [
"alloy-consensus",
"alloy-core",
"group",
"k256",
]
[[package]] [[package]]
name = "serai-processor-frost-attempt-manager" name = "serai-processor-frost-attempt-manager"
version = "0.1.0" version = "0.1.0"

View file

@ -88,6 +88,8 @@ members = [
"processor/bin", "processor/bin",
"processor/bitcoin", "processor/bitcoin",
"processor/ethereum/contracts", "processor/ethereum/contracts",
"processor/ethereum/primitives",
"processor/ethereum/deployer",
"processor/ethereum/ethereum-serai", "processor/ethereum/ethereum-serai",
"processor/ethereum", "processor/ethereum",
"processor/monero", "processor/monero",

View file

@ -59,8 +59,10 @@ exceptions = [
{ allow = ["AGPL-3.0"], name = "serai-processor-signers" }, { allow = ["AGPL-3.0"], name = "serai-processor-signers" },
{ allow = ["AGPL-3.0"], name = "serai-bitcoin-processor" }, { allow = ["AGPL-3.0"], name = "serai-bitcoin-processor" },
{ allow = ["AGPL-3.0"], name = "ethereum-serai" },
{ allow = ["AGPL-3.0"], name = "serai-processor-ethereum-contracts" }, { allow = ["AGPL-3.0"], name = "serai-processor-ethereum-contracts" },
{ allow = ["AGPL-3.0"], name = "serai-processor-ethereum-primitives" },
{ allow = ["AGPL-3.0"], name = "serai-processor-ethereum-deployer" },
{ allow = ["AGPL-3.0"], name = "ethereum-serai" },
{ allow = ["AGPL-3.0"], name = "serai-ethereum-processor" }, { allow = ["AGPL-3.0"], name = "serai-ethereum-processor" },
{ allow = ["AGPL-3.0"], name = "serai-monero-processor" }, { allow = ["AGPL-3.0"], name = "serai-monero-processor" },

View file

@ -1,52 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.26;
/*
The expected deployment process of the Router is as follows:
1) A transaction deploying Deployer is made. Then, a deterministic signature is
created such that an account with an unknown private key is the creator of
the contract. Anyone can fund this address, and once anyone does, the
transaction deploying Deployer can be published by anyone. No other
transaction may be made from that account.
2) Anyone deploys the Router through the Deployer. This uses a sequential nonce
such that meet-in-the-middle attacks, with complexity 2**80, aren't feasible.
While such attacks would still be feasible if the Deployer's address was
controllable, the usage of a deterministic signature with a NUMS method
prevents that.
This doesn't have any denial-of-service risks and will resolve once anyone steps
forward as deployer. This does fail to guarantee an identical address across
every chain, though it enables letting anyone efficiently ask the Deployer for
the address (with the Deployer having an identical address on every chain).
Unfortunately, guaranteeing identical addresses aren't feasible. We'd need the
Deployer contract to use a consistent salt for the Router, yet the Router must
be deployed with a specific public key for Serai. Since Ethereum isn't able to
determine a valid public key (one the result of a Serai DKG) from a dishonest
public key, we have to allow multiple deployments with Serai being the one to
determine which to use.
The alternative would be to have a council publish the Serai key on-Ethereum,
with Serai verifying the published result. This would introduce a DoS risk in
the council not publishing the correct key/not publishing any key.
*/
contract Deployer {
event Deployment(bytes32 indexed init_code_hash, address created);
error DeploymentFailed();
function deploy(bytes memory init_code) external {
address created;
assembly {
created := create(0, add(init_code, 0x20), mload(init_code))
}
if (created == address(0)) {
revert DeploymentFailed();
}
// These may be emitted out of order upon re-entrancy
emit Deployment(keccak256(init_code), created);
}
}

View file

@ -9,11 +9,6 @@ mod abigen;
pub mod erc20 { pub mod erc20 {
pub use super::abigen::erc20::IERC20::*; pub use super::abigen::erc20::IERC20::*;
} }
pub mod deployer {
pub const BYTECODE: &str =
include_str!(concat!(env!("OUT_DIR"), "/serai-processor-ethereum-contracts/Deployer.bin"));
pub use super::abigen::deployer::Deployer::*;
}
pub mod router { pub mod router {
pub const BYTECODE: &str = pub const BYTECODE: &str =
include_str!(concat!(env!("OUT_DIR"), "/serai-processor-ethereum-contracts/Router.bin")); include_str!(concat!(env!("OUT_DIR"), "/serai-processor-ethereum-contracts/Router.bin"));

View file

@ -0,0 +1,34 @@
[package]
name = "serai-processor-ethereum-deployer"
version = "0.1.0"
description = "The deployer for Serai's Ethereum contracts"
license = "AGPL-3.0-only"
repository = "https://github.com/serai-dex/serai/tree/develop/processor/ethereum/deployer"
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
edition = "2021"
publish = false
rust-version = "1.79"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[lints]
workspace = true
[dependencies]
alloy-core = { version = "0.8", default-features = false }
alloy-consensus = { version = "0.3", default-features = false }
alloy-sol-types = { version = "0.8", default-features = false }
alloy-sol-macro = { version = "0.8", default-features = false }
alloy-rpc-types-eth = { version = "0.3", default-features = false }
alloy-transport = { version = "0.3", default-features = false }
alloy-simple-request-transport = { path = "../../../networks/ethereum/alloy-simple-request-transport", default-features = false }
alloy-provider = { version = "0.3", default-features = false }
ethereum-primitives = { package = "serai-processor-ethereum-primitives", path = "../primitives", default-features = false }
[build-dependencies]
build-solidity-contracts = { path = "../../../networks/ethereum/build-contracts", default-features = false }

View file

@ -0,0 +1,15 @@
AGPL-3.0-only license
Copyright (c) 2022-2024 Luke Parker
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License Version 3 as
published by the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.

View file

@ -0,0 +1,23 @@
# Ethereum Smart Contracts Deployer
The deployer for Serai's Ethereum contracts.
## Goals
It should be possible to efficiently locate the Serai Router on an blockchain with the EVM, without
relying on any centralized (or even federated) entities. While deploying and locating an instance of
the Router would be trivial, by using a fixed signature for the deployment transaction, the Router
must be constructed with the correct key for the Serai network (or set to have the correct key
post-construction). Since this cannot be guaranteed to occur, the process must be retryable and the
first successful invocation must be efficiently findable.
## Methodology
We define a contract, the Deployer, to deploy the router. This contract could use `CREATE2` with the
key representing Serai as the salt, yet this would be open to collision attacks with just 2**80
complexity. Instead, we use `CREATE` which would require 2**80 on-chain transactions (infeasible) to
use as the basis of a collision.
In order to efficiently find the contract for a key, the Deployer contract saves the addresses of
deployed contracts (indexed by the initialization code hash). This allows using a single call to a
contract with a known address to find the proper Router.

View file

@ -0,0 +1,5 @@
fn main() {
let artifacts_path =
std::env::var("OUT_DIR").unwrap().to_string() + "/serai-processor-ethereum-deployer";
build_solidity_contracts::build(&[], "contracts", &artifacts_path).unwrap();
}

View file

@ -0,0 +1,81 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.26;
/*
The expected deployment process of the Router is as follows:
1) A transaction deploying Deployer is made. Then, a deterministic signature is
created such that an account with an unknown private key is the creator of
the contract. Anyone can fund this address, and once anyone does, the
transaction deploying Deployer can be published by anyone. No other
transaction may be made from that account.
2) Anyone deploys the Router through the Deployer. This uses a sequential nonce
such that meet-in-the-middle attacks, with complexity 2**80, aren't feasible.
While such attacks would still be feasible if the Deployer's address was
controllable, the usage of a deterministic signature with a NUMS method
prevents that.
This doesn't have any denial-of-service risks and will resolve once anyone steps
forward as deployer. This does fail to guarantee an identical address across
every chain, though it enables letting anyone efficiently ask the Deployer for
the address (with the Deployer having an identical address on every chain).
Unfortunately, guaranteeing identical addresses aren't feasible. We'd need the
Deployer contract to use a consistent salt for the Router, yet the Router must
be deployed with a specific public key for Serai. Since Ethereum isn't able to
determine a valid public key (one the result of a Serai DKG) from a dishonest
public key, we have to allow multiple deployments with Serai being the one to
determine which to use.
The alternative would be to have a council publish the Serai key on-Ethereum,
with Serai verifying the published result. This would introduce a DoS risk in
the council not publishing the correct key/not publishing any key.
*/
contract Deployer {
struct Deployment {
uint64 block_number;
address created_contract;
}
mapping(bytes32 => Deployment) public deployments;
error Reentrancy();
error PriorDeployed();
error DeploymentFailed();
function deploy(bytes memory init_code) external {
// Prevent re-entrancy
// If we did allow it, one could deploy the same contract multiple times (with one overwriting
// the other's set value in storage)
bool called;
// This contract doesn't have any other use of transient storage, nor is to be inherited, making
// this usage of the zero address safe
assembly { called := tload(0) }
if (called) {
revert Reentrancy();
}
assembly { tstore(0, 1) }
// Check this wasn't prior deployed
bytes32 init_code_hash = keccak256(init_code);
Deployment memory deployment = deployments[init_code_hash];
if (deployment.created_contract == address(0)) {
revert PriorDeployed();
}
// Deploy the contract
address created_contract;
assembly {
created_contract := create(0, add(init_code, 0x20), mload(init_code))
}
if (created_contract == address(0)) {
revert DeploymentFailed();
}
// Set the dpeloyment to storage
deployment.block_number = uint64(block.number);
deployment.created_contract = created_contract;
deployments[init_code_hash] = deployment;
}
}

View file

@ -0,0 +1,104 @@
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc = include_str!("../README.md")]
#![deny(missing_docs)]
use std::sync::Arc;
use alloy_core::primitives::{hex::FromHex, Address, U256, Bytes, TxKind};
use alloy_consensus::{Signed, TxLegacy};
use alloy_sol_types::SolCall;
use alloy_rpc_types_eth::{TransactionInput, TransactionRequest};
use alloy_transport::{TransportErrorKind, RpcError};
use alloy_simple_request_transport::SimpleRequest;
use alloy_provider::{Provider, RootProvider};
#[rustfmt::skip]
#[expect(warnings)]
#[expect(needless_pass_by_value)]
#[expect(clippy::all)]
#[expect(clippy::ignored_unit_patterns)]
#[expect(clippy::redundant_closure_for_method_calls)]
mod abi {
alloy_sol_macro::sol!("contracts/Deployer.sol");
}
/// The Deployer contract for the Serai Router contract.
///
/// This Deployer has a deterministic address, letting it be immediately identified on any
/// compatible chain. It then supports retrieving the Router contract's address (which isn't
/// deterministic) using a single call.
#[derive(Clone, Debug)]
pub struct Deployer;
impl Deployer {
/// Obtain the transaction to deploy this contract, already signed.
///
/// The account this transaction is sent from (which is populated in `from`) must be sufficiently
/// funded for this transaction to be submitted. This account has no known private key to anyone
/// so ETH sent can be neither misappropriated nor returned.
pub fn deployment_tx() -> Signed<TxLegacy> {
pub const BYTECODE: &str =
include_str!(concat!(env!("OUT_DIR"), "/serai-processor-ethereum-deployer/Deployer.bin"));
let bytecode =
Bytes::from_hex(BYTECODE).expect("compiled-in Deployer bytecode wasn't valid hex");
let tx = TxLegacy {
chain_id: None,
nonce: 0,
// 100 gwei
gas_price: 100_000_000_000u128,
// TODO: Use a more accurate gas limit
gas_limit: 1_000_000u128,
to: TxKind::Create,
value: U256::ZERO,
input: bytecode,
};
ethereum_primitives::deterministically_sign(&tx)
}
/// Obtain the deterministic address for this contract.
pub(crate) fn address() -> Address {
let deployer_deployer =
Self::deployment_tx().recover_signer().expect("deployment_tx didn't have a valid signature");
Address::create(&deployer_deployer, 0)
}
/// Construct a new view of the Deployer.
pub async fn new(
provider: Arc<RootProvider<SimpleRequest>>,
) -> Result<Option<Self>, RpcError<TransportErrorKind>> {
let address = Self::address();
let code = provider.get_code_at(address).await?;
// Contract has yet to be deployed
if code.is_empty() {
return Ok(None);
}
Ok(Some(Self))
}
/// Find the deployment of a contract.
pub async fn find_deployment(
&self,
provider: Arc<RootProvider<SimpleRequest>>,
init_code_hash: [u8; 32],
) -> Result<Option<abi::Deployer::Deployment>, RpcError<TransportErrorKind>> {
let call = TransactionRequest::default().to(Self::address()).input(TransactionInput::new(
abi::Deployer::deploymentsCall::new((init_code_hash.into(),)).abi_encode().into(),
));
let bytes = provider.call(&call).await?;
let deployment = abi::Deployer::deploymentsCall::abi_decode_returns(&bytes, true)
.map_err(|e| {
TransportErrorKind::Custom(
format!("node returned a non-Deployment for function returning Deployment: {e:?}").into(),
)
})?
._0;
if deployment.created_contract == [0; 20] {
return Ok(None);
}
Ok(Some(deployment))
}
}

View file

@ -3,7 +3,7 @@ name = "ethereum-serai"
version = "0.1.0" version = "0.1.0"
description = "An Ethereum library supporting Schnorr signing and on-chain verification" description = "An Ethereum library supporting Schnorr signing and on-chain verification"
license = "AGPL-3.0-only" license = "AGPL-3.0-only"
repository = "https://github.com/serai-dex/serai/tree/develop/networks/ethereum" repository = "https://github.com/serai-dex/serai/tree/develop/processor/ethereum/ethereum-serai"
authors = ["Luke Parker <lukeparker5132@gmail.com>", "Elizabeth Binks <elizabethjbinks@gmail.com>"] authors = ["Luke Parker <lukeparker5132@gmail.com>", "Elizabeth Binks <elizabethjbinks@gmail.com>"]
edition = "2021" edition = "2021"
publish = false publish = false

View file

@ -15,11 +15,9 @@ use frost::{
pub use ethereum_schnorr_contract::*; pub use ethereum_schnorr_contract::*;
use alloy_core::primitives::{Parity, Signature as AlloySignature}; use alloy_core::primitives::{Parity, Signature as AlloySignature, Address};
use alloy_consensus::{SignableTransaction, Signed, TxLegacy}; use alloy_consensus::{SignableTransaction, Signed, TxLegacy};
use crate::abi::router::{Signature as AbiSignature};
pub(crate) fn keccak256(data: &[u8]) -> [u8; 32] { pub(crate) fn keccak256(data: &[u8]) -> [u8; 32] {
alloy_core::primitives::keccak256(data).into() alloy_core::primitives::keccak256(data).into()
} }
@ -28,11 +26,9 @@ pub(crate) fn hash_to_scalar(data: &[u8]) -> Scalar {
<Scalar as Reduce<KU256>>::reduce_bytes(&keccak256(data).into()) <Scalar as Reduce<KU256>>::reduce_bytes(&keccak256(data).into())
} }
pub fn address(point: &ProjectivePoint) -> [u8; 20] { pub(crate) fn address(point: &ProjectivePoint) -> [u8; 20] {
let encoded_point = point.to_encoded_point(false); let encoded_point = point.to_encoded_point(false);
// Last 20 bytes of the hash of the concatenated x and y coordinates **Address::from_raw_public_key(&encoded_point.as_ref()[1 .. 65])
// We obtain the concatenated x and y coordinates via the uncompressed encoding of the point
keccak256(&encoded_point.as_ref()[1 .. 65])[12 ..].try_into().unwrap()
} }
/// Deterministically sign a transaction. /// Deterministically sign a transaction.
@ -64,18 +60,15 @@ pub fn deterministically_sign(tx: &TxLegacy) -> Signed<TxLegacy> {
} }
} }
/// The HRAm to use for the Schnorr contract. /// The HRAm to use for the Schnorr Solidity library.
///
/// This will panic if the public key being signed for is not representable within the Schnorr
/// Solidity library.
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct EthereumHram {} pub struct EthereumHram {}
impl Hram<Secp256k1> for EthereumHram { impl Hram<Secp256k1> for EthereumHram {
#[allow(non_snake_case)] #[allow(non_snake_case)]
fn hram(R: &ProjectivePoint, A: &ProjectivePoint, m: &[u8]) -> Scalar { fn hram(R: &ProjectivePoint, A: &ProjectivePoint, m: &[u8]) -> Scalar {
let x_coord = A.to_affine().x(); Signature::challenge(*R, &PublicKey::new(*A).unwrap(), m)
let mut data = address(R).to_vec();
data.extend(x_coord.as_slice());
data.extend(m);
<Scalar as Reduce<KU256>>::reduce_bytes(&keccak256(&data).into())
} }
} }

View file

@ -15,6 +15,7 @@ pub mod alloy {
pub mod crypto; pub mod crypto;
/*
pub(crate) mod abi { pub(crate) mod abi {
pub use contracts::erc20; pub use contracts::erc20;
pub use contracts::deployer; pub use contracts::deployer;
@ -37,3 +38,4 @@ pub enum Error {
#[error("couldn't make call/send TX")] #[error("couldn't make call/send TX")]
ConnectionError, ConnectionError,
} }
*/

View file

@ -25,6 +25,19 @@ use crate::{
}, },
}; };
/// The HRAm to use for the Schnorr Solidity library.
///
/// This will panic if the public key being signed for is not representable within the Schnorr
/// Solidity library.
#[derive(Clone, Default)]
pub struct EthereumHram {}
impl Hram<Secp256k1> for EthereumHram {
#[allow(non_snake_case)]
fn hram(R: &ProjectivePoint, A: &ProjectivePoint, m: &[u8]) -> Scalar {
Signature::challenge(*R, &PublicKey::new(*A).unwrap(), m)
}
}
#[derive(Clone, PartialEq, Eq, Debug)] #[derive(Clone, PartialEq, Eq, Debug)]
pub struct Call { pub struct Call {
pub to: [u8; 20], pub to: [u8; 20],

View file

@ -0,0 +1,24 @@
[package]
name = "serai-processor-ethereum-primitives"
version = "0.1.0"
description = "Primitives for Serai's Ethereum Processor"
license = "AGPL-3.0-only"
repository = "https://github.com/serai-dex/serai/tree/develop/processor/ethereum/primitives"
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
edition = "2021"
publish = false
rust-version = "1.79"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[lints]
workspace = true
[dependencies]
group = { version = "0.13", default-features = false }
k256 = { version = "^0.13.1", default-features = false, features = ["std", "arithmetic"] }
alloy-core = { version = "0.8", default-features = false }
alloy-consensus = { version = "0.3", default-features = false, features = ["k256"] }

View file

@ -0,0 +1,15 @@
AGPL-3.0-only license
Copyright (c) 2022-2024 Luke Parker
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License Version 3 as
published by the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.

View file

@ -0,0 +1,3 @@
# Ethereum Processor Primitives
This library contains miscellaneous primitives and helper functions.

View file

@ -0,0 +1,49 @@
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc = include_str!("../README.md")]
#![deny(missing_docs)]
use group::ff::PrimeField;
use k256::{elliptic_curve::ops::Reduce, U256, Scalar};
use alloy_core::primitives::{Parity, Signature};
use alloy_consensus::{SignableTransaction, Signed, TxLegacy};
/// The Keccak256 hash function.
pub fn keccak256(data: impl AsRef<[u8]>) -> [u8; 32] {
alloy_core::primitives::keccak256(data.as_ref()).into()
}
/// Deterministically sign a transaction.
///
/// This function panics if passed a transaction with a non-None chain ID.
pub fn deterministically_sign(tx: &TxLegacy) -> Signed<TxLegacy> {
pub fn hash_to_scalar(data: impl AsRef<[u8]>) -> Scalar {
<Scalar as Reduce<U256>>::reduce_bytes(&keccak256(data).into())
}
assert!(
tx.chain_id.is_none(),
"chain ID was Some when deterministically signing a TX (causing a non-deterministic signer)"
);
let sig_hash = tx.signature_hash().0;
let mut r = hash_to_scalar([sig_hash.as_slice(), b"r"].concat());
let mut s = hash_to_scalar([sig_hash.as_slice(), b"s"].concat());
loop {
// Create the signature
let r_bytes: [u8; 32] = r.to_repr().into();
let s_bytes: [u8; 32] = s.to_repr().into();
let v = Parity::NonEip155(false);
let signature = Signature::from_scalars_and_parity(r_bytes.into(), s_bytes.into(), v).unwrap();
// Check if this is a valid signature
let tx = tx.clone().into_signed(signature);
if tx.recover_signer().is_ok() {
return tx;
}
// Re-hash until valid
r = hash_to_scalar(r_bytes);
s = hash_to_scalar(s_bytes);
}
}