Skip to content

[BUG] #[note] rejects a type whose Packable impl is conditional on a generic bound (e.g. Foo<T> where T: ToField + FromField) #23836

Description

@shiqicao

What are you trying to do?

Use a generic type that implements Packable only under a trait bound as an Aztec note. Concretely, a wrapper like Foo<T> whose Packable impl is impl<T> Packable for Foo<T> where T: ToField + FromField. #[note] rejects the type even though every concrete instantiation that would actually be used as a note (e.g. Foo<Field>, since Field: ToField + FromField) satisfies the bound.

Real-world case: a PackableBoundedVec<T, MaxLen> (BoundedVec wrapper that is Packable only when T: ToField + FromField) — it cannot be used as / embedded generically in a note.

Code Reference

Nargo.toml:

[package]
name = "note_mre"
type = "contract"
authors = [""]
compiler_version = ">=1.0.0"

[dependencies]
aztec = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-aztecnr-rc.2", directory = "noir-projects/aztec-nr/aztec" }

src/main.nr:

use aztec::macros::aztec;

#[aztec]
pub contract NoteMre {
    use aztec::macros::notes::note;
    use aztec::protocol::traits::{FromField, Packable, ToField};

    // `Foo<T>` is `Packable` only when `T` can round-trip through `Field`.
    #[note]
    pub struct Foo<T> {
        pub inner: T,
    }

    impl<T> Packable for Foo<T>
    where
        T: ToField + FromField,
    {
        let N: u32 = 1;

        fn pack(self) -> [Field; Self::N] {
            [self.inner.to_field()]
        }

        fn unpack(packed: [Field; Self::N]) -> Self {
            Foo { inner: T::from_field(packed[0]) }
        }
    }
}

aztec compile fails with:

error: Foo does not implement Packable trait. Either implement it manually or place #[derive(Packable)] on the note struct before #[note] macro invocation.
286 │         note.as_type().implements(packable_constraint),

(Foo<T> does implement Packable — just conditionally.)

Root cause

#[note]'s assert_has_packable in noir-projects/aztec-nr/aztec/src/macros/notes.nr does:

note.as_type().implements(packable_constraint)

For an unbounded generic whose Packable impl is conditional (where T: ToField + FromField), implements(...) returns false, so the note is rejected — even though it is Packable for every T that satisfies the bound.

The error message suggests #[derive(Packable)], but that does not apply here either: the derive emits impl<T> Packable for Foo<T> where T: Packable, whereas this type is packable via T: ToField + FromField, not T: Packable.

Expected behavior

#[note] (and ideally #[derive(Packable)]) should support generic types that are conditionally Packable: e.g. assert_has_packable should accept a conditional impl (and the generated NoteType/NoteHash impls should carry the same where bounds), so a bounded-generic type can be used as a note. Today the only workaround is to monomorphize at the note site (e.g. struct MyNote { foo: Foo<Field> }), which works because Field: ToField + FromField but prevents the generic itself from being a note.

Aztec Version

v4.2.0-aztecnr-rc.2 (noirc 1.0.0-beta.18)

OS

Linux

Node Version

v22.14.0

Proposed solutions

Option 1 — support generic type constraints on the struct definition.
Allow a where clause to be declared on the note struct (or honored from the existing conditional impl) and propagated to the macro-generated impls:

#[note]
struct Foo<T>
where
    T: ToField + FromField,
{
    inner: T,
}

assert_has_packable would then evaluate implements(Packable) under the declared bound (instead of on the unbounded generic), and the generated NoteType/NoteHash (and #[derive(Packable)]) impls would carry the same where T: ToField + FromField. This makes the bounded generic itself usable as a note.

Option 2 — support a newtype wrapper that transparently forwards the trait impls.
Provide a newtype/transparent-derive mechanism where a single-field wrapper delegates Packable (and the note's NoteType/NoteHash) to its inner field's impl:

#[note]
struct Foo(InnerPackable);  // newtype; Packable derived by delegation to the inner field

The wrapper's Packable is obtained by forwarding to the inner type's existing impl, so #[note] never needs to reason about the inner type's generic bounds at all.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions