use std_shims::{
vec,
vec::Vec,
io::{self, Read, Write},
};
use zeroize::{Zeroize, ZeroizeOnDrop};
use subtle::{Choice, ConstantTimeEq as _};
use crate::{
io::*,
ed25519::{Scalar, CompressedPoint, Point, Commitment},
transaction::Timelock,
address::SubaddressIndex,
extra::{MAX_ARBITRARY_DATA_SIZE, MAX_EXTRA_SIZE_BY_RELAY_RULE, PaymentId},
};
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub(crate) struct AbsoluteId {
pub(crate) transaction: [u8; 32],
pub(crate) index_in_transaction: u64,
}
impl core::fmt::Debug for AbsoluteId {
fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
fmt
.debug_struct("AbsoluteId")
.field("transaction", &hex::encode(self.transaction))
.field("index_in_transaction", &self.index_in_transaction)
.finish()
}
}
impl AbsoluteId {
fn ct_eq(&self, other: &Self) -> Choice {
self.transaction.ct_eq(&other.transaction) &
self.index_in_transaction.ct_eq(&other.index_in_transaction)
}
fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
w.write_all(&self.transaction)?;
w.write_all(&self.index_in_transaction.to_le_bytes())
}
fn read<R: Read>(r: &mut R) -> io::Result<AbsoluteId> {
Ok(AbsoluteId { transaction: read_bytes(r)?, index_in_transaction: read_u64(r)? })
}
}
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub(crate) struct RelativeId {
pub(crate) index_on_blockchain: u64,
}
impl core::fmt::Debug for RelativeId {
fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
fmt.debug_struct("RelativeId").field("index_on_blockchain", &self.index_on_blockchain).finish()
}
}
impl RelativeId {
fn ct_eq(&self, other: &Self) -> Choice {
self.index_on_blockchain.ct_eq(&other.index_on_blockchain)
}
fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
w.write_all(&self.index_on_blockchain.to_le_bytes())
}
fn read<R: Read>(r: &mut R) -> io::Result<Self> {
Ok(RelativeId { index_on_blockchain: read_u64(r)? })
}
}
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub(crate) struct OutputData {
pub(crate) key: Point,
pub(crate) key_offset: Scalar,
pub(crate) commitment: Commitment,
}
impl core::fmt::Debug for OutputData {
fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
fmt
.debug_struct("OutputData")
.field("key", &hex::encode(self.key.compress().to_bytes()))
.field("commitment", &self.commitment)
.finish_non_exhaustive()
}
}
impl OutputData {
pub(crate) fn ct_eq(&self, other: &Self) -> Choice {
self.key.ct_eq(&other.key) &
self.key_offset.ct_eq(&other.key_offset) &
self.commitment.ct_eq(&other.commitment)
}
pub(crate) fn key(&self) -> Point {
self.key
}
pub(crate) fn key_offset(&self) -> Scalar {
self.key_offset
}
pub(crate) fn commitment(&self) -> &Commitment {
&self.commitment
}
pub(crate) fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
w.write_all(&self.key.compress().to_bytes())?;
self.key_offset.write(w)?;
self.commitment.write(w)
}
pub(crate) fn read<R: Read>(r: &mut R) -> io::Result<OutputData> {
Ok(OutputData {
key: CompressedPoint::read(r)?
.decompress()
.ok_or_else(|| io::Error::other("output data included an invalid key"))?,
key_offset: Scalar::read(r)?,
commitment: Commitment::read(r)?,
})
}
}
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub(crate) struct Metadata {
pub(crate) additional_timelock: Timelock,
pub(crate) subaddress: Option<SubaddressIndex>,
pub(crate) payment_id: Option<PaymentId>,
pub(crate) arbitrary_data: Vec<Vec<u8>>,
}
impl core::fmt::Debug for Metadata {
fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
fmt
.debug_struct("Metadata")
.field("additional_timelock", &self.additional_timelock)
.field("subaddress", &self.subaddress)
.field("payment_id", &self.payment_id)
.field("arbitrary_data", &self.arbitrary_data.iter().map(hex::encode).collect::<Vec<_>>())
.finish()
}
}
impl Metadata {
fn eq(&self, other: &Self) -> bool {
(self.additional_timelock == other.additional_timelock) &&
(self.subaddress == other.subaddress) &&
(self.payment_id == other.payment_id) &&
(self.arbitrary_data == other.arbitrary_data)
}
fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
self.additional_timelock.write(w)?;
if let Some(subaddress) = self.subaddress {
w.write_all(&[1])?;
w.write_all(&subaddress.account().to_le_bytes())?;
w.write_all(&subaddress.address().to_le_bytes())?;
} else {
w.write_all(&[0])?;
}
if let Some(payment_id) = self.payment_id {
w.write_all(&[1])?;
payment_id.write(w)?;
} else {
w.write_all(&[0])?;
}
VarInt::write(&self.arbitrary_data.len(), w)?;
for part in &self.arbitrary_data {
#[expect(clippy::as_conversions)]
const _ASSERT_MAX_ARBITRARY_DATA_SIZE_FITS_WITHIN_U8: [();
(u8::MAX as usize) - MAX_ARBITRARY_DATA_SIZE] = [(); _];
w.write_all(&[
u8::try_from(part.len()).expect("piece of arbitrary data exceeded max length of u8::MAX")
])?;
w.write_all(part)?;
}
Ok(())
}
fn read<R: Read>(r: &mut R) -> io::Result<Metadata> {
let additional_timelock = Timelock::read(r)?;
let subaddress = match read_byte(r)? {
0 => None,
1 => Some(
SubaddressIndex::new(read_u32(r)?, read_u32(r)?)
.ok_or_else(|| io::Error::other("invalid subaddress in metadata"))?,
),
_ => Err(io::Error::other("invalid subaddress is_some boolean in metadata"))?,
};
Ok(Metadata {
additional_timelock,
subaddress,
payment_id: if read_byte(r)? == 1 { PaymentId::read(r).ok() } else { None },
arbitrary_data: {
let chunks = <usize as VarInt>::read(r)?;
if chunks > MAX_EXTRA_SIZE_BY_RELAY_RULE {
Err(io::Error::other(
"amount of arbitrary data chunks exceeded amount possible under policy",
))?;
}
let mut data = vec![];
let mut total_len = 0usize;
for _ in 0 .. chunks {
let len = read_byte(r)?;
let chunk = read_raw_vec(read_byte, usize::from(len), r)?;
total_len = total_len.saturating_add(chunk.len());
if total_len > MAX_EXTRA_SIZE_BY_RELAY_RULE {
Err(io::Error::other("amount of arbitrary data exceeded amount allowed by policy"))?;
}
data.push(chunk);
}
data
},
})
}
}
#[derive(Clone, Debug, Zeroize, ZeroizeOnDrop)]
pub struct WalletOutput {
pub(crate) absolute_id: AbsoluteId,
pub(crate) relative_id: RelativeId,
pub(crate) data: OutputData,
pub(crate) metadata: Metadata,
}
impl PartialEq for WalletOutput {
fn eq(&self, other: &Self) -> bool {
bool::from(
self.absolute_id.ct_eq(&other.absolute_id) &
self.relative_id.ct_eq(&other.relative_id) &
self.data.ct_eq(&other.data),
) & self.metadata.eq(&other.metadata)
}
}
impl Eq for WalletOutput {}
impl WalletOutput {
pub fn transaction(&self) -> [u8; 32] {
self.absolute_id.transaction
}
pub fn index_in_transaction(&self) -> u64 {
self.absolute_id.index_in_transaction
}
pub fn index_on_blockchain(&self) -> u64 {
self.relative_id.index_on_blockchain
}
pub fn key(&self) -> Point {
self.data.key()
}
pub fn key_offset(&self) -> Scalar {
self.data.key_offset()
}
pub fn commitment(&self) -> &Commitment {
self.data.commitment()
}
pub fn additional_timelock(&self) -> Timelock {
self.metadata.additional_timelock
}
pub fn subaddress(&self) -> Option<SubaddressIndex> {
self.metadata.subaddress
}
pub fn payment_id(&self) -> Option<PaymentId> {
self.metadata.payment_id
}
pub fn arbitrary_data(&self) -> &[Vec<u8>] {
&self.metadata.arbitrary_data
}
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
self.absolute_id.write(w)?;
self.relative_id.write(w)?;
self.data.write(w)?;
self.metadata.write(w)
}
pub fn serialize(&self) -> Vec<u8> {
let mut serialized = Vec::with_capacity(128);
self.write(&mut serialized).expect("write failed but <Vec as io::Write> doesn't fail");
serialized
}
pub fn read<R: Read>(r: &mut R) -> io::Result<WalletOutput> {
Ok(WalletOutput {
absolute_id: AbsoluteId::read(r)?,
relative_id: RelativeId::read(r)?,
data: OutputData::read(r)?,
metadata: Metadata::read(r)?,
})
}
}