mirror of
https://github.com/serai-dex/serai.git
synced 2024-12-25 13:09:30 +00:00
Update how signatures are handled by the Router
This commit is contained in:
parent
6a520a7412
commit
cf4123b0f8
3 changed files with 151 additions and 77 deletions
|
@ -126,76 +126,133 @@ contract Router {
|
||||||
/// @notice Escaping when escape hatch wasn't invoked.
|
/// @notice Escaping when escape hatch wasn't invoked.
|
||||||
error EscapeHatchNotInvoked();
|
error EscapeHatchNotInvoked();
|
||||||
|
|
||||||
/**
|
/// @dev Updates the Serai key. This does not update `_nextNonce`
|
||||||
* @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
|
|
||||||
* `_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
|
||||||
modifier updateSeraiKeyAtEndOfFn(uint256 nonceUpdatedWith, bytes32 newSeraiKey) {
|
function _updateSeraiKey(uint256 nonceUpdatedWith, bytes32 newSeraiKey) private {
|
||||||
// Run the function itself
|
|
||||||
_;
|
|
||||||
|
|
||||||
// Update the key
|
|
||||||
_seraiKey = newSeraiKey;
|
_seraiKey = newSeraiKey;
|
||||||
emit SeraiKeyUpdated(nonceUpdatedWith, newSeraiKey);
|
emit SeraiKeyUpdated(nonceUpdatedWith, newSeraiKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice The constructor for the relayer
|
/// @notice The constructor for the relayer
|
||||||
/// @param initialSeraiKey The initial key for Serai's Ethereum validators
|
/// @param initialSeraiKey The initial key for Serai's Ethereum validators
|
||||||
constructor(bytes32 initialSeraiKey) updateSeraiKeyAtEndOfFn(0, initialSeraiKey) {
|
constructor(bytes32 initialSeraiKey) {
|
||||||
// Nonces are incremented by 1 upon account creation, prior to any code execution, per EIP-161
|
// Nonces are incremented by 1 upon account creation, prior to any code execution, per EIP-161
|
||||||
// This is incompatible with any networks which don't have their nonces start at 0
|
// This is incompatible with any networks which don't have their nonces start at 0
|
||||||
_smartContractNonce = 1;
|
_smartContractNonce = 1;
|
||||||
|
|
||||||
// We consumed nonce 0 when setting the initial Serai key
|
// Set the Serai key
|
||||||
|
_updateSeraiKey(0, initialSeraiKey);
|
||||||
|
|
||||||
|
// We just consumed nonce 0 when setting the initial Serai key
|
||||||
_nextNonce = 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @dev Verify a signature
|
/**
|
||||||
/// @param message The message to pass to the Schnorr contract
|
* @dev
|
||||||
/// @param signature The signature by the current key for this message
|
* Verify a signature of the calldata, placed immediately after the function selector. The calldata
|
||||||
function verifySignature(bytes32 message, Signature calldata signature) private {
|
* should be signed with the nonce taking the place of the signature's commitment to its nonce, and
|
||||||
|
* the signature solution zeroed.
|
||||||
|
*/
|
||||||
|
function verifySignature()
|
||||||
|
private
|
||||||
|
returns (uint256 nonceUsed, bytes memory message, bytes32 messageHash)
|
||||||
|
{
|
||||||
// If the escape hatch was triggered, reject further signatures
|
// If the escape hatch was triggered, reject further signatures
|
||||||
if (_escapedTo != address(0)) {
|
if (_escapedTo != address(0)) {
|
||||||
revert EscapeHatchInvoked();
|
revert EscapeHatchInvoked();
|
||||||
}
|
}
|
||||||
// Verify the signature
|
|
||||||
if (!Schnorr.verify(_seraiKey, message, signature.c, signature.s)) {
|
message = msg.data;
|
||||||
|
uint256 messageLen = message.length;
|
||||||
|
/*
|
||||||
|
function selector, signature
|
||||||
|
|
||||||
|
This check means we don't read memory, and as we attempt to clear portions, write past it
|
||||||
|
(triggering undefined behavior).
|
||||||
|
*/
|
||||||
|
if (messageLen < 68) {
|
||||||
revert InvalidSignature();
|
revert InvalidSignature();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read _nextNonce into memory as the nonce we'll use
|
||||||
|
nonceUsed = _nextNonce;
|
||||||
|
|
||||||
|
// Declare memory to copy the signature out to
|
||||||
|
bytes32 signatureC;
|
||||||
|
bytes32 signatureS;
|
||||||
|
|
||||||
|
// slither-disable-next-line assembly
|
||||||
|
assembly {
|
||||||
|
// Read the signature (placed after the function signature)
|
||||||
|
signatureC := mload(add(message, 36))
|
||||||
|
signatureS := mload(add(message, 68))
|
||||||
|
|
||||||
|
// Overwrite the signature challenge with the nonce
|
||||||
|
mstore(add(message, 36), nonceUsed)
|
||||||
|
// Overwrite the signature response with 0
|
||||||
|
mstore(add(message, 68), 0)
|
||||||
|
|
||||||
|
// Calculate the message hash
|
||||||
|
messageHash := keccak256(add(message, 32), messageLen)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the signature
|
||||||
|
if (!Schnorr.verify(_seraiKey, messageHash, signatureC, signatureS)) {
|
||||||
|
revert InvalidSignature();
|
||||||
|
}
|
||||||
|
|
||||||
// Set the next nonce
|
// Set the next nonce
|
||||||
unchecked {
|
unchecked {
|
||||||
_nextNonce++;
|
_nextNonce = nonceUsed + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Advance the message past the function selector, enabling decoding the arguments. Ideally, we'd
|
||||||
|
also advance past the signature (to simplify decoding arguments and save some memory). This
|
||||||
|
would transfrom message from:
|
||||||
|
|
||||||
|
message (pointer)
|
||||||
|
v
|
||||||
|
------------------------------------------------------------
|
||||||
|
| 32-byte length | 4-byte selector | Signature | Arguments |
|
||||||
|
------------------------------------------------------------
|
||||||
|
|
||||||
|
to:
|
||||||
|
|
||||||
|
message (pointer)
|
||||||
|
v
|
||||||
|
----------------------------------------------
|
||||||
|
| Junk 68 bytes | 32-byte length | Arguments |
|
||||||
|
----------------------------------------------
|
||||||
|
|
||||||
|
Unfortunately, doing so corrupts the offsets defined within the ABI itself. We settle for a
|
||||||
|
transform to:
|
||||||
|
|
||||||
|
message (pointer)
|
||||||
|
v
|
||||||
|
---------------------------------------------------------
|
||||||
|
| Junk 4 bytes | 32-byte length | Signature | Arguments |
|
||||||
|
---------------------------------------------------------
|
||||||
|
*/
|
||||||
|
// slither-disable-next-line assembly
|
||||||
|
assembly {
|
||||||
|
message := add(message, 4)
|
||||||
|
mstore(message, sub(messageLen, 4))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @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
|
/// @dev This assumes the key is correct. No checks on it are performed
|
||||||
/// @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
|
// @param newSeraiKey The key to update to
|
||||||
function updateSeraiKey(bytes32 newSeraiKey, Signature calldata signature)
|
function updateSeraiKey() external {
|
||||||
external
|
(uint256 nonceUsed, bytes memory args,) = verifySignature();
|
||||||
updateSeraiKeyAtEndOfFn(_nextNonce, newSeraiKey)
|
(,, bytes32 newSeraiKey) = abi.decode(args, (bytes32, bytes32, bytes32));
|
||||||
{
|
_updateSeraiKey(nonceUsed, newSeraiKey);
|
||||||
/*
|
|
||||||
This DST needs a length prefix as well to prevent DSTs potentially being substrings of each
|
|
||||||
other, yet this is fine for our well-defined, extremely-limited use.
|
|
||||||
|
|
||||||
We don't encode the chain ID as Serai generates independent keys for each integration. If
|
|
||||||
Ethereum L2s are integrated, and they reuse the Ethereum validator set, we would use the
|
|
||||||
existing Serai key yet we'd apply an off-chain derivation scheme to bind it to specific
|
|
||||||
networks. This also lets Serai identify EVMs per however it wants, solving the edge case where
|
|
||||||
two instances of the EVM share a chain ID for whatever horrific reason.
|
|
||||||
|
|
||||||
This uses encodePacked as all items present here are of fixed length.
|
|
||||||
*/
|
|
||||||
bytes32 message = keccak256(abi.encodePacked("updateSeraiKey", _nextNonce, newSeraiKey));
|
|
||||||
verifySignature(message, signature);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Transfer coins into Serai with an instruction
|
/// @notice Transfer coins into Serai with an instruction
|
||||||
|
@ -357,26 +414,19 @@ contract Router {
|
||||||
* @dev All `OutInstruction`s in a batch are only for a single coin to simplify handling of the
|
* @dev All `OutInstruction`s in a batch are only for a single coin to simplify handling of the
|
||||||
* fee
|
* fee
|
||||||
*/
|
*/
|
||||||
/// @param coin The coin all of these `OutInstruction`s are for
|
// @param signature The signature by the current key for Serai's Ethereum validators
|
||||||
/// @param fee The fee to pay (in coin) to the caller for their relaying of this batch
|
// @param coin The coin all of these `OutInstruction`s are for
|
||||||
/// @param outs The `OutInstruction`s to act on
|
// @param fee The fee to pay (in coin) to the caller for their relaying of this batch
|
||||||
/// @param signature The signature by the current key for Serai's Ethereum validators
|
// @param outs The `OutInstruction`s to act on
|
||||||
// Each individual call is explicitly metered to ensure there isn't a DoS here
|
// Each individual call is explicitly metered to ensure there isn't a DoS here
|
||||||
// slither-disable-next-line calls-loop
|
// slither-disable-next-line calls-loop
|
||||||
function execute(
|
function execute() external {
|
||||||
address coin,
|
(uint256 nonceUsed, bytes memory args, bytes32 message) = verifySignature();
|
||||||
uint256 fee,
|
(,, address coin, uint256 fee, OutInstruction[] memory outs) =
|
||||||
OutInstruction[] calldata outs,
|
abi.decode(args, (bytes32, bytes32, address, uint256, OutInstruction[]));
|
||||||
Signature calldata signature
|
|
||||||
) external {
|
|
||||||
// Verify the signature
|
|
||||||
// This uses `encode`, not `encodePacked`, as `outs` is of variable length
|
|
||||||
// TODO: Use a custom encode in verifySignature here with assembly (benchmarking before/after)
|
|
||||||
bytes32 message = keccak256(abi.encode("execute", _nextNonce, coin, fee, outs));
|
|
||||||
verifySignature(message, signature);
|
|
||||||
|
|
||||||
// TODO: Also include a bit mask here
|
// TODO: Also include a bit mask here
|
||||||
emit Executed(_nextNonce, message);
|
emit Executed(nonceUsed, 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
|
||||||
|
@ -439,9 +489,14 @@ contract Router {
|
||||||
|
|
||||||
/// @notice Escapes to a new smart contract
|
/// @notice Escapes to a new smart contract
|
||||||
/// @dev This should be used upon an invariant being reached or new functionality being needed
|
/// @dev This should be used upon an invariant being reached or new functionality being needed
|
||||||
/// @param escapeTo The address to escape to
|
// @param signature The signature by the current key for Serai's Ethereum validators
|
||||||
/// @param signature The signature by the current key for Serai's Ethereum validators
|
// @param escapeTo The address to escape to
|
||||||
function escapeHatch(address escapeTo, Signature calldata signature) external {
|
function escapeHatch() external {
|
||||||
|
// Verify the signature
|
||||||
|
(, bytes memory args,) = verifySignature();
|
||||||
|
|
||||||
|
(,, address escapeTo) = abi.decode(args, (bytes32, bytes32, address));
|
||||||
|
|
||||||
if (escapeTo == address(0)) {
|
if (escapeTo == address(0)) {
|
||||||
revert InvalidEscapeAddress();
|
revert InvalidEscapeAddress();
|
||||||
}
|
}
|
||||||
|
@ -454,10 +509,6 @@ contract Router {
|
||||||
revert EscapeHatchInvoked();
|
revert EscapeHatchInvoked();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the signature
|
|
||||||
bytes32 message = keccak256(abi.encodePacked("escapeHatch", _nextNonce, escapeTo));
|
|
||||||
verifySignature(message, signature);
|
|
||||||
|
|
||||||
_escapedTo = escapeTo;
|
_escapedTo = escapeTo;
|
||||||
emit EscapeHatch(escapeTo);
|
emit EscapeHatch(escapeTo);
|
||||||
}
|
}
|
||||||
|
|
|
@ -309,26 +309,36 @@ impl Router {
|
||||||
|
|
||||||
/// Get the message to be signed in order to update the key for Serai.
|
/// 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> {
|
pub fn update_serai_key_message(nonce: u64, key: &PublicKey) -> Vec<u8> {
|
||||||
("updateSeraiKey", U256::try_from(nonce).expect("couldn't convert u64 to u256"), key.eth_repr())
|
[
|
||||||
.abi_encode_packed()
|
abi::updateSeraiKeyCall::SELECTOR.as_slice(),
|
||||||
|
&(U256::try_from(nonce).unwrap(), U256::ZERO, key.eth_repr()).abi_encode_params(),
|
||||||
|
]
|
||||||
|
.concat()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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 {
|
||||||
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_encode()
|
abi::updateSeraiKeyCall::SELECTOR.as_slice(),
|
||||||
.into(),
|
&(abi::Signature::from(sig), public_key.eth_repr()).abi_encode_params(),
|
||||||
gas_limit: 40748 * 120 / 100,
|
]
|
||||||
|
.concat()
|
||||||
|
.into(),
|
||||||
|
gas_limit: 40927 * 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".to_string(), U256::try_from(nonce).unwrap(), coin.address(), fee, outs.0)
|
[
|
||||||
.abi_encode_sequence()
|
abi::executeCall::SELECTOR.as_slice(),
|
||||||
|
&(U256::try_from(nonce).unwrap(), U256::ZERO, coin.address(), fee, outs.0)
|
||||||
|
.abi_encode_params(),
|
||||||
|
]
|
||||||
|
.concat()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Construct a transaction to execute a batch of `OutInstruction`s.
|
/// Construct a transaction to execute a batch of `OutInstruction`s.
|
||||||
|
@ -336,7 +346,12 @@ impl Router {
|
||||||
let outs_len = outs.0.len();
|
let outs_len = outs.0.len();
|
||||||
TxLegacy {
|
TxLegacy {
|
||||||
to: TxKind::Call(self.1),
|
to: TxKind::Call(self.1),
|
||||||
input: abi::executeCall::new((coin.address(), fee, outs.0, sig.into())).abi_encode().into(),
|
input: [
|
||||||
|
abi::executeCall::SELECTOR.as_slice(),
|
||||||
|
&(abi::Signature::from(sig), coin.address(), fee, outs.0).abi_encode_params(),
|
||||||
|
]
|
||||||
|
.concat()
|
||||||
|
.into(),
|
||||||
// TODO
|
// TODO
|
||||||
gas_limit: 100_000 + ((200_000 + 10_000) * u128::try_from(outs_len).unwrap()),
|
gas_limit: 100_000 + ((200_000 + 10_000) * u128::try_from(outs_len).unwrap()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|
|
@ -10,7 +10,7 @@ use alloy_sol_types::SolCall;
|
||||||
|
|
||||||
use alloy_consensus::TxLegacy;
|
use alloy_consensus::TxLegacy;
|
||||||
|
|
||||||
use alloy_rpc_types_eth::BlockNumberOrTag;
|
use alloy_rpc_types_eth::{BlockNumberOrTag, TransactionReceipt};
|
||||||
use alloy_simple_request_transport::SimpleRequest;
|
use alloy_simple_request_transport::SimpleRequest;
|
||||||
use alloy_rpc_client::ClientBuilder;
|
use alloy_rpc_client::ClientBuilder;
|
||||||
use alloy_provider::RootProvider;
|
use alloy_provider::RootProvider;
|
||||||
|
@ -154,8 +154,16 @@ async fn test_erc20_in_instruction() {
|
||||||
todo!("TODO")
|
todo!("TODO")
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn publish_outs(key: (Scalar, PublicKey), nonce: u64, coin: Coin, fee: U256, outs: OutInstructions) -> TransactionReceipt {
|
async fn publish_outs(
|
||||||
let msg = Router::execute_message(nonce, coin, fee, instructions.clone());
|
provider: &RootProvider<SimpleRequest>,
|
||||||
|
router: &Router,
|
||||||
|
key: (Scalar, PublicKey),
|
||||||
|
nonce: u64,
|
||||||
|
coin: Coin,
|
||||||
|
fee: U256,
|
||||||
|
outs: OutInstructions,
|
||||||
|
) -> TransactionReceipt {
|
||||||
|
let msg = Router::execute_message(nonce, coin, fee, outs.clone());
|
||||||
|
|
||||||
let nonce = Scalar::random(&mut OsRng);
|
let nonce = Scalar::random(&mut OsRng);
|
||||||
let c = Signature::challenge(ProjectivePoint::GENERATOR * nonce, &key.1, &msg);
|
let c = Signature::challenge(ProjectivePoint::GENERATOR * nonce, &key.1, &msg);
|
||||||
|
@ -163,10 +171,10 @@ async fn publish_outs(key: (Scalar, PublicKey), nonce: u64, coin: Coin, fee: U25
|
||||||
|
|
||||||
let sig = Signature::new(c, s).unwrap();
|
let sig = Signature::new(c, s).unwrap();
|
||||||
|
|
||||||
let mut tx = router.execute(coin, fee, instructions, &sig);
|
let mut tx = router.execute(coin, fee, outs, &sig);
|
||||||
tx.gas_price = 100_000_000_000u128;
|
tx.gas_price = 100_000_000_000u128;
|
||||||
let tx = ethereum_primitives::deterministically_sign(&tx);
|
let tx = ethereum_primitives::deterministically_sign(&tx);
|
||||||
ethereum_test_primitives::publish_tx(&provider, tx).await
|
ethereum_test_primitives::publish_tx(provider, tx).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
@ -182,7 +190,7 @@ async fn test_eth_address_out_instruction() {
|
||||||
ethereum_test_primitives::fund_account(&provider, router.address(), amount).await;
|
ethereum_test_primitives::fund_account(&provider, router.address(), amount).await;
|
||||||
|
|
||||||
let instructions = OutInstructions::from([].as_slice());
|
let instructions = OutInstructions::from([].as_slice());
|
||||||
let receipt = publish_outs(key, 1, Coin::Ether, fee, instructions);
|
let receipt = publish_outs(&provider, &router, key, 1, Coin::Ether, fee, instructions).await;
|
||||||
assert!(receipt.status());
|
assert!(receipt.status());
|
||||||
println!("empty execute used {} gas:", receipt.gas_used);
|
println!("empty execute used {} gas:", receipt.gas_used);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue