Remove OutInstruction's data field

It makes sense for networks which support arbitrary data to do as part of their
address. This reduces the ability to perform DoSs, achieves better performance,
and better uses the type system (as now networks we don't support data on don't
have a data field).

Updates the Ethereum address definition in serai-client accordingly
This commit is contained in:
Luke Parker
2024-09-15 12:48:09 -04:00
parent 39be23d807
commit eb9bce6862
18 changed files with 121 additions and 150 deletions

View File

@@ -1,35 +1,93 @@
use core::{str::FromStr, fmt};
use core::str::FromStr;
use std::io::Read;
use borsh::{BorshSerialize, BorshDeserialize};
use crate::primitives::ExternalAddress;
use crate::primitives::{MAX_ADDRESS_LEN, ExternalAddress};
/// A representation of an Ethereum address.
#[derive(Clone, Copy, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)]
pub struct Address([u8; 20]);
#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)]
pub struct ContractDeployment {
/// The gas limit to use for this contract's execution.
///
/// THis MUST be less than the Serai gas limit. The cost of it will be deducted from the amount
/// transferred.
gas: u32,
/// The initialization code of the contract to deploy.
///
/// This contract will be deployed (executing the initialization code). No further calls will
/// be made.
code: Vec<u8>,
}
impl From<[u8; 20]> for Address {
fn from(address: [u8; 20]) -> Self {
Self(address)
/// A contract to deploy, enabling executing arbitrary code.
impl ContractDeployment {
pub fn new(gas: u32, code: Vec<u8>) -> Option<Self> {
// The max address length, minus the type byte, minus the size of the gas
const MAX_CODE_LEN: usize = (MAX_ADDRESS_LEN as usize) - (1 + core::mem::size_of::<u32>());
if code.len() > MAX_CODE_LEN {
None?;
}
Some(Self { gas, code })
}
}
impl From<Address> for [u8; 20] {
fn from(address: Address) -> Self {
address.0
/// A representation of an Ethereum address.
#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)]
pub enum Address {
/// A traditional address.
Address([u8; 20]),
/// A contract to deploy, enabling executing arbitrary code.
Contract(ContractDeployment),
}
impl From<[u8; 20]> for Address {
fn from(address: [u8; 20]) -> Self {
Address::Address(address)
}
}
impl TryFrom<ExternalAddress> for Address {
type Error = ();
fn try_from(data: ExternalAddress) -> Result<Address, ()> {
Ok(Self(data.as_ref().try_into().map_err(|_| ())?))
let mut kind = [0xff];
let mut reader: &[u8] = data.as_ref();
reader.read_exact(&mut kind).map_err(|_| ())?;
Ok(match kind[0] {
0 => {
let mut address = [0xff; 20];
reader.read_exact(&mut address).map_err(|_| ())?;
Address::Address(address)
}
1 => {
let mut gas = [0xff; 4];
reader.read_exact(&mut gas).map_err(|_| ())?;
// The code is whatever's left since the ExternalAddress is a delimited container of
// appropriately bounded length
Address::Contract(ContractDeployment {
gas: u32::from_le_bytes(gas),
code: reader.to_vec(),
})
}
_ => Err(())?,
})
}
}
impl From<Address> for ExternalAddress {
fn from(address: Address) -> ExternalAddress {
// This is 20 bytes which is less than MAX_ADDRESS_LEN
ExternalAddress::new(address.0.to_vec()).unwrap()
let mut res = Vec::with_capacity(1 + 20);
match address {
Address::Address(address) => {
res.push(0);
res.extend(&address);
}
Address::Contract(ContractDeployment { gas, code }) => {
res.push(1);
res.extend(&gas.to_le_bytes());
res.extend(&code);
}
}
// We only construct addresses whose code is small enough this can safely be constructed
ExternalAddress::new(res).unwrap()
}
}
@@ -40,12 +98,8 @@ impl FromStr for Address {
if address.len() != 40 {
Err(())?
};
Ok(Self(hex::decode(address.to_lowercase()).map_err(|_| ())?.try_into().unwrap()))
}
}
impl fmt::Display for Address {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "0x{}", hex::encode(self.0))
Ok(Address::Address(
hex::decode(address.to_lowercase()).map_err(|_| ())?.try_into().map_err(|_| ())?,
))
}
}

View File

@@ -12,7 +12,7 @@ use sp_core::Pair;
use serai_client::{
primitives::{
Amount, NetworkId, Coin, Balance, BlockHash, SeraiAddress, Data, ExternalAddress,
Amount, NetworkId, Coin, Balance, BlockHash, SeraiAddress, ExternalAddress,
insecure_pair_from_name,
},
in_instructions::{
@@ -55,39 +55,35 @@ serai_test!(
let block = provide_batch(&serai, batch.clone()).await;
let instruction = {
let serai = serai.as_of(block);
let batches = serai.in_instructions().batch_events().await.unwrap();
assert_eq!(
batches,
vec![InInstructionsEvent::Batch {
network,
id,
block: block_hash,
instructions_hash: Blake2b::<U32>::digest(batch.instructions.encode()).into(),
}]
);
let serai = serai.as_of(block);
let batches = serai.in_instructions().batch_events().await.unwrap();
assert_eq!(
batches,
vec![InInstructionsEvent::Batch {
network,
id,
block: block_hash,
instructions_hash: Blake2b::<U32>::digest(batch.instructions.encode()).into(),
}]
);
assert_eq!(
serai.coins().mint_events().await.unwrap(),
vec![CoinsEvent::Mint { to: address, balance }]
);
assert_eq!(serai.coins().coin_supply(coin).await.unwrap(), amount);
assert_eq!(serai.coins().coin_balance(coin, address).await.unwrap(), amount);
assert_eq!(
serai.coins().mint_events().await.unwrap(),
vec![CoinsEvent::Mint { to: address, balance }]
);
assert_eq!(serai.coins().coin_supply(coin).await.unwrap(), amount);
assert_eq!(serai.coins().coin_balance(coin, address).await.unwrap(), amount);
// Now burn it
let mut rand_bytes = vec![0; 32];
OsRng.fill_bytes(&mut rand_bytes);
let external_address = ExternalAddress::new(rand_bytes).unwrap();
// Now burn it
let mut rand_bytes = vec![0; 32];
OsRng.fill_bytes(&mut rand_bytes);
let external_address = ExternalAddress::new(rand_bytes).unwrap();
let mut rand_bytes = vec![0; 32];
OsRng.fill_bytes(&mut rand_bytes);
let data = Data::new(rand_bytes).unwrap();
OutInstructionWithBalance {
balance,
instruction: OutInstruction { address: external_address, data: Some(data) },
}
};
OutInstructionWithBalance {
balance,
instruction: OutInstruction { address: external_address },
}
};
let block = publish_tx(
&serai,