diff --git a/keetanetwork-block/Cargo.toml b/keetanetwork-block/Cargo.toml index dbc58c8..9d6dc15 100644 --- a/keetanetwork-block/Cargo.toml +++ b/keetanetwork-block/Cargo.toml @@ -8,11 +8,20 @@ repository.workspace = true homepage.workspace = true description = "Block structure and operations for Keetanetwork blockchain" +[features] +default = ["std"] +std = ["alloc"] +alloc = ["der/alloc"] + [dependencies] -keetanetwork-error = { version = "0.1.0", path = "../keetanetwork-error" } -snafu = { workspace = true } +der = { version = "0.7.10", default-features = false, features = ["derive"] } +serde = { version = "1.0", default-features = false, features = ["derive"] } +serde-json-core = { version = "0.6", default-features = false } +base64ct = { version = "1.8", default-features = false } [dev-dependencies] +der = "0.7.10" +hex-literal = "0.4" [lib] name = "keetanetwork_block" diff --git a/keetanetwork-block/src/block.rs b/keetanetwork-block/src/block.rs new file mode 100644 index 0000000..ff1195a --- /dev/null +++ b/keetanetwork-block/src/block.rs @@ -0,0 +1,690 @@ +//! KeetaBlock DER parsing and encoding +//! +//! This module provides `Decode` and `Encode` implementations for `KeetaBlock`, +//! enabling full block serialization and deserialization. +//! +//! ## Block Formats +//! +//! - **V1**: Plain SEQUENCE with version field = 0 +//! - **V2**: Wrapped in context tag `1` + +#[cfg(all(feature = "alloc", not(feature = "std")))] +use alloc::vec::Vec; + +#[cfg(feature = "std")] +use std::vec::Vec; + +use der::{asn1::OctetStringRef, Decode, Encode, Header, Length, Reader, Tag, TagNumber, Writer}; + +use crate::types::{ + BlockHeader, BlockPurpose, BlockVersion, KeetaBlock, MultiSigSigner, MultiSigSignerInfo, Operation, SignerField, +}; + +// ============================================================================ +// KeetaBlock Decode Implementation +// ============================================================================ + +#[cfg(any(feature = "alloc", feature = "std"))] +impl<'a> Decode<'a> for KeetaBlock<'a> { + fn decode>(reader: &mut R) -> der::Result { + // Peek at the first tag to determine block version + let tag = reader.peek_tag()?; + + if tag == Tag::Sequence { + // V1 block: plain SEQUENCE + decode_v1_block(reader) + } else if tag.is_context_specific() && tag.number() == TagNumber::new(1) { + // V2 block: context tag [1] + decode_v2_block(reader) + } else { + Err(Tag::Sequence.value_error()) + } + } +} + +/// Decode a V1 block +/// +/// V1 Structure: +/// ```text +/// SEQUENCE { +/// version INTEGER (= 0), +/// network INTEGER, +/// [subnet] NULL or INTEGER, +/// date GeneralizedTime, +/// account OCTET STRING, +/// [signer] NULL or OCTET STRING, +/// previous OCTET STRING, +/// operations SEQUENCE OF Operation, +/// signature OCTET STRING +/// } +/// ``` +#[cfg(any(feature = "alloc", feature = "std"))] +fn decode_v1_block<'a, R: Reader<'a>>(reader: &mut R) -> der::Result> { + reader.sequence(|seq| { + // version (must be 0 for V1) + let version: u8 = seq.decode()?; + if version != 0 { + return Err(Tag::Integer.value_error()); + } + + // network + let network: u64 = seq.decode()?; + + // subnet + let subnet = decode_null_or_integer(seq)?; + + // date + let date = read_generalized_time_bytes(seq)?; + + // account + let account: OctetStringRef = seq.decode()?; + + // signer + let signer = decode_v1_signer(seq)?; + + // previous + let previous: OctetStringRef = seq.decode()?; + + // operations + let operations = decode_operations(seq)?; + + // signature + let signature: OctetStringRef = seq.decode()?; + + Ok(KeetaBlock { + version: BlockVersion::V1, + header: BlockHeader { + network, + subnet, + date, + purpose: BlockPurpose::Generic, + account: account.as_bytes(), + signer, + previous: previous.as_bytes(), + }, + operations, + signatures: vec![signature.as_bytes()], + }) + }) +} + +/// Decode a V2 block +/// +/// V2 Structure: +/// ```text +/// [1] EXPLICIT { +/// SEQUENCE { +/// network INTEGER, +/// date GeneralizedTime, +/// purpose INTEGER, +/// account OCTET STRING, +/// signer NULL | OCTET STRING | SEQUENCE, +/// previous OCTET STRING, +/// operations SEQUENCE OF Operation, +/// signature OCTET STRING +/// } +/// } +/// ``` +#[cfg(any(feature = "alloc", feature = "std"))] +fn decode_v2_block<'a, R: Reader<'a>>(reader: &mut R) -> der::Result> { + // Read and verify context tag [1] + let header = Header::decode(reader)?; + if !header.tag.is_context_specific() || header.tag.number() != TagNumber::new(1) { + return Err(Tag::ContextSpecific { number: TagNumber::new(1), constructed: true }.value_error()); + } + + // Read the inner SEQUENCE + reader.sequence(|seq| { + // network + let network: u64 = seq.decode()?; + + // date + let date = read_generalized_time_bytes(seq)?; + + // purpose + let purpose_val: u8 = seq.decode()?; + let purpose = BlockPurpose::try_from(purpose_val).map_err(|_| Tag::Integer.value_error())?; + + // account + let account: OctetStringRef = seq.decode()?; + + // signer + let signer = decode_v2_signer(seq)?; + + // previous + let previous: OctetStringRef = seq.decode()?; + + // operations + let operations = decode_operations(seq)?; + + // signatures + let signatures = decode_signatures(seq)?; + + Ok(KeetaBlock { + version: BlockVersion::V2, + header: BlockHeader { + network, + subnet: None, + date, + purpose, + account: account.as_bytes(), + signer, + previous: previous.as_bytes(), + }, + operations, + signatures, + }) + }) +} + +// ============================================================================ +// Helper Functions for Decoding +// ============================================================================ + +/// Decodes `NULL` or `INTEGER`, returning `Option`. +#[cfg(any(feature = "alloc", feature = "std"))] +fn decode_null_or_integer<'a, R: Reader<'a>>(reader: &mut R) -> der::Result> { + let tag = reader.peek_tag()?; + if tag == Tag::Null { + let _: der::asn1::Null = reader.decode()?; + Ok(None) + } else { + let value: u64 = reader.decode()?; + Ok(Some(value)) + } +} + +/// Reads `GeneralizedTime` as raw content bytes. +#[cfg(any(feature = "alloc", feature = "std"))] +fn read_generalized_time_bytes<'a, R: Reader<'a>>(reader: &mut R) -> der::Result<&'a [u8]> { + let header = Header::decode(reader)?; + if header.tag != Tag::GeneralizedTime { + return Err(Tag::GeneralizedTime.value_error()); + } + reader.read_slice(header.length) +} + +/// Decodes V1 signer field. +#[cfg(any(feature = "alloc", feature = "std"))] +fn decode_v1_signer<'a, R: Reader<'a>>(reader: &mut R) -> der::Result> { + let tag = reader.peek_tag()?; + if tag == Tag::Null { + let _: der::asn1::Null = reader.decode()?; + Ok(SignerField::AccountIsSigner) + } else { + let signer: OctetStringRef = reader.decode()?; + Ok(SignerField::Single(signer.as_bytes())) + } +} + +/// Decodes V2 signer field. +#[cfg(any(feature = "alloc", feature = "std"))] +fn decode_v2_signer<'a, R: Reader<'a>>(reader: &mut R) -> der::Result> { + let tag = reader.peek_tag()?; + + if tag == Tag::Null { + let _: der::asn1::Null = reader.decode()?; + Ok(SignerField::AccountIsSigner) + } else if tag == Tag::OctetString { + let signer: OctetStringRef = reader.decode()?; + Ok(SignerField::Single(signer.as_bytes())) + } else if tag == Tag::Sequence { + // Multisig signer - read sequence content directly + let header = Header::decode(reader)?; + let content = reader.read_slice(header.length)?; + let multisig = decode_multisig_content(content)?; + Ok(SignerField::Multisig(multisig)) + } else { + Err(tag.value_error()) + } +} + +/// Decodes multisig signer info from `SEQUENCE` content. +#[cfg(any(feature = "alloc", feature = "std"))] +fn decode_multisig_content<'a>(content: &'a [u8]) -> der::Result> { + use der::SliceReader; + let mut reader = SliceReader::new(content)?; + + // multisig public key + let multisig_pub_key: OctetStringRef = reader.decode()?; + + // Read inner SEQUENCE header for signers + let inner_header = Header::decode(&mut reader)?; + if inner_header.tag != Tag::Sequence { + return Err(Tag::Sequence.value_error()); + } + let signers_content = reader.read_slice(inner_header.length)?; + + // Read signers from the inner content + let signers = decode_multisig_signers(signers_content)?; + + Ok(MultiSigSignerInfo { multisig_pub_key: multisig_pub_key.as_bytes(), signers }) +} + +/// Decodes a list of multisig signers from raw bytes. +#[cfg(any(feature = "alloc", feature = "std"))] +fn decode_multisig_signers<'a>(content: &'a [u8]) -> der::Result>> { + use der::SliceReader; + let mut reader = SliceReader::new(content)?; + let mut signers = Vec::new(); + + while !reader.is_finished() { + let tag = reader.peek_tag()?; + if tag == Tag::OctetString { + let key: OctetStringRef = reader.decode()?; + signers.push(MultiSigSigner::Key(key.as_bytes())); + } else if tag == Tag::Sequence { + // For nested multisig, read the content and recurse + let header = Header::decode(&mut reader)?; + let nested_content = reader.read_slice(header.length)?; + let nested = decode_multisig_content(nested_content)?; + signers.push(MultiSigSigner::Nested(Box::new(nested))); + } else { + return Err(tag.value_error()); + } + } + + Ok(signers) +} + +/// Decodes signatures from a single or multiple `OCTET STRING`. +#[cfg(any(feature = "alloc", feature = "std"))] +fn decode_signatures<'a, R: Reader<'a>>(reader: &mut R) -> der::Result> { + let tag = reader.peek_tag()?; + + if tag == Tag::OctetString { + // Single signature + let sig: OctetStringRef = reader.decode()?; + Ok(vec![sig.as_bytes()]) + } else if tag == Tag::Sequence { + // Multiple signatures wrapped in SEQUENCE + let header = Header::decode(reader)?; + let content = reader.read_slice(header.length)?; + + // Parse signatures from the sequence content + use der::SliceReader; + let mut sig_reader = SliceReader::new(content)?; + let mut signatures = Vec::new(); + + while !sig_reader.is_finished() { + let sig: OctetStringRef = sig_reader.decode()?; + signatures.push(sig.as_bytes()); + } + + Ok(signatures) + } else { + Err(Tag::OctetString.value_error()) + } +} + +/// Decodes operations sequence. +#[cfg(any(feature = "alloc", feature = "std"))] +fn decode_operations<'a, R: Reader<'a>>(reader: &mut R) -> der::Result>> { + let mut operations = Vec::new(); + reader.sequence(|seq| { + while !seq.is_finished() { + operations.push(Operation::decode(seq)?); + } + Ok(()) + })?; + Ok(operations) +} + +// ============================================================================ +// KeetaBlock Encode Implementation +// ============================================================================ + +#[cfg(any(feature = "alloc", feature = "std"))] +impl Encode for KeetaBlock<'_> { + fn encoded_len(&self) -> der::Result { + match self.version { + BlockVersion::V1 => self.v1_encoded_len(), + BlockVersion::V2 => self.v2_encoded_len(), + } + } + + fn encode(&self, writer: &mut impl Writer) -> der::Result<()> { + match self.version { + BlockVersion::V1 => self.encode_v1(writer), + BlockVersion::V2 => self.encode_v2(writer), + } + } +} + +#[cfg(any(feature = "alloc", feature = "std"))] +impl KeetaBlock<'_> { + /// Calculate encoded length for V1 block + fn v1_encoded_len(&self) -> der::Result { + let content_len = self.v1_content_len()?; + Header::new(Tag::Sequence, content_len)?.encoded_len() + content_len + } + + /// Calculate V1 content length + fn v1_content_len(&self) -> der::Result { + // version (0 for V1) + let version_len = 0u8.encoded_len()?; + + // network + let network_len = self.header.network.encoded_len()?; + + // subnet (NULL or INTEGER) + let subnet_len = match self.header.subnet { + Some(s) => s.encoded_len()?, + None => der::asn1::Null.encoded_len()?, + }; + + // date + let date_len = encode_generalized_time_len(self.header.date)?; + + // account + let account_len = OctetStringRef::new(self.header.account)?.encoded_len()?; + + // signer + let signer_len = encode_signer_len(&self.header.signer)?; + + // previous + let previous_len = OctetStringRef::new(self.header.previous)?.encoded_len()?; + + // operations + let ops_len = self.operations_encoded_len()?; + + // signature + let sig_len = if !self.signatures.is_empty() { + OctetStringRef::new(self.signatures[0])?.encoded_len()? + } else { + Length::ZERO + }; + + version_len + network_len + subnet_len + date_len + account_len + signer_len + previous_len + ops_len + sig_len + } + + /// Encode V1 block + fn encode_v1(&self, writer: &mut impl Writer) -> der::Result<()> { + let content_len = self.v1_content_len()?; + Header::new(Tag::Sequence, content_len)?.encode(writer)?; + + // version (0 for V1) + 0u8.encode(writer)?; + + // network + self.header.network.encode(writer)?; + + // subnet + match self.header.subnet { + Some(s) => s.encode(writer)?, + None => der::asn1::Null.encode(writer)?, + }; + + // date + encode_generalized_time(self.header.date, writer)?; + + // account + OctetStringRef::new(self.header.account)?.encode(writer)?; + + // signer + encode_signer(&self.header.signer, writer)?; + + // previous + OctetStringRef::new(self.header.previous)?.encode(writer)?; + + // operations + self.encode_operations(writer)?; + + // signature + if !self.signatures.is_empty() { + OctetStringRef::new(self.signatures[0])?.encode(writer)?; + } + + Ok(()) + } + + /// Calculate encoded length for V2 block + fn v2_encoded_len(&self) -> der::Result { + let inner_content_len = self.v2_content_len()?; + let inner_seq_len = (Header::new(Tag::Sequence, inner_content_len)?.encoded_len() + inner_content_len)?; + + // Context tag [1] wrapping + let ctx_header = + Header::new(Tag::ContextSpecific { number: TagNumber::new(1), constructed: true }, inner_seq_len)?; + ctx_header.encoded_len() + inner_seq_len + } + + /// Calculate V2 content length + fn v2_content_len(&self) -> der::Result { + // network + let network_len = self.header.network.encoded_len()?; + + // date + let date_len = encode_generalized_time_len(self.header.date)?; + + // purpose + let purpose_len = (self.header.purpose as u8).encoded_len()?; + + // account + let account_len = OctetStringRef::new(self.header.account)?.encoded_len()?; + + // signer + let signer_len = encode_signer_len(&self.header.signer)?; + + // previous + let previous_len = OctetStringRef::new(self.header.previous)?.encoded_len()?; + + // operations + let ops_len = self.operations_encoded_len()?; + + // signatures + let sig_len = self.signatures_encoded_len()?; + + network_len + date_len + purpose_len + account_len + signer_len + previous_len + ops_len + sig_len + } + + /// Encode V2 block + fn encode_v2(&self, writer: &mut impl Writer) -> der::Result<()> { + let inner_content_len = self.v2_content_len()?; + let inner_seq_len = (Header::new(Tag::Sequence, inner_content_len)?.encoded_len() + inner_content_len)?; + + // Write context tag [1] + Header::new(Tag::ContextSpecific { number: TagNumber::new(1), constructed: true }, inner_seq_len)? + .encode(writer)?; + + // Write inner SEQUENCE + Header::new(Tag::Sequence, inner_content_len)?.encode(writer)?; + + // network + self.header.network.encode(writer)?; + + // date + encode_generalized_time(self.header.date, writer)?; + + // purpose + (self.header.purpose as u8).encode(writer)?; + + // account + OctetStringRef::new(self.header.account)?.encode(writer)?; + + // signer + encode_signer(&self.header.signer, writer)?; + + // previous + OctetStringRef::new(self.header.previous)?.encode(writer)?; + + // operations + self.encode_operations(writer)?; + + // signature(s) + self.encode_signatures(writer)?; + + Ok(()) + } + + /// Calculate encoded length of signatures + fn signatures_encoded_len(&self) -> der::Result { + if self.signatures.is_empty() { + return Ok(Length::ZERO); + } + + if self.signatures.len() == 1 { + // Single signature: just OCTET STRING + OctetStringRef::new(self.signatures[0])?.encoded_len() + } else { + // Multiple signatures: SEQUENCE of OCTET STRINGs + let mut content_len = Length::ZERO; + for sig in &self.signatures { + content_len = (content_len + OctetStringRef::new(sig)?.encoded_len()?)?; + } + Header::new(Tag::Sequence, content_len)?.encoded_len() + content_len + } + } + + /// Encode signatures + fn encode_signatures(&self, writer: &mut impl Writer) -> der::Result<()> { + if self.signatures.is_empty() { + return Ok(()); + } + + if self.signatures.len() == 1 { + // Single signature: just OCTET STRING + OctetStringRef::new(self.signatures[0])?.encode(writer) + } else { + // Multiple signatures: SEQUENCE of OCTET STRINGs + let mut content_len = Length::ZERO; + for sig in &self.signatures { + content_len = (content_len + OctetStringRef::new(sig)?.encoded_len()?)?; + } + Header::new(Tag::Sequence, content_len)?.encode(writer)?; + for sig in &self.signatures { + OctetStringRef::new(sig)?.encode(writer)?; + } + Ok(()) + } + } + + /// Calculate encoded length of operations sequence + fn operations_encoded_len(&self) -> der::Result { + let mut content_len = Length::ZERO; + for op in &self.operations { + content_len = (content_len + op.encoded_len()?)?; + } + Header::new(Tag::Sequence, content_len)?.encoded_len() + content_len + } + + /// Encode operations sequence + fn encode_operations(&self, writer: &mut impl Writer) -> der::Result<()> { + let mut content_len = Length::ZERO; + for op in &self.operations { + content_len = (content_len + op.encoded_len()?)?; + } + Header::new(Tag::Sequence, content_len)?.encode(writer)?; + for op in &self.operations { + op.encode(writer)?; + } + Ok(()) + } +} + +// ============================================================================ +// Helper Functions for Encoding +// ============================================================================ + +/// Calculates signer field encoded length. +#[cfg(any(feature = "alloc", feature = "std"))] +fn encode_signer_len(signer: &SignerField) -> der::Result { + match signer { + SignerField::AccountIsSigner => der::asn1::Null.encoded_len(), + SignerField::Single(key) => OctetStringRef::new(key)?.encoded_len(), + SignerField::Multisig(info) => encode_multisig_len(info), + } +} + +/// Encodes signer field. +#[cfg(any(feature = "alloc", feature = "std"))] +fn encode_signer(signer: &SignerField, writer: &mut impl Writer) -> der::Result<()> { + match signer { + SignerField::AccountIsSigner => der::asn1::Null.encode(writer), + SignerField::Single(key) => OctetStringRef::new(key)?.encode(writer), + SignerField::Multisig(info) => encode_multisig(info, writer), + } +} + +/// Calculates multisig signer encoded length. +#[cfg(any(feature = "alloc", feature = "std"))] +fn encode_multisig_len(info: &MultiSigSignerInfo) -> der::Result { + let pubkey_len = OctetStringRef::new(info.multisig_pub_key)?.encoded_len()?; + + let mut signers_content_len = Length::ZERO; + for signer in &info.signers { + let signer_len = match signer { + MultiSigSigner::Key(key) => OctetStringRef::new(key)?.encoded_len()?, + MultiSigSigner::Nested(nested) => encode_multisig_len(nested)?, + }; + signers_content_len = (signers_content_len + signer_len)?; + } + let signers_seq_len = (Header::new(Tag::Sequence, signers_content_len)?.encoded_len() + signers_content_len)?; + + let content_len = (pubkey_len + signers_seq_len)?; + Header::new(Tag::Sequence, content_len)?.encoded_len() + content_len +} + +/// Encodes multisig signer. +#[cfg(any(feature = "alloc", feature = "std"))] +fn encode_multisig(info: &MultiSigSignerInfo, writer: &mut impl Writer) -> der::Result<()> { + let pubkey_len = OctetStringRef::new(info.multisig_pub_key)?.encoded_len()?; + + let mut signers_content_len = Length::ZERO; + for signer in &info.signers { + let signer_len = match signer { + MultiSigSigner::Key(key) => OctetStringRef::new(key)?.encoded_len()?, + MultiSigSigner::Nested(nested) => encode_multisig_len(nested)?, + }; + signers_content_len = (signers_content_len + signer_len)?; + } + let signers_seq_len = (Header::new(Tag::Sequence, signers_content_len)?.encoded_len() + signers_content_len)?; + + let content_len = (pubkey_len + signers_seq_len)?; + + // Write outer sequence + Header::new(Tag::Sequence, content_len)?.encode(writer)?; + + // Write pubkey + OctetStringRef::new(info.multisig_pub_key)?.encode(writer)?; + + // Write signers sequence + Header::new(Tag::Sequence, signers_content_len)?.encode(writer)?; + for signer in &info.signers { + match signer { + MultiSigSigner::Key(key) => OctetStringRef::new(key)?.encode(writer)?, + MultiSigSigner::Nested(nested) => encode_multisig(nested, writer)?, + } + } + + Ok(()) +} + +/// Calculates `GeneralizedTime` encoded length. +#[cfg(any(feature = "alloc", feature = "std"))] +fn encode_generalized_time_len(date_bytes: &[u8]) -> der::Result { + let content_len = Length::try_from(date_bytes.len())?; + Header::new(Tag::GeneralizedTime, content_len)?.encoded_len() + content_len +} + +/// Encodes `GeneralizedTime` from raw bytes. +#[cfg(any(feature = "alloc", feature = "std"))] +fn encode_generalized_time(date_bytes: &[u8], writer: &mut impl Writer) -> der::Result<()> { + let content_len = Length::try_from(date_bytes.len())?; + Header::new(Tag::GeneralizedTime, content_len)?.encode(writer)?; + writer.write(date_bytes) +} + +// ============================================================================ +// BlockPurpose Encode +// ============================================================================ + +#[cfg(any(feature = "alloc", feature = "std"))] +impl From for u8 { + fn from(purpose: BlockPurpose) -> u8 { + match purpose { + BlockPurpose::Generic => 0, + BlockPurpose::Fee => 1, + } + } +} diff --git a/keetanetwork-block/src/lib.rs b/keetanetwork-block/src/lib.rs index 135bf02..8befacc 100644 --- a/keetanetwork-block/src/lib.rs +++ b/keetanetwork-block/src/lib.rs @@ -1,3 +1,73 @@ //! # Keetanetwork Block //! //! This crate provides block structure and operations for the Keetanetwork blockchain. + +#![cfg_attr(not(feature = "std"), no_std)] + +mod parse; +pub mod metadata; +pub mod permissions; +mod types; + +#[cfg(any(feature = "alloc", feature = "std"))] +mod block; + +pub use parse::extract_operations_slice; + +// Types that require alloc (use Vec) +#[cfg(any(feature = "alloc", feature = "std"))] +pub use types::{ + // Vote/Certificate types + AlgorithmIdentifier, + CertificateExtensionWrapper, + FeeData, + HashData, + // Block types + KeetaBlock, + KeetaBlockBuilder, + SubjectPublicKeyInfo, + TbsCertificate, + Validity, + Vote, + VoteStaple, +}; + +pub use types::{ + // Enums + AdjustMethod, + AdjustMethodRelative, + // Block types + BlockHeader, + BlockPurpose, + BlockVersion, + // Type aliases (maps to der types or raw bytes) + Bytes, + // Operation structs + CancelSwapOp, + CreateIdentifierArgs, + CreateIdentifierOp, + // Supporting types + FeeRate, + FeeValue, + FeeValueWithRecipient, + Int, + ManageCertificateOp, + MatchSwapOp, + ModifyPermissionsOp, + MultisigArgs, + // Value-or-none wrapper (like Option but NULL has meaning) + NullOr, + // Operation enum + Operation, + Permission, + ReceiveOp, + SendOp, + SetInfoOp, + SetRepOp, + Str, + SwapArgs, + TokenAdminModifyBalanceOp, + TokenAdminSupplyOp, + TokenRate, + TokenValue, +}; diff --git a/keetanetwork-block/src/metadata.rs b/keetanetwork-block/src/metadata.rs new file mode 100644 index 0000000..adf4407 --- /dev/null +++ b/keetanetwork-block/src/metadata.rs @@ -0,0 +1,256 @@ +//! Metadata parsing. +//! +//! Decodes base64 JSON metadata and extracts asset_id/authority/symbol. +//! This module is no_std compatible and does not allocate. + +use base64ct::{Base64, Encoding}; +use serde::Deserialize; + +const MAX_FIELD_LEN: usize = 64; + +/// Raw metadata structure for JSON deserialization. +#[derive(Deserialize)] +struct RawMetadata<'a> { + #[serde(default)] + asset_id: Option<&'a str>, + #[serde(default)] + authority: Option<&'a str>, + #[serde(default)] + symbol: Option<&'a str>, +} + +/// Decoded metadata with fixed-size buffers. +pub struct DecodedMetadata { + pub asset_id: [u8; MAX_FIELD_LEN], + pub asset_id_len: usize, + pub authority: [u8; MAX_FIELD_LEN], + pub authority_len: usize, + pub symbol: [u8; MAX_FIELD_LEN], + pub symbol_len: usize, +} + +impl DecodedMetadata { + /// Create a new empty metadata instance. + pub fn new() -> Self { + Self { + asset_id: [0u8; MAX_FIELD_LEN], + asset_id_len: 0, + authority: [0u8; MAX_FIELD_LEN], + authority_len: 0, + symbol: [0u8; MAX_FIELD_LEN], + symbol_len: 0, + } + } + + /// Get the asset_id as a string slice, if present. + pub fn asset_id_str(&self) -> Option<&str> { + if self.asset_id_len > 0 { + core::str::from_utf8(&self.asset_id[..self.asset_id_len]).ok() + } else { + None + } + } + + /// Get the authority as a string slice, if present. + pub fn authority_str(&self) -> Option<&str> { + if self.authority_len > 0 { + core::str::from_utf8(&self.authority[..self.authority_len]).ok() + } else { + None + } + } + + /// Get the symbol as a string slice, if present. + pub fn symbol_str(&self) -> Option<&str> { + if self.symbol_len > 0 { + core::str::from_utf8(&self.symbol[..self.symbol_len]).ok() + } else { + None + } + } +} + +impl Default for DecodedMetadata { + fn default() -> Self { + Self::new() + } +} + +/// Result of metadata decoding. +pub enum MetadataDisplay { + /// Successfully decoded metadata with at least one known field. + Decoded(DecodedMetadata), + /// Valid JSON but no known fields (asset_id, authority, symbol). + Unknown, + /// Invalid base64 encoding or malformed JSON. + Invalid, + /// Empty input string. + Empty, +} + +/// Decode base64 metadata and extract known fields. +/// +/// # Arguments +/// * `base64_input` - Base64-encoded JSON string +/// * `decode_buf` - Buffer for base64 decoding, must be >= input.len() * 3/4 +/// +/// # Returns +/// A `MetadataDisplay` variant indicating the result. +pub fn decode_metadata(base64_input: &str, decode_buf: &mut [u8]) -> MetadataDisplay { + if base64_input.is_empty() { + return MetadataDisplay::Empty; + } + + let decoded = match Base64::decode(base64_input.as_bytes(), decode_buf) { + Ok(bytes) => bytes, + Err(_) => return MetadataDisplay::Invalid, + }; + + let raw: RawMetadata = match serde_json_core::from_slice(decoded) { + Ok((meta, _)) => meta, + Err(_) => return MetadataDisplay::Invalid, + }; + + let mut result = DecodedMetadata::new(); + let mut found_any = false; + + if let Some(value) = raw.asset_id { + let bytes = value.as_bytes(); + let copy_len = bytes.len().min(MAX_FIELD_LEN); + result.asset_id[..copy_len].copy_from_slice(&bytes[..copy_len]); + result.asset_id_len = copy_len; + found_any = true; + } + + if let Some(value) = raw.authority { + let bytes = value.as_bytes(); + let copy_len = bytes.len().min(MAX_FIELD_LEN); + result.authority[..copy_len].copy_from_slice(&bytes[..copy_len]); + result.authority_len = copy_len; + found_any = true; + } + + if let Some(value) = raw.symbol { + let bytes = value.as_bytes(); + let copy_len = bytes.len().min(MAX_FIELD_LEN); + result.symbol[..copy_len].copy_from_slice(&bytes[..copy_len]); + result.symbol_len = copy_len; + found_any = true; + } + + if found_any { + MetadataDisplay::Decoded(result) + } else { + MetadataDisplay::Unknown + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_decode_metadata_full() { + let mut buf = [0u8; 512]; + + let json = r#"{"asset_id":"asset://1f0ccae9-5666/1","authority":"keeta_abc123def456","signature":"c2lnbmF0dXJl"}"#; + let b64 = base64_encode_for_test(json.as_bytes()); + + match decode_metadata(&b64, &mut buf) { + MetadataDisplay::Decoded(meta) => { + assert_eq!(meta.asset_id_str(), Some("asset://1f0ccae9-5666/1")); + assert_eq!(meta.authority_str(), Some("keeta_abc123def456")); + } + _ => panic!("Expected Decoded variant"), + } + } + + #[test] + fn test_decode_metadata_with_symbol() { + let mut buf = [0u8; 512]; + + let json = r#"{"asset_id":"asset://123","symbol":"KEETA"}"#; + let b64 = base64_encode_for_test(json.as_bytes()); + + match decode_metadata(&b64, &mut buf) { + MetadataDisplay::Decoded(meta) => { + assert_eq!(meta.asset_id_str(), Some("asset://123")); + assert_eq!(meta.symbol_str(), Some("KEETA")); + assert_eq!(meta.authority_str(), None); + } + _ => panic!("Expected Decoded variant"), + } + } + + #[test] + fn test_decode_metadata_empty() { + let mut buf = [0u8; 100]; + match decode_metadata("", &mut buf) { + MetadataDisplay::Empty => {} + _ => panic!("Expected Empty variant"), + } + } + + #[test] + fn test_decode_metadata_unknown_fields_only() { + let mut buf = [0u8; 512]; + + let json = r#"{"foo":"bar","baz":123}"#; + let b64 = base64_encode_for_test(json.as_bytes()); + + match decode_metadata(&b64, &mut buf) { + MetadataDisplay::Unknown => {} + _ => panic!("Expected Unknown variant"), + } + } + + #[test] + fn test_decode_metadata_invalid_base64() { + let mut buf = [0u8; 100]; + match decode_metadata("not-valid-base64!!!", &mut buf) { + MetadataDisplay::Invalid => {} + _ => panic!("Expected Invalid variant"), + } + } + + #[test] + fn test_decode_metadata_invalid_json() { + let mut buf = [0u8; 100]; + let b64 = base64_encode_for_test(b"not json at all"); + + match decode_metadata(&b64, &mut buf) { + MetadataDisplay::Invalid => {} + _ => panic!("Expected Invalid variant"), + } + } + + fn base64_encode_for_test(input: &[u8]) -> String { + const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + let mut result = String::new(); + + for chunk in input.chunks(3) { + let b0 = chunk[0] as u32; + let b1 = chunk.get(1).copied().unwrap_or(0) as u32; + let b2 = chunk.get(2).copied().unwrap_or(0) as u32; + + let n = (b0 << 16) | (b1 << 8) | b2; + + result.push(CHARS[((n >> 18) & 0x3F) as usize] as char); + result.push(CHARS[((n >> 12) & 0x3F) as usize] as char); + + if chunk.len() > 1 { + result.push(CHARS[((n >> 6) & 0x3F) as usize] as char); + } else { + result.push('='); + } + + if chunk.len() > 2 { + result.push(CHARS[(n & 0x3F) as usize] as char); + } else { + result.push('='); + } + } + + result + } +} diff --git a/keetanetwork-block/src/parse.rs b/keetanetwork-block/src/parse.rs new file mode 100644 index 0000000..22328a8 --- /dev/null +++ b/keetanetwork-block/src/parse.rs @@ -0,0 +1,83 @@ +//! DER utility functions for zero-copy parsing. + +use der::{Decode, Header, Reader, SliceReader, Tag, TagNumber}; + +/// Extracts the operations SEQUENCE content from raw block bytes. +/// +/// This function skips block header fields and returns a slice containing +/// the raw DER content of the operations SEQUENCE. +pub fn extract_operations_slice(data: &[u8]) -> Option<&[u8]> { + if data.is_empty() { + return None; + } + + let mut reader = SliceReader::new(data).ok()?; + let tag = reader.peek_tag().ok()?; + + if tag == Tag::Sequence { + // V1 block: plain SEQUENCE + extract_operations_v1(&mut reader) + } else if tag.is_context_specific() && tag.number() == TagNumber::new(1) { + // V2 block: context tag [1] + extract_operations_v2(&mut reader) + } else { + None + } +} + +/// Extract operations from V1 block. +fn extract_operations_v1<'a>(reader: &mut SliceReader<'a>) -> Option<&'a [u8]> { + let header = Header::decode(reader).ok()?; + if header.tag != Tag::Sequence { + return None; + } + let content = reader.read_slice(header.length).ok()?; + let mut inner = SliceReader::new(content).ok()?; + + // Skip V1 header fields: version, network, subnet, date, account, signer, previous + for _ in 0..7 { + skip_any(&mut inner).ok()?; + } + + read_sequence_content(&mut inner) +} + +/// Extract operations from V2 block. +fn extract_operations_v2<'a>(reader: &mut SliceReader<'a>) -> Option<&'a [u8]> { + let ctx_header = Header::decode(reader).ok()?; + if !ctx_header.tag.is_context_specific() || ctx_header.tag.number() != TagNumber::new(1) { + return None; + } + let inner_content = reader.read_slice(ctx_header.length).ok()?; + let mut inner = SliceReader::new(inner_content).ok()?; + + let seq_header = Header::decode(&mut inner).ok()?; + if seq_header.tag != Tag::Sequence { + return None; + } + let seq_content = inner.read_slice(seq_header.length).ok()?; + let mut seq_reader = SliceReader::new(seq_content).ok()?; + + // Skip V2 header fields: network, date, purpose, account, signer, previous + for _ in 0..6 { + skip_any(&mut seq_reader).ok()?; + } + + read_sequence_content(&mut seq_reader) +} + +/// Skip any DER element (tag + length + content). +pub(crate) fn skip_any<'a>(reader: &mut SliceReader<'a>) -> der::Result<()> { + let header = Header::decode(reader)?; + reader.read_slice(header.length)?; + Ok(()) +} + +/// Read a SEQUENCE and return its content bytes. +fn read_sequence_content<'a>(reader: &mut SliceReader<'a>) -> Option<&'a [u8]> { + let header = Header::decode(reader).ok()?; + if header.tag != Tag::Sequence { + return None; + } + reader.read_slice(header.length).ok() +} diff --git a/keetanetwork-block/src/permissions.rs b/keetanetwork-block/src/permissions.rs new file mode 100644 index 0000000..a3a2b1e --- /dev/null +++ b/keetanetwork-block/src/permissions.rs @@ -0,0 +1,63 @@ +//! Permission bit definitions for the Keetanetwork blockchain. +//! +//! This module defines the semantic meaning of permission bits used in +//! [`Permission`](crate::Permission) values. + +// Base permission bit constants +pub const ACCESS: u64 = 1 << 0; +pub const OWNER: u64 = 1 << 1; +pub const ADMIN: u64 = 1 << 2; +pub const UPDATE_INFO: u64 = 1 << 3; +pub const SEND_ON_BEHALF: u64 = 1 << 4; +pub const TOKEN_CREATE: u64 = 1 << 5; +pub const TOKEN_SUPPLY: u64 = 1 << 6; +pub const TOKEN_BALANCE: u64 = 1 << 7; +pub const STORAGE_CREATE: u64 = 1 << 8; +pub const STORAGE_HOLD: u64 = 1 << 9; +pub const STORAGE_DEPOSIT: u64 = 1 << 10; +pub const PERM_ADD: u64 = 1 << 11; +pub const PERM_REMOVE: u64 = 1 << 12; +pub const MANAGE_CERT: u64 = 1 << 13; +pub const MULTISIG_SIGNER: u64 = 1 << 14; + +/// Lookup table mapping each base permission bit to its display name. +pub const BASE_PERMISSIONS: [(u64, &str); 15] = [ + (ACCESS, "ACCESS"), + (OWNER, "OWNER"), + (ADMIN, "ADMIN"), + (UPDATE_INFO, "INFO"), + (SEND_ON_BEHALF, "SEND"), + (TOKEN_CREATE, "T_CREATE"), + (TOKEN_SUPPLY, "T_SUPPLY"), + (TOKEN_BALANCE, "T_BALANCE"), + (STORAGE_CREATE, "S_CREATE"), + (STORAGE_HOLD, "S_HOLD"), + (STORAGE_DEPOSIT, "S_DEPOSIT"), + (PERM_ADD, "P_ADD"), + (PERM_REMOVE, "P_REMOVE"), + (MANAGE_CERT, "CERT"), + (MULTISIG_SIGNER, "MULTISIG"), +]; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn no_duplicate_bits() { + let mut combined = 0u64; + for &(mask, _) in &BASE_PERMISSIONS { + assert_eq!(combined & mask, 0, "duplicate bit in BASE_PERMISSIONS"); + combined |= mask; + } + } + + #[test] + fn all_bits_contiguous() { + let mut combined = 0u64; + for &(mask, _) in &BASE_PERMISSIONS { + combined |= mask; + } + assert_eq!(combined, (1 << 15) - 1); + } +} diff --git a/keetanetwork-block/src/types.rs b/keetanetwork-block/src/types.rs new file mode 100644 index 0000000..ccadb52 --- /dev/null +++ b/keetanetwork-block/src/types.rs @@ -0,0 +1,1507 @@ +//! Keeta Network Block Types +//! +//! Type definitions for Keeta blockchain blocks and operations. +//! Zero-copy types with lifetime parameters for efficient parsing in `no_std` environments. +//! +//! ## Operation Tags +//! +//! | Tag | Operation | +//! |------|-------------------------| +//! | `0` | Send | +//! | `1` | SetRep | +//! | `2` | SetInfo | +//! | `3` | ModifyPermissions | +//! | `4` | CreateIdentifier | +//! | `5` | TokenAdminSupply | +//! | `6` | TokenAdminModifyBalance | +//! | `7` | Receive | +//! | `8` | ManageCertificate | +//! | `9` | MatchSwap | +//! | `10` | CancelSwap | + +// Use alloc for Vec when not using std +#[cfg(all(feature = "alloc", not(feature = "std")))] +extern crate alloc; + +#[cfg(all(feature = "alloc", not(feature = "std")))] +use alloc::vec::Vec; + +#[cfg(feature = "std")] +use std::vec::Vec; + +// DER encoding/decoding support +use der::{ + asn1::{IntRef, Null, OctetStringRef, Utf8StringRef}, + Choice, Decode, DecodeValue, Encode, EncodeValue, Header, Length, Reader, Sequence, SliceReader, Tag, Writer, +}; + +/// Reads raw TLV bytes (tag + length + content) from reader. +fn read_tlv_bytes<'a, R: Reader<'a>>(reader: &mut R) -> der::Result<&'a [u8]> { + let header = reader.peek_header()?; + let tlv_len = (header.encoded_len()? + header.length)?; + reader.read_slice(tlv_len) +} + +// Type aliases for DER types +pub type Bytes<'a> = OctetStringRef<'a>; +pub type Int<'a> = IntRef<'a>; +pub type Str<'a> = Utf8StringRef<'a>; + +// ============================================================================ +// NullOr Type +// ============================================================================ + +/// Either a value of type `T` or an explicit "none" marker. +/// +/// Used for fields where `None` has a specific meaning (e.g., "use sell token +/// as fee token") rather than just being absent. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NullOr { + /// NULL - represents none/default + Null, + /// Value present + Value(T), +} + +impl NullOr { + /// Get the value if present, None if NULL + pub fn value(&self) -> Option<&T> { + match self { + NullOr::Null => None, + NullOr::Value(v) => Some(v), + } + } +} + +// Implement Decode for NullOr +impl<'a, T: Decode<'a>> Decode<'a> for NullOr { + fn decode>(reader: &mut R) -> der::Result { + let tag = reader.peek_tag()?; + if tag == Tag::Null { + let _: Null = reader.decode()?; + Ok(NullOr::Null) + } else { + Ok(NullOr::Value(T::decode(reader)?)) + } + } +} + +// Implement Encode for NullOr +impl Encode for NullOr { + fn encoded_len(&self) -> der::Result { + match self { + NullOr::Null => Null.encoded_len(), + NullOr::Value(v) => v.encoded_len(), + } + } + + fn encode(&self, writer: &mut impl Writer) -> der::Result<()> { + match self { + NullOr::Null => Null.encode(writer), + NullOr::Value(v) => v.encode(writer), + } + } +} + +// ============================================================================ +// Block Types +// ============================================================================ + +/// Block version +/// +/// - V1: Unwrapped format with internal version field = 0 +/// - V2: Tagged format (explicit context tag 1) +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum BlockVersion { + V1 = 1, + V2 = 2, +} + +impl BlockVersion { + /// V1 blocks have internal version field 0, V2 blocks use tag 1 + pub fn from_version_field(version: u8) -> Option { + match version { + 0 => Some(BlockVersion::V1), + _ => None, + } + } + + /// Create from tag value (for V2 blocks) + pub fn from_context_tag(tag: u8) -> Option { + match tag { + 1 => Some(BlockVersion::V2), + _ => None, + } + } +} + +/// Block purpose (V2 only) +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BlockPurpose { + Generic, + Fee, +} + +impl TryFrom for BlockPurpose { + type Error = u8; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(BlockPurpose::Generic), + 1 => Ok(BlockPurpose::Fee), + n => Err(n), + } + } +} + +/// Block header information +#[derive(Debug, Clone)] +pub struct BlockHeader<'a> { + /// Network ID + pub network: u64, + /// Subnet ID (optional, V1 only - ignored for V2 blocks) + pub subnet: Option, + /// Block timestamp as raw bytes + pub date: &'a [u8], + /// Block purpose (V2 only, defaults to Generic for V1) + pub purpose: BlockPurpose, + /// Account public key with type prefix + pub account: &'a [u8], + /// Signer information (V1: always Single, V2: can be Single/Multisig/AccountIsSigner) + #[cfg(any(feature = "alloc", feature = "std"))] + pub signer: SignerField<'a>, + /// Previous block hash (32 bytes) + pub previous: &'a [u8], +} + +/// Parsed Keeta block (unified view for both V1 and V2) +#[cfg(any(feature = "alloc", feature = "std"))] +#[derive(Debug, Clone)] +pub struct KeetaBlock<'a> { + /// Block version + pub version: BlockVersion, + /// Block header/metadata + pub header: BlockHeader<'a>, + /// Operations in the block + pub operations: Vec>, + /// Signatures (V1: single, V2: one or more) + pub signatures: Vec<&'a [u8]>, +} + +/// Builder for constructing KeetaBlock instances +#[cfg(any(feature = "alloc", feature = "std"))] +#[derive(Debug, Clone)] +pub struct KeetaBlockBuilder<'a> { + version: BlockVersion, + header: BlockHeader<'a>, + operations: Vec>, + signatures: Vec<&'a [u8]>, +} + +#[cfg(any(feature = "alloc", feature = "std"))] +impl<'a> KeetaBlockBuilder<'a> { + /// Create a new builder with the specified block version and header + pub fn new(version: BlockVersion, header: BlockHeader<'a>) -> Self { + Self { version, header, operations: Vec::new(), signatures: Vec::new() } + } + + /// Set the operations + pub fn operations(mut self, operations: Vec>) -> Self { + self.operations = operations; + self + } + + /// Add a single operation + pub fn operation(mut self, operation: Operation<'a>) -> Self { + self.operations.push(operation); + self + } + + /// Set the signatures + pub fn signatures(mut self, signatures: Vec<&'a [u8]>) -> Self { + self.signatures = signatures; + self + } + + /// Add a single signature + pub fn signature(mut self, signature: &'a [u8]) -> Self { + self.signatures.push(signature); + self + } + + /// Build the KeetaBlock + pub fn build(self) -> KeetaBlock<'a> { + KeetaBlock { + version: self.version, + header: self.header, + operations: self.operations, + signatures: self.signatures, + } + } +} + +// ============================================================================ +// Signer Types +// ============================================================================ + +/// Individual signer in a multisig (can be nested) +#[cfg(any(feature = "alloc", feature = "std"))] +#[derive(Debug, Clone)] +pub enum MultiSigSigner<'a> { + /// Nested multisig signer info + Nested(Box>), + /// Single key signer (public key with type prefix) + Key(&'a [u8]), +} + +/// Multisig signer information for V2 blocks +#[cfg(any(feature = "alloc", feature = "std"))] +#[derive(Debug, Clone)] +pub struct MultiSigSignerInfo<'a> { + /// Public key of the multisig account + pub multisig_pub_key: &'a [u8], + /// Signers (can be nested multisig or single keys) + pub signers: Vec>, +} + +/// Signer field for blocks +/// +/// V1 blocks always use Single. V2 blocks can use any variant. +#[cfg(any(feature = "alloc", feature = "std"))] +#[derive(Debug, Clone)] +pub enum SignerField<'a> { + /// Single signer (public key with type prefix) + Single(&'a [u8]), + /// Multisig signer info (V2 only) + Multisig(MultiSigSignerInfo<'a>), + /// Account is signer (null case, V2 only) + AccountIsSigner, +} + +// ============================================================================ +// Enum Types +// ============================================================================ + +/// Adjust method for supply/balance/permissions operations. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum AdjustMethod { + Add = 0, + Subtract = 1, + Set = 2, +} + +impl<'a> Decode<'a> for AdjustMethod { + fn decode>(reader: &mut R) -> der::Result { + let value: u8 = reader.decode()?; + match value { + 0 => Ok(AdjustMethod::Add), + 1 => Ok(AdjustMethod::Subtract), + 2 => Ok(AdjustMethod::Set), + _ => Err(Tag::Integer.value_error()), + } + } +} + +impl Encode for AdjustMethod { + fn encoded_len(&self) -> der::Result { + (*self as u8).encoded_len() + } + + fn encode(&self, writer: &mut impl Writer) -> der::Result<()> { + (*self as u8).encode(writer) + } +} + +/// Adjust method for relative operations (add/subtract only, no set). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum AdjustMethodRelative { + Add = 0, + Subtract = 1, +} + +impl<'a> Decode<'a> for AdjustMethodRelative { + fn decode>(reader: &mut R) -> der::Result { + let value: u8 = reader.decode()?; + match value { + 0 => Ok(AdjustMethodRelative::Add), + 1 => Ok(AdjustMethodRelative::Subtract), + _ => Err(Tag::Integer.value_error()), + } + } +} + +impl Encode for AdjustMethodRelative { + fn encoded_len(&self) -> der::Result { + (*self as u8).encoded_len() + } + + fn encode(&self, writer: &mut impl Writer) -> der::Result<()> { + (*self as u8).encode(writer) + } +} + +// ============================================================================ +// Supporting Types +// ============================================================================ + +/// Token and rate pair (used in CREATE_IDENTIFIER swap arguments) +#[derive(Debug, Clone, Copy, Sequence)] +pub struct TokenRate<'a> { + /// Token public key + pub token: Bytes<'a>, + /// Rate + pub rate: Int<'a>, +} + +/// Fee rate for CREATE_IDENTIFIER swap arguments +#[derive(Debug, Clone, Copy, Sequence)] +pub struct FeeRate<'a> { + /// Fee token (NULL means use sell token) + pub token: NullOr>, + /// Fee rate + pub rate: Int<'a>, +} + +/// Token and value pair (used in MATCH_SWAP/CANCEL_SWAP operations) +#[derive(Debug, Clone, Copy, Sequence)] +pub struct TokenValue<'a> { + /// Token public key + pub token: Bytes<'a>, + /// Value + pub value: Int<'a>, +} + +/// Fee value for CANCEL_SWAP operation +#[derive(Debug, Clone, Copy, Sequence)] +pub struct FeeValue<'a> { + /// Fee token (NULL means use sell token) + pub token: NullOr>, + /// Fee value + pub value: Int<'a>, +} + +/// Fee value with recipient for MATCH_SWAP operation +#[derive(Debug, Clone, Copy, Sequence)] +pub struct FeeValueWithRecipient<'a> { + /// Fee token (NULL means use sell token) + pub token: NullOr>, + /// Fee value + pub value: Int<'a>, + /// Fee recipient + pub recipient: Bytes<'a>, +} + +/// Permission value (base and external) +#[derive(Debug, Clone, Copy, Sequence)] +pub struct Permission { + /// Base permissions + pub base: u64, + /// External permissions + pub external: u64, +} + +// ============================================================================ +// Operation Structures +// ============================================================================ + +/// Tag `0`: SEND operation - Transfer tokens to another account +#[derive(Debug, Clone, Sequence)] +pub struct SendOp<'a> { + /// Destination account + pub to: Bytes<'a>, + /// Amount to send + pub amount: Int<'a>, + /// Token ID to send + pub token: Bytes<'a>, + /// External reference (optional) + pub external: Option>, +} + +/// Tag `1`: SET_REP operation - Set representative for delegation +#[derive(Debug, Clone, Sequence)] +pub struct SetRepOp<'a> { + /// Representative to delegate to + pub to: Bytes<'a>, +} + +/// Tag `2`: SET_INFO operation - Set account information +#[derive(Debug, Clone, Sequence)] +pub struct SetInfoOp<'a> { + /// Account name + pub name: Str<'a>, + /// Account description + pub description: Str<'a>, + /// Account metadata + pub metadata: Str<'a>, + /// Default permission (optional) + #[asn1(optional = "true")] + pub default_permission: Option, +} + +/// Tag `3`: MODIFY_PERMISSIONS operation - Modify account permissions +#[derive(Debug, Clone, Sequence)] +pub struct ModifyPermissionsOp<'a> { + /// Principal to modify permissions for + pub principal: Bytes<'a>, + /// Method to modify (add/subtract/set) + pub method: AdjustMethod, + /// Permissions to modify (NULL = clear) + pub permissions: NullOr, + /// Target account (optional) + #[asn1(optional = "true")] + pub target: Option>, +} + +/// Multisig creation arguments +/// +/// The `signers` field contains raw DER-encoded bytes representing +/// SEQUENCE OF OCTET STRING. Use `iter_signers()` to iterate over +/// individual signer public keys without allocation. +#[derive(Debug, Clone)] +pub struct MultisigArgs<'a> { + /// Raw DER bytes of the signers SEQUENCE (tag 0x30 + length + content). + /// Contains SEQUENCE OF OCTET STRING where each OCTET STRING is a signer public key. + pub signers: &'a [u8], + /// Required number of signatures + pub quorum: u64, +} + +impl<'a> MultisigArgs<'a> { + /// Returns an iterator over signer public keys. + /// + /// Each item is a Result containing the raw bytes of one signer's + /// public key (the OCTET STRING content, without tag/length). + /// + /// # Example + /// ```ignore + /// for signer_result in multisig_args.iter_signers() { + /// let signer_pubkey = signer_result?; + /// // signer_pubkey is &[u8] containing the public key bytes + /// } + /// ``` + pub fn iter_signers(&self) -> SignersIter<'a> { + SignersIter::new(self.signers) + } + + /// Returns the number of signers. + /// + /// Returns an error if the signers data is malformed. + pub fn signer_count(&self) -> der::Result { + let mut count = 0; + for result in self.iter_signers() { + result?; + count += 1; + } + Ok(count) + } +} + +impl<'a> DecodeValue<'a> for MultisigArgs<'a> { + fn decode_value>(reader: &mut R, _header: Header) -> der::Result { + // Read signers as raw TLV bytes (SEQUENCE OF OCTET STRING) + let signers = read_tlv_bytes(reader)?; + let quorum: u64 = reader.decode()?; + Ok(MultisigArgs { signers, quorum }) + } +} + +impl EncodeValue for MultisigArgs<'_> { + fn value_len(&self) -> der::Result { + Length::try_from(self.signers.len())? + self.quorum.encoded_len()? + } + + fn encode_value(&self, writer: &mut impl Writer) -> der::Result<()> { + writer.write(self.signers)?; + self.quorum.encode(writer) + } +} + +impl<'a> Sequence<'a> for MultisigArgs<'a> {} + +/// Iterator that yields raw signer public key bytes (without DER tag/length). +#[derive(Debug, Clone)] +pub struct SignersIter<'a> { + content: SliceReader<'a>, +} + +impl<'a> SignersIter<'a> { + /// Creates a new iterator from the raw signers SEQUENCE bytes. + fn new(signers_sequence: &'a [u8]) -> Self { + if signers_sequence.is_empty() { + return SignersIter { content: SliceReader::new(&[]).unwrap() }; + } + + // Parse the SEQUENCE header to get the content bytes + let outer_reader = match SliceReader::new(signers_sequence) { + Ok(r) => r, + Err(_) => return SignersIter { content: SliceReader::new(&[]).unwrap() }, + }; + + // Read the SEQUENCE header and get content length + let header = match outer_reader.peek_header() { + Ok(h) => h, + Err(_) => return SignersIter { content: SliceReader::new(&[]).unwrap() }, + }; + + if header.tag != Tag::Sequence { + return SignersIter { content: SliceReader::new(&[]).unwrap() }; + } + + // Skip the header and create a reader for just the content + let header_len = match header.encoded_len() { + Ok(len) => len, + Err(_) => return SignersIter { content: SliceReader::new(&[]).unwrap() }, + }; + + let content_len: usize = header.length.try_into().unwrap_or(0); + let header_bytes: usize = header_len.try_into().unwrap_or(0); + + if signers_sequence.len() < header_bytes + content_len { + return SignersIter { content: SliceReader::new(&[]).unwrap() }; + } + + let content = &signers_sequence[header_bytes..header_bytes + content_len]; + SignersIter { content: SliceReader::new(content).unwrap() } + } +} + +impl<'a> Iterator for SignersIter<'a> { + type Item = der::Result<&'a [u8]>; + + fn next(&mut self) -> Option { + if self.content.is_finished() { + return None; + } + + // Decode the next OCTET STRING + match Bytes::decode(&mut self.content) { + Ok(octet_string) => Some(Ok(octet_string.as_bytes())), + Err(e) => Some(Err(e)), + } + } +} + +/// Swap creation arguments +#[derive(Debug, Clone, Sequence)] +pub struct SwapArgs<'a> { + /// Token being sold and rate + pub sell_token_rate: TokenRate<'a>, + /// Token being bought and rate + pub buy_token_rate: TokenRate<'a>, + /// Fee token and rate (NULL = no fee) + pub fee_token_rate: NullOr>, + /// Quantity + pub quantity: Int<'a>, +} + +/// Identifier creation arguments +#[derive(Debug, Clone, Choice)] +pub enum CreateIdentifierArgs<'a> { + /// Multisig creation arguments (tag `7`) + #[asn1(context_specific = "7", tag_mode = "EXPLICIT", constructed = "true")] + Multisig(MultisigArgs<'a>), + /// Swap creation arguments (tag `8`) + #[asn1(context_specific = "8", tag_mode = "EXPLICIT", constructed = "true")] + Swap(SwapArgs<'a>), +} + +/// Tag `4`: CREATE_IDENTIFIER operation - Create token, multisig, or swap +#[derive(Debug, Clone, Sequence)] +pub struct CreateIdentifierOp<'a> { + /// Identifier to create + pub identifier: Bytes<'a>, + /// Creation arguments (optional, depends on identifier type) + #[asn1(optional = "true")] + pub create_arguments: Option>, +} + +/// Tag `5`: TOKEN_ADMIN_SUPPLY operation - Modify token supply +#[derive(Debug, Clone, Copy, Sequence)] +pub struct TokenAdminSupplyOp<'a> { + /// Amount to modify + pub amount: Int<'a>, + /// Method (add/subtract only, set is not allowed) + pub method: AdjustMethodRelative, +} + +/// Tag `6`: TOKEN_ADMIN_MODIFY_BALANCE operation - Modify account token balance +#[derive(Debug, Clone, Sequence)] +pub struct TokenAdminModifyBalanceOp<'a> { + /// Token to modify balance of + pub token: Bytes<'a>, + /// Amount to modify + pub amount: Int<'a>, + /// Method (add/subtract/set) + pub method: AdjustMethod, +} + +/// Tag `7`: RECEIVE operation - Receive tokens from another account +#[derive(Debug, Clone, Sequence)] +pub struct ReceiveOp<'a> { + /// Amount to receive + pub amount: Int<'a>, + /// Token to receive + pub token: Bytes<'a>, + /// Sender account + pub from: Bytes<'a>, + /// Whether amount must match exactly + pub exact: bool, + /// Forward to another account (optional) + #[asn1(optional = "true")] + pub forward: Option>, +} + +/// Tag `8`: MANAGE_CERTIFICATE operation - Add or subtract certificates. +/// +/// Certificate data is stored as raw DER bytes (can be OCTET STRING or SEQUENCE). +#[derive(Debug, Clone)] +pub struct ManageCertificateOp<'a> { + /// Method (add/subtract). + pub method: AdjustMethodRelative, + /// Certificate DER bytes (if adding) or certificate hash (if removing). + /// Stored with tag+length for roundtrip encoding. + pub certificate_or_hash: &'a [u8], + /// Intermediate certificates DER bytes, NULL, or absent. + pub intermediate_certificates: Option>, +} + +impl<'a> DecodeValue<'a> for ManageCertificateOp<'a> { + fn decode_value>(reader: &mut R, _header: Header) -> der::Result { + let method = reader.decode()?; + + // Read certificate as raw TLV bytes (any tag type) + let certificate_or_hash = read_tlv_bytes(reader)?; + + // Handle optional intermediate_certificates field + let intermediate_certificates = if reader.is_finished() { + None + } else { + let tag = reader.peek_tag()?; + if tag == Tag::Null { + let _: Null = reader.decode()?; + Some(NullOr::Null) + } else { + Some(NullOr::Value(read_tlv_bytes(reader)?)) + } + }; + + Ok(ManageCertificateOp { method, certificate_or_hash, intermediate_certificates }) + } +} + +impl EncodeValue for ManageCertificateOp<'_> { + fn value_len(&self) -> der::Result { + self.method.encoded_len()? + + Length::try_from(self.certificate_or_hash.len())? + + self + .intermediate_certificates + .as_ref() + .map(|v| match v { + NullOr::Null => Null.encoded_len(), + NullOr::Value(bytes) => Length::try_from(bytes.len()), + }) + .transpose()? + .unwrap_or(Length::ZERO) + } + + fn encode_value(&self, writer: &mut impl Writer) -> der::Result<()> { + self.method.encode(writer)?; + writer.write(self.certificate_or_hash)?; + if let Some(ref certs) = self.intermediate_certificates { + match certs { + NullOr::Null => Null.encode(writer)?, + NullOr::Value(bytes) => writer.write(bytes)?, + } + } + Ok(()) + } +} + +impl<'a> Sequence<'a> for ManageCertificateOp<'a> {} + +/// Tag `9`: MATCH_SWAP operation - Match two swap orders +#[derive(Debug, Clone, Sequence)] +pub struct MatchSwapOp<'a> { + /// Swap account being used + pub swap: Bytes<'a>, + /// Other swap account to match against + pub other: Bytes<'a>, + /// Token being sold and value + pub sell: TokenValue<'a>, + /// Token being bought and value + pub buy: TokenValue<'a>, + /// Fee value with recipient (NULL = no fee) + pub fee: NullOr>, +} + +/// Tag `10`: CANCEL_SWAP operation - Cancel a swap order +#[derive(Debug, Clone, Sequence)] +pub struct CancelSwapOp<'a> { + /// Swap account to cancel + pub swap: Bytes<'a>, + /// Sell token and value being returned + pub sell: TokenValue<'a>, + /// Fee value (NULL = no fee) + pub fee: NullOr>, +} + +// ============================================================================ +// Operation Enum +// ============================================================================ + +/// Keeta blockchain operation +#[derive(Debug, Clone, Choice)] +pub enum Operation<'a> { + /// Tag `0`: Send tokens + #[asn1(context_specific = "0", tag_mode = "EXPLICIT", constructed = "true")] + Send(SendOp<'a>), + /// Tag `1`: Set representative + #[asn1(context_specific = "1", tag_mode = "EXPLICIT", constructed = "true")] + SetRep(SetRepOp<'a>), + /// Tag `2`: Set account info + #[asn1(context_specific = "2", tag_mode = "EXPLICIT", constructed = "true")] + SetInfo(SetInfoOp<'a>), + /// Tag `3`: Modify permissions + #[asn1(context_specific = "3", tag_mode = "EXPLICIT", constructed = "true")] + ModifyPermissions(ModifyPermissionsOp<'a>), + /// Tag `4`: Create identifier (token, multisig, swap) + #[asn1(context_specific = "4", tag_mode = "EXPLICIT", constructed = "true")] + CreateIdentifier(CreateIdentifierOp<'a>), + /// Tag `5`: Token admin supply + #[asn1(context_specific = "5", tag_mode = "EXPLICIT", constructed = "true")] + TokenAdminSupply(TokenAdminSupplyOp<'a>), + /// Tag `6`: Token admin modify balance + #[asn1(context_specific = "6", tag_mode = "EXPLICIT", constructed = "true")] + TokenAdminModifyBalance(TokenAdminModifyBalanceOp<'a>), + /// Tag `7`: Receive tokens + #[asn1(context_specific = "7", tag_mode = "EXPLICIT", constructed = "true")] + Receive(ReceiveOp<'a>), + /// Tag `8`: Manage certificate + #[asn1(context_specific = "8", tag_mode = "EXPLICIT", constructed = "true")] + ManageCertificate(ManageCertificateOp<'a>), + /// Tag `9`: Match swap + #[asn1(context_specific = "9", tag_mode = "EXPLICIT", constructed = "true")] + MatchSwap(MatchSwapOp<'a>), + /// Tag `10`: Cancel swap + #[asn1(context_specific = "10", tag_mode = "EXPLICIT", constructed = "true")] + CancelSwap(CancelSwapOp<'a>), +} + +// ============================================================================ +// Vote Types (X.509 Certificate) +// ============================================================================ + +/// Algorithm identifier (used in certificates) +#[cfg(any(feature = "alloc", feature = "std"))] +#[derive(Debug, Clone, Copy)] +pub struct AlgorithmIdentifier<'a> { + /// Algorithm identifier + pub algorithm: &'a [u8], + /// Optional parameters (e.g., curve identifier for ECDSA) + pub parameters: Option<&'a [u8]>, +} + +/// Validity period for certificates +#[cfg(any(feature = "alloc", feature = "std"))] +#[derive(Debug, Clone, Copy)] +pub struct Validity<'a> { + /// Certificate validity start time + pub not_before: &'a str, + /// Certificate validity end time + pub not_after: &'a str, +} + +/// Subject public key info +#[cfg(any(feature = "alloc", feature = "std"))] +#[derive(Debug, Clone, Copy)] +pub struct SubjectPublicKeyInfo<'a> { + /// Algorithm identifier + pub algorithm: AlgorithmIdentifier<'a>, + /// Public key as raw bytes + pub public_key: &'a [u8], +} + +/// Certificate extension wrapper +#[cfg(any(feature = "alloc", feature = "std"))] +#[derive(Debug, Clone, Copy)] +pub struct CertificateExtensionWrapper<'a> { + /// Extension identifier + pub extension_id: &'a [u8], + /// Critical flag + pub critical: bool, + /// Extension data (contains HashData or FeeData) + pub data: &'a [u8], +} + +/// Hash data extension content (OID: 2.16.840.1.101.3.3.1.3) +#[cfg(any(feature = "alloc", feature = "std"))] +#[derive(Debug, Clone)] +pub struct HashData<'a> { + /// Hash algorithm identifier + pub hash_algorithm: &'a [u8], + /// Block hashes + pub hashes: Vec<&'a [u8]>, +} + +/// Fee data extension content (OID: 1.3.6.1.4.1.62675.0.1.0) +#[cfg(any(feature = "alloc", feature = "std"))] +#[derive(Debug, Clone, Copy)] +pub struct FeeData<'a> { + /// Whether this is a quote (`true`) or a vote (`false`) + pub quote: bool, + /// Amount + pub amount: &'a [u8], + /// Pay to account (optional) + pub pay_to: Option<&'a [u8]>, + /// Token account (optional) + pub token: Option<&'a [u8]>, +} + +/// TBS (To Be Signed) Certificate data +#[cfg(any(feature = "alloc", feature = "std"))] +#[derive(Debug, Clone)] +pub struct TbsCertificate<'a> { + /// Version (always 2 for v3 certificates) + pub version: u8, + /// Serial number + pub serial: &'a [u8], + /// Signature algorithm + pub signature_algorithm: AlgorithmIdentifier<'a>, + /// Issuer common name + pub issuer_cn: &'a str, + /// Validity period + pub validity: Validity<'a>, + /// Subject serial number + pub subject_serial: &'a str, + /// Subject public key info + pub subject_public_key_info: SubjectPublicKeyInfo<'a>, + /// Extensions + pub extensions: Vec>, +} + +/// Vote (X.509v3 Certificate for blockchain voting) +#[cfg(any(feature = "alloc", feature = "std"))] +#[derive(Debug, Clone)] +pub struct Vote<'a> { + /// TBS Certificate (data to be signed) + pub tbs_certificate: TbsCertificate<'a>, + /// Signature algorithm + pub signature_algorithm: AlgorithmIdentifier<'a>, + /// Signature as raw bytes + pub signature: &'a [u8], +} + +/// Vote staple - bundles blocks with their votes +#[cfg(any(feature = "alloc", feature = "std"))] +#[derive(Debug, Clone)] +pub struct VoteStaple<'a> { + /// Blocks + pub blocks: Vec>, + /// Votes (X.509 certificates) + pub votes: Vec>, +} + +// ============================================================================ +// Tests +// ============================================================================ + +#[cfg(test)] +mod tests { + use super::*; + use der::{Decode, Encode}; + + // Helper to create test bytes (33-byte address with Ed25519 prefix) + fn test_address() -> [u8; 33] { + let mut addr = [0u8; 33]; + addr[0] = 0x01; // Ed25519 prefix + addr[1] = 0xAB; + addr[32] = 0xCD; + addr + } + + // Helper to create test token (33 bytes) + fn test_token() -> [u8; 33] { + let mut token = [0u8; 33]; + token[0] = 0x01; + token[1] = 0xDE; + token[32] = 0xAD; + token + } + + // ============================================================================ + // NullOr Tests + // ============================================================================ + + #[test] + fn nullor_null_roundtrip() { + let original: NullOr = NullOr::Null; + let encoded = original.to_der().unwrap(); + let decoded: NullOr = NullOr::from_der(&encoded).unwrap(); + assert_eq!(decoded, NullOr::Null); + } + + #[test] + fn nullor_value_roundtrip() { + let data = [1u8, 2, 3, 4]; + let original: NullOr = NullOr::Value(Bytes::new(&data).unwrap()); + let encoded = original.to_der().unwrap(); + let decoded: NullOr = NullOr::from_der(&encoded).unwrap(); + + match decoded { + NullOr::Value(bytes) => assert_eq!(bytes.as_bytes(), &data), + NullOr::Null => panic!("Expected Value, got Null"), + } + } + + #[test] + fn nullor_value_method() { + let data = [1u8, 2, 3]; + let null: NullOr = NullOr::Null; + let value: NullOr = NullOr::Value(Bytes::new(&data).unwrap()); + + assert!(null.value().is_none()); + assert_eq!(value.value().unwrap().as_bytes(), &data); + } + + // ============================================================================ + // Enum Tests + // ============================================================================ + + #[test] + fn adjust_method_roundtrip() { + for method in [AdjustMethod::Add, AdjustMethod::Subtract, AdjustMethod::Set] { + let encoded = method.to_der().unwrap(); + let decoded: AdjustMethod = AdjustMethod::from_der(&encoded).unwrap(); + assert_eq!(decoded, method); + } + } + + #[test] + fn adjust_method_relative_roundtrip() { + for method in [AdjustMethodRelative::Add, AdjustMethodRelative::Subtract] { + let encoded = method.to_der().unwrap(); + let decoded: AdjustMethodRelative = AdjustMethodRelative::from_der(&encoded).unwrap(); + assert_eq!(decoded, method); + } + } + + // ============================================================================ + // Supporting Type Tests + // ============================================================================ + + #[test] + fn permission_roundtrip() { + let original = Permission { base: 0x1234, external: 0x5678 }; + let encoded = original.to_der().unwrap(); + let decoded: Permission = Permission::from_der(&encoded).unwrap(); + assert_eq!(decoded.base, original.base); + assert_eq!(decoded.external, original.external); + } + + #[test] + fn token_rate_roundtrip() { + let token = test_token(); + let rate = [0x64]; // 100 (canonical form) + + let original = TokenRate { token: Bytes::new(&token).unwrap(), rate: Int::new(&rate).unwrap() }; + let encoded = original.to_der().unwrap(); + let decoded: TokenRate = TokenRate::from_der(&encoded).unwrap(); + + assert_eq!(decoded.token.as_bytes(), &token); + assert_eq!(decoded.rate.as_bytes(), &rate); + } + + #[test] + fn fee_rate_with_null_token_roundtrip() { + let rate = [0x0A]; // 10 + + let original = FeeRate { token: NullOr::Null, rate: Int::new(&rate).unwrap() }; + let encoded = original.to_der().unwrap(); + let decoded: FeeRate = FeeRate::from_der(&encoded).unwrap(); + + assert!(matches!(decoded.token, NullOr::Null)); + assert_eq!(decoded.rate.as_bytes(), &rate); + } + + #[test] + fn fee_rate_with_token_roundtrip() { + let token = test_token(); + let rate = [0x0A]; // 10 + + let original = FeeRate { token: NullOr::Value(Bytes::new(&token).unwrap()), rate: Int::new(&rate).unwrap() }; + let encoded = original.to_der().unwrap(); + let decoded: FeeRate = FeeRate::from_der(&encoded).unwrap(); + + match decoded.token { + NullOr::Value(t) => assert_eq!(t.as_bytes(), &token), + NullOr::Null => panic!("Expected token, got Null"), + } + } + + #[test] + fn token_value_roundtrip() { + let token = test_token(); + let value = [0x00, 0xFF]; // 255 + + let original = TokenValue { token: Bytes::new(&token).unwrap(), value: Int::new(&value).unwrap() }; + let encoded = original.to_der().unwrap(); + let decoded: TokenValue = TokenValue::from_der(&encoded).unwrap(); + + assert_eq!(decoded.token.as_bytes(), &token); + assert_eq!(decoded.value.as_bytes(), &value); + } + + #[test] + fn fee_value_roundtrip() { + let value = [0x64]; // 100 + + let original = FeeValue { token: NullOr::Null, value: Int::new(&value).unwrap() }; + let encoded = original.to_der().unwrap(); + let decoded: FeeValue = FeeValue::from_der(&encoded).unwrap(); + + assert!(matches!(decoded.token, NullOr::Null)); + assert_eq!(decoded.value.as_bytes(), &value); + } + + #[test] + fn fee_value_with_recipient_roundtrip() { + let token = test_token(); + let value = [0x64]; // 100 + let recipient = test_address(); + + let original = FeeValueWithRecipient { + token: NullOr::Value(Bytes::new(&token).unwrap()), + value: Int::new(&value).unwrap(), + recipient: Bytes::new(&recipient).unwrap(), + }; + let encoded = original.to_der().unwrap(); + let decoded: FeeValueWithRecipient = FeeValueWithRecipient::from_der(&encoded).unwrap(); + + match decoded.token { + NullOr::Value(t) => assert_eq!(t.as_bytes(), &token), + NullOr::Null => panic!("Expected token, got Null"), + } + assert_eq!(decoded.recipient.as_bytes(), &recipient); + } + + // ============================================================================ + // Operation Tests + // ============================================================================ + + #[test] + fn send_op_roundtrip() { + let to = test_address(); + let amount = [0x10]; // 16 (canonical form - no leading zeros) + let token = test_token(); + + let original = SendOp { + to: Bytes::new(&to).unwrap(), + amount: Int::new(&amount).unwrap(), + token: Bytes::new(&token).unwrap(), + external: None, + }; + let encoded = original.to_der().unwrap(); + let decoded: SendOp = SendOp::from_der(&encoded).unwrap(); + + assert_eq!(decoded.to.as_bytes(), &to); + assert_eq!(decoded.amount.as_bytes(), &amount); + assert_eq!(decoded.token.as_bytes(), &token); + assert!(decoded.external.is_none()); + } + + #[test] + fn send_op_with_external_roundtrip() { + let to = test_address(); + let amount = [0x10]; // 16 + let token = test_token(); + let external = "ref-123"; + + let original = SendOp { + to: Bytes::new(&to).unwrap(), + amount: Int::new(&amount).unwrap(), + token: Bytes::new(&token).unwrap(), + external: Some(Str::new(external).unwrap()), + }; + let encoded = original.to_der().unwrap(); + let decoded: SendOp = SendOp::from_der(&encoded).unwrap(); + + assert_eq!(decoded.external.unwrap().as_str(), external); + } + + #[test] + fn set_rep_op_roundtrip() { + let to = test_address(); + + let original = SetRepOp { to: Bytes::new(&to).unwrap() }; + let encoded = original.to_der().unwrap(); + let decoded: SetRepOp = SetRepOp::from_der(&encoded).unwrap(); + + assert_eq!(decoded.to.as_bytes(), &to); + } + + #[test] + fn set_info_op_roundtrip() { + let original = SetInfoOp { + name: Str::new("Test Account").unwrap(), + description: Str::new("A test account").unwrap(), + metadata: Str::new("{}").unwrap(), + default_permission: None, + }; + let encoded = original.to_der().unwrap(); + let decoded: SetInfoOp = SetInfoOp::from_der(&encoded).unwrap(); + + assert_eq!(decoded.name.as_str(), "Test Account"); + assert_eq!(decoded.description.as_str(), "A test account"); + assert_eq!(decoded.metadata.as_str(), "{}"); + } + + #[test] + fn set_info_op_with_permission_roundtrip() { + let original = SetInfoOp { + name: Str::new("Test").unwrap(), + description: Str::new("Desc").unwrap(), + metadata: Str::new("{}").unwrap(), + default_permission: Some(Permission { base: 0xFF, external: 0xAA }), + }; + let encoded = original.to_der().unwrap(); + let decoded: SetInfoOp = SetInfoOp::from_der(&encoded).unwrap(); + + let perm = decoded.default_permission.unwrap(); + assert_eq!(perm.base, 0xFF); + assert_eq!(perm.external, 0xAA); + } + + #[test] + fn modify_permissions_op_roundtrip() { + let principal = test_address(); + + let original = ModifyPermissionsOp { + principal: Bytes::new(&principal).unwrap(), + method: AdjustMethod::Add, + permissions: NullOr::Value(Permission { base: 0x10, external: 0x20 }), + target: None, + }; + let encoded = original.to_der().unwrap(); + let decoded: ModifyPermissionsOp = ModifyPermissionsOp::from_der(&encoded).unwrap(); + + assert_eq!(decoded.principal.as_bytes(), &principal); + assert_eq!(decoded.method, AdjustMethod::Add); + match decoded.permissions { + NullOr::Value(p) => { + assert_eq!(p.base, 0x10); + assert_eq!(p.external, 0x20); + } + NullOr::Null => panic!("Expected permissions"), + } + } + + #[test] + fn modify_permissions_op_clear_roundtrip() { + let principal = test_address(); + + let original = ModifyPermissionsOp { + principal: Bytes::new(&principal).unwrap(), + method: AdjustMethod::Set, + permissions: NullOr::Null, + target: None, + }; + let encoded = original.to_der().unwrap(); + let decoded: ModifyPermissionsOp = ModifyPermissionsOp::from_der(&encoded).unwrap(); + + assert!(matches!(decoded.permissions, NullOr::Null)); + } + + #[test] + fn token_admin_supply_op_roundtrip() { + let amount = [0x00, 0xFF, 0xFF]; // 65535 + + let original = TokenAdminSupplyOp { amount: Int::new(&amount).unwrap(), method: AdjustMethodRelative::Add }; + let encoded = original.to_der().unwrap(); + let decoded: TokenAdminSupplyOp = TokenAdminSupplyOp::from_der(&encoded).unwrap(); + + assert_eq!(decoded.amount.as_bytes(), &amount); + assert_eq!(decoded.method, AdjustMethodRelative::Add); + } + + #[test] + fn token_admin_modify_balance_op_roundtrip() { + let token = test_token(); + let amount = [0x64]; // 100 + + let original = TokenAdminModifyBalanceOp { + token: Bytes::new(&token).unwrap(), + amount: Int::new(&amount).unwrap(), + method: AdjustMethod::Subtract, + }; + let encoded = original.to_der().unwrap(); + let decoded: TokenAdminModifyBalanceOp = TokenAdminModifyBalanceOp::from_der(&encoded).unwrap(); + + assert_eq!(decoded.token.as_bytes(), &token); + assert_eq!(decoded.method, AdjustMethod::Subtract); + } + + #[test] + fn receive_op_roundtrip() { + let from = test_address(); + let token = test_token(); + let amount = [0x10]; // 16 + + let original = ReceiveOp { + amount: Int::new(&amount).unwrap(), + token: Bytes::new(&token).unwrap(), + from: Bytes::new(&from).unwrap(), + exact: true, + forward: None, + }; + let encoded = original.to_der().unwrap(); + let decoded: ReceiveOp = ReceiveOp::from_der(&encoded).unwrap(); + + assert_eq!(decoded.from.as_bytes(), &from); + assert!(decoded.exact); + assert!(decoded.forward.is_none()); + } + + #[test] + fn receive_op_with_forward_roundtrip() { + let from = test_address(); + let token = test_token(); + let amount = [0x10]; + let mut forward = [0u8; 33]; + forward[0] = 0x02; // Different address + + let original = ReceiveOp { + amount: Int::new(&amount).unwrap(), + token: Bytes::new(&token).unwrap(), + from: Bytes::new(&from).unwrap(), + exact: false, + forward: Some(Bytes::new(&forward).unwrap()), + }; + let encoded = original.to_der().unwrap(); + let decoded: ReceiveOp = ReceiveOp::from_der(&encoded).unwrap(); + + assert!(!decoded.exact); + assert_eq!(decoded.forward.unwrap().as_bytes(), &forward); + } + + #[test] + fn manage_certificate_op_add_roundtrip() { + // Raw DER: SEQUENCE with some content (mock X.509 certificate) + let cert_der = [0x30, 0x03, 0x01, 0x01, 0xFF]; // SEQUENCE { BOOLEAN TRUE } + let intermediate_der = [0x30, 0x03, 0x02, 0x01, 0x00]; // SEQUENCE { INTEGER 0 } + + let original = ManageCertificateOp { + method: AdjustMethodRelative::Add, + certificate_or_hash: &cert_der, + intermediate_certificates: Some(NullOr::Value(&intermediate_der)), + }; + let encoded = original.to_der().unwrap(); + let decoded: ManageCertificateOp = ManageCertificateOp::from_der(&encoded).unwrap(); + + assert_eq!(decoded.method, AdjustMethodRelative::Add); + assert_eq!(decoded.certificate_or_hash, &cert_der); + match decoded.intermediate_certificates { + Some(NullOr::Value(i)) => assert_eq!(i, &intermediate_der), + _ => panic!("Expected intermediate certificates"), + } + } + + #[test] + fn manage_certificate_op_add_no_intermediate_roundtrip() { + let cert_der = [0x30, 0x03, 0x01, 0x01, 0xFF]; + + let original = ManageCertificateOp { + method: AdjustMethodRelative::Add, + certificate_or_hash: &cert_der, + intermediate_certificates: Some(NullOr::Null), + }; + let encoded = original.to_der().unwrap(); + let decoded: ManageCertificateOp = ManageCertificateOp::from_der(&encoded).unwrap(); + + assert!(matches!(decoded.intermediate_certificates, Some(NullOr::Null))); + } + + #[test] + fn manage_certificate_op_subtract_roundtrip() { + // OCTET STRING containing 32-byte hash + let mut hash_der = [0u8; 34]; + hash_der[0] = 0x04; // OCTET STRING tag + hash_der[1] = 0x20; // length 32 + hash_der[2..].fill(0xAB); // hash bytes + + let original = ManageCertificateOp { + method: AdjustMethodRelative::Subtract, + certificate_or_hash: &hash_der, + intermediate_certificates: None, + }; + let encoded = original.to_der().unwrap(); + let decoded: ManageCertificateOp = ManageCertificateOp::from_der(&encoded).unwrap(); + + assert_eq!(decoded.method, AdjustMethodRelative::Subtract); + assert!(decoded.intermediate_certificates.is_none()); + } + + #[test] + fn match_swap_op_roundtrip() { + let swap = test_address(); + let other = test_token(); + let sell_token = test_token(); + let buy_token = test_address(); + + let original = MatchSwapOp { + swap: Bytes::new(&swap).unwrap(), + other: Bytes::new(&other).unwrap(), + sell: TokenValue { token: Bytes::new(&sell_token).unwrap(), value: Int::new(&[0x64]).unwrap() }, + buy: TokenValue { token: Bytes::new(&buy_token).unwrap(), value: Int::new(&[0x32]).unwrap() }, + fee: NullOr::Null, + }; + let encoded = original.to_der().unwrap(); + let decoded: MatchSwapOp = MatchSwapOp::from_der(&encoded).unwrap(); + + assert_eq!(decoded.swap.as_bytes(), &swap); + assert!(matches!(decoded.fee, NullOr::Null)); + } + + #[test] + fn cancel_swap_op_roundtrip() { + let swap = test_address(); + let sell_token = test_token(); + + let original = CancelSwapOp { + swap: Bytes::new(&swap).unwrap(), + sell: TokenValue { token: Bytes::new(&sell_token).unwrap(), value: Int::new(&[0x10]).unwrap() }, + fee: NullOr::Null, + }; + let encoded = original.to_der().unwrap(); + let decoded: CancelSwapOp = CancelSwapOp::from_der(&encoded).unwrap(); + + assert_eq!(decoded.swap.as_bytes(), &swap); + } + + // ============================================================================ + // CreateIdentifier Tests + // ============================================================================ + + #[test] + fn create_identifier_token_roundtrip() { + let identifier = test_token(); + + let original = CreateIdentifierOp { identifier: Bytes::new(&identifier).unwrap(), create_arguments: None }; + let encoded = original.to_der().unwrap(); + let decoded: CreateIdentifierOp = CreateIdentifierOp::from_der(&encoded).unwrap(); + + assert_eq!(decoded.identifier.as_bytes(), &identifier); + assert!(decoded.create_arguments.is_none()); + } + + #[test] + fn create_identifier_multisig_roundtrip() { + let identifier = test_token(); + // SEQUENCE OF OCTET STRING with 2 signers (each 3 bytes) + // SEQUENCE { OCTET STRING (AA BB CC), OCTET STRING (DD EE FF) } + let signers_raw = [ + 0x30, 0x0A, // SEQUENCE, length 10 + 0x04, 0x03, 0xAA, 0xBB, 0xCC, // OCTET STRING, length 3, content + 0x04, 0x03, 0xDD, 0xEE, 0xFF, // OCTET STRING, length 3, content + ]; + + let original = CreateIdentifierOp { + identifier: Bytes::new(&identifier).unwrap(), + create_arguments: Some(CreateIdentifierArgs::Multisig(MultisigArgs { signers: &signers_raw, quorum: 2 })), + }; + let encoded = original.to_der().unwrap(); + let decoded: CreateIdentifierOp = CreateIdentifierOp::from_der(&encoded).unwrap(); + + match decoded.create_arguments { + Some(CreateIdentifierArgs::Multisig(args)) => { + assert_eq!(args.signers, &signers_raw); + assert_eq!(args.quorum, 2); + + // Test the iterator + let signers: Vec<&[u8]> = args.iter_signers().map(|r| r.unwrap()).collect(); + assert_eq!(signers.len(), 2); + assert_eq!(signers[0], &[0xAA, 0xBB, 0xCC]); + assert_eq!(signers[1], &[0xDD, 0xEE, 0xFF]); + + // Test signer_count + assert_eq!(args.signer_count().unwrap(), 2); + } + _ => panic!("Expected Multisig args"), + } + } + + #[test] + fn multisig_args_iter_signers() { + // Test with realistic 33-byte public keys (Ed25519 prefix + 32 bytes) + let signer1 = test_address(); // 33 bytes + let signer2 = test_token(); // 33 bytes + + // Build SEQUENCE OF OCTET STRING manually + // Each signer: 04 21 <33 bytes> = 35 bytes + // Total content: 70 bytes + // SEQUENCE header: 30 46 (0x46 = 70) + let mut signers_raw = vec![ + 0x30, // SEQUENCE tag + 0x46, // length 70 + 0x04, // OCTET STRING tag + 0x21, // length 33 + ]; + signers_raw.extend_from_slice(&signer1); + signers_raw.extend_from_slice(&[0x04, 0x21]); // OCTET STRING tag + length 33 + signers_raw.extend_from_slice(&signer2); + + let args = MultisigArgs { signers: &signers_raw, quorum: 2 }; + + // Test iteration + let collected: Vec<&[u8]> = args.iter_signers().map(|r| r.unwrap()).collect(); + assert_eq!(collected.len(), 2); + assert_eq!(collected[0], &signer1[..]); + assert_eq!(collected[1], &signer2[..]); + + // Test signer_count + assert_eq!(args.signer_count().unwrap(), 2); + } + + #[test] + fn multisig_args_empty_signers() { + // Empty SEQUENCE + let signers_raw = [0x30, 0x00]; // SEQUENCE, length 0 + + let args = MultisigArgs { signers: &signers_raw, quorum: 0 }; + + let collected: Vec<&[u8]> = args.iter_signers().map(|r| r.unwrap()).collect(); + assert_eq!(collected.len(), 0); + assert_eq!(args.signer_count().unwrap(), 0); + } + + #[test] + fn create_identifier_swap_roundtrip() { + let identifier = test_token(); + let sell_token = test_token(); + let buy_token = test_address(); + + let original = CreateIdentifierOp { + identifier: Bytes::new(&identifier).unwrap(), + create_arguments: Some(CreateIdentifierArgs::Swap(SwapArgs { + sell_token_rate: TokenRate { + token: Bytes::new(&sell_token).unwrap(), + rate: Int::new(&[0x64]).unwrap(), + }, + buy_token_rate: TokenRate { token: Bytes::new(&buy_token).unwrap(), rate: Int::new(&[0x32]).unwrap() }, + fee_token_rate: NullOr::Null, + quantity: Int::new(&[0x0A]).unwrap(), + })), + }; + let encoded = original.to_der().unwrap(); + let decoded: CreateIdentifierOp = CreateIdentifierOp::from_der(&encoded).unwrap(); + + match decoded.create_arguments { + Some(CreateIdentifierArgs::Swap(args)) => { + assert!(matches!(args.fee_token_rate, NullOr::Null)); + } + _ => panic!("Expected Swap args"), + } + } +} diff --git a/keetanetwork-block/tests/samples.rs b/keetanetwork-block/tests/samples.rs new file mode 100644 index 0000000..28330c2 --- /dev/null +++ b/keetanetwork-block/tests/samples.rs @@ -0,0 +1,864 @@ +//! Block test samples for SDK compatibility testing. +//! +//! These samples are DER-encoded blocks used to verify roundtrip encoding/decoding. + +use hex_literal::hex; + +// ============================================================================ +// From Explorer (Mainnet Blocks) +// ============================================================================ + +/// SetRep operation +/// Source: https://explorer.keeta.com/block/F16B80A3FD3DC60D390C7370D71C159A77484DE5A9C480D38407CC17EB095FE7 +pub const SET_REP: &[u8] = &hex!( + "3081d2020100020253820500181332303236303131363139343334372e373430" + "5a04220002b6164ed249d17be9e805a342669b3888883b4f2b7e758612804dbf" + "be6653eb3d05000420614a2f8d130983c1811e8e88482f915e0455cdbb0f2c67" + "e8eb5f13f10f2ffcb23028a126302404220003565af39d790ef8c12d48831ec5" + "b3f78aa26b88cb200902d895017d7022e527d504407fb9b96926e341bd4ed778" + "df4e836f9797bd6840451b1308649b90121e665d6e22cec3c0fed918652389cc" + "fc9a0726333e31ad9de91ee94d0cb93dd0ff6510c8" +); + +/// SetInfo operation +/// Source: https://explorer.keeta.com/block/AC1982A17272A36C4346B60587416799953CCB26277CD83443A98E8BE109FBE5 +pub const SET_INFO: &[u8] = &hex!( + "3082013e020100020253820500181332303236303131363231323530392e3039" + "345a042200036e2c89c42b41859f1386f4384cde7ec515ec201c84702f49f491" + "f9130754805a042103b1dd74a8e92843a6b74dbf7afbb0983fa57c5994d921b0" + "b0079703560adb325e04203fe7a9f50684cfdeaece79285c8d1578f76b346eec" + "9cac6f658a9908d2c4ebc53073a260305e0c075445535454574f0c0574657374" + "320c4465794a6b5a574e7062574673637949364f5377695a47566a6157316862" + "46427359574e6c637949364f53776963336c74596d3973496a6f695645565456" + "46525854794a393006020101020100a50f300d02080de0b6b3a7640000020100" + "0440b6d93c88db24a34387c58ec3d219e89124a75aab7e2b260be7ce1d14c7a0" + "e4a458d7d56049726526d666a76dc6f1ab8fd69989ec11d4412db4fcd8d7de27" + "2eb3" +); + +/// CreateIdentifier (token) operation +/// Source: https://explorer.keeta.com/block/3A11F804843FC448E14F86C576824A9C3D1938DB7B68859799904532F7DF5DD6 +pub const CREATE_IDENTIFIER: &[u8] = &hex!( + "3081d1020100020253820500181332303236303131363231323530372e343032" + "5a042200036e2c89c42b41859f1386f4384cde7ec515ec201c84702f49f491f9" + "130754805a05000420231ee41fb259e22daab123593683b9cfa9c8a3e014ec14" + "70cdda90cea002aec23027a4253023042103b1dd74a8e92843a6b74dbf7afbb0" + "983fa57c5994d921b0b0079703560adb325e0440ce1a7b115f10d2ff2f5aa6e1" + "36654b33bf1e8d82d810d4073aa2ced26d0277535df4c8e2ee5625846709783a" + "152d2781942581efdab3dbcff2a68e549d1c5ea2" +); + +/// TokenAdminSupply operation +/// Source: https://explorer.keeta.com/block/BCBCAA4BAD69472FED8A895CED6ED5EFCAB11902EC531961E2FC1B01613C02DA +pub const TOKEN_ADMIN_SUPPLY: &[u8] = &hex!( + "a182022b3082022702025382181332303236303131363232333935352e383133" + "5a02010004210360342de0c8c8a1d38015bdd2a7f262e627ebd60c50b1aa1396" + "615a922b4becf2308191042107f7b2db86355f93f9dc5d27e94a7e74632ed987" + "8581603103253c00788b074d26306c042200039f4cf0896341a71c016a39826a" + "45f7dc1016cea1e1aa1d34295b6a1c3f7b4cb0042200035d779b71c36fa7120f" + "73b4a3f70b5abf03db277ab6d489e2b66357eee3b97e3d042200025bdd5f4ff5" + "195f24b76733b0dbe94b71176affaf88f4b5ac3ec51bcb2716cba0042028e270" + "466961540fd09f1304d9c4b5881a7c2ff0e98c26271d5182570f15b79b3067a5" + "10300e020910d942ab903b064c00020100a05330510421041731817c8c8f3563" + "a821c7ff771b60d2a13154e8f945bba5339b9846f45d88f6020910d942ab903b" + "064c0004210360342de0c8c8a1d38015bdd2a7f262e627ebd60c50b1aa139661" + "5a922b4becf23081c6044069b5f243e359f8c7153445cfc92c964a194e018587" + "c6561631380deb6b84a6c70b0290babfc1a91888bd29d2147f64098774b9a1b3" + "93431b9bbfe53b4b63e982044099300795f999e89b2bc6cd499d925bd5073a2d" + "84b4f85d228729317f1972b9db7b8d21b07c36852a0cd34a33d83cf6fa457731" + "1a26e0cf60c9d10a0046af4380044059008a5072a4f266edfe2cbd2e6626806e" + "4ffe04bd818d2873173c7f9fec50355be972447601515d609611228ce2d87f7c" + "f8276a39ea5ecf218a45568e37e4b2" +); + +/// Send operation +/// Source: https://explorer.keeta.com/block/05248C7FA3585433D18099BDA283F12F675F8C434479C1F6A52BA98F98E6B79F +pub const SEND: &[u8] = &hex!( + "a1820201308201fd02025382181332303236303131363231353135392e313738" + "5a02010004220002925bb66095636ab410ab4e1f4652163c45fa6ad8370881b1" + "447bb0e7fdc3e37b0500042043e4aa9ad6398fd1d42532b39ee2917faee20bf6" + "6b6934c6e12680c87f28c64c30820153a054305204220002afe7f6f95f62cff0" + "bd728265069219ddb3f97647f028ab330bcae77c1448f1c402095dae70d23e7d" + "87d659042103bbd871ca4d787885dfcff2bd9d15897dc71b657b9547f4f59eac" + "52250ceb2f9ba0523050042103bbd871ca4d787885dfcff2bd9d15897dc71b65" + "7b9547f4f59eac52250ceb2f9b020804cfa5d7c4187972042103bbd871ca4d78" + "7885dfcff2bd9d15897dc71b657b9547f4f59eac52250ceb2f9ba05230500422" + "00025f18c47c939ec74bc368aabcfae28ca2581e8b3975125a54d304b5782e52" + "f42b0207426f8d094cc00004210360342de0c8c8a1d38015bdd2a7f262e627eb" + "d60c50b1aa1396615a922b4becf2a0533051042200025f18c47c939ec74bc368" + "aabcfae28ca2581e8b3975125a54d304b5782e52f42b020800b1a2bc2ec50000" + "04210360342de0c8c8a1d38015bdd2a7f262e627ebd60c50b1aa1396615a922b" + "4becf204408649d74f02569672dee5f8b7290773e8b084bf01893361863c2bf7" + "a586f56ec64297f198407a9ca3e1545093d0c5a0a8a98463d17c5832ee1a18f1" + "a704a66685" +); + +/// ManageCertificate (add) operation +/// Source: https://explorer.keeta.com/block/02F1D8D6A4091979142AB1AFC13F1FC95121C17B3EF854F13A0824A220292321 +pub const MANAGE_CERTIFICATE: &[u8] = &hex!( + "30821334020100020253820500181332303236303131363130323633322e3237" + "355a04220002311db074927ac49fa1c3930cda10ec1030f4466efa3b66819778" + "11c286bf46ae050004202260aaf4a832a1732aa1be40f2d2cf638988abd36e15" + "1ad6e1f9686a2ccd872c30821288a88212843082128002010004821101308210" + "fd308210a2a0030201020210418811a2c0fde40f0af0a8bc45c94a41300b0609" + "60864801650304030a3018311630140603550403130d466f6f747072696e7420" + "4b5943301e170d3236303131363130323633305a170d32373031313631303236" + "33305a3050314e304c060355040316456b656574615f6161626463686e716f73" + "6a68767265377568627a6764673263647762616d6875697a7870756f33677167" + "6c7871656f63713237756e6c7573797666376936613036301006072a8648ce3d" + "020106052b8104000a03220002311db074927ac49fa1c3930cda10ec1030f446" + "6efa3b6681977811c286bf46aea3820fb630820fb2300e0603551d0f0101ff04" + "04030200c0301f0603551d23041830168014afce467da1b3582966dddb962f96" + "82564797d740301d0603551d0e04160414f93a08d95023b16f5e1a8ffb8c44e4" + "d8c179273630820f5e060a2b0601040183e953000004820f4e30820f4a308201" + "46060a2b0601040183e953010081820136308201320201003081ad0609608648" + "01650304012e040c0ebab29e3a1927f1312740290481910431b4956c8f962386" + "867217216823e5265640d5b18ce4735cdeed629b8c5343c8e6b46becb99dc36a" + "65722f2196b63c94be40f6a573c2bf764f29ef856138630118c182be51496978" + "e441c64a36b160c6f3e9104def1c9ad03e6781f5c729442f79ae480498c57a50" + "f7437b14d315c68846ab4d07f153556794f392699dcce751fc6061b48ea0af99" + "a41a04b75cedd2d2305f043004970d09c45586c7c2760f79d2c958a9d3da574b" + "20e60e50247a0769d4fe43937b974549d7689a84073b24ead61e06ab06096086" + "4801650304020804206193fa61ebcb465cf0756314161d7754f99261329ac6a2" + "0bc93b0aee0d25ebf9041c40c47440ca4acd4e80b025ca91a05191839955a2d5" + "9cd885bc0214d030820142060b2b0601040183e953010001818201313082012d" + "0201003081ad060960864801650304012e040cbe61f8923c0df64f1410887f04" + "819104ede4d07ae133bb50df0390f5efe420ae8ff5858b634ce1f2ae288bfe42" + "133ae22cea5c3428eb4b15b30e90e4e228a4fe1172a66c04caf4b59cc2b79378" + "be799930cbd5b583a81163d416d5e1a8107a1a4d6324d95cf40d0fb3ceaa43f3" + "632129e8cf8195e27bedd254f9b604bce9d8f591a6e37244268189a03ec5e1fc" + "4dfdc74391ff26971744c1adf030136f2ea814305f04307f3eefdd2d105c987b" + "ca03711feae98051f67650f8ac4a4d40eeb1c6f387dcd65236abf48c080a2459" + "907b9ffcdb6cae06096086480165030402080420e1fded8f77454a468ade5ffb" + "3054d04e639be4d97dafe63c8efe770543319abb0417eace8fcafa00df227965" + "95883866578b79b6f5c1a7418e30820141060b2b0601040183e9530100028182" + "01303082012c0201003081ad060960864801650304012e040cb8b2f3bcb4ee94" + "de7d9d228904819104b0bf33fafe7c3d7a5c310cff648db129b4b1933694616e" + "6c79a611fdd05d81789ac443a17d0da034a0ae8d8460a9fa319e13b939e107d8" + "2c1ff012cdc4217fd8af920d79d41b3685e990884c6695b928d39346f71ecb73" + "bb3b0dd925a78337285702d25b9f067ebd9e5d7e708e19846dbd6d1490244a4c" + "22f97aff425ad14cc8291cd69804cde887fb0f4385a7eb1d93305f04300e5c7f" + "61dfc6bd9ce71353af13d987090a4961e118d4cf9e94c2537b5bb6864319b49c" + "67cbad88dc6b016405ea3bb7fb060960864801650304020804206d863c8d12f3" + "6d1e20263666243e3294e24cc4fbc22533a0d91dc29fd547db0604166359f4ca" + "be62562de8762a034f05a408804812a7fa1630820149060a2b0601040183e953" + "010181820139308201350201003081ad060960864801650304012e040cf1d29e" + "37f13f22e267026aad048191045ee124e44c10e7af8577a59f8e57ca4c81740c" + "47d35e5e3355440d1615430aa658d6e21a6fc43f8f99c3c93dc0382f9ee29508" + "ed14154a68af44a03a16f6ac87f43482c7106905b1a95e72d1229d7b261fadc9" + "c74c6f8f075380a7e646ef946b8bb4bb60a997dc744042891d8463203a2340d9" + "e3a40999853c1d2177406250ff61b5a1b01c6b8d558a3b71611a57b533305f04" + "306a23280c102b33ac5392b908c97c38af7710732e446fc1a0d5613f3639f358" + "53d11d1ddf4db4dcf9b93c9e585b4404a306096086480165030402080420327f" + "7660a4bd119c602ec4ee167c7fda69eb94803c3346b5e2ec511f9b75e3c5041f" + "5a96e94f5c8fff49c46b28f70c16782dd68b4b1d0b2af585b4a4e6e5dfdede30" + "82014e060a2b0601040183e95301038182013e3082013a0201003081ad060960" + "864801650304012e040c557535200b4d2f488721a40b0481910481453fcdbba2" + "46bf7d1f49d3f8874fd0978711b158613bebf269b6595842af240004182b3ed2" + "8b57b032af4f4151ab9db7e6cd34c0801f35a2221fbb5aff32c341b1699b6194" + "bb91874932c791b20160d608d150a53581d55fcc6896b1167f8f0e6cecdedf06" + "e689134446b856b10b7890b7b5482befc164d1ef69b2dbe9d708cc85c248f678" + "14789ca41586f84e819e305f04306d82713e91cd14a693f2fc556dcba0c7cb12" + "7e619701d422ce6895859a84968b83406e5cbda4a685b496cb54f36b82da0609" + "60864801650304020804201a91e4c748d5d7a0c46619bbe9f17fa2f46be57703" + "9a3bde5db4ce8f896063fe04247bd135edf8a413b6bfb7a3f813267380dfb260" + "2a2c7bbf89c4ba13d5b7ecf0827c18b16e308201b9060a2b0601040183e95301" + "02818201a9308201a50201003081ad060960864801650304012e040c33185b23" + "ccc5e222ad980f27048191044361899b6d12b9913b11d6bd4583a1595a1e21dc" + "c83dac8db1161e8c9555f2433868698feea20d21774c4e93bb39b7729f800a31" + "27f4d40c9f2e629d405641c4f45cd4f2d582e5a128a1f3f7a4042c43a14a1dc5" + "faefce43d02cca76968d972d5ecec6d78c4055f40697eb63c93a7350f2a19670" + "7f1cb6449fc5ed687155bcc5afa6914fbd88280381152e1d90157206305f0430" + "d4278aee91bf54f73675f063d0eef1d7be6d8d8cee3c9625c013bc54c15ba4d9" + "6e4cb921fadd62f6f2fefb2bb8525e7d06096086480165030402080420c88eaf" + "5b6eea26a73d450f67e06889aaf42a99ca8653b4cf360103e8b696e8b304818e" + "42b2dcd4527433534342fa6c3237405b98331d6117b147d974e0f213687b1450" + "6faa3a13f7f55b160e499c33babb74c58f5232a426560ebc695cca41eb7763f6" + "05762162c47de2de801cadabc1f2e8baca9736fe6e6f146bd912094510080ec5" + "00ecee3c034a1f71b21f52d6849e7f409a1d0012d1e6a2512ef5ad51e3d423d9" + "9dc544cf310dd445513f0558013330820325060b2b0601040183e953010b0281" + "820314308203100201003081ad060960864801650304012e040ceeed6982dacc" + "98aa78c8066d048191047f10851a53793079cd400ca20593f707c06298fc5cd5" + "3916b3eceb331969566c4870c73c08ea578cdce63874c7844ea273e57ee59d54" + "26773151bd15beec5ba81c8649c6943863cf09a1f17101454a788c8da1d5f872" + "4255f372578beaa9950a6941fb609d1c53764ad6be9cc10d56cfac469820c848" + "c600501364f2f8f91a18e3b05e6b3648b48654fbe769df272bfd305f0430cb40" + "2beb5bc1e03cfdf912da8906695db05d1f483c5f42ccfcc644306a55760d9ddd" + "7d4be3d87be44ad9b757e76f90f00609608648016503040208042022d385729e" + "28131f4f823d0886ee485101f56d2ebb4a86f4ba7546f15558ace6048201f811" + "0abfadfbc1b9b9638793aa79d1b49f21d12c5bdca6a9013e678dfe4b4ce4b1b1" + "0648796c8a6c239f2b0c89ba526fd7f8eaf43fd8f7329b5035ef7edb9931c3fb" + "48c7a32da89fa1c0988e8761d98337ed8d05f0792374691a2e5e7b5ec495a0da" + "16e03c3a2b71940dce07158846efff4f46cbe1eb68726bcda95ffb17c265a6b4" + "7607dfbe8fb5c04c3c834aec245479d45c8fdae4afd77de7a997be20fb38867e" + "df14b60644cdf37265a10cf16601fd04ca4c73a761fdb47a2f8c52862fdbc7bc" + "65d95dfe4231a5a1035ec482fe42eb1ed8b1a330ea81bb5ba6b56d94348543fb" + "a447a20d09271ddbec90998bc987afe6c9459f6d349a463286cf54976f48c387" + "1f95815a7531251c29a0dd26d0c21e3e8f86da6dddad4befff267326f21baa88" + "32ccc3c25073e6a6f0d4d859722f5295acb4eff59a7cbed91cc3030287e6fdb6" + "5d15589634364dd37bcd79308688d5f79dbbec61a3bb90b7a8c344662b853bc3" + "1bfe62d7a48aed9571e3eb1ff437fb9369be534b70858a69ad2a38455d185a77" + "9f778ebe819f7308964745953cc73234f704e6ca12da10c3b33ca512744e363f" + "352dde9d13fe5d760dea82137ffcd884dde50abc85f8cb10ea0af261b0deebf6" + "e11e58fbe9c78709ff648664fb3bed72e9fd0b60484f32b786cb3210e606df7a" + "48c2fef451334dd190d881c3324c14e1f5bba8308f3e763082013e060a2b0601" + "040183e953010a8182012e3082012a0201003081ad060960864801650304012e" + "040c6bfaab6c7cf3ec5ac42782830481910433bfe1a0be8dd0616956f6937aea" + "bad5a06bf4debeb0e01060f35a009fb2b1498e9099c3586a84281248afe031f9" + "0819d7579d9b54cbf71bf90e1efde17c1eba6874a191fcfe1873bf4518226141" + "4bdb31f6966a56c79579fd9ef37d0649077ce8749e9880e7dc586378fee73221" + "993281576aa9db74e5b2152b5073ed0a6e6f18fe61b7e1f0ea1eac5268fb90db" + "ac08305f04308388b437df389d7fa866bbf1871b65f79f8be399d8b500f69ddf" + "eb513963a26ccc6adcfb6ecc6bd4dfbc71884581d14006096086480165030402" + "0804202fdb75a19811a24c6b9ffbfd6fb8e601f67d5d517c7e601fcc3817f126" + "20b90604142ecc0fb9ade666b30886e09f51c30897c2fa389c3082015d060a2b" + "0601040183e95301088182014d308201490201003081ad060960864801650304" + "012e040c5eadc5fbdf33c0aabcf2f6a60481910463c6d537bbacfcd2d031b040" + "3d91421f2ba627d0cb86b66b51b50a35cd66ff571e53a58d9519bedee5a1bfa7" + "66ddb156dac80c96494a843c30f9563591361d66fa22c02a59a9d26b40cccd05" + "238113238a9828fc67ccd11c56812d146e1521f3f7798610dd354945c0026630" + "15b2e8958dfedb1e29ff168fdb005380f9e827cc3bf54a7ca1a9f27bccdb56d3" + "39c75371305f04306070d9f4ebd84d97b02d6e01e79401c04412740901621d3c" + "43bc0a3c6723f30299f7e2bcfab1b57c04cf1bcccf07ad180609608648016503" + "0402080420e0e84faf993a28b1c2f9d4f08d47b3c825b5e19e8b20e073d9ea3d" + "d17fbb9eaf0433c4f359b86892db7a01d00691181fc1c4ee40d431e19b3528ed" + "3e546312b1823eca6f19bf436a1e71aa173fb4142d5da114c3bc30820149060a" + "2b0601040183e953010481820139308201350201003081ad0609608648016503" + "04012e040caa5cbc5d3df2da044d25f0f804819104be20c5346be430c435455e" + "61e6807bdef2c1c9b0801326deea602f40e7ffacfc2df98483eddd1510fc0bba" + "996bce73c07f747b4c5696955a75a38c04a0930254ca097b9f05e31e041e12c7" + "25ece9a4e7c712fe033690269cdae8d62e14b831cc551bbfdbb078d12c7ebf9f" + "bb022b4cf60c7d9549a53d6f38687a7d40dde422e3fd18e538dfaab79218e318" + "9388546845305f0430d7487f86a8c83f15d36c5c7581db9aed1687e34876e0ce" + "a681b41180f3732581b6aed67589c523044ff2117e9a41c60e06096086480165" + "030402080420ea02e7154352b7518d9704c8e5f3f611a9c983a209a3e350f9fe" + "d09e60af9b41041fec53d59d416bcb6933fa87104458ada2d075fcf92994bd3c" + "a6fe1c746a0823300b060960864801650304030a0348003045022100a91ef865" + "2999023aa5c13364e2adccaa242705f1aae1fa4c9b80b78df33dec6502200804" + "6996f5d0a5d4ef6f66635dc758048789032fe5b15728e54e509df2b7ba053082" + "0174048201703082016c30820112a003020102020102300b0609608648016503" + "04030a302431223020060355040313194b65657461204e6574776f726b204b59" + "4320526f6f74204341301e170d3235313032383033313134305a170d32373130" + "32383033313134305a3018311630140603550403130d466f6f747072696e7420" + "4b59433036301006072a8648ce3d020106052b8104000a032200024227c41bd2" + "deb2a2e1cab9b3013f29c2930e71bbb954a57ab667091bb991e9dca363306130" + "0f0603551d130101ff040530030101ff300e0603551d0f0101ff0404030200c6" + "301f0603551d230418301680143b0a9628b3b6408b6fc6b4e30c5314089f068d" + "fa301d0603551d0e04160414afce467da1b3582966dddb962f9682564797d740" + "300b060960864801650304030a03470030440220406b546206a914c030a959b6" + "65a3815d6268c88f9a164c5f194ea41c9064aebe02206271ee9a237fffbaf9de" + "3e7676232250f35618f43e113a5a5cdc1fb9547d28c30440f158e88895ba97d8" + "2c10a3108294c1d5480dad8bb8a2962b8e447e75d92acc3062c64e1ad86642b9" + "387f086ed938156a782f72cb369036ad74ca787da36f8aaa" +); + +// ============================================================================ +// Manually Constructed (Basic Operations) +// ============================================================================ + +/// CreateIdentifier with Multisig arguments - 3 signers, quorum of 2 +pub const CREATE_IDENTIFIER_MULTISIG: &[u8] = &hex!( + "3082014a020100020253820500181332303236303131383132303030302e3030" + "305a04220002eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeeeee0500042000000000000000000000000000000000000000000000" + "0000000000000000000030819fa4819c30819904220007dddddddddddddddddd" + "dddddddddddddddddddddddddddddddddddddddddddddda7733071306c042200" + "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aa04220002bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + "bbbbbbbbbb04220002cccccccccccccccccccccccccccccccccccccccccccccc" + "cccccccccccccccccc02010204405a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a" +); + +/// CreateIdentifier with SwapArgs - fee_token_rate=Null +pub const CREATE_SWAP: &[u8] = &hex!( + "3082012f020100020253820500181332303236303131383132303030302e3030" + "305a04220002eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeeeee0500042000000000000000000000000000000000000000000000" + "00000000000000000000308184a48181307f04220002aaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa8593057302704220002" + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + "020164302704220002cccccccccccccccccccccccccccccccccccccccccccccc" + "cccccccccccccccccc020132050002010a04405a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" +); + +/// Receive operation - basic (exact=true, no forward) +pub const RECEIVE: &[u8] = &hex!( + "3081fc020100020253820500181332303236303131383132303030302e303030" + "5a04220002eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeee050004200000000000000000000000000000000000000000000000" + "0000000000000000003052a750304e02016404220002cccccccccccccccccccc" + "cccccccccccccccccccccccccccccccccccccccccccc04220002dddddddddddd" + "dddddddddddddddddddddddddddddddddddddddddddddddddddd0101ff04405a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" +); + +/// ModifyPermissions operation - method=Add, permissions={base:16, external:32} +pub const MODIFY_PERMISSIONS: &[u8] = &hex!( + "3081dd020100020253820500181332303236303131383132303030302e303030" + "5a04220002eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeee050004200000000000000000000000000000000000000000000000" + "0000000000000000003033a331302f04220002aaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa02010030060201100201200440" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" +); + +/// TokenAdminModifyBalance operation - method=Add, amount=1000 +pub const TOKEN_ADMIN_MODIFY_BALANCE: &[u8] = &hex!( + "3081d9020100020253820500181332303236303131383132303030302e303030" + "5a04220002eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeee050004200000000000000000000000000000000000000000000000" + "000000000000000000302fa62d302b04220002bbbbbbbbbbbbbbbbbbbbbbbbbb" + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb020203e802010004405a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" +); + +/// MatchSwap operation - fee=NULL +pub const MATCH_SWAP: &[u8] = &hex!( + "3082014d020100020253820500181332303236303131383132303030302e3030" + "305a04220002eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeeeee0500042000000000000000000000000000000000000000000000" + "000000000000000000003081a2a9819f30819c04220002aaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa04220002bbbbbbbbbb" + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb3027042200" + "02cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + "cc020164302704220002dddddddddddddddddddddddddddddddddddddddddddd" + "dddddddddddddddddddd020132050004405a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" +); + +/// CancelSwap operation - fee=NULL +pub const CANCEL_SWAP: &[u8] = &hex!( + "3081fd020100020253820500181332303236303131383132303030302e303030" + "5a04220002eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeee050004200000000000000000000000000000000000000000000000" + "0000000000000000003053aa51304f04220002aaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa302704220002cccccccccccccc" + "cccccccccccccccccccccccccccccccccccccccccccccccccc02016405000440" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" +); + +// ============================================================================ +// Edge Cases (Optional Fields) +// ============================================================================ + +/// Send operation with external field +pub const SEND_WITH_EXTERNAL: &[u8] = &hex!( + "30820104020100020253820500181332303236303131383132303030302e3030" + "305a04220002eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeeeee0500042000000000000000000000000000000000000000000000" + "00000000000000000000305aa058305604220002aaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa02016404220002bbbbbbbbbb" + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb0c09726566" + "2d313233343504405a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a" +); + +/// SetInfo operation with default_permission +pub const SET_INFO_WITH_PERMISSION: &[u8] = &hex!( + "3081da020100020253820500181332303236303131383132303030302e303030" + "5a04220002eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeee050004200000000000000000000000000000000000000000000000" + "0000000000000000003030a22e302c0c0c54657374204163636f756e740c0e41" + "2074657374206163636f756e740c027b7d3008020200ff020200aa04405a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" +); + +/// Receive operation with forward field +pub const RECEIVE_WITH_FORWARD: &[u8] = &hex!( + "30820120020100020253820500181332303236303131383132303030302e3030" + "305a04220002eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeeeee0500042000000000000000000000000000000000000000000000" + "000000000000000000003076a774307202016404220002cccccccccccccccccc" + "cccccccccccccccccccccccccccccccccccccccccccccc04220002dddddddddd" + "dddddddddddddddddddddddddddddddddddddddddddddddddddddd0101ff0422" + "0002ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + "ffff04405a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a" +); + +/// ModifyPermissions with permissions=Null (clear) +pub const MODIFY_PERMISSIONS_CLEAR: &[u8] = &hex!( + "3081d7020100020253820500181332303236303131383132303030302e303030" + "5a04220002eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeee050004200000000000000000000000000000000000000000000000" + "000000000000000000302da32b302904220002aaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa020102050004405a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" +); + +/// ModifyPermissions with target field +pub const MODIFY_PERMISSIONS_WITH_TARGET: &[u8] = &hex!( + "30820101020100020253820500181332303236303131383132303030302e3030" + "305a04220002eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeeeee0500042000000000000000000000000000000000000000000000" + "000000000000000000003057a355305304220002aaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa020100300602011002012004" + "220002cccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + "cccccc04405a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a" +); + +/// MatchSwap with fee (FeeValueWithRecipient) +pub const MATCH_SWAP_WITH_FEE: &[u8] = &hex!( + "30820198020100020253820500181332303236303131383132303030302e3030" + "305a04220002eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeeeee0500042000000000000000000000000000000000000000000000" + "000000000000000000003081eda981ea3081e704220002aaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa04220002bbbbbbbbbb" + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb3027042200" + "02cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + "cc020164302704220002dddddddddddddddddddddddddddddddddddddddddddd" + "dddddddddddddddddddd020132304b04220002eeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee02010a04220002ffffffffffff" + "ffffffffffffffffffffffffffffffffffffffffffffffffffff04405a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" +); + +/// CancelSwap with fee (FeeValue) +pub const CANCEL_SWAP_WITH_FEE: &[u8] = &hex!( + "30820124020100020253820500181332303236303131383132303030302e3030" + "305a04220002eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeeeee0500042000000000000000000000000000000000000000000000" + "00000000000000000000307aaa78307604220002aaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa302704220002cccccccccccc" + "cccccccccccccccccccccccccccccccccccccccccccccccccccc020164302704" + "220002eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeee02010504405a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a" +); + +/// MatchSwap with fee.token=Null (use sell token as fee token) +pub const MATCH_SWAP_FEE_NULL_TOKEN: &[u8] = &hex!( + "30820176020100020253820500181332303236303131383132303030302e3030" + "305a04220002eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeeeee0500042000000000000000000000000000000000000000000000" + "000000000000000000003081cba981c83081c504220002aaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa04220002bbbbbbbbbb" + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb3027042200" + "02cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + "cc020164302704220002dddddddddddddddddddddddddddddddddddddddddddd" + "dddddddddddddddddddd0201323029050002010a04220002ffffffffffffffff" + "ffffffffffffffffffffffffffffffffffffffffffffffff04405a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" +); + +/// CancelSwap with fee.token=Null (use sell token as fee token) +pub const CANCEL_SWAP_FEE_NULL_TOKEN: &[u8] = &hex!( + "30820102020100020253820500181332303236303131383132303030302e3030" + "305a04220002eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeeeee0500042000000000000000000000000000000000000000000000" + "000000000000000000003058aa56305404220002aaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa302704220002cccccccccccc" + "cccccccccccccccccccccccccccccccccccccccccccccccccccc020164300505" + "0002010504405a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a" +); + +/// TokenAdminSupply with method=Subtract +pub const TOKEN_ADMIN_SUPPLY_SUBTRACT: &[u8] = &hex!( + "3081b5020100020253820500181332303236303131383132303030302e303030" + "5a04220002eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeee050004200000000000000000000000000000000000000000000000" + "000000000000000000300ba5093007020201f402010104405a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" +); + +/// Receive with exact=false (no forward) +pub const RECEIVE_EXACT_FALSE: &[u8] = &hex!( + "3081fc020100020253820500181332303236303131383132303030302e303030" + "5a04220002eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeee050004200000000000000000000000000000000000000000000000" + "0000000000000000003052a750304e02016404220002cccccccccccccccccccc" + "cccccccccccccccccccccccccccccccccccccccccccc04220002dddddddddddd" + "dddddddddddddddddddddddddddddddddddddddddddddddddddd01010004405a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" +); + +/// ManageCertificate with method=Subtract (remove by hash) +pub const MANAGE_CERTIFICATE_SUBTRACT: &[u8] = &hex!( + "3081d3020100020253820500181332303236303131383132303030302e303030" + "5a04220002eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeee050004200000000000000000000000000000000000000000000000" + "0000000000000000003029a82730250201010420abababababababababababab" + "abababababababababababababababababababab04405a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" +); + +/// ManageCertificate with intermediate_certificates=Some(Null) +pub const MANAGE_CERTIFICATE_NULL_INTERMEDIATE: &[u8] = &hex!( + "3081bb020100020253820500181332303236303131383132303030302e303030" + "5a04220002eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeee050004200000000000000000000000000000000000000000000000" + "0000000000000000003011a80f300d0201003006020101020102050004405a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" +); + +// ============================================================================ +// Block-Level Edge Cases +// ============================================================================ + +/// Block with multiple operations (SET_REP + SEND) +pub const BLOCK_MULTI_OPS: &[u8] = &hex!( + "30820121020100020253820500181332303236303131383132303030302e3030" + "305a04220002eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeeeee0500042000000000000000000000000000000000000000000000" + "000000000000000000003077a126302404220002aaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa04d304b04220002bbbbbbbb" + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb02016404" + "220002cccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + "cccccc04405a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a" +); + +/// Block with explicit signer key instead of AccountIsSigner (NULL) +pub const BLOCK_WITH_SIGNER_KEY: &[u8] = &hex!( + "3081f4020100020253820500181332303236303131383132303030302e303030" + "5a04220002eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeee04220002ffffffffffffffffffffffffffffffffffffffffffffff" + "ffffffffffffffffff0420000000000000000000000000000000000000000000" + "00000000000000000000003028a126302404220002aaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa04405a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" +); + +/// V1 block with explicit subnet value (subnet=5 instead of NULL) +/// Tests V1 header parsing with non-null subnet field +pub const BLOCK_V1_WITH_SUBNET: &[u8] = &hex!( + "3081d302010002025382020105181332303236303131383132303030302e3030" + "305a04220002eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeeeee0500042000000000000000000000000000000000000000000000" + "000000000000000000003028a126302404220002aaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa04405a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" +); + +// ============================================================================ +// Additional Method Variants +// ============================================================================ + +/// ModifyPermissions with method=Subtract +pub const MODIFY_PERMISSIONS_SUBTRACT: &[u8] = &hex!( + "3081dd020100020253820500181332303236303131383132303030302e303030" + "5a04220002eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeee050004200000000000000000000000000000000000000000000000" + "0000000000000000003033a331302f04220002aaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa02010130060201100201200440" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" +); + +/// ModifyPermissions with method=Set and permissions=Value (not NULL) +/// This tests the combination of Set method with actual permission values +pub const MODIFY_PERMISSIONS_SET: &[u8] = &hex!( + "3081dd020100020253820500181332303236303131383132303030302e303030" + "5a04220002eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeee050004200000000000000000000000000000000000000000000000" + "0000000000000000003033a331302f04220002aaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa02010230060201100201200440" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" +); + +/// TokenAdminModifyBalance with method=Subtract +pub const TOKEN_ADMIN_MODIFY_BALANCE_SUBTRACT: &[u8] = &hex!( + "3081d9020100020253820500181332303236303131383132303030302e303030" + "5a04220002eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeee050004200000000000000000000000000000000000000000000000" + "000000000000000000302fa62d302b04220002bbbbbbbbbbbbbbbbbbbbbbbbbb" + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb020203e802010104405a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" +); + +/// TokenAdminModifyBalance with method=Set +pub const TOKEN_ADMIN_MODIFY_BALANCE_SET: &[u8] = &hex!( + "3081d9020100020253820500181332303236303131383132303030302e303030" + "5a04220002eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeee050004200000000000000000000000000000000000000000000000" + "000000000000000000302fa62d302b04220002bbbbbbbbbbbbbbbbbbbbbbbbbb" + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb020203e802010204405a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" +); + +// ============================================================================ +// Permission Value Edge Cases +// ============================================================================ + +/// ModifyPermissions with zero permissions (base=0, external=0) +/// Tests minimum valid permission values +pub const MODIFY_PERMISSIONS_ZERO: &[u8] = &hex!( + "3081dd020100020253820500181332303236303131383132303030302e303030" + "5a04220002eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeee050004200000000000000000000000000000000000000000000000" + "0000000000000000003033a331302f04220002aaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa02010030060201000201000440" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" +); + +/// ModifyPermissions at DER integer encoding boundary (base=127, external=128) +/// 127 = 0x7F (single byte), 128 = 0x80 (requires leading zero in DER) +pub const MODIFY_PERMISSIONS_BOUNDARY: &[u8] = &hex!( + "3081de020100020253820500181332303236303131383132303030302e303030" + "5a04220002eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeee050004200000000000000000000000000000000000000000000000" + "0000000000000000003034a332303004220002aaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa020100300702017f0202008004" + "405a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a" +); + +/// ModifyPermissions with large multi-byte values (base=256, external=65535) +/// Tests DER encoding of larger integers requiring multiple bytes +pub const MODIFY_PERMISSIONS_LARGE: &[u8] = &hex!( + "3081e0020100020253820500181332303236303131383132303030302e303030" + "5a04220002eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeee050004200000000000000000000000000000000000000000000000" + "0000000000000000003036a334303204220002aaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa020100300902020100020300ff" + "ff04405a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a" +); + +// ============================================================================ +// CreateIdentifier SwapArgs Fee Variants +// ============================================================================ + +/// CreateIdentifier with SwapArgs - fee_token_rate=Value(FeeRate{token:Value, rate:5}) +pub const CREATE_SWAP_WITH_FEE: &[u8] = &hex!( + "30820158020100020253820500181332303236303131383132303030302e3030" + "305a04220002eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeeeee0500042000000000000000000000000000000000000000000000" + "000000000000000000003081ada481aa3081a704220002aaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa88180307e30270422" + "0002bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + "bbbb020164302704220002cccccccccccccccccccccccccccccccccccccccccc" + "cccccccccccccccccccccc020132302704220002dddddddddddddddddddddddd" + "dddddddddddddddddddddddddddddddddddddddd02010502010a04405a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" +); + +/// CreateIdentifier with SwapArgs - fee_token_rate=Value(FeeRate{token:Null, rate:5}) +pub const CREATE_SWAP_FEE_NULL_TOKEN: &[u8] = &hex!( + "30820135020100020253820500181332303236303131383132303030302e3030" + "305a04220002eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeeeee0500042000000000000000000000000000000000000000000000" + "0000000000000000000030818aa4818730818404220002aaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa85e305c3027042200" + "02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + "bb020164302704220002cccccccccccccccccccccccccccccccccccccccccccc" + "cccccccccccccccccccc0201323005050002010502010a04405a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" +); + +// ============================================================================ +// V2 Block Samples +// ============================================================================ + +/// V2 block with purpose=Generic, single signer +pub const BLOCK_V2_BASIC: &[u8] = &hex!( + "a181f53081f202025382181332303236303131383132303030302e3030305a02" + "010004220002eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeeeee04220002ffffffffffffffffffffffffffffffffffffffffffff" + "ffffffffffffffffffff04200000000000000000000000000000000000000000" + "0000000000000000000000003028a126302404220002aaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa04405a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" +); + +/// V2 block with purpose=Fee +pub const BLOCK_V2_PURPOSE_FEE: &[u8] = &hex!( + "a181f53081f202025382181332303236303131383132303030302e3030305a02" + "010104220002eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeeeee04220002ffffffffffffffffffffffffffffffffffffffffffff" + "ffffffffffffffffffff04200000000000000000000000000000000000000000" + "0000000000000000000000003028a126302404220002aaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa04405a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" +); + +/// V2 block with signer=NULL (AccountIsSigner) +pub const BLOCK_V2_ACCOUNT_IS_SIGNER: &[u8] = &hex!( + "a181d33081d002025382181332303236303131383132303030302e3030305a02" + "010004220002eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeeeee0500042000000000000000000000000000000000000000000000" + "000000000000000000003028a126302404220002aaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa04405a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" +); + +/// V2 block with multisig signer (2 signers, 2 signatures) +pub const BLOCK_V2_MULTISIG_SIGNER: &[u8] = &hex!( + "a18201873082018302025382181332303236303131383132303030302e303030" + "5a02010004220002eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeeeeeeeee306e04220002dddddddddddddddddddddddddddddddddddd" + "dddddddddddddddddddddddddddd304804220002111111111111111111111111" + "1111111111111111111111111111111111111111042200022222222222222222" + "2222222222222222222222222222222222222222222222220420000000000000" + "00000000000000000000000000000000000000000000000000003028a1263024" + "04220002aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaa30818404405a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a04406b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b" + "6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b" + "6b6b6b6b6b6b6b6b6b6b6b" +); + +/// V2 block with multiple signatures (single signer, 2 signatures) +pub const BLOCK_V2_MULTI_SIGS: &[u8] = &hex!( + "a182013b3082013702025382181332303236303131383132303030302e303030" + "5a02010004220002eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeeeeeeeee04220002ffffffffffffffffffffffffffffffffffffffff" + "ffffffffffffffffffffffff0420000000000000000000000000000000000000" + "00000000000000000000000000003028a126302404220002aaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa30818404405a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a04406b" + "6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b" + "6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b" +); + +/// V2 block with nested multisig signers (outer + inner multisig, 3 signatures) +pub const BLOCK_V2_NESTED_MULTISIG: &[u8] = &hex!( + "a18202173082021302025382181332303236303131383132303030302e303030" + "5a02010004220002eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeeeeeeeee3081bb04220002cccccccccccccccccccccccccccccccccc" + "cccccccccccccccccccccccccccccc308194306e04220002dddddddddddddddd" + "dddddddddddddddddddddddddddddddddddddddddddddddd3048042200021111" + "1111111111111111111111111111111111111111111111111111111111110422" + "0002222222222222222222222222222222222222222222222222222222222222" + "2222042200023333333333333333333333333333333333333333333333333333" + "3333333333330420000000000000000000000000000000000000000000000000" + "00000000000000003028a126302404220002aaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa3081c604405a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a04406b6b6b6b6b6b6b" + "6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b" + "6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b04407c7c7c7c7c" + "7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c" + "7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c" +); + +/// V2 block with 3-level deep nested multisig (L1 -> L2 -> L3 -> leaf signers) +/// Structure: L1(key=AA, signers=[L2, 44]), L2(key=BB, signers=[L3, 33]), L3(key=CC, signers=[11, 22]) +/// This tests deeply nested signer structures with 4 leaf signers requiring 3 signatures +/// (one path through the multisig tree: L1->L2->L3->leaf1, then L3->leaf2, then L2->leaf3). +pub const BLOCK_V2_DEEP_NESTED_MULTISIG: &[u8] = &hex!( + "a1820266308202620202538218133230323630313138313230303030" + "2e3030305a02010004220002eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee3082010904220002aaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "3081e23081bb04220002bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + "bbbbbbbbbbbbbbbbbbbbbbbbbbbb308194306e04220002cccccccccc" + "cccccccccccccccccccccccccccccccccccccccccccccccccccccc30" + "48042200021111111111111111111111111111111111111111111111" + "11111111111111111104220002222222222222222222222222222222" + "22222222222222222222222222222222220422000233333333333333" + "33333333333333333333333333333333333333333333333333042200" + "02444444444444444444444444444444444444444444444444444444" + "44444444440420000000000000000000000000000000000000000000" + "00000000000000000000003028a126302404220002dddddddddddddd" + "dddddddddddddddddddddddddddddddddddddddddddddddddd3081c6" + "04405a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + "5a5a5a5a5a5a5a5a5a5a04405b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b" + "5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b" + "5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b04405c5c5c5c5c5c" + "5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c" + "5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c" + "5c5c" +); + +// ============================================================================ +// All Samples Array +// ============================================================================ + +/// All samples for iteration in tests +pub const ALL_SAMPLES: &[(&str, &[u8])] = &[ + // From explorer (mainnet blocks) + ("SET_REP", SET_REP), + ("SET_INFO", SET_INFO), + ("CREATE_IDENTIFIER", CREATE_IDENTIFIER), + ("TOKEN_ADMIN_SUPPLY", TOKEN_ADMIN_SUPPLY), + ("SEND", SEND), + ("MANAGE_CERTIFICATE", MANAGE_CERTIFICATE), + // Manually constructed (basic operations) + ("CREATE_IDENTIFIER_MULTISIG", CREATE_IDENTIFIER_MULTISIG), + ("CREATE_SWAP", CREATE_SWAP), + ("RECEIVE", RECEIVE), + ("MODIFY_PERMISSIONS", MODIFY_PERMISSIONS), + ("TOKEN_ADMIN_MODIFY_BALANCE", TOKEN_ADMIN_MODIFY_BALANCE), + ("MATCH_SWAP", MATCH_SWAP), + ("CANCEL_SWAP", CANCEL_SWAP), + // Edge cases (optional fields) + ("SEND_WITH_EXTERNAL", SEND_WITH_EXTERNAL), + ("SET_INFO_WITH_PERMISSION", SET_INFO_WITH_PERMISSION), + ("RECEIVE_WITH_FORWARD", RECEIVE_WITH_FORWARD), + ("MODIFY_PERMISSIONS_CLEAR", MODIFY_PERMISSIONS_CLEAR), + ("MODIFY_PERMISSIONS_WITH_TARGET", MODIFY_PERMISSIONS_WITH_TARGET), + ("MATCH_SWAP_WITH_FEE", MATCH_SWAP_WITH_FEE), + ("CANCEL_SWAP_WITH_FEE", CANCEL_SWAP_WITH_FEE), + ("MATCH_SWAP_FEE_NULL_TOKEN", MATCH_SWAP_FEE_NULL_TOKEN), + ("CANCEL_SWAP_FEE_NULL_TOKEN", CANCEL_SWAP_FEE_NULL_TOKEN), + ("TOKEN_ADMIN_SUPPLY_SUBTRACT", TOKEN_ADMIN_SUPPLY_SUBTRACT), + ("RECEIVE_EXACT_FALSE", RECEIVE_EXACT_FALSE), + ("MANAGE_CERTIFICATE_SUBTRACT", MANAGE_CERTIFICATE_SUBTRACT), + ("MANAGE_CERTIFICATE_NULL_INTERMEDIATE", MANAGE_CERTIFICATE_NULL_INTERMEDIATE), + // Block-level edge cases + ("BLOCK_MULTI_OPS", BLOCK_MULTI_OPS), + ("BLOCK_WITH_SIGNER_KEY", BLOCK_WITH_SIGNER_KEY), + ("BLOCK_V1_WITH_SUBNET", BLOCK_V1_WITH_SUBNET), + // Additional method variants + ("MODIFY_PERMISSIONS_SUBTRACT", MODIFY_PERMISSIONS_SUBTRACT), + ("MODIFY_PERMISSIONS_SET", MODIFY_PERMISSIONS_SET), + ("TOKEN_ADMIN_MODIFY_BALANCE_SUBTRACT", TOKEN_ADMIN_MODIFY_BALANCE_SUBTRACT), + ("TOKEN_ADMIN_MODIFY_BALANCE_SET", TOKEN_ADMIN_MODIFY_BALANCE_SET), + // Permission value edge cases + ("MODIFY_PERMISSIONS_ZERO", MODIFY_PERMISSIONS_ZERO), + ("MODIFY_PERMISSIONS_BOUNDARY", MODIFY_PERMISSIONS_BOUNDARY), + ("MODIFY_PERMISSIONS_LARGE", MODIFY_PERMISSIONS_LARGE), + // CreateIdentifier SwapArgs fee variants + ("CREATE_SWAP_WITH_FEE", CREATE_SWAP_WITH_FEE), + ("CREATE_SWAP_FEE_NULL_TOKEN", CREATE_SWAP_FEE_NULL_TOKEN), + // V2 block samples + ("BLOCK_V2_BASIC", BLOCK_V2_BASIC), + ("BLOCK_V2_PURPOSE_FEE", BLOCK_V2_PURPOSE_FEE), + ("BLOCK_V2_ACCOUNT_IS_SIGNER", BLOCK_V2_ACCOUNT_IS_SIGNER), + ("BLOCK_V2_MULTISIG_SIGNER", BLOCK_V2_MULTISIG_SIGNER), + ("BLOCK_V2_MULTI_SIGS", BLOCK_V2_MULTI_SIGS), + ("BLOCK_V2_NESTED_MULTISIG", BLOCK_V2_NESTED_MULTISIG), + ("BLOCK_V2_DEEP_NESTED_MULTISIG", BLOCK_V2_DEEP_NESTED_MULTISIG), +]; diff --git a/keetanetwork-block/tests/sdk_compat.rs b/keetanetwork-block/tests/sdk_compat.rs new file mode 100644 index 0000000..bb4d65c --- /dev/null +++ b/keetanetwork-block/tests/sdk_compat.rs @@ -0,0 +1,159 @@ +//! SDK Compatibility Tests +//! +//! These tests parse block DER bytes and verify roundtrip encoding. +//! Test data includes: +//! - Mainnet blocks from the Keeta Network explorer +//! - Manually constructed test vectors for edge cases and operation variants + +mod samples; + +use der::{Decode, Encode, Reader, SliceReader}; +use keetanetwork_block::{extract_operations_slice, KeetaBlock, Operation}; + +#[test] +fn test_block_roundtrip_all_samples() { + for (sample_name, original_bytes) in samples::ALL_SAMPLES { + let block = match KeetaBlock::from_der(original_bytes) { + Ok(b) => b, + Err(e) => panic!("Failed to parse {} block: {:?}", sample_name, e), + }; + + assert!(!block.operations.is_empty(), "{} block should have operations", sample_name); + assert!(!block.signatures.is_empty(), "{} block should have signatures", sample_name); + + let encoded = block + .to_der() + .unwrap_or_else(|_| panic!("Failed to encode {} block", sample_name)); + + assert_eq!(encoded, *original_bytes, "Roundtrip encoding mismatch for {} block", sample_name); + } +} + +// ============================================================================ +// Invalid/Malformed DER Input Tests +// ============================================================================ + +/// Test that empty input returns an error (not panic) +#[test] +fn test_malformed_empty_input() { + let result = KeetaBlock::from_der(&[]); + assert!(result.is_err(), "Empty input should fail to parse"); +} + +/// Test that truncated data returns an error +#[test] +fn test_malformed_truncated_data() { + // Take a valid sample and truncate it + let valid = samples::SET_REP; + let truncated = &valid[..valid.len() / 2]; + let result = KeetaBlock::from_der(truncated); + assert!(result.is_err(), "Truncated data should fail to parse"); +} + +/// Test that invalid outer tag returns an error +#[test] +fn test_malformed_invalid_tag() { + // Use OCTET STRING tag (0x04) instead of SEQUENCE (0x30) or [1] (0xa1) + let invalid = [0x04, 0x05, 0x01, 0x02, 0x03, 0x04, 0x05]; + let result = KeetaBlock::from_der(&invalid); + assert!(result.is_err(), "Invalid outer tag should fail to parse"); +} + +/// Test that length exceeding data returns an error +#[test] +fn test_malformed_length_overflow() { + // SEQUENCE with length 0xFF but only 5 bytes of data + let invalid = [0x30, 0x81, 0xff, 0x01, 0x02, 0x03, 0x04, 0x05]; + let result = KeetaBlock::from_der(&invalid); + assert!(result.is_err(), "Length overflow should fail to parse"); +} + +/// Test that extra trailing bytes are handled +#[test] +fn test_malformed_trailing_data() { + // Take a valid sample and append extra bytes + let valid = samples::SET_REP; + let mut with_trailing = valid.to_vec(); + with_trailing.extend_from_slice(&[0xde, 0xad, 0xbe, 0xef]); + let result = KeetaBlock::from_der(&with_trailing); + // The der crate typically rejects trailing data + assert!(result.is_err(), "Trailing data should fail to parse"); +} + +/// Test that invalid nested structure returns an error +#[test] +fn test_malformed_invalid_nested_structure() { + // V1 block header start, but with invalid content inside + // SEQUENCE { INTEGER 0, ... garbage } + let invalid = [ + 0x30, 0x10, // SEQUENCE of 16 bytes + 0x02, 0x01, 0x00, // INTEGER 0 (version) + 0x02, 0x02, 0x53, 0x82, // INTEGER 21378 (parent) + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // garbage + ]; + let result = KeetaBlock::from_der(&invalid); + assert!(result.is_err(), "Invalid nested structure should fail to parse"); +} + +/// Test that zero-length required fields return an error +#[test] +fn test_malformed_zero_length_field() { + // SEQUENCE with empty OCTET STRING where account key is expected + let invalid = [ + 0x30, 0x08, // SEQUENCE of 8 bytes + 0x02, 0x01, 0x00, // INTEGER 0 (version) + 0x02, 0x01, 0x01, // INTEGER 1 (parent) + 0x04, 0x00, // Empty OCTET STRING (invalid account) + ]; + let result = KeetaBlock::from_der(&invalid); + assert!(result.is_err(), "Zero-length required field should fail to parse"); +} + +// ============================================================================ +// extract_operations_slice Tests +// ============================================================================ + +/// Test that extract_operations_slice works with all sample blocks +#[test] +fn test_extract_operations_slice_all_samples() { + for (sample_name, original_bytes) in samples::ALL_SAMPLES { + // First parse using full KeetaBlock to get expected operations + let block = KeetaBlock::from_der(original_bytes) + .unwrap_or_else(|e| panic!("Failed to parse {} block: {:?}", sample_name, e)); + + let ops_slice = extract_operations_slice(original_bytes) + .unwrap_or_else(|| panic!("extract_operations_slice failed for {}", sample_name)); + + // Parse operations from the slice and verify count matches + let mut reader = SliceReader::new(ops_slice).expect("SliceReader failed"); + let mut op_count = 0; + while !reader.is_finished() { + let _op = Operation::decode(&mut reader) + .unwrap_or_else(|e| panic!("Failed to decode operation {} in {}: {:?}", op_count, sample_name, e)); + op_count += 1; + } + + assert_eq!( + op_count, + block.operations.len(), + "Operation count mismatch for {}: extracted {} vs block {}", + sample_name, + op_count, + block.operations.len() + ); + } +} + +/// Test that extract_operations_slice returns None for invalid input +#[test] +fn test_extract_operations_slice_invalid() { + // Empty input + assert!(extract_operations_slice(&[]).is_none()); + + // Invalid tag + assert!(extract_operations_slice(&[0x04, 0x05, 0x01, 0x02, 0x03, 0x04, 0x05]).is_none()); + + // Truncated data + let valid = samples::SET_REP; + assert!(extract_operations_slice(&valid[..10]).is_none()); +} diff --git a/keetanetwork-x509/src/utils.rs b/keetanetwork-x509/src/utils.rs index 783946b..408a64c 100644 --- a/keetanetwork-x509/src/utils.rs +++ b/keetanetwork-x509/src/utils.rs @@ -978,7 +978,7 @@ mod tests { assert_eq!(multi_pairs.len(), 7); // Verify each mapping - let expected_mappings = vec![ + let expected_mappings = [ ("commonName", "example.com"), ("organizationName", "Example Organization"), ("organizationalUnitName", "IT Department"),