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

@ -192,7 +192,7 @@ contract Router {
_transferOut(nextAddress, transactions[i].coin, transactions[i].value);
// Perform the calls with a set gas budget
(uint24 gas, bytes memory code) = abi.decode(transactions[i].destination, (uint24, bytes));
(uint32 gas, bytes memory code) = abi.decode(transactions[i].destination, (uint32, bytes));
address(this).call{
gas: gas
}(abi.encodeWithSelector(Router.arbitaryCallOut.selector, code));

View file

@ -3,7 +3,7 @@ use std::io;
use scale::{Encode, Decode, IoReader};
use borsh::{BorshSerialize, BorshDeserialize};
use serai_primitives::{Balance, Data};
use serai_primitives::Balance;
use serai_coins_primitives::OutInstructionWithBalance;
use crate::Address;
@ -13,7 +13,6 @@ use crate::Address;
pub struct Payment<A: Address> {
address: A,
balance: Balance,
data: Option<Vec<u8>>,
}
impl<A: Address> TryFrom<OutInstructionWithBalance> for Payment<A> {
@ -22,15 +21,14 @@ impl<A: Address> TryFrom<OutInstructionWithBalance> for Payment<A> {
Ok(Payment {
address: out_instruction_with_balance.instruction.address.try_into().map_err(|_| ())?,
balance: out_instruction_with_balance.balance,
data: out_instruction_with_balance.instruction.data.map(Data::consume),
})
}
}
impl<A: Address> Payment<A> {
/// Create a new Payment.
pub fn new(address: A, balance: Balance, data: Option<Vec<u8>>) -> Self {
Payment { address, balance, data }
pub fn new(address: A, balance: Balance) -> Self {
Payment { address, balance }
}
/// The address to pay.
@ -41,24 +39,18 @@ impl<A: Address> Payment<A> {
pub fn balance(&self) -> Balance {
self.balance
}
/// The data to associate with this payment.
pub fn data(&self) -> &Option<Vec<u8>> {
&self.data
}
/// Read a Payment.
pub fn read(reader: &mut impl io::Read) -> io::Result<Self> {
let address = A::deserialize_reader(reader)?;
let reader = &mut IoReader(reader);
let balance = Balance::decode(reader).map_err(io::Error::other)?;
let data = Option::<Vec<u8>>::decode(reader).map_err(io::Error::other)?;
Ok(Self { address, balance, data })
Ok(Self { address, balance })
}
/// Write the Payment.
pub fn write(&self, writer: &mut impl io::Write) -> io::Result<()> {
self.address.serialize(writer)?;
self.balance.encode_to(writer);
self.data.encode_to(writer);
Ok(())
}
}

View file

@ -4,7 +4,6 @@ use std::collections::HashMap;
use scale::Decode;
use serai_db::{Get, DbTxn, Db};
use serai_primitives::MAX_DATA_LEN;
use serai_in_instructions_primitives::{
Shorthand, RefundableInInstruction, InInstruction, InInstructionWithBalance,
};
@ -56,16 +55,6 @@ fn in_instruction_from_output<S: ScannerFeed>(
let presumed_origin = output.presumed_origin();
let mut data = output.data();
let max_data_len = usize::try_from(MAX_DATA_LEN).unwrap();
if data.len() > max_data_len {
log::info!(
"data in output {} exceeded MAX_DATA_LEN ({MAX_DATA_LEN}): {}. skipping",
hex::encode(output.id()),
data.len(),
);
return (presumed_origin, None);
}
let shorthand = match Shorthand::decode(&mut data) {
Ok(shorthand) => shorthand,
Err(e) => {

View file

@ -142,7 +142,7 @@ impl<D: Db, S: ScannerFeed> ContinuallyRan for SubstrateTask<D, S> {
if let Some(report::ReturnInformation { address, balance }) = return_information {
burns.push(OutInstructionWithBalance {
instruction: OutInstruction { address: address.into(), data: None },
instruction: OutInstruction { address: address.into() },
balance,
});
}

View file

@ -130,7 +130,7 @@ impl<S: ScannerFeed, SC: Send + Sync + SmartContract<S>> SchedulerTrait<S> for S
.returns()
.iter()
.map(|to_return| {
Payment::new(to_return.address().clone(), to_return.output().balance(), None)
Payment::new(to_return.address().clone(), to_return.output().balance())
})
.collect::<Vec<_>>(),
),

View file

@ -115,11 +115,7 @@ impl<A: Address> TreeTransaction<A> {
.filter_map(|(payment, amount)| {
amount.map(|amount| {
// The existing payment, with the new amount
Payment::new(
payment.address().clone(),
Balance { coin, amount: Amount(amount) },
payment.data().clone(),
)
Payment::new(payment.address().clone(), Balance { coin, amount: Amount(amount) })
})
})
.collect()
@ -130,7 +126,7 @@ impl<A: Address> TreeTransaction<A> {
.filter_map(|amount| {
amount.map(|amount| {
// A branch output with the new amount
Payment::new(branch_address.clone(), Balance { coin, amount: Amount(amount) }, None)
Payment::new(branch_address.clone(), Balance { coin, amount: Amount(amount) })
})
})
.collect()

View file

@ -489,7 +489,7 @@ impl<S: ScannerFeed, P: TransactionPlanner<S, ()>> SchedulerTrait<S> for Schedul
&mut 0,
block,
vec![forward.clone()],
vec![Payment::new(P::forwarding_address(forward_to_key), forward.balance(), None)],
vec![Payment::new(P::forwarding_address(forward_to_key), forward.balance())],
None,
)
.await?
@ -501,7 +501,7 @@ impl<S: ScannerFeed, P: TransactionPlanner<S, ()>> SchedulerTrait<S> for Schedul
for to_return in update.returns() {
let key = to_return.output().key();
let out_instruction =
Payment::new(to_return.address().clone(), to_return.output().balance(), None);
Payment::new(to_return.address().clone(), to_return.output().balance());
let Some(plan) = self
.planner
.plan_transaction_with_fee_amortization(

View file

@ -507,7 +507,7 @@ impl<S: ScannerFeed, P: TransactionPlanner<S, EffectedReceivedOutputs<S>>> Sched
&mut 0,
block,
vec![forward.clone()],
vec![Payment::new(P::forwarding_address(forward_to_key), forward.balance(), None)],
vec![Payment::new(P::forwarding_address(forward_to_key), forward.balance())],
None,
)
.await?
@ -519,7 +519,7 @@ impl<S: ScannerFeed, P: TransactionPlanner<S, EffectedReceivedOutputs<S>>> Sched
for to_return in update.returns() {
let key = to_return.output().key();
let out_instruction =
Payment::new(to_return.address().clone(), to_return.output().balance(), None);
Payment::new(to_return.address().clone(), to_return.output().balance());
let Some(plan) = self
.planner
.plan_transaction_with_fee_amortization(

View file

@ -184,7 +184,6 @@ pub async fn test_signer<N: Network>(
let mut scheduler = N::Scheduler::new::<MemDb>(&mut txn, key, N::NETWORK);
let payments = vec![Payment {
address: N::external_address(&network, key).await,
data: None,
balance: Balance {
coin: match N::NETWORK {
NetworkId::Serai => panic!("test_signer called with Serai"),

View file

@ -88,7 +88,6 @@ pub async fn test_wallet<N: Network>(
outputs.clone(),
vec![Payment {
address: N::external_address(&network, key).await,
data: None,
balance: Balance {
coin: match N::NETWORK {
NetworkId::Serai => panic!("test_wallet called with Serai"),
@ -116,7 +115,6 @@ pub async fn test_wallet<N: Network>(
plans[0].payments,
vec![Payment {
address: N::external_address(&network, key).await,
data: None,
balance: Balance {
coin: match N::NETWORK {
NetworkId::Serai => panic!("test_wallet called with Serai"),

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::{
@ -79,15 +79,11 @@ serai_test!(
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) },
instruction: OutInstruction { address: external_address },
}
};
};
let block = publish_tx(
&serai,

View file

@ -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)]

View file

@ -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)?;

View file

@ -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];

View file

@ -247,7 +247,6 @@ async fn sign_test() {
balance,
instruction: OutInstruction {
address: ExternalAddress::new(b"external".to_vec()).unwrap(),
data: None,
},
};
serai

View file

@ -493,7 +493,7 @@ async fn mint_and_burn_test() {
move |nonce, coin, amount, address| async move {
let out_instruction = OutInstructionWithBalance {
balance: Balance { coin, amount: Amount(amount) },
instruction: OutInstruction { address, data: None },
instruction: OutInstruction { address },
};
serai

View file

@ -246,7 +246,7 @@ fn send_test() {
},
block: substrate_block_num,
burns: vec![OutInstructionWithBalance {
instruction: OutInstruction { address: wallet.address(), data: None },
instruction: OutInstruction { address: wallet.address() },
balance: Balance { coin: balance_sent.coin, amount: amount_minted },
}],
batches: vec![batch.batch.id],