mirror of
https://github.com/logos-co/nomos-node.git
synced 2026-08-27 09:31:10 +00:00
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:
+184
-152
@@ -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]
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
@@ -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!(
|
||||
|
||||
@@ -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(),
|
||||
}]),
|
||||
|
||||
@@ -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
@@ -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),
|
||||
);
|
||||
|
||||
@@ -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,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),
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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");
|
||||
};
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
(
|
||||
|
||||
@@ -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(),
|
||||
)]);
|
||||
|
||||
Reference in New Issue
Block a user