chore: migrate proofs to NomEncode and NomDecode (#3078)

This commit is contained in:
Antonio
2026-07-03 13:18:00 +02:00
committed by GitHub
parent edd184fc65
commit 2c1fda553e
19 changed files with 296 additions and 222 deletions
Generated
+1
View File
@@ -4452,6 +4452,7 @@ dependencies = [
"logos-blockchain-poseidon2",
"logos-blockchain-utils",
"logos-blockchain-utxotree",
"logos-blockchain-zksign",
"multiaddr",
"nom 8.0.0",
"num-bigint",
+1
View File
@@ -32,6 +32,7 @@ lb-pol = { workspace = true }
lb-poseidon2 = { workspace = true }
lb-utils = { workspace = true }
lb-utxotree = { workspace = true }
lb-zksign = { workspace = true }
multiaddr = { workspace = true }
nom = { features = ["alloc"], workspace = true }
num-bigint = { workspace = true }
+36 -129
View File
@@ -1,14 +1,12 @@
use lb_groth16::{CompressedGroth16Proof, Fr, fr_from_bytes};
use lb_key_management_system_keys::keys::{Ed25519Signature, ZkSignature};
use lb_groth16::{COMPRESSED_PROOF_SIZE, Fr, fr_from_bytes};
use lb_key_management_system_keys::keys::{ED25519_SIGNATURE_SIZE, Ed25519Signature, ZkSignature};
use lb_utils::bounded_vec::UpperBoundedVec;
use nom::{
IResult, Parser as _,
bytes::complete::take,
combinator::{map, map_res},
error::{Error, ErrorKind},
multi::length_count,
number::complete::{le_u16, le_u64},
sequence::pair,
number::complete::le_u64,
};
use time::OffsetDateTime;
@@ -83,12 +81,12 @@ fn decode_ops_proofs<'a>(input: &'a [u8], ops: &[Op]) -> IResult<&'a [u8], Vec<O
fn decode_op_proof<'a>(input: &'a [u8], op: &Op) -> IResult<&'a [u8], OpProof> {
match op {
// Ed25519SigProof = Ed25519Signature
Op::ChannelInscribe(_) => map(decode_ed25519_signature, OpProof::Ed25519Sig).parse(input),
Op::ChannelInscribe(_) => map(Ed25519Signature::decode, OpProof::Ed25519Sig).parse(input),
// ZkAndEd25519SigsProof = ZkSignature Ed25519Signature
Op::SDPDeclare(_) => {
let (input, zk_sig) = decode_zk_signature(input)?;
let (input, ed25519_sig) = decode_ed25519_signature(input)?;
let (input, zk_sig) = ZkSignature::decode(input)?;
let (input, ed25519_sig) = Ed25519Signature::decode(input)?;
Ok((
input,
OpProof::ZkAndEd25519Sigs {
@@ -100,21 +98,16 @@ fn decode_op_proof<'a>(input: &'a [u8], op: &Op) -> IResult<&'a [u8], OpProof> {
// ZkSigProof = ZkSignature
Op::SDPWithdraw(_) | Op::SDPActive(_) | Op::Transfer(_) | Op::ChannelDeposit(_) => {
map(decode_zk_signature, OpProof::ZkSig).parse(input)
map(ZkSignature::decode, OpProof::ZkSig).parse(input)
}
// ProofOfClaimProof = Groth16
Op::LeaderClaim(_) => map(decode_groth16, |proof| {
OpProof::PoC(Groth16LeaderClaimProof::new(proof))
})
.parse(input),
Op::LeaderClaim(_) => map(Groth16LeaderClaimProof::decode, OpProof::PoC).parse(input),
// ChannelMultiSigProof — also used by ChannelConfig (threshold sigs)
Op::ChannelWithdraw(_) | Op::ChannelConfig(_) => map(
decode_channel_multi_sig_proof,
OpProof::ChannelMultiSigProof,
)
.parse(input),
Op::ChannelWithdraw(_) | Op::ChannelConfig(_) => {
map(ChannelMultiSigProof::decode, OpProof::ChannelMultiSigProof).parse(input)
}
}
}
@@ -122,53 +115,9 @@ fn decode_op_proof<'a>(input: &'a [u8], op: &Op) -> IResult<&'a [u8], OpProof> {
// Cryptographic Primitive Decoders
// ==============================================================================
fn decode_zk_signature(input: &[u8]) -> IResult<&[u8], ZkSignature> {
// ZkSignature = Groth16
map(decode_groth16, ZkSignature::new).parse(input)
}
const GROTH16_BYTES: usize = 128;
fn decode_groth16(input: &[u8]) -> IResult<&[u8], CompressedGroth16Proof> {
// Groth16 = 128BYTE
map(
decode_array::<GROTH16_BYTES>,
|proof: [u8; GROTH16_BYTES]| CompressedGroth16Proof::from_bytes(&proof),
)
.parse(input)
}
const ED25519_SIG_BYTES: usize = 64;
fn decode_ed25519_signature(input: &[u8]) -> IResult<&[u8], Ed25519Signature> {
// Ed25519Signature = 64BYTE
map(
decode_array::<ED25519_SIG_BYTES>,
|bytes: [u8; ED25519_SIG_BYTES]| Ed25519Signature::from_bytes(&bytes),
)
.parse(input)
}
const fn calculate_channel_multi_sig_proof_byte_size(threshold: ChannelKeyIndex) -> usize {
// Encoding: u16 signature count + N * (Ed25519 sig + u16 key index)
2 + (threshold as usize) * (ED25519_SIG_BYTES + 2)
}
fn decode_channel_multi_sig_proof(input: &[u8]) -> IResult<&[u8], ChannelMultiSigProof> {
// ChannelMultiSigProof = 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<IndexedSignature> = signatures
.into_iter()
.map(|(signature, index)| IndexedSignature::from((index, signature)))
.collect();
ChannelMultiSigProof::new(signatures)
.map(|proof| (input, proof))
.map_err(|_| nom::Err::Failure(Error::new(input, ErrorKind::Verify)))
2 + (threshold as usize) * (ED25519_SIGNATURE_SIZE + 2)
}
pub(crate) fn decode_field_element(input: &[u8]) -> IResult<&[u8], Fr> {
@@ -182,14 +131,6 @@ pub(crate) fn decode_field_element(input: &[u8]) -> IResult<&[u8], Fr> {
// ==============================================================================
// Primitive Decoders
// ==============================================================================
fn decode_array<const N: usize>(input: &[u8]) -> IResult<&[u8], [u8; N]> {
map(take(N), |bytes: &[u8]| {
let mut arr = [0u8; N];
arr.copy_from_slice(bytes);
arr
})
.parse(input)
}
pub(crate) fn decode_utf8_string(input: &[u8], len: usize) -> IResult<&[u8], String> {
map_res(take(len), |bytes: &[u8]| {
@@ -200,11 +141,6 @@ pub(crate) fn decode_utf8_string(input: &[u8], len: usize) -> IResult<&[u8], Str
.parse(input)
}
fn decode_uint16(input: &[u8]) -> IResult<&[u8], u16> {
// UINT16 = 2BYTE
le_u16(input)
}
pub(crate) fn decode_uint64(input: &[u8]) -> IResult<&[u8], u64> {
// UINT64 = 8BYTE
le_u64(input)
@@ -230,14 +166,10 @@ use lb_groth16::fr_to_bytes;
use crate::{
mantle::{Utxo, ops::channel::ChannelKeyIndex, tx::MantleTxGasContext},
proofs::channel_multi_sig_proof::{ChannelMultiSigProof, IndexedSignature},
proofs::channel_multi_sig_proof::ChannelMultiSigProof,
};
/// Encode primitives
fn encode_uint16(value: u16) -> Vec<u8> {
value.to_le_bytes().to_vec()
}
pub(crate) fn encode_uint64(value: u64) -> Vec<u8> {
value.to_le_bytes().to_vec()
}
@@ -258,36 +190,6 @@ pub(crate) fn encode_field_element(fr: &Fr) -> Vec<u8> {
fr_to_bytes(fr).to_vec()
}
/// Encode cryptographic primitives
fn encode_ed25519_signature(sig: &Ed25519Signature) -> Vec<u8> {
sig.to_bytes().to_vec()
}
fn encode_zk_signature(sig: &ZkSignature) -> Vec<u8> {
// ZkSignature wraps ZkSignProof which is CompressedGroth16Proof
encode_groth16_proof(sig.as_proof())
}
fn encode_poc(poc: &Groth16LeaderClaimProof) -> Vec<u8> {
// Groth16LeaderClaimProof wraps PocProof which is CompressedGroth16Proof
encode_groth16_proof(poc.proof())
}
fn encode_groth16_proof(proof: &CompressedGroth16Proof) -> Vec<u8> {
proof.to_bytes().to_vec()
}
fn encode_channel_multi_sig_proof(proof: &ChannelMultiSigProof) -> 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
}
// Check if proofs correspond to ops
#[must_use]
pub const fn proof_matches(proof: &OpProof, op: &Op) -> bool {
@@ -311,18 +213,18 @@ pub const fn proof_matches(proof: &OpProof, op: &Op) -> bool {
fn encode_op_proof(proof: &OpProof, op: &Op) -> Vec<u8> {
if proof_matches(proof, op) {
match proof {
OpProof::Ed25519Sig(sig) => encode_ed25519_signature(sig),
OpProof::ChannelMultiSigProof(proof) => encode_channel_multi_sig_proof(proof),
OpProof::Ed25519Sig(sig) => sig.encode(),
OpProof::ChannelMultiSigProof(proof) => proof.encode(),
OpProof::ZkAndEd25519Sigs {
zk_sig,
ed25519_sig,
} => {
let mut bytes = encode_zk_signature(zk_sig);
bytes.extend(encode_ed25519_signature(ed25519_sig));
let mut bytes = zk_sig.encode();
bytes.extend(ed25519_sig.encode());
bytes
}
OpProof::ZkSig(sig) => encode_zk_signature(sig),
OpProof::PoC(poc) => encode_poc(poc),
OpProof::ZkSig(sig) => sig.encode(),
OpProof::PoC(poc) => poc.encode(),
}
} else {
panic!("Mismatch between proof type and operation type");
@@ -359,7 +261,7 @@ pub(crate) fn predict_signed_mantle_tx_size(tx: &MantleTx, context: &MantleTxGas
.iter()
.map(|op| match op {
// Ed25519SigProof = Ed25519Signature
Op::ChannelInscribe(_) => ED25519_SIG_BYTES,
Op::ChannelInscribe(_) => ED25519_SIGNATURE_SIZE,
// ChannelMultiSigProof — for an existing channel, threshold sigs;
// for a new channel (just-in-time created here), no sigs required.
@@ -373,11 +275,11 @@ pub(crate) fn predict_signed_mantle_tx_size(tx: &MantleTx, context: &MantleTxGas
}
// ZkAndEd25519SigsProof = ZkSignature Ed25519Signature
Op::SDPDeclare(_) => GROTH16_BYTES + ED25519_SIG_BYTES,
Op::SDPDeclare(_) => COMPRESSED_PROOF_SIZE + ED25519_SIGNATURE_SIZE,
// ZkSigProof = ZkSignature = ProofOfClaimProof = Groth16
Op::SDPWithdraw(_) | Op::SDPActive(_) | Op::LeaderClaim(_) | Op::Transfer(_) => {
GROTH16_BYTES
COMPRESSED_PROOF_SIZE
}
// ChannelMultiSigProof
@@ -401,6 +303,7 @@ mod tests {
use std::{collections::HashMap, panic};
use ark_ff::AdditiveGroup as _;
use lb_groth16::CompressedGroth16Proof;
use lb_key_management_system_keys::keys::{Ed25519Key, ZkKey, ZkPublicKey};
use lb_utils::bounded_vec::BoundedError;
use multiaddr::Multiaddr;
@@ -424,6 +327,7 @@ mod tests {
},
tx::GasPrices,
},
proofs::channel_multi_sig_proof::IndexedSignature,
sdp::{
ActivityMetadata, DeclarationId, Locator, MAX_LOCATOR_BYTE_SIZE, ProviderId,
ServiceType, blend::ActivityProof,
@@ -596,7 +500,7 @@ mod tests {
// ChannelConfig creates the channel just-in-time, so no signatures are
// required for validation — empty proof is well-formed.
let config_proof = ChannelMultiSigProof::new(vec![]).unwrap();
let config_proof = ChannelMultiSigProof::try_new([].into()).unwrap();
// Encode and decode roundtrip test (no hardcoded test vector since signatures
// are deterministic)
@@ -810,7 +714,7 @@ mod tests {
// Create a signed tx and encode it to get actual size. New channel
// → empty proof (no signatures required for just-in-time create).
let config_proof = ChannelMultiSigProof::new(vec![]).unwrap();
let config_proof = ChannelMultiSigProof::try_new([].into()).unwrap();
let signed_tx =
SignedMantleTx::new(mantle_tx, vec![OpProof::ChannelMultiSigProof(config_proof)])
.unwrap();
@@ -998,7 +902,7 @@ mod tests {
let op_sig = signing_key.sign_payload(&txhash.as_signing_bytes());
// Create a signed tx and encode it to get actual size. ChannelConfig
// creates the channel here, so its proof has no signatures.
let config_proof = ChannelMultiSigProof::new(vec![]).unwrap();
let config_proof = ChannelMultiSigProof::try_new([].into()).unwrap();
let signed_tx = SignedMantleTx::new(
mantle_tx,
vec![
@@ -1111,7 +1015,7 @@ mod tests {
// creates the channel here, so its proof has no signatures.
let txhash = mantle_tx.hash();
let op_ed25519_sig = signing_key1.sign_payload(&txhash.as_signing_bytes());
let config_proof = ChannelMultiSigProof::new(vec![]).unwrap();
let config_proof = ChannelMultiSigProof::try_new([].into()).unwrap();
let signed_tx = SignedMantleTx::new(
mantle_tx,
vec![
@@ -1176,7 +1080,7 @@ mod tests {
let op = Op::LeaderClaim(leader_claim_op);
let encoded = encode_op_proof(&OpProof::PoC(poc_proof), &op);
assert_eq!(encoded.len(), GROTH16_BYTES);
assert_eq!(encoded.len(), COMPRESSED_PROOF_SIZE);
let (remaining, decoded) = decode_op_proof(&encoded, &op).unwrap();
assert!(remaining.is_empty());
@@ -1220,10 +1124,13 @@ mod tests {
},
)]));
let tx_hash = mantle_tx.hash();
let proof = ChannelMultiSigProof::new(vec![IndexedSignature::new(
0,
signing_key.sign_payload(tx_hash.as_signing_bytes().as_ref()),
)])
let proof = ChannelMultiSigProof::try_new(
[IndexedSignature::new(
0,
signing_key.sign_payload(tx_hash.as_signing_bytes().as_ref()),
)]
.into(),
)
.unwrap();
let signed_tx =
SignedMantleTx::new(mantle_tx, vec![OpProof::ChannelMultiSigProof(proof)]).unwrap();
+12 -1
View File
@@ -1,12 +1,23 @@
use lb_core_macros::nom_wire_fixtures;
use lb_groth16::Fr;
use lb_key_management_system_keys::keys::{Ed25519PublicKey, ZkPublicKey};
use lb_key_management_system_keys::keys::{
Ed25519PublicKey, Ed25519Signature, ZkPublicKey, ZkSignature,
};
use lb_zksign::ZkSignProof;
nom_wire_fixtures!(
Ed25519PublicKey,
Self::from_bytes(&[1u8; _]).unwrap() => "0101010101010101010101010101010101010101010101010101010101010101"
);
nom_wire_fixtures!(
ZkPublicKey,
Fr::from(1u64).into() => "0100000000000000000000000000000000000000000000000000000000000000"
);
nom_wire_fixtures!(Ed25519Signature, Self::from_bytes(&[1u8; _]) => "01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101");
nom_wire_fixtures!(
ZkSignature,
Self::new(ZkSignProof::from_bytes(&[1u8; _])) => "0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101"
);
+28 -12
View File
@@ -1,19 +1,22 @@
use lb_core_macros::nom_wire_fixtures;
use lb_key_management_system_keys::keys::Ed25519PublicKey;
use lb_key_management_system_keys::keys::{Ed25519PublicKey, Ed25519Signature};
use crate::mantle::{
channel::{SlotTimeframe, SlotTimeout},
ledger::{Inputs, Outputs},
ops::{
Op,
channel::{
ChannelId, MsgId,
config::ChannelConfigOp,
deposit::{DepositOp, Metadata},
inscribe::InscriptionOp,
withdraw::ChannelWithdrawOp,
use crate::{
mantle::{
channel::{SlotTimeframe, SlotTimeout},
ledger::{Inputs, Outputs},
ops::{
Op,
channel::{
ChannelId, MsgId,
config::ChannelConfigOp,
deposit::{DepositOp, Metadata},
inscribe::InscriptionOp,
withdraw::ChannelWithdrawOp,
},
},
},
proofs::channel_multi_sig_proof::{ChannelMultiSigProof, IndexedSignature},
};
nom_wire_fixtures!(ChannelId, ChannelId::from([0u8; 32]) => "0000000000000000000000000000000000000000000000000000000000000000");
@@ -67,3 +70,16 @@ nom_wire_fixtures!(
.value
) => "1100000000000000000000000000000000000000000000000000000000000000000700000067656e6573697300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
);
nom_wire_fixtures!(
IndexedSignature,
Self {
channel_key_index: 1,
signature: Ed25519Signature::from_bytes(&[0u8; _])
} => "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100"
);
nom_wire_fixtures!(ChannelMultiSigProof,
Self::try_new([].into()).unwrap() => "0000",
Self::try_new([IndexedSignature::new(0, Ed25519Signature::from_bytes(&[0u8; _]))].into()).unwrap() => "0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
);
@@ -1,7 +1,11 @@
use lb_core_macros::nom_wire_fixtures;
use lb_groth16::{AdditiveGroup as _, Field as _, Fr};
use lb_poc::PoCProof;
use crate::mantle::ops::leader_claim::{LeaderClaimOp, RewardsRoot, VoucherNullifier};
use crate::{
mantle::ops::leader_claim::{LeaderClaimOp, RewardsRoot, VoucherNullifier},
proofs::leader_claim_proof::Groth16LeaderClaimProof,
};
nom_wire_fixtures!(
RewardsRoot,
@@ -21,3 +25,8 @@ nom_wire_fixtures!(
pk: Fr::ZERO.into()
} => "000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
);
nom_wire_fixtures!(
Groth16LeaderClaimProof,
Self::new(PoCProof::from_bytes(&[1u8; _])) => "0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101"
);
+30 -1
View File
@@ -1,5 +1,8 @@
use lb_groth16::Fr;
use lb_key_management_system_keys::keys::{Ed25519PublicKey, ZkPublicKey};
use lb_key_management_system_keys::keys::{
Ed25519PublicKey, Ed25519Signature, ZkPublicKey, ZkSignature,
};
use lb_zksign::ZkSignProof;
use nom::{
IResult,
error::{Error, ErrorKind},
@@ -37,3 +40,29 @@ impl NomDecode for ZkPublicKey {
Ok((bytes, Self::new(inner)))
}
}
impl NomEncode for Ed25519Signature {
fn encode(&self) -> Vec<u8> {
self.to_bytes().encode()
}
}
impl NomDecode for Ed25519Signature {
fn decode(bytes: &[u8]) -> IResult<&[u8], Self> {
let (remaining_bytes, inner) = <[u8; _]>::decode(bytes)?;
Ok((remaining_bytes, Self::from_bytes(&inner)))
}
}
impl NomEncode for ZkSignature {
fn encode(&self) -> Vec<u8> {
self.as_proof().to_bytes().encode()
}
}
impl NomDecode for ZkSignature {
fn decode(bytes: &[u8]) -> IResult<&[u8], Self> {
let (remaining_bytes, inner) = <[u8; _]>::decode(bytes)?;
Ok((remaining_bytes, Self::new(ZkSignProof::from_bytes(&inner))))
}
}
+6 -4
View File
@@ -700,7 +700,7 @@ mod tests {
use super::*;
use crate::{
mantle::{Note, ledger::Outputs, ops::channel::inscribe::InscriptionOp},
proofs::channel_multi_sig_proof::IndexedSignature,
proofs::channel_multi_sig_proof::{IndexedSignature, IndexedSignatures},
};
fn create_test_mantle_tx(ops: Vec<Op>) -> MantleTx {
@@ -771,7 +771,7 @@ mod tests {
withdraw_nonce: 0,
})]);
let tx_hash = mantle_tx.hash();
let signatures = signing_keys
let signatures: IndexedSignatures = signing_keys
.iter()
.enumerate()
.map(|(index, key)| {
@@ -780,8 +780,10 @@ mod tests {
key.sign_payload(tx_hash.as_signing_bytes().as_ref()),
)
})
.collect();
let proof = ChannelMultiSigProof::new(signatures).unwrap();
.collect::<Vec<_>>()
.try_into()
.unwrap();
let proof = ChannelMultiSigProof::try_new(signatures).unwrap();
SignedMantleTx::new(mantle_tx, vec![OpProof::ChannelMultiSigProof(proof)]).unwrap()
}
+92 -41
View File
@@ -1,25 +1,34 @@
use std::cmp::Ordering;
use lb_core_macros::NomCodec;
use lb_key_management_system_keys::keys::Ed25519Signature;
use lb_utils::bounded_vec::UpperBoundedVec;
use nom::{
Err,
error::{Error as NomError, ErrorKind},
};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::mantle::ops::channel::ChannelKeyIndex;
use crate::mantle::{
nom::{NomDecode, NomEncode},
ops::channel::ChannelKeyIndex,
};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, NomCodec)]
pub struct IndexedSignature {
pub signature: Ed25519Signature,
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 IndexedSignature {
#[must_use]
pub const fn new(channel_key_index: ChannelKeyIndex, signature: Ed25519Signature) -> Self {
Self {
channel_key_index,
signature,
channel_key_index,
}
}
}
@@ -52,6 +61,9 @@ pub enum Error {
TooManySignatures { actual: usize, maximum: usize },
}
pub const MAX_SIGNATURES: usize = u16::MAX as usize;
pub type IndexedSignatures = UpperBoundedVec<IndexedSignature, MAX_SIGNATURES>;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
// Serde goes through `ChannelMultiSigProofRepr` via `try_from`/`into`: `Deserialize`
// routes through `new`, so the well-formedness invariant (strictly-increasing
@@ -65,7 +77,22 @@ pub enum Error {
pub struct ChannelMultiSigProof {
// Invariant: signature indices are strictly increasing (hence ordered and
// unique), as required by the spec.
signatures: Vec<IndexedSignature>,
signatures: IndexedSignatures,
}
impl NomEncode for ChannelMultiSigProof {
fn encode(&self) -> Vec<u8> {
self.signatures.encode()
}
}
impl NomDecode for ChannelMultiSigProof {
fn decode(bytes: &[u8]) -> nom::IResult<&[u8], Self> {
let (remaining_bytes, inner) = IndexedSignatures::decode(bytes)?;
let proof = Self::try_new(inner)
.map_err(|_| Err::Error(NomError::new(bytes, ErrorKind::MapRes)))?;
Ok((remaining_bytes, proof))
}
}
/// Serde wire representation of [`ChannelMultiSigProof`] — a struct with a
@@ -74,14 +101,14 @@ pub struct ChannelMultiSigProof {
/// preserving the `{ "signatures": [..] }` JSON shape.
#[derive(Serialize, Deserialize)]
struct ChannelMultiSigProofRepr {
signatures: Vec<IndexedSignature>,
signatures: IndexedSignatures,
}
impl TryFrom<ChannelMultiSigProofRepr> for ChannelMultiSigProof {
type Error = Error;
fn try_from(repr: ChannelMultiSigProofRepr) -> Result<Self, Self::Error> {
Self::new(repr.signatures)
Self::try_new(repr.signatures)
}
}
@@ -94,20 +121,19 @@ impl From<ChannelMultiSigProof> for ChannelMultiSigProofRepr {
}
impl ChannelMultiSigProof {
pub fn new(signatures: Vec<IndexedSignature>) -> Result<Self, Error> {
Self::validate_well_formedness(&signatures)?;
pub fn try_new(signatures: IndexedSignatures) -> Result<Self, Error> {
let signatures = Self::validate_well_formedness(signatures)?;
Ok(Self { signatures })
}
/// Validates that the proof is structurally well-formed: signature indices
/// must be strictly increasing (so they are ordered and unique, per the
/// `CHANNEL_CONFIG` / `CHANNEL_WITHDRAW` spec), and the count must not
/// exceed `ChannelKeyIndex::MAX`.
/// `CHANNEL_CONFIG` / `CHANNEL_WITHDRAW` spec).
///
/// This validates structural correctness only. Cryptographic validity
/// (signature verification, threshold requirements, index-to-key
/// correspondence) must be checked separately.
fn validate_well_formedness(signatures: &[IndexedSignature]) -> Result<(), Error> {
fn validate_well_formedness(signatures: IndexedSignatures) -> Result<IndexedSignatures, Error> {
if signatures
.windows(2)
.any(|w| w[0].channel_key_index >= w[1].channel_key_index)
@@ -116,27 +142,12 @@ impl ChannelMultiSigProof {
signatures.iter().map(|s| s.channel_key_index).collect(),
));
}
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(())
Ok(signatures)
}
#[must_use]
pub const fn signatures(&self) -> &Vec<IndexedSignature> {
&self.signatures
}
}
impl TryFrom<Vec<IndexedSignature>> for ChannelMultiSigProof {
type Error = Error;
fn try_from(value: Vec<IndexedSignature>) -> Result<Self, Self::Error> {
Self::new(value)
pub fn signatures(&self) -> &[IndexedSignature] {
self.signatures.as_slice()
}
}
@@ -151,12 +162,12 @@ mod tests {
#[test]
fn rejects_repeated_index() {
// Same index twice (distinct sigs): not strictly increasing, so rejected.
let signatures = vec![
let signatures = [
IndexedSignature::new(0, sig(1)),
IndexedSignature::new(0, sig(2)),
];
assert!(matches!(
ChannelMultiSigProof::new(signatures),
ChannelMultiSigProof::try_new(signatures.into()),
Err(Error::IndicesNotStrictlyIncreasing(_))
));
}
@@ -165,23 +176,23 @@ mod tests {
fn rejects_unsorted_indices() {
// Unique but not strictly increasing (descending): rejected (we no longer
// silently sort — the spec asserts monotonic order).
let signatures = vec![
let signatures = [
IndexedSignature::new(1, sig(1)),
IndexedSignature::new(0, sig(2)),
];
assert!(matches!(
ChannelMultiSigProof::new(signatures),
ChannelMultiSigProof::try_new(signatures.into()),
Err(Error::IndicesNotStrictlyIncreasing(_))
));
}
#[test]
fn accepts_strictly_increasing_indices() {
let signatures = vec![
let signatures = [
IndexedSignature::new(0, sig(1)),
IndexedSignature::new(1, sig(2)),
];
let proof = ChannelMultiSigProof::new(signatures)
let proof = ChannelMultiSigProof::try_new(signatures.into())
.expect("strictly-increasing indices are well-formed");
assert_eq!(proof.signatures().len(), 2);
}
@@ -195,7 +206,7 @@ mod tests {
fn deserialize_rejects_non_monotonic_indices() {
// Two distinct signatures sharing index 0 — not strictly increasing, so
// `new` (and now `Deserialize`) must reject it.
let raw = vec![
let raw = [
IndexedSignature::new(0, sig(1)),
IndexedSignature::new(0, sig(2)),
];
@@ -210,10 +221,13 @@ mod tests {
// A well-formed proof still round-trips, and keeps the `{ "signatures": [..] }`
// JSON shape.
let ok = ChannelMultiSigProof::new(vec![
IndexedSignature::new(0, sig(1)),
IndexedSignature::new(1, sig(2)),
])
let ok = ChannelMultiSigProof::try_new(
[
IndexedSignature::new(0, sig(1)),
IndexedSignature::new(1, sig(2)),
]
.into(),
)
.expect("distinct indices are well-formed");
let serialized = serde_json::to_string(&ok).expect("serialize proof");
assert!(
@@ -224,4 +238,41 @@ mod tests {
serde_json::from_str(&serialized).expect("well-formed proof round-trips");
assert_eq!(round_tripped, ok);
}
/// The nom decoder must uphold the same well-formedness invariant as `new`:
/// a wire-encoded vector with a repeated index is not strictly increasing,
/// so `decode` must fail (the `try_new` inside `decode` maps to a nom
/// error).
#[test]
fn decode_rejects_repeated_index() {
// Encode a raw vector (bypassing `ChannelMultiSigProof`) with the same
// index twice, then decode it back through the proof's nom path.
let raw: IndexedSignatures = [
IndexedSignature::new(0, sig(1)),
IndexedSignature::new(0, sig(2)),
]
.into();
let bytes = raw.encode();
assert!(
ChannelMultiSigProof::decode(&bytes).is_err(),
"decoding a proof with a repeated index must fail"
);
}
/// Companion to the above: a wire-encoded vector whose indices are unique
/// but out of order (descending) is also not strictly increasing, so the
/// nom decoder must reject it.
#[test]
fn decode_rejects_out_of_order_indices() {
let raw: IndexedSignatures = [
IndexedSignature::new(1, sig(1)),
IndexedSignature::new(0, sig(2)),
]
.into();
let bytes = raw.encode();
assert!(
ChannelMultiSigProof::decode(&bytes).is_err(),
"decoding a proof with out-of-order indices must fail"
);
}
}
+25 -1
View File
@@ -5,7 +5,13 @@ use serde::{Deserialize, Serialize};
use thiserror::Error;
use tracing::error;
use crate::{mantle::ops::leader_claim::VoucherSecret, proofs::merkle::mmr_path_to_witness};
use crate::{
mantle::{
nom::{NomDecode, NomEncode},
ops::leader_claim::VoucherSecret,
},
proofs::merkle::mmr_path_to_witness,
};
const LOG_TARGET: &str = proofs::LEADER_CLAIM;
@@ -15,6 +21,24 @@ pub struct Groth16LeaderClaimProof {
proof: lb_poc::PoCProof,
}
impl NomEncode for Groth16LeaderClaimProof {
fn encode(&self) -> Vec<u8> {
self.proof.to_bytes().encode()
}
}
impl NomDecode for Groth16LeaderClaimProof {
fn decode(bytes: &[u8]) -> nom::IResult<&[u8], Self> {
let (remaining_bytes, inner) = <[u8; _]>::decode(bytes)?;
Ok((
remaining_bytes,
Self {
proof: lb_poc::PoCProof::from_bytes(&inner),
},
))
}
}
#[derive(Debug, Error)]
pub enum Error {
#[error("Proof of claim failed: {0}")]
@@ -214,7 +214,7 @@ pub(crate) fn run_config_combine(args: ConfigCombineArgs) -> RunResult<()> {
signature: sig.signature,
});
}
let proof = ChannelMultiSigProof::new(
let proof = ChannelMultiSigProof::try_new(
signature_entries
.iter()
.map(|sig| {
@@ -222,7 +222,8 @@ pub(crate) fn run_config_combine(args: ConfigCombineArgs) -> RunResult<()> {
decode_hex_bincode::<Ed25519Signature>(&sig.signature)
.map(|signature| IndexedSignature::new(sig.signer_key_index, signature))
})
.collect::<RunResult<Vec<_>>>()?,
.collect::<RunResult<Vec<_>>>()?
.try_into()?,
)?;
if proof.signatures().len() != intent.required_threshold as usize {
return Err(format!(
@@ -172,7 +172,7 @@ pub(crate) fn run_withdraw_combine(args: WithdrawCombineArgs) -> RunResult<()> {
signature: sig.signature,
});
}
let proof = ChannelMultiSigProof::new(
let proof = ChannelMultiSigProof::try_new(
signature_entries
.iter()
.map(|sig| {
@@ -180,7 +180,8 @@ pub(crate) fn run_withdraw_combine(args: WithdrawCombineArgs) -> RunResult<()> {
decode_hex_bincode::<Ed25519Signature>(&sig.signature)
.map(|signature| IndexedSignature::new(sig.signer_key_index, signature))
})
.collect::<RunResult<Vec<_>>>()?,
.collect::<RunResult<Vec<_>>>()?
.try_into()?,
)?;
if proof.signatures().len() < intent.required_threshold as usize {
return Err(format!(
+28 -16
View File
@@ -1055,10 +1055,13 @@ mod tests {
let config_tx = MantleTx([Op::ChannelConfig(config_op.clone())].into());
let config_tx_hash = config_tx.hash();
let config_proof = ChannelMultiSigProof::new(vec![IndexedSignature::new(
0,
signing_key.sign_payload(config_tx_hash.as_signing_bytes().as_ref()),
)])
let config_proof = ChannelMultiSigProof::try_new(
[IndexedSignature::new(
0,
signing_key.sign_payload(config_tx_hash.as_signing_bytes().as_ref()),
)]
.into(),
)
.unwrap();
let tx = create_signed_tx(
@@ -1221,10 +1224,13 @@ mod tests {
};
let withdraw_tx = MantleTx([Op::ChannelWithdraw(withdraw.clone())].into());
let withdraw_tx_hash = withdraw_tx.hash();
let withdraw_proof = ChannelMultiSigProof::new(vec![IndexedSignature::new(
0,
signing_key.sign_payload(withdraw_tx_hash.as_signing_bytes().as_ref()),
)])
let withdraw_proof = ChannelMultiSigProof::try_new(
[IndexedSignature::new(
0,
signing_key.sign_payload(withdraw_tx_hash.as_signing_bytes().as_ref()),
)]
.into(),
)
.unwrap();
let signed_tx = create_multi_signed_tx(
@@ -1311,10 +1317,13 @@ mod tests {
let wrong_key = Ed25519Key::from_bytes(&[42; 32]);
let withdraw_tx = MantleTx([Op::ChannelWithdraw(withdraw.clone())].into());
let withdraw_tx_hash = withdraw_tx.hash();
let invalid_proof = ChannelMultiSigProof::new(vec![IndexedSignature::new(
0,
wrong_key.sign_payload(withdraw_tx_hash.as_signing_bytes().as_ref()),
)])
let invalid_proof = ChannelMultiSigProof::try_new(
[IndexedSignature::new(
0,
wrong_key.sign_payload(withdraw_tx_hash.as_signing_bytes().as_ref()),
)]
.into(),
)
.unwrap();
let signed_tx = create_multi_signed_tx(
@@ -1528,10 +1537,13 @@ mod tests {
];
let config_tx = MantleTx(Ops::new_unchecked(ops.clone()));
let config_tx_hash = config_tx.hash();
let config_proof = ChannelMultiSigProof::new(vec![IndexedSignature::new(
0,
sk1.sign_payload(config_tx_hash.as_signing_bytes().as_ref()),
)])
let config_proof = ChannelMultiSigProof::try_new(
[IndexedSignature::new(
0,
sk1.sign_payload(config_tx_hash.as_signing_bytes().as_ref()),
)]
.into(),
)
.unwrap();
let tx = create_multi_signed_tx(
@@ -1588,7 +1588,7 @@ pub async fn submit_zone_withdraw(
})?;
let withdraw_proof =
match ChannelMultiSigProof::new(vec![IndexedSignature::new(0, withdraw_sig)]) {
match ChannelMultiSigProof::try_new([IndexedSignature::new(0, withdraw_sig)].into()) {
Ok(proof) => proof,
Err(error) => {
return Err(ZoneTestError::SubmitWithdraw {
+7 -4
View File
@@ -403,10 +403,13 @@ fn signed_channel_withdraw(
) -> SignedMantleTx {
let mantle_tx = MantleTx([Op::ChannelWithdraw(withdraw)].into());
let tx_hash = mantle_tx.hash();
let withdraw_proof = ChannelMultiSigProof::new(vec![IndexedSignature::new(
0,
signing_key.sign_payload(tx_hash.as_signing_bytes().as_ref()),
)])
let withdraw_proof = ChannelMultiSigProof::try_new(
[IndexedSignature::new(
0,
signing_key.sign_payload(tx_hash.as_signing_bytes().as_ref()),
)]
.into(),
)
.expect("withdraw proof should be valid");
SignedMantleTx::new(
+2
View File
@@ -281,6 +281,8 @@ pub type UpperBoundedVec<T, const MAX: usize> = BoundedVec<T, 0, MAX>;
pub type LowerBoundedVec<T, const MIN: usize> = BoundedVec<T, MIN, { usize::MAX }>;
// `[1, MAX]` elements.
pub type NonEmptyBoundedVec<T, const MAX: usize> = BoundedVec<T, 1, MAX>;
// `[0, usize::MAX]` elements.
pub type MaxBoundedVec<T> = UpperBoundedVec<T, { usize::MAX }>;
#[cfg(test)]
mod tests {
+1 -1
View File
@@ -1,7 +1,7 @@
mod curve;
mod from_json_error;
mod proof;
pub use proof::{CompressSize, CompressedProof};
pub use proof::{COMPRESSED_PROOF_SIZE, CompressSize, CompressedProof};
mod protocol;
mod public_input;
+5 -3
View File
@@ -26,6 +26,8 @@ pub trait CompressSize: Pairing {
type G2CompressedSize: ArrayLength;
}
pub const COMPRESSED_PROOF_SIZE: usize = 128;
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct CompressedProof<E: CompressSize> {
pub pi_a: GenericArray<u8, E::G1CompressedSize>,
@@ -67,8 +69,8 @@ impl<E: CompressSize> CompressedProof<E> {
impl CompressedProof<Bn254> {
/// Total size = G1 + G2 + G1 at the type level.
#[must_use]
pub fn to_bytes(&self) -> [u8; 128] {
let mut bytes = [0u8; 128];
pub fn to_bytes(&self) -> [u8; COMPRESSED_PROOF_SIZE] {
let mut bytes = [0u8; COMPRESSED_PROOF_SIZE];
let g1 = <Bn254 as CompressSize>::G1CompressedSize::to_usize();
let g2 = <Bn254 as CompressSize>::G2CompressedSize::to_usize();
@@ -81,7 +83,7 @@ impl CompressedProof<Bn254> {
/// Type-level length bound: accepts exactly G1 + G2 + G1 bytes.
#[must_use]
pub fn from_bytes(bytes: &[u8; 128]) -> Self {
pub fn from_bytes(bytes: &[u8; COMPRESSED_PROOF_SIZE]) -> Self {
let g1 = <Bn254 as CompressSize>::G1CompressedSize::to_usize();
let g2 = <Bn254 as CompressSize>::G2CompressedSize::to_usize();
+5 -3
View File
@@ -29,7 +29,7 @@ pub(super) fn build_atomic_withdraw_ops_proofs(
own_sig: Ed25519Signature,
) -> Result<Vec<OpProof>, Error> {
let withdraw_proof =
ChannelMultiSigProof::new(vec![IndexedSignature::new(own_key_index, own_sig)])
ChannelMultiSigProof::try_new([IndexedSignature::new(own_key_index, own_sig)].into())
.map_err(|e| Error::Network(format!("multi-sig proof assembly failed: {e:?}")))?;
let mut ops_proofs = Vec::with_capacity(tx.ops().len());
for op in tx.ops() {
@@ -125,8 +125,10 @@ pub(super) fn create_channel_config_tx(
key.sign_payload(tx_hash.as_signing_bytes().as_ref()),
)
})
.collect();
let proof = ChannelMultiSigProof::new(signatures).unwrap();
.collect::<Vec<_>>()
.try_into()
.unwrap();
let proof = ChannelMultiSigProof::try_new(signatures).unwrap();
SignedMantleTx {
ops_proofs: vec![OpProof::ChannelMultiSigProof(proof)],