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:
akildemir 2022-12-09 18:58:11 +03:00 committed by GitHub
parent 9e82416e7d
commit d5a5704ba4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 128 additions and 34 deletions

View file

@ -10,6 +10,8 @@ use crate::serialize::{
write_point, write_vec,
};
pub const MAX_TX_EXTRA_NONCE_SIZE: usize = 255;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
pub(crate) enum PaymentId {
Unencrypted([u8; 32]),
@ -91,7 +93,7 @@ impl ExtraField {
1 => ExtraField::PublicKey(read_point(r)?),
2 => ExtraField::Nonce({
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"))?;
}
nonce
@ -131,8 +133,9 @@ impl Extra {
None
}
pub(crate) fn data(&self) -> Option<Vec<u8>> {
pub(crate) fn data(&self) -> Vec<Vec<u8>> {
let mut first = true;
let mut res = vec![];
for field in &self.0 {
if let ExtraField::Nonce(data) = field {
// Skip the first Nonce, which should be the payment ID
@ -140,10 +143,10 @@ impl Extra {
first = false;
continue;
}
return Some(data.clone());
res.push(data.clone());
}
}
None
res
}
pub(crate) fn new(mut keys: Vec<EdwardsPoint>) -> Extra {
@ -162,15 +165,15 @@ impl Extra {
}
#[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
(1 + 32) +
// PublicKeys, length, additional keys
(1 + 1 + (outputs.saturating_sub(1) * 32)) +
// PaymentId (Nonce), length, encrypted, ID
(1 + 1 + 1 + 8) +
// Nonce, length, data (if existant)
data.map(|v| 1 + varint_len(v.len()) + v.len()).unwrap_or(0)
// Nonce, length, data (if existent)
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<()> {

View file

@ -73,7 +73,7 @@ pub struct Metadata {
// have this making it simplest for it to be as-is.
pub payment_id: [u8; 8],
/// Arbitrary data encoded in TX extra.
pub arbitrary_data: Option<Vec<u8>>,
pub arbitrary_data: Vec<Vec<u8>>,
}
impl Metadata {
@ -82,11 +82,11 @@ impl Metadata {
res.extend(self.subaddress.0.to_le_bytes());
res.extend(self.subaddress.1.to_le_bytes());
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(data);
} else {
res.extend([0]);
res.extend(u32::try_from(self.arbitrary_data.len()).unwrap().to_le_bytes());
for part in &self.arbitrary_data {
res.extend([u8::try_from(part.len()).unwrap()]);
res.extend(part);
}
res
}
@ -96,12 +96,12 @@ impl Metadata {
subaddress: (read_u32(r)?, read_u32(r)?),
payment_id: read_bytes(r)?,
arbitrary_data: {
if read_byte(r)? == 1 {
let mut data = vec![];
for _ in 0 .. read_u32(r)? {
let len = read_byte(r)?;
Some(read_raw_vec(read_byte, usize::from(len), r)?)
} else {
None
data.push(read_raw_vec(read_byte, usize::from(len), r)?);
}
data
},
})
}
@ -128,6 +128,10 @@ impl ReceivedOutput {
self.data.commitment.clone()
}
pub fn arbitrary_data(&self) -> &[Vec<u8>] {
&self.metadata.arbitrary_data
}
pub fn serialize(&self) -> Vec<u8> {
let mut serialized = self.absolute.serialize();
serialized.extend(&self.data.serialize());

View file

@ -4,7 +4,10 @@ use zeroize::{Zeroize, ZeroizeOnDrop};
use crate::{
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)]
@ -15,14 +18,14 @@ struct SignableTransactionBuilderInternal {
inputs: Vec<SpendableOutput>,
payments: Vec<(MoneroAddress, u64)>,
change_address: Option<MoneroAddress>,
data: Option<Vec<u8>>,
data: Vec<Vec<u8>>,
}
impl SignableTransactionBuilderInternal {
// 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
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) {
@ -39,8 +42,8 @@ impl SignableTransactionBuilderInternal {
self.payments.extend(payments);
}
fn set_data(&mut self, data: Vec<u8>) {
self.data = Some(data);
fn add_data(&mut self, data: Vec<u8>) {
self.data.push(data);
}
}
@ -100,9 +103,12 @@ impl SignableTransactionBuilder {
self.shallow_copy()
}
pub fn set_data(&mut self, data: Vec<u8>) -> Self {
self.0.write().unwrap().set_data(data);
self.shallow_copy()
pub fn add_data(&mut self, data: Vec<u8>) -> Result<Self, TransactionError> {
if data.len() > MAX_TX_EXTRA_NONCE_SIZE {
Err(TransactionError::TooMuchData)?;
}
self.0.write().unwrap().add_data(data);
Ok(self.shallow_copy())
}
pub fn build(self) -> Result<SignableTransaction, TransactionError> {

View file

@ -24,7 +24,7 @@ use crate::{
rpc::{Rpc, RpcError},
wallet::{
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,
inputs: Vec<SpendableOutput>,
payments: Vec<(MoneroAddress, u64)>,
data: Option<Vec<u8>>,
data: Vec<Vec<u8>>,
fee: u64,
}
@ -191,7 +191,7 @@ impl SignableTransaction {
inputs: Vec<SpendableOutput>,
mut payments: Vec<(MoneroAddress, u64)>,
change_address: Option<MoneroAddress>,
data: Option<Vec<u8>>,
data: Vec<Vec<u8>>,
fee_rate: Fee,
) -> Result<SignableTransaction, TransactionError> {
// Make sure there's only one payment ID
@ -220,8 +220,10 @@ impl SignableTransaction {
Err(TransactionError::NoOutputs)?;
}
if data.as_ref().map(|v| v.len()).unwrap_or(0) > 255 {
Err(TransactionError::TooMuchData)?;
for part in &data {
if part.len() > MAX_TX_EXTRA_NONCE_SIZE {
Err(TransactionError::TooMuchData)?;
}
}
// TODO TX MAX SIZE
@ -313,8 +315,8 @@ impl SignableTransaction {
extra.push(ExtraField::Nonce(id_vec));
// Include data if present
if let Some(data) = self.data.take() {
extra.push(ExtraField::Nonce(data));
for part in self.data.drain(..) {
extra.push(ExtraField::Nonce(part));
}
let mut serialized = Vec::with_capacity(Extra::fee_weight(outputs.len(), self.data.as_ref()));

View 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 {},
),
);

View file

@ -178,7 +178,7 @@ impl Coin for Monero {
inputs.drain(..).map(|input| input.0).collect(),
payments.to_vec(),
Some(self.address(spend)),
None,
vec![],
fee,
)
.map_err(|_| CoinError::ConnectionError)?,
@ -261,7 +261,7 @@ impl Coin for Monero {
outputs,
vec![(address, amount - fee)],
Some(Self::empty_address()),
None,
vec![],
self.rpc.get_fee().await.unwrap(),
)
.unwrap()