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)

/// Ed25519: 32-byte pubkey
pub const ED25519: u8 = 0x01;
/// secp256r1 (NIST P-256): 33-byte compressed pubkey
pub const SECP256R1: u8 = 0x02;
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
pub const SECP256R1: u8 = 0x02;
pub const SECP256R1: u8 = 0x06;

/// Destination account
pub to: &'a [u8],
/// Amount to send
pub amount: u64,
Copy link
Member

Choose a reason for hiding this comment

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

The amount may exceed u64's range and a bigint type is really required

Same is true of all below

Copy link
Collaborator Author

@mamonet mamonet Jan 10, 2026

Choose a reason for hiding this comment

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

This types are shared with a Ledger device app (no_std, no alloc). I'm already using &'a [u8] for addresses, tokens, and other fields to reference the raw DER bytes directly (zero-copy). The same approach works for integers, &'a [u8] stores the big-endian DER integer bytes, representing arbitrary-precision values without allocation.

Copy link
Collaborator

Choose a reason for hiding this comment

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

He is referring to amount which will overflow.

Copy link
Collaborator

@sephynox sephynox Jan 13, 2026

Choose a reason for hiding this comment

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

Would https://github.com/RustCrypto/crypto-bigint work? PS, this is re-exported in keetanetwork-crypto.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

From docs.keeta.com/components/ledger: "balances are maintained on a per-token basis on each account and are maintained as an arbitrary large big integer." but in other 3rd-party websites like https://coingecko.com/learn/what-is-keeta-network I can see "KTA is the native token of the Keeta Network and the backbone of its economy. It has a fixed maximum supply of 1 billion tokens" in this case we can safely use crypto-bigint U128

Copy link
Member

Choose a reason for hiding this comment

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

That only refers to that native KTA token, other tokens exist on the network.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

In this case, we will get back to the reference approach since all crypto-bigint types are limited.

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
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AdjustMethod {
Add,
Remove,
Copy link
Collaborator

Choose a reason for hiding this comment

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

The TS uses Subtract not Remove. We should probably keep the naming consistent.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ASN.1 uses Remove instead of Subtract, I was confused by this one too.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

should I change it for AdjustMethodRelative too?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, naming should be the same

Comment on lines 85 to 102
pub struct BlockHeader<'a> {
/// Network ID
pub network: u64,
/// Subnet ID (optional)
pub subnet: Option<u64>,
/// Idempotent key for deduplication (optional)
pub idempotent: Option<&'a [u8]>,
/// Date as GeneralizedTime 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 public key with type prefix (may be same as account)
pub signer: &'a [u8],
/// Previous block hash (32 bytes)
pub previous: &'a [u8],
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

The signatures are currently missing. Is this going to be added later?

Copy link
Collaborator Author

@mamonet mamonet Jan 14, 2026

Choose a reason for hiding this comment

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

I will add signatures next

Comment on lines 301 to 320
/// [5] TOKEN_ADMIN_SUPPLY operation - Modify token supply
#[derive(Debug, Clone, Copy)]
pub struct TokenAdminSupplyOp<'a> {
/// Amount to modify
pub amount: &'a [u8],
/// Method (add/remove/set)
pub method: AdjustMethod,
}

/// [6] TOKEN_MODIFY_BALANCE operation - Modify account token balance
#[derive(Debug, Clone)]
pub struct TokenModifyBalanceOp<'a> {
/// Token to modify balance of
pub token: &'a [u8],
/// Amount to modify
pub amount: &'a [u8],
/// Method (add/remove/set)
pub method: AdjustMethod,
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

You used TOKEN_ADMIN_SUPPLY but not TOKEN_ADMIN_MODIFY_BALANCE. We could probably make it consistent.

@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 173 to 181
impl From<u8> for AdjustMethodRelative {
fn from(value: u8) -> Self {
match value {
0 => AdjustMethodRelative::Add,
1 => AdjustMethodRelative::Remove,
n => AdjustMethodRelative::Unknown(n),
}
}
}
Copy link
Member

Choose a reason for hiding this comment

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

This should reject/throw since its invalid otherwise

Copy link
Collaborator

Choose a reason for hiding this comment

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

It cannot so it will need to be a TryFrom instead.

Copy link
Collaborator Author

@mamonet mamonet Jan 13, 2026

Choose a reason for hiding this comment

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

I used TryFrom instead

Comment on lines 378 to 403
#[derive(Debug, Clone)]
pub enum Operation<'a> {
/// [0] Send tokens
Send(SendOp<'a>),
/// [1] Set representative
SetRep(SetRepOp<'a>),
/// [2] Set account info
SetInfo(SetInfoOp<'a>),
/// [3] Modify permissions
ModifyPermissions(ModifyPermissionsOp<'a>),
/// [4] Create identifier (token, multisig, swap)
CreateIdentifier(CreateIdentifierOp<'a>),
/// [5] Token admin supply
TokenAdminSupply(TokenAdminSupplyOp<'a>),
/// [6] Token modify balance
TokenModifyBalance(TokenModifyBalanceOp<'a>),
/// [7] Receive tokens
Receive(ReceiveOp<'a>),
/// [8] Manage certificate
ManageCertificate(ManageCertificateOp<'a>),
/// [9] Match swap
MatchSwap(MatchSwapOp<'a>),
/// [10] Cancel swap
CancelSwap(CancelSwapOp<'a>),
/// Unknown operation type
Unknown(u32),
Copy link
Collaborator

Choose a reason for hiding this comment

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

The operation IDs would need to be explicitly defined with implemented DER.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Moreover, I don't know if this would work as expected. We would need explicit context tagging.

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 plan to address that by adding #[derive(Choice)] with #[asn1(context_specific = "N")] attributes - the der crate handles explicit tagging automatically

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!

0 => AdjustMethod::Add,
1 => AdjustMethod::Remove,
2 => AdjustMethod::Set,
n => AdjustMethod::Unknown(n),
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should return Result not use Unknown.

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

pub enum BlockPurpose {
Generic,
Fee,
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.

There should be no Unknowns.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed

Comment on lines 46 to 49
/// Expected key sizes (prefix + pubkey)
const SECP256K1_KEY_SIZE: usize = 34; // 1 + 33
const ED25519_KEY_SIZE: usize = 33; // 1 + 32
const SECP256R1_KEY_SIZE: usize = 34; // 1 + 33
Copy link
Collaborator

Choose a reason for hiding this comment

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

These things should probably be in keetanetwork-crypto if they are not already.

Copy link
Collaborator

Choose a reason for hiding this comment

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

We can feature gate minimal items like constants and utils

Comment on lines 60 to 70
pub fn is_valid_address(addr: &[u8]) -> bool {
if addr.is_empty() {
return false;
}
match addr[0] {
algo_prefix::SECP256K1 => addr.len() == SECP256K1_KEY_SIZE,
algo_prefix::ED25519 => addr.len() == ED25519_KEY_SIZE,
algo_prefix::SECP256R1 => addr.len() == SECP256R1_KEY_SIZE,
_ => false, // Unknown algorithm prefix
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should also probably be in keetanetwork-accounts.

Copy link
Collaborator

Choose a reason for hiding this comment

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

We can feature gate minimal items like constants and utils.

#[derive(Debug, Clone)]
pub struct CancelSwapOp<'a> {
/// Swap account to cancel
pub swap: &'a [u8],
Copy link
Member

Choose a reason for hiding this comment

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

Should this be a reference to an account instead?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Using &[u8] for accounts/addresses is intentional, this is a zero-copy design for Ledger (no alloc). The field name and doc comment convey the semantic type.

@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

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

@sonarqubecloud
Copy link

sonarqubecloud bot commented Feb 5, 2026

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