mirror of
https://github.com/serai-dex/serai.git
synced 2024-12-23 03:59:22 +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:
parent
39be23d807
commit
eb9bce6862
18 changed files with 121 additions and 150 deletions
|
@ -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));
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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<_>>(),
|
||||
),
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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::{
|
||||
|
@ -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,
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -247,7 +247,6 @@ async fn sign_test() {
|
|||
balance,
|
||||
instruction: OutInstruction {
|
||||
address: ExternalAddress::new(b"external".to_vec()).unwrap(),
|
||||
data: None,
|
||||
},
|
||||
};
|
||||
serai
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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],
|
||||
|
|
Loading…
Reference in a new issue