diff --git a/Cargo.lock b/Cargo.lock index a68106a1d..aab6c7187 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4452,6 +4452,7 @@ dependencies = [ "logos-blockchain-poseidon2", "logos-blockchain-utils", "logos-blockchain-utxotree", + "logos-blockchain-zksign", "multiaddr", "nom 8.0.0", "num-bigint", diff --git a/core/Cargo.toml b/core/Cargo.toml index 859916038..253b98c3a 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -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 } diff --git a/core/src/mantle/encoding.rs b/core/src/mantle/encoding.rs index 232d8b629..2fbb78e66 100644 --- a/core/src/mantle/encoding.rs +++ b/core/src/mantle/encoding.rs @@ -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(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::, - |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::, - |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 = 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(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 { - value.to_le_bytes().to_vec() -} - pub(crate) fn encode_uint64(value: u64) -> Vec { value.to_le_bytes().to_vec() } @@ -258,36 +190,6 @@ pub(crate) fn encode_field_element(fr: &Fr) -> Vec { fr_to_bytes(fr).to_vec() } -/// Encode cryptographic primitives -fn encode_ed25519_signature(sig: &Ed25519Signature) -> Vec { - sig.to_bytes().to_vec() -} - -fn encode_zk_signature(sig: &ZkSignature) -> Vec { - // ZkSignature wraps ZkSignProof which is CompressedGroth16Proof - encode_groth16_proof(sig.as_proof()) -} - -fn encode_poc(poc: &Groth16LeaderClaimProof) -> Vec { - // Groth16LeaderClaimProof wraps PocProof which is CompressedGroth16Proof - encode_groth16_proof(poc.proof()) -} - -fn encode_groth16_proof(proof: &CompressedGroth16Proof) -> Vec { - proof.to_bytes().to_vec() -} - -fn encode_channel_multi_sig_proof(proof: &ChannelMultiSigProof) -> Vec { - 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 { 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(); diff --git a/core/src/mantle/nom/fixtures/kms.rs b/core/src/mantle/nom/fixtures/kms.rs index 714512a54..f6dd34a27 100644 --- a/core/src/mantle/nom/fixtures/kms.rs +++ b/core/src/mantle/nom/fixtures/kms.rs @@ -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" +); diff --git a/core/src/mantle/nom/fixtures/ops/channel.rs b/core/src/mantle/nom/fixtures/ops/channel.rs index 8c286e640..6221c6e64 100644 --- a/core/src/mantle/nom/fixtures/ops/channel.rs +++ b/core/src/mantle/nom/fixtures/ops/channel.rs @@ -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" +); diff --git a/core/src/mantle/nom/fixtures/ops/leader_claim.rs b/core/src/mantle/nom/fixtures/ops/leader_claim.rs index 1dc223a62..1e6be245c 100644 --- a/core/src/mantle/nom/fixtures/ops/leader_claim.rs +++ b/core/src/mantle/nom/fixtures/ops/leader_claim.rs @@ -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" +); diff --git a/core/src/mantle/nom/kms.rs b/core/src/mantle/nom/kms.rs index 0003a6438..9a20936f2 100644 --- a/core/src/mantle/nom/kms.rs +++ b/core/src/mantle/nom/kms.rs @@ -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 { + 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 { + 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)))) + } +} diff --git a/core/src/mantle/tx.rs b/core/src/mantle/tx.rs index 291b3d16a..a755fd0b4 100644 --- a/core/src/mantle/tx.rs +++ b/core/src/mantle/tx.rs @@ -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) -> 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::>() + .try_into() + .unwrap(); + let proof = ChannelMultiSigProof::try_new(signatures).unwrap(); SignedMantleTx::new(mantle_tx, vec![OpProof::ChannelMultiSigProof(proof)]).unwrap() } diff --git a/core/src/proofs/channel_multi_sig_proof.rs b/core/src/proofs/channel_multi_sig_proof.rs index e9c7f898d..8334e9376 100644 --- a/core/src/proofs/channel_multi_sig_proof.rs +++ b/core/src/proofs/channel_multi_sig_proof.rs @@ -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; + #[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, + signatures: IndexedSignatures, +} + +impl NomEncode for ChannelMultiSigProof { + fn encode(&self) -> Vec { + 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, + signatures: IndexedSignatures, } impl TryFrom for ChannelMultiSigProof { type Error = Error; fn try_from(repr: ChannelMultiSigProofRepr) -> Result { - Self::new(repr.signatures) + Self::try_new(repr.signatures) } } @@ -94,20 +121,19 @@ impl From for ChannelMultiSigProofRepr { } impl ChannelMultiSigProof { - pub fn new(signatures: Vec) -> Result { - Self::validate_well_formedness(&signatures)?; + pub fn try_new(signatures: IndexedSignatures) -> Result { + 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 { 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 { - &self.signatures - } -} - -impl TryFrom> for ChannelMultiSigProof { - type Error = Error; - - fn try_from(value: Vec) -> Result { - 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" + ); + } } diff --git a/core/src/proofs/leader_claim_proof.rs b/core/src/proofs/leader_claim_proof.rs index 50a555b55..726841ff8 100644 --- a/core/src/proofs/leader_claim_proof.rs +++ b/core/src/proofs/leader_claim_proof.rs @@ -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 { + 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}")] diff --git a/deployment/tui-zone/src/run_commands/run_config.rs b/deployment/tui-zone/src/run_commands/run_config.rs index 470047402..27c3fda4f 100644 --- a/deployment/tui-zone/src/run_commands/run_config.rs +++ b/deployment/tui-zone/src/run_commands/run_config.rs @@ -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::(&sig.signature) .map(|signature| IndexedSignature::new(sig.signer_key_index, signature)) }) - .collect::>>()?, + .collect::>>()? + .try_into()?, )?; if proof.signatures().len() != intent.required_threshold as usize { return Err(format!( diff --git a/deployment/tui-zone/src/run_commands/run_withdraw.rs b/deployment/tui-zone/src/run_commands/run_withdraw.rs index bf9597165..535321c57 100644 --- a/deployment/tui-zone/src/run_commands/run_withdraw.rs +++ b/deployment/tui-zone/src/run_commands/run_withdraw.rs @@ -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::(&sig.signature) .map(|signature| IndexedSignature::new(sig.signer_key_index, signature)) }) - .collect::>>()?, + .collect::>>()? + .try_into()?, )?; if proof.signatures().len() < intent.required_threshold as usize { return Err(format!( diff --git a/ledger/src/lib.rs b/ledger/src/lib.rs index 6db59b7db..09696b557 100644 --- a/ledger/src/lib.rs +++ b/ledger/src/lib.rs @@ -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( diff --git a/tests/src/cucumber/steps/manual_zone/support.rs b/tests/src/cucumber/steps/manual_zone/support.rs index a49216e36..228e10526 100644 --- a/tests/src/cucumber/steps/manual_zone/support.rs +++ b/tests/src/cucumber/steps/manual_zone/support.rs @@ -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 { diff --git a/tests/src/tests/mantle/channel.rs b/tests/src/tests/mantle/channel.rs index 1d86018ca..a800f13f4 100644 --- a/tests/src/tests/mantle/channel.rs +++ b/tests/src/tests/mantle/channel.rs @@ -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( diff --git a/utils/src/bounded_vec.rs b/utils/src/bounded_vec.rs index adb4a541b..b81d4b679 100644 --- a/utils/src/bounded_vec.rs +++ b/utils/src/bounded_vec.rs @@ -281,6 +281,8 @@ pub type UpperBoundedVec = BoundedVec; pub type LowerBoundedVec = BoundedVec; // `[1, MAX]` elements. pub type NonEmptyBoundedVec = BoundedVec; +// `[0, usize::MAX]` elements. +pub type MaxBoundedVec = UpperBoundedVec; #[cfg(test)] mod tests { diff --git a/zk/groth16/src/lib.rs b/zk/groth16/src/lib.rs index 114b81334..cb21e95bd 100644 --- a/zk/groth16/src/lib.rs +++ b/zk/groth16/src/lib.rs @@ -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; diff --git a/zk/groth16/src/proof/mod.rs b/zk/groth16/src/proof/mod.rs index 6dca02571..13fc36094 100644 --- a/zk/groth16/src/proof/mod.rs +++ b/zk/groth16/src/proof/mod.rs @@ -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 { pub pi_a: GenericArray, @@ -67,8 +69,8 @@ impl CompressedProof { impl CompressedProof { /// 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 = ::G1CompressedSize::to_usize(); let g2 = ::G2CompressedSize::to_usize(); @@ -81,7 +83,7 @@ impl CompressedProof { /// 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 = ::G1CompressedSize::to_usize(); let g2 = ::G2CompressedSize::to_usize(); diff --git a/zone-sdk/src/sequencer/tx_builder.rs b/zone-sdk/src/sequencer/tx_builder.rs index 33d7f7d10..cb1d0ad88 100644 --- a/zone-sdk/src/sequencer/tx_builder.rs +++ b/zone-sdk/src/sequencer/tx_builder.rs @@ -29,7 +29,7 @@ pub(super) fn build_atomic_withdraw_ops_proofs( own_sig: Ed25519Signature, ) -> Result, 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::>() + .try_into() + .unwrap(); + let proof = ChannelMultiSigProof::try_new(signatures).unwrap(); SignedMantleTx { ops_proofs: vec![OpProof::ChannelMultiSigProof(proof)],