mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 12:19:24 +00:00
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:
@@ -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(|_| ())?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -13,17 +13,17 @@ use serde::{Serialize, Deserialize};
|
||||
use scale::{Encode, Decode, MaxEncodedLen};
|
||||
use scale_info::TypeInfo;
|
||||
|
||||
use serai_primitives::{Balance, SeraiAddress, ExternalAddress, Data, system_address};
|
||||
use serai_primitives::{Balance, SeraiAddress, ExternalAddress, system_address};
|
||||
|
||||
pub const FEE_ACCOUNT: SeraiAddress = system_address(b"Coins-fees");
|
||||
|
||||
// TODO: Replace entirely with just Address
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)]
|
||||
#[cfg_attr(feature = "std", derive(Zeroize))]
|
||||
#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub struct OutInstruction {
|
||||
pub address: ExternalAddress,
|
||||
pub data: Option<Data>,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)]
|
||||
|
||||
@@ -205,11 +205,7 @@ pub mod pallet {
|
||||
let coin_balance =
|
||||
Coins::<T>::balance(IN_INSTRUCTION_EXECUTOR.into(), out_balance.coin);
|
||||
let instruction = OutInstructionWithBalance {
|
||||
instruction: OutInstruction {
|
||||
address: out_address.as_external().unwrap(),
|
||||
// TODO: Properly pass data. Replace address with an OutInstruction entirely?
|
||||
data: None,
|
||||
},
|
||||
instruction: OutInstruction { address: out_address.as_external().unwrap() },
|
||||
balance: Balance { coin: out_balance.coin, amount: coin_balance },
|
||||
};
|
||||
Coins::<T>::burn_with_instruction(origin.into(), instruction)?;
|
||||
|
||||
@@ -59,10 +59,7 @@ pub fn borsh_deserialize_bounded_vec<R: borsh::io::Read, T: BorshDeserialize, co
|
||||
vec.try_into().map_err(|_| borsh::io::Error::other("bound exceeded"))
|
||||
}
|
||||
|
||||
// Monero, our current longest address candidate, has a longest address of featured
|
||||
// 1 (enum) + 1 (flags) + 64 (two keys) = 66
|
||||
// When JAMTIS arrives, it'll become 112 or potentially even 142 bytes
|
||||
pub const MAX_ADDRESS_LEN: u32 = 192;
|
||||
pub const MAX_ADDRESS_LEN: u32 = 512;
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)]
|
||||
#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))]
|
||||
@@ -102,51 +99,6 @@ impl AsRef<[u8]> for ExternalAddress {
|
||||
}
|
||||
}
|
||||
|
||||
// Should be enough for a Uniswap v3 call
|
||||
pub const MAX_DATA_LEN: u32 = 512;
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)]
|
||||
#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub struct Data(
|
||||
#[cfg_attr(
|
||||
feature = "borsh",
|
||||
borsh(
|
||||
serialize_with = "borsh_serialize_bounded_vec",
|
||||
deserialize_with = "borsh_deserialize_bounded_vec"
|
||||
)
|
||||
)]
|
||||
BoundedVec<u8, ConstU32<{ MAX_DATA_LEN }>>,
|
||||
);
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl Zeroize for Data {
|
||||
fn zeroize(&mut self) {
|
||||
self.0.as_mut().zeroize()
|
||||
}
|
||||
}
|
||||
|
||||
impl Data {
|
||||
#[cfg(feature = "std")]
|
||||
pub fn new(data: Vec<u8>) -> Result<Data, &'static str> {
|
||||
Ok(Data(data.try_into().map_err(|_| "data length exceeds {MAX_DATA_LEN}")?))
|
||||
}
|
||||
|
||||
pub fn data(&self) -> &[u8] {
|
||||
self.0.as_ref()
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
pub fn consume(self) -> Vec<u8> {
|
||||
self.0.into_inner()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for Data {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
/// Lexicographically reverses a given byte array.
|
||||
pub fn reverse_lexicographic_order<const N: usize>(bytes: [u8; N]) -> [u8; N] {
|
||||
let mut res = [0u8; N];
|
||||
|
||||
Reference in New Issue
Block a user