Add Serai key confirmation to prevent rotating to an unusable key

Also updates alloy to the latest version
This commit is contained in:
Luke Parker 2024-12-08 20:42:37 -05:00
parent 8013c56195
commit 3192370484
No known key found for this signature in database
18 changed files with 679 additions and 326 deletions

618
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -6,7 +6,7 @@ license = "MIT"
repository = "https://github.com/serai-dex/serai/tree/develop/networks/ethereum/alloy-simple-request-transport"
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
edition = "2021"
rust-version = "1.78"
rust-version = "1.81"
[package.metadata.docs.rs]
all-features = true
@ -16,13 +16,13 @@ rustdoc-args = ["--cfg", "docsrs"]
workspace = true
[dependencies]
tower = "0.4"
tower = "0.5"
serde_json = { version = "1", default-features = false }
simple-request = { path = "../../../common/request", version = "0.1", default-features = false }
alloy-json-rpc = { version = "0.3", default-features = false }
alloy-transport = { version = "0.3", default-features = false }
alloy-json-rpc = { version = "0.7", default-features = false }
alloy-transport = { version = "0.7", default-features = false }
[features]
default = ["tls"]

View file

@ -33,10 +33,10 @@ alloy-core = { version = "0.8", default-features = false }
alloy-sol-types = { version = "0.8", default-features = false }
alloy-simple-request-transport = { path = "../../../networks/ethereum/alloy-simple-request-transport", default-features = false }
alloy-rpc-types-eth = { version = "0.3", default-features = false }
alloy-rpc-client = { version = "0.3", default-features = false }
alloy-provider = { version = "0.3", default-features = false }
alloy-rpc-types-eth = { version = "0.7", default-features = false }
alloy-rpc-client = { version = "0.7", default-features = false }
alloy-provider = { version = "0.7", default-features = false }
alloy-node-bindings = { version = "0.3", default-features = false }
alloy-node-bindings = { version = "0.7", default-features = false }
tokio = { version = "1", default-features = false, features = ["macros"] }

View file

@ -8,6 +8,7 @@ authors = ["Luke Parker <lukeparker5132@gmail.com>"]
keywords = []
edition = "2021"
publish = false
rust-version = "1.81"
[package.metadata.docs.rs]
all-features = true
@ -33,11 +34,11 @@ k256 = { version = "^0.13.1", default-features = false, features = ["std"] }
alloy-core = { version = "0.8", default-features = false }
alloy-rlp = { 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-rpc-types-eth = { version = "0.7", default-features = false }
alloy-transport = { version = "0.7", default-features = false }
alloy-simple-request-transport = { path = "../../networks/ethereum/alloy-simple-request-transport", default-features = false }
alloy-rpc-client = { version = "0.3", default-features = false }
alloy-provider = { version = "0.3", default-features = false }
alloy-rpc-client = { version = "0.7", default-features = false }
alloy-provider = { version = "0.7", default-features = false }
serai-client = { path = "../../substrate/client", default-features = false, features = ["ethereum"] }

View file

@ -7,7 +7,7 @@ repository = "https://github.com/serai-dex/serai/tree/develop/processor/ethereum
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
edition = "2021"
publish = false
rust-version = "1.79"
rust-version = "1.81"
[package.metadata.docs.rs]
all-features = true
@ -18,15 +18,16 @@ 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-consensus = { version = "0.7", default-features = false }
alloy-rpc-types-eth = { version = "0.7", default-features = false }
alloy-transport = { version = "0.7", 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 }
alloy-provider = { version = "0.7", default-features = false }
ethereum-primitives = { package = "serai-processor-ethereum-primitives", path = "../primitives", default-features = false }

View file

@ -49,7 +49,7 @@ impl Deployer {
// 100 gwei
gas_price: 100_000_000_000u128,
// TODO: Use a more accurate gas limit
gas_limit: 1_000_000u128,
gas_limit: 1_000_000u64,
to: TxKind::Create,
value: U256::ZERO,
input: bytecode,

View file

@ -7,7 +7,7 @@ repository = "https://github.com/serai-dex/serai/tree/develop/processor/ethereum
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
edition = "2021"
publish = false
rust-version = "1.79"
rust-version = "1.81"
[package.metadata.docs.rs]
all-features = true
@ -22,9 +22,9 @@ alloy-core = { version = "0.8", 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-rpc-types-eth = { version = "0.7", default-features = false }
alloy-transport = { version = "0.7", 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 }
alloy-provider = { version = "0.7", default-features = false }
tokio = { version = "1", default-features = false, features = ["rt"] }

View file

@ -8,7 +8,7 @@ use alloy_core::primitives::{Address, B256, U256};
use alloy_sol_types::{SolInterface, SolEvent};
use alloy_rpc_types_eth::Filter;
use alloy_rpc_types_eth::{Filter, TransactionTrait};
use alloy_transport::{TransportErrorKind, RpcError};
use alloy_simple_request_transport::SimpleRequest;
use alloy_provider::{Provider, RootProvider};
@ -66,7 +66,7 @@ impl Erc20 {
// If this is a top-level call...
// Don't validate the encoding as this can't be re-encoded to an identical bytestring due
// to the `InInstruction` appended after the call itself
if let Ok(call) = IERC20Calls::abi_decode(&transaction.input, false) {
if let Ok(call) = IERC20Calls::abi_decode(transaction.inner.input(), false) {
// Extract the top-level call's from/to/value
let (from, call_to, value) = match call {
IERC20Calls::transfer(transferCall { to, value }) => (transaction.from, to, value),
@ -92,7 +92,7 @@ impl Erc20 {
// Find the log for this transfer
for log in receipt.inner.logs() {
// If this log was emitted by a different contract, continue
if Some(log.address()) != transaction.to {
if Some(log.address()) != transaction.inner.to() {
continue;
}
@ -122,7 +122,7 @@ impl Erc20 {
// Read the data appended after
let encoded = call.abi_encode();
let data = transaction.input.as_ref()[encoded.len() ..].to_vec();
let data = transaction.inner.input().as_ref()[encoded.len() ..].to_vec();
return Ok(Some(TopLevelTransfer {
id: (*transaction_id, log_index),

View file

@ -7,7 +7,7 @@ repository = "https://github.com/serai-dex/serai/tree/develop/processor/ethereum
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
edition = "2021"
publish = false
rust-version = "1.79"
rust-version = "1.81"
[package.metadata.docs.rs]
all-features = true
@ -21,4 +21,4 @@ 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"] }
alloy-consensus = { version = "0.7", default-features = false, features = ["k256"] }

View file

@ -5,7 +5,7 @@
use group::ff::PrimeField;
use k256::Scalar;
use alloy_core::primitives::{Parity, Signature};
use alloy_core::primitives::PrimitiveSignature;
use alloy_consensus::{SignableTransaction, Signed, TxLegacy};
/// The Keccak256 hash function.
@ -34,8 +34,8 @@ pub fn deterministically_sign(tx: &TxLegacy) -> Signed<TxLegacy> {
// 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();
let signature =
PrimitiveSignature::from_scalars_and_parity(r_bytes.into(), s_bytes.into(), false);
// Check if this is a valid signature
let tx = tx.clone().into_signed(signature);

View file

@ -7,7 +7,7 @@ repository = "https://github.com/serai-dex/serai/tree/develop/processor/ethereum
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
edition = "2021"
publish = false
rust-version = "1.79"
rust-version = "1.81"
[package.metadata.docs.rs]
all-features = true
@ -24,12 +24,12 @@ alloy-core = { version = "0.8", default-features = false }
alloy-sol-types = { version = "0.8", default-features = false }
alloy-sol-macro = { version = "0.8", default-features = false }
alloy-consensus = { version = "0.3", default-features = false }
alloy-consensus = { version = "0.7", default-features = false }
alloy-rpc-types-eth = { version = "0.3", default-features = false }
alloy-transport = { version = "0.3", default-features = false }
alloy-rpc-types-eth = { version = "0.7", default-features = false }
alloy-transport = { version = "0.7", 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 }
alloy-provider = { version = "0.7", default-features = false }
ethereum-schnorr = { package = "ethereum-schnorr-contract", path = "../../../networks/ethereum/schnorr", default-features = false }
@ -53,8 +53,8 @@ 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 }
alloy-rpc-client = { version = "0.7", default-features = false }
alloy-node-bindings = { version = "0.7", default-features = false }
tokio = { version = "1.0", default-features = false, features = ["rt-multi-thread", "macros"] }

View file

@ -26,6 +26,13 @@ fn main() {
fs::create_dir(&artifacts_path).unwrap();
}
build_solidity_contracts::build(
&["../../../networks/ethereum/schnorr/contracts", "../erc20/contracts", "contracts"],
"contracts",
&artifacts_path,
)
.unwrap();
// This cannot be handled with the sol! macro. The Router requires an import
// https://github.com/alloy-rs/core/issues/602
sol(

View file

@ -5,6 +5,11 @@ pragma solidity ^0.8.26;
/// @author Luke Parker <lukeparker@serai.exchange>
/// @notice Intakes coins for the Serai network and handles relaying batches of transfers out
interface IRouterWithoutCollisions {
/// @notice Emitted when the next key for Serai's Ethereum validators is set
/// @param nonce The nonce consumed to update this key
/// @param key The key updated to
event NextSeraiKeySet(uint256 indexed nonce, bytes32 indexed key);
/// @notice Emitted when the key for Serai's Ethereum validators is updated
/// @param nonce The nonce consumed to update this key
/// @param key The key updated to
@ -39,6 +44,9 @@ interface IRouterWithoutCollisions {
/// @param coin The coin which escaped
event Escaped(address indexed coin);
/// @notice The key for Serai was invalid
/// @dev This is incomplete and not always guaranteed to be thrown upon an invalid key
error InvalidSeraiKey();
/// @notice The contract has had its escape hatch invoked and won't accept further actions
error EscapeHatchInvoked();
/// @notice The signature was invalid
@ -86,8 +94,15 @@ interface IRouterWithoutCollisions {
/// return The next nonce to use by an action published to this contract
function nextNonce() external view returns (uint256);
/// @notice Fetch the next key for Serai's Ethereum validator set
/// @return The next key for Serai's Ethereum validator set or bytes32(0) if none is currently set
function nextSeraiKey() external view returns (bytes32);
/// @notice Fetch the current key for Serai's Ethereum validator set
/// @return The current key for Serai's Ethereum validator set
/**
* @return The current key for Serai's Ethereum validator set or bytes32(0) if none is currently
* set
*/
function seraiKey() external view returns (bytes32);
/// @notice Fetch the address escaped to
@ -134,15 +149,25 @@ interface IRouter is IRouterWithoutCollisions {
}
/// @notice Update the key representing Serai's Ethereum validators
/// @dev This assumes the key is correct. No checks on it are performed
/**
* @dev This does not validate the passed-in key as much as possible. This is accepted as the key
* won't actually be rotated to until it provides a signature confirming the update however
* (proving signatures can be made by the key in question and verified via our Schnorr
* contract).
*/
// @param signature The signature by the current key authorizing this update
/// @param signature The signature by the current key authorizing this update
/// @param newSeraiKey The key to update to
function updateSeraiKey(Signature calldata signature, bytes32 newSeraiKey) external;
/// @param nextSeraiKeyVar The key to update to, once it confirms the update
function updateSeraiKey(Signature calldata signature, bytes32 nextSeraiKeyVar) external;
/// @notice Confirm the next key representing Serai's Ethereum validators, updating to it
/// @param signature The signature by the next key confirming its validity
function confirmNextSeraiKey(Signature calldata signature) external;
/// @notice Execute a batch of `OutInstruction`s
/**
* @dev All `OutInstruction`s in a batch are only for a single coin to simplify handling of the
* fee
* fee
*/
/// @param signature The signature by the current key for Serai's Ethereum validators
/// @param coin The coin all of these `OutInstruction`s are for

View file

@ -50,6 +50,12 @@ contract Router is IRouterWithoutCollisions {
*/
uint256 private _nextNonce;
/**
* @dev The next public key for Serai's Ethereum validator set, in the form the Schnorr library
* expects
*/
bytes32 private _nextSeraiKey;
/**
* @dev The current public key for Serai's Ethereum validator set, in the form the Schnorr library
* expects
@ -59,12 +65,16 @@ contract Router is IRouterWithoutCollisions {
/// @dev The address escaped to
address private _escapedTo;
/// @dev Updates the Serai key. This does not update `_nextNonce`
/// @param nonceUpdatedWith The nonce used to update the key
/// @param newSeraiKey The key updated to
function _updateSeraiKey(uint256 nonceUpdatedWith, bytes32 newSeraiKey) private {
_seraiKey = newSeraiKey;
emit SeraiKeyUpdated(nonceUpdatedWith, newSeraiKey);
/// @dev Set the next Serai key. This does not read from/write to `_nextNonce`
/// @param nonceUpdatedWith The nonce used to set the next key
/// @param nextSeraiKeyVar The key to set as next
function _setNextSeraiKey(uint256 nonceUpdatedWith, bytes32 nextSeraiKeyVar) private {
// Explicitly disallow 0 so we can always consider 0 as None and non-zero as Some
if (nextSeraiKeyVar == bytes32(0)) {
revert InvalidSeraiKey();
}
_nextSeraiKey = nextSeraiKeyVar;
emit NextSeraiKeySet(nonceUpdatedWith, nextSeraiKeyVar);
}
/// @notice The constructor for the relayer
@ -74,8 +84,10 @@ contract Router is IRouterWithoutCollisions {
// This is incompatible with any networks which don't have their nonces start at 0
_smartContractNonce = 1;
// Set the Serai key
_updateSeraiKey(0, initialSeraiKey);
// Set the next Serai key
_setNextSeraiKey(0, initialSeraiKey);
// Set the current Serai key to None
_seraiKey = bytes32(0);
// We just consumed nonce 0 when setting the initial Serai key
_nextNonce = 1;
@ -90,7 +102,7 @@ contract Router is IRouterWithoutCollisions {
* calldata should be signed with the nonce taking the place of the signature's commitment to
* its nonce, and the signature solution zeroed.
*/
function verifySignature()
function verifySignature(bytes32 key)
private
returns (uint256 nonceUsed, bytes memory message, bytes32 messageHash)
{
@ -99,6 +111,15 @@ contract Router is IRouterWithoutCollisions {
revert EscapeHatchInvoked();
}
/*
If this key isn't set, reject it.
The Schnorr contract should already reject this public key yet it's best to be explicit.
*/
if (key == bytes32(0)) {
revert InvalidSignature();
}
message = msg.data;
uint256 messageLen = message.length;
/*
@ -134,7 +155,7 @@ contract Router is IRouterWithoutCollisions {
}
// Verify the signature
if (!Schnorr.verify(_seraiKey, messageHash, signatureC, signatureS)) {
if (!Schnorr.verify(key, messageHash, signatureC, signatureS)) {
revert InvalidSignature();
}
@ -178,22 +199,38 @@ contract Router is IRouterWithoutCollisions {
}
}
/// @notice Update the key representing Serai's Ethereum validators
/// @notice Start updating the key representing Serai's Ethereum validators
/**
* @dev This assumes the key is correct. No checks on it are performed.
* @dev This does not validate the passed-in key as much as possible. This is accepted as the key
* won't actually be rotated to until it provides a signature confirming the update however
* (proving signatures can be made by the key in question and verified via our Schnorr
* contract).
*
* The hex bytes are to cause a collision with `IRouter.updateSeraiKey`.
*/
// @param signature The signature by the current key authorizing this update
// @param newSeraiKey The key to update to
// @param nextSeraiKey The key to update to
function updateSeraiKey5A8542A2() external {
(uint256 nonceUsed, bytes memory args,) = verifySignature();
(uint256 nonceUsed, bytes memory args,) = verifySignature(_seraiKey);
/*
We could replace this with a length check (if we don't simply assume the calldata is valid as
it was properly signed) + mload to save 24 gas but it's not worth the complexity.
*/
(,, bytes32 newSeraiKey) = abi.decode(args, (bytes32, bytes32, bytes32));
_updateSeraiKey(nonceUsed, newSeraiKey);
(,, bytes32 nextSeraiKeyVar) = abi.decode(args, (bytes32, bytes32, bytes32));
_setNextSeraiKey(nonceUsed, nextSeraiKeyVar);
}
/// @notice Confirm the next key representing Serai's Ethereum validators, updating to it
/// @dev The hex bytes are to cause a collision with `IRouter.confirmSeraiKey`.
// @param signature The signature by the next key confirming its validity
function confirmNextSeraiKey34AC53AC() external {
// Checks
bytes32 nextSeraiKeyVar = _nextSeraiKey;
(uint256 nonceUsed,,) = verifySignature(nextSeraiKeyVar);
// Effects
_nextSeraiKey = bytes32(0);
_seraiKey = nextSeraiKeyVar;
emit SeraiKeyUpdated(nonceUsed, nextSeraiKeyVar);
}
/// @notice Transfer coins into Serai with an instruction
@ -384,7 +421,7 @@ contract Router is IRouterWithoutCollisions {
revert ReenteredExecute();
}
(uint256 nonceUsed, bytes memory args, bytes32 message) = verifySignature();
(uint256 nonceUsed, bytes memory args, bytes32 message) = verifySignature(_seraiKey);
(,, address coin, uint256 fee, IRouter.OutInstruction[] memory outs) =
abi.decode(args, (bytes32, bytes32, address, uint256, IRouter.OutInstruction[]));
@ -481,7 +518,7 @@ contract Router is IRouterWithoutCollisions {
// @param escapeTo The address to escape to
function escapeHatchDCDD91CC() external {
// Verify the signature
(, bytes memory args,) = verifySignature();
(, bytes memory args,) = verifySignature(_seraiKey);
(,, address escapeTo) = abi.decode(args, (bytes32, bytes32, address));
@ -526,8 +563,17 @@ contract Router is IRouterWithoutCollisions {
return _nextNonce;
}
/// @notice Fetch the next key for Serai's Ethereum validator set
/// @return The next key for Serai's Ethereum validator set or bytes32(0) if none is currently set
function nextSeraiKey() external view returns (bytes32) {
return _nextSeraiKey;
}
/// @notice Fetch the current key for Serai's Ethereum validator set
/// @return The current key for Serai's Ethereum validator set
/**
* @return The current key for Serai's Ethereum validator set or bytes32(0) if none is currently
* set
*/
function seraiKey() external view returns (bytes32) {
return _seraiKey;
}

View file

@ -275,6 +275,11 @@ impl Executed {
#[derive(Clone, Debug)]
pub struct Router(Arc<RootProvider<SimpleRequest>>, Address);
impl Router {
const DEPLOYMENT_GAS: u64 = 995_000;
const CONFIRM_NEXT_SERAI_KEY_GAS: u64 = 58_000;
const UPDATE_SERAI_KEY_GAS: u64 = 61_000;
const EXECUTE_BASE_GAS: u64 = 48_000;
fn code() -> Vec<u8> {
const BYTECODE: &[u8] =
include_bytes!(concat!(env!("OUT_DIR"), "/serai-processor-ethereum-router/Router.bin"));
@ -293,7 +298,7 @@ impl Router {
/// 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.gas_limit = Self::DEPLOYMENT_GAS * 120 / 100;
tx
}
@ -322,6 +327,25 @@ impl Router {
self.1
}
/// Get the message to be signed in order to confirm the next key for Serai.
pub fn confirm_next_serai_key_message(nonce: u64) -> Vec<u8> {
abi::confirmNextSeraiKeyCall::new((abi::Signature {
c: U256::try_from(nonce).unwrap().into(),
s: U256::ZERO.into(),
},))
.abi_encode()
}
/// Construct a transaction to confirm the next key representing Serai.
pub fn confirm_next_serai_key(&self, sig: &Signature) -> TxLegacy {
TxLegacy {
to: TxKind::Call(self.1),
input: abi::confirmNextSeraiKeyCall::new((abi::Signature::from(sig),)).abi_encode().into(),
gas_limit: Self::CONFIRM_NEXT_SERAI_KEY_GAS * 120 / 100,
..Default::default()
}
}
/// Get the message to be signed in order to update the key for Serai.
pub fn update_serai_key_message(nonce: u64, key: &PublicKey) -> Vec<u8> {
abi::updateSeraiKeyCall::new((
@ -341,7 +365,7 @@ impl Router {
))
.abi_encode()
.into(),
gas_limit: 40_889 * 120 / 100,
gas_limit: Self::UPDATE_SERAI_KEY_GAS * 120 / 100,
..Default::default()
}
}
@ -359,14 +383,14 @@ impl Router {
/// Construct a transaction to execute a batch of `OutInstruction`s.
pub fn execute(&self, coin: Coin, fee: U256, outs: OutInstructions, sig: &Signature) -> TxLegacy {
let outs_len = outs.0.len();
// TODO
let gas_limit = Self::EXECUTE_BASE_GAS + outs.0.iter().map(|_| 200_000 + 10_000).sum::<u64>();
TxLegacy {
to: TxKind::Call(self.1),
input: abi::executeCall::new((abi::Signature::from(sig), coin.address(), fee, outs.0))
.abi_encode()
.into(),
// TODO
gas_limit: (45_501 + ((200_000 + 10_000) * u128::try_from(outs_len).unwrap())) * 120 / 100,
gas_limit: gas_limit * 120 / 100,
..Default::default()
}
}
@ -536,7 +560,7 @@ impl Router {
res.push(Executed::SetKey {
nonce: log.nonce.try_into().map_err(|e| {
TransportErrorKind::Custom(format!("filtered to convert nonce to u64: {e:?}").into())
TransportErrorKind::Custom(format!("failed to convert nonce to u64: {e:?}").into())
})?,
key: log.key.into(),
});
@ -568,7 +592,7 @@ impl Router {
res.push(Executed::Batch {
nonce: log.nonce.try_into().map_err(|e| {
TransportErrorKind::Custom(format!("filtered to convert nonce to u64: {e:?}").into())
TransportErrorKind::Custom(format!("failed to convert nonce to u64: {e:?}").into())
})?,
message_hash: log.messageHash.into(),
});
@ -580,19 +604,40 @@ impl Router {
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()));
async fn fetch_key(
&self,
block: BlockId,
call: Vec<u8>,
) -> Result<Option<PublicKey>, RpcError<TransportErrorKind>> {
let call = TransactionRequest::default().to(self.1).input(TransactionInput::new(call.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(|| {
// This is fine as both key calls share a return type
let res = abi::nextSeraiKeyCall::abi_decode_returns(&bytes, true)
.map_err(|e| TransportErrorKind::Custom(format!("failed to decode key: {e:?}").into()))?;
let eth_repr = <[u8; 32]>::from(res._0);
Ok(if eth_repr == [0; 32] {
None
} else {
Some(PublicKey::from_eth_repr(eth_repr).ok_or_else(|| {
TransportErrorKind::Custom("invalid key set on router".to_string().into())
})?,
)
})?)
})
}
/// Fetch the next key for Serai's Ethereum validators
pub async fn next_key(
&self,
block: BlockId,
) -> Result<Option<PublicKey>, RpcError<TransportErrorKind>> {
self.fetch_key(block, abi::nextSeraiKeyCall::new(()).abi_encode()).await
}
/// Fetch the current key for Serai's Ethereum validators
pub async fn key(
&self,
block: BlockId,
) -> Result<Option<PublicKey>, RpcError<TransportErrorKind>> {
self.fetch_key(block, abi::seraiKeyCall::new(()).abi_encode()).await
}
/// Fetch the nonce of the next action to execute
@ -602,7 +647,7 @@ impl Router {
.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()))?;
.map_err(|e| TransportErrorKind::Custom(format!("failed to decode nonce: {e:?}").into()))?;
Ok(u64::try_from(res._0).map_err(|_| {
TransportErrorKind::Custom("nonce returned exceeded 2**64".to_string().into())
})?)
@ -615,7 +660,7 @@ impl Router {
.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())
TransportErrorKind::Custom(format!("failed to decode the address escaped to: {e:?}").into())
})?;
Ok(res._0)
}

View file

@ -37,13 +37,17 @@ fn execute_reentrancy_guard() {
#[test]
fn selector_collisions() {
assert_eq!(
crate::_irouter_abi::IRouter::executeCall::SELECTOR,
crate::_router_abi::Router::execute4DE42904Call::SELECTOR
crate::_irouter_abi::IRouter::confirmNextSeraiKeyCall::SELECTOR,
crate::_router_abi::Router::confirmNextSeraiKey34AC53ACCall::SELECTOR
);
assert_eq!(
crate::_irouter_abi::IRouter::updateSeraiKeyCall::SELECTOR,
crate::_router_abi::Router::updateSeraiKey5A8542A2Call::SELECTOR
);
assert_eq!(
crate::_irouter_abi::IRouter::executeCall::SELECTOR,
crate::_router_abi::Router::execute4DE42904Call::SELECTOR
);
assert_eq!(
crate::_irouter_abi::IRouter::escapeHatchCall::SELECTOR,
crate::_router_abi::Router::escapeHatchDCDD91CCCall::SELECTOR
@ -78,13 +82,13 @@ async fn setup_test(
// 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;
tx.gas_price = 100_000_000_000;
// 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);
assert_eq!(u128::from(Router::DEPLOYMENT_GAS), ((receipt.gas_used + 1000) / 1000) * 1000);
let router = Router::new(provider.clone(), &public_key).await.unwrap().unwrap();
@ -94,7 +98,8 @@ async fn setup_test(
#[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_key(BlockNumberOrTag::Latest.into()).await.unwrap(), Some(key.1));
assert_eq!(router.key(BlockNumberOrTag::Latest.into()).await.unwrap(), None);
assert_eq!(router.next_nonce(BlockNumberOrTag::Latest.into()).await.unwrap(), 1);
assert_eq!(
router.escaped_to(BlockNumberOrTag::Latest.into()).await.unwrap(),
@ -102,12 +107,54 @@ async fn test_constructor() {
);
}
async fn confirm_next_serai_key(
provider: &Arc<RootProvider<SimpleRequest>>,
router: &Router,
nonce: u64,
key: (Scalar, PublicKey),
) -> TransactionReceipt {
let msg = Router::confirm_next_serai_key_message(nonce);
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.confirm_next_serai_key(&sig);
tx.gas_price = 100_000_000_000;
let tx = ethereum_primitives::deterministically_sign(&tx);
let receipt = ethereum_test_primitives::publish_tx(provider, tx).await;
assert!(receipt.status());
assert_eq!(
u128::from(Router::CONFIRM_NEXT_SERAI_KEY_GAS),
((receipt.gas_used + 1000) / 1000) * 1000
);
receipt
}
#[tokio::test]
async fn test_confirm_next_serai_key() {
let (_anvil, provider, router, key) = setup_test().await;
assert_eq!(router.next_key(BlockNumberOrTag::Latest.into()).await.unwrap(), Some(key.1));
assert_eq!(router.key(BlockNumberOrTag::Latest.into()).await.unwrap(), None);
assert_eq!(router.next_nonce(BlockNumberOrTag::Latest.into()).await.unwrap(), 1);
let receipt = confirm_next_serai_key(&provider, &router, 1, key).await;
assert_eq!(router.next_key(receipt.block_hash.unwrap().into()).await.unwrap(), None);
assert_eq!(router.key(receipt.block_hash.unwrap().into()).await.unwrap(), Some(key.1));
assert_eq!(router.next_nonce(receipt.block_hash.unwrap().into()).await.unwrap(), 2);
}
#[tokio::test]
async fn test_update_serai_key() {
let (_anvil, provider, router, key) = setup_test().await;
confirm_next_serai_key(&provider, &router, 1, key).await;
let update_to = test_key().1;
let msg = Router::update_serai_key_message(1, &update_to);
let msg = Router::update_serai_key_message(2, &update_to);
let nonce = Scalar::random(&mut OsRng);
let c = Signature::challenge(ProjectivePoint::GENERATOR * nonce, &key.1, &msg);
@ -116,19 +163,22 @@ async fn test_update_serai_key() {
let sig = Signature::new(c, s).unwrap();
let mut tx = router.update_serai_key(&update_to, &sig);
tx.gas_price = 100_000_000_000u128;
tx.gas_price = 100_000_000_000;
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!(u128::from(Router::UPDATE_SERAI_KEY_GAS), ((receipt.gas_used + 1000) / 1000) * 1000);
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);
assert_eq!(router.key(receipt.block_hash.unwrap().into()).await.unwrap(), Some(key.1));
assert_eq!(router.next_key(receipt.block_hash.unwrap().into()).await.unwrap(), Some(update_to));
assert_eq!(router.next_nonce(receipt.block_hash.unwrap().into()).await.unwrap(), 3);
}
#[tokio::test]
async fn test_eth_in_instruction() {
let (_anvil, provider, router, _key) = setup_test().await;
let (_anvil, provider, router, key) = setup_test().await;
// TODO: Do we want to allow InInstructions before any key has been confirmed?
confirm_next_serai_key(&provider, &router, 1, key).await;
let amount = U256::try_from(OsRng.next_u64()).unwrap();
let mut in_instruction = vec![0; usize::try_from(OsRng.next_u64() % 256).unwrap()];
@ -138,8 +188,8 @@ async fn test_eth_in_instruction() {
chain_id: None,
nonce: 0,
// 100 gwei
gas_price: 100_000_000_000u128,
gas_limit: 1_000_000u128,
gas_price: 100_000_000_000,
gas_limit: 1_000_000,
to: TxKind::Call(router.address()),
value: amount,
input: crate::abi::inInstructionCall::new((
@ -200,7 +250,7 @@ async fn publish_outs(
let sig = Signature::new(c, s).unwrap();
let mut tx = router.execute(coin, fee, outs, &sig);
tx.gas_price = 100_000_000_000u128;
tx.gas_price = 100_000_000_000;
let tx = ethereum_primitives::deterministically_sign(&tx);
ethereum_test_primitives::publish_tx(provider, tx).await
}
@ -208,6 +258,7 @@ async fn publish_outs(
#[tokio::test]
async fn test_eth_address_out_instruction() {
let (_anvil, provider, router, key) = setup_test().await;
confirm_next_serai_key(&provider, &router, 1, key).await;
let mut amount = U256::try_from(OsRng.next_u64()).unwrap();
let mut fee = U256::try_from(OsRng.next_u64()).unwrap();
@ -218,11 +269,11 @@ async fn test_eth_address_out_instruction() {
ethereum_test_primitives::fund_account(&provider, router.address(), amount).await;
let instructions = OutInstructions::from([].as_slice());
let receipt = publish_outs(&provider, &router, key, 1, Coin::Ether, fee, instructions).await;
let receipt = publish_outs(&provider, &router, key, 2, Coin::Ether, fee, instructions).await;
assert!(receipt.status());
println!("empty execute used {} gas:", receipt.gas_used);
assert_eq!(u128::from(Router::EXECUTE_BASE_GAS), ((receipt.gas_used + 1000) / 1000) * 1000);
assert_eq!(router.next_nonce(receipt.block_hash.unwrap().into()).await.unwrap(), 2);
assert_eq!(router.next_nonce(receipt.block_hash.unwrap().into()).await.unwrap(), 3);
}
#[tokio::test]

View file

@ -7,6 +7,7 @@ repository = "https://github.com/serai-dex/serai/tree/develop/processor/ethereum
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
edition = "2021"
publish = false
rust-version = "1.81"
[package.metadata.docs.rs]
all-features = true
@ -19,10 +20,10 @@ workspace = true
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-consensus = { version = "0.7", default-features = false, features = ["std"] }
alloy-rpc-types-eth = { version = "0.3", default-features = false }
alloy-rpc-types-eth = { version = "0.7", 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 }
alloy-provider = { version = "0.7", default-features = false }
ethereum-primitives = { package = "serai-processor-ethereum-primitives", path = "../primitives", default-features = false }

View file

@ -5,7 +5,7 @@
use k256::{elliptic_curve::sec1::ToEncodedPoint, ProjectivePoint};
use alloy_core::{
primitives::{Address, U256, Bytes, Signature, TxKind},
primitives::{Address, U256, Bytes, PrimitiveSignature, TxKind},
hex::FromHex,
};
use alloy_consensus::{SignableTransaction, TxLegacy, Signed};
@ -46,7 +46,7 @@ pub async fn publish_tx(
let (tx, sig, _) = tx.into_parts();
let mut bytes = vec![];
tx.encode_with_signature_fields(&sig, &mut bytes);
tx.into_signed(sig).eip2718_encode(&mut bytes);
let pending_tx = provider.send_raw_transaction(&bytes).await.unwrap();
pending_tx.get_receipt().await.unwrap()
}
@ -111,7 +111,7 @@ pub async fn send(
);
let mut bytes = vec![];
tx.encode_with_signature_fields(&Signature::from(sig), &mut bytes);
tx.into_signed(PrimitiveSignature::from(sig)).eip2718_encode(&mut bytes);
let pending_tx = provider.send_raw_transaction(&bytes).await.unwrap();
pending_tx.get_receipt().await.unwrap()
}