Skip to content

feat: BCS ABNF compile-time generation#1040

Open
thibault-martinez wants to merge 25 commits intodevelopfrom
bcs-abnf-poc
Open

feat: BCS ABNF compile-time generation#1040
thibault-martinez wants to merge 25 commits intodevelopfrom
bcs-abnf-poc

Conversation

@thibault-martinez
Copy link
Copy Markdown
Member

@thibault-martinez thibault-martinez commented Mar 25, 2026

Summary

This PR introduces a machine-readable BCS schema for iota-sdk-types, expressed as an ABNF grammar file, along with the tooling to generate and validate it.

What's included

  • crates/iota-bcs-schema — a new proc-macro crate providing a #[derive(BcsSchema)] macro. When applied to a type, it emits a bcs_schema_definition() method that returns the type's ABNF grammar fragment. The macro handles structs, enums, and common generic wrappers (Option<T>, Vec<T>, HashMap<K, V>), and supports override attributes (#[bcs_schema(name = "...", definition = "...")] on types and #[bcs_schema(as_type = "...")] on fields/variants) for cases where the derived output needs manual tuning.

  • crates/iota-sdk-types/bcs-schema.abnf — the auto-generated ABNF schema covering all SDK types (transactions, effects, checkpoints, crypto primitives, etc.). It is committed to the repo and regenerated on demand via BCS_SCHEMA=1 cargo check -p iota-sdk-types --features bcs-schema.

  • crates/iota-sdk-types/build.rs — a build script that, when BCS_SCHEMA=1 is set alongside the bcs-schema feature, deletes the old schema file and touches all .rs sources to force every #[derive(BcsSchema)] to re-run, producing a fresh schema from scratch.

  • Grammar-driven fuzz tests (tests/bcs_schema_fuzzing.rs) — a test suite that parses bcs-schema.abnf, randomly generates conforming byte sequences for each rule, and asserts that the BCS deserializer accepts them. This validates that the grammar is a sound description of the wire format.

  • CI — a new lint workflow job that regenerates the schema and fails if the committed file is out of date.

Motivation

Having a formal, human- and machine-readable description of the BCS wire format makes it easier to:

  • build cross-language BCS parsers/validators without reimplementing the Rust types,
  • catch accidental serialization-breaking changes at the schema level, and
  • fuzz the deserializer against grammar-valid inputs.

Usage

# Regenerate the schema file
BCS_SCHEMA=1 cargo check -p iota-sdk-types --features bcs-schema

# Normal development — no impact
cargo check --all-features

Derive attributes

Attribute Level Purpose
#[bcs_schema(definition = "32OCTET")] Type Override entire RHS (for [u8; Self::LENGTH], etc.)
#[bcs_schema(name = "custom-name")] Type Override the ABNF rule name
#[bcs_schema(as_type = "u64")] Field Override schema type (for type aliases like Version = u64)
#[bcs_schema(as_type = "call-arg")] Field Reference a wire-type that differs from the Rust type
#[bcs_schema(skip)] Field Omit from schema

Known limitations

  • as_type and definition values are trusted — no validation against the actual serde impl
  • Custom Serialize impls are invisible to the macro; divergences must be annotated manually
  • Serde attributes (skip, rename, flatten) are not read by the macro

Test plan

  • cargo check -p iota-sdk-types --all-features — no regressions
  • BCS_SCHEMA=1 cargo check --features bcs-schema produces complete, sorted bcs-schema.abnf
  • Two consecutive BCS_SCHEMA=1 builds produce byte-identical output
  • Removing BcsSchema from a field type causes a compile error
  • cargo check without the feature is unaffected (fully incremental)

Comment thread crates/iota-sdk-types/src/effects/v1.rs
Comment thread crates/iota-bcs-schema/Cargo.toml Outdated
@DaughterOfMars DaughterOfMars marked this pull request as ready for review April 8, 2026 10:14
@DaughterOfMars DaughterOfMars requested a review from a team as a code owner April 8, 2026 10:14
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
#[cfg_attr(feature = "bcs-schema", derive(iota_bcs_schema::BcsSchema))]
#[cfg_attr(feature = "bcs-schema", bcs_schema(definition = "%x60 96OCTET"))]
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

can these be merged? If yes, do it for all instances please

Comment thread crates/iota-bcs-schema/src/lib.rs Outdated
@thibault-martinez thibault-martinez changed the title feat: BCS ABNF PoC feat: BCS ABNF compile-time generation Apr 8, 2026
Comment thread crates/iota-bcs-schema/src/lib.rs
Comment thread crates/iota-bcs-schema/src/lib.rs
Comment thread crates/iota-bcs-schema/src/lib.rs
Comment thread crates/iota-bcs-schema/Cargo.toml
Comment thread crates/iota-sdk-types/src/effects/v1.rs Outdated
Comment thread crates/iota-sdk-types/src/transaction/mod.rs
Comment thread crates/iota-sdk-types/src/type_tag/mod.rs Outdated
Comment thread crates/iota-sdk-types/src/object.rs
Copy link
Copy Markdown
Member

@Thoralf-M Thoralf-M left a comment

Choose a reason for hiding this comment

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

We should add a README.md in iota-bcs-schema/ which explains what this is good for and how to use it

Comment thread crates/iota-sdk-types/bcs-schema.abnf Outdated
Comment thread crates/iota-sdk-types/bcs-schema.abnf
Comment thread crates/iota-sdk-types/bcs-schema.abnf Outdated
Comment thread crates/iota-sdk-types/bcs-schema.abnf Outdated
#[cfg_attr(
feature = "bcs-schema",
derive(iota_bcs_schema::BcsSchema),
bcs_schema(definition = "%x60 96OCTET")
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Can you check if "OCTET" is the standard?

#[cfg_attr(
feature = "bcs-schema",
derive(iota_bcs_schema::BcsSchema),
bcs_schema(definition = "%x60 96OCTET")
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Can we have it in decimal?

#[cfg_attr(
feature = "bcs-schema",
derive(iota_bcs_schema::BcsSchema),
bcs_schema(definition = "bytes")
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

What is bytes? Is it defined in the ABNF? length-prefixed?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There are now definitions added to the file for these basic types

#[cfg_attr(
feature = "bcs-schema",
derive(iota_bcs_schema::BcsSchema),
bcs_schema(definition = "string")
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Properly define string in the ABNF?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

done

#[cfg_attr(
feature = "bcs-schema",
derive(iota_bcs_schema::BcsSchema),
bcs_schema(name = "type-tag")
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

What would happen if I created a different type with the same BCS schema name? Can we make sure it fails?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There is now duplicate detection

#[cfg_attr(
feature = "bcs-schema",
derive(iota_bcs_schema::BcsSchema),
bcs_schema(definition = "32OCTET")
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Can we put a space? I always read it as 320 :(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

no :c

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I could add something like byte = OCTET so it would look like 32byte instead?

Comment thread crates/iota-sdk-types/src/object.rs Outdated

#[derive(serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "bcs-schema", derive(iota_bcs_schema::BcsSchema))]
#[cfg_attr(feature = "bcs-schema", bcs_schema(name = "genesis-object"))]
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Combine

@thibault-martinez thibault-martinez linked an issue Apr 9, 2026 that may be closed by this pull request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rethink BCS compatibility check

4 participants