Skip to content

feat: implement manual Packable for structs with sub-Field members#21576

Merged
benesjan merged 1 commit intomerge-train/fairiesfrom
nico/f-260-manual-packable-impls
Mar 17, 2026
Merged

feat: implement manual Packable for structs with sub-Field members#21576
benesjan merged 1 commit intomerge-train/fairiesfrom
nico/f-260-manual-packable-impls

Conversation

@benesjan
Copy link
Contributor

@benesjan benesjan commented Mar 16, 2026

Motivation for this PR was this other PR implemented by Nico since that PR resulted in a failing CI that in turn was a result of brittle bootstrap.sh scripts that did not expect any output to be coming from the tests. Since that PR now prints out a warning to stdout when an inefficient Packable implementation is detected this broke that assumption.

I came to a conclusion that the best approach here is to just do the efficient implementations. Since bootstrap.sh is only an internal script keeping that brittle seems fine.

Summary

  • Replace #[derive(Packable)] with efficient manual implementations for 6 structs that had sub-optimal packing (multiple sub-Field members each occupying a full Field)
  • This avoids the new compile-time warning breaking CI (the warning was printed to stdout, which nargo test --list-tests output parsing treated as a test entry)
  • Reduces public storage costs (fewer SLOADs/SSTOREs) and DA costs for Asset, Order, Game, and PlayerEntry; reduces proving time for Card and SubscriptionNote
Struct Storage Fields before Fields after
Asset PublicMutable 4 3
Order PublicImmutable 3 2
Game PublicMutable 13 9
PlayerEntry (nested in Game) 3 2
Card Private (FieldNote) 2 1
SubscriptionNote PrivateMutable 2 1

Test plan

  • All new pack/unpack roundtrip unit tests pass (17 tests across 4 packages)
  • Zero warning: lines in nargo test --list-tests output
  • Existing tests unaffected

🤖 Generated with Claude Code

@benesjan benesjan requested a review from LeilaWang as a code owner March 16, 2026 08:44
let post_value = MockStruct { a: 3, b: 4 };

let sdc = ScheduledDelayChange::<0u64>::new(Option::some(1), Option::some(50), 2);
let sdc = ScheduledDelayChange::<0_u64>::new(Option::some(1), Option::some(50), 2);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unrelated change that was a result of me running nargo fmt on the repo.


fn unpack(packed: [Field; Self::N]) -> Self {
let remaining_txs = packed[0] as u32;
let expiry_block_number = ((packed[0] - remaining_txs as Field) / 2.pow_32(32)) as u32;
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe a comment here would be nice. these kinds of unpacking functions normally rely on shifts and bitmasks, I guess we don't have yet those in Noir?

Copy link
Contributor

Choose a reason for hiding this comment

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

I see you use the same approach in the other contracts, maybe worth adding a reusable function?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

these kinds of unpacking functions normally rely on shifts and bitmasks, I guess we don't have yet those in Noir?

We have them in Noir but unlike in CPU instruction sets, bit shifts are super inefficient in circuits while multiplication and division are efficient (circuits have MUL and AND gates and you can perform division by multiplying by modulo inverse).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We intentionally use multiply/divide by powers of 2 rather than bit shifts — they map more efficiently to both AVM opcodes (for public functions) and proving backend primitives (for private functions). The Packable trait docs already recommend this approach.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I considered this but a reusable helper doesn't really help here — the as TargetType casts (e.g. as u32, as u64, as u128) are the core of each unpacking since they provide the circuit range constraints, and each site uses different type combinations (u32+u32, u32+u64, u128+u64, u128+bool, bools+u32s). A helper returning raw Fields would require callers to still do all the casting, resulting in more lines not fewer. The Packable trait docs already serve as the guide with a worked example.

@benesjan benesjan requested a review from mverzilli March 17, 2026 09:34
@benesjan benesjan force-pushed the nico/f-260-manual-packable-impls branch from 090f818 to e9d06d3 Compare March 17, 2026 09:38
@benesjan benesjan changed the base branch from nico/f-260-warn-on-packable-with-small-types to merge-train/fairies March 17, 2026 09:38
Replace `#[derive(Packable)]` with efficient manual implementations for 6 structs
that had sub-optimal packing (multiple sub-Field members each occupying a full Field).
This avoids the new compile-time warning and reduces storage/hashing costs.

- Asset: 4 -> 3 Fields (pack u128 + u64 together)
- Order: 3 -> 2 Fields (pack u128 + bool together)
- Game: 13 -> 9 Fields (pack 3 bools + 2 u32s together)
- PlayerEntry: 3 -> 2 Fields (pack u32 + u64 together)
- Card: 2 -> 1 Field (pack 2 u32s together)
- SubscriptionNote: 2 -> 1 Field (pack 2 u32s together)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@benesjan benesjan force-pushed the nico/f-260-manual-packable-impls branch from e9d06d3 to 150e534 Compare March 17, 2026 09:39
@benesjan benesjan enabled auto-merge (squash) March 17, 2026 09:40
@benesjan benesjan merged commit 6241a7a into merge-train/fairies Mar 17, 2026
11 checks passed
@AztecBot
Copy link
Collaborator

✅ Successfully backported to backport-to-v4-next-staging #21654.

github-merge-queue bot pushed a commit that referenced this pull request Mar 17, 2026
BEGIN_COMMIT_OVERRIDE
feat: entrypoint replay protection (#21649)
feat: guard BoundedVec oracle returns against dirty trailing storage
(#21589)
feat: implement manual Packable for structs with sub-Field members
(#21576)
fix: off-by-1 in getBlockHashMembershipWitness archive snapshot (#21648)
END_COMMIT_OVERRIDE
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants