mirror of
https://github.com/logos-co/nomos-node.git
synced 2026-08-27 09:31:10 +00:00
feat(ledger): Channel Withdraw (#2440)
Co-authored-by: Youngjoon Lee <5462944+youngjoon-lee@users.noreply.github.com>
This commit is contained in:
Generated
+440
-422
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,3 +1,4 @@
|
||||
pub mod channel_withdraw_proof;
|
||||
pub mod leader_claim_proof;
|
||||
pub mod leader_proof;
|
||||
mod merkle;
|
||||
|
||||
@@ -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(&[(¬e_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
@@ -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
|
||||
|
||||
@@ -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 { .. })));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,3 +1,4 @@
|
||||
mod cryptarchia;
|
||||
pub mod leader;
|
||||
pub(crate) use cryptarchia::cryptarchia_ledger_state;
|
||||
pub use cryptarchia::{Cryptarchia, cryptarchia_headers, cryptarchia_info};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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.
|
||||
///
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user