feat(ledger): Channel Withdraw (#2440)

Co-authored-by: Youngjoon Lee <5462944+youngjoon-lee@users.noreply.github.com>
This commit is contained in:
Álex
2026-04-02 14:55:21 +09:00
committed by GitHub
co-authored by Youngjoon Lee
parent 6dde6161b6
commit a10bb64c7f
37 changed files with 2160 additions and 633 deletions
Generated
+440 -422
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -244,13 +244,13 @@ pub unsafe extern "C" fn get_known_addresses(
///
/// if (result.status == OperationStatus_Ok) {
/// KnownAddresses addresses = result.value;
///
///
/// // Use the addresses...
/// for (size_t i = 0; i < addresses.len; i++) {
/// uint8_t* address = addresses.addresses[i];
/// // Process the 32-byte address...
/// }
///
///
/// // Free the memory when done
/// free_known_addresses(addresses);
/// }
@@ -306,7 +306,7 @@ pub(crate) fn get_balance_sync(
.await;
api.get_balance(Some(tip), wallet_address)
.await
.map(|tip_response| tip_response.response)
.map(|tip_response| tip_response.response.map(|balance| balance.balance))
})
.map_err(|_| OperationStatus::DynError)
}
+148 -19
View File
@@ -5,8 +5,9 @@ use nom::{
bytes::complete::take,
combinator::{map, map_res},
error::{Error, ErrorKind},
multi::count,
number::complete::{le_u32, le_u64, u8 as decode_u8},
multi::{count, length_count},
number::complete::{le_u16, le_u32, le_u64, u8 as decode_u8},
sequence::pair,
};
use crate::{
@@ -76,6 +77,7 @@ pub fn decode_op(input: &[u8]) -> IResult<&[u8], Op> {
opcode::INSCRIBE => map(decode_channel_inscribe, Op::ChannelInscribe).parse(input),
opcode::SET_CHANNEL_KEYS => map(decode_channel_set_keys, Op::ChannelSetKeys).parse(input),
opcode::CHANNEL_DEPOSIT => map(decode_channel_deposit, Op::ChannelDeposit).parse(input),
opcode::CHANNEL_WITHDRAW => map(decode_channel_withdraw, Op::ChannelWithdraw).parse(input),
opcode::SDP_DECLARE => map(decode_sdp_declare, Op::SDPDeclare).parse(input),
opcode::SDP_WITHDRAW => map(decode_sdp_withdraw, Op::SDPWithdraw).parse(input),
opcode::SDP_ACTIVE => map(decode_sdp_active, Op::SDPActive).parse(input),
@@ -138,6 +140,13 @@ fn decode_channel_deposit(input: &[u8]) -> IResult<&[u8], DepositOp> {
))
}
fn decode_channel_withdraw(input: &[u8]) -> IResult<&[u8], ChannelWithdrawOp> {
// ChannelWithdraw = ChannelId Amount
let (input, channel_id) = map(decode_hash32, ChannelId::from).parse(input)?;
let (input, amount) = decode_uint64(input)?;
Ok((input, ChannelWithdrawOp { channel_id, amount }))
}
// ==============================================================================
// SDP Operation Decoders
// ==============================================================================
@@ -328,6 +337,11 @@ fn decode_op_proof<'a>(input: &'a [u8], op: &Op) -> IResult<&'a [u8], OpProof> {
})
.parse(input),
// ChannelWithdrawProof
Op::ChannelWithdraw(_) => {
map(decode_channel_withdraw_proof, OpProof::ChannelWithdrawProof).parse(input)
}
// None. It's indirectly signed through the Ledger Transaction signature.
Op::ChannelDeposit(_) => Ok((input, OpProof::NoProof)),
}
@@ -379,6 +393,31 @@ fn decode_ed25519_signature(input: &[u8]) -> IResult<&[u8], Ed25519Signature> {
.parse(input)
}
const fn calculate_channel_withdraw_proof_byte_size(
channel_withdraw_threshold: ChannelKeyIndex,
) -> usize {
(channel_withdraw_threshold as usize) * (ED25519_SIG_BYTES + 4)
}
fn decode_channel_withdraw_proof(input: &[u8]) -> IResult<&[u8], ChannelWithdrawProof> {
// ChannelWithdrawProof = SignatureCount *WithdrawSignature
// WithdrawSignature = Ed25519Signature Index
let (input, signatures) = length_count(
map(decode_uint16, |n: ChannelKeyIndex| n as usize),
pair(decode_ed25519_signature, decode_uint16),
)
.parse(input)?;
let signatures: Vec<WithdrawSignature> = signatures
.into_iter()
.map(|(signature, index)| WithdrawSignature::from((index, signature)))
.collect();
ChannelWithdrawProof::new(signatures)
.map(|proof| (input, proof))
.map_err(|_| nom::Err::Failure(Error::new(input, ErrorKind::Verify)))
}
fn decode_field_element(input: &[u8]) -> IResult<&[u8], Fr> {
// FieldElement = 32BYTE
map_res(take(32usize), |bytes: &[u8]| {
@@ -404,9 +443,9 @@ fn decode_array<const N: usize>(input: &[u8]) -> IResult<&[u8], [u8; N]> {
.parse(input)
}
fn decode_uint64(input: &[u8]) -> IResult<&[u8], u64> {
// UINT64 = 8BYTE
le_u64(input)
fn decode_uint16(input: &[u8]) -> IResult<&[u8], u16> {
// UINT16 = 2BYTE
le_u16(input)
}
fn decode_uint32(input: &[u8]) -> IResult<&[u8], u32> {
@@ -414,6 +453,11 @@ fn decode_uint32(input: &[u8]) -> IResult<&[u8], u32> {
le_u32(input)
}
fn decode_uint64(input: &[u8]) -> IResult<&[u8], u64> {
// UINT64 = 8BYTE
le_u64(input)
}
fn decode_byte(input: &[u8]) -> IResult<&[u8], u8> {
// Byte = OCTET
decode_u8(input)
@@ -426,9 +470,16 @@ fn decode_byte(input: &[u8]) -> IResult<&[u8], u8> {
use lb_groth16::fr_to_bytes;
use super::ops::opcode;
use crate::{
mantle::{
ops::channel::{ChannelKeyIndex, withdraw::ChannelWithdrawOp},
tx::MantleTxGasContext,
},
proofs::channel_withdraw_proof::{ChannelWithdrawProof, WithdrawSignature},
};
// Encode primitives
/// Encode primitives
fn encode_uint64(value: u64) -> Vec<u8> {
fn encode_uint16(value: u16) -> Vec<u8> {
value.to_le_bytes().to_vec()
}
@@ -436,6 +487,10 @@ fn encode_uint32(value: u32) -> Vec<u8> {
value.to_le_bytes().to_vec()
}
fn encode_uint64(value: u64) -> Vec<u8> {
value.to_le_bytes().to_vec()
}
fn encode_byte(value: u8) -> Vec<u8> {
vec![value]
}
@@ -471,6 +526,17 @@ fn encode_groth16_proof(proof: &CompressedGroth16Proof) -> Vec<u8> {
proof.to_bytes().to_vec()
}
fn encode_channel_withdraw_proof(proof: &ChannelWithdrawProof) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.extend(encode_uint16(proof.signatures().len() as ChannelKeyIndex));
bytes.extend(proof.signatures().iter().flat_map(|signature| {
encode_ed25519_signature(&signature.signature)
.into_iter()
.chain(encode_uint16(signature.channel_key_index))
}));
bytes
}
/// Encode channel operations
#[must_use]
pub fn encode_channel_inscribe(op: &InscriptionOp) -> Vec<u8> {
@@ -502,6 +568,13 @@ fn encode_channel_deposit(op: &DepositOp) -> Vec<u8> {
bytes
}
fn encode_channel_withdraw(op: &ChannelWithdrawOp) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.extend(encode_hash32(op.channel_id.as_ref()));
bytes.extend(encode_uint64(op.amount));
bytes
}
/// Encode SDP operations
fn encode_locator(locator: &multiaddr::Multiaddr) -> Vec<u8> {
let locator_bytes = locator.to_vec();
@@ -614,6 +687,10 @@ pub fn encode_op(op: &Op) -> Vec<u8> {
bytes.extend(encode_byte(opcode::CHANNEL_DEPOSIT));
bytes.extend(encode_channel_deposit(op));
}
Op::ChannelWithdraw(op) => {
bytes.extend(encode_byte(opcode::CHANNEL_WITHDRAW));
bytes.extend(encode_channel_withdraw(op));
}
Op::SDPDeclare(op) => {
bytes.extend(encode_byte(opcode::SDP_DECLARE));
bytes.extend(encode_sdp_declare(op));
@@ -654,6 +731,9 @@ fn encode_op_proof(proof: &OpProof, op: &Op) -> Vec<u8> {
encode_ed25519_signature(sig)
}
(OpProof::NoProof, Op::ChannelDeposit(_)) => Vec::new(),
(OpProof::ChannelWithdrawProof(proof), Op::ChannelWithdraw(_)) => {
encode_channel_withdraw_proof(proof)
}
(
OpProof::ZkAndEd25519Sigs {
zk_sig,
@@ -701,7 +781,7 @@ pub fn encode_signed_mantle_tx(tx: &SignedMantleTx) -> Vec<u8> {
bytes
}
pub(crate) fn predict_signed_mantle_tx_size(tx: &MantleTx) -> usize {
pub(crate) fn predict_signed_mantle_tx_size(tx: &MantleTx, context: &MantleTxGasContext) -> usize {
let mantle_tx_size = encode_mantle_tx(tx).len();
let ops_proofs_size = tx
@@ -719,6 +799,14 @@ pub(crate) fn predict_signed_mantle_tx_size(tx: &MantleTx) -> usize {
GROTH16_BYTES
}
// WithdrawProof
Op::ChannelWithdraw(operation) => {
let channel_withdraw_threshold = context.withdraw_threshold(&operation.channel_id).expect(
"Operation should have been verified before reaching this point, so the channel must exist in the context."
);
calculate_channel_withdraw_proof_byte_size(channel_withdraw_threshold)
}
// None
Op::ChannelDeposit(_) => 0,
})
@@ -729,6 +817,8 @@ pub(crate) fn predict_signed_mantle_tx_size(tx: &MantleTx) -> usize {
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use ark_ff::Field as _;
use lb_key_management_system_keys::keys::{Ed25519Key, ZkKey};
use num_bigint::BigUint;
@@ -950,7 +1040,9 @@ mod tests {
let encoded = encode_signed_mantle_tx(&signed_tx);
let predicted_size = predict_signed_mantle_tx_size(&signed_tx.mantle_tx);
let gas_context = MantleTxGasContext::new(HashMap::new());
let predicted_size =
predict_signed_mantle_tx_size(&signed_tx.mantle_tx, &gas_context);
assert_eq!(
predicted_size,
encoded.len(),
@@ -1057,7 +1149,8 @@ mod tests {
};
// Predict size
let predicted_size = predict_signed_mantle_tx_size(&mantle_tx);
let gas_context = MantleTxGasContext::new(HashMap::new());
let predicted_size = predict_signed_mantle_tx_size(&mantle_tx, &gas_context);
// Create a signed tx and encode it to get actual size
let signed_tx = SignedMantleTx::new(mantle_tx, vec![]).unwrap();
@@ -1084,7 +1177,8 @@ mod tests {
};
// Predict size
let predicted_size = predict_signed_mantle_tx_size(&mantle_tx);
let gas_context = MantleTxGasContext::new(HashMap::new());
let predicted_size = predict_signed_mantle_tx_size(&mantle_tx, &gas_context);
// Create a signed tx and encode it to get actual size
let txhash = mantle_tx.hash();
@@ -1118,7 +1212,8 @@ mod tests {
};
// Predict size
let predicted_size = predict_signed_mantle_tx_size(&mantle_tx);
let gas_context = MantleTxGasContext::new(HashMap::new());
let predicted_size = predict_signed_mantle_tx_size(&mantle_tx, &gas_context);
// Create a signed tx and encode it to get actual size
let dummy_ed25519_sig = Ed25519Signature::from_bytes(&[0; 64]);
@@ -1163,7 +1258,8 @@ mod tests {
};
// Predict size
let predicted_size = predict_signed_mantle_tx_size(&mantle_tx);
let gas_context = MantleTxGasContext::new(HashMap::new());
let predicted_size = predict_signed_mantle_tx_size(&mantle_tx, &gas_context);
// Create a signed tx and encode it to get actual size
let txhash = mantle_tx.hash();
@@ -1200,7 +1296,8 @@ mod tests {
let txhash = mantle_tx.hash();
// Predict size
let predicted_size = predict_signed_mantle_tx_size(&mantle_tx);
let gas_context = MantleTxGasContext::new(HashMap::new());
let predicted_size = predict_signed_mantle_tx_size(&mantle_tx, &gas_context);
// Create a signed tx and encode it to get actual size
let signed_tx = SignedMantleTx::new(
@@ -1242,7 +1339,8 @@ mod tests {
storage_gas_price: 50,
};
let predicted_size = predict_signed_mantle_tx_size(&mantle_tx);
let gas_context = MantleTxGasContext::new(HashMap::new());
let predicted_size = predict_signed_mantle_tx_size(&mantle_tx, &gas_context);
let txhash = mantle_tx.hash();
let signed_tx = SignedMantleTx::new(
@@ -1301,7 +1399,8 @@ mod tests {
};
// Predict size
let predicted_size = predict_signed_mantle_tx_size(&mantle_tx);
let gas_context = MantleTxGasContext::new(HashMap::new());
let predicted_size = predict_signed_mantle_tx_size(&mantle_tx, &gas_context);
let txhash = mantle_tx.hash();
let op_sig = signing_key.sign_payload(&txhash.as_signing_bytes());
@@ -1344,7 +1443,8 @@ mod tests {
};
// Predict size
let predicted_size = predict_signed_mantle_tx_size(&mantle_tx);
let gas_context = MantleTxGasContext::new(HashMap::new());
let predicted_size = predict_signed_mantle_tx_size(&mantle_tx, &gas_context);
// Create a signed tx and encode it to get actual size
let signed_tx = SignedMantleTx::new(
@@ -1405,7 +1505,8 @@ mod tests {
};
// Predict size
let predicted_size = predict_signed_mantle_tx_size(&mantle_tx);
let gas_context = MantleTxGasContext::new(HashMap::new());
let predicted_size = predict_signed_mantle_tx_size(&mantle_tx, &gas_context);
// Create a signed tx and encode it to get actual size
let txhash = mantle_tx.hash();
@@ -1444,7 +1545,8 @@ mod tests {
storage_gas_price: 50,
};
let predicted_size = predict_signed_mantle_tx_size(&mantle_tx);
let empty_gas_context = MantleTxGasContext::new(HashMap::new());
let predicted_size = predict_signed_mantle_tx_size(&mantle_tx, &empty_gas_context);
let poc_proof = Groth16LeaderClaimProof::new(
CompressedGroth16Proof::from_bytes(&[0u8; 128]),
@@ -1491,4 +1593,31 @@ mod tests {
))
);
}
#[test]
fn test_encode_decode_channel_withdraw_tx() {
let signing_key = Ed25519Key::from_bytes(&[21u8; 32]);
let mantle_tx = MantleTx {
ops: vec![Op::ChannelWithdraw(ChannelWithdrawOp {
channel_id: ChannelId::from([0xAB; 32]),
amount: 17,
})],
execution_gas_price: 100,
storage_gas_price: 50,
};
let tx_hash = mantle_tx.hash();
let proof = ChannelWithdrawProof::new(vec![WithdrawSignature::new(
0,
signing_key.sign_payload(tx_hash.as_signing_bytes().as_ref()),
)])
.unwrap();
let signed_tx =
SignedMantleTx::new(mantle_tx, vec![OpProof::ChannelWithdrawProof(proof)]).unwrap();
let encoded = encode_signed_mantle_tx(&signed_tx);
let (remaining, decoded_tx) = decode_signed_mantle_tx(&encoded).unwrap();
assert!(remaining.is_empty());
assert_eq!(decoded_tx, signed_tx);
}
}
+20 -12
View File
@@ -1,28 +1,32 @@
pub type Gas = crate::mantle::ledger::Value;
pub trait GasCost {
type Context;
/// Returns the gas cost of this operation.
fn total_gas_cost<Constants: GasConstants>(&self) -> Gas;
fn storage_gas_cost(&self) -> Gas;
fn execution_gas_consumption<Constants: GasConstants>(&self) -> Gas;
fn storage_gas_consumption(&self) -> Gas;
fn total_gas_cost<Constants: GasConstants>(&self, context: &Self::Context) -> Gas;
fn storage_gas_cost(&self, context: &Self::Context) -> Gas;
fn execution_gas_consumption<Constants: GasConstants>(&self, context: &Self::Context) -> Gas;
fn storage_gas_consumption(&self, context: &Self::Context) -> Gas;
}
impl<T: GasCost> GasCost for &T {
fn total_gas_cost<Constants: GasConstants>(&self) -> Gas {
T::total_gas_cost::<Constants>(self)
type Context = T::Context;
fn total_gas_cost<Constants: GasConstants>(&self, context: &Self::Context) -> Gas {
T::total_gas_cost::<Constants>(self, context)
}
fn storage_gas_cost(&self) -> Gas {
T::storage_gas_cost(self)
fn storage_gas_cost(&self, context: &Self::Context) -> Gas {
T::storage_gas_cost(self, context)
}
fn execution_gas_consumption<Constants: GasConstants>(&self) -> Gas {
T::execution_gas_consumption::<Constants>(self)
fn execution_gas_consumption<Constants: GasConstants>(&self, context: &Self::Context) -> Gas {
T::execution_gas_consumption::<Constants>(self, context)
}
fn storage_gas_consumption(&self) -> Gas {
T::storage_gas_consumption(self)
fn storage_gas_consumption(&self, context: &Self::Context) -> Gas {
T::storage_gas_consumption(self, context)
}
}
@@ -39,6 +43,9 @@ pub trait GasConstants {
/// Verify the deposit signature.
const CHANNEL_DEPOSIT: Gas;
/// Verify the withdrawal signature.
const CHANNEL_WITHDRAW: Gas;
/// Verify the proof of ownership.
const SDP_DECLARE: Gas;
@@ -59,6 +66,7 @@ impl GasConstants for MainnetGasConstants {
const CHANNEL_INSCRIBE: Gas = 22;
const CHANNEL_SET_KEYS: Gas = 22;
const CHANNEL_DEPOSIT: Gas = 0;
const CHANNEL_WITHDRAW: Gas = 22;
const SDP_DECLARE: Gas = 2727;
const SDP_WITHDRAW: Gas = 2705;
const SDP_ACTIVE: Gas = 2705;
+10 -6
View File
@@ -3,6 +3,8 @@ use lb_poseidon2::Digest;
use serde::{Deserialize, Serialize};
use super::{OpProof, SignedMantleTx, ops::sdp::SDPDeclareOp};
#[cfg(feature = "mock")]
use crate::mantle::tx::MantleTxGasContext;
use crate::{
crypto::ZkHasher,
mantle::{
@@ -74,11 +76,11 @@ impl GenesisTx {
#[cfg(feature = "mock")]
#[must_use]
pub fn new_mocked() -> Self {
pub fn new_mocked(context: MantleTxGasContext) -> Self {
use crate::mantle::tx_builder::MantleTxBuilder;
Self(SignedMantleTx::new_unverified(
MantleTxBuilder::new().build(),
MantleTxBuilder::new(context).build(),
vec![],
))
}
@@ -116,22 +118,24 @@ impl Transaction for GenesisTx {
}
impl GasCost for GenesisTx {
fn total_gas_cost<Constants: GasConstants>(&self) -> Gas {
type Context = ();
fn total_gas_cost<Constants: GasConstants>(&self, _context: &Self::Context) -> Gas {
// Genesis transactions have zero gas cost as per spec
0
}
fn storage_gas_cost(&self) -> Gas {
fn storage_gas_cost(&self, _context: &Self::Context) -> Gas {
// Genesis transactions have zero gas cost as per spec
0
}
fn execution_gas_consumption<Constants: GasConstants>(&self) -> Gas {
fn execution_gas_consumption<Constants: GasConstants>(&self, _context: &Self::Context) -> Gas {
// Genesis transactions have zero gas cost as per spec
0
}
fn storage_gas_consumption(&self) -> Gas {
fn storage_gas_consumption(&self, _context: &Self::Context) -> Gas {
// Genesis transactions have zero gas cost as per spec
0
}
+39 -2
View File
@@ -19,9 +19,9 @@ use lb_groth16::Fr;
pub use ledger::{Note, NoteId, Utxo, Value};
pub use ops::{Op, OpProof};
use ops::{channel::inscribe::InscriptionOp, sdp::SDPDeclareOp};
pub use tx::{MantleTx, SignedMantleTx, TxHash};
pub use tx::{MantleTx, SignedMantleTx, TxHash, VerificationError};
use crate::mantle::ops::transfer::TransferOp;
use crate::mantle::{gas::Gas, ops::transfer::TransferOp};
pub const MAX_MANTLE_TXS: usize = 1024;
@@ -50,6 +50,17 @@ pub trait AuthenticatedMantleTx: Transaction<Hash = TxHash> + GasCost + StorageS
fn mantle_tx(&self) -> &MantleTx;
fn ops_with_proof(&self) -> impl Iterator<Item = (&Op, &OpProof)>;
// Gas Cost functions with context already handled
fn total_gas_cost<Constants: GasConstants>(&self) -> Gas;
fn storage_gas_cost(&self) -> Gas;
fn execution_gas_consumption<Constants: GasConstants>(&self) -> Gas;
fn storage_gas_consumption(&self) -> Gas;
fn verify_ops_proofs_with_helper(
&self,
helper: &impl tx::OperationVerificationHelper,
) -> Result<(), VerificationError>;
}
/// A genesis transaction as specified in
@@ -84,6 +95,32 @@ impl<T: AuthenticatedMantleTx> AuthenticatedMantleTx for &T {
fn ops_with_proof(&self) -> impl Iterator<Item = (&Op, &OpProof)> {
T::ops_with_proof(self)
}
fn total_gas_cost<Constants: GasConstants>(&self) -> Gas {
<T as AuthenticatedMantleTx>::total_gas_cost::<Constants>(self)
}
fn storage_gas_cost(&self) -> Gas {
<T as AuthenticatedMantleTx>::storage_gas_cost(self)
}
fn execution_gas_consumption<Constants: GasConstants>(&self) -> Gas {
<T as AuthenticatedMantleTx>::execution_gas_consumption::<Constants>(self)
}
fn storage_gas_consumption(&self) -> Gas {
<T as AuthenticatedMantleTx>::storage_gas_consumption(self)
}
fn verify_ops_proofs_with_helper(
&self,
operation_verification_helper: &impl tx::OperationVerificationHelper,
) -> Result<(), VerificationError> {
<T as AuthenticatedMantleTx>::verify_ops_proofs_with_helper(
self,
operation_verification_helper,
)
}
}
impl<T: GenesisTx> GenesisTx for &T {
+12
View File
@@ -1,13 +1,25 @@
pub mod deposit;
pub mod inscribe;
pub mod set_keys;
pub mod withdraw;
use std::fmt::{Display, Formatter};
use crate::utils::serde_bytes_newtype;
pub type ChannelKeyIndex = u16;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct ChannelId([u8; 32]);
serde_bytes_newtype!(ChannelId, 32);
impl Display for ChannelId {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let hex_string = hex::encode(self.0);
write!(f, "{hex_string}")
}
}
/// The id of the previous message in the channel
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct MsgId([u8; 32]);
+9
View File
@@ -0,0 +1,9 @@
use serde::{Deserialize, Serialize};
use crate::mantle::{Value, ops::channel::ChannelId};
#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct ChannelWithdrawOp {
pub channel_id: ChannelId,
pub amount: Value,
}
+17 -2
View File
@@ -5,13 +5,14 @@ use super::{
channel::{deposit::DepositOp, inscribe::InscriptionOp, set_keys::SetKeysOp},
leader_claim::LeaderClaimOp,
opcode::{
CHANNEL_DEPOSIT, INSCRIBE, LEADER_CLAIM, SDP_ACTIVE, SDP_DECLARE, SDP_WITHDRAW,
SET_CHANNEL_KEYS, TRANSFER,
CHANNEL_DEPOSIT, CHANNEL_WITHDRAW, INSCRIBE, LEADER_CLAIM, SDP_ACTIVE, SDP_DECLARE,
SDP_WITHDRAW, SET_CHANNEL_KEYS, TRANSFER,
},
sdp::{SDPActiveOp, SDPDeclareOp, SDPWithdrawOp},
serde_,
transfer::TransferOp,
};
use crate::mantle::ops::channel::withdraw::ChannelWithdrawOp;
/// Core set of supported Mantle operations and their serialization behaviour.
#[derive(Serialize)]
@@ -33,6 +34,12 @@ pub enum OpSer<'a> {
)]
&'a DepositOp,
),
ChannelWithdraw(
#[serde(
serialize_with = "serde_::serialize_op_variant::<{CHANNEL_WITHDRAW}, ChannelWithdrawOp, _>"
)]
&'a ChannelWithdrawOp,
),
SDPDeclare(
#[serde(serialize_with = "serde_::serialize_op_variant::<{SDP_DECLARE}, SDPDeclareOp, _>")]
&'a SDPDeclareOp,
@@ -65,6 +72,7 @@ impl<'a> From<&'a Op> for OpSer<'a> {
Op::ChannelInscribe(op) => OpSer::ChannelInscribe(op),
Op::ChannelSetKeys(op) => OpSer::ChannelSetKeys(op),
Op::ChannelDeposit(op) => OpSer::ChannelDeposit(op),
Op::ChannelWithdraw(op) => OpSer::ChannelWithdraw(op),
Op::SDPDeclare(op) => OpSer::SDPDeclare(op),
Op::SDPWithdraw(op) => OpSer::SDPWithdraw(op),
Op::SDPActive(op) => OpSer::SDPActive(op),
@@ -96,6 +104,12 @@ pub enum OpDe {
)]
DepositOp,
),
ChannelWithdraw(
#[serde(
deserialize_with = "serde_::deserialize_op_variant::<{CHANNEL_WITHDRAW}, ChannelWithdrawOp, _>"
)]
ChannelWithdrawOp,
),
SDPDeclare(
#[serde(
deserialize_with = "serde_::deserialize_op_variant::<{SDP_DECLARE}, SDPDeclareOp, _>"
@@ -132,6 +146,7 @@ impl From<OpDe> for Op {
OpDe::ChannelInscribe(inscribe) => Self::ChannelInscribe(inscribe),
OpDe::ChannelSetKeys(channel_set_keys) => Self::ChannelSetKeys(channel_set_keys),
OpDe::ChannelDeposit(channel_deposit) => Self::ChannelDeposit(channel_deposit),
OpDe::ChannelWithdraw(channel_withdraw) => Self::ChannelWithdraw(channel_withdraw),
OpDe::SDPDeclare(sdp_declare) => Self::SDPDeclare(sdp_declare),
OpDe::SDPWithdraw(sdp_withdraw) => Self::SDPWithdraw(sdp_withdraw),
OpDe::SDPActive(sdp_active) => Self::SDPActive(sdp_active),
+12 -3
View File
@@ -6,7 +6,9 @@ pub mod sdp;
mod serde_;
pub mod transfer;
use channel::{deposit::DepositOp, inscribe::InscriptionOp, set_keys::SetKeysOp};
use channel::{
deposit::DepositOp, inscribe::InscriptionOp, set_keys::SetKeysOp, withdraw::ChannelWithdrawOp,
};
use lb_key_management_system_keys::keys::{Ed25519Signature, ZkSignature};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
@@ -26,11 +28,13 @@ use crate::{
encoding::{decode_op, encode_op},
ops::{
internal::{OpDe, OpSer},
opcode::CHANNEL_DEPOSIT,
opcode::{CHANNEL_DEPOSIT, CHANNEL_WITHDRAW},
transfer::TransferOp,
},
},
proofs::leader_claim_proof::Groth16LeaderClaimProof,
proofs::{
channel_withdraw_proof::ChannelWithdrawProof, leader_claim_proof::Groth16LeaderClaimProof,
},
};
/// Core set of supported Mantle operations.
@@ -49,6 +53,7 @@ pub enum Op {
ChannelInscribe(InscriptionOp),
ChannelSetKeys(SetKeysOp),
ChannelDeposit(DepositOp),
ChannelWithdraw(ChannelWithdrawOp),
SDPDeclare(SDPDeclareOp),
SDPWithdraw(SDPWithdrawOp),
SDPActive(SDPActiveOp),
@@ -66,6 +71,7 @@ pub enum OpProof {
ed25519_sig: Ed25519Signature,
},
PoC(Groth16LeaderClaimProof),
ChannelWithdrawProof(ChannelWithdrawProof),
}
/// Delegates serialization through the [`OpInternal`] representation.
@@ -117,6 +123,7 @@ impl Op {
Self::ChannelInscribe(_) => "ChannelInscribe",
Self::ChannelSetKeys(_) => "ChannelSetKeys",
Self::ChannelDeposit(_) => "ChannelDeposit",
Self::ChannelWithdraw(_) => "ChannelWithdraw",
Self::SDPDeclare(_) => "SDPDeclare",
Self::SDPWithdraw(_) => "SDPWithdraw",
Self::SDPActive(_) => "SDPActive",
@@ -130,6 +137,7 @@ impl Op {
Self::ChannelInscribe(_) => INSCRIBE,
Self::ChannelSetKeys(_) => SET_CHANNEL_KEYS,
Self::ChannelDeposit(_) => CHANNEL_DEPOSIT,
Self::ChannelWithdraw(_) => CHANNEL_WITHDRAW,
Self::SDPDeclare(_) => SDP_DECLARE,
Self::SDPWithdraw(_) => SDP_WITHDRAW,
Self::SDPActive(_) => SDP_ACTIVE,
@@ -144,6 +152,7 @@ impl Op {
Self::ChannelInscribe(_) => Constants::CHANNEL_INSCRIBE,
Self::ChannelSetKeys(_) => Constants::CHANNEL_SET_KEYS,
Self::ChannelDeposit(_) => Constants::CHANNEL_DEPOSIT,
Self::ChannelWithdraw(_) => Constants::CHANNEL_WITHDRAW,
Self::SDPDeclare(_) => Constants::SDP_DECLARE,
Self::SDPWithdraw(_) => Constants::SDP_WITHDRAW,
Self::SDPActive(_) => Constants::SDP_ACTIVE,
+1
View File
@@ -2,6 +2,7 @@ pub const TRANSFER: u8 = 0x00;
pub const SET_CHANNEL_KEYS: u8 = 0x10;
pub const INSCRIBE: u8 = 0x11;
pub const CHANNEL_DEPOSIT: u8 = 0x12;
pub const CHANNEL_WITHDRAW: u8 = 0x13;
pub const SDP_DECLARE: u8 = 0x20;
pub const SDP_WITHDRAW: u8 = 0x21;
pub const SDP_ACTIVE: u8 = 0x22;
+380 -24
View File
@@ -1,7 +1,11 @@
use std::sync::LazyLock;
use std::{
collections::{HashMap, HashSet},
sync::LazyLock,
};
use bytes::Bytes;
use lb_groth16::{Fr, fr_from_bytes, fr_from_bytes_unchecked, fr_to_bytes, serde::serde_fr};
use lb_key_management_system_keys::keys::Ed25519PublicKey;
use lb_poseidon2::{Digest, ZkHash};
use num_bigint::BigUint;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
@@ -12,9 +16,16 @@ use crate::{
AuthenticatedMantleTx, StorageSize, Transaction, TransactionHasher,
encoding::{decode_mantle_tx, encode_mantle_tx, encode_signed_mantle_tx},
gas::{Gas, GasConstants, GasCost},
ops::{Op, OpProof, transfer::TransferOp},
ops::{
Op, OpProof,
channel::{ChannelId, ChannelKeyIndex, withdraw::ChannelWithdrawOp},
transfer::TransferOp,
},
},
proofs::{
channel_withdraw_proof::ChannelWithdrawProof,
leader_claim_proof::{LeaderClaimProof as _, LeaderClaimPublic},
},
proofs::leader_claim_proof::{LeaderClaimProof as _, LeaderClaimPublic},
};
/// The hash of a transaction
@@ -80,6 +91,25 @@ struct MantleTxDeSerImpl {
pub storage_gas_price: Gas,
}
#[derive(Debug, Clone)]
pub struct MantleTxGasContext {
withdraw_thresholds: HashMap<ChannelId, ChannelKeyIndex>,
}
impl MantleTxGasContext {
#[must_use]
pub const fn new(withdraw_thresholds: HashMap<ChannelId, ChannelKeyIndex>) -> Self {
Self {
withdraw_thresholds,
}
}
#[must_use]
pub fn withdraw_threshold(&self, channel_id: &ChannelId) -> Option<ChannelKeyIndex> {
self.withdraw_thresholds.get(channel_id).copied()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MantleTx {
pub ops: Vec<Op>,
@@ -151,32 +181,34 @@ impl<'de> Deserialize<'de> for MantleTx {
}
impl GasCost for MantleTx {
fn total_gas_cost<Constants: GasConstants>(&self) -> Gas {
let execution_gas = self.execution_gas_consumption::<Constants>();
type Context = MantleTxGasContext;
execution_gas * self.execution_gas_price + self.storage_gas_cost()
fn total_gas_cost<Constants: GasConstants>(&self, context: &Self::Context) -> Gas {
let execution_gas = self.execution_gas_consumption::<Constants>(context);
execution_gas * self.execution_gas_price + self.storage_gas_cost(context)
}
fn storage_gas_cost(&self) -> Gas {
self.storage_gas_consumption() * self.storage_gas_price
fn storage_gas_cost(&self, context: &Self::Context) -> Gas {
self.storage_gas_consumption(context) * self.storage_gas_price
}
fn execution_gas_consumption<Constants: GasConstants>(&self) -> Gas {
fn execution_gas_consumption<Constants: GasConstants>(&self, _context: &Self::Context) -> Gas {
self.ops
.iter()
.map(Op::execution_gas::<Constants>)
.sum::<Gas>()
}
fn storage_gas_consumption(&self) -> Gas {
self.signed_serialized_size()
fn storage_gas_consumption(&self, context: &Self::Context) -> Gas {
self.signed_serialized_size(context)
}
}
impl MantleTx {
#[must_use]
pub fn signed_serialized_size(&self) -> u64 {
super::encoding::predict_signed_mantle_tx_size(self) as u64
pub fn signed_serialized_size(&self, context: &<Self as GasCost>::Context) -> u64 {
super::encoding::predict_signed_mantle_tx_size(self, context) as u64
}
#[must_use]
@@ -218,6 +250,10 @@ impl From<SignedMantleTx> for MantleTx {
}
}
// Deserializing here is dangerous, as it bypasses the verification without
// confirmation.
// TODO: Split entity into a system that allows for verification in different
// stages.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct SignedMantleTx {
pub mantle_tx: MantleTx,
@@ -241,11 +277,50 @@ pub enum VerificationError {
op_type: &'static str,
op_index: usize,
},
#[error("Number of proofs ({proofs_count}) does not match number of operations ({ops_count})")]
#[error(
"The number of proofs ({proofs_count}) does not match the number of operations ({ops_count})"
)]
ProofCountMismatch {
ops_count: usize,
proofs_count: usize,
},
#[error("Channel {channel_id} could not be found")]
ChannelNotFound { channel_id: ChannelId },
#[error("Key {key_index} could not be found in channel {channel_id}")]
KeyNotFound {
channel_id: ChannelId,
key_index: ChannelKeyIndex,
},
#[error(
"Not enough signatures in ChannelWithdrawProof at index {op_index}: got {actual}, required {required}"
)]
ChannelWithdrawProofNotEnoughSignatures {
op_index: usize,
actual: usize,
required: ChannelKeyIndex,
},
#[error("Duplicate signature indices in ChannelWithdrawProof at index {op_index}")]
ChannelWithdrawProofDuplicateIndices { op_index: usize },
#[error(
"Invalid signature in ChannelWithdrawProof at index {op_index} for signature index {signature_index}"
)]
ChannelWithdrawProofInvalidSignature {
op_index: usize,
signature_index: usize,
},
}
pub trait OperationVerificationHelper {
fn get_channel_withdraw_threshold(
&self,
channel_id: &ChannelId,
) -> Result<ChannelKeyIndex, VerificationError>;
fn get_key_from_channel_at_index(
&self,
channel_id: &ChannelId,
key_index: &ChannelKeyIndex,
) -> Result<Ed25519PublicKey, VerificationError>;
}
impl SignedMantleTx {
@@ -319,7 +394,53 @@ impl SignedMantleTx {
}
}
// Other operations are checked by the ledger or don't require verification here
_ => {}
_ => {
// TODO: If the op and proof don't match, we are silently
// delaying the error
// until tx execution.
}
}
}
Ok(())
}
pub fn verify_ops_proofs_with_helper(
&self,
operation_verification_helper: &impl OperationVerificationHelper,
) -> Result<(), VerificationError> {
let tx_hash = self.hash();
let tx_hash_bytes = tx_hash.as_signing_bytes();
for (idx, (op, proof)) in self
.mantle_tx
.ops
.iter()
.zip(self.ops_proofs.iter())
.enumerate()
{
#[expect(
clippy::single_match_else,
reason = "Clearer and follows the pattern of verify_ops_proofs."
)]
match (op, proof) {
(
Op::ChannelWithdraw(channel_withdraw_op),
OpProof::ChannelWithdrawProof(proof),
) => {
verify_channel_withdraw(
channel_withdraw_op,
proof,
&tx_hash_bytes,
operation_verification_helper,
idx,
)?;
}
// Other operations don't require verification here
_ => {
// TODO: If the op and proof don't match, we are silently
// delaying the error until tx execution.
}
}
}
@@ -331,6 +452,49 @@ impl SignedMantleTx {
}
}
fn verify_channel_withdraw(
operation: &ChannelWithdrawOp,
proof: &ChannelWithdrawProof,
tx_hash_bytes: &Bytes,
helper: &impl OperationVerificationHelper,
op_index: usize,
) -> Result<(), VerificationError> {
let channel_id = &operation.channel_id;
let withdraw_threshold = helper.get_channel_withdraw_threshold(channel_id)?;
let signatures = proof.signatures();
let signatures_len = signatures.len();
if signatures_len < withdraw_threshold as usize {
return Err(VerificationError::ChannelWithdrawProofNotEnoughSignatures {
op_index,
actual: signatures_len,
required: withdraw_threshold,
});
}
let indices_set = signatures
.iter()
.map(|signature| signature.channel_key_index)
.collect::<HashSet<_>>();
let indices_set_len = indices_set.len();
if indices_set_len != signatures_len {
return Err(VerificationError::ChannelWithdrawProofDuplicateIndices { op_index });
}
for (i, signature) in signatures.iter().enumerate() {
let public_key =
helper.get_key_from_channel_at_index(channel_id, &signature.channel_key_index)?;
if let Err(_error) = public_key.verify(tx_hash_bytes.as_ref(), &signature.signature) {
return Err(VerificationError::ChannelWithdrawProofInvalidSignature {
op_index,
signature_index: i,
});
}
}
Ok(())
}
impl Transaction for SignedMantleTx {
const HASHER: TransactionHasher<Self> =
|tx| <ZkHasher as Digest>::digest(&tx.as_signing_frs()).into();
@@ -349,20 +513,46 @@ impl AuthenticatedMantleTx for SignedMantleTx {
fn ops_with_proof(&self) -> impl Iterator<Item = (&Op, &OpProof)> {
self.mantle_tx.ops.iter().zip(self.ops_proofs.iter())
}
}
impl GasCost for SignedMantleTx {
fn total_gas_cost<Constants: GasConstants>(&self) -> Gas {
let execution_gas = self.execution_gas_consumption::<Constants>();
execution_gas * self.mantle_tx.execution_gas_price + self.storage_gas_cost()
GasCost::total_gas_cost::<Constants>(&self, &())
}
fn storage_gas_cost(&self) -> Gas {
self.storage_gas_consumption() * self.mantle_tx.storage_gas_price
GasCost::storage_gas_cost(&self, &())
}
fn execution_gas_consumption<Constants: GasConstants>(&self) -> Gas {
GasCost::execution_gas_consumption::<Constants>(&self, &())
}
fn storage_gas_consumption(&self) -> Gas {
GasCost::storage_gas_consumption(&self, &())
}
fn verify_ops_proofs_with_helper(
&self,
operation_verification_helper: &impl OperationVerificationHelper,
) -> Result<(), VerificationError> {
Self::verify_ops_proofs_with_helper(self, operation_verification_helper)
}
}
impl GasCost for SignedMantleTx {
type Context = ();
fn total_gas_cost<Constants: GasConstants>(&self, context: &Self::Context) -> Gas {
let execution_gas = GasCost::execution_gas_consumption::<Constants>(&self, context);
let storage_gas = GasCost::storage_gas_consumption(&self, context);
execution_gas * self.mantle_tx.execution_gas_price + storage_gas
}
fn storage_gas_cost(&self, context: &Self::Context) -> Gas {
let storage_gas = GasCost::storage_gas_consumption(&self, context);
storage_gas * self.mantle_tx.storage_gas_price
}
fn execution_gas_consumption<Constants: GasConstants>(&self, _context: &Self::Context) -> Gas {
self.mantle_tx
.ops
.iter()
@@ -370,7 +560,7 @@ impl GasCost for SignedMantleTx {
.sum::<Gas>()
}
fn storage_gas_consumption(&self) -> Gas {
fn storage_gas_consumption(&self, _context: &Self::Context) -> Gas {
self.gas_storage_size()
}
}
@@ -402,7 +592,10 @@ mod tests {
use lb_key_management_system_keys::keys::{Ed25519Key, ZkKey};
use super::*;
use crate::mantle::ops::channel::inscribe::InscriptionOp;
use crate::{
mantle::ops::channel::inscribe::InscriptionOp,
proofs::channel_withdraw_proof::WithdrawSignature,
};
fn create_test_mantle_tx(ops: Vec<Op>) -> MantleTx {
MantleTx {
@@ -421,6 +614,70 @@ mod tests {
}
}
struct TestOperationVerificationHelper {
thresholds: HashMap<ChannelId, ChannelKeyIndex>,
keys: HashMap<(ChannelId, ChannelKeyIndex), Ed25519PublicKey>,
}
impl TestOperationVerificationHelper {
fn new(
thresholds: impl IntoIterator<Item = (ChannelId, ChannelKeyIndex)>,
keys: impl IntoIterator<Item = ((ChannelId, ChannelKeyIndex), Ed25519PublicKey)>,
) -> Self {
Self {
thresholds: thresholds.into_iter().collect(),
keys: keys.into_iter().collect(),
}
}
}
impl OperationVerificationHelper for TestOperationVerificationHelper {
fn get_channel_withdraw_threshold(
&self,
channel_id: &ChannelId,
) -> Result<ChannelKeyIndex, VerificationError> {
self.thresholds
.get(channel_id)
.copied()
.ok_or(VerificationError::ChannelNotFound {
channel_id: *channel_id,
})
}
fn get_key_from_channel_at_index(
&self,
channel_id: &ChannelId,
key_index: &ChannelKeyIndex,
) -> Result<Ed25519PublicKey, VerificationError> {
self.keys.get(&(*channel_id, *key_index)).copied().ok_or(
VerificationError::KeyNotFound {
channel_id: *channel_id,
key_index: *key_index,
},
)
}
}
fn create_withdraw_tx(channel_id: ChannelId, signing_keys: &[&Ed25519Key]) -> SignedMantleTx {
let mantle_tx = create_test_mantle_tx(vec![Op::ChannelWithdraw(ChannelWithdrawOp {
channel_id,
amount: 5,
})]);
let tx_hash = mantle_tx.hash();
let signatures = signing_keys
.iter()
.enumerate()
.map(|(index, key)| {
WithdrawSignature::new(
index as ChannelKeyIndex,
key.sign_payload(tx_hash.as_signing_bytes().as_ref()),
)
})
.collect();
let proof = ChannelWithdrawProof::new(signatures).unwrap();
SignedMantleTx::new(mantle_tx, vec![OpProof::ChannelWithdrawProof(proof)]).unwrap()
}
#[test]
fn test_signed_mantle_tx_new_with_valid_inscribe_proof() {
let signing_key = Ed25519Key::from_bytes(&[1; 32]);
@@ -583,7 +840,7 @@ mod tests {
let err_msg = deserialized.unwrap_err().to_string();
assert_eq!(
err_msg,
"Number of proofs (0) does not match number of operations (1)"
"The number of proofs (0) does not match the number of operations (1)"
);
}
@@ -644,4 +901,103 @@ mod tests {
})
));
}
#[test]
fn helper_backed_verification_accepts_valid_channel_withdraw() {
let channel_id = ChannelId::from([8u8; 32]);
let key0 = Ed25519Key::from_bytes(&[8; 32]);
let key1 = Ed25519Key::from_bytes(&[9; 32]);
let signed_tx = create_withdraw_tx(channel_id, &[&key0, &key1]);
let helper = TestOperationVerificationHelper::new(
[(channel_id, 2)],
[
((channel_id, 0), key0.public_key()),
((channel_id, 1), key1.public_key()),
],
);
assert!(signed_tx.verify_ops_proofs_with_helper(&helper).is_ok());
}
#[test]
fn helper_backed_verification_rejects_missing_channel() {
let channel_id = ChannelId::from([10u8; 32]);
let key0 = Ed25519Key::from_bytes(&[0; 32]);
let signed_tx = create_withdraw_tx(channel_id, &[&key0]);
let helper = TestOperationVerificationHelper::new([], []);
let verification_result = signed_tx.verify_ops_proofs_with_helper(&helper);
assert_eq!(
verification_result,
Err(VerificationError::ChannelNotFound { channel_id })
);
}
#[test]
fn helper_backed_verification_rejects_missing_key() {
let channel_id = ChannelId::from([10u8; 32]);
let key0 = Ed25519Key::from_bytes(&[0; 32]);
let key1 = Ed25519Key::from_bytes(&[1; 32]);
let signed_tx = create_withdraw_tx(channel_id, &[&key0, &key1]);
let helper = TestOperationVerificationHelper::new(
[(channel_id, 2)],
[((channel_id, 0), key0.public_key())],
);
let verification_result = signed_tx.verify_ops_proofs_with_helper(&helper);
assert_eq!(
verification_result,
Err(VerificationError::KeyNotFound {
channel_id,
key_index: 1
})
);
}
#[test]
fn helper_backed_verification_rejects_not_enough_signatures() {
let channel_id = ChannelId::from([10u8; 32]);
let key0 = Ed25519Key::from_bytes(&[0; 32]);
let signed_tx = create_withdraw_tx(channel_id, &[&key0]);
let helper = TestOperationVerificationHelper::new(
[(channel_id, 2)],
[((channel_id, 0), key0.public_key())],
);
let verification_result = signed_tx.verify_ops_proofs_with_helper(&helper);
assert_eq!(
verification_result,
Err(VerificationError::ChannelWithdrawProofNotEnoughSignatures {
op_index: 0,
actual: 1,
required: 2
})
);
}
#[test]
fn helper_backed_verification_rejects_invalid_signature() {
let channel_id = ChannelId::from([10u8; 32]);
let expected_key = Ed25519Key::from_bytes(&[0; 32]);
let wrong_key = Ed25519Key::from_bytes(&[9; 32]);
let signed_tx = create_withdraw_tx(channel_id, &[&wrong_key]);
let helper = TestOperationVerificationHelper::new(
[(channel_id, 1)],
[((channel_id, 0), expected_key.public_key())],
);
let verification_result = signed_tx.verify_ops_proofs_with_helper(&helper);
assert_eq!(
verification_result,
Err(VerificationError::ChannelWithdrawProofInvalidSignature {
op_index: 0,
signature_index: 0
})
);
}
}
+29 -10
View File
@@ -1,21 +1,31 @@
use std::cmp::Ordering;
use std::{cmp::Ordering, collections::HashMap};
use lb_key_management_system_keys::keys::ZkPublicKey;
use super::{GasConstants, GasCost as _, MantleTx, Note, Op, Utxo};
use crate::mantle::{NoteId, ops::transfer::TransferOp};
use crate::{
mantle::{
NoteId,
ops::{channel::withdraw::ChannelWithdrawOp, transfer::TransferOp},
tx::MantleTxGasContext,
},
proofs::channel_withdraw_proof::ChannelWithdrawProof,
};
#[derive(Debug, Clone)]
pub struct MantleTxBuilder {
mantle_tx: MantleTx,
ledger_inputs: Vec<Utxo>,
pending_transfer: TransferOp,
// Maps a Proof to its Op by the Op Index
channel_withdraw_proofs: HashMap<usize, ChannelWithdrawProof>,
context: MantleTxGasContext,
}
// TODO: refactor to support more than 32 inputs (more than a single transfer)
impl MantleTxBuilder {
#[must_use]
pub const fn new() -> Self {
pub fn new(context: MantleTxGasContext) -> Self {
Self {
mantle_tx: MantleTx {
ops: vec![],
@@ -24,6 +34,8 @@ impl MantleTxBuilder {
},
ledger_inputs: vec![],
pending_transfer: TransferOp::new(vec![], vec![]),
channel_withdraw_proofs: HashMap::new(),
context,
}
}
@@ -38,6 +50,14 @@ impl MantleTxBuilder {
self
}
#[must_use]
pub fn push_channel_withdraw(self, op: ChannelWithdrawOp, proof: ChannelWithdrawProof) -> Self {
let mut builder = self.push_op(Op::ChannelWithdraw(op));
let index = builder.mantle_tx.ops.len() - 1;
builder.channel_withdraw_proofs.insert(index, proof);
builder
}
#[must_use]
pub fn add_ledger_input(self, utxo: Utxo) -> Self {
self.extend_ledger_inputs([utxo])
@@ -139,7 +159,7 @@ impl MantleTxBuilder {
#[must_use]
pub fn gas_cost<G: GasConstants>(&self) -> u64 {
let build = self.clone().build();
build.total_gas_cost::<G>()
build.total_gas_cost::<G>(&self.context)
}
#[must_use]
@@ -167,15 +187,14 @@ impl MantleTxBuilder {
&self.ledger_inputs
}
#[must_use]
pub const fn channel_withdraw_proofs(&self) -> &HashMap<usize, ChannelWithdrawProof> {
&self.channel_withdraw_proofs
}
#[must_use]
pub fn build(mut self) -> MantleTx {
self.mantle_tx.ops.push(Op::Transfer(self.pending_transfer));
self.mantle_tx
}
}
impl Default for MantleTxBuilder {
fn default() -> Self {
Self::new()
}
}
+122
View File
@@ -0,0 +1,122 @@
use std::cmp::Ordering;
use lb_key_management_system_keys::keys::Ed25519Signature;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::mantle::ops::channel::ChannelKeyIndex;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct WithdrawSignature {
pub channel_key_index: ChannelKeyIndex, /* Using ChannelKeyIndex ensures indices are
* bounded, and MAX provides an upper limit for the
* number of unique signatures (one per index) */
pub signature: Ed25519Signature,
}
impl WithdrawSignature {
#[must_use]
pub const fn new(channel_key_index: ChannelKeyIndex, signature: Ed25519Signature) -> Self {
Self {
channel_key_index,
signature,
}
}
}
impl From<(ChannelKeyIndex, Ed25519Signature)> for WithdrawSignature {
fn from((index, signature): (ChannelKeyIndex, Ed25519Signature)) -> Self {
Self::new(index, signature)
}
}
impl PartialOrd<Self> for WithdrawSignature {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for WithdrawSignature {
fn cmp(&self, other: &Self) -> Ordering {
self.channel_key_index
.cmp(&other.channel_key_index)
.then_with(|| self.signature.to_bytes().cmp(&other.signature.to_bytes()))
}
}
#[derive(Debug, Error)]
pub enum Error {
#[error("Duplicate indices found: {0:?}.")]
DuplicateIndices(Vec<ChannelKeyIndex>),
#[error("Too many signatures: got {actual}, maximum allowed is {maximum}.")]
TooManySignatures { actual: usize, maximum: usize },
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChannelWithdrawProof {
// Invariant: signatures are sorted by index (then signature) with no duplicates
signatures: Vec<WithdrawSignature>,
}
impl ChannelWithdrawProof {
pub fn new(signatures: Vec<WithdrawSignature>) -> Result<Self, Error> {
let signatures = Self::normalize_signatures(signatures);
Self::validate_well_formedness(&signatures)?;
Ok(Self { signatures })
}
/// Sorts and removes duplicate signatures.
///
/// This is required for the Proof to be well-formed, but it's not
/// sufficient for the Proof to be valid.
fn normalize_signatures(mut signatures: Vec<WithdrawSignature>) -> Vec<WithdrawSignature> {
signatures.sort_unstable();
signatures.dedup();
signatures
}
/// Validates that the proof is structurally well-formed.
///
/// Must be called after [`Self::normalize_signatures`].
///
/// # Checks
///
/// - No duplicate indices (each index appears at most once)
/// - Signature count doesn't exceed `ChannelKeyIndex::MAX`
///
/// # Note
///
/// This validates structural correctness only. Cryptographic validity
/// (e.g.: signature verification, threshold requirements, index-to-key
/// correspondence) must be checked separately.
fn validate_well_formedness(signatures: &[WithdrawSignature]) -> Result<(), Error> {
let unique_indices = signatures
.iter()
.map(|signature| signature.channel_key_index)
.collect::<Vec<_>>();
if unique_indices.len() != signatures.len() {
return Err(Error::DuplicateIndices(unique_indices));
}
let max_signatures_allowed = usize::from(ChannelKeyIndex::MAX) + 1;
if signatures.len() > max_signatures_allowed {
return Err(Error::TooManySignatures {
actual: signatures.len(),
maximum: max_signatures_allowed,
});
}
Ok(())
}
#[must_use]
pub const fn signatures(&self) -> &Vec<WithdrawSignature> {
&self.signatures
}
}
impl TryFrom<Vec<WithdrawSignature>> for ChannelWithdrawProof {
type Error = Error;
fn try_from(value: Vec<WithdrawSignature>) -> Result<Self, Self::Error> {
Self::new(value)
}
}
+1
View File
@@ -1,3 +1,4 @@
pub mod channel_withdraw_proof;
pub mod leader_claim_proof;
pub mod leader_proof;
mod merkle;
+5 -5
View File
@@ -648,8 +648,8 @@ pub mod tests {
use lb_core::{
crypto::{Digest as _, Hasher},
mantle::{
GasCost as _, MantleTx, Note, Op, OpProof::ZkSig, SignedMantleTx, Transaction as _,
gas::MainnetGasConstants, ops::leader_claim::VoucherCm,
AuthenticatedMantleTx, MantleTx, Note, Op, OpProof::ZkSig, SignedMantleTx,
Transaction as _, gas::MainnetGasConstants, ops::leader_claim::VoucherCm,
},
sdp::ServiceParameters,
};
@@ -1280,7 +1280,7 @@ pub mod tests {
let (tx, transfer_op, transfer_sig) =
create_tx_with_transfer(&[(&note_sk, &input_utxo)], vec![output_note1, output_note2]);
let _fees = tx.total_gas_cost::<MainnetGasConstants>();
let _fees = AuthenticatedMantleTx::total_gas_cost::<MainnetGasConstants>(&tx);
let (new_state, balance) = ledger_state
.try_apply_transfer::<(), MainnetGasConstants>(
&locked_notes,
@@ -1316,7 +1316,7 @@ pub mod tests {
vec![],
);
let locked_notes = LockedNotes::new();
let _fees = tx.total_gas_cost::<MainnetGasConstants>();
let _fees = AuthenticatedMantleTx::total_gas_cost::<MainnetGasConstants>(&tx);
let (final_state, final_balance) = new_state
.try_apply_transfer::<(), MainnetGasConstants>(
&locked_notes,
@@ -1444,7 +1444,7 @@ pub mod tests {
let (tx, transfer_op, transfer_sig) =
create_tx_with_transfer(&[(&input_sk, &input_utxo)], vec![]);
let _fees = tx.total_gas_cost::<MainnetGasConstants>();
let _fees = AuthenticatedMantleTx::total_gas_cost::<MainnetGasConstants>(&tx);
let result = ledger_state.try_apply_transfer::<(), MainnetGasConstants>(
&locked_notes,
&transfer_op,
+281 -28
View File
@@ -2,7 +2,7 @@ mod config;
// The ledger is split into two modules:
// - `cryptarchia`: the base functionalities needed by the Cryptarchia consensus
// algorithm, including a minimal UTxO model.
// - `mantle_ops` : our extensions in the form of Mantle operations, e.g. SDP.
// - `mantle_ops`: our extensions in the form of Mantle operations, e.g. SDP.
pub mod cryptarchia;
pub mod mantle;
@@ -14,7 +14,7 @@ pub use cryptarchia::{EpochState, UtxoTree};
use lb_core::{
block::BlockNumber,
mantle::{
AuthenticatedMantleTx, GenesisTx, NoteId, Op, OpProof, Utxo, Value,
AuthenticatedMantleTx, GenesisTx, NoteId, Op, OpProof, Utxo, Value, VerificationError,
gas::{Gas, GasConstants},
},
proofs::leader_proof,
@@ -25,6 +25,8 @@ use lb_groth16::{Field as _, Fr};
use mantle::LedgerState as MantleLedger;
use thiserror::Error;
use crate::mantle::helpers::MantleOperationVerificationHelper;
const WINDOW_SIZE: usize = 120;
/// Denominator of 1/(`I_max` * `D1_target` * `Delta_t` * `T`)
@@ -102,6 +104,8 @@ pub enum LedgerError<Id> {
TooMuchExecutionGas { gas: Gas, limit: Gas },
#[error("Storage fees aren't equal to the storage fee of the current epoch")]
InvalidStoragePrice,
#[error("Verification error: {0}")]
VerificationError(#[from] VerificationError),
}
#[derive(Clone, Debug, PartialEq)]
@@ -328,16 +332,17 @@ impl LedgerState {
(self, balance) = self.try_apply_tx::<_, Constants>(config, &tx)?;
// Check the transaction is balanced
match balance.cmp(&tx.total_gas_cost::<Constants>().into()) {
let total_gas_cost = AuthenticatedMantleTx::total_gas_cost::<Constants>(&tx);
match balance.cmp(&total_gas_cost.into()) {
Ordering::Less => return Err(LedgerError::InsufficientBalance),
Ordering::Greater => return Err(LedgerError::UnbalancedTransaction),
Ordering::Equal => {} // OK!
}
// Update the total of fee burned and tipped in the block
let tx_fee_burned = tx.execution_gas_consumption::<Constants>()
let tx_fee_burned = AuthenticatedMantleTx::execution_gas_consumption::<Constants>(&tx)
* self.cryptarchia_ledger.execution_base_fee()
+ tx.storage_gas_cost();
+ AuthenticatedMantleTx::storage_gas_cost(&tx);
// Check that the transaction at least pays for the base fee
if balance < tx_fee_burned.into() {
@@ -353,7 +358,8 @@ impl LedgerState {
let tx_fee_tip = balance as Gas - tx_fee_burned;
total_fee_burned += tx_fee_burned;
total_fee_tip += tx_fee_tip;
total_block_execution_gas += &tx.execution_gas_consumption::<Constants>();
total_block_execution_gas +=
&AuthenticatedMantleTx::execution_gas_consumption::<Constants>(&tx);
// Check that the block is not exceeding the Gas limit
if total_block_execution_gas > EXECUTION_GAS_LIMIT {
@@ -462,15 +468,47 @@ impl LedgerState {
self.mantle_ledger.active_sessions()
}
/// Applies a transaction to the ledger state, returning the updated state
/// and the net balance change.
///
/// # Prerequisites
///
/// A transaction must not be applied unless all required proofs have been
/// fully verified.
///
/// Proof verification is currently split across multiple paths depending on
/// the operation:
/// - `SignedMantleTx::verify_ops_proofs`: Invoked during construction
/// (`SignedMantleTx::new`, e.g. on deserialization). Handles:
/// `ChannelInscribe`, `LeaderClaim`.
/// - `SignedMantleTx::verify_ops_proofs_with_helper`: Invoked here before
/// applying the transaction. Handles: `ChannelWithdraw`.
/// - Additional validation: Performed by the ledger or implicitly satisfied
/// by certain operations.
///
/// This fragmented design means verification may be:
/// - Distributed across different stages, and
/// - Potentially duplicated or missed if assumptions about prior
/// verification are incorrect.
///
/// Callers are responsible for ensuring that all required proofs have been
/// verified before applying the transaction.
///
/// TODO: A refactor into a typed state model to enforce verification at
/// compile is planned.
fn try_apply_tx<Id, Constants: GasConstants>(
mut self,
config: &Config,
tx: impl AuthenticatedMantleTx,
) -> Result<(Self, Balance), LedgerError<Id>> {
let operation_verification_helper =
MantleOperationVerificationHelper::new(&self.mantle_ledger);
tx.verify_ops_proofs_with_helper(&operation_verification_helper)
.map_err(LedgerError::VerificationError)?;
let mut balance: Balance = 0;
let tx_hash = tx.hash();
let ops = tx.ops_with_proof().map(|(op, proof)| (op, Some(proof)));
for (op, proof) in ops {
for (op, proof) in tx.ops_with_proof() {
match (op, proof) {
// The signature for channel ops can be verified before reaching this point,
// as you only need the signer's public key and tx hash
@@ -478,12 +516,12 @@ impl LedgerState {
(Op::ChannelInscribe(op), _) => {
self.mantle_ledger = self.mantle_ledger.try_apply_channel_inscription(op)?;
}
(Op::ChannelSetKeys(op), Some(OpProof::Ed25519Sig(sig))) => {
(Op::ChannelSetKeys(op), OpProof::Ed25519Sig(sig)) => {
self.mantle_ledger = self
.mantle_ledger
.try_apply_channel_set_keys(op, sig, &tx_hash)?;
}
(Op::ChannelDeposit(op), Some(OpProof::NoProof)) => {
(Op::ChannelDeposit(op), OpProof::NoProof) => {
let deposit_amount;
(self.mantle_ledger, deposit_amount) =
self.mantle_ledger.try_apply_channel_deposit(op)?;
@@ -491,12 +529,20 @@ impl LedgerState {
.checked_sub(deposit_amount.into())
.ok_or(LedgerError::BalanceOverflow)?;
}
(Op::ChannelWithdraw(op), OpProof::ChannelWithdrawProof(_proof)) => {
let withdraw_amount;
(self.mantle_ledger, withdraw_amount) =
self.mantle_ledger.try_apply_channel_withdraw(op)?;
balance = balance
.checked_add(withdraw_amount.into())
.ok_or(LedgerError::BalanceOverflow)?;
}
(
Op::SDPDeclare(op),
Some(OpProof::ZkAndEd25519Sigs {
OpProof::ZkAndEd25519Sigs {
zk_sig,
ed25519_sig,
}),
},
) => {
self.mantle_ledger = self.mantle_ledger.try_apply_sdp_declaration(
op,
@@ -507,17 +553,17 @@ impl LedgerState {
config,
)?;
}
(Op::SDPActive(op), Some(OpProof::ZkSig(sig))) => {
(Op::SDPActive(op), OpProof::ZkSig(sig)) => {
self.mantle_ledger = self
.mantle_ledger
.try_apply_sdp_active(op, sig, tx_hash, config)?;
}
(Op::SDPWithdraw(op), Some(OpProof::ZkSig(sig))) => {
(Op::SDPWithdraw(op), OpProof::ZkSig(sig)) => {
self.mantle_ledger = self
.mantle_ledger
.try_apply_sdp_withdraw(op, sig, tx_hash, config)?;
}
(Op::LeaderClaim(op), None) => {
(Op::LeaderClaim(op), OpProof::PoC(_)) => {
// Correct derivation of the voucher nullifier and membership in the merkle tree
// can be verified outside of this function since public inputs are already
// available. Callers are expected to validate the proof
@@ -528,7 +574,7 @@ impl LedgerState {
.checked_add(reward.into())
.ok_or(LedgerError::BalanceOverflow)?;
}
(Op::Transfer(op), Some(OpProof::ZkSig(sig))) => {
(Op::Transfer(op), OpProof::ZkSig(sig)) => {
let transfer_balance;
(self.cryptarchia_ledger, transfer_balance) =
self.cryptarchia_ledger.try_apply_transfer::<_, Constants>(
@@ -553,15 +599,19 @@ impl LedgerState {
#[cfg(test)]
mod tests {
use cryptarchia::tests::{config, generate_proof, utxo};
use lb_core::mantle::{
GasCost as _, MantleTx, Note, SignedMantleTx, Transaction as _,
gas::MainnetGasConstants,
ops::{
channel::{
ChannelId, MsgId, deposit::DepositOp, inscribe::InscriptionOp, set_keys::SetKeysOp,
use lb_core::{
mantle::{
MantleTx, Note, SignedMantleTx, Transaction as _,
gas::MainnetGasConstants,
ops::{
channel::{
ChannelId, MsgId, deposit::DepositOp, inscribe::InscriptionOp,
set_keys::SetKeysOp, withdraw::ChannelWithdrawOp,
},
transfer::TransferOp,
},
transfer::TransferOp,
},
proofs::channel_withdraw_proof::{ChannelWithdrawProof, WithdrawSignature},
};
use lb_key_management_system_keys::keys::{Ed25519Key, Ed25519PublicKey, ZkKey, ZkPublicKey};
use num_bigint::BigUint;
@@ -613,6 +663,8 @@ mod tests {
enum Key {
Ed25519(Ed25519Key),
Zk(ZkKey),
EmptyZk,
Withdraw(ChannelWithdrawProof),
None,
}
@@ -638,6 +690,8 @@ mod tests {
Key::Zk(key) => OpProof::ZkSig(
ZkKey::multi_sign(std::slice::from_ref(key), tx_hash.as_ref()).unwrap(),
),
Key::EmptyZk => OpProof::ZkSig(ZkKey::multi_sign(&[], tx_hash.as_ref()).unwrap()),
Key::Withdraw(proof) => OpProof::ChannelWithdrawProof(proof.clone()),
Key::None => OpProof::NoProof,
})
.collect();
@@ -692,7 +746,7 @@ mod tests {
1,
1,
);
let fees = tx.total_gas_cost::<MainnetGasConstants>();
let fees = AuthenticatedMantleTx::total_gas_cost::<MainnetGasConstants>(&tx);
output_note.value = utxo.note.value - fees;
let tx = create_tx(vec![utxo.id()], vec![output_note], &[sk], 1, 1);
@@ -851,6 +905,205 @@ mod tests {
assert_eq!(balance, Balance::from(0));
}
#[test]
fn test_channel_withdraw_operation() {
let test_config = config();
let (sk, utxo) = utxo_with_sk();
let mut ledger_state = LedgerState::from_utxos([utxo], &test_config);
let (signing_key, verifying_key) = create_test_keys();
let channel_id = ChannelId::from([9; 32]);
ledger_state = create_channel(
ledger_state,
&test_config,
channel_id,
&signing_key,
verifying_key,
);
// Deposit some funds into the channel
let deposit = DepositOp {
channel_id,
amount: 10,
metadata: vec![5, 6, 7, 8],
};
let deposit_ops = vec![
Op::ChannelDeposit(deposit.clone()),
Op::Transfer(TransferOp {
inputs: vec![utxo.id()],
outputs: vec![Note::new(
utxo.note.value - deposit.amount,
sk.to_public_key(),
)],
}),
];
ledger_state = ledger_state
.try_apply_tx::<HeaderId, MainnetGasConstants>(
&test_config,
create_multi_signed_tx(deposit_ops, vec![&Key::None, &Key::Zk(sk)]),
)
.unwrap()
.0;
// Withdraw some funds from the channel
let withdraw = ChannelWithdrawOp {
channel_id,
amount: 6,
};
let recipient_sk = ZkKey::from(BigUint::from(99u8));
let recipient_pk = recipient_sk.to_public_key();
let transfer_op = TransferOp {
inputs: vec![],
outputs: vec![Note::new(withdraw.amount, recipient_pk)],
};
let withdraw_tx = MantleTx {
ops: vec![
Op::ChannelWithdraw(withdraw.clone()),
Op::Transfer(transfer_op.clone()),
],
execution_gas_price: 0,
storage_gas_price: 0,
};
let withdraw_tx_hash = withdraw_tx.hash();
let withdraw_proof = ChannelWithdrawProof::new(vec![WithdrawSignature::new(
0,
signing_key.sign_payload(withdraw_tx_hash.as_signing_bytes().as_ref()),
)])
.unwrap();
let signed_tx = create_multi_signed_tx(
withdraw_tx.ops,
vec![&Key::Withdraw(withdraw_proof), &Key::EmptyZk],
);
let result =
ledger_state.try_apply_tx::<HeaderId, MainnetGasConstants>(&test_config, signed_tx);
assert!(result.is_ok());
let (new_state, tx_balance) = result.unwrap();
assert_eq!(tx_balance, 0);
let channel_balance = new_state
.mantle_ledger()
.channels()
.channels
.get(&channel_id)
.unwrap()
.balance;
assert_eq!(channel_balance, 4);
let output_utxo = transfer_op.utxo_by_index(0).unwrap();
assert_eq!(output_utxo.note.value, withdraw.amount);
assert_eq!(output_utxo.note.pk, recipient_sk.to_public_key());
assert!(new_state.latest_utxos().contains(&output_utxo.id()));
}
#[test]
fn test_channel_withdraw_invalid_helper_backed_proof_fails_on_apply() {
let test_config = config();
let (sk, utxo) = utxo_with_sk();
let mut ledger_state = LedgerState::from_utxos([utxo], &test_config);
let (signing_key, verifying_key) = create_test_keys();
let channel_id = ChannelId::from([10; 32]);
ledger_state = create_channel(
ledger_state,
&test_config,
channel_id,
&signing_key,
verifying_key,
);
// Deposit some funds into the channel
let deposit = DepositOp {
channel_id,
amount: 10,
metadata: vec![],
};
let deposit_ops = vec![
Op::ChannelDeposit(deposit.clone()),
Op::Transfer(TransferOp {
inputs: vec![utxo.id()],
outputs: vec![Note::new(
utxo.note.value - deposit.amount,
sk.to_public_key(),
)],
}),
];
ledger_state = ledger_state
.try_apply_tx::<HeaderId, MainnetGasConstants>(
&test_config,
create_multi_signed_tx(deposit_ops, vec![&Key::None, &Key::Zk(sk)]),
)
.unwrap()
.0;
let channel_balance_after_deposit = ledger_state
.mantle_ledger()
.channels()
.channels
.get(&channel_id)
.unwrap()
.balance;
// Try to withdraw some funds from the channel, but with an invalid proof
let withdraw = ChannelWithdrawOp {
channel_id,
amount: 6,
};
let wrong_key = Ed25519Key::from_bytes(&[42; 32]);
let recipient_sk = ZkKey::from(BigUint::from(100u8));
let recipient_pk = recipient_sk.to_public_key();
let transfer_op = TransferOp {
inputs: vec![],
outputs: vec![Note::new(withdraw.amount, recipient_pk)],
};
let withdraw_tx = MantleTx {
ops: vec![
Op::ChannelWithdraw(withdraw),
Op::Transfer(transfer_op.clone()),
],
execution_gas_price: 0,
storage_gas_price: 0,
};
let withdraw_tx_hash = withdraw_tx.hash();
let invalid_proof = ChannelWithdrawProof::new(vec![WithdrawSignature::new(
0,
wrong_key.sign_payload(withdraw_tx_hash.as_signing_bytes().as_ref()),
)])
.unwrap();
let signed_tx = create_multi_signed_tx(
withdraw_tx.ops,
vec![&Key::Withdraw(invalid_proof), &Key::EmptyZk],
);
let result = ledger_state
.clone()
.try_apply_tx::<HeaderId, MainnetGasConstants>(&test_config, signed_tx);
assert_eq!(
result,
Err(LedgerError::VerificationError(
VerificationError::ChannelWithdrawProofInvalidSignature {
op_index: 0,
signature_index: 0,
}
))
);
let channel_balance_after_withdraw = ledger_state
.mantle_ledger()
.channels()
.channels
.get(&channel_id)
.unwrap()
.balance;
assert_eq!(channel_balance_after_deposit, 10);
assert_eq!(
channel_balance_after_deposit,
channel_balance_after_withdraw
);
let recipient_utxo = transfer_op.utxo_by_index(0).unwrap();
assert!(!ledger_state.latest_utxos().contains(&recipient_utxo.id()));
}
#[test]
fn test_invalid_parent_error() {
let test_config = config();
@@ -1104,7 +1357,7 @@ mod tests {
1,
0,
);
let fees = tx.total_gas_cost::<MainnetGasConstants>();
let fees = AuthenticatedMantleTx::total_gas_cost::<MainnetGasConstants>(&tx);
output_note.value = utxo.note.value - fees;
let tx = create_tx(vec![utxo.id()], vec![output_note], &[sk], 1, 0);
@@ -1129,7 +1382,7 @@ mod tests {
1,
);
// Pays 2925 fees = 2705 execution base fee + 0 execution tip + 220 storage
let fees = tx.total_gas_cost::<MainnetGasConstants>();
let fees = AuthenticatedMantleTx::total_gas_cost::<MainnetGasConstants>(&tx);
output_note.value = utxo.note.value - fees;
let tx = create_tx(vec![utxo.id()], vec![output_note], &[sk], 1, 1);
@@ -1165,7 +1418,7 @@ mod tests {
);
// The tx ays 2925 fees = 2705 execution base fee + 0 execution tip + 220
// storage
let fees = tx.total_gas_cost::<MainnetGasConstants>();
let fees = AuthenticatedMantleTx::total_gas_cost::<MainnetGasConstants>(&tx);
output_note.value = utxo.note.value - fees;
let tx = create_tx(
vec![utxo.id()],
@@ -1190,7 +1443,7 @@ mod tests {
);
// The tx ays 5630 fees = 2705 execution base fee + 2705 execution tip + 220
// storage
let fees = tx.total_gas_cost::<MainnetGasConstants>();
let fees = AuthenticatedMantleTx::total_gas_cost::<MainnetGasConstants>(&tx);
output_note.value = utxo.note.value - fees;
let tx = create_tx(vec![utxo.id()], vec![output_note], &[sk], 2, 1);
let result = ledger
+159 -3
View File
@@ -3,9 +3,10 @@ use std::sync::Arc;
use lb_core::mantle::{
TxHash, Value,
ops::channel::{
ChannelId, Ed25519PublicKey as PublicKey, MsgId, deposit::DepositOp,
inscribe::InscriptionOp, set_keys::SetKeysOp,
ChannelId, ChannelKeyIndex, Ed25519PublicKey as PublicKey, MsgId, deposit::DepositOp,
inscribe::InscriptionOp, set_keys::SetKeysOp, withdraw::ChannelWithdrawOp,
},
tx::MantleTxGasContext,
};
use lb_key_management_system_keys::keys::Ed25519Signature;
#[cfg(feature = "serde")]
@@ -30,6 +31,8 @@ pub enum Error {
EmptyKeys { channel_id: ChannelId },
#[error("Channel {channel_id:?} not found")]
ChannelNotFound { channel_id: ChannelId },
#[error("Insufficient funds")]
InsufficientFunds,
#[error("Balance overflow")]
BalanceOverflow,
}
@@ -40,15 +43,31 @@ pub struct Channels {
pub channels: rpds::HashTrieMapSync<ChannelId, ChannelState>,
}
impl From<&Channels> for MantleTxGasContext {
fn from(value: &Channels) -> Self {
let withdraw_thresholds = value
.channels
.iter()
.map(|(channel_id, channel)| (*channel_id, channel.withdraw_threshold))
.collect();
Self::new(withdraw_thresholds)
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChannelState {
pub tip: MsgId,
// avoid cloning the keys every new message
pub keys: Arc<[PublicKey]>,
pub keys: Arc<[PublicKey]>, // keys.len() <= ChannelKeyIndex::MAX
pub balance: Value,
// Indicating how many accredited keys are required to withdraw
// funds from the channel.
pub withdraw_threshold: ChannelKeyIndex,
}
const DEFAULT_WITHDRAW_THRESHOLD: ChannelKeyIndex = 1;
impl Default for Channels {
fn default() -> Self {
Self::new()
@@ -75,6 +94,7 @@ impl Channels {
tip: MsgId::root(),
keys: vec![*signer].into(),
balance: 0,
withdraw_threshold: DEFAULT_WITHDRAW_THRESHOLD,
});
if *parent != channel.tip {
@@ -98,11 +118,13 @@ impl Channels {
tip: msg,
keys: Arc::clone(&channel.keys),
balance: channel.balance,
withdraw_threshold: channel.withdraw_threshold,
},
);
Ok(self)
}
// TODO: Replace with CHANNEL_CONFIG op: https://github.com/logos-blockchain/logos-blockchain/issues/2461
pub fn set_keys(
mut self,
channel_id: ChannelId,
@@ -129,6 +151,9 @@ impl Channels {
tip: MsgId::root(),
keys: op.keys.clone().into(),
balance: 0,
// TODO: Replace with `ChannelConfig.withdraw_threshold`
// once this op is replaced with CHANNEL_CONFIG op: https://github.com/logos-blockchain/logos-blockchain/issues/2461
withdraw_threshold: DEFAULT_WITHDRAW_THRESHOLD,
},
);
}
@@ -150,6 +175,20 @@ impl Channels {
}
}
pub fn withdraw(mut self, op: &ChannelWithdrawOp) -> Result<Self, Error> {
if let Some(channel) = self.channels.get_mut(&op.channel_id) {
channel.balance = channel
.balance
.checked_sub(op.amount)
.ok_or(Error::InsufficientFunds)?;
Ok(self)
} else {
Err(Error::ChannelNotFound {
channel_id: op.channel_id,
})
}
}
#[must_use]
pub fn new() -> Self {
Self {
@@ -162,3 +201,120 @@ impl Channels {
self.channels.get(channel_id)
}
}
#[cfg(test)]
mod tests {
use lb_key_management_system_keys::keys::Ed25519Key;
use super::*;
fn test_public_key(seed: u8) -> PublicKey {
Ed25519Key::from_bytes(&[seed; 32]).public_key()
}
impl Channels {
#[must_use]
pub fn with_balance(channel_id: ChannelId, balance: Value) -> Self {
Self {
channels: rpds::HashTrieMapSync::new_sync().insert(
channel_id,
ChannelState {
tip: MsgId::root(),
keys: vec![test_public_key(7)].into(),
balance,
withdraw_threshold: 1,
},
),
}
}
}
#[test]
fn channels_to_gas_context_tracks_withdraw_thresholds() {
let first_id = ChannelId::from([1u8; 32]);
let second_id = ChannelId::from([2u8; 32]);
let missing_id = ChannelId::from([0u8; 32]);
let channels = Channels {
channels: rpds::HashTrieMapSync::new_sync()
.insert(
first_id,
ChannelState {
tip: MsgId::root(),
keys: vec![test_public_key(11)].into(),
balance: 5,
withdraw_threshold: 1,
},
)
.insert(
second_id,
ChannelState {
tip: MsgId::root(),
keys: vec![test_public_key(22), test_public_key(23)].into(),
balance: 9,
withdraw_threshold: 2,
},
),
};
let gas_context = MantleTxGasContext::from(&channels);
assert_eq!(gas_context.withdraw_threshold(&first_id), Some(1));
assert_eq!(gas_context.withdraw_threshold(&second_id), Some(2));
assert_eq!(gas_context.withdraw_threshold(&missing_id), None);
}
#[test]
fn deposit_increases_channel_balance() {
let channel_id = ChannelId::from([0u8; 32]);
let channels = Channels::with_balance(channel_id, 10);
let updated = channels
.deposit(&DepositOp {
channel_id,
amount: 6,
metadata: vec![],
})
.expect("deposit should succeed");
assert_eq!(updated.channel_state(&channel_id).unwrap().balance, 16);
}
#[test]
fn withdraw_decreases_channel_balance() {
let channel_id = ChannelId::from([0u8; 32]);
let channels = Channels::with_balance(channel_id, 10);
let updated = channels
.withdraw(&ChannelWithdrawOp {
channel_id,
amount: 6,
})
.expect("withdraw should succeed");
assert_eq!(updated.channel_state(&channel_id).unwrap().balance, 4);
}
#[test]
fn withdraw_fails_with_insufficient_funds() {
let channel_id = ChannelId::from([0u8; 32]);
let channels = Channels::with_balance(channel_id, 3);
let result = channels.withdraw(&ChannelWithdrawOp {
channel_id,
amount: 6,
});
assert!(matches!(result, Err(Error::InsufficientFunds)));
}
#[test]
fn withdraw_fails_for_missing_channel() {
let result = Channels::new().withdraw(&ChannelWithdrawOp {
channel_id: ChannelId::from([0u8; 32]),
amount: 1,
});
assert!(matches!(result, Err(Error::ChannelNotFound { .. })));
}
}
+53
View File
@@ -0,0 +1,53 @@
use lb_core::mantle::{
ops::channel::{ChannelId, ChannelKeyIndex},
tx::{OperationVerificationHelper, VerificationError},
};
use lb_key_management_system_keys::keys::Ed25519PublicKey;
use crate::mantle::LedgerState;
pub struct MantleOperationVerificationHelper<'a> {
ledger_state: &'a LedgerState,
}
impl<'a> MantleOperationVerificationHelper<'a> {
#[must_use]
pub const fn new(ledger_state: &'a LedgerState) -> Self {
Self { ledger_state }
}
}
impl OperationVerificationHelper for MantleOperationVerificationHelper<'_> {
fn get_channel_withdraw_threshold(
&self,
channel_id: &ChannelId,
) -> Result<ChannelKeyIndex, VerificationError> {
self.ledger_state
.channels()
.channel_state(channel_id)
.ok_or(VerificationError::ChannelNotFound {
channel_id: *channel_id,
})
.map(|channel_state| channel_state.withdraw_threshold)
}
fn get_key_from_channel_at_index(
&self,
channel_id: &ChannelId,
key_index: &ChannelKeyIndex,
) -> Result<Ed25519PublicKey, VerificationError> {
self.ledger_state
.channels()
.channel_state(channel_id)
.ok_or(VerificationError::ChannelNotFound {
channel_id: *channel_id,
})?
.keys
.get(*key_index as usize)
.ok_or(VerificationError::KeyNotFound {
channel_id: *channel_id,
key_index: *key_index,
})
.cloned()
}
}
+15 -1
View File
@@ -1,4 +1,5 @@
pub mod channel;
pub mod helpers;
pub mod leader;
pub mod sdp;
@@ -9,7 +10,10 @@ use lb_core::{
mantle::{
GenesisTx, NoteId, TxHash, Utxo, Value,
ops::{
channel::{deposit::DepositOp, inscribe::InscriptionOp, set_keys::SetKeysOp},
channel::{
deposit::DepositOp, inscribe::InscriptionOp, set_keys::SetKeysOp,
withdraw::ChannelWithdrawOp,
},
leader_claim::{LeaderClaimOp, RewardsRoot, VoucherCm},
sdp::{SDPActiveOp, SDPDeclareOp, SDPWithdrawOp},
},
@@ -184,6 +188,16 @@ impl LedgerState {
Ok((self, op.amount))
}
pub fn try_apply_channel_withdraw(
mut self,
op: &ChannelWithdrawOp,
) -> Result<(Self, Value), Error> {
self.channels = self.channels.withdraw(op).inspect_err(
|err| error!(target: LOG_TARGET, %err, "Failed to apply the Channel Withdraw message."),
)?;
Ok((self, op.amount))
}
pub fn try_apply_sdp_declaration(
mut self,
sdp_declare_op: &SDPDeclareOp,
+7 -1
View File
@@ -1,9 +1,14 @@
pub mod balance {
use std::collections::HashMap;
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
};
use lb_core::{header::HeaderId, mantle::Value};
use lb_core::{
header::HeaderId,
mantle::{NoteId, Value},
};
use lb_key_management_system_keys::keys::ZkPublicKey;
use serde::{Deserialize, Serialize};
use tracing::error;
@@ -12,6 +17,7 @@ pub mod balance {
pub struct WalletBalanceResponseBody {
pub tip: HeaderId,
pub balance: Value,
pub notes: HashMap<NoteId, Value>,
pub address: ZkPublicKey,
}
+1
View File
@@ -7,6 +7,7 @@ pub const CRYPTARCHIA_LIB_STREAM: &str = "/cryptarchia/lib-stream";
pub const NETWORK_INFO: &str = "/network/info";
pub const STORAGE_BLOCK: &str = "/storage/block";
pub const MEMPOOL_ADD_TX: &str = "/mempool/add/tx";
pub const CHANNEL: &str = "/channel/:id";
pub const CHANNEL_DEPOSIT: &str = "/channel/deposit";
pub const SDP_POST_DECLARATION: &str = "/sdp/declaration";
pub const SDP_POST_ACTIVITY: &str = "/sdp/activity";
+3 -1
View File
@@ -47,7 +47,8 @@ use crate::{
WalletService,
api::{
handlers::{
channel_deposit, leader_claim, post_activity, post_declaration, post_withdrawal,
channel, channel_deposit, leader_claim, post_activity, post_declaration,
post_withdrawal,
},
openapi::ApiDoc,
},
@@ -217,6 +218,7 @@ where
paths::MEMPOOL_ADD_TX,
routing::post(add_tx::<MempoolStorageAdapter, RuntimeServiceId>),
)
.route(paths::CHANNEL, routing::get(channel::<RuntimeServiceId>))
.route(
paths::CHANNEL_DEPOSIT,
routing::post(
+25 -3
View File
@@ -20,7 +20,8 @@ use lb_core::{
block::Block,
header::HeaderId,
mantle::{
Op, SignedMantleTx, Transaction, gas::MainnetGasConstants, tx_builder::MantleTxBuilder,
Op, SignedMantleTx, Transaction, gas::MainnetGasConstants, ops::channel::ChannelId,
tx_builder::MantleTxBuilder,
},
};
use lb_http_api_common::{
@@ -365,6 +366,25 @@ where
>(&handle, tx, Transaction::hash))
}
#[utoipa::path(
get,
path = paths::CHANNEL,
responses(
(status = 200, description = "Channel state"),
(status = 500, description = "Internal server error", body = String),
)
)]
pub async fn channel<RuntimeServiceId>(
State(handle): State<OverwatchHandle<RuntimeServiceId>>,
Path(id): Path<ChannelId>,
) -> Response
where
RuntimeServiceId:
Debug + Send + Sync + Display + 'static + AsServiceId<Cryptarchia<RuntimeServiceId>>,
{
make_request_and_return_response!(mantle::channel::<RuntimeServiceId>(&handle, id))
}
#[utoipa::path(
post,
path = paths::CHANNEL_DEPOSIT,
@@ -418,7 +438,8 @@ where
handle.relay::<WalletService>().await?,
);
let tx_builder = MantleTxBuilder::new()
let gas_context = wallet.get_gas_context(None).await?;
let tx_builder = MantleTxBuilder::new(gas_context)
.push_op(Op::ChannelDeposit(req.deposit))
.push_op(Op::Transfer(req.burn));
let lb_wallet_service::TipResponse {
@@ -705,7 +726,8 @@ pub mod wallet {
response: Some(balance),
}) => WalletBalanceResponseBody {
tip,
balance,
balance: balance.balance,
notes: balance.notes,
address,
}
.into_response(),
+1
View File
@@ -28,6 +28,7 @@ lb-chain-broadcast-service = { workspace = true }
lb-chain-leader-service = { workspace = true }
lb-chain-service = { features = ["libp2p"], workspace = true }
lb-core = { workspace = true }
lb-ledger = { workspace = true }
lb-network-service = { workspace = true }
lb-sdp-service = { workspace = true }
lb-storage-service = { features = ["rocksdb-backend"], workspace = true }
@@ -2,6 +2,7 @@ use std::fmt::{Debug, Display};
use lb_chain_service::{ConsensusMsg, CryptarchiaConsensus, CryptarchiaInfo};
use lb_core::{header::HeaderId, mantle::SignedMantleTx};
use lb_ledger::LedgerState;
use lb_storage_service::backends::rocksdb::RocksBackend;
use lb_time_service::backends::ntp::NtpTimeBackend;
use overwatch::{overwatch::handle::OverwatchHandle, services::AsServiceId};
@@ -51,3 +52,27 @@ where
Ok(receiver.await?)
}
pub async fn cryptarchia_ledger_state<RuntimeServiceId>(
handle: &OverwatchHandle<RuntimeServiceId>,
) -> Result<LedgerState, DynError>
where
RuntimeServiceId:
Debug + Send + Sync + Display + 'static + AsServiceId<Cryptarchia<RuntimeServiceId>>,
{
let info = cryptarchia_info(handle).await?;
let relay = handle.relay().await?;
let (sender, receiver) = oneshot::channel();
relay
.send(ConsensusMsg::GetLedgerState {
block_id: info.tip,
tx: sender,
})
.await
.map_err(|(e, _)| e)?;
receiver
.await?
.ok_or_else(|| "ledger state for tip must exist".into())
}
+1
View File
@@ -1,3 +1,4 @@
mod cryptarchia;
pub mod leader;
pub(crate) use cryptarchia::cryptarchia_ledger_state;
pub use cryptarchia::{Cryptarchia, cryptarchia_headers, cryptarchia_info};
+24 -10
View File
@@ -11,9 +11,10 @@ use lb_chain_service::{
use lb_core::{
block::Block,
header::HeaderId,
mantle::{SignedMantleTx, Transaction, TxHash},
mantle::{SignedMantleTx, Transaction, TxHash, ops::channel::ChannelId},
sdp::Declaration,
};
use lb_ledger::mantle::channel::ChannelState;
use lb_storage_service::{
StorageMsg, StorageService,
api::{
@@ -31,6 +32,8 @@ use serde::{Serialize, de::DeserializeOwned};
use tokio::sync::oneshot;
use tokio_stream::wrappers::BroadcastStream;
use crate::http::consensus::{Cryptarchia, cryptarchia_ledger_state};
/// A block along with the current chain state (tip and LIB) at the time it was
/// processed. This allows clients to track the canonical chain without needing
/// to poll /cryptarchia/info.
@@ -56,6 +59,23 @@ pub type MempoolService<StorageAdapter, RuntimeServiceId> = TxMempoolService<
RuntimeServiceId,
>;
pub async fn channel<RuntimeServiceId>(
handle: &overwatch::overwatch::handle::OverwatchHandle<RuntimeServiceId>,
id: ChannelId,
) -> Result<ChannelState, super::DynError>
where
RuntimeServiceId:
Debug + Send + Sync + Display + 'static + AsServiceId<Cryptarchia<RuntimeServiceId>>,
{
let ledger_state = cryptarchia_ledger_state(handle).await?;
ledger_state
.mantle_ledger()
.channels()
.channel_state(&id)
.cloned()
.ok_or_else(|| "channel not found".into())
}
pub async fn mantle_mempool_metrics<StorageAdapter, RuntimeServiceId>(
handle: &overwatch::overwatch::handle::OverwatchHandle<RuntimeServiceId>,
) -> Result<MempoolMetrics, super::DynError>
@@ -349,16 +369,10 @@ pub async fn get_sdp_declarations<RuntimeServiceId>(
handle: &overwatch::overwatch::handle::OverwatchHandle<RuntimeServiceId>,
) -> Result<Vec<Declaration>, super::DynError>
where
RuntimeServiceId: Debug
+ Send
+ Sync
+ Display
+ 'static
+ AsServiceId<super::consensus::Cryptarchia<RuntimeServiceId>>,
RuntimeServiceId:
Debug + Send + Sync + Display + 'static + AsServiceId<Cryptarchia<RuntimeServiceId>>,
{
let relay = handle
.relay::<super::consensus::Cryptarchia<RuntimeServiceId>>()
.await?;
let relay = handle.relay::<Cryptarchia<RuntimeServiceId>>().await?;
let (sender, receiver) = oneshot::channel();
relay
+7 -1
View File
@@ -81,7 +81,7 @@ pub enum Error {
#[error("Failed to create valid block during proposal: {0}")]
BlockCreation(#[from] BlockError),
#[error("Wallet API error: {0}")]
Wallet(#[from] WalletApiError),
Wallet(#[from] Box<WalletApiError>),
#[error("Leader wallet error: {0}")]
LeaderWallet(#[from] LeaderWalletError),
#[error("Mempool error: {0}")]
@@ -94,6 +94,12 @@ pub enum Error {
LedgerStateNotFound(HeaderId),
}
impl From<WalletApiError> for Error {
fn from(error: WalletApiError) -> Self {
Self::Wallet(Box::new(error))
}
}
#[derive(Debug)]
pub enum LeaderMsg {
/// Request a new receiver that yields PoL-winning slot information.
+5 -1
View File
@@ -32,7 +32,11 @@ where
Wallet: WalletServiceData,
RuntimeServiceId: Debug + Send + Sync + Display + 'static + AsServiceId<Wallet>,
{
let tx_builder = MantleTxBuilder::new().push_op(Op::LeaderClaim(op));
let gas_context = wallet
.get_gas_context(Some(tip))
.await
.map_err(|error| LeaderWalletError::WalletApi(Box::new(error)))?;
let tx_builder = MantleTxBuilder::new(gas_context).push_op(Op::LeaderClaim(op));
let funded_tx_builder = wallet
.fund_tx(
Some(tip),
+60 -8
View File
@@ -13,7 +13,8 @@ use futures::Stream;
use lb_chain_service::api::{CryptarchiaServiceApi, CryptarchiaServiceData};
use lb_core::{
block::BlockNumber,
mantle::{NoteId, SignedMantleTx, tx_builder::MantleTxBuilder},
header::HeaderId,
mantle::{NoteId, SignedMantleTx, tx::MantleTxGasContext, tx_builder::MantleTxBuilder},
sdp::{
ActiveMessage, ActivityMetadata, DeclarationId, DeclarationMessage, Locator, ProviderId,
ServiceType, WithdrawMessage,
@@ -182,8 +183,13 @@ where
SdpMessage::PostActivity { metadata, .. } => {
metrics::activity_posts_total();
self.handle_post_activity(metadata, &wallet_adapter, &mempool_adapter)
.await;
self.handle_post_activity(
metadata,
&wallet_adapter,
&mempool_adapter,
&chain_api,
)
.await;
}
SdpMessage::PostDeclaration {
declaration,
@@ -196,14 +202,20 @@ where
&wallet_adapter,
&mempool_adapter,
reply_channel,
&chain_api,
)
.await;
}
SdpMessage::PostWithdrawal { declaration_id } => {
metrics::withdrawals_total();
self.handle_post_withdrawal(declaration_id, &wallet_adapter, &mempool_adapter)
.await;
self.handle_post_withdrawal(
declaration_id,
&wallet_adapter,
&mempool_adapter,
&chain_api,
)
.await;
}
}
}
@@ -307,8 +319,13 @@ where
wallet_adapter: &WalletAdapter,
mempool_adapter: &MempoolAdapter,
reply_channel: oneshot::Sender<Result<DeclarationId, DynError>>,
chain_api: &CryptarchiaServiceApi<ChainService, RuntimeServiceId>,
) {
let tx_builder = MantleTxBuilder::new();
let Ok(gas_context) = self.get_gas_context(None, chain_api).await else {
tracing::error!("Failed to get gas context for declaration");
return;
};
let tx_builder = MantleTxBuilder::new(gas_context);
let declaration_id = declaration.id();
let signed_tx = match wallet_adapter
@@ -341,6 +358,7 @@ where
metadata: ActivityMetadata,
wallet_adapter: &WalletAdapter,
mempool_adapter: &MempoolAdapter,
chain_api: &CryptarchiaServiceApi<ChainService, RuntimeServiceId>,
) {
// Check if we have a declaration_id
let Some(ref declaration) = self.current_declaration else {
@@ -354,7 +372,11 @@ where
metadata,
};
let tx_builder = MantleTxBuilder::new();
let Ok(gas_context) = self.get_gas_context(None, chain_api).await else {
tracing::error!("Failed to get gas context for activity");
return;
};
let tx_builder = MantleTxBuilder::new(gas_context);
let signed_tx = match wallet_adapter
.active_tx(tx_builder, active_message, &self.wallet_config)
@@ -381,6 +403,7 @@ where
declaration_id: DeclarationId,
wallet_adapter: &WalletAdapter,
mempool_adapter: &MempoolAdapter,
chain_api: &CryptarchiaServiceApi<ChainService, RuntimeServiceId>,
) {
if let Err(e) = self.validate_withdrawal(&declaration_id) {
tracing::error!("{}", e);
@@ -395,7 +418,11 @@ where
nonce: self.bump_nonce(),
};
let tx_builder = MantleTxBuilder::new();
let Ok(gas_context) = self.get_gas_context(None, chain_api).await else {
tracing::error!("Failed to get gas context for withdrawal");
return;
};
let tx_builder = MantleTxBuilder::new(gas_context);
let signed_tx = match wallet_adapter
.withdraw_tx(tx_builder, withdraw_message, &self.wallet_config)
@@ -437,6 +464,31 @@ where
Ok(())
}
async fn block_id_or_tip(
&self,
block_id: Option<HeaderId>,
chain_api: &CryptarchiaServiceApi<ChainService, RuntimeServiceId>,
) -> Result<HeaderId, DynError> {
if let Some(block_id) = block_id {
Ok(block_id)
} else {
let info = chain_api.info().await?;
Ok(info.tip)
}
}
async fn get_gas_context(
&self,
block_id: Option<HeaderId>,
chain_api: &CryptarchiaServiceApi<ChainService, RuntimeServiceId>,
) -> Result<MantleTxGasContext, DynError> {
let block_id = self.block_id_or_tip(block_id, chain_api).await?;
let Some(ledger_state) = chain_api.get_ledger_state(block_id).await? else {
return Err(format!("Ledger state not found for block {block_id:?}").into());
};
Ok(ledger_state.mantle_ledger().channels().into())
}
/// Increments the nonce of the current declaration, and returns the
/// incremented nonce.
///
+96 -4
View File
@@ -1,10 +1,12 @@
use lb_core::{
header::HeaderId,
mantle::{
Note, SignedMantleTx, Value, ops::leader_claim::VoucherCm, tx_builder::MantleTxBuilder,
Note, SignedMantleTx, Value, ops::leader_claim::VoucherCm, tx::MantleTxGasContext,
tx_builder::MantleTxBuilder,
},
};
use lb_key_management_system_service::keys::ZkPublicKey;
use lb_wallet::WalletBalance;
use overwatch::{
overwatch::OverwatchHandle,
services::{
@@ -24,7 +26,7 @@ pub enum WalletApiError {
#[error("Failed to relay message with wallet:{relay_error:?}, msg={msg:?}")]
RelaySend {
relay_error: RelayError,
msg: WalletMsg,
msg: Box<WalletMsg>,
},
#[error("Failed to recv message from wallet: {0}")]
RelayRecv(#[from] RecvError),
@@ -34,6 +36,7 @@ pub enum WalletApiError {
impl From<(RelayError, WalletMsg)> for WalletApiError {
fn from((relay_error, msg): (RelayError, WalletMsg)) -> Self {
let msg = Box::new(msg);
Self::RelaySend { relay_error, msg }
}
}
@@ -95,7 +98,7 @@ where
&self,
tip: Option<HeaderId>,
pk: ZkPublicKey,
) -> Result<TipResponse<Option<Value>>, WalletApiError> {
) -> Result<TipResponse<Option<WalletBalance>>, WalletApiError> {
let (resp_tx, rx) = oneshot::channel();
self.relay
@@ -127,6 +130,17 @@ where
Ok(rx.await??)
}
pub async fn get_gas_context(
&self,
block_id: Option<HeaderId>,
) -> Result<MantleTxGasContext, WalletApiError> {
let (resp_tx, rx) = oneshot::channel();
self.relay
.send(WalletMsg::GetGasContext { block_id, resp_tx })
.await?;
Ok(rx.await??)
}
pub async fn transfer_funds(
&self,
tip: Option<HeaderId>,
@@ -135,8 +149,9 @@ where
recipient_pk: ZkPublicKey,
amount: Value,
) -> Result<TipResponse<SignedMantleTx>, WalletApiError> {
let context = self.get_gas_context(tip).await?;
let mantle_tx_builder =
MantleTxBuilder::new().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?;
@@ -193,3 +208,80 @@ where
Ok(rx.await??)
}
}
#[cfg(test)]
mod tests {
use std::fmt::{self, Display, Formatter};
use lb_core::mantle::ops::channel::{ChannelId, ChannelKeyIndex};
use overwatch::services::state::{NoOperator, NoState};
use tokio::sync::mpsc;
use super::*;
struct DummyWallet;
impl ServiceData for DummyWallet {
type Settings = WalletServiceSettings;
type State = NoState<Self::Settings>;
type StateOperator = NoOperator<Self::State>;
type Message = WalletMsg;
}
impl WalletServiceData for DummyWallet {
type Kms = ();
type Cryptarchia = ();
type Tx = ();
type Storage = ();
}
#[derive(Debug)]
struct TestRuntimeServiceId;
impl AsServiceId<DummyWallet> for TestRuntimeServiceId {
const SERVICE_ID: Self = Self;
}
impl Display for TestRuntimeServiceId {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "TestRuntimeServiceId")
}
}
#[tokio::test]
async fn get_gas_context_round_trips_through_wallet_api() {
let expected_block_id = HeaderId::from([7u8; 32]);
let expected_channel_id = ChannelId::from([9u8; 32]);
let expected_threshold: ChannelKeyIndex = 2;
let (msg_sender, mut msg_receiver) = mpsc::channel(1);
tokio::spawn(async move {
while let Some(msg) = msg_receiver.recv().await {
if let WalletMsg::GetGasContext { block_id, resp_tx } = msg {
assert_eq!(block_id, Some(expected_block_id));
let context = MantleTxGasContext::new(
std::iter::once((expected_channel_id, expected_threshold)).collect(),
);
drop(resp_tx.send(Ok(context)));
break;
}
}
});
let api =
WalletApi::<DummyWallet, TestRuntimeServiceId>::new(OutboundRelay::new(msg_sender));
let context = api
.get_gas_context(Some(expected_block_id))
.await
.expect("gas context should round-trip through the wallet API");
assert_eq!(
context.withdraw_threshold(&expected_channel_id),
Some(expected_threshold)
);
assert_eq!(
context.withdraw_threshold(&ChannelId::from([1u8; 32])),
None
);
}
}
+59 -8
View File
@@ -15,7 +15,7 @@ use lb_core::{
block::Block,
header::HeaderId,
mantle::{
AuthenticatedMantleTx, Op, OpProof, SignedMantleTx, Transaction as _, TxHash, Utxo, Value,
AuthenticatedMantleTx, Op, OpProof, SignedMantleTx, Transaction as _, TxHash, Utxo,
gas::MainnetGasConstants,
ops::{
channel::{ChannelId, inscribe::InscriptionOp, set_keys::SetKeysOp},
@@ -24,6 +24,7 @@ use lb_core::{
},
sdp::{SDPActiveOp, SDPDeclareOp, SDPWithdrawOp},
},
tx::MantleTxGasContext,
tx_builder::MantleTxBuilder,
},
proofs::leader_claim_proof::{Groth16LeaderClaimProof, LeaderClaimPrivate, LeaderClaimPublic},
@@ -45,7 +46,7 @@ use lb_services_utils::{
};
use lb_storage_service::{api::chain::StorageChainApi, backends::StorageBackend};
use lb_utxotree::MerklePath;
use lb_wallet::{WalletBlock, WalletError};
use lb_wallet::{WalletBalance, WalletBlock, WalletError};
use overwatch::{
DynError, OpaqueServiceResourcesHandle,
services::{AsServiceId, ServiceCore, ServiceData},
@@ -105,6 +106,9 @@ pub enum WalletServiceError {
#[error("blocking task failed: {0}")]
TaskJoin(#[from] JoinError),
#[error("Failed to fetch Channel Withdraw proof for op index {0} from the TxBuilder")]
ChannelWithdrawProofNotFound(usize),
}
#[derive(Debug)]
@@ -112,7 +116,7 @@ pub enum WalletMsg {
GetBalance {
tip: Option<HeaderId>,
pk: ZkPublicKey,
resp_tx: Sender<Result<TipResponse<Option<Value>>, WalletServiceError>>,
resp_tx: Sender<Result<TipResponse<Option<WalletBalance>>, WalletServiceError>>,
},
FundTx {
tip: Option<HeaderId>,
@@ -141,6 +145,10 @@ pub enum WalletMsg {
GetKnownAddresses {
resp_tx: Sender<Result<Vec<ZkPublicKey>, WalletServiceError>>,
},
GetGasContext {
block_id: Option<HeaderId>,
resp_tx: Sender<Result<MantleTxGasContext, WalletServiceError>>,
},
}
#[derive(Debug)]
@@ -171,7 +179,8 @@ impl WalletMsg {
| Self::FundTx { tip, .. }
| Self::SignTx { tip, .. }
| Self::GetLeaderAgedNotes { tip, .. }
| Self::GetClaimableVoucher { tip, .. } => *tip,
| Self::GetClaimableVoucher { tip, .. }
| Self::GetGasContext { block_id: tip, .. } => *tip,
Self::GenerateNewVoucherSecret { .. } | Self::GetKnownAddresses { .. } => None,
}
}
@@ -367,6 +376,7 @@ where
}
}
#[expect(clippy::too_many_lines, reason = "TODO: Address this at some point.")]
async fn handle_wallet_message(
msg: WalletMsg,
state: &mut ServiceState<'_>,
@@ -477,13 +487,16 @@ where
WalletMsg::GetKnownAddresses { resp_tx } => {
Self::get_known_addresses(state.wallet(), resp_tx);
}
WalletMsg::GetGasContext { block_id, resp_tx } => {
Self::get_gas_context(block_id, resp_tx, cryptarchia).await;
}
}
}
async fn handle_get_balance(
tip: Option<HeaderId>,
pk: ZkPublicKey,
resp_tx: Sender<Result<TipResponse<Option<u64>>, WalletServiceError>>,
resp_tx: Sender<Result<TipResponse<Option<WalletBalance>>, WalletServiceError>>,
wallet: &Wallet,
cryptarchia: &CryptarchiaServiceApi<Cryptarchia, RuntimeServiceId>,
) {
@@ -508,7 +521,7 @@ where
}
}
async fn sign_insciption(
async fn sign_inscription(
tx_hash: TxHash,
inscribe_op: &InscriptionOp,
kms: &KmsServiceApi<Kms, RuntimeServiceId>,
@@ -673,19 +686,26 @@ where
.map(|utxo| utxo.note.pk)
.collect();
let mut channel_withdraw_proofs = tx_builder.channel_withdraw_proofs().clone();
let mantle_tx = tx_builder.build();
let tx_hash = mantle_tx.hash();
let mut ops_proofs = Vec::new();
for op in &mantle_tx.ops {
for (i, op) in mantle_tx.ops.iter().enumerate() {
let proof = match op {
Op::ChannelInscribe(inscribe_op) => {
Self::sign_insciption(tx_hash, inscribe_op, kms).await?
Self::sign_inscription(tx_hash, inscribe_op, kms).await?
}
Op::ChannelSetKeys(set_keys_op) => {
Self::sign_channel_set_key(tx_hash, set_keys_op, &ledger, kms).await?
}
Op::ChannelDeposit(_deposit_op) => OpProof::NoProof,
Op::ChannelWithdraw(_channel_withdraw_op) => {
let proof = channel_withdraw_proofs
.remove(&i)
.ok_or(WalletServiceError::ChannelWithdrawProofNotFound(i))?;
OpProof::ChannelWithdrawProof(proof)
}
Op::SDPDeclare(declare_op) => {
Self::sign_sdp_declare(tx_hash, declare_op, &ledger, kms).await?
}
@@ -1093,4 +1113,35 @@ where
error!(err = ?e, "Failed to send known addresses response");
}
}
async fn get_gas_context(
block_id: Option<HeaderId>,
resp_tx: Sender<Result<MantleTxGasContext, WalletServiceError>>,
cryptarchia: &CryptarchiaServiceApi<Cryptarchia, RuntimeServiceId>,
) {
let block_id = match Self::msg_tip_or_latest(block_id, cryptarchia).await {
Ok(block_id) => block_id,
Err(error) => {
Self::send_err(resp_tx, error);
return;
}
};
let ledger_state = match cryptarchia.get_ledger_state(block_id).await {
Ok(Some(ledger_state)) => ledger_state,
Ok(None) => {
Self::send_err(resp_tx, WalletServiceError::LedgerStateNotFound(block_id));
return;
}
Err(err) => {
Self::send_err(resp_tx, WalletServiceError::from(err));
return;
}
};
let gas_context = ledger_state.mantle_ledger().channels().into();
if let Err(e) = resp_tx.send(Ok(gas_context)) {
error!(err = ?e, "Failed to send gas context response");
}
}
}
@@ -43,7 +43,7 @@ impl Display for WalletStateType {
use std::str::FromStr;
use lb_core::mantle::OpProof;
use lb_core::mantle::{OpProof, tx::MantleTxGasContext};
use lb_http_api_common::bodies::wallet::transfer_funds::WalletTransferFundsRequestBody;
use crate::cucumber::{
@@ -83,7 +83,8 @@ pub async fn create_and_submit_transaction(
ref wallet_account, ..
} => {
let wallet_state = wallet_state_from_utxos(available_utxos);
let mut tx_builder = MantleTxBuilder::new();
let empty_context = MantleTxGasContext::new(HashMap::new());
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));
}
@@ -240,7 +240,6 @@ impl<'a, E: LbcScenarioEnv + LbcBlockFeedEnv> InscriptionRunner<'a, E> {
let Some(channel) = self.channels.get_mut(channel_idx) else {
return Ok(());
};
let (tx, msg_id, tx_hash) = build_inscription_transaction(channel, self.payload_bytes)?;
submit_transaction_via_cluster(self.ctx, Arc::new(tx)).await?;
@@ -9,7 +9,7 @@ use std::{
use async_trait::async_trait;
use lb_core::mantle::{
GenesisTx as _, Note, OpProof, SignedMantleTx, Transaction as _, Utxo,
GenesisTx as _, Note, OpProof, SignedMantleTx, Transaction as _, Utxo, tx::MantleTxGasContext,
tx_builder::MantleTxBuilder,
};
use lb_key_management_system_service::keys::{ZkKey, ZkPublicKey};
@@ -184,13 +184,13 @@ impl<'a, E: LbcScenarioEnv> Submission<'a, E> {
}
async fn execute(mut self) -> Result<(), DynError> {
let gas_context = MantleTxGasContext::new(HashMap::new());
while let Some(input) = self.plan.pop_front() {
submit_wallet_transaction(self.ctx, &input).await?;
submit_wallet_transaction(self.ctx, &input, gas_context.clone()).await?;
if !self.interval.is_zero() {
sleep(self.interval).await;
}
}
Ok(())
}
}
@@ -198,8 +198,9 @@ impl<'a, E: LbcScenarioEnv> Submission<'a, E> {
async fn submit_wallet_transaction(
ctx: &RunContext<impl LbcScenarioEnv>,
input: &WalletInput,
gas_context: MantleTxGasContext,
) -> Result<(), DynError> {
let signed_tx = Arc::new(build_wallet_transaction(input)?);
let signed_tx = Arc::new(build_wallet_transaction(input, gas_context)?);
submit_transaction_via_cluster(ctx, signed_tx).await
}
@@ -264,8 +265,11 @@ fn cluster_client_exhausted_error() -> DynError {
TxWorkloadError::ClusterClientExhausted.into()
}
fn build_wallet_transaction(input: &WalletInput) -> Result<SignedMantleTx, DynError> {
let tx = MantleTxBuilder::new()
fn build_wallet_transaction(
input: &WalletInput,
gas_context: MantleTxGasContext,
) -> Result<SignedMantleTx, DynError> {
let tx = MantleTxBuilder::new(gas_context)
.add_ledger_input(input.utxo)
.add_ledger_output(Note::new(input.utxo.note.value, input.account.public_key()))
.build();
+77 -47
View File
@@ -140,13 +140,17 @@ impl WalletState {
}
#[must_use]
pub fn balance(&self, pk: ZkPublicKey) -> Option<Value> {
let balance = self
.pk_index
.get(&pk)?
.iter()
.map(|id| self.utxos[id].note.value)
.sum();
pub fn balance(&self, pk: ZkPublicKey) -> Option<WalletBalance> {
let mut balance = WalletBalance {
balance: 0,
notes: HashMap::new(),
};
self.pk_index.get(&pk)?.iter().for_each(|id| {
let value = self.utxos[id].note.value;
balance.balance += value;
balance.notes.insert(*id, value);
});
Some(balance)
}
@@ -269,7 +273,11 @@ where
Ok(())
}
pub fn balance(&self, tip: HeaderId, pk: ZkPublicKey) -> Result<Option<Value>, WalletError> {
pub fn balance(
&self,
tip: HeaderId,
pk: ZkPublicKey,
) -> Result<Option<WalletBalance>, WalletError> {
Ok(self.wallet_state_at(tip)?.balance(pk))
}
@@ -326,6 +334,12 @@ where
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WalletBalance {
pub balance: Value,
pub notes: HashMap<NoteId, Value>,
}
#[cfg(test)]
mod tests {
use std::{
@@ -340,6 +354,7 @@ mod tests {
Note, Op, TxHash,
gas::MainnetGasConstants as Gas,
ops::channel::{ChannelId, MsgId, inscribe::InscriptionOp},
tx::MantleTxGasContext,
},
sdp::{MinStake, ServiceParameters, ServiceType},
};
@@ -404,7 +419,10 @@ mod tests {
genesis,
&ledger,
);
assert_eq!(wallet.balance(genesis, alice).unwrap(), Some(104));
assert_eq!(
wallet.balance(genesis, alice).unwrap().unwrap().balance,
104
);
assert_eq!(wallet.balance(genesis, bob).unwrap(), None);
assert_eq!(
wallet.vouchers().get(&voucher_cm),
@@ -414,7 +432,7 @@ mod tests {
let wallet =
Wallet::<_, TestVoucherId>::from_lib([(bob, 2)], Vouchers::default(), genesis, &ledger);
assert_eq!(wallet.balance(genesis, alice).unwrap(), None);
assert_eq!(wallet.balance(genesis, bob).unwrap(), Some(20));
assert_eq!(wallet.balance(genesis, bob).unwrap().unwrap().balance, 20);
let wallet = Wallet::<_, TestVoucherId>::from_lib(
[(alice, 1), (bob, 2)],
@@ -422,8 +440,11 @@ mod tests {
genesis,
&ledger,
);
assert_eq!(wallet.balance(genesis, alice).unwrap(), Some(104));
assert_eq!(wallet.balance(genesis, bob).unwrap(), Some(20));
assert_eq!(
wallet.balance(genesis, alice).unwrap().unwrap().balance,
104
);
assert_eq!(wallet.balance(genesis, bob).unwrap().unwrap().balance, 20);
}
#[test]
@@ -475,24 +496,33 @@ mod tests {
assert_eq!(wallet.balance(genesis, alice).unwrap(), None);
assert_eq!(wallet.balance(genesis, bob).unwrap(), None);
assert_eq!(wallet.balance(block_1.id, alice).unwrap(), Some(104));
assert_eq!(
wallet.balance(block_1.id, alice).unwrap().unwrap().balance,
104
);
assert_eq!(wallet.balance(block_1.id, bob).unwrap(), None);
assert_eq!(wallet.balance(block_2.id, alice).unwrap(), Some(84));
assert_eq!(wallet.balance(block_2.id, bob).unwrap(), Some(20));
assert_eq!(
wallet.balance(block_2.id, alice).unwrap().unwrap().balance,
84
);
assert_eq!(
wallet.balance(block_2.id, bob).unwrap().unwrap().balance,
20
);
}
#[test]
fn test_fund_tx_with_change() {
let alice = pk(1);
let alice_utxo = Utxo::new(tx_hash(0), 0, Note::new(5000, alice));
let ledger_state = LedgerState::from_utxos([alice_utxo], &ledger_config());
let wallet_state = WalletState::from_ledger(
&HashMap::from_iter([(alice, 1)]),
&LedgerState::from_utxos([alice_utxo], &ledger_config()),
);
let wallet_state =
WalletState::from_ledger(&HashMap::from_iter([(alice, 1)]), &ledger_state);
let tx_builder = MantleTxBuilder::new()
let context: MantleTxGasContext = ledger_state.mantle_ledger().channels().into();
let tx_builder = MantleTxBuilder::new(context)
.set_execution_gas_price(1)
.set_storage_gas_price(1);
@@ -526,21 +556,20 @@ mod tests {
#[test]
fn test_fund_tx_insufficient_funds() {
let alice = pk(1);
let wallet_state = WalletState::from_ledger(
&HashMap::from_iter([(alice, 1)]),
&LedgerState::from_utxos(
[
Utxo::new(tx_hash(0), 0, Note::new(100, alice)),
Utxo::new(tx_hash(0), 1, Note::new(100, alice)),
Utxo::new(tx_hash(0), 2, Note::new(100, alice)),
Utxo::new(tx_hash(0), 3, Note::new(100, alice)),
],
&ledger_config(),
),
let ledger_state = LedgerState::from_utxos(
[
Utxo::new(tx_hash(0), 0, Note::new(100, alice)),
Utxo::new(tx_hash(0), 1, Note::new(100, alice)),
Utxo::new(tx_hash(0), 2, Note::new(100, alice)),
Utxo::new(tx_hash(0), 3, Note::new(100, alice)),
],
&ledger_config(),
);
let mut tx_builder = MantleTxBuilder::new()
let wallet_state =
WalletState::from_ledger(&HashMap::from_iter([(alice, 1)]), &ledger_state);
let context: MantleTxGasContext = ledger_state.mantle_ledger().channels().into();
let mut tx_builder = MantleTxBuilder::new(context)
.set_execution_gas_price(1)
.set_storage_gas_price(1);
@@ -567,13 +596,13 @@ mod tests {
#[test]
fn test_fund_tx_zero_funds() {
let alice = pk(1);
let ledger_state = LedgerState::from_utxos([], &ledger_config());
let wallet_state = WalletState::from_ledger(
&HashMap::from_iter([(alice, 1)]),
&LedgerState::from_utxos([], &ledger_config()),
);
let wallet_state =
WalletState::from_ledger(&HashMap::from_iter([(alice, 1)]), &ledger_state);
let tx_builder = MantleTxBuilder::new()
let context: MantleTxGasContext = ledger_state.mantle_ledger().channels().into();
let tx_builder = MantleTxBuilder::new(context)
.set_execution_gas_price(1)
.set_storage_gas_price(1);
@@ -589,16 +618,16 @@ mod tests {
fn test_fund_tx_respects_pk_list() {
let alice = pk(1);
let bob = pk(2);
let wallet_state = WalletState::from_ledger(
&HashMap::from_iter([(alice, 1), (bob, 2)]),
&LedgerState::from_utxos(
[Utxo::new(tx_hash(0), 0, Note::new(1_000_000, bob))],
&ledger_config(),
),
let ledger_state = LedgerState::from_utxos(
[Utxo::new(tx_hash(0), 0, Note::new(1_000_000, bob))],
&ledger_config(),
);
let tx_builder = MantleTxBuilder::new()
let wallet_state =
WalletState::from_ledger(&HashMap::from_iter([(alice, 1), (bob, 2)]), &ledger_state);
let context: MantleTxGasContext = ledger_state.mantle_ledger().channels().into();
let tx_builder = MantleTxBuilder::new(context)
.set_execution_gas_price(1)
.set_storage_gas_price(1);
@@ -620,7 +649,8 @@ mod tests {
fn test_fund_tx_unfundable_region() {
let alice = pk(1);
let tx_builder = MantleTxBuilder::new()
let context = MantleTxGasContext::new(HashMap::new());
let tx_builder = MantleTxBuilder::new(context)
.set_execution_gas_price(1)
.set_storage_gas_price(1);