Skip to content

feat(uniffi): application membership across the FFI, every binding regenerated - #687

Closed
bahdotsh wants to merge 1 commit into
mainfrom
feat/app-membership-udl
Closed

bahdotsh wants to merge 1 commit into
mainfrom
feat/app-membership-udl

Conversation

@bahdotsh

@bahdotsh bahdotsh commented Nov 4, 2026

Copy link
Copy Markdown
Member

Step 6 of the shared network plan (ADR 0037). First of a stack: steps 7 to 12 are stacked on this branch.

What an application gets

  • ProtocolConfig.app_publisher_key: optional, 32 bytes. Any other length is refused at construction, so a key that was meant to be set never silently configures nothing.
  • set_app_membership(cert) and app_membership_status() on the engine. The status reports this node's own configuration only (configured, its certificate's not_after while current, how many neighbours are visible).
  • sign_app_membership(publisher_private_key, address, not_after): a pure namespace function for embedded mode (decision 6a). A source guard (sign_app_membership_never_lets_the_key_out) refuses any log, event, storage or send in its body. I checked it with a negative control: planting a debug! in the body makes it fail.

Nothing is gated yet. Visibility is still read only by tests; the gate is the next PR.

#685 review items folded in

  1. No restart when the same certificate is supplied again. Before this, an app that hands the certificate over on every foreground made every neighbour sign and seal again. A hello burst with no link change is also something only a configured node produces. A renewed certificate still restarts every exchange (tested).
  2. No hello for a peer with no mesh link. Discovery also reports relay, Nostr and LAN peers. Each of those cost a Lamport tick and an Ed25519 signature before send_to_neighbor refused it. A later mesh link is still greeted at once (tested).
  3. Tests for the 512-exchange and 1024-visible bounds.

Regenerated

The whole set was regenerated from the one definition: Swift, Kotlin, Python, Ruby, Java, the C dispatcher, the C header (no change), the Python local API table, and the C# generated sources.

  • C# generator: it refused sequence<u8>? on ProtocolConfig, which is what it should do. It now has a rule for that type: standard base64, the C API's JSON form for bytes.
  • Local API: the three calls are platform operations.
    • The certificate binds the one identity every client shares, so supplying it is the operator's job.
    • The signing helper takes a private key, which must never cross a socket.
  • Java: ConsumerCheckTest.config() passes null for the new field.

Tested locally

  • cargo test -p offline-protocol --lib membership: 29 pass, plus the two signing tests.
  • cargo test -p offline-protocol-uniffi: 318 pass, including the two new FFI tests.
  • Generator checks:
    • capi-dispatch-gen --check: clean.
    • generate_local_api_table.py --check: clean.
    • gen_csharp_local_api.py --check: clean.
  • Python tests/local_api: 129 pass.
  • Clippy: cargo clippy --workspace and --no-default-features on offline-protocol are both clean.
  • Rustdoc under -D warnings: clean.

Not run locally

Java, Ruby, C#, Swift and Android. CI runs those.

Owed to later PRs in the stack

  • React Native and Flutter wrapper surfaces for the three calls.
  • The integrator guide (step 12).

…generated

Step 6 of the shared network plan (ADR 0037). The membership exchange
has run on every node since the previous change; this is how an
application turns it on.

- `ProtocolConfig.app_publisher_key` (optional, 32 bytes; any other
  length is refused at construction, so a key meant to be set never
  silently configures nothing).
- `set_app_membership(cert)` and `app_membership_status()` on the
  engine.
- `sign_app_membership(publisher_private_key, address, not_after)`, a
  pure namespace function for embedded mode. A source guard refuses any
  log, event, storage or send in its body, so the key cannot reach one
  of those whatever is added around it.

Two review items from #685 land here, because this is the first change
that lets an application call the code:

- Supplying the same certificate again is a no-op. Before, every call
  restarted every exchange, so an app that hands the certificate over
  on each foreground made every neighbour sign and seal again, and a
  hello burst with no link change is something only a configured node
  produces.
- The hello is not built for a peer with no mesh link. Discovery also
  reports relay, Nostr and LAN peers, and each one cost a clock tick and
  an identity signature before the send refused it.

Tests for both, for the status, for a certificate from the signing
helper being accepted, and for the 512-exchange and 1024-visible bounds.

Every binding is regenerated from the one definition: Swift, Kotlin,
Python, Ruby, Java, the C dispatcher and the C# generated sources. The
C# generator refused the new `sequence<u8>?` config field, as it
should; it now has a rule for it (standard base64, the C API's JSON
form). The three calls are platform operations on the local API: the
certificate binds the one identity every client shares, and the
signing helper takes a private key that must never cross a socket.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant