chore: add bounded_vec to ledger tx builder inputs and outputs (#2791)

Will address outstanding _Copilot_ comments in a separate PR.
This commit is contained in:
Hansie Odendaal
2026-05-29 09:50:53 +02:00
committed by GitHub
parent 49daa70f62
commit ecc66fc23a
42 changed files with 675 additions and 461 deletions
+184 -152
View File
@@ -1,13 +1,14 @@
use std::fmt::{Debug, Formatter};
use lb_key_management_system_keys::keys::Ed25519Signature;
use lb_utils::bounded_vec::BoundedError;
use crate::{
block::Block,
header::Header,
mantle::{
MantleTx, Note, Op, OpProof, SignedMantleTx,
encoding::Ops,
encoding::{BoundedOutputs, Ops},
genesis_tx::{self, GenesisTx},
ledger::{Inputs, Outputs},
ops::{channel::inscribe::InscriptionOp, sdp::SDPDeclareOp, transfer::TransferOp},
@@ -27,12 +28,62 @@ pub enum Error {
/// unsupported ops).
#[error("Invalid genesis transaction: {0}")]
InvalidGenesisTx(#[from] genesis_tx::Error),
#[error("add_notes called with empty iterator")]
EmptyNotes,
#[error("too many notes for genesis transfer outputs: attempted {actual}, max {max}")]
TooManyNotes { actual: usize, max: usize },
}
/// Convenience [`Result`](core::result::Result) alias for genesis block
/// construction.
pub type Result<T> = core::result::Result<T, Error>;
const fn map_notes_bounded_error(error: &BoundedError) -> Error {
match error {
BoundedError::TooLong { actual, max } => Error::TooManyNotes {
actual: *actual,
max: *max,
},
BoundedError::EmptyInput => Error::EmptyNotes,
}
}
fn collect_non_empty_notes<I, N>(notes: I) -> Result<BoundedOutputs>
where
I: IntoIterator<Item = N>,
N: Into<Note>,
{
let notes: Vec<Note> = notes.into_iter().map(Into::into).collect();
if notes.is_empty() {
return Err(Error::EmptyNotes);
}
BoundedOutputs::try_from(notes).map_err(|error| map_notes_bounded_error(&error))
}
fn push_note(mut notes: BoundedOutputs, note: Note) -> Result<BoundedOutputs> {
notes
.try_push(note)
.map_err(|error| map_notes_bounded_error(&error))?;
Ok(notes)
}
fn extend_non_empty_notes<I, N>(mut existing: BoundedOutputs, notes: I) -> Result<BoundedOutputs>
where
I: IntoIterator<Item = N>,
N: Into<Note>,
{
let mut iter = notes.into_iter().peekable();
if iter.peek().is_none() {
return Err(Error::EmptyNotes);
}
for note in iter.map(Into::into) {
existing
.try_push(note)
.map_err(|error| map_notes_bounded_error(&error))?;
}
Ok(existing)
}
/// A [`Block`] whose transactions are all [`GenesisTx`] values.
///
/// The block carries a sentinel
@@ -78,7 +129,7 @@ pub struct WithGenesisTx {
/// Typestate marker: builder has genesis transfer output notes only.
pub struct WithNotes {
notes: Vec<Note>,
notes: BoundedOutputs,
}
/// Typestate marker: builder has a genesis inscription only.
@@ -93,13 +144,13 @@ pub struct WithDeclarations {
/// Typestate marker: builder has genesis notes and an inscription.
pub struct WithNotesAndInscription {
notes: Vec<Note>,
notes: BoundedOutputs,
inscription: InscriptionOp,
}
/// Typestate marker: builder has genesis notes and SDP declarations.
pub struct WithNotesAndDeclarations {
notes: Vec<Note>,
notes: BoundedOutputs,
sdp_declarations: Vec<SDPDeclareOp>,
}
@@ -117,7 +168,7 @@ pub struct WithInscriptionAndDeclarations {
/// [`GenesisTx`] — notes, an inscription, and at least one SDP declaration.
/// This is the only state that exposes [`GenesisBlockBuilder::build`].
pub struct WithAll {
notes: Vec<Note>,
notes: BoundedOutputs,
inscription: InscriptionOp,
sdp_declarations: Vec<SDPDeclareOp>,
}
@@ -197,29 +248,32 @@ impl GenesisBlockBuilder<Empty> {
#[must_use]
pub fn add_note(self, note: Note) -> GenesisBlockBuilder<WithNotes> {
GenesisBlockBuilder {
state: WithNotes { notes: vec![note] },
state: WithNotes {
notes: [note].into(),
},
}
}
/// Try add multiple genesis transfer output notes at once, transitioning to
/// [`WithNotes`].
pub fn try_add_notes(
self,
notes: impl IntoIterator<Item = impl Into<Note>>,
) -> Result<GenesisBlockBuilder<WithNotes>> {
let notes = collect_non_empty_notes(notes)?;
Ok(GenesisBlockBuilder {
state: WithNotes { notes },
})
}
/// Add multiple genesis transfer output notes at once, transitioning to
/// [`WithNotes`].
///
/// # Panics
///
/// Panics if `notes` is empty.
#[must_use]
pub fn add_notes(
self,
notes: impl IntoIterator<Item = impl Into<Note>>,
) -> GenesisBlockBuilder<WithNotes> {
let mut iter = notes.into_iter().peekable();
assert!(
iter.peek().is_some(),
"add_notes called with empty iterator"
);
pub fn add_notes<const N: usize>(self, notes: [Note; N]) -> GenesisBlockBuilder<WithNotes> {
GenesisBlockBuilder {
state: WithNotes {
notes: iter.map(Into::into).collect(),
notes: notes.into(),
},
}
}
@@ -278,36 +332,28 @@ impl GenesisBlockBuilder<Empty> {
impl GenesisBlockBuilder<WithNotes> {
/// Append another genesis transfer output note.
#[must_use]
pub fn add_note(self, note: Note) -> Self {
pub fn try_add_note(self, note: Note) -> Result<Self> {
let Self {
state: WithNotes { mut notes },
} = self;
notes.push(note);
Self {
notes = push_note(notes, note)?;
Ok(Self {
state: WithNotes { notes },
}
})
}
/// Append multiple genesis transfer output notes at once.
///
/// # Panics
///
/// Panics if `notes` is empty.
#[must_use]
pub fn add_notes(self, notes: impl IntoIterator<Item = impl Into<Note>>) -> Self {
let mut iter = notes.into_iter().peekable();
assert!(
iter.peek().is_some(),
"add_notes called with empty iterator"
);
/// Try append multiple genesis transfer output notes at once.
pub fn try_add_notes(
self,
notes_to_add: impl IntoIterator<Item = impl Into<Note>>,
) -> Result<Self> {
let Self {
state: WithNotes { mut notes },
} = self;
notes.extend(iter.map(Into::into));
Self {
notes = extend_non_empty_notes(notes, notes_to_add)?;
Ok(Self {
state: WithNotes { notes },
}
})
}
/// Set the genesis inscription, transitioning to
@@ -384,34 +430,42 @@ impl GenesisBlockBuilder<WithInscription> {
} = self;
GenesisBlockBuilder {
state: WithNotesAndInscription {
notes: vec![note],
notes: [note].into(),
inscription,
},
}
}
/// Add multiple genesis transfer output notes at once, transitioning to
/// Try add multiple genesis transfer output notes at once, transitioning to
/// [`WithNotesAndInscription`].
///
/// # Panics
///
/// Panics if `notes` is empty.
#[must_use]
pub fn add_notes(
pub fn try_add_notes(
self,
notes: impl IntoIterator<Item = impl Into<Note>>,
) -> Result<GenesisBlockBuilder<WithNotesAndInscription>> {
let Self {
state: WithInscription { inscription },
} = self;
Ok(GenesisBlockBuilder {
state: WithNotesAndInscription {
notes: collect_non_empty_notes(notes)?,
inscription,
},
})
}
/// Add multiple genesis transfer output notes at once, transitioning to
/// [`WithNotesAndInscription`].
#[must_use]
pub fn add_notes<const N: usize>(
self,
notes: [Note; N],
) -> GenesisBlockBuilder<WithNotesAndInscription> {
let mut iter = notes.into_iter().peekable();
assert!(
iter.peek().is_some(),
"add_notes called with empty iterator"
);
let Self {
state: WithInscription { inscription },
} = self;
GenesisBlockBuilder {
state: WithNotesAndInscription {
notes: iter.map(Into::into).collect(),
notes: notes.into(),
inscription,
},
}
@@ -484,34 +538,42 @@ impl GenesisBlockBuilder<WithDeclarations> {
} = self;
GenesisBlockBuilder {
state: WithNotesAndDeclarations {
notes: vec![note],
notes: [note].into(),
sdp_declarations,
},
}
}
/// Add multiple genesis transfer output notes at once, transitioning to
/// Try add multiple genesis transfer output notes at once, transitioning to
/// [`WithNotesAndDeclarations`].
///
/// # Panics
///
/// Panics if `notes` is empty.
#[must_use]
pub fn add_notes(
pub fn try_add_notes(
self,
notes: impl IntoIterator<Item = impl Into<Note>>,
) -> Result<GenesisBlockBuilder<WithNotesAndDeclarations>> {
let Self {
state: WithDeclarations { sdp_declarations },
} = self;
Ok(GenesisBlockBuilder {
state: WithNotesAndDeclarations {
notes: collect_non_empty_notes(notes)?,
sdp_declarations,
},
})
}
/// Add multiple genesis transfer output notes at once, transitioning to
/// [`WithNotesAndDeclarations`].
#[must_use]
pub fn add_notes<const N: usize>(
self,
notes: [Note; N],
) -> GenesisBlockBuilder<WithNotesAndDeclarations> {
let mut iter = notes.into_iter().peekable();
assert!(
iter.peek().is_some(),
"add_notes called with empty iterator"
);
let Self {
state: WithDeclarations { sdp_declarations },
} = self;
GenesisBlockBuilder {
state: WithNotesAndDeclarations {
notes: iter.map(Into::into).collect(),
notes: notes.into(),
sdp_declarations,
},
}
@@ -581,8 +643,7 @@ impl GenesisBlockBuilder<WithDeclarations> {
impl GenesisBlockBuilder<WithNotesAndInscription> {
/// Append another genesis transfer output note.
#[must_use]
pub fn add_note(self, note: Note) -> Self {
pub fn add_note(self, note: Note) -> Result<Self> {
let Self {
state:
WithNotesAndInscription {
@@ -590,24 +651,17 @@ impl GenesisBlockBuilder<WithNotesAndInscription> {
inscription,
},
} = self;
notes.push(note);
Self {
notes = push_note(notes, note)?;
Ok(Self {
state: WithNotesAndInscription { notes, inscription },
}
})
}
/// Append multiple genesis transfer output notes at once.
///
/// # Panics
///
/// Panics if `notes` is empty.
#[must_use]
pub fn add_notes(self, notes: impl IntoIterator<Item = impl Into<Note>>) -> Self {
let mut iter = notes.into_iter().peekable();
assert!(
iter.peek().is_some(),
"add_notes called with empty iterator"
);
pub fn add_notes(
self,
notes_to_add: impl IntoIterator<Item = impl Into<Note>>,
) -> Result<Self> {
let Self {
state:
WithNotesAndInscription {
@@ -615,10 +669,10 @@ impl GenesisBlockBuilder<WithNotesAndInscription> {
inscription,
},
} = self;
notes.extend(iter.map(Into::into));
Self {
notes = extend_non_empty_notes(notes, notes_to_add)?;
Ok(Self {
state: WithNotesAndInscription { notes, inscription },
}
})
}
/// Replace the current inscription.
@@ -695,8 +749,7 @@ impl GenesisBlockBuilder<WithNotesAndInscription> {
impl GenesisBlockBuilder<WithNotesAndDeclarations> {
/// Append another genesis transfer output note.
#[must_use]
pub fn add_note(self, note: Note) -> Self {
pub fn add_note(self, note: Note) -> Result<Self> {
let Self {
state:
WithNotesAndDeclarations {
@@ -704,27 +757,20 @@ impl GenesisBlockBuilder<WithNotesAndDeclarations> {
sdp_declarations,
},
} = self;
notes.push(note);
Self {
notes = push_note(notes, note)?;
Ok(Self {
state: WithNotesAndDeclarations {
notes,
sdp_declarations,
},
}
})
}
/// Append multiple genesis transfer output notes at once.
///
/// # Panics
///
/// Panics if `notes` is empty.
#[must_use]
pub fn add_notes(self, notes: impl IntoIterator<Item = impl Into<Note>>) -> Self {
let mut iter = notes.into_iter().peekable();
assert!(
iter.peek().is_some(),
"add_notes called with empty iterator"
);
pub fn add_notes(
self,
notes_to_add: impl IntoIterator<Item = impl Into<Note>>,
) -> Result<Self> {
let Self {
state:
WithNotesAndDeclarations {
@@ -732,13 +778,13 @@ impl GenesisBlockBuilder<WithNotesAndDeclarations> {
sdp_declarations,
},
} = self;
notes.extend(iter.map(Into::into));
Self {
notes = extend_non_empty_notes(notes, notes_to_add)?;
Ok(Self {
state: WithNotesAndDeclarations {
notes,
sdp_declarations,
},
}
})
}
/// Set the genesis inscription, completing all three pieces and
@@ -829,7 +875,7 @@ impl GenesisBlockBuilder<WithInscriptionAndDeclarations> {
} = self;
GenesisBlockBuilder {
state: WithAll {
notes: vec![note],
notes: [note].into(),
inscription,
sdp_declarations,
},
@@ -842,16 +888,10 @@ impl GenesisBlockBuilder<WithInscriptionAndDeclarations> {
/// # Panics
///
/// Panics if `notes` is empty.
#[must_use]
pub fn add_notes(
self,
notes: impl IntoIterator<Item = impl Into<Note>>,
) -> GenesisBlockBuilder<WithAll> {
let mut iter = notes.into_iter().peekable();
assert!(
iter.peek().is_some(),
"add_notes called with empty iterator"
);
) -> Result<GenesisBlockBuilder<WithAll>> {
let Self {
state:
WithInscriptionAndDeclarations {
@@ -859,13 +899,13 @@ impl GenesisBlockBuilder<WithInscriptionAndDeclarations> {
sdp_declarations,
},
} = self;
GenesisBlockBuilder {
Ok(GenesisBlockBuilder {
state: WithAll {
notes: iter.map(Into::into).collect(),
notes: collect_non_empty_notes(notes)?,
inscription,
sdp_declarations,
},
}
})
}
/// Replace the current inscription.
@@ -941,8 +981,7 @@ impl GenesisBlockBuilder<WithInscriptionAndDeclarations> {
impl GenesisBlockBuilder<WithAll> {
/// Append another genesis transfer output note.
#[must_use]
pub fn add_note(self, note: Note) -> Self {
pub fn add_note(self, note: Note) -> Result<Self> {
let Self {
state:
WithAll {
@@ -951,28 +990,21 @@ impl GenesisBlockBuilder<WithAll> {
sdp_declarations,
},
} = self;
notes.push(note);
Self {
notes = push_note(notes, note)?;
Ok(Self {
state: WithAll {
notes,
inscription,
sdp_declarations,
},
}
})
}
/// Append multiple genesis transfer output notes at once.
///
/// # Panics
///
/// Panics if `notes` is empty.
#[must_use]
pub fn add_notes(self, notes: impl IntoIterator<Item = impl Into<Note>>) -> Self {
let mut iter = notes.into_iter().peekable();
assert!(
iter.peek().is_some(),
"add_notes called with empty iterator"
);
pub fn add_notes(
self,
notes_to_add: impl IntoIterator<Item = impl Into<Note>>,
) -> Result<Self> {
let Self {
state:
WithAll {
@@ -981,14 +1013,14 @@ impl GenesisBlockBuilder<WithAll> {
sdp_declarations,
},
} = self;
notes.extend(iter.map(Into::into));
Self {
notes = extend_non_empty_notes(notes, notes_to_add)?;
Ok(Self {
state: WithAll {
notes,
inscription,
sdp_declarations,
},
}
})
}
/// Replace the current inscription.
@@ -1203,7 +1235,7 @@ mod tests {
let mut ops = vec![
Op::Transfer(TransferOp::new(
Inputs::empty(),
Outputs::new(vec![make_note(1_000)]),
Outputs::new([make_note(1_000)]),
)),
Op::ChannelInscribe(valid_inscription()),
];
@@ -1337,9 +1369,7 @@ mod tests {
#[test]
fn multiple_notes_are_preserved() {
let block = GenesisBlockBuilder::new()
.add_note(make_note(100))
.add_note(make_note(200))
.add_note(make_note(300))
.add_notes([make_note(100), make_note(200), make_note(300)])
.set_inscription(valid_inscription())
.add_declaration(make_sdp_decl(0))
.build()
@@ -1370,9 +1400,11 @@ mod tests {
.add_note(make_note(10))
.add_declaration(make_sdp_decl(0))
.add_note(make_note(20))
.unwrap()
.set_inscription(valid_inscription())
.add_declaration(make_sdp_decl(1))
.add_note(make_note(30))
.unwrap()
.build()
.unwrap();
@@ -1459,8 +1491,7 @@ mod tests {
#[test]
fn add_notes_and_add_declarations_interleaved_with_batch() {
let block = GenesisBlockBuilder::new()
.add_note(make_note(1))
.add_notes([make_note(2), make_note(3)])
.add_notes([make_note(1), make_note(2), make_note(3)])
.set_inscription(valid_inscription())
.add_declaration(make_sdp_decl(0))
.add_declarations([make_sdp_decl(1), make_sdp_decl(2)])
@@ -1473,19 +1504,20 @@ mod tests {
}
#[test]
#[should_panic(expected = "add_notes called with empty iterator")]
fn add_notes_panics_on_empty_from_empty() {
drop(GenesisBlockBuilder::new().add_notes(std::iter::empty::<Note>()));
fn try_add_notes_errors_on_empty_from_empty() {
let err = GenesisBlockBuilder::new()
.try_add_notes(std::iter::empty::<Note>())
.unwrap_err();
assert!(matches!(err, Error::EmptyNotes));
}
#[test]
#[should_panic(expected = "add_notes called with empty iterator")]
fn add_notes_panics_on_empty_from_with_notes() {
drop(
GenesisBlockBuilder::new()
.add_note(make_note(1))
.add_notes(std::iter::empty::<Note>()),
);
fn try_add_notes_errors_on_empty_from_with_notes() {
let err = GenesisBlockBuilder::new()
.add_note(make_note(1))
.try_add_notes(std::iter::empty::<Note>())
.unwrap_err();
assert!(matches!(err, Error::EmptyNotes));
}
#[test]
+3 -3
View File
@@ -433,7 +433,7 @@ mod tests {
let withdraw_op = ChannelWithdrawOp {
channel_id,
outputs: Outputs::new(vec![Note {
outputs: Outputs::new([Note {
value: 6,
pk: ZkPublicKey::zero(),
}]),
@@ -465,7 +465,7 @@ mod tests {
let withdraw_op = ChannelWithdrawOp {
channel_id,
outputs: Outputs::new(vec![Note {
outputs: Outputs::new([Note {
value: 6,
pk: ZkPublicKey::zero(),
}]),
@@ -490,7 +490,7 @@ mod tests {
let withdraw_op = ChannelWithdrawOp {
channel_id,
outputs: Outputs::new(vec![Note {
outputs: Outputs::new([Note {
value: 6,
pk: ZkPublicKey::zero(),
}]),
+27 -71
View File
@@ -50,6 +50,16 @@ const LOCATOR_BYTES_SIZE_LIMIT: usize = 329usize;
pub const MAX_OPS_PER_TX: usize = u8::MAX as usize;
pub type Ops = UpperBoundedVec<Op, MAX_OPS_PER_TX>;
type NomOps<'a> = NomBoundedVec<'a, Op, { Ops::MIN }, { Ops::MAX }, 1>;
const MAX_TRANSACTION_INPUTS: usize = u8::MAX as usize;
const MAX_TRANSACTION_OUTPUTS: usize = u8::MAX as usize;
pub type BoundedUtxos = UpperBoundedVec<Utxo, MAX_TRANSACTION_INPUTS>;
pub type BoundedInputs = UpperBoundedVec<NoteId, MAX_TRANSACTION_INPUTS>;
pub type NomInputs<'a> =
NomBoundedVec<'a, NoteId, { BoundedInputs::MIN }, { BoundedInputs::MAX }, 1>;
pub type BoundedOutputs = UpperBoundedVec<Note, MAX_TRANSACTION_OUTPUTS>;
pub type NomOutputs<'a> =
NomBoundedVec<'a, Note, { BoundedOutputs::MIN }, { BoundedOutputs::MAX }, 1>;
// ==============================================================================
// Top-Level Transaction Decoders
@@ -215,37 +225,16 @@ pub(crate) fn decode_leader_claim(input: &[u8]) -> IResult<&[u8], LeaderClaimOp>
// Transfer Decoders
// ==============================================================================
fn decode_note(input: &[u8]) -> IResult<&[u8], Note> {
// Note = Value ZkPublicKey
let (input, value) = decode_uint64(input)?;
let (input, pk) = decode_zk_public_key(input)?;
Ok((input, Note::new(value, pk)))
}
fn decode_inputs(input: &[u8]) -> IResult<&[u8], Inputs> {
// Inputs = InputCount *NoteId
let (input, input_count) = decode_byte(input)?;
let (input, bounded_inputs) = NomInputs::decode(input)?;
let (input, note_ids) =
count(map(decode_field_element, NoteId), input_count as usize).parse(input)?;
Ok((
input,
// TODO: This will go once all ops use the same `Inputs` type.
Inputs::new(
note_ids
.try_into()
.map_err(|_| nom::Err::Error(Error::new(input, ErrorKind::Fail)))?,
),
))
Ok((input, Inputs::new(bounded_inputs)))
}
fn decode_outputs(input: &[u8]) -> IResult<&[u8], Outputs> {
// Outputs = OutputCount *Note
let (input, output_count) = decode_byte(input)?;
let (input, notes) = count(decode_note, output_count as usize).parse(input)?;
let (input, bounded_outputs) = NomOutputs::decode(input)?;
Ok((input, Outputs::new(notes)))
Ok((input, Outputs::new(bounded_outputs)))
}
pub(crate) fn decode_transfer(input: &[u8]) -> IResult<&[u8], TransferOp> {
@@ -333,7 +322,7 @@ fn decode_groth16(input: &[u8]) -> IResult<&[u8], CompressedGroth16Proof> {
.parse(input)
}
fn decode_zk_public_key(input: &[u8]) -> IResult<&[u8], ZkPublicKey> {
pub(crate) fn decode_zk_public_key(input: &[u8]) -> IResult<&[u8], ZkPublicKey> {
// ZkPublicKey = FieldElement
map(decode_field_element, ZkPublicKey::new).parse(input)
}
@@ -458,6 +447,7 @@ use lb_groth16::fr_to_bytes;
use crate::{
mantle::{
Utxo,
ledger::{Inputs, Outputs},
ops::channel::{ChannelKeyIndex, withdraw::ChannelWithdrawOp},
tx::MantleTxGasContext,
@@ -633,13 +623,7 @@ fn encode_note(note: &Note) -> Vec<u8> {
bytes
}
fn encode_inputs(inputs: &[NoteId]) -> Vec<u8> {
assert!(
u8::try_from(inputs.len()).is_ok(),
"Fatal error in 'encode_inputs' - {} inputs clipped to {}",
inputs.len(),
u8::MAX
);
fn encode_inputs(inputs: &BoundedInputs) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.extend(encode_byte(inputs.len() as u8));
for input in inputs {
@@ -648,14 +632,8 @@ fn encode_inputs(inputs: &[NoteId]) -> Vec<u8> {
bytes
}
fn encode_outputs(outputs: &[Note]) -> Vec<u8> {
fn encode_outputs(outputs: &BoundedOutputs) -> Vec<u8> {
let mut bytes = Vec::new();
assert!(
u8::try_from(outputs.len()).is_ok(),
"Fatal error in 'encode_outputs' - {} outputs clipped to {}",
outputs.len(),
u8::MAX
);
bytes.extend(encode_byte(outputs.len() as u8));
for output in outputs {
bytes.extend(encode_note(output));
@@ -1065,7 +1043,7 @@ mod tests {
let pk = ZkPublicKey::from(BigUint::from(42u64));
let note = Note::new(1000, pk);
let note_id = NoteId(BigUint::from(123u64).into());
let transfer_op = TransferOp::new(note_id.into(), Outputs::new(vec![note]));
let transfer_op = TransferOp::new(Inputs::new([note_id]), Outputs::new([note]));
let original_tx = MantleTx(Ops::new_unchecked(vec![Op::Transfer(transfer_op)]));
@@ -1191,7 +1169,7 @@ mod tests {
let locator2: Multiaddr = "/ip6/::1/tcp/9090".parse().unwrap();
let locked_note_sk = ZkKey::from(BigUint::from(1u64));
let locked_note = crate::mantle::Utxo {
let locked_note = Utxo {
op_id: [1u8; 32],
output_index: 12,
note: Note {
@@ -1390,8 +1368,8 @@ mod tests {
let note_id3 = NoteId(BigUint::from(333u64).into());
let transfer_op = TransferOp::new(
[note_id1, note_id2, note_id3].into(),
Outputs::new(vec![note1, note2]),
Inputs::new([note_id1, note_id2, note_id3]),
Outputs::new([note1, note2]),
);
let mantle_tx = MantleTx(Ops::new_unchecked(vec![Op::Transfer(transfer_op)]));
@@ -1438,8 +1416,8 @@ mod tests {
let locked_note_sk = ZkKey::from(BigUint::from(1u64));
let transfer_op = TransferOp {
inputs: NoteId(BigUint::from(777u64).into()).into(),
outputs: Outputs::new(vec![Note::new(5000, locked_note_sk.to_public_key())]),
inputs: Inputs::new([NoteId(BigUint::from(777u64).into())]),
outputs: Outputs::new([Note::new(5000, locked_note_sk.to_public_key())]),
};
let locator: Multiaddr = "/dns4/example.com/tcp/443".parse().unwrap();
@@ -1584,7 +1562,7 @@ mod tests {
let mantle_tx = MantleTx(Ops::new_unchecked(vec![Op::ChannelWithdraw(
ChannelWithdrawOp {
channel_id: ChannelId::from([0xAB; 32]),
outputs: Outputs::new(vec![note1, note2]),
outputs: Outputs::new([note1, note2]),
withdraw_nonce: 0,
},
)]));
@@ -1881,30 +1859,6 @@ mod tests {
);
}
#[test]
fn test_encode_reject_excessive_input_count() {
let note_id = NoteId(BigUint::from(111u64).into());
let inputs = [note_id; u8::MAX as usize + 1];
// Should panic
let result = panic::catch_unwind(|| {
encode_inputs(&inputs);
});
assert!(result.is_err(), "Should reject excessive output count");
}
#[test]
fn test_encode_reject_excessive_output_count() {
let note = Note::new(1000, ZkPublicKey::from(BigUint::from(42u64)));
let outputs = [note; u8::MAX as usize + 1];
// Should panic
let result = panic::catch_unwind(|| {
encode_outputs(&outputs);
});
assert!(result.is_err(), "Should reject excessive output count");
}
#[test]
fn test_decode_reject_oversized_locator() {
// Create a malicious input with oversized locator
@@ -1992,6 +1946,7 @@ mod tests {
fn test_encode_decode_max_inputs() {
let note_id = NoteId(BigUint::from(111u64).into());
let inputs = [note_id; u8::MAX as usize];
let inputs = BoundedInputs::from(inputs);
// Encode should succeed
let encoded = encode_inputs(&inputs);
@@ -2015,6 +1970,7 @@ mod tests {
fn test_encode_decode_max_outputs() {
let note = Note::new(1000, ZkPublicKey::from(BigUint::from(42u64)));
let outputs = [note; u8::MAX as usize];
let outputs = BoundedOutputs::from(outputs);
// Encode should succeed
let encoded = encode_outputs(&outputs);
+1 -2
View File
@@ -343,8 +343,7 @@ mod tests {
// Helper function to create a basic signed transaction
// Genesis transactions don't need verified proofs for Blob/Inscription ops
fn create_tx(mut ops: Vec<Op>, mut ops_proofs: Vec<OpProof>) -> SignedMantleTx {
let transfer_op =
TransferOp::new(Inputs::empty(), Outputs::new(vec![create_test_note(1000)]));
let transfer_op = TransferOp::new(Inputs::empty(), Outputs::new([create_test_note(1000)]));
let mut new_ops = vec![Op::Transfer(transfer_op)];
new_ops.append(&mut ops);
let mantle_tx = MantleTx(Ops::new_unchecked(new_ops));
+99 -39
View File
@@ -1,11 +1,11 @@
use std::{collections::HashSet, slice, sync::LazyLock};
use std::{collections::HashSet, sync::LazyLock};
use ark_ff::PrimeField as _;
use bytes::Bytes;
use lb_groth16::{Fr, fr_from_bytes, serde::serde_fr};
use lb_key_management_system_keys::keys::ZkPublicKey;
use lb_poseidon2::Digest as _;
use lb_utils::bounded_vec::{BoundedError, UpperBoundedVec};
use lb_utils::bounded_vec::BoundedError;
use lb_utxotree::UtxoTree;
use num_bigint::BigUint;
use serde::{Deserialize, Serialize};
@@ -15,7 +15,8 @@ use crate::{
crypto::{Hash, ZkHasher},
events::Events,
mantle::{
nom::{NomBoundedVec, NomDecode, NomEncode},
encoding::{BoundedInputs, BoundedOutputs, NomInputs, decode_uint64, decode_zk_public_key},
nom::{NomDecode, NomEncode},
ops::OpId,
},
sdp::{Declaration, DeclarationId, locked_notes::LockedNotes},
@@ -48,6 +49,8 @@ pub enum InputsError {
DoubleSpend,
#[error("Sum of input values overflows")]
InputsOverflow,
#[error(transparent)]
BoundedError(#[from] BoundedError),
}
#[derive(Clone, Debug, Error, Eq, PartialEq)]
@@ -56,6 +59,8 @@ pub enum OutputsError {
ZeroValueNote,
#[error("Sum of output values overflows")]
OutputsOverflow,
#[error(transparent)]
BoundedError(#[from] BoundedError),
}
#[derive(Clone, Debug, Error, Eq, PartialEq)]
@@ -67,12 +72,23 @@ pub enum LedgerError {
}
#[derive(Clone, Eq, Debug, PartialEq, Serialize, Deserialize)]
pub struct Outputs(Vec<Note>);
pub struct Outputs(BoundedOutputs);
impl Outputs {
pub fn try_new(
notes: impl TryInto<BoundedOutputs, Error = BoundedError>,
) -> Result<Self, OutputsError> {
notes.try_into().map(Self).map_err(OutputsError::from)
}
#[must_use]
pub const fn new(notes: Vec<Note>) -> Self {
Self(notes)
pub fn new(notes: impl Into<BoundedOutputs>) -> Self {
Self(notes.into())
}
#[must_use]
pub fn empty() -> Self {
Self(BoundedOutputs::default())
}
pub fn utxos<O: OpId>(&self, op: &O) -> impl Iterator<Item = Utxo> {
@@ -128,66 +144,54 @@ impl Outputs {
self.0.is_empty()
}
pub fn iter(&self) -> slice::Iter<'_, Note> {
pub fn iter(&self) -> impl Iterator<Item = &Note> {
<&Self as IntoIterator>::into_iter(self)
}
}
impl AsRef<Vec<Note>> for Outputs {
fn as_ref(&self) -> &Vec<Note> {
impl AsRef<BoundedOutputs> for Outputs {
fn as_ref(&self) -> &BoundedOutputs {
&self.0
}
}
impl AsMut<Vec<Note>> for Outputs {
fn as_mut(&mut self) -> &mut Vec<Note> {
impl AsMut<BoundedOutputs> for Outputs {
fn as_mut(&mut self) -> &mut BoundedOutputs {
&mut self.0
}
}
impl<'output> IntoIterator for &'output Outputs {
type Item = <slice::Iter<'output, Note> as IntoIterator>::Item;
type IntoIter = slice::Iter<'output, Note>;
type Item = <&'output BoundedOutputs as IntoIterator>::Item;
type IntoIter = <&'output BoundedOutputs as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
(&self.0).into_iter()
}
}
pub type InnerInputs = UpperBoundedVec<NoteId, { u8::MAX as usize }>;
type NomInputs<'a> = NomBoundedVec<'a, NoteId, { InnerInputs::MIN }, { InnerInputs::MAX }, 1>;
#[derive(Clone, Eq, Debug, PartialEq, Hash, Serialize, Deserialize)]
pub struct Inputs(InnerInputs);
impl AsRef<[NoteId]> for Inputs {
fn as_ref(&self) -> &[NoteId] {
&self.0
}
}
impl<I> From<I> for Inputs
where
I: Into<InnerInputs>,
{
fn from(value: I) -> Self {
Self(value.into())
}
}
pub struct Inputs(BoundedInputs);
impl Inputs {
#[must_use]
pub const fn new(inputs: InnerInputs) -> Self {
Self(inputs)
pub fn new(note_ids: impl Into<BoundedInputs>) -> Self {
Self(note_ids.into())
}
pub fn try_new(
note_ids: impl TryInto<BoundedInputs, Error = BoundedError>,
) -> Result<Self, InputsError> {
note_ids.try_into().map(Self).map_err(InputsError::from)
}
#[must_use]
pub const fn empty() -> Self {
Self(InnerInputs::new_unchecked(vec![]))
pub fn empty() -> Self {
Self(BoundedInputs::default())
}
#[must_use]
pub fn into_inner(self) -> InnerInputs {
pub fn into_inner(self) -> BoundedInputs {
self.0
}
@@ -206,7 +210,7 @@ impl Inputs {
}
pub fn iter(&self) -> impl Iterator<Item = &NoteId> {
self.0.iter()
<&Self as IntoIterator>::into_iter(self)
}
pub fn validate(&self, locked_notes: &LockedNotes, utxos: &Utxos) -> Result<(), InputsError> {
@@ -264,6 +268,41 @@ impl Inputs {
}
}
impl AsRef<BoundedInputs> for Inputs {
fn as_ref(&self) -> &BoundedInputs {
&self.0
}
}
impl AsRef<[NoteId]> for Inputs {
fn as_ref(&self) -> &[NoteId] {
&self.0
}
}
impl<I> From<I> for Inputs
where
I: Into<BoundedInputs>,
{
fn from(value: I) -> Self {
Self(value.into())
}
}
impl AsMut<BoundedInputs> for Inputs {
fn as_mut(&mut self) -> &mut BoundedInputs {
&mut self.0
}
}
impl<'input> IntoIterator for &'input Inputs {
type Item = <&'input BoundedInputs as IntoIterator>::Item;
type IntoIter = <&'input BoundedInputs as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
(&self.0).into_iter()
}
}
impl NomEncode for Inputs {
fn encode(&self) -> Vec<u8> {
NomInputs::from(&self.0).encode()
@@ -338,6 +377,27 @@ impl Note {
}
}
impl NomEncode for Note {
fn encode(&self) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.extend(crate::mantle::encoding::encode_uint64(self.value));
bytes.extend(crate::mantle::encoding::encode_field_element(
self.pk.as_fr(),
));
bytes
}
}
impl NomDecode for Note {
type Output = Self;
fn decode(bytes: &[u8]) -> nom::IResult<&[u8], Self::Output> {
let (bytes, value) = decode_uint64(bytes)?;
let (bytes, pk) = decode_zk_public_key(bytes)?;
Ok((bytes, Self::new(value, pk)))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Utxo {
pub op_id: Hash,
+2 -2
View File
@@ -116,8 +116,8 @@ mod test {
let pk1 = ZkPublicKey::from(Fr::from(BigUint::from(1u8)));
let pk2 = ZkPublicKey::from(Fr::from(BigUint::from(2u8)));
let transfer = TransferOp {
inputs: NoteId(BigUint::from(0u8).into()).into(),
outputs: Outputs::new(vec![
inputs: Inputs::new([NoteId(BigUint::from(0u8).into())]),
outputs: Outputs::new([
Note::new(100, pk0),
Note::new(200, pk1),
Note::new(300, pk2),
+1 -1
View File
@@ -734,7 +734,7 @@ mod tests {
};
let mantle_tx = create_test_mantle_tx(vec![Op::ChannelWithdraw(ChannelWithdrawOp {
channel_id,
outputs: Outputs::new(vec![withdraw_note]),
outputs: Outputs::new([withdraw_note]),
withdraw_nonce: 0,
})]);
let tx_hash = mantle_tx.hash();
+125 -55
View File
@@ -1,12 +1,14 @@
use std::{cmp::Ordering, collections::HashMap};
use lb_key_management_system_keys::keys::ZkPublicKey;
use lb_utils::bounded_vec::BoundedError;
use thiserror::Error;
use super::{GasCalculator as _, GasConstants, MantleTx, Note, Op, Utxo};
use crate::{
mantle::{
NoteId,
encoding::Ops,
encoding::BoundedUtxos,
gas::{GasCost, GasOverflow},
ledger::{Inputs, Outputs},
ops::{channel::withdraw::ChannelWithdrawOp, transfer::TransferOp},
@@ -15,10 +17,44 @@ use crate::{
proofs::channel_multi_sig_proof::ChannelMultiSigProof,
};
#[derive(Debug, Error)]
pub enum TxBuilderError {
#[error("Too many operations in transaction: attempted {actual}, max {max}")]
TooManyOps { actual: usize, max: usize },
#[error("Too many ledger inputs in transfer: attempted {actual}, max {max}")]
TooManyInputs { actual: usize, max: usize },
#[error("Too many ledger outputs in transfer: attempted {actual}, max {max}")]
TooManyOutputs { actual: usize, max: usize },
#[error("Gas computation overflow: {0}")]
GasOverflow(#[from] GasOverflow),
}
#[derive(Debug, Clone, Copy)]
enum TooManyTag {
Ops,
Inputs,
Outputs,
}
impl From<(BoundedError, TooManyTag)> for TxBuilderError {
fn from((err, tag): (BoundedError, TooManyTag)) -> Self {
let (actual, max) = match err {
BoundedError::TooLong { actual, max } => (actual, max),
BoundedError::EmptyInput => (0, 0),
};
match tag {
TooManyTag::Ops => Self::TooManyOps { actual, max },
TooManyTag::Inputs => Self::TooManyInputs { actual, max },
TooManyTag::Outputs => Self::TooManyOutputs { actual, max },
}
}
}
#[derive(Debug, Clone)]
pub struct MantleTxBuilder {
mantle_tx: MantleTx,
ledger_inputs: Vec<Utxo>,
ledger_inputs: BoundedUtxos,
pending_transfer: TransferOp,
// Maps a Proof to its Op by the Op Index
channel_multi_sig_proofs: HashMap<usize, ChannelMultiSigProof>,
@@ -30,9 +66,9 @@ impl MantleTxBuilder {
#[must_use]
pub fn new(context: MantleTxContext) -> Self {
Self {
mantle_tx: MantleTx(Ops::empty()),
ledger_inputs: vec![],
pending_transfer: TransferOp::new(Inputs::empty(), Outputs::new(vec![])),
mantle_tx: MantleTx([].into()),
ledger_inputs: BoundedUtxos::default(),
pending_transfer: TransferOp::new(Inputs::empty(), Outputs::empty()),
channel_multi_sig_proofs: HashMap::new(),
context,
}
@@ -43,64 +79,80 @@ impl MantleTxBuilder {
self.context.gas_context.get_gas_prices()
}
#[must_use]
pub fn push_op(self, op: Op) -> Self {
pub fn push_op(self, op: Op) -> Result<Self, TxBuilderError> {
self.extend_ops([op])
}
// TODO: Change this to a `Result` if trying to push too many ops in the genesis
// block.
#[must_use]
pub fn extend_ops(mut self, ops: impl IntoIterator<Item = Op>) -> Self {
pub fn extend_ops(mut self, ops: impl IntoIterator<Item = Op>) -> Result<Self, TxBuilderError> {
for op in ops {
self.mantle_tx.0.try_push(op).expect("Too many ops.");
self.mantle_tx
.0
.try_push(op)
.map_err(|err| TxBuilderError::from((err, TooManyTag::Ops)))?;
}
self
Ok(self)
}
#[must_use]
pub fn push_channel_withdraw(self, op: ChannelWithdrawOp, proof: ChannelMultiSigProof) -> Self {
let mut builder = self.push_op(Op::ChannelWithdraw(op));
pub fn push_channel_withdraw(
self,
op: ChannelWithdrawOp,
proof: ChannelMultiSigProof,
) -> Result<Self, TxBuilderError> {
let mut builder = self.push_op(Op::ChannelWithdraw(op))?;
let index = builder.mantle_tx.ops().len() - 1;
builder.channel_multi_sig_proofs.insert(index, proof);
builder
Ok(builder)
}
#[must_use]
pub fn add_ledger_input(self, utxo: Utxo) -> Self {
pub fn add_ledger_input(self, utxo: Utxo) -> Result<Self, TxBuilderError> {
self.extend_ledger_inputs([utxo])
}
#[must_use]
pub fn extend_ledger_inputs(mut self, utxos: impl IntoIterator<Item = Utxo>) -> Self {
pub fn extend_ledger_inputs(
mut self,
utxos: impl IntoIterator<Item = Utxo>,
) -> Result<Self, TxBuilderError> {
for utxo in utxos {
assert_eq!(self.pending_transfer.inputs.len(), self.ledger_inputs.len());
self.pending_transfer
.inputs
.as_mut()
.try_push(utxo.id())
.expect("Too many inputs in transfer op.");
self.ledger_inputs.push(utxo);
.map_err(|err| TxBuilderError::from((err, TooManyTag::Inputs)))?;
self.ledger_inputs
.try_push(utxo)
.map_err(|err| TxBuilderError::from((err, TooManyTag::Inputs)))?;
}
self
Ok(self)
}
#[must_use]
pub fn add_ledger_output(self, note: Note) -> Self {
pub fn add_ledger_output(self, note: Note) -> Result<Self, TxBuilderError> {
self.extend_ledger_outputs([note])
}
#[must_use]
pub fn extend_ledger_outputs(mut self, notes: impl IntoIterator<Item = Note>) -> Self {
self.pending_transfer.outputs.as_mut().extend(notes);
self
pub fn extend_ledger_outputs(
mut self,
notes: impl IntoIterator<Item = Note>,
) -> Result<Self, TxBuilderError> {
for note in notes {
self.pending_transfer
.outputs
.as_mut()
.try_push(note)
.map_err(|err| TxBuilderError::from((err, TooManyTag::Outputs)))?;
}
Ok(self)
}
pub fn return_change<G: GasConstants>(
self,
change_pk: ZkPublicKey,
) -> Result<Option<Self>, GasOverflow> {
) -> Result<Option<Self>, TxBuilderError> {
// Calculate the funding delta with a dummy change note to account for
// the gas cost increase from adding the output
let delta_with_change = self.with_dummy_change_note().funding_delta::<G>()?;
let delta_with_change = self.with_dummy_change_note()?.funding_delta::<G>()?;
match delta_with_change.cmp(&0) {
Ordering::Less | Ordering::Equal => {
@@ -121,18 +173,17 @@ impl MantleTxBuilder {
let tx_with_change = self.add_ledger_output(Note {
value: change,
pk: change_pk,
});
})?;
// Now the net balance should exactly equal the gas cost.
assert_eq!(tx_with_change.funding_delta::<G>().unwrap(), 0);
assert_eq!(tx_with_change.funding_delta::<G>()?, 0);
Ok(Some(tx_with_change))
}
}
}
#[must_use]
pub fn with_dummy_change_note(&self) -> Self {
pub fn with_dummy_change_note(&self) -> Result<Self, TxBuilderError> {
self.clone().add_ledger_output(Note {
value: 0,
pk: ZkPublicKey::zero(),
@@ -157,12 +208,12 @@ impl MantleTxBuilder {
in_sum - out_sum
}
pub fn gas_cost<G: GasConstants>(&self) -> Result<GasCost, GasOverflow> {
let build = self.clone().build();
build.total_gas_cost::<G>(&self.context.gas_context)
pub fn gas_cost<G: GasConstants>(&self) -> Result<GasCost, TxBuilderError> {
let build = self.clone().build()?;
Ok(build.total_gas_cost::<G>(&self.context.gas_context)?)
}
pub fn funding_delta<G: GasConstants>(&self) -> Result<i128, GasOverflow> {
pub fn funding_delta<G: GasConstants>(&self) -> Result<i128, TxBuilderError> {
Ok(self.net_balance() - i128::from(self.gas_cost::<G>()?.into_inner()))
}
@@ -201,13 +252,12 @@ impl MantleTxBuilder {
// TODO: Change this to a `Result` if genesis tx already contains max number of
// ops.
#[must_use]
pub fn build(mut self) -> MantleTx {
pub fn build(mut self) -> Result<MantleTx, TxBuilderError> {
self.mantle_tx
.0
.try_push(Op::Transfer(self.pending_transfer))
.expect("Failed to push transfer op. Too many ops defined.");
self.mantle_tx
.map_err(|err| TxBuilderError::from((err, TooManyTag::Ops)))?;
Ok(self.mantle_tx)
}
}
@@ -249,7 +299,9 @@ mod tests {
gas_context: MantleTxGasContext::default(),
leader_reward_amount: 30,
};
let builder = MantleTxBuilder::new(context).push_op(Op::ChannelInscribe(op));
let builder = MantleTxBuilder::new(context)
.push_op(Op::ChannelInscribe(op))
.unwrap();
// Check that the tx is already balanced because of zero gas price
assert_eq!(builder.net_balance(), 0);
@@ -261,7 +313,7 @@ mod tests {
// Build an operation
let op = DepositOp {
channel_id: [0; 32].into(),
inputs: NoteId(Fr::ZERO).into(),
inputs: Inputs::new([NoteId(Fr::ZERO)]),
metadata: b"Mint 1 to Alice in Zone".into(),
};
@@ -270,7 +322,9 @@ mod tests {
gas_context: MantleTxGasContext::default(),
leader_reward_amount: 30,
};
let builder = MantleTxBuilder::new(context).push_op(Op::ChannelDeposit(op));
let builder = MantleTxBuilder::new(context)
.push_op(Op::ChannelDeposit(op))
.unwrap();
// Check that the tx is already balanced because of zero gas price
assert_eq!(builder.net_balance(), 0);
@@ -286,7 +340,7 @@ mod tests {
};
let op = ChannelWithdrawOp {
channel_id: [0; 32].into(),
outputs: Outputs::new(vec![withdraw_note]),
outputs: Outputs::new([withdraw_note]),
withdraw_nonce: 0,
};
@@ -299,7 +353,9 @@ mod tests {
),
leader_reward_amount: 30,
};
let builder = MantleTxBuilder::new(context).push_op(Op::ChannelWithdraw(op));
let builder = MantleTxBuilder::new(context)
.push_op(Op::ChannelWithdraw(op))
.unwrap();
// Check that the tx is already balanced because of zero gas price
assert_eq!(builder.net_balance(), 0);
@@ -320,7 +376,9 @@ mod tests {
gas_context: MantleTxGasContext::default(),
leader_reward_amount: 30,
};
let builder = MantleTxBuilder::new(context).push_op(Op::LeaderClaim(op));
let builder = MantleTxBuilder::new(context)
.push_op(Op::LeaderClaim(op))
.unwrap();
// Check that the tx is already balanced because of zero gas price
assert_eq!(builder.net_balance(), 0);
@@ -336,7 +394,9 @@ mod tests {
};
let builder = MantleTxBuilder::new(context)
.add_ledger_output(Note::new(40, ZkPublicKey::zero()))
.unwrap()
.add_ledger_input(Utxo::new([0u8; 32], 0, Note::new(50, ZkPublicKey::zero())));
let builder = builder.unwrap();
// Check that the balance is 10 (= 50 - 40)
assert_eq!(builder.net_balance(), 10);
@@ -382,22 +442,27 @@ mod tests {
parent: [1; 32].into(),
signer: Ed25519Key::from_bytes(&[0; 32]).public_key(),
}))
.unwrap()
.push_op(Op::ChannelDeposit(DepositOp {
channel_id,
inputs: NoteId(Fr::ZERO).into(),
inputs: Inputs::new([NoteId(Fr::ZERO)]),
metadata: b"Mint 10 to Alice in Zone".into(),
}))
.unwrap()
.push_op(Op::ChannelWithdraw(ChannelWithdrawOp {
channel_id,
outputs: Outputs::new(vec![withdraw_note]),
outputs: Outputs::new([withdraw_note]),
withdraw_nonce: 0,
}))
.unwrap()
.push_op(Op::LeaderClaim(LeaderClaimOp {
rewards_root: Fr::ZERO.into(),
voucher_nullifier: Fr::ZERO.into(),
pk: ZkPublicKey::zero(),
}))
.add_ledger_output(Note::new(40, ZkPublicKey::zero()));
.unwrap()
.add_ledger_output(Note::new(40, ZkPublicKey::zero()))
.unwrap();
// Check the balance before funding tx
assert_eq!(builder.net_balance(), -40);
@@ -407,8 +472,9 @@ mod tests {
);
// Fund tx
let builder =
builder.add_ledger_input(Utxo::new([0u8; 32], 0, Note::new(40, ZkPublicKey::zero())));
let builder = builder
.add_ledger_input(Utxo::new([0u8; 32], 0, Note::new(40, ZkPublicKey::zero())))
.unwrap();
// Check the tx is balanced
assert_eq!(builder.net_balance(), 0);
@@ -433,9 +499,10 @@ mod tests {
let builder = MantleTxBuilder::new(context)
.push_op(Op::ChannelDeposit(DepositOp {
channel_id: [0; 32].into(),
inputs: deposit_input.into(),
inputs: Inputs::new([deposit_input]),
metadata: Metadata::empty(),
}))
.unwrap()
.push_op(Op::SDPDeclare(SDPDeclareOp {
service_type: ServiceType::BlendNetwork,
locators: "/ip4/1.1.1.1/udp/0".parse::<Locator>().unwrap().into(),
@@ -443,12 +510,15 @@ mod tests {
zk_id: ZkPublicKey::zero(),
locked_note_id: declare_locked,
}))
.unwrap()
.push_op(Op::SDPWithdraw(SDPWithdrawOp {
declaration_id: DeclarationId([0; 32]),
locked_note_id: withdraw_locked,
nonce: 1,
}))
.add_ledger_input(transfer_input);
.unwrap()
.add_ledger_input(transfer_input)
.unwrap();
let consumed_or_locked: Vec<_> = builder.consumed_or_locked_notes().collect();
assert!(
+1 -1
View File
@@ -44,7 +44,7 @@ mod tests {
MantleTx(
[Op::Transfer(TransferOp::new(
Inputs::empty(),
Outputs::new(vec![Note {
Outputs::new([Note {
value: seed.into(),
pk: ZkPublicKey::zero(),
}]),
+9 -3
View File
@@ -542,7 +542,13 @@ impl LedgerState {
) -> Result<Self, LedgerError<Id>> {
let transfer_op = tx.genesis_transfer();
if !transfer_op.inputs.is_empty() {
return Err(LedgerError::InputInGenesis(transfer_op.inputs.as_ref()[0]));
let first_input = transfer_op
.inputs
.iter()
.next()
.copied()
.expect("is not empty");
return Err(LedgerError::InputInGenesis(first_input));
}
Ok(Self::from_utxos(
@@ -1268,8 +1274,8 @@ pub mod tests {
.collect::<Vec<_>>();
let inputs = inputs.iter().map(|(_, utxo)| utxo.id()).collect::<Vec<_>>();
let transfer_op = TransferOp::new(
Inputs::new(inputs.try_into().expect("Too many inputs in transfer op.")),
Outputs::new(outputs),
Inputs::try_new(inputs).expect("Invalid inputs size"),
Outputs::try_new(outputs).expect("Invalid outputs size"),
);
let mantle_tx = MantleTx([Op::Transfer(transfer_op.clone())].into());
let transfer_sig = ZkKey::multi_sign(&sks, &mantle_tx.hash().to_fr()).unwrap();
+17 -14
View File
@@ -763,8 +763,11 @@ mod tests {
type HeaderId = [u8; 32];
fn create_tx(inputs: Inputs, outputs: Vec<Note>, sks: &[ZkKey]) -> SignedMantleTx {
let transfer_op = TransferOp::new(inputs, Outputs::new(outputs));
fn create_tx(inputs: Vec<NoteId>, outputs: Vec<Note>, sks: &[ZkKey]) -> SignedMantleTx {
let transfer_op = TransferOp::new(
Inputs::try_new(inputs).expect("Invalid inputs size"),
Outputs::try_new(outputs).expect("Invalid outputs size"),
);
let mantle_tx = MantleTx([Op::Transfer(transfer_op)].into());
SignedMantleTx {
ops_proofs: vec![OpProof::ZkSig(
@@ -873,7 +876,7 @@ mod tests {
let sk = ZkKey::from(BigUint::from(0u8));
// determine fees
let tx = create_tx(
[utxo.id()].into(),
vec![utxo.id()],
vec![output_note],
std::slice::from_ref(&sk),
);
@@ -881,7 +884,7 @@ mod tests {
AuthenticatedMantleTx::total_gas_cost::<MainnetGasConstants>(&tx, GasPrices::default())
.unwrap();
output_note.value = utxo.note.value - fees.into_inner();
let tx = create_tx([utxo.id()].into(), vec![output_note], &[sk]);
let tx = create_tx(vec![utxo.id()], vec![output_note], &[sk]);
// Create a dummy proof (using same structure as in cryptarchia tests)
@@ -1025,7 +1028,7 @@ mod tests {
// Submit a deposit operation
let deposit = DepositOp {
channel_id,
inputs: [utxo.id()].into(),
inputs: Inputs::new([utxo.id()]),
metadata: [5, 6, 7, 8].into(),
};
let ops = vec![Op::ChannelDeposit(deposit.clone())];
@@ -1085,7 +1088,7 @@ mod tests {
// Deposit some funds into the channel
let deposit = DepositOp {
channel_id,
inputs: [utxo.id()].into(),
inputs: Inputs::new([utxo.id()]),
metadata: [5, 6, 7, 8].into(),
};
let deposit_ops = vec![Op::ChannelDeposit(deposit)];
@@ -1117,7 +1120,7 @@ mod tests {
};
let withdraw = ChannelWithdrawOp {
channel_id,
outputs: Outputs::new(vec![withdraw_note]),
outputs: Outputs::new([withdraw_note]),
withdraw_nonce: 0,
};
let withdraw_tx = MantleTx([Op::ChannelWithdraw(withdraw.clone())].into());
@@ -1176,7 +1179,7 @@ mod tests {
// Deposit some funds into the channel
let deposit = DepositOp {
channel_id,
inputs: [utxo.id()].into(),
inputs: Inputs::new([utxo.id()]),
metadata: Metadata::empty(),
};
let deposit_ops = vec![Op::ChannelDeposit(deposit)];
@@ -1204,7 +1207,7 @@ mod tests {
};
let withdraw = ChannelWithdrawOp {
channel_id,
outputs: Outputs::new(vec![withdraw_note]),
outputs: Outputs::new([withdraw_note]),
withdraw_nonce: 0,
};
let wrong_key = Ed25519Key::from_bytes(&[42; 32]);
@@ -1495,7 +1498,7 @@ mod tests {
let mut output_note = Note::new(1, ZkPublicKey::new(BigUint::from(0u8).into()));
let sk = ZkKey::from(BigUint::from(0u8));
let tx = create_tx(
[utxo.id()].into(),
vec![utxo.id()],
vec![output_note],
std::slice::from_ref(&sk),
);
@@ -1506,7 +1509,7 @@ mod tests {
)
.unwrap();
output_note.value = utxo.note.value - fees.into_inner();
let tx = create_tx([utxo.id()].into(), vec![output_note], &[sk]);
let tx = create_tx(vec![utxo.id()], vec![output_note], &[sk]);
let result = ledger
.clone()
@@ -1534,7 +1537,7 @@ mod tests {
let mut output_note = Note::new(1, ZkPublicKey::new(BigUint::from(0u8).into()));
let sk = ZkKey::from(BigUint::from(0u8));
let tx = create_tx(
[utxo.id()].into(),
vec![utxo.id()],
vec![output_note],
std::slice::from_ref(&sk),
);
@@ -1548,7 +1551,7 @@ mod tests {
.unwrap();
output_note.value = utxo.note.value - fees.into_inner();
let tx = create_tx(
[utxo.id()].into(),
vec![utxo.id()],
vec![output_note],
std::slice::from_ref(&sk),
);
@@ -1564,7 +1567,7 @@ mod tests {
// storage
output_note.value = utxo.note.value - fees.into_inner() - 1000;
let tx = create_tx(
[utxo.id()].into(),
vec![utxo.id()],
vec![output_note],
std::slice::from_ref(&sk),
);
+3 -1
View File
@@ -894,7 +894,9 @@ where
);
let tx_context = wallet.get_tx_context(None).await?;
let tx_builder = MantleTxBuilder::new(tx_context).push_op(Op::ChannelDeposit(req.deposit));
let tx_builder = MantleTxBuilder::new(tx_context)
.push_op(Op::ChannelDeposit(req.deposit))
.map_err(|e| overwatch::DynError::from(e.to_string()))?;
let lb_wallet_service::TipResponse {
tip,
response: funded_tx_builder,
@@ -36,7 +36,7 @@ where
declaration: DeclarationMessage,
config: &SdpWalletConfig,
) -> Result<SignedMantleTx, SdpWalletError> {
tx_builder = tx_builder.push_op(Op::SDPDeclare(declaration));
tx_builder = tx_builder.push_op(Op::SDPDeclare(declaration))?;
let funded = self
.api
@@ -69,7 +69,7 @@ where
withdraw: WithdrawMessage,
config: &SdpWalletConfig,
) -> Result<SignedMantleTx, SdpWalletError> {
tx_builder = tx_builder.push_op(Op::SDPWithdraw(withdraw));
tx_builder = tx_builder.push_op(Op::SDPWithdraw(withdraw))?;
let funded = self
.api
@@ -102,7 +102,7 @@ where
active: ActiveMessage,
config: &SdpWalletConfig,
) -> Result<SignedMantleTx, SdpWalletError> {
tx_builder = tx_builder.push_op(Op::SDPActive(active));
tx_builder = tx_builder.push_op(Op::SDPActive(active))?;
let funded = self
.api
@@ -403,7 +403,7 @@ mod pol_tests {
let pk = sk.to_public_key();
// Create a UTXO
let transfer = TransferOp::new(Inputs::empty(), Outputs::new(vec![Note::new(1000u64, pk)]));
let transfer = TransferOp::new(Inputs::empty(), Outputs::new([Note::new(1000u64, pk)]));
let utxo = transfer.outputs.utxo_by_index(0, &transfer).unwrap();
// Create aged/latest UTXO trees
+6 -2
View File
@@ -6,7 +6,7 @@ use lb_core::{
Note, Op, SignedMantleTx, Value,
gas::{GasCost, GasOverflow, MainnetGasConstants},
ops::leader_claim::LeaderClaimOp,
tx_builder::MantleTxBuilder,
tx_builder::{MantleTxBuilder, TxBuilderError},
},
};
use lb_key_management_system_service::keys::ZkPublicKey;
@@ -41,7 +41,9 @@ where
.map_err(|error| LeaderWalletError::WalletApi(Box::new(error)))?;
let tx_builder = MantleTxBuilder::new(tx_context)
.push_op(Op::LeaderClaim(op))
.add_ledger_output(Note::new(reward_amount, config.funding_pk));
.map_err(LeaderWalletError::TxBuilder)?
.add_ledger_output(Note::new(reward_amount, config.funding_pk))
.map_err(LeaderWalletError::TxBuilder)?;
let funded_tx_builder = wallet
.fund_tx(
Some(tip),
@@ -83,4 +85,6 @@ pub enum LeaderWalletError {
TxFeeExceedsMaxFee { max_fee: GasCost, tx_fee: GasCost },
#[error(transparent)]
GasOverflow(#[from] GasOverflow),
#[error(transparent)]
TxBuilder(#[from] TxBuilderError),
}
+3 -1
View File
@@ -2,7 +2,7 @@ use lb_core::{
mantle::{
SignedMantleTx,
gas::{GasCost, GasOverflow},
tx_builder::MantleTxBuilder,
tx_builder::{MantleTxBuilder, TxBuilderError},
},
sdp::{ActiveMessage, DeclarationMessage, WithdrawMessage},
};
@@ -20,6 +20,8 @@ pub enum SdpWalletError {
TxFeeExceedsMaxFee { max_fee: GasCost, tx_fee: GasCost },
#[error(transparent)]
GasOverflow(#[from] GasOverflow),
#[error(transparent)]
TxBuilder(#[from] TxBuilderError),
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
+7 -3
View File
@@ -1,8 +1,10 @@
use lb_core::{
header::HeaderId,
mantle::{
Note, SignedMantleTx, TxHash, Value, ops::leader_claim::VoucherCm, tx::MantleTxContext,
tx_builder::MantleTxBuilder,
Note, SignedMantleTx, TxHash, Value,
ops::leader_claim::VoucherCm,
tx::MantleTxContext,
tx_builder::{MantleTxBuilder, TxBuilderError},
},
};
use lb_key_management_system_service::keys::{
@@ -34,6 +36,8 @@ pub enum WalletApiError {
RelayRecv(#[from] RecvError),
#[error(transparent)]
Wallet(#[from] WalletServiceError),
#[error(transparent)]
TxBuilderError(#[from] TxBuilderError),
}
impl From<(RelayError, WalletMsg)> for WalletApiError {
@@ -153,7 +157,7 @@ where
) -> Result<TipResponse<SignedMantleTx>, WalletApiError> {
let context = self.get_tx_context(tip).await?;
let mantle_tx_builder =
MantleTxBuilder::new(context).add_ledger_output(Note::new(amount, recipient_pk));
MantleTxBuilder::new(context).add_ledger_output(Note::new(amount, recipient_pk))?;
let funded_tx_builder = self
.fund_tx(tip, mantle_tx_builder, change_pk, funding_pks)
.await?;
+5 -2
View File
@@ -27,7 +27,7 @@ use lb_core::{
sdp::{SDPActiveOp, SDPDeclareOp, SDPWithdrawOp},
},
tx::MantleTxContext,
tx_builder::MantleTxBuilder,
tx_builder::{MantleTxBuilder, TxBuilderError},
},
proofs::leader_claim_proof::{Groth16LeaderClaimProof, LeaderClaimPrivate, LeaderClaimPublic},
};
@@ -99,6 +99,9 @@ pub enum WalletServiceError {
#[error("Input note {0:?} is missing in ledger")]
MissingInputNote(NoteId),
#[error(transparent)]
TxBuilder(#[from] TxBuilderError),
#[error("PoC generation failed: {0:?}")]
PoCGenerationFailed(#[from] lb_core::proofs::leader_claim_proof::Error),
@@ -752,7 +755,7 @@ where
) -> Result<SignedMantleTx, WalletServiceError> {
// Extract input public keys before building the transaction
let mut channel_multi_sig_proofs = tx_builder.channel_multi_sig_proofs().clone();
let mantle_tx = tx_builder.clone().build();
let mantle_tx = tx_builder.clone().build()?;
let tx_hash = mantle_tx.hash();
let mut ops_proofs = Vec::new();
+8 -6
View File
@@ -34,12 +34,14 @@ pub fn build_inscription_tx_builder(
leader_reward_amount: 0,
};
MantleTxBuilder::new(tx_context).push_op(Op::ChannelInscribe(InscriptionOp {
channel_id,
inscription,
parent: parent.unwrap_or_else(MsgId::root),
signer: signing_key.public_key(),
}))
MantleTxBuilder::new(tx_context)
.push_op(Op::ChannelInscribe(InscriptionOp {
channel_id,
inscription,
parent: parent.unwrap_or_else(MsgId::root),
signer: signing_key.public_key(),
}))
.expect("inscription test builder should fit op bounds")
}
#[must_use]
+2 -9
View File
@@ -221,15 +221,8 @@ pub fn build_wallet_funded_transfer(
let selected_inputs = selected_inputs.into_utxos();
let transfer = TransferOp {
inputs: Inputs::new(
selected_inputs
.iter()
.map(Utxo::id)
.collect::<Vec<_>>()
.try_into()
.expect("Too many inputs for transfer op."),
),
outputs: Outputs::new(transfer_outputs),
inputs: Inputs::try_new(selected_inputs.iter().map(Utxo::id).collect::<Vec<_>>())?,
outputs: Outputs::try_new(transfer_outputs)?,
};
Ok(WalletFundedTransfer {
@@ -105,7 +105,7 @@ fn fund_sponsored_wallet_transaction(
sender_change_pk,
output_total,
sender_inputs.total(),
);
)?;
fund_builder_from_plan(
&builder_with_sender_outputs,
@@ -122,12 +122,14 @@ fn add_sender_change_output(
change_pk: ZkPublicKey,
output_total: u64,
input_total: u64,
) -> MantleTxBuilder {
) -> Result<MantleTxBuilder, WalletError> {
let change = input_total - output_total;
if change > 0 {
tx_builder.add_ledger_output(Note::new(change, change_pk))
} else {
tx_builder
.add_ledger_output(Note::new(change, change_pk))
.map_err(Into::into)
} else {
Ok(tx_builder)
}
}
@@ -169,7 +171,7 @@ fn evaluate_standard_funding_inputs(
) -> Result<WalletFundingOutcome<MantleTxBuilder>, WalletError> {
let funded_builder = tx_builder
.clone()
.extend_ledger_inputs(selected_inputs.iter().copied());
.extend_ledger_inputs(selected_inputs.iter().copied())?;
match funded_builder
.funding_delta::<MainnetGasConstants>()?
@@ -203,7 +205,7 @@ fn build_chunked_funded_tx(
.sum::<u128>();
let output_sum = pending_transfer_output_sum(tx_builder);
let chunked_builder = with_transfer_input_chunks(tx_builder, funding_utxos);
let chunked_builder = with_transfer_input_chunks(tx_builder, funding_utxos)?;
let funding_delta = funding_delta_for_chunked_builder(&chunked_builder, input_sum, output_sum)?;
match funding_delta.cmp(&0) {
@@ -228,7 +230,7 @@ fn add_chunked_change_output(
) -> Result<Option<MantleTxBuilder>, WalletError> {
let builder_with_dummy_change = chunked_builder
.clone()
.add_ledger_output(Note::new(0, change_pk));
.add_ledger_output(Note::new(0, change_pk))?;
let delta_with_change =
funding_delta_for_chunked_builder(&builder_with_dummy_change, input_sum, output_sum)?;
@@ -237,7 +239,7 @@ fn add_chunked_change_output(
}
let change = u64::try_from(delta_with_change).expect("Positive delta must fit in u64");
let tx_with_change = chunked_builder.add_ledger_output(Note::new(change, change_pk));
let tx_with_change = chunked_builder.add_ledger_output(Note::new(change, change_pk))?;
assert_eq!(
funding_delta_for_chunked_builder(
@@ -255,7 +257,7 @@ fn add_chunked_change_output(
fn with_transfer_input_chunks(
tx_builder: &MantleTxBuilder,
funding_utxos: &[Utxo],
) -> MantleTxBuilder {
) -> Result<MantleTxBuilder, WalletError> {
let final_chunk_len = match funding_utxos.len() % super::signing::ZKSIGN_MAX_INPUTS {
0 => super::signing::ZKSIGN_MAX_INPUTS,
remainder => remainder,
@@ -265,29 +267,27 @@ fn with_transfer_input_chunks(
let mut builder = tx_builder.clone();
for chunk in funding_utxos[..split_index].chunks(super::signing::ZKSIGN_MAX_INPUTS) {
builder = builder.push_op(Op::Transfer(TransferOp::new(
Inputs::new(
chunk
.iter()
.map(Utxo::id)
.collect::<Vec<_>>()
.try_into()
.expect("Too many inputs for transfer op."),
),
Outputs::new(vec![]),
)));
Inputs::try_new(chunk.iter().map(Utxo::id).collect::<Vec<_>>())?,
Outputs::empty(),
)))?;
}
builder.extend_ledger_inputs(funding_utxos[split_index..].iter().copied())
builder
.extend_ledger_inputs(funding_utxos[split_index..].iter().copied())
.map_err(Into::into)
}
fn pending_transfer_output_sum(tx_builder: &MantleTxBuilder) -> u128 {
match tx_builder.clone().build().0.iter().last() {
Some(Op::Transfer(transfer)) => transfer
.outputs
.iter()
.map(|note| u128::from(note.value))
.sum(),
_ => 0,
match tx_builder.clone().build() {
Ok(tx) => match tx.0.iter().last() {
Some(Op::Transfer(transfer)) => transfer
.outputs
.iter()
.map(|note| u128::from(note.value))
.sum(),
_ => 0,
},
Err(_) => 0,
}
}
@@ -349,7 +349,7 @@ mod tests {
0
);
let funded_tx = funded_builder.build();
let funded_tx = funded_builder.build().expect("funded builder should build");
let Some(Op::Transfer(transfer)) = funded_tx.ops().last() else {
panic!("wallet funding should leave a transfer op at the end");
};
+3 -1
View File
@@ -1,6 +1,6 @@
//! Shared error type for wallet transaction preparation.
use lb_core::mantle::{NoteId, VerificationError, gas::GasOverflow};
use lb_core::mantle::{NoteId, VerificationError, gas::GasOverflow, tx_builder::TxBuilderError};
use lb_wallet::WalletError;
use lb_zksign::ZkSignError;
use thiserror::Error;
@@ -19,6 +19,8 @@ pub enum WalletTransactionError {
Verification(#[from] VerificationError),
#[error(transparent)]
Gas(#[from] GasOverflow),
#[error(transparent)]
Builder(#[from] TxBuilderError),
#[error("wallet transaction output total overflowed u64")]
OutputTotalOverflow,
}
@@ -46,7 +46,7 @@ impl WalletTransactionIntent {
let mut tx_builder = MantleTxBuilder::new(empty_context);
for (receiver_pk, value) in receivers {
tx_builder = tx_builder.add_ledger_output(Note::new(*value, *receiver_pk));
tx_builder = tx_builder.add_ledger_output(Note::new(*value, *receiver_pk))?;
}
Self::from_builder(tx_builder)
@@ -60,7 +60,7 @@ impl WalletTransactionIntent {
fn transfer_output_total(tx_builder: &MantleTxBuilder) -> Result<u64, WalletTransactionError> {
tx_builder
.clone()
.build()
.build()?
.ops()
.iter()
.filter_map(|op| match op {
@@ -24,7 +24,7 @@ pub fn prepare_wallet_transaction(
let input_utxos_by_note_id = input_utxos_by_note_id(&resources);
let funded_builder = fund_wallet_transaction(intent, resources)?;
let mantle_tx = funded_builder.clone().build();
let mantle_tx = funded_builder.clone().build()?;
let tx_hash = mantle_tx.hash();
let transfer_proofs = build_transfer_proofs(mantle_tx.ops(), &tx_hash, &transfer_signers)?;
let funding_inputs = funding_inputs_from_transfers(&mantle_tx, &input_utxos_by_note_id)?;
@@ -22,7 +22,7 @@ pub(super) fn sign_prepared_wallet_transaction(
leading_op_proofs: Vec<OpProof>,
) -> Result<SignedWalletTransaction, WalletTransactionError> {
let gas_prices = funded_builder.get_gas_prices();
let mantle_tx = funded_builder.build();
let mantle_tx = funded_builder.build()?;
let mut op_proofs = leading_op_proofs;
op_proofs.extend(transfer_proofs);
@@ -181,7 +181,11 @@ async fn transaction_is_in_chain(
pub fn create_invalid_transaction() -> SignedMantleTx {
let output_note = Note::new(1000, ZkPublicKey::new(1u8.into()));
let transfer_op = TransferOp::new(Inputs::empty(), Outputs::new(vec![output_note]));
let transfer_op = TransferOp::new(
Inputs::empty(),
// Outputs::new([output_note]),
Outputs::new([output_note]),
);
let mantle_tx = MantleTx([Op::Transfer(transfer_op)].into());
+26 -18
View File
@@ -19,7 +19,7 @@ use lb_config::consensus::{ProviderInfo, create_genesis_block_with_declarations}
use lb_core::{
mantle::{
GenesisTx as _, MantleTx, Note, Op, OpProof, Transaction as _, Utxo, Value,
ledger::Outputs,
ledger::{Inputs, Outputs, OutputsError},
ops::{
channel::{
ChannelId,
@@ -45,7 +45,7 @@ use lb_testing_framework::{
DeploymentBuilder, LbcEnv, LbcLocalDeployer, LbcManualCluster, NodeHttpClient, TopologyConfig,
internal::DeploymentPlan,
};
use lb_utils::math::NonNegativeRatio;
use lb_utils::{bounded_vec::BoundedError, math::NonNegativeRatio};
use lb_zone_sdk::{
ZoneMessage,
adapter::NodeHttpClient as ZoneNodeHttpClient,
@@ -123,6 +123,10 @@ pub enum ZoneTestError {
WithdrawTimeout,
#[error("zone sequencer event stream stopped before observing the expected event")]
SequencerStopped,
#[error(transparent)]
BoundedError(#[from] BoundedError),
#[error(transparent)]
OutputsError(#[from] OutputsError),
}
/// Prepared deployment resources for the single-node zone test cluster.
@@ -1190,7 +1194,7 @@ pub fn build_zone_deposit(
Ok(ZoneDeposit {
deposit: DepositOp {
channel_id,
inputs: note.id().into(),
inputs: Inputs::new([note.id()]),
metadata,
},
reserved_inputs: vec![note],
@@ -1318,7 +1322,7 @@ fn build_atomic_deposit_op(
Ok(DepositOp {
channel_id,
inputs: deposit_note_id.into(),
inputs: Inputs::new([deposit_note_id]),
metadata,
})
}
@@ -1334,7 +1338,7 @@ pub async fn submit_zone_withdraw(
) -> Result<ZoneWithdrawSubmission, ZoneTestError> {
let withdraw = ChannelWithdrawOp {
channel_id,
outputs: Outputs::new(vec![Note::new(amount, funding_public_key)]),
outputs: Outputs::new([Note::new(amount, funding_public_key)]),
withdraw_nonce: 0,
};
@@ -1424,15 +1428,17 @@ pub async fn publish_atomic_zone_withdraw(
}
let withdraw_args: Vec<WithdrawArg> = outputs_per_arg
.iter()
.map(|amounts| WithdrawArg {
outputs: Outputs::new(
amounts
.iter()
.map(|amount| Note::new(*amount, funding_public_key))
.collect(),
),
.map(|amounts| {
Ok::<WithdrawArg, ZoneTestError>(WithdrawArg {
outputs: Outputs::try_new(
amounts
.iter()
.map(|amount| Note::new(*amount, funding_public_key))
.collect::<Vec<_>>(),
)?,
})
})
.collect();
.collect::<Result<Vec<_>, _>>()?;
sequencer
.publish_atomic_withdraw(inscription_data.clone(), withdraw_args)
@@ -1549,11 +1555,13 @@ fn add_exact_deposit_notes_to_funding_key(
.genesis_transfer()
.clone();
transfer_op.outputs.as_mut().extend(
values
.into_iter()
.map(|value| Note::new(value, funding_public_key)),
);
for value in values {
transfer_op
.outputs
.as_mut()
.try_push(Note::new(value, funding_public_key))
.expect("zone helper note set should stay within transfer output bounds");
}
let providers = deployment
.nodes()
+3
View File
@@ -320,6 +320,9 @@ fn wallet_transaction_error(error: WalletTransactionError) -> StepError {
WalletTransactionError::Gas(error) => StepError::LogicalError {
message: error.to_string(),
},
WalletTransactionError::Builder(error) => StepError::LogicalError {
message: error.to_string(),
},
WalletTransactionError::OutputTotalOverflow => StepError::LogicalError {
message: error.to_string(),
},
+2 -1
View File
@@ -97,7 +97,8 @@ fn test_config(mut config: RunConfig, genesis_time: OffsetDateTime) -> RunConfig
};
config.deployment.cryptarchia.genesis_block = GenesisBlockBuilder::new()
.add_notes(genesis_tx.genesis_transfer().outputs.iter().copied())
.try_add_notes(genesis_tx.genesis_transfer().outputs.iter().copied())
.unwrap()
.set_inscription(inscription)
.build()
.expect("Failed to build genesis block");
+2 -1
View File
@@ -7,6 +7,7 @@ use lb_core::{
mantle::{
GenesisTx as _, NoteId, Transaction as _,
gas::GasCost,
ledger::Inputs,
ops::channel::{ChannelId, deposit::DepositOp},
},
};
@@ -96,7 +97,7 @@ async fn channel_deposit() {
assert_eq!(selected_deposit_amount, deposit_amount);
let deposit_op = DepositOp {
channel_id,
inputs: note_id.into(),
inputs: Inputs::new([note_id]),
metadata: format!("Mint {deposit_amount} to Alice in Zone")
.into_bytes()
.try_into()
+9 -2
View File
@@ -475,7 +475,9 @@ async fn fund_sdp_transaction(
gas_context: empty_context,
leader_reward_amount: 0,
};
let tx_builder = MantleTxBuilder::new(tx_context).push_op(extra_op);
let tx_builder = MantleTxBuilder::new(tx_context)
.push_op(extra_op)
.expect("mixed-op helper should fit op bounds");
let funded_builder = fund_builder_from_wallet_source(&funding_source, &tx_builder)
.expect("funding mixed-op transaction should succeed");
@@ -486,5 +488,10 @@ async fn fund_sdp_transaction(
.map(|_| funding_wallet.secret_key.clone())
.collect::<Vec<_>>();
(funded_builder.build(), signing_keys)
(
funded_builder
.build()
.expect("funded mixed-op builder should build"),
signing_keys,
)
}
@@ -78,7 +78,7 @@ pub fn apply_wallet_genesis_overrides(
.expect("Genesis block should have a genesis tx")
.genesis_transfer()
.clone();
for output in transfer_op.outputs.as_mut() {
for output in transfer_op.outputs.as_mut().iter_mut() {
if leader_keys.contains(&output.pk) {
output.value = leader_stake;
}
@@ -87,7 +87,8 @@ pub fn apply_wallet_genesis_overrides(
transfer_op
.outputs
.as_mut()
.push(Note::new(*value, secret_key.to_public_key()));
.try_push(Note::new(*value, secret_key.to_public_key()))
.expect("wallet account outputs must fit transfer output bounds");
}
let genesis_block =
@@ -286,8 +286,11 @@ fn build_wallet_transaction(
let provisional_tx = MantleTxBuilder::new(tx_context.clone())
.add_ledger_input(input.utxo)
.map_err(|err| format!("failed to add provisional input: {err}"))?
.add_ledger_output(Note::new(input.utxo.note.value, receiver))
.build();
.map_err(|err| format!("failed to add provisional output: {err}"))?
.build()
.map_err(|err| format!("failed to build provisional tx: {err}"))?;
let fee = provisional_tx
.total_gas_cost::<MainnetGasConstants>(gas_context)?
@@ -301,8 +304,11 @@ fn build_wallet_transaction(
let tx = MantleTxBuilder::new(tx_context)
.add_ledger_input(input.utxo)
.map_err(|err| format!("failed to add input: {err}"))?
.add_ledger_output(Note::new(output_value, receiver))
.build();
.map_err(|err| format!("failed to add output: {err}"))?
.build()
.map_err(|err| format!("failed to build tx: {err}"))?;
let signature = ZkKey::multi_sign(
slice::from_ref(&input.account.secret_key),
+3 -1
View File
@@ -357,7 +357,9 @@ fn build_genesis_block(
// Accumulate additional notes into WithNotes state.
let mut builder = GenesisBlockBuilder::new().add_note(first_note);
for note in notes_iter {
builder = builder.add_note(note);
builder = builder
.try_add_note(note)
.context("failed to append note to genesis transfer")?;
}
// Transition: WithNotes → WithNotesAndInscription → WithAll.
@@ -46,7 +46,8 @@ impl GenesisTransferOp {
notes.push(Note::new(faucet.funds, faucet.zk_id));
let outputs = Outputs::new(notes);
let outputs = Outputs::try_new(notes)
.expect("genesis distribution outputs must fit transfer output bounds");
let transfer_op = TransferOp::new(Inputs::empty(), outputs.clone());
Self {
+3 -1
View File
@@ -127,7 +127,9 @@ pub fn create_genesis_block(utxos: &[Utxo], test_context: Option<&str>) -> Genes
let genesis_builder = if let Some(note) = outputs.next() {
let mut genesis_builder = GenesisBlockBuilder::new().add_note(note);
for note in outputs {
genesis_builder = genesis_builder.add_note(note);
genesis_builder = genesis_builder
.try_add_note(note)
.expect("note count must fit in genesis transfer outputs");
}
genesis_builder
} else {
+13 -1
View File
@@ -7,7 +7,7 @@ use std::{str::FromStr, vec::IntoIter};
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Error, Eq, PartialEq)]
#[derive(Debug, Error, Eq, PartialEq, Clone)]
pub enum BoundedError {
#[error("Input cannot be empty.")]
EmptyInput,
@@ -91,6 +91,18 @@ impl<T, const MIN: usize, const MAX: usize> BoundedVec<T, MIN, MAX> {
}
}
impl<T, const MIN: usize, const MAX: usize> Default for BoundedVec<T, MIN, MAX> {
fn default() -> Self {
const {
assert!(
MIN == 0,
"Default is only valid for BoundedVec with MIN == 0"
);
}
Self(Vec::new())
}
}
impl<T, const MIN: usize, const MAX: usize> TryFrom<Vec<T>> for BoundedVec<T, MIN, MAX> {
type Error = BoundedError;
+1
View File
@@ -18,6 +18,7 @@ lb-cryptarchia-engine = { workspace = true }
lb-key-management-system-keys = { workspace = true }
lb-ledger = { workspace = true }
lb-mmr = { workspace = true }
lb-utils = { workspace = true }
num-bigint = { workspace = true }
rpds = { features = ["serde"], workspace = true }
serde = { features = ["derive"], workspace = true }
+23 -1
View File
@@ -1,4 +1,12 @@
use lb_core::{header::HeaderId, mantle::gas::GasOverflow};
use lb_core::{
header::HeaderId,
mantle::{
gas::GasOverflow,
ledger::{InputsError, OutputsError},
tx_builder::TxBuilderError,
},
};
use lb_utils::bounded_vec::BoundedError;
use thiserror::Error;
#[derive(Error, Debug, PartialEq, Eq)]
@@ -9,4 +17,18 @@ pub enum WalletError {
InsufficientFunds { available: u64 },
#[error(transparent)]
GasOverflow(#[from] GasOverflow),
#[error("Transaction builder error: {0}")]
TxBuilder(String),
#[error(transparent)]
BoundedError(#[from] BoundedError),
#[error(transparent)]
InputsError(#[from] InputsError),
#[error(transparent)]
OutputsError(#[from] OutputsError),
}
impl From<TxBuilderError> for WalletError {
fn from(error: TxBuilderError) -> Self {
Self::TxBuilder(error.to_string())
}
}
+17 -12
View File
@@ -183,7 +183,7 @@ impl WalletState {
for i in 0..utxos.len() {
let funded_tx_builder = tx_builder
.clone()
.extend_ledger_inputs(utxos[..=i].iter().copied());
.extend_ledger_inputs(utxos[..=i].iter().copied())?;
let funding_delta = funded_tx_builder.funding_delta::<G>()?;
@@ -706,7 +706,7 @@ mod tests {
// - voucher v1 is ours -> should be tracked
let transfer1 = TransferOp {
inputs: Inputs::empty(),
outputs: Outputs::new(vec![Note::new(100, alice), Note::new(4, alice)]),
outputs: Outputs::new([Note::new(100, alice), Note::new(4, alice)]),
};
// immediately lock the 2nd note from `transfer1`
let locked_note = transfer1.outputs.utxo_by_index(1, &transfer1).unwrap().id();
@@ -740,8 +740,8 @@ mod tests {
voucher_cm: v2_cm,
spent_notes: vec![alice_100_nmo_utxo.id()],
transfers: vec![TransferOp {
inputs: [alice_100_nmo_utxo.id()].into(),
outputs: Outputs::new(vec![Note::new(20, bob), Note::new(80, alice)]),
inputs: Inputs::new([alice_100_nmo_utxo.id()]),
outputs: Outputs::new([Note::new(20, bob), Note::new(80, alice)]),
}],
// Unknown locked note that will be ignored
locked_notes: HashSet::from([NoteId::from(Fr::ONE)]),
@@ -843,15 +843,15 @@ mod tests {
assert_eq!(794, funded_tx_builder.net_balance());
assert_eq!(0, funded_tx_builder.funding_delta::<Gas>().unwrap());
let funded_tx = funded_tx_builder.build();
let funded_tx = funded_tx_builder.build().unwrap();
if let Op::Transfer(transfer_op) = &funded_tx.ops()[funded_tx.ops().len() - 1] {
// ensure alices utxo was used to pay the fee
assert_eq!(transfer_op.inputs, utxo2.id().into());
assert_eq!(transfer_op.inputs, Inputs::new([utxo2.id()]));
// ensure change was returned to alice
assert_eq!(
transfer_op.outputs,
Outputs::new(vec![Note {
Outputs::new([Note {
value: 4206,
pk: alice,
}])
@@ -895,7 +895,7 @@ mod tests {
signer: signing_key.public_key(),
});
tx_builder = tx_builder.push_op(inscription);
tx_builder = tx_builder.push_op(inscription).unwrap();
// Fund the transaction
let fund_attempt = wallet_state.fund_tx::<Gas>(&tx_builder, alice, [alice]);
@@ -993,6 +993,7 @@ mod tests {
tx_builder
.clone()
.add_ledger_input(Utxo::new(tx_hash(0), 0, Note::new(0, pk(0))))
.unwrap()
.gas_cost::<Gas>()
.unwrap()
.into_inner()
@@ -1011,13 +1012,14 @@ mod tests {
let funded_tx_wo_change = wallet_state
.fund_tx::<Gas>(&tx_builder, alice, [alice])
.unwrap()
.build(); // successfully funded the tx
.build()
.unwrap(); // successfully funded the tx
// verify that no change output was used.
if let Op::Transfer(transfer_op) =
&funded_tx_wo_change.ops()[funded_tx_wo_change.ops().len() - 1]
{
assert_eq!(transfer_op.outputs, Outputs::new(vec![]));
assert_eq!(transfer_op.outputs, Outputs::empty());
} else {
panic!("last op must be a transfer")
}
@@ -1028,7 +1030,9 @@ mod tests {
tx_builder
.clone()
.add_ledger_input(Utxo::new(tx_hash(0), 0, Note::new(0, pk(0))))
.unwrap()
.with_dummy_change_note()
.unwrap()
.gas_cost::<Gas>()
.unwrap()
.into_inner()
@@ -1066,13 +1070,14 @@ mod tests {
let funded_tx_wo_change = wallet_state
.fund_tx::<Gas>(&tx_builder, alice, [alice])
.unwrap()
.build(); // successfully funded the tx
.build()
.unwrap(); // successfully funded the tx
// verify that indeed a change output was used.
if let Op::Transfer(transfer_op) =
&funded_tx_wo_change.ops()[funded_tx_wo_change.ops().len() - 1]
{
assert_eq!(transfer_op.outputs, Outputs::new(vec![Note::new(1, alice)]));
assert_eq!(transfer_op.outputs, Outputs::new([Note::new(1, alice)]));
} else {
panic!("the last operation must be a transfer")
}
+7 -7
View File
@@ -153,7 +153,7 @@ mod tests {
let messages = vec![
(block_msg(1, &[1]), Slot::new(0)),
(
deposit_msg([NoteId::from(Fr::from(10u32))].into(), 0, [10].into()),
deposit_msg(Inputs::new([NoteId::from(Fr::from(10u32))]), 0, [10].into()),
Slot::new(0),
),
(block_msg(2, &[2]), Slot::new(1)),
@@ -173,7 +173,7 @@ mod tests {
let messages = vec![
(block_msg(1, &[1]), Slot::new(0)),
(
deposit_msg([NoteId::from(Fr::from(10u32))].into(), 0, [10].into()),
deposit_msg(Inputs::new([NoteId::from(Fr::from(10u32))]), 0, [10].into()),
Slot::new(1),
),
(block_msg(2, &[2]), Slot::new(2)), // after LIB
@@ -192,12 +192,12 @@ mod tests {
let messages = vec![
(block_msg(1, &[1]), Slot::new(0)),
(
deposit_msg([NoteId::from(Fr::from(10u32))].into(), 0, [10].into()),
deposit_msg(Inputs::new([NoteId::from(Fr::from(10u32))]), 0, [10].into()),
Slot::new(0),
),
(block_msg(2, &[2]), Slot::new(1)),
(
deposit_msg([NoteId::from(Fr::from(11u32))].into(), 0, [11].into()),
deposit_msg(Inputs::new([NoteId::from(Fr::from(11u32))]), 0, [11].into()),
Slot::new(2),
),
(block_msg(3, &[3]), Slot::new(2)),
@@ -217,7 +217,7 @@ mod tests {
let messages = vec![
(block_msg(1, &[1]), Slot::new(0)),
(
deposit_msg([NoteId::from(Fr::from(10u32))].into(), 0, [10].into()),
deposit_msg(Inputs::new([NoteId::from(Fr::from(10u32))]), 0, [10].into()),
Slot::new(0),
),
(block_msg(2, &[2]), Slot::new(1)),
@@ -252,7 +252,7 @@ mod tests {
let messages = vec![
(block_msg(1, &[1]), Slot::new(0)),
(
deposit_msg([NoteId::from(Fr::from(10u32))].into(), 0, [10].into()),
deposit_msg(Inputs::new([NoteId::from(Fr::from(10u32))]), 0, [10].into()),
BATCH_SIZE,
),
(
@@ -264,7 +264,7 @@ mod tests {
BATCH_SIZE.into_inner().checked_mul(2).unwrap().into(),
),
(
deposit_msg([NoteId::from(Fr::from(11u32))].into(), 0, [11].into()),
deposit_msg(Inputs::new([NoteId::from(Fr::from(11u32))]), 0, [11].into()),
BATCH_SIZE.into_inner().checked_mul(3).unwrap().into(),
),
(
+6 -6
View File
@@ -2419,7 +2419,7 @@ fn extract_inscriptions(txs: &[SignedMantleTx], channel_id: ChannelId) -> Vec<In
tx_hash,
parent_msg,
this_msg: config.id(),
payload: Inscription::empty(),
payload: [].into(),
};
last_in_block = Some(info.this_msg);
items.push(info);
@@ -2562,7 +2562,6 @@ fn sign_tx(tx_hash: TxHash, signing_key: &Ed25519Key) -> Ed25519Signature {
#[cfg(test)]
mod tests {
use async_trait::async_trait;
use lb_common_http_client::{
ApiBlock, ApiHeader, BlockInfo, ChainServiceMode, CryptarchiaInfo, State,
@@ -2571,6 +2570,7 @@ mod tests {
header::ContentId,
mantle::{
Note, Utxo,
ledger::Inputs,
ops::channel::deposit::{DepositOp, Metadata},
},
proofs::leader_proof::Groth16LeaderProof,
@@ -2616,7 +2616,7 @@ mod tests {
let (sk, utxo) = utxo_with_sk();
let deposit_op = DepositOp {
channel_id,
inputs: [utxo.id()].into(),
inputs: Inputs::new([utxo.id()]),
metadata: b"to Alice".into(),
};
@@ -2680,7 +2680,7 @@ mod tests {
use lb_groth16::Fr;
DepositOp {
channel_id,
inputs: [NoteId::from(Fr::from(input_seed))].into(),
inputs: Inputs::new([NoteId::from(Fr::from(input_seed))]),
metadata,
}
}
@@ -2826,7 +2826,7 @@ mod tests {
// finalized view, not a "what we tracked locally" view.
let channel_id = ChannelId::from([0; 32]);
let other_channel = ChannelId::from([9; 32]);
let outputs = Outputs::new(vec![Note::new(
let outputs = Outputs::new([Note::new(
42,
ZkKey::from(BigUint::from(0u64)).to_public_key(),
)]);
@@ -2901,7 +2901,7 @@ mod tests {
// withdraws field populated, so on orphan we emit
// OrphanedTx::AtomicWithdraw (not Inscription).
let channel_id = ChannelId::from([1u8; 32]);
let outputs = Outputs::new(vec![Note::new(
let outputs = Outputs::new([Note::new(
5,
ZkKey::from(BigUint::from(0u64)).to_public_key(),
)]);