mirror of
https://github.com/serai-dex/serai.git
synced 2024-11-17 01:17:36 +00:00
support add/read multiple arbitrary tx data (#189)
* support add/read multiple arbitrary tx data * fix clippy errors * resolve pr issues
This commit is contained in:
parent
9e82416e7d
commit
d5a5704ba4
6 changed files with 128 additions and 34 deletions
|
@ -10,6 +10,8 @@ use crate::serialize::{
|
||||||
write_point, write_vec,
|
write_point, write_vec,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const MAX_TX_EXTRA_NONCE_SIZE: usize = 255;
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
||||||
pub(crate) enum PaymentId {
|
pub(crate) enum PaymentId {
|
||||||
Unencrypted([u8; 32]),
|
Unencrypted([u8; 32]),
|
||||||
|
@ -91,7 +93,7 @@ impl ExtraField {
|
||||||
1 => ExtraField::PublicKey(read_point(r)?),
|
1 => ExtraField::PublicKey(read_point(r)?),
|
||||||
2 => ExtraField::Nonce({
|
2 => ExtraField::Nonce({
|
||||||
let nonce = read_vec(read_byte, r)?;
|
let nonce = read_vec(read_byte, r)?;
|
||||||
if nonce.len() > 255 {
|
if nonce.len() > MAX_TX_EXTRA_NONCE_SIZE {
|
||||||
Err(io::Error::new(io::ErrorKind::Other, "too long nonce"))?;
|
Err(io::Error::new(io::ErrorKind::Other, "too long nonce"))?;
|
||||||
}
|
}
|
||||||
nonce
|
nonce
|
||||||
|
@ -131,8 +133,9 @@ impl Extra {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn data(&self) -> Option<Vec<u8>> {
|
pub(crate) fn data(&self) -> Vec<Vec<u8>> {
|
||||||
let mut first = true;
|
let mut first = true;
|
||||||
|
let mut res = vec![];
|
||||||
for field in &self.0 {
|
for field in &self.0 {
|
||||||
if let ExtraField::Nonce(data) = field {
|
if let ExtraField::Nonce(data) = field {
|
||||||
// Skip the first Nonce, which should be the payment ID
|
// Skip the first Nonce, which should be the payment ID
|
||||||
|
@ -140,10 +143,10 @@ impl Extra {
|
||||||
first = false;
|
first = false;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
return Some(data.clone());
|
res.push(data.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn new(mut keys: Vec<EdwardsPoint>) -> Extra {
|
pub(crate) fn new(mut keys: Vec<EdwardsPoint>) -> Extra {
|
||||||
|
@ -162,15 +165,15 @@ impl Extra {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
pub(crate) fn fee_weight(outputs: usize, data: Option<&Vec<u8>>) -> usize {
|
pub(crate) fn fee_weight(outputs: usize, data: &[Vec<u8>]) -> usize {
|
||||||
// PublicKey, key
|
// PublicKey, key
|
||||||
(1 + 32) +
|
(1 + 32) +
|
||||||
// PublicKeys, length, additional keys
|
// PublicKeys, length, additional keys
|
||||||
(1 + 1 + (outputs.saturating_sub(1) * 32)) +
|
(1 + 1 + (outputs.saturating_sub(1) * 32)) +
|
||||||
// PaymentId (Nonce), length, encrypted, ID
|
// PaymentId (Nonce), length, encrypted, ID
|
||||||
(1 + 1 + 1 + 8) +
|
(1 + 1 + 1 + 8) +
|
||||||
// Nonce, length, data (if existant)
|
// Nonce, length, data (if existent)
|
||||||
data.map(|v| 1 + varint_len(v.len()) + v.len()).unwrap_or(0)
|
data.iter().map(|v| 1 + varint_len(v.len()) + v.len()).sum::<usize>()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn serialize<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
pub(crate) fn serialize<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
||||||
|
|
|
@ -73,7 +73,7 @@ pub struct Metadata {
|
||||||
// have this making it simplest for it to be as-is.
|
// have this making it simplest for it to be as-is.
|
||||||
pub payment_id: [u8; 8],
|
pub payment_id: [u8; 8],
|
||||||
/// Arbitrary data encoded in TX extra.
|
/// Arbitrary data encoded in TX extra.
|
||||||
pub arbitrary_data: Option<Vec<u8>>,
|
pub arbitrary_data: Vec<Vec<u8>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Metadata {
|
impl Metadata {
|
||||||
|
@ -82,11 +82,11 @@ impl Metadata {
|
||||||
res.extend(self.subaddress.0.to_le_bytes());
|
res.extend(self.subaddress.0.to_le_bytes());
|
||||||
res.extend(self.subaddress.1.to_le_bytes());
|
res.extend(self.subaddress.1.to_le_bytes());
|
||||||
res.extend(self.payment_id);
|
res.extend(self.payment_id);
|
||||||
if let Some(data) = self.arbitrary_data.as_ref() {
|
|
||||||
res.extend([1, u8::try_from(data.len()).unwrap()]);
|
res.extend(u32::try_from(self.arbitrary_data.len()).unwrap().to_le_bytes());
|
||||||
res.extend(data);
|
for part in &self.arbitrary_data {
|
||||||
} else {
|
res.extend([u8::try_from(part.len()).unwrap()]);
|
||||||
res.extend([0]);
|
res.extend(part);
|
||||||
}
|
}
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
@ -96,12 +96,12 @@ impl Metadata {
|
||||||
subaddress: (read_u32(r)?, read_u32(r)?),
|
subaddress: (read_u32(r)?, read_u32(r)?),
|
||||||
payment_id: read_bytes(r)?,
|
payment_id: read_bytes(r)?,
|
||||||
arbitrary_data: {
|
arbitrary_data: {
|
||||||
if read_byte(r)? == 1 {
|
let mut data = vec![];
|
||||||
|
for _ in 0 .. read_u32(r)? {
|
||||||
let len = read_byte(r)?;
|
let len = read_byte(r)?;
|
||||||
Some(read_raw_vec(read_byte, usize::from(len), r)?)
|
data.push(read_raw_vec(read_byte, usize::from(len), r)?);
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
data
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -128,6 +128,10 @@ impl ReceivedOutput {
|
||||||
self.data.commitment.clone()
|
self.data.commitment.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn arbitrary_data(&self) -> &[Vec<u8>] {
|
||||||
|
&self.metadata.arbitrary_data
|
||||||
|
}
|
||||||
|
|
||||||
pub fn serialize(&self) -> Vec<u8> {
|
pub fn serialize(&self) -> Vec<u8> {
|
||||||
let mut serialized = self.absolute.serialize();
|
let mut serialized = self.absolute.serialize();
|
||||||
serialized.extend(&self.data.serialize());
|
serialized.extend(&self.data.serialize());
|
||||||
|
|
|
@ -4,7 +4,10 @@ use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Protocol,
|
Protocol,
|
||||||
wallet::{address::MoneroAddress, Fee, SpendableOutput, SignableTransaction, TransactionError},
|
wallet::{
|
||||||
|
address::MoneroAddress, Fee, SpendableOutput, SignableTransaction, TransactionError,
|
||||||
|
extra::MAX_TX_EXTRA_NONCE_SIZE,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
|
#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
|
||||||
|
@ -15,14 +18,14 @@ struct SignableTransactionBuilderInternal {
|
||||||
inputs: Vec<SpendableOutput>,
|
inputs: Vec<SpendableOutput>,
|
||||||
payments: Vec<(MoneroAddress, u64)>,
|
payments: Vec<(MoneroAddress, u64)>,
|
||||||
change_address: Option<MoneroAddress>,
|
change_address: Option<MoneroAddress>,
|
||||||
data: Option<Vec<u8>>,
|
data: Vec<Vec<u8>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SignableTransactionBuilderInternal {
|
impl SignableTransactionBuilderInternal {
|
||||||
// Takes in the change address so users don't miss that they have to manually set one
|
// Takes in the change address so users don't miss that they have to manually set one
|
||||||
// If they don't, all leftover funds will become part of the fee
|
// If they don't, all leftover funds will become part of the fee
|
||||||
fn new(protocol: Protocol, fee: Fee, change_address: Option<MoneroAddress>) -> Self {
|
fn new(protocol: Protocol, fee: Fee, change_address: Option<MoneroAddress>) -> Self {
|
||||||
Self { protocol, fee, inputs: vec![], payments: vec![], change_address, data: None }
|
Self { protocol, fee, inputs: vec![], payments: vec![], change_address, data: vec![] }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_input(&mut self, input: SpendableOutput) {
|
fn add_input(&mut self, input: SpendableOutput) {
|
||||||
|
@ -39,8 +42,8 @@ impl SignableTransactionBuilderInternal {
|
||||||
self.payments.extend(payments);
|
self.payments.extend(payments);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_data(&mut self, data: Vec<u8>) {
|
fn add_data(&mut self, data: Vec<u8>) {
|
||||||
self.data = Some(data);
|
self.data.push(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,9 +103,12 @@ impl SignableTransactionBuilder {
|
||||||
self.shallow_copy()
|
self.shallow_copy()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_data(&mut self, data: Vec<u8>) -> Self {
|
pub fn add_data(&mut self, data: Vec<u8>) -> Result<Self, TransactionError> {
|
||||||
self.0.write().unwrap().set_data(data);
|
if data.len() > MAX_TX_EXTRA_NONCE_SIZE {
|
||||||
self.shallow_copy()
|
Err(TransactionError::TooMuchData)?;
|
||||||
|
}
|
||||||
|
self.0.write().unwrap().add_data(data);
|
||||||
|
Ok(self.shallow_copy())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build(self) -> Result<SignableTransaction, TransactionError> {
|
pub fn build(self) -> Result<SignableTransaction, TransactionError> {
|
||||||
|
|
|
@ -24,7 +24,7 @@ use crate::{
|
||||||
rpc::{Rpc, RpcError},
|
rpc::{Rpc, RpcError},
|
||||||
wallet::{
|
wallet::{
|
||||||
address::MoneroAddress, SpendableOutput, Decoys, PaymentId, ExtraField, Extra, key_image_sort,
|
address::MoneroAddress, SpendableOutput, Decoys, PaymentId, ExtraField, Extra, key_image_sort,
|
||||||
uniqueness, shared_key, commitment_mask, amount_encryption,
|
uniqueness, shared_key, commitment_mask, amount_encryption, extra::MAX_TX_EXTRA_NONCE_SIZE,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -177,7 +177,7 @@ pub struct SignableTransaction {
|
||||||
protocol: Protocol,
|
protocol: Protocol,
|
||||||
inputs: Vec<SpendableOutput>,
|
inputs: Vec<SpendableOutput>,
|
||||||
payments: Vec<(MoneroAddress, u64)>,
|
payments: Vec<(MoneroAddress, u64)>,
|
||||||
data: Option<Vec<u8>>,
|
data: Vec<Vec<u8>>,
|
||||||
fee: u64,
|
fee: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -191,7 +191,7 @@ impl SignableTransaction {
|
||||||
inputs: Vec<SpendableOutput>,
|
inputs: Vec<SpendableOutput>,
|
||||||
mut payments: Vec<(MoneroAddress, u64)>,
|
mut payments: Vec<(MoneroAddress, u64)>,
|
||||||
change_address: Option<MoneroAddress>,
|
change_address: Option<MoneroAddress>,
|
||||||
data: Option<Vec<u8>>,
|
data: Vec<Vec<u8>>,
|
||||||
fee_rate: Fee,
|
fee_rate: Fee,
|
||||||
) -> Result<SignableTransaction, TransactionError> {
|
) -> Result<SignableTransaction, TransactionError> {
|
||||||
// Make sure there's only one payment ID
|
// Make sure there's only one payment ID
|
||||||
|
@ -220,8 +220,10 @@ impl SignableTransaction {
|
||||||
Err(TransactionError::NoOutputs)?;
|
Err(TransactionError::NoOutputs)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.as_ref().map(|v| v.len()).unwrap_or(0) > 255 {
|
for part in &data {
|
||||||
Err(TransactionError::TooMuchData)?;
|
if part.len() > MAX_TX_EXTRA_NONCE_SIZE {
|
||||||
|
Err(TransactionError::TooMuchData)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO TX MAX SIZE
|
// TODO TX MAX SIZE
|
||||||
|
@ -313,8 +315,8 @@ impl SignableTransaction {
|
||||||
extra.push(ExtraField::Nonce(id_vec));
|
extra.push(ExtraField::Nonce(id_vec));
|
||||||
|
|
||||||
// Include data if present
|
// Include data if present
|
||||||
if let Some(data) = self.data.take() {
|
for part in self.data.drain(..) {
|
||||||
extra.push(ExtraField::Nonce(data));
|
extra.push(ExtraField::Nonce(part));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut serialized = Vec::with_capacity(Extra::fee_weight(outputs.len(), self.data.as_ref()));
|
let mut serialized = Vec::with_capacity(Extra::fee_weight(outputs.len(), self.data.as_ref()));
|
||||||
|
|
79
coins/monero/tests/add_data.rs
Normal file
79
coins/monero/tests/add_data.rs
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
use monero_serai::{rpc::Rpc, wallet::TransactionError, transaction::Transaction};
|
||||||
|
|
||||||
|
mod runner;
|
||||||
|
|
||||||
|
test!(
|
||||||
|
add_single_data_less_than_255,
|
||||||
|
(
|
||||||
|
|_, mut builder: Builder, addr| async move {
|
||||||
|
// make a data that is less than 255 bytes
|
||||||
|
let arbitrary_data = Vec::from("this is an arbitrary data less than 255 bytes");
|
||||||
|
|
||||||
|
// make sure we can add to tx
|
||||||
|
let result = builder.add_data(arbitrary_data.clone());
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
builder.add_payment(addr, 5);
|
||||||
|
(builder.build().unwrap(), (arbitrary_data,))
|
||||||
|
},
|
||||||
|
|rpc: Rpc, signed: Transaction, mut scanner: Scanner, state: (Vec<u8>,)| async move {
|
||||||
|
let tx = rpc.get_transaction(signed.hash()).await.unwrap();
|
||||||
|
let output = scanner.scan_transaction(&tx).not_locked().swap_remove(0);
|
||||||
|
assert_eq!(output.commitment().amount, 5);
|
||||||
|
assert_eq!(output.arbitrary_data()[0], state.0);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
test!(
|
||||||
|
add_multiple_data_less_than_255,
|
||||||
|
(
|
||||||
|
|_, mut builder: Builder, addr| async move {
|
||||||
|
// make a data that is less than 255 bytes
|
||||||
|
let arbitrary_data = Vec::from("this is an arbitrary data less than 255 bytes");
|
||||||
|
|
||||||
|
// add tx multiple times
|
||||||
|
for _ in 0 .. 5 {
|
||||||
|
let result = builder.add_data(arbitrary_data.clone());
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.add_payment(addr, 5);
|
||||||
|
(builder.build().unwrap(), (arbitrary_data,))
|
||||||
|
},
|
||||||
|
|rpc: Rpc, signed: Transaction, mut scanner: Scanner, state: (Vec<u8>,)| async move {
|
||||||
|
let tx = rpc.get_transaction(signed.hash()).await.unwrap();
|
||||||
|
let output = scanner.scan_transaction(&tx).not_locked().swap_remove(0);
|
||||||
|
assert_eq!(output.commitment().amount, 5);
|
||||||
|
let data = output.arbitrary_data();
|
||||||
|
for i in 0 .. 5 {
|
||||||
|
assert_eq!(data[i], state.0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
test!(
|
||||||
|
add_single_data_more_than_255,
|
||||||
|
(
|
||||||
|
|_, mut builder: Builder, addr| async move {
|
||||||
|
// make a data that is bigger than 255 bytes
|
||||||
|
let mut arbitrary_data = vec![];
|
||||||
|
for _ in 0 .. 256 {
|
||||||
|
arbitrary_data.push(b'a');
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure we get an error if we try to add it to tx
|
||||||
|
let mut result = builder.add_payment(addr, 5).add_data(arbitrary_data.clone());
|
||||||
|
assert_eq!(result, Err(TransactionError::TooMuchData));
|
||||||
|
|
||||||
|
// reduce data size and re-try
|
||||||
|
arbitrary_data.swap_remove(0);
|
||||||
|
result = builder.add_data(arbitrary_data);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
(builder.build().unwrap(), ())
|
||||||
|
},
|
||||||
|
|_, _, _, _| async move {},
|
||||||
|
),
|
||||||
|
);
|
|
@ -178,7 +178,7 @@ impl Coin for Monero {
|
||||||
inputs.drain(..).map(|input| input.0).collect(),
|
inputs.drain(..).map(|input| input.0).collect(),
|
||||||
payments.to_vec(),
|
payments.to_vec(),
|
||||||
Some(self.address(spend)),
|
Some(self.address(spend)),
|
||||||
None,
|
vec![],
|
||||||
fee,
|
fee,
|
||||||
)
|
)
|
||||||
.map_err(|_| CoinError::ConnectionError)?,
|
.map_err(|_| CoinError::ConnectionError)?,
|
||||||
|
@ -261,7 +261,7 @@ impl Coin for Monero {
|
||||||
outputs,
|
outputs,
|
||||||
vec![(address, amount - fee)],
|
vec![(address, amount - fee)],
|
||||||
Some(Self::empty_address()),
|
Some(Self::empty_address()),
|
||||||
None,
|
vec![],
|
||||||
self.rpc.get_fee().await.unwrap(),
|
self.rpc.get_fee().await.unwrap(),
|
||||||
)
|
)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
|
Loading…
Reference in a new issue