mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 12:19:24 +00:00
Work on testing the Router
This commit is contained in:
1
.github/workflows/tests.yml
vendored
1
.github/workflows/tests.yml
vendored
@@ -53,6 +53,7 @@ jobs:
|
|||||||
-p serai-processor-bin \
|
-p serai-processor-bin \
|
||||||
-p serai-bitcoin-processor \
|
-p serai-bitcoin-processor \
|
||||||
-p serai-processor-ethereum-primitives \
|
-p serai-processor-ethereum-primitives \
|
||||||
|
-p serai-processor-ethereum-test-primitives \
|
||||||
-p serai-processor-ethereum-deployer \
|
-p serai-processor-ethereum-deployer \
|
||||||
-p serai-processor-ethereum-router \
|
-p serai-processor-ethereum-router \
|
||||||
-p serai-processor-ethereum-erc20 \
|
-p serai-processor-ethereum-erc20 \
|
||||||
|
|||||||
19
Cargo.lock
generated
19
Cargo.lock
generated
@@ -8374,6 +8374,19 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serai-ethereum-test-primitives"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"alloy-consensus",
|
||||||
|
"alloy-core",
|
||||||
|
"alloy-provider",
|
||||||
|
"alloy-rpc-types-eth",
|
||||||
|
"alloy-simple-request-transport",
|
||||||
|
"k256",
|
||||||
|
"serai-processor-ethereum-primitives",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serai-full-stack-tests"
|
name = "serai-full-stack-tests"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -8706,7 +8719,9 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"alloy-consensus",
|
"alloy-consensus",
|
||||||
"alloy-core",
|
"alloy-core",
|
||||||
|
"alloy-node-bindings",
|
||||||
"alloy-provider",
|
"alloy-provider",
|
||||||
|
"alloy-rpc-client",
|
||||||
"alloy-rpc-types-eth",
|
"alloy-rpc-types-eth",
|
||||||
"alloy-simple-request-transport",
|
"alloy-simple-request-transport",
|
||||||
"alloy-sol-macro-expander",
|
"alloy-sol-macro-expander",
|
||||||
@@ -8716,12 +8731,16 @@ dependencies = [
|
|||||||
"build-solidity-contracts",
|
"build-solidity-contracts",
|
||||||
"ethereum-schnorr-contract",
|
"ethereum-schnorr-contract",
|
||||||
"group",
|
"group",
|
||||||
|
"k256",
|
||||||
|
"rand_core",
|
||||||
"serai-client",
|
"serai-client",
|
||||||
|
"serai-ethereum-test-primitives",
|
||||||
"serai-processor-ethereum-deployer",
|
"serai-processor-ethereum-deployer",
|
||||||
"serai-processor-ethereum-erc20",
|
"serai-processor-ethereum-erc20",
|
||||||
"serai-processor-ethereum-primitives",
|
"serai-processor-ethereum-primitives",
|
||||||
"syn 2.0.77",
|
"syn 2.0.77",
|
||||||
"syn-solidity",
|
"syn-solidity",
|
||||||
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ members = [
|
|||||||
"processor/bin",
|
"processor/bin",
|
||||||
"processor/bitcoin",
|
"processor/bitcoin",
|
||||||
"processor/ethereum/primitives",
|
"processor/ethereum/primitives",
|
||||||
|
"processor/ethereum/test-primitives",
|
||||||
"processor/ethereum/deployer",
|
"processor/ethereum/deployer",
|
||||||
"processor/ethereum/router",
|
"processor/ethereum/router",
|
||||||
"processor/ethereum/erc20",
|
"processor/ethereum/erc20",
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ exceptions = [
|
|||||||
|
|
||||||
{ allow = ["AGPL-3.0"], name = "serai-bitcoin-processor" },
|
{ allow = ["AGPL-3.0"], name = "serai-bitcoin-processor" },
|
||||||
{ allow = ["AGPL-3.0"], name = "serai-processor-ethereum-primitives" },
|
{ allow = ["AGPL-3.0"], name = "serai-processor-ethereum-primitives" },
|
||||||
|
{ allow = ["AGPL-3.0"], name = "serai-processor-ethereum-test-primitives" },
|
||||||
{ allow = ["AGPL-3.0"], name = "serai-processor-ethereum-deployer" },
|
{ allow = ["AGPL-3.0"], name = "serai-processor-ethereum-deployer" },
|
||||||
{ allow = ["AGPL-3.0"], name = "serai-processor-ethereum-router" },
|
{ allow = ["AGPL-3.0"], name = "serai-processor-ethereum-router" },
|
||||||
{ allow = ["AGPL-3.0"], name = "serai-processor-ethereum-erc20" },
|
{ allow = ["AGPL-3.0"], name = "serai-processor-ethereum-erc20" },
|
||||||
|
|||||||
@@ -59,12 +59,27 @@ impl Deployer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Obtain the deterministic address for this contract.
|
/// Obtain the deterministic address for this contract.
|
||||||
pub(crate) fn address() -> Address {
|
pub fn address() -> Address {
|
||||||
let deployer_deployer =
|
let deployer_deployer =
|
||||||
Self::deployment_tx().recover_signer().expect("deployment_tx didn't have a valid signature");
|
Self::deployment_tx().recover_signer().expect("deployment_tx didn't have a valid signature");
|
||||||
Address::create(&deployer_deployer, 0)
|
Address::create(&deployer_deployer, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Obtain the unsigned transaction to deploy a contract.
|
||||||
|
///
|
||||||
|
/// This will not have its `nonce`, `gas_price`, nor `gas_limit` filled out.
|
||||||
|
pub fn deploy_tx(init_code: Vec<u8>) -> TxLegacy {
|
||||||
|
TxLegacy {
|
||||||
|
chain_id: None,
|
||||||
|
nonce: 0,
|
||||||
|
gas_price: 0,
|
||||||
|
gas_limit: 0,
|
||||||
|
to: TxKind::Call(Self::address()),
|
||||||
|
value: U256::ZERO,
|
||||||
|
input: abi::Deployer::deployCall::new((init_code.into(),)).abi_encode().into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Construct a new view of the Deployer.
|
/// Construct a new view of the Deployer.
|
||||||
///
|
///
|
||||||
/// This will return `None` if the Deployer has yet to be deployed on-chain.
|
/// This will return `None` if the Deployer has yet to be deployed on-chain.
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ workspace = true
|
|||||||
group = { version = "0.13", default-features = false }
|
group = { version = "0.13", default-features = false }
|
||||||
|
|
||||||
alloy-core = { version = "0.8", default-features = false }
|
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-types = { version = "0.8", default-features = false }
|
||||||
|
|
||||||
|
alloy-consensus = { version = "0.3", default-features = false }
|
||||||
|
|
||||||
alloy-rpc-types-eth = { version = "0.3", default-features = false }
|
alloy-rpc-types-eth = { version = "0.3", default-features = false }
|
||||||
alloy-transport = { 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-simple-request-transport = { path = "../../../networks/ethereum/alloy-simple-request-transport", default-features = false }
|
||||||
@@ -45,3 +45,15 @@ syn = { version = "2", default-features = false, features = ["proc-macro"] }
|
|||||||
syn-solidity = { version = "0.8", default-features = false }
|
syn-solidity = { version = "0.8", default-features = false }
|
||||||
alloy-sol-macro-input = { version = "0.8", default-features = false }
|
alloy-sol-macro-input = { version = "0.8", default-features = false }
|
||||||
alloy-sol-macro-expander = { version = "0.8", default-features = false }
|
alloy-sol-macro-expander = { version = "0.8", default-features = false }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
rand_core = { version = "0.6", default-features = false, features = ["std"] }
|
||||||
|
|
||||||
|
k256 = { version = "0.13", default-features = false, features = ["std"] }
|
||||||
|
|
||||||
|
alloy-rpc-client = { version = "0.3", default-features = false }
|
||||||
|
alloy-node-bindings = { version = "0.3", default-features = false }
|
||||||
|
|
||||||
|
tokio = { version = "1.0", default-features = false, features = ["rt-multi-thread", "macros"] }
|
||||||
|
|
||||||
|
ethereum-test-primitives = { package = "serai-ethereum-test-primitives", path = "../test-primitives" }
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
pragma solidity ^0.8.26;
|
pragma solidity ^0.8.26;
|
||||||
|
|
||||||
|
// TODO: MIT licensed interface
|
||||||
|
|
||||||
import "IERC20.sol";
|
import "IERC20.sol";
|
||||||
|
|
||||||
import "Schnorr.sol";
|
import "Schnorr.sol";
|
||||||
@@ -34,8 +36,11 @@ contract Router {
|
|||||||
*/
|
*/
|
||||||
uint256 private _smartContractNonce;
|
uint256 private _smartContractNonce;
|
||||||
|
|
||||||
/// @dev A nonce incremented upon an action to prevent replays/out-of-order execution
|
/**
|
||||||
uint256 private _nonce;
|
* @dev The nonce to verify the next signature with, incremented upon an action to prevent
|
||||||
|
* replays/out-of-order execution
|
||||||
|
*/
|
||||||
|
uint256 private _nextNonce;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dev The current public key for Serai's Ethereum validator set, in the form the Schnorr library
|
* @dev The current public key for Serai's Ethereum validator set, in the form the Schnorr library
|
||||||
@@ -124,7 +129,7 @@ contract Router {
|
|||||||
/**
|
/**
|
||||||
* @dev Updates the Serai key at the end of the current function. Executing at the end of the
|
* @dev Updates the Serai key at the end of the current function. Executing at the end of the
|
||||||
* current function allows verifying a signature with the current key. This does not update
|
* current function allows verifying a signature with the current key. This does not update
|
||||||
* `_nonce`
|
* `_nextNonce`
|
||||||
*/
|
*/
|
||||||
/// @param nonceUpdatedWith The nonce used to update the key
|
/// @param nonceUpdatedWith The nonce used to update the key
|
||||||
/// @param newSeraiKey The key updated to
|
/// @param newSeraiKey The key updated to
|
||||||
@@ -145,7 +150,7 @@ contract Router {
|
|||||||
_smartContractNonce = 1;
|
_smartContractNonce = 1;
|
||||||
|
|
||||||
// We consumed nonce 0 when setting the initial Serai key
|
// We consumed nonce 0 when setting the initial Serai key
|
||||||
_nonce = 1;
|
_nextNonce = 1;
|
||||||
|
|
||||||
// We haven't escaped to any address yet
|
// We haven't escaped to any address yet
|
||||||
_escapedTo = address(0);
|
_escapedTo = address(0);
|
||||||
@@ -163,18 +168,19 @@ contract Router {
|
|||||||
if (!Schnorr.verify(_seraiKey, message, signature.c, signature.s)) {
|
if (!Schnorr.verify(_seraiKey, message, signature.c, signature.s)) {
|
||||||
revert InvalidSignature();
|
revert InvalidSignature();
|
||||||
}
|
}
|
||||||
// Increment the nonce
|
// Set the next nonce
|
||||||
unchecked {
|
unchecked {
|
||||||
_nonce++;
|
_nextNonce++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Update the key representing Serai's Ethereum validators
|
/// @notice Update the key representing Serai's Ethereum validators
|
||||||
|
/// @dev This assumes the key is correct. No checks on it are performed
|
||||||
/// @param newSeraiKey The key to update to
|
/// @param newSeraiKey The key to update to
|
||||||
/// @param signature The signature by the current key authorizing this update
|
/// @param signature The signature by the current key authorizing this update
|
||||||
function updateSeraiKey(bytes32 newSeraiKey, Signature calldata signature)
|
function updateSeraiKey(bytes32 newSeraiKey, Signature calldata signature)
|
||||||
external
|
external
|
||||||
updateSeraiKeyAtEndOfFn(_nonce, newSeraiKey)
|
updateSeraiKeyAtEndOfFn(_nextNonce, newSeraiKey)
|
||||||
{
|
{
|
||||||
/*
|
/*
|
||||||
This DST needs a length prefix as well to prevent DSTs potentially being substrings of each
|
This DST needs a length prefix as well to prevent DSTs potentially being substrings of each
|
||||||
@@ -188,7 +194,7 @@ contract Router {
|
|||||||
|
|
||||||
This uses encodePacked as all items present here are of fixed length.
|
This uses encodePacked as all items present here are of fixed length.
|
||||||
*/
|
*/
|
||||||
bytes32 message = keccak256(abi.encodePacked("updateSeraiKey", _nonce, newSeraiKey));
|
bytes32 message = keccak256(abi.encodePacked("updateSeraiKey", _nextNonce, newSeraiKey));
|
||||||
verifySignature(message, signature);
|
verifySignature(message, signature);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,11 +372,11 @@ contract Router {
|
|||||||
// Verify the signature
|
// Verify the signature
|
||||||
// This uses `encode`, not `encodePacked`, as `outs` is of variable length
|
// This uses `encode`, not `encodePacked`, as `outs` is of variable length
|
||||||
// TODO: Use a custom encode in verifySignature here with assembly (benchmarking before/after)
|
// TODO: Use a custom encode in verifySignature here with assembly (benchmarking before/after)
|
||||||
bytes32 message = keccak256(abi.encode("execute", _nonce, coin, fee, outs));
|
bytes32 message = keccak256(abi.encode("execute", _nextNonce, coin, fee, outs));
|
||||||
verifySignature(message, signature);
|
verifySignature(message, signature);
|
||||||
|
|
||||||
// _nonce: Also include a bit mask here
|
// TODO: Also include a bit mask here
|
||||||
emit Executed(_nonce, message);
|
emit Executed(_nextNonce, message);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Since we don't have a re-entrancy guard, it is possible for instructions from later batches to
|
Since we don't have a re-entrancy guard, it is possible for instructions from later batches to
|
||||||
@@ -449,7 +455,7 @@ contract Router {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verify the signature
|
// Verify the signature
|
||||||
bytes32 message = keccak256(abi.encodePacked("escapeHatch", _nonce, escapeTo));
|
bytes32 message = keccak256(abi.encodePacked("escapeHatch", _nextNonce, escapeTo));
|
||||||
verifySignature(message, signature);
|
verifySignature(message, signature);
|
||||||
|
|
||||||
_escapedTo = escapeTo;
|
_escapedTo = escapeTo;
|
||||||
@@ -477,8 +483,8 @@ contract Router {
|
|||||||
|
|
||||||
/// @notice Fetch the next nonce to use by an action published to this contract
|
/// @notice Fetch the next nonce to use by an action published to this contract
|
||||||
/// return The next nonce to use by an action published to this contract
|
/// return The next nonce to use by an action published to this contract
|
||||||
function nonce() external view returns (uint256) {
|
function nextNonce() external view returns (uint256) {
|
||||||
return _nonce;
|
return _nextNonce;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Fetch the current key for Serai's Ethereum validator set
|
/// @notice Fetch the current key for Serai's Ethereum validator set
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ use std::{sync::Arc, io, collections::HashSet};
|
|||||||
use group::ff::PrimeField;
|
use group::ff::PrimeField;
|
||||||
|
|
||||||
use alloy_core::primitives::{hex::FromHex, Address, U256, Bytes, TxKind};
|
use alloy_core::primitives::{hex::FromHex, Address, U256, Bytes, TxKind};
|
||||||
use alloy_consensus::TxLegacy;
|
|
||||||
|
|
||||||
use alloy_sol_types::{SolValue, SolConstructor, SolCall, SolEvent};
|
use alloy_sol_types::{SolValue, SolConstructor, SolCall, SolEvent};
|
||||||
|
|
||||||
use alloy_rpc_types_eth::Filter;
|
use alloy_consensus::TxLegacy;
|
||||||
|
|
||||||
|
use alloy_rpc_types_eth::{TransactionRequest, TransactionInput, BlockId, Filter};
|
||||||
use alloy_transport::{TransportErrorKind, RpcError};
|
use alloy_transport::{TransportErrorKind, RpcError};
|
||||||
use alloy_simple_request_transport::SimpleRequest;
|
use alloy_simple_request_transport::SimpleRequest;
|
||||||
use alloy_provider::{Provider, RootProvider};
|
use alloy_provider::{Provider, RootProvider};
|
||||||
@@ -37,6 +37,9 @@ use abi::{
|
|||||||
Executed as ExecutedEvent,
|
Executed as ExecutedEvent,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
|
|
||||||
impl From<&Signature> for abi::Signature {
|
impl From<&Signature> for abi::Signature {
|
||||||
fn from(signature: &Signature) -> Self {
|
fn from(signature: &Signature) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -270,6 +273,15 @@ impl Router {
|
|||||||
bytecode
|
bytecode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Obtain the transaction to deploy this contract.
|
||||||
|
///
|
||||||
|
/// This transaction assumes the `Deployer` has already been deployed.
|
||||||
|
pub fn deployment_tx(initial_serai_key: &PublicKey) -> TxLegacy {
|
||||||
|
let mut tx = Deployer::deploy_tx(Self::init_code(initial_serai_key));
|
||||||
|
tx.gas_limit = 883654 * 120 / 100;
|
||||||
|
tx
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a new view of the Router.
|
/// Create a new view of the Router.
|
||||||
///
|
///
|
||||||
/// This performs an on-chain lookup for the first deployed Router constructed with this public
|
/// This performs an on-chain lookup for the first deployed Router constructed with this public
|
||||||
@@ -303,20 +315,20 @@ impl Router {
|
|||||||
|
|
||||||
/// Construct a transaction to update the key representing Serai.
|
/// Construct a transaction to update the key representing Serai.
|
||||||
pub fn update_serai_key(&self, public_key: &PublicKey, sig: &Signature) -> TxLegacy {
|
pub fn update_serai_key(&self, public_key: &PublicKey, sig: &Signature) -> TxLegacy {
|
||||||
// TODO: Set a more accurate gas
|
|
||||||
TxLegacy {
|
TxLegacy {
|
||||||
to: TxKind::Call(self.1),
|
to: TxKind::Call(self.1),
|
||||||
input: abi::updateSeraiKeyCall::new((public_key.eth_repr().into(), sig.into()))
|
input: abi::updateSeraiKeyCall::new((public_key.eth_repr().into(), sig.into()))
|
||||||
.abi_encode()
|
.abi_encode()
|
||||||
.into(),
|
.into(),
|
||||||
gas_limit: 100_000,
|
gas_limit: 40748 * 120 / 100,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the message to be signed in order to execute a series of `OutInstruction`s.
|
/// Get the message to be signed in order to execute a series of `OutInstruction`s.
|
||||||
pub fn execute_message(nonce: u64, coin: Coin, fee: U256, outs: OutInstructions) -> Vec<u8> {
|
pub fn execute_message(nonce: u64, coin: Coin, fee: U256, outs: OutInstructions) -> Vec<u8> {
|
||||||
("execute", U256::try_from(nonce).unwrap(), coin.address(), fee, outs.0).abi_encode()
|
("execute".to_string(), U256::try_from(nonce).unwrap(), coin.address(), fee, outs.0)
|
||||||
|
.abi_encode_sequence()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Construct a transaction to execute a batch of `OutInstruction`s.
|
/// Construct a transaction to execute a batch of `OutInstruction`s.
|
||||||
@@ -539,4 +551,44 @@ impl Router {
|
|||||||
|
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fetch the current key for Serai's Ethereum validators
|
||||||
|
pub async fn key(&self, block: BlockId) -> Result<PublicKey, RpcError<TransportErrorKind>> {
|
||||||
|
let call = TransactionRequest::default()
|
||||||
|
.to(self.1)
|
||||||
|
.input(TransactionInput::new(abi::seraiKeyCall::new(()).abi_encode().into()));
|
||||||
|
let bytes = self.0.call(&call).block(block).await?;
|
||||||
|
let res = abi::seraiKeyCall::abi_decode_returns(&bytes, true)
|
||||||
|
.map_err(|e| TransportErrorKind::Custom(format!("filtered to decode key: {e:?}").into()))?;
|
||||||
|
Ok(
|
||||||
|
PublicKey::from_eth_repr(res._0.into()).ok_or_else(|| {
|
||||||
|
TransportErrorKind::Custom("invalid key set on router".to_string().into())
|
||||||
|
})?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetch the nonce of the next action to execute
|
||||||
|
pub async fn next_nonce(&self, block: BlockId) -> Result<u64, RpcError<TransportErrorKind>> {
|
||||||
|
let call = TransactionRequest::default()
|
||||||
|
.to(self.1)
|
||||||
|
.input(TransactionInput::new(abi::nextNonceCall::new(()).abi_encode().into()));
|
||||||
|
let bytes = self.0.call(&call).block(block).await?;
|
||||||
|
let res = abi::nextNonceCall::abi_decode_returns(&bytes, true)
|
||||||
|
.map_err(|e| TransportErrorKind::Custom(format!("filtered to decode nonce: {e:?}").into()))?;
|
||||||
|
Ok(u64::try_from(res._0).map_err(|_| {
|
||||||
|
TransportErrorKind::Custom("nonce returned exceeded 2**64".to_string().into())
|
||||||
|
})?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetch the address the escape hatch was set to
|
||||||
|
pub async fn escaped_to(&self, block: BlockId) -> Result<Address, RpcError<TransportErrorKind>> {
|
||||||
|
let call = TransactionRequest::default()
|
||||||
|
.to(self.1)
|
||||||
|
.input(TransactionInput::new(abi::escapedToCall::new(()).abi_encode().into()));
|
||||||
|
let bytes = self.0.call(&call).block(block).await?;
|
||||||
|
let res = abi::escapedToCall::abi_decode_returns(&bytes, true).map_err(|e| {
|
||||||
|
TransportErrorKind::Custom(format!("filtered to decode the address escaped to: {e:?}").into())
|
||||||
|
})?;
|
||||||
|
Ok(res._0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
210
processor/ethereum/router/src/tests/mod.rs
Normal file
210
processor/ethereum/router/src/tests/mod.rs
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
use std::{sync::Arc, collections::HashSet};
|
||||||
|
|
||||||
|
use rand_core::{RngCore, OsRng};
|
||||||
|
|
||||||
|
use group::ff::Field;
|
||||||
|
use k256::{Scalar, ProjectivePoint};
|
||||||
|
|
||||||
|
use alloy_core::primitives::{Address, U256, TxKind};
|
||||||
|
use alloy_sol_types::SolCall;
|
||||||
|
|
||||||
|
use alloy_consensus::TxLegacy;
|
||||||
|
|
||||||
|
use alloy_rpc_types_eth::BlockNumberOrTag;
|
||||||
|
use alloy_simple_request_transport::SimpleRequest;
|
||||||
|
use alloy_rpc_client::ClientBuilder;
|
||||||
|
use alloy_provider::RootProvider;
|
||||||
|
|
||||||
|
use alloy_node_bindings::{Anvil, AnvilInstance};
|
||||||
|
|
||||||
|
use ethereum_schnorr::{PublicKey, Signature};
|
||||||
|
use ethereum_deployer::Deployer;
|
||||||
|
|
||||||
|
use crate::{Coin, OutInstructions, Router};
|
||||||
|
|
||||||
|
pub(crate) fn test_key() -> (Scalar, PublicKey) {
|
||||||
|
loop {
|
||||||
|
let key = Scalar::random(&mut OsRng);
|
||||||
|
let point = ProjectivePoint::GENERATOR * key;
|
||||||
|
if let Some(public_key) = PublicKey::new(point) {
|
||||||
|
return (key, public_key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn setup_test(
|
||||||
|
) -> (AnvilInstance, Arc<RootProvider<SimpleRequest>>, Router, (Scalar, PublicKey)) {
|
||||||
|
let anvil = Anvil::new().spawn();
|
||||||
|
|
||||||
|
let provider = Arc::new(RootProvider::new(
|
||||||
|
ClientBuilder::default().transport(SimpleRequest::new(anvil.endpoint()), true),
|
||||||
|
));
|
||||||
|
|
||||||
|
let (private_key, public_key) = test_key();
|
||||||
|
assert!(Router::new(provider.clone(), &public_key).await.unwrap().is_none());
|
||||||
|
|
||||||
|
// Deploy the Deployer
|
||||||
|
let receipt = ethereum_test_primitives::publish_tx(&provider, Deployer::deployment_tx()).await;
|
||||||
|
assert!(receipt.status());
|
||||||
|
|
||||||
|
// Get the TX to deploy the Router
|
||||||
|
let mut tx = Router::deployment_tx(&public_key);
|
||||||
|
// Set a gas price (100 gwei)
|
||||||
|
tx.gas_price = 100_000_000_000u128;
|
||||||
|
// Sign it
|
||||||
|
let tx = ethereum_primitives::deterministically_sign(&tx);
|
||||||
|
// Publish it
|
||||||
|
let receipt = ethereum_test_primitives::publish_tx(&provider, tx).await;
|
||||||
|
assert!(receipt.status());
|
||||||
|
println!("Router deployment used {} gas:", receipt.gas_used);
|
||||||
|
|
||||||
|
let router = Router::new(provider.clone(), &public_key).await.unwrap().unwrap();
|
||||||
|
|
||||||
|
(anvil, provider, router, (private_key, public_key))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_constructor() {
|
||||||
|
let (_anvil, _provider, router, key) = setup_test().await;
|
||||||
|
assert_eq!(router.key(BlockNumberOrTag::Latest.into()).await.unwrap(), key.1);
|
||||||
|
assert_eq!(router.next_nonce(BlockNumberOrTag::Latest.into()).await.unwrap(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
router.escaped_to(BlockNumberOrTag::Latest.into()).await.unwrap(),
|
||||||
|
Address::from([0; 20])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_update_serai_key() {
|
||||||
|
let (_anvil, provider, router, key) = setup_test().await;
|
||||||
|
|
||||||
|
let update_to = test_key().1;
|
||||||
|
let msg = Router::update_serai_key_message(1, &update_to);
|
||||||
|
|
||||||
|
let nonce = Scalar::random(&mut OsRng);
|
||||||
|
let c = Signature::challenge(ProjectivePoint::GENERATOR * nonce, &key.1, &msg);
|
||||||
|
let s = nonce + (c * key.0);
|
||||||
|
|
||||||
|
let sig = Signature::new(c, s).unwrap();
|
||||||
|
|
||||||
|
let mut tx = router.update_serai_key(&update_to, &sig);
|
||||||
|
tx.gas_price = 100_000_000_000u128;
|
||||||
|
let tx = ethereum_primitives::deterministically_sign(&tx);
|
||||||
|
let receipt = ethereum_test_primitives::publish_tx(&provider, tx).await;
|
||||||
|
assert!(receipt.status());
|
||||||
|
println!("update_serai_key used {} gas:", receipt.gas_used);
|
||||||
|
|
||||||
|
assert_eq!(router.key(receipt.block_hash.unwrap().into()).await.unwrap(), update_to);
|
||||||
|
assert_eq!(router.next_nonce(receipt.block_hash.unwrap().into()).await.unwrap(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_eth_in_instruction() {
|
||||||
|
let (_anvil, provider, router, _key) = setup_test().await;
|
||||||
|
|
||||||
|
let amount = U256::try_from(OsRng.next_u64()).unwrap();
|
||||||
|
let mut in_instruction = vec![0; usize::try_from(OsRng.next_u64() % 256).unwrap()];
|
||||||
|
OsRng.fill_bytes(&mut in_instruction);
|
||||||
|
|
||||||
|
let tx = TxLegacy {
|
||||||
|
chain_id: None,
|
||||||
|
nonce: 0,
|
||||||
|
// 100 gwei
|
||||||
|
gas_price: 100_000_000_000u128,
|
||||||
|
gas_limit: 1_000_000u128,
|
||||||
|
to: TxKind::Call(router.address()),
|
||||||
|
value: amount,
|
||||||
|
input: crate::abi::inInstructionCall::new((
|
||||||
|
[0; 20].into(),
|
||||||
|
amount,
|
||||||
|
in_instruction.clone().into(),
|
||||||
|
))
|
||||||
|
.abi_encode()
|
||||||
|
.into(),
|
||||||
|
};
|
||||||
|
let tx = ethereum_primitives::deterministically_sign(&tx);
|
||||||
|
let signer = tx.recover_signer().unwrap();
|
||||||
|
|
||||||
|
let receipt = ethereum_test_primitives::publish_tx(&provider, tx).await;
|
||||||
|
assert!(receipt.status());
|
||||||
|
|
||||||
|
assert_eq!(receipt.inner.logs().len(), 1);
|
||||||
|
let parsed_log =
|
||||||
|
receipt.inner.logs()[0].log_decode::<crate::InInstructionEvent>().unwrap().inner.data;
|
||||||
|
assert_eq!(parsed_log.from, signer);
|
||||||
|
assert_eq!(parsed_log.coin, Address::from([0; 20]));
|
||||||
|
assert_eq!(parsed_log.amount, amount);
|
||||||
|
assert_eq!(parsed_log.instruction.as_ref(), &in_instruction);
|
||||||
|
|
||||||
|
let parsed_in_instructions =
|
||||||
|
router.in_instructions(receipt.block_number.unwrap(), &HashSet::new()).await.unwrap();
|
||||||
|
assert_eq!(parsed_in_instructions.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
parsed_in_instructions[0].id,
|
||||||
|
(<[u8; 32]>::from(receipt.block_hash.unwrap()), receipt.inner.logs()[0].log_index.unwrap())
|
||||||
|
);
|
||||||
|
assert_eq!(parsed_in_instructions[0].from, signer);
|
||||||
|
assert_eq!(parsed_in_instructions[0].coin, Coin::Ether);
|
||||||
|
assert_eq!(parsed_in_instructions[0].amount, amount);
|
||||||
|
assert_eq!(parsed_in_instructions[0].data, in_instruction);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_erc20_in_instruction() {
|
||||||
|
todo!("TODO")
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn publish_outs(key: (Scalar, PublicKey), nonce: u64, coin: Coin, fee: U256, outs: OutInstructions) -> TransactionReceipt {
|
||||||
|
let msg = Router::execute_message(nonce, coin, fee, instructions.clone());
|
||||||
|
|
||||||
|
let nonce = Scalar::random(&mut OsRng);
|
||||||
|
let c = Signature::challenge(ProjectivePoint::GENERATOR * nonce, &key.1, &msg);
|
||||||
|
let s = nonce + (c * key.0);
|
||||||
|
|
||||||
|
let sig = Signature::new(c, s).unwrap();
|
||||||
|
|
||||||
|
let mut tx = router.execute(coin, fee, instructions, &sig);
|
||||||
|
tx.gas_price = 100_000_000_000u128;
|
||||||
|
let tx = ethereum_primitives::deterministically_sign(&tx);
|
||||||
|
ethereum_test_primitives::publish_tx(&provider, tx).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_eth_address_out_instruction() {
|
||||||
|
let (_anvil, provider, router, key) = setup_test().await;
|
||||||
|
|
||||||
|
let mut amount = U256::try_from(OsRng.next_u64()).unwrap();
|
||||||
|
let mut fee = U256::try_from(OsRng.next_u64()).unwrap();
|
||||||
|
if fee > amount {
|
||||||
|
core::mem::swap(&mut amount, &mut fee);
|
||||||
|
}
|
||||||
|
assert!(amount >= fee);
|
||||||
|
ethereum_test_primitives::fund_account(&provider, router.address(), amount).await;
|
||||||
|
|
||||||
|
let instructions = OutInstructions::from([].as_slice());
|
||||||
|
let receipt = publish_outs(key, 1, Coin::Ether, fee, instructions);
|
||||||
|
assert!(receipt.status());
|
||||||
|
println!("empty execute used {} gas:", receipt.gas_used);
|
||||||
|
|
||||||
|
assert_eq!(router.next_nonce(receipt.block_hash.unwrap().into()).await.unwrap(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_erc20_address_out_instruction() {
|
||||||
|
todo!("TODO")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_eth_code_out_instruction() {
|
||||||
|
todo!("TODO")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_erc20_code_out_instruction() {
|
||||||
|
todo!("TODO")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_escape_hatch() {
|
||||||
|
todo!("TODO")
|
||||||
|
}
|
||||||
28
processor/ethereum/test-primitives/Cargo.toml
Normal file
28
processor/ethereum/test-primitives/Cargo.toml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
[package]
|
||||||
|
name = "serai-ethereum-test-primitives"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Test primitives for Ethereum"
|
||||||
|
license = "AGPL-3.0-only"
|
||||||
|
repository = "https://github.com/serai-dex/serai/tree/develop/processor/ethereum/test-primitives"
|
||||||
|
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
all-features = true
|
||||||
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
k256 = { version = "0.13", default-features = false, features = ["std"] }
|
||||||
|
|
||||||
|
alloy-core = { version = "0.8", default-features = false }
|
||||||
|
alloy-consensus = { version = "0.3", default-features = false, features = ["std"] }
|
||||||
|
|
||||||
|
alloy-rpc-types-eth = { 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 }
|
||||||
15
processor/ethereum/test-primitives/LICENSE
Normal file
15
processor/ethereum/test-primitives/LICENSE
Normal 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/>.
|
||||||
5
processor/ethereum/test-primitives/README.md
Normal file
5
processor/ethereum/test-primitives/README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Ethereum Router
|
||||||
|
|
||||||
|
The [Router contract](./contracts/Router.sol) is extensively documented to ensure clarity and
|
||||||
|
understanding of the design decisions made. Please refer to it for understanding of why/what this
|
||||||
|
is.
|
||||||
117
processor/ethereum/test-primitives/src/lib.rs
Normal file
117
processor/ethereum/test-primitives/src/lib.rs
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||||
|
#![doc = include_str!("../README.md")]
|
||||||
|
#![deny(missing_docs)]
|
||||||
|
|
||||||
|
use k256::{elliptic_curve::sec1::ToEncodedPoint, ProjectivePoint};
|
||||||
|
|
||||||
|
use alloy_core::{
|
||||||
|
primitives::{Address, U256, Bytes, Signature, TxKind},
|
||||||
|
hex::FromHex,
|
||||||
|
};
|
||||||
|
use alloy_consensus::{SignableTransaction, TxLegacy, Signed};
|
||||||
|
|
||||||
|
use alloy_rpc_types_eth::TransactionReceipt;
|
||||||
|
use alloy_simple_request_transport::SimpleRequest;
|
||||||
|
use alloy_provider::{Provider, RootProvider};
|
||||||
|
|
||||||
|
use ethereum_primitives::{keccak256, deterministically_sign};
|
||||||
|
|
||||||
|
fn address(point: &ProjectivePoint) -> [u8; 20] {
|
||||||
|
let encoded_point = point.to_encoded_point(false);
|
||||||
|
// Last 20 bytes of the hash of the concatenated x and y coordinates
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fund an account.
|
||||||
|
pub async fn fund_account(provider: &RootProvider<SimpleRequest>, address: Address, value: U256) {
|
||||||
|
let _: () = provider
|
||||||
|
.raw_request("anvil_setBalance".into(), [address.to_string(), value.to_string()])
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Publish an already-signed transaction.
|
||||||
|
pub async fn publish_tx(
|
||||||
|
provider: &RootProvider<SimpleRequest>,
|
||||||
|
tx: Signed<TxLegacy>,
|
||||||
|
) -> TransactionReceipt {
|
||||||
|
// Fund the sender's address
|
||||||
|
fund_account(
|
||||||
|
provider,
|
||||||
|
tx.recover_signer().unwrap(),
|
||||||
|
(U256::from(tx.tx().gas_limit) * U256::from(tx.tx().gas_price)) + tx.tx().value,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let (tx, sig, _) = tx.into_parts();
|
||||||
|
let mut bytes = vec![];
|
||||||
|
tx.encode_with_signature_fields(&sig, &mut bytes);
|
||||||
|
let pending_tx = provider.send_raw_transaction(&bytes).await.unwrap();
|
||||||
|
pending_tx.get_receipt().await.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deploy a contract.
|
||||||
|
///
|
||||||
|
/// The contract deployment will be done by a random account.
|
||||||
|
pub async fn deploy_contract(
|
||||||
|
provider: &RootProvider<SimpleRequest>,
|
||||||
|
file_path: &str,
|
||||||
|
constructor_arguments: &[u8],
|
||||||
|
) -> Address {
|
||||||
|
let hex_bin_buf = std::fs::read_to_string(file_path).unwrap();
|
||||||
|
let hex_bin =
|
||||||
|
if let Some(stripped) = hex_bin_buf.strip_prefix("0x") { stripped } else { &hex_bin_buf };
|
||||||
|
let mut bin = Vec::<u8>::from(Bytes::from_hex(hex_bin).unwrap());
|
||||||
|
bin.extend(constructor_arguments);
|
||||||
|
|
||||||
|
let deployment_tx = TxLegacy {
|
||||||
|
chain_id: None,
|
||||||
|
nonce: 0,
|
||||||
|
// 100 gwei
|
||||||
|
gas_price: 100_000_000_000u128,
|
||||||
|
gas_limit: 1_000_000,
|
||||||
|
to: TxKind::Create,
|
||||||
|
value: U256::ZERO,
|
||||||
|
input: bin.into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let deployment_tx = deterministically_sign(&deployment_tx);
|
||||||
|
|
||||||
|
let receipt = publish_tx(provider, deployment_tx).await;
|
||||||
|
assert!(receipt.status());
|
||||||
|
|
||||||
|
receipt.contract_address.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sign and send a transaction from the specified wallet.
|
||||||
|
///
|
||||||
|
/// This assumes the wallet is funded.
|
||||||
|
pub async fn send(
|
||||||
|
provider: &RootProvider<SimpleRequest>,
|
||||||
|
wallet: &k256::ecdsa::SigningKey,
|
||||||
|
mut tx: TxLegacy,
|
||||||
|
) -> TransactionReceipt {
|
||||||
|
let verifying_key = *wallet.verifying_key().as_affine();
|
||||||
|
let address = Address::from(address(&verifying_key.into()));
|
||||||
|
|
||||||
|
// https://github.com/alloy-rs/alloy/issues/539
|
||||||
|
// let chain_id = provider.get_chain_id().await.unwrap();
|
||||||
|
// tx.chain_id = Some(chain_id);
|
||||||
|
tx.chain_id = None;
|
||||||
|
tx.nonce = provider.get_transaction_count(address).await.unwrap();
|
||||||
|
// 100 gwei
|
||||||
|
tx.gas_price = 100_000_000_000u128;
|
||||||
|
|
||||||
|
let sig = wallet.sign_prehash_recoverable(tx.signature_hash().as_ref()).unwrap();
|
||||||
|
assert_eq!(address, tx.clone().into_signed(sig.into()).recover_signer().unwrap());
|
||||||
|
assert!(
|
||||||
|
provider.get_balance(address).await.unwrap() >
|
||||||
|
((U256::from(tx.gas_price) * U256::from(tx.gas_limit)) + tx.value)
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut bytes = vec![];
|
||||||
|
tx.encode_with_signature_fields(&Signature::from(sig), &mut bytes);
|
||||||
|
let pending_tx = provider.send_raw_transaction(&bytes).await.unwrap();
|
||||||
|
pending_tx.get_receipt().await.unwrap()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user