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
18 changed files with 679 additions and 326 deletions

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]