Skip to content

Implement keetanetwork-block crate with block and operation types#3

Open
mamonet wants to merge 25 commits intomainfrom
feat/block
Open

Implement keetanetwork-block crate with block and operation types#3
mamonet wants to merge 25 commits intomainfrom
feat/block

Conversation

@mamonet
Copy link
Collaborator

@mamonet mamonet commented Jan 10, 2026

This PR adds the following features:

  • Shared keetanetwork-block crate for block and operation types
  • Zero-copy types with lifetime parameters for no_std environments
  • Includes all 11 operation types matching the protocol spec
  • Add address validation based on algorithm prefix (secp256k1/Ed25519/secp256r1) [REMOVED - Duplicate with account code]

TODO

  • Vote types (X.509 certificate-based)
  • VoteStaple types
  • MultiSigSignerInfo (nested multisig)

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new keetanetwork-block crate that provides foundational block and operation type definitions for the Keeta blockchain. The implementation emphasizes zero-copy design with lifetime parameters to support no_std environments while maintaining flexibility through optional alloc and std features.

Changes:

  • Added shared type definitions for blocks (V1 and V2) and all 11 blockchain operations
  • Implemented address validation based on algorithm prefix (secp256k1, Ed25519, secp256r1)
  • Configured feature flags to support no_std, alloc, and std environments

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
keetanetwork-block/Cargo.toml Defines feature flags for std/alloc/no_std support and removes unused dependencies
keetanetwork-block/src/lib.rs Exports public API with conditional compilation for alloc-dependent types
keetanetwork-block/src/types.rs Implements all block types, operation structures, and address validation logic

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@mamonet
Copy link
Collaborator Author

mamonet commented Jan 11, 2026

@rkeene do you suggest to put the remaining types here or in different PR?

@sephynox sephynox self-requested a review January 12, 2026 18:59
@sephynox sephynox added the enhancement New feature or request label Jan 12, 2026
@rkeene
Copy link
Member

rkeene commented Jan 12, 2026

@rkeene do you suggest to put the remaining types here or in different PR?

I think it's fine to include that change in this PR since it's a dependency -- otherwise you would need to re-org and re-base but without much gained from it here.

Comment on lines 167 to 171
pub enum AdjustMethodRelative {
Add,
Remove,
Unknown(u8),
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure this is a good approach. We should just return Result instead when using an unsupported AdjustMethod.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

Comment on lines 121 to 128
pub fn new(header: BlockHeader<'a>, operations: Vec<Operation<'a>>) -> Self {
Self { version: BlockVersion::V1, header, operations }
}

/// Create a new V2 block
pub fn new_v2(header: BlockHeader<'a>, operations: Vec<Operation<'a>>) -> Self {
Self { version: BlockVersion::V2, header, operations }
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may want a KeetaBlockBuilder instead of new_v*

let block = KeetaBlockBuilder::from(BlockVersion::V*)
    .with_block_header(header)
    .with_operations(operations)
    .build();

Something like so.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added KeetaBlockBuilder but I set header in constructor since it's not optional for both versions

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Builder should be in a draft PR and not here

@mamonet
Copy link
Collaborator Author

mamonet commented Jan 19, 2026

I think the changes covers the keetanetwork-block work we scoped.

Summary of implementation:

  • DER encoding/decoding: support for all types (V1/V2 blocks, operations, signatures) with support for no_alloc environments
  • Multisig support: Added signer types in addition to vote & certificate types
  • JS SDK compatibility: Roundtrip tests verify byte-exact encoding against real mainnet blocks and manually constructed test vectors

Unrelated Changes

  • Fixed clippy useless_vec warning in keetanetwork-x509/src/utils.rs (replaced vec![] with array literal in test)

All tests passing. Ready for review.

#[derive(Debug, Clone, Copy, Sequence)]
pub struct TokenRate<'a> {
/// Token public key
pub token: Bytes<'a>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we use undifferentiated Bytes here instead of something higher-level and more specific (account) ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This layer mirrors the ASN.1 (all OCTET STRING); semantic validation happens upstream in TS via assertKeyType()
can add aliases for clarity: pub type TokenId<'a> = Bytes<'a>; No compile-time safety, but documents intent. Prefer newtype wrappers instead?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sephynox @mamonet It still seems like it should be a type with an assertKeyType() method rather than undifferentiated bytes

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could use the existing types but we would need to make those crates partially no_std.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a newtype wrapper would be better, something like:

pub struct TokenId<'a>(Bytes<'a>);

impl<'a> TokenId<'a> {
    // validate algorithm prefix and length
    pub fn assert_key_type(&self) -> Result<(), Error> {}

    pub fn as_bytes(&self) -> &'a [u8] {
        self.0.as_bytes()
    }
}

I will implement these types for Bytes fields.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will be resolved when using account crate

pub signature: &'a [u8],
}

/// Vote staple - bundles blocks with their votes
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vote Staples make few other guarantees as well:

  1. All votes have the same set of block hashes in the same order
  2. All blocks are referenced by the votes
  3. All blocks referenced in the votes are present
  4. All votes are of the same level of permanence (temporary or permanent)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting design for packaging - I'm familiar with OCSP stapling, which bundles certificate status proofs so verifiers don't need extra roundtrips, similar concept here with votes and blocks.
These invariants make sense for that model, want me to add them as doc comments on VoteStaple, implement a validate() method or both?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either is fine at this point, but the validation is important to do at some point because it's not a valid vote staple unless those conditions are met

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. Before jumping into the implementation, it might be worth discussing how vote staple validation should behave under different circumstances — e.g., should validate() be called on construction (ensuring invalid staples can't exist), on deserialization from the network, or deferred to the consumer? The answer likely affects where the method lives and what it returns. Happy to sync on this and then implement accordingly.

@sonarqubecloud
Copy link

sonarqubecloud bot commented Feb 5, 2026

Comment on lines +70 to +92
// 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()?;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These comments are pointless.

Comment on lines +138 to +161
// 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)?;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These comments are pointless.

Comment on lines +375 to +395

// 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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These comments are pointless.

Comment on lines +454 to +478
// 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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These comments are pointless.

Comment on lines +664 to +676
#[cfg(any(feature = "alloc", feature = "std"))]
fn encode_generalized_time_len(date_bytes: &[u8]) -> der::Result<Length> {
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)
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One mutates and the other returns but you would not know this from this naming scheme.

Comment on lines +117 to +139
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;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could probably DRY this.

Comment on lines +194 to +225
#[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"),
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could data-drive these tests.

Comment on lines +37 to +76
#[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();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can probably data-drive all of these KeetaBlock::from_der tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants