diff --git a/core/src/mantle/transactions/tx_list/op_proof_refs.rs b/core/src/mantle/transactions/tx_list/op_proof_refs.rs index 48eb5c3ac..d7b622044 100644 --- a/core/src/mantle/transactions/tx_list/op_proof_refs.rs +++ b/core/src/mantle/transactions/tx_list/op_proof_refs.rs @@ -32,3 +32,51 @@ impl Serialize for OpProofRefs<'_> { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + codec::SerializeOp as _, + mantle::{OpProof, ops::NoOpProof, transactions::OpProofs}, + }; + + #[test] + fn serialize_to_json() { + let op_proofs = OpProofs::sample(); + let op_proof_refs = + OpProofRefs::try_from(op_proofs.iter().map(OpProof::by_ref).collect::>()) + .expect("the sample is within bounds"); + + assert_eq!( + serde_json::to_value(&op_proof_refs).expect("the human-readable arm serializes"), + serde_json::to_value(op_proof_refs.as_slice()).expect("the inner column serializes") + ); + } + + #[test] + fn serialize_to_json_round_trips_into_owned_op_proofs() { + let op_proofs = OpProofs::sample(); + let op_proof_refs = + OpProofRefs::try_from(op_proofs.iter().map(OpProof::by_ref).collect::>()) + .expect("the sample is within bounds"); + + let json = + serde_json::to_string(&op_proof_refs).expect("the human-readable arm serializes"); + + assert_eq!( + serde_json::from_str::(&json).expect("the human-readable arm deserializes"), + op_proofs + ); + } + + #[test] + fn serialize_rejects_binary() { + let op_proof = OpProof::None(NoOpProof); + let op_proof_refs = OpProofRefs::from([op_proof.by_ref()]); + + op_proof_refs + .to_bytes() + .expect_err("Context-less binary encoding is unsupported."); + } +} diff --git a/core/src/mantle/transactions/tx_list/op_proofs.rs b/core/src/mantle/transactions/tx_list/op_proofs.rs index 8c2d54856..26c87be45 100644 --- a/core/src/mantle/transactions/tx_list/op_proofs.rs +++ b/core/src/mantle/transactions/tx_list/op_proofs.rs @@ -87,3 +87,25 @@ impl<'de> Deserialize<'de> for OpProofs { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::codec::DeserializeOp as _; + + #[test] + fn deserialize_from_json() { + let op_proofs = OpProofs::sample(); + let json = serde_json::to_value(op_proofs.inner()).expect("the inner column serializes"); + + assert_eq!( + serde_json::from_value::(json).expect("the human-readable arm deserializes"), + op_proofs + ); + } + + #[test] + fn deserialize_rejects_binary() { + OpProofs::from_bytes(&[0u8; 8]).expect_err("Context-less binary decoding is unsupported."); + } +} diff --git a/core/src/mantle/transactions/tx_list/op_refs.rs b/core/src/mantle/transactions/tx_list/op_refs.rs index c9f22fd04..c938a8875 100644 --- a/core/src/mantle/transactions/tx_list/op_refs.rs +++ b/core/src/mantle/transactions/tx_list/op_refs.rs @@ -63,3 +63,74 @@ impl Serialize for OpRefs<'_> { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::codec::{DeserializeOp as _, SerializeOp as _}; + + #[test] + fn get_returns_the_op_at_the_index_or_none_past_the_end() { + let ops = Ops::sample(); + let last = ops.len() - 1; + let references = OpRefs::from(&ops); + + assert_eq!(references.get(0), Some(ops[0].by_ref())); + assert_eq!(references.get(last), Some(ops[last].by_ref())); + assert_eq!(references.get(ops.len()), None); + } + + #[test] + fn hash_matches_the_ops_it_borrows() { + let ops = Ops::sample(); + + assert_eq!(OpRefs::from(&ops).hash(), ops.hash()); + } + + #[test] + fn serialize_to_json() { + let ops = Ops::sample(); + let op_refs = OpRefs::from(&ops); + + assert_eq!( + serde_json::to_value(&op_refs).expect("the human-readable arm serializes"), + serde_json::to_value(op_refs.inner()).expect("the inner column serializes") + ); + } + + #[test] + fn serialize_to_json_round_trips_into_ops() { + let ops = Ops::sample(); + let json = + serde_json::to_string(&OpRefs::from(&ops)).expect("the human-readable arm serializes"); + + assert_eq!( + serde_json::from_str::(&json).expect("the human-readable arm deserializes"), + ops + ); + } + + #[test] + fn serialize_to_binary() { + let ops = Ops::sample(); + let op_refs = OpRefs::from(&ops); + + assert_eq!( + op_refs.to_bytes().expect("the reference view serializes"), + bincode::serialize(&op_refs.encode().into_vec()).expect("the envelope serializes") + ); + } + + #[test] + fn serialize_to_binary_round_trips_into_ops() { + let ops = Ops::sample(); + let bytes = OpRefs::from(&ops) + .to_bytes() + .expect("the reference view serializes"); + + assert_eq!( + Ops::from_bytes(&bytes).expect("the owned column deserializes"), + ops + ); + } +} diff --git a/core/src/mantle/transactions/tx_list/ops.rs b/core/src/mantle/transactions/tx_list/ops.rs index 0f85bf059..648c56ee2 100644 --- a/core/src/mantle/transactions/tx_list/ops.rs +++ b/core/src/mantle/transactions/tx_list/ops.rs @@ -347,22 +347,176 @@ pub mod mantle_spec { #[cfg(test)] mod tests { use super::*; + use crate::{ + codec::{DeserializeOp as _, SerializeOp as _}, + mantle::gas::MainnetGasProfile, + }; + + const CONFIGURATION_THRESHOLD: ChannelKeyIndex = 3; + const TRANSFER_THRESHOLD: ChannelKeyIndex = 2; #[test] - fn binary_serde_rejects_trailing_bytes_inside_transaction_envelope() { - let tx = Ops::empty(); - let mut encoded_tx = tx.encode().into_vec(); - encoded_tx.push(0); - let envelope = bincode::serialize(&encoded_tx).unwrap(); + fn minimum_total_gas_cost_sums_execution_and_storage() { + let ops = Ops::from([Op::ChannelInscribe(InscriptionOp::sample())]); + let context = OpsGasContext::new(HashMap::new(), HashMap::new(), GasPrices::new(2, 3)); + + assert_eq!( + ops.minimum_total_gas_cost::(&context), + Ok(GasCost::new(643)) + ); + } + + #[test] + fn minimum_execution_gas_consumption_uses_channel_thresholds() { + let ops = Ops::from([ + Op::ChannelConfig(ChannelConfigOp::sample()), + Op::ChannelDeposit(DepositOp::sample()), + Op::ChannelWithdraw(ChannelWithdrawOp::sample()), + ]); + let context = OpsGasContext::new( + [(ChannelWithdrawOp::sample().channel_id, TRANSFER_THRESHOLD)].into(), + [(ChannelConfigOp::sample().channel, CONFIGURATION_THRESHOLD)].into(), + GasPrices::new(1, 0), + ); + + assert_eq!( + ops.minimum_execution_gas_consumption::(&context), + Ok(Gas::from(168 + 590 + 112)) + ); + } + + #[test] + fn minimum_execution_gas_consumption_charges_nothing_for_a_channel_the_context_does_not_hold() { + let ops = Ops::from([ + Op::ChannelConfig(ChannelConfigOp::sample()), + Op::ChannelWithdraw(ChannelWithdrawOp::sample()), + Op::ChannelTransfer(ChannelTransferOp::sample()), + ]); + + assert_eq!( + ops.minimum_execution_gas_consumption::(&OpsGasContext::default()), + Ok(Gas::from(0)) + ); + } + + #[test] + fn minimum_execution_gas_consumption_charges_the_flat_cost_of_an_op_without_a_channel() { + let ops = Ops::from([Op::ChannelDeposit(DepositOp::sample())]); + + assert_eq!( + ops.minimum_execution_gas_consumption::(&OpsGasContext::default()), + Ok(Gas::from(590)) + ); + } + + #[test] + fn minimum_storage_gas_cost_prices_the_signed_size() { + let ops = Ops::from([Op::ChannelInscribe(InscriptionOp::sample())]); + let context = OpsGasContext::new(HashMap::new(), HashMap::new(), GasPrices::new(2, 3)); + + assert_eq!( + ops.minimum_storage_gas_cost(&context), + Ok(GasCost::new(531)) + ); + } + + #[test] + fn minimum_signed_serialized_size_counts_the_length_prefix_of_an_empty_column() { + assert_eq!( + Ops::empty().minimum_signed_serialized_size(&OpsGasContext::default()), + 1 + ); + } + + #[test] + fn minimum_signed_serialized_size_adds_the_proof_each_op_will_carry() { + let ops = Ops::from([Op::ChannelInscribe(InscriptionOp::sample())]); + + assert_eq!( + ops.minimum_signed_serialized_size(&OpsGasContext::default()), + 177 + ); + } + + #[test] + fn serialize_to_json() { + let ops = Ops::sample(); + + assert_eq!( + serde_json::to_value(&ops).expect("the human-readable arm serializes"), + serde_json::to_value(ops.inner()).expect("the inner column serializes") + ); + } + + #[test] + fn serialize_to_binary() { + let ops = Ops::sample(); + + assert_eq!( + ops.to_bytes().expect("the binary arm serializes"), + bincode::serialize(&ops.encode().into_vec()).expect("the envelope serializes") + ); + } + + #[test] + fn deserialize_from_json() { + let ops = Ops::sample(); + let json = serde_json::to_value(ops.inner()).expect("the inner column serializes"); + + assert_eq!( + serde_json::from_value::(json).expect("the human-readable arm deserializes"), + ops + ); + } + + #[test] + fn deserialize_from_binary() { + let ops = Ops::sample(); + let envelope = + bincode::serialize(&ops.encode().into_vec()).expect("the envelope serializes"); + + assert_eq!( + Ops::from_bytes(&envelope).expect("the binary arm deserializes"), + ops + ); + } + + #[test] + fn deserialize_from_binary_rejects_trailing_bytes() { + let mut encoded_ops = Ops::empty().encode().into_vec(); + encoded_ops.push(0); + let envelope = bincode::serialize(&encoded_ops).expect("the envelope serializes"); assert!(bincode::deserialize::(&envelope).is_err()); } #[test] - fn binary_serde_rejects_oversized_transaction_envelope() { + fn deserialize_from_binary_rejects_an_oversized_envelope() { let oversized = vec![0u8; MAX_BLOCK_TRANSACTIONS_SIZE + 1]; - let envelope = bincode::serialize(&oversized).unwrap(); + let envelope = bincode::serialize(&oversized).expect("the envelope serializes"); assert!(bincode::deserialize::(&envelope).is_err()); } + + #[test] + fn mantle_spec_serialize_wraps_the_column_in_an_ops_field() { + let ops = Ops::sample(); + + assert_eq!( + mantle_spec::serialize(&ops, serde_json::value::Serializer) + .expect("the human-readable arm serializes"), + serde_json::json!({ "ops": serde_json::to_value(&ops).expect("the column serializes") }) + ); + } + + #[test] + fn mantle_spec_deserialize_reads_the_ops_field() { + let ops = Ops::sample(); + let json = serde_json::json!({ "ops": serde_json::to_value(&ops).expect("the column serializes") }); + + assert_eq!( + mantle_spec::deserialize(json).expect("the human-readable arm deserializes"), + ops + ); + } } diff --git a/core/src/mantle/transactions/tx_list/signed_ops.rs b/core/src/mantle/transactions/tx_list/signed_ops.rs index 9a4867603..a4e83aba9 100644 --- a/core/src/mantle/transactions/tx_list/signed_ops.rs +++ b/core/src/mantle/transactions/tx_list/signed_ops.rs @@ -492,34 +492,51 @@ pub mod test_utils { #[cfg(test)] mod tests { + use lb_codec::BinaryEncode as _; use lb_groth16::Fr; use lb_key_management_system_keys::keys::{Ed25519Key, ZkKey}; use num_bigint::BigUint; - use crate::mantle::{ - Note, NoteId, Op, OpProof, SignedOps, TxGasCalculator, Utxo, VerificationError, - channel::Error, - gas::MainnetGasProfile, - ledger::{Inputs, Outputs, OutputsError, verification_mode::StandardMode}, - ops::{ - channel::{ - ChannelId, config::ChannelConfigOp, deposit::DepositOp, - verification::test_utils::create_channel_multi_sig_proof, - withdraw::ChannelWithdrawOp, + use super::Error as SignedOpsError; + use crate::{ + codec::{DeserializeOp as _, SerializeOp as _}, + mantle::{ + Note, NoteId, Op, OpProof, SignedOps, TxGasCalculator, Utxo, VerificationError, + channel::Error, + gas::{Gas, GasCost, MainnetGasProfile}, + ledger::{Inputs, Outputs, OutputsError, verification_mode::StandardMode}, + ops::{ + channel::{ + ChannelId, config::ChannelConfigOp, deposit::DepositOp, + inscribe::InscriptionOp, + verification::test_utils::create_channel_multi_sig_proof, + withdraw::ChannelWithdrawOp, + }, + transfer::{TransferError, TransferOp}, }, - transfer::{TransferError, TransferOp}, - }, - traits::Hashable as _, - transactions::{ - GasPrices, OpProofs, - states::{Preverified, Unverified}, - tx_list::{ - ops::OpsGasContext, - signed_ops::test_utils::{create_test_inscribe_op, create_test_mantle_tx}, + traits::Hashable as _, + transactions::{ + GasPrices, OpProofs, Ops, + states::{Preverified, Unverified}, + tx_list::signed_ops::test_utils::{create_test_inscribe_op, create_test_mantle_tx}, }, }, }; + fn sample_columns() -> (Ops, OpProofs) { + let ops = Ops::sample(); + let op_proofs = OpProofs::new_unchecked(ops.iter().map(Op::sample_proof).collect()); + (ops, op_proofs) + } + + fn mantle_spec_json(ops: &Ops, op_proofs: &OpProofs) -> serde_json::Value { + serde_json::json!({ + "mantle_tx": { "ops": serde_json::to_value(ops).expect("the op column serializes") }, + "ops_proofs": serde_json::to_value(op_proofs.inner()) + .expect("the proof column serializes"), + }) + } + fn create_config_op(channel: ChannelId, signing_key: &Ed25519Key) -> ChannelConfigOp { ChannelConfigOp { channel, @@ -539,6 +556,17 @@ mod tests { } } + fn inscribe_ops() -> Ops { + Ops::from([Op::ChannelInscribe(InscriptionOp::sample())]) + } + + fn two_column_ops() -> Ops { + Ops::from([ + Op::ChannelInscribe(InscriptionOp::sample()), + Op::ChannelDeposit(DepositOp::sample()), + ]) + } + fn create_withdraw_op(channel_id: ChannelId) -> ChannelWithdrawOp { ChannelWithdrawOp { channel_id, @@ -547,88 +575,36 @@ mod tests { } #[test] - fn unsigned_execution_gas_uses_channel_thresholds() { - let signing_key = Ed25519Key::from_bytes(&[1; 32]); + fn from_parts_rejects_a_proof_column_of_a_different_length() { + let ops = two_column_ops(); + let op_proofs = OpProofs::new_unchecked(vec![ops[0].sample_proof()]); - let config_channel = ChannelId::from([2; 32]); - let deposit_channel = ChannelId::from([3; 32]); - let withdraw_channel = ChannelId::from([4; 32]); - - let mantle_tx = create_test_mantle_tx(vec![ - Op::ChannelConfig(create_config_op(config_channel, &signing_key)), - Op::ChannelDeposit(create_deposit_op(deposit_channel)), - Op::ChannelWithdraw(create_withdraw_op(withdraw_channel)), - ]); - - let config_threshold = 3; - let transfer_threshold = 2; - let context = OpsGasContext::new( - [(withdraw_channel, transfer_threshold)].into(), - [(config_channel, config_threshold)].into(), - GasPrices::new(1, 0), - ); - - let gas = mantle_tx - .minimum_execution_gas_consumption::(&context) - .unwrap(); - - let expected_config_gas = u64::from(config_threshold) * 56; - let expected_deposit_gas = 590; - let expected_withdraw_gas = u64::from(transfer_threshold) * 56; - let expected_total_gas = expected_config_gas + expected_deposit_gas + expected_withdraw_gas; - - assert_eq!(gas.into_inner(), expected_total_gas); + assert!(matches!( + SignedOps::::from_parts(ops, op_proofs), + Err(SignedOpsError::LengthMismatch { + operations: 2, + proofs: 1 + }) + )); } #[test] - fn signed_execution_gas_uses_multi_signature_proof_lengths() { - let config_keys = [ - Ed25519Key::from_bytes(&[1; 32]), - Ed25519Key::from_bytes(&[2; 32]), - Ed25519Key::from_bytes(&[3; 32]), - ]; - let withdraw_keys = [ - Ed25519Key::from_bytes(&[4; 32]), - Ed25519Key::from_bytes(&[5; 32]), - ]; - let config_signers = [&config_keys[0], &config_keys[1], &config_keys[2]]; - let withdraw_signers = [&withdraw_keys[0], &withdraw_keys[1]]; - - let config_channel = ChannelId::from([6; 32]); - let deposit_channel = ChannelId::from([7; 32]); - let withdraw_channel = ChannelId::from([8; 32]); - - let mantle_tx = create_test_mantle_tx(vec![ - Op::ChannelConfig(create_config_op(config_channel, &config_keys[0])), - Op::ChannelDeposit(create_deposit_op(deposit_channel)), - Op::ChannelWithdraw(create_withdraw_op(withdraw_channel)), + fn from_parts_reports_the_index_of_the_mismatched_proof() { + let ops = Ops::from([ + Op::ChannelInscribe(InscriptionOp::sample()), + Op::ChannelDeposit(DepositOp::sample()), + Op::ChannelInscribe(InscriptionOp::sample()), + ]); + let op_proofs = OpProofs::new_unchecked(vec![ + ops[0].sample_proof(), + ops[2].sample_proof(), + ops[2].sample_proof(), ]); - let tx_hash = mantle_tx.hash(); - let config_proof = create_channel_multi_sig_proof(&tx_hash, &config_signers); - let deposit_proof = ZkKey::multi_sign(&[], &tx_hash.to_fr()).unwrap(); - let withdraw_proof = create_channel_multi_sig_proof(&tx_hash, &withdraw_signers); - - let op_proofs = OpProofs::from([ - OpProof::ChannelMultiSigProof(config_proof), - OpProof::ZkSig(deposit_proof), - OpProof::ChannelMultiSigProof(withdraw_proof), - ]); - let signed_ops = SignedOps::<_, StandardMode>::from_parts(mantle_tx, op_proofs).unwrap(); - - let gas_prices = GasPrices::new(1, 0); - let gas = TxGasCalculator::execution_gas_consumption::( - &signed_ops, - &gas_prices, - ) - .unwrap(); - - let expected_config_gas = config_keys.len() as u64 * 56; - let expected_deposit_gas = 590; - let expected_withdraw_gas = withdraw_keys.len() as u64 * 56; - let expected_total_gas = expected_config_gas + expected_deposit_gas + expected_withdraw_gas; - - assert_eq!(gas.into_inner(), expected_total_gas); + assert!(matches!( + SignedOps::::from_parts(ops, op_proofs), + Err(SignedOpsError::OpProofMismatch { index: 1, .. }) + )); } #[test] @@ -758,6 +734,189 @@ mod tests { ); } + #[test] + fn trusted_constructors_skip_preverification() { + let signing_key = Ed25519Key::from_bytes(&[1; 32]); + let wrong_key = Ed25519Key::from_bytes(&[2; 32]); + let ops = create_test_mantle_tx(vec![Op::ChannelInscribe(create_test_inscribe_op( + &signing_key, + ))]); + let op_proofs = OpProofs::from([OpProof::Ed25519Sig( + wrong_key.sign_payload(&ops.hash().as_signing_bytes()), + )]); + + assert!( + SignedOps::::from_parts(ops.clone(), op_proofs.clone()) + .expect("the proof matches the op") + .preverify() + .is_err() + ); + + let trusted = SignedOps::::from_parts_trusted( + ops.clone(), + op_proofs.clone(), + ) + .expect("the proof matches the op"); + + assert_eq!(trusted.op_refs(), ops.by_ref()); + assert_eq!( + SignedOps::::from_parts(ops, op_proofs) + .expect("the proof matches the op") + .into_preverified_trusted(), + trusted + ); + } + + #[test] + fn changing_a_proof_does_not_change_the_transaction_hash() { + let signing_key = Ed25519Key::from_bytes(&[1; 32]); + let other_key = Ed25519Key::from_bytes(&[2; 32]); + let ops = create_test_mantle_tx(vec![Op::ChannelInscribe(create_test_inscribe_op( + &signing_key, + ))]); + let tx_hash = ops.hash(); + + let signed = SignedOps::::from_parts( + ops.clone(), + OpProofs::from([OpProof::Ed25519Sig( + signing_key.sign_payload(&tx_hash.as_signing_bytes()), + )]), + ) + .expect("the proof matches the op"); + let resigned = SignedOps::::from_parts( + ops, + OpProofs::from([OpProof::Ed25519Sig( + other_key.sign_payload(&tx_hash.as_signing_bytes()), + )]), + ) + .expect("the proof matches the op"); + + assert_ne!(signed, resigned); + assert_eq!(signed.hash(), tx_hash); + assert_eq!(resigned.hash(), tx_hash); + } + + #[test] + fn signed_execution_gas_uses_multi_signature_proof_lengths() { + let config_keys = [ + Ed25519Key::from_bytes(&[1; 32]), + Ed25519Key::from_bytes(&[2; 32]), + Ed25519Key::from_bytes(&[3; 32]), + ]; + let withdraw_keys = [ + Ed25519Key::from_bytes(&[4; 32]), + Ed25519Key::from_bytes(&[5; 32]), + ]; + let config_signers = [&config_keys[0], &config_keys[1], &config_keys[2]]; + let withdraw_signers = [&withdraw_keys[0], &withdraw_keys[1]]; + + let config_channel = ChannelId::from([6; 32]); + let deposit_channel = ChannelId::from([7; 32]); + let withdraw_channel = ChannelId::from([8; 32]); + + let mantle_tx = create_test_mantle_tx(vec![ + Op::ChannelConfig(create_config_op(config_channel, &config_keys[0])), + Op::ChannelDeposit(create_deposit_op(deposit_channel)), + Op::ChannelWithdraw(create_withdraw_op(withdraw_channel)), + ]); + + let tx_hash = mantle_tx.hash(); + let config_proof = create_channel_multi_sig_proof(&tx_hash, &config_signers); + let deposit_proof = ZkKey::multi_sign(&[], &tx_hash.to_fr()).unwrap(); + let withdraw_proof = create_channel_multi_sig_proof(&tx_hash, &withdraw_signers); + + let op_proofs = OpProofs::from([ + OpProof::ChannelMultiSigProof(config_proof), + OpProof::ZkSig(deposit_proof), + OpProof::ChannelMultiSigProof(withdraw_proof), + ]); + let signed_ops = SignedOps::<_, StandardMode>::from_parts(mantle_tx, op_proofs).unwrap(); + + let gas_prices = GasPrices::new(1, 0); + let gas = TxGasCalculator::execution_gas_consumption::( + &signed_ops, + &gas_prices, + ) + .unwrap(); + + let expected_config_gas = config_keys.len() as u64 * 56; + let expected_deposit_gas = 590; + let expected_withdraw_gas = withdraw_keys.len() as u64 * 56; + let expected_total_gas = expected_config_gas + expected_deposit_gas + expected_withdraw_gas; + + assert_eq!(gas.into_inner(), expected_total_gas); + } + + #[test] + fn storage_gas_consumption_counts_the_signed_encoded_bytes() { + let signed_ops = + SignedOps::::from_ops_with_sample_proofs(inscribe_ops()); + + assert_eq!( + TxGasCalculator::storage_gas_consumption(&signed_ops, &GasPrices::new(2, 3)), + Ok(Gas::from(177)) + ); + } + + #[test] + fn total_gas_cost_sums_execution_and_storage() { + let signed_ops = + SignedOps::::from_ops_with_sample_proofs(inscribe_ops()); + + assert_eq!( + signed_ops.total_gas_cost::(&GasPrices::new(2, 3)), + Ok(GasCost::new(643)) + ); + } + + #[test] + fn serialize_to_json() { + let (ops, op_proofs) = sample_columns(); + let signed_ops = + SignedOps::::from_parts(ops.clone(), op_proofs.clone()) + .expect("sample proofs pair with their ops"); + + assert_eq!( + serde_json::to_value(&signed_ops).expect("the human-readable arm serializes"), + mantle_spec_json(&ops, &op_proofs) + ); + } + + #[test] + fn serialize_to_binary() { + let signed_ops = SignedOps::::sample(); + + assert_eq!( + signed_ops.to_bytes().expect("the binary arm serializes"), + bincode::serialize(&signed_ops.encode_to_vec()).expect("the envelope serializes") + ); + } + + #[test] + fn deserialize_from_json() { + let (ops, op_proofs) = sample_columns(); + let json = mantle_spec_json(&ops, &op_proofs); + + assert_eq!( + serde_json::from_value::>(json) + .expect("the human-readable arm deserializes"), + SignedOps::from_parts(ops, op_proofs).expect("sample proofs pair with their ops") + ); + } + + #[test] + fn deserialize_from_binary() { + let signed_ops = SignedOps::::sample(); + let envelope = + bincode::serialize(&signed_ops.encode_to_vec()).expect("the envelope serializes"); + + assert_eq!( + SignedOps::::from_bytes(&envelope) + .expect("the binary arm deserializes"), + signed_ops + ); + } + #[test] fn test_signed_mantle_tx_deserialize_with_valid_proof() { let signing_key = Ed25519Key::from_bytes(&[1; 32]);