Skip to content

perf(mobile): sign events with native libsecp256k1 on iOS and Android - #8654

Merged
brow merged 3 commits into
mainfrom
mobile-native-secp256k1
Oct 28, 2026
Merged

brow merged 3 commits into
mainfrom
mobile-native-secp256k1

Conversation

@brow

@brow brow commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

Mobile signs every Nostr event (sends, relay AUTH, media auth headers, preference writes) in pure Dart on the UI isolate. On an iPhone SE (2nd gen, Profile build) one BIP-340 signature costs about 10.7 ms and deriving the public key another 5.6 ms, so a burst of signed writes drops frames.

This adds mobile/packages/buzz_secp256k1, a small FFI package that builds vendored bitcoin-core/libsecp256k1 v0.8.0 (ecdh, extrakeys, schnorrsig modules) through a Dart build hook, so the same C and the same Dart API ship on iOS and Android with no platform bridge code. All 13 nostr.Event.from signing sites now go through signNostrEvent, which keeps the nostr package's event-id serialization and moves only key derivation and signing to native code. Calls stay synchronous.

The package also exposes schnorrVerify and ecdhX; follow-up PRs move verification and the NIP-44 conversation key over. Incoming relay events are still not verified, unchanged by this PR.

Safety: the signing context is randomized once per isolate, secret keys and keypairs are wiped from the native scratch buffer after each call, aux randomness comes from Random.secure(), and the static verify context runs libsecp256k1's self test before first use.

Validation

  • Added tests: BIP-340 official vectors and native-vs-pure-Dart differential tests in the package; event_signing_test.dart signs with 64 random keys and checks pubkey, event id and signature against the existing nostr builder and verifier, plus malformed-key rejection.
  • Full mobile analyze and test suite pass locally.
  • Builds: iOS release (framework ~1.3 MB) and Android debug APK with the library for arm64-v8a, armeabi-v7a and x86_64 (~1.2–1.3 MB each).
  • On-device (iPhone SE 2nd gen, Profile, median per op): sign 10,736 µs → 41 µs, derive pubkey 5,580 µs → 16.5 µs, full signed event 10,744 µs → 65 µs. Event-id JSON and SHA-256 are 23 µs, so they stay in Dart.
  • Device, iOS (iPhone SE 2nd gen, Profile): buzz_secp256k1.framework embedded; the signed-in app authenticated to a real community relay, loaded channels and history, and paged older messages. No native-asset or secp256k1 errors in the console.
  • Device, Android (emulator, debug): APK contains libbuzz_secp256k1.so and loads it. Against a local mock relay that verifies with the pure-Dart nostr package, the app's NIP-42 AUTH, NIP-98 POST /query headers, and kind 20001/30078 events all verified.
  • Blossom GET auth, iOS: the dev relay returns 401 for unauthenticated /media/ GETs. From inside the running app, the production MediaGetAuthService.headersFor() header for a nonexistent hash got 404 (accepted, file missing), compared with 401 without it. The community has no relay-hosted image, so none was rendered.

buzz-review-completed (agent review, Codex and security review clean; device validation above; human app test not applicable under the mobile testing exception)

🤖 Generated with Claude Code

Add a buzz_secp256k1 package that builds vendored bitcoin-core/libsecp256k1
v0.8.0 (ecdh, extrakeys, schnorrsig) through a Dart build hook, and route
every nostr.Event.from signing site through signNostrEvent, which keeps the
nostr event-id serialization and moves pubkey derivation and BIP-340
signing to native code.

Co-authored-by: george <973be878edd6d4f79692a3827f20a6f7fe0ed0362f269ff6b6ba8867033d7d8e@sprout-oss.stage.blox.sqprod.co>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tom Brow <tomb@block.xyz>
@brow

brow commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-28T21:43:47.681824Z ad11110 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

The BIP-340 vectors and native-vs-Dart cross checks live in the package
and were not part of the mobile test run. Also drop a stale cost comment.

Co-authored-by: george <973be878edd6d4f79692a3827f20a6f7fe0ed0362f269ff6b6ba8867033d7d8e@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: Tom Brow <tomb@block.xyz>
@brow
brow marked this pull request as ready for review October 28, 2026 20:57
@brow
brow requested a review from a team as a code owner October 28, 2026 20:57
@github-actions github-actions Bot added the codex-security-review-current The posted Codex security review matches its recorded range. label Oct 28, 2026
@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

🔐 Codex Security Review

Note: This is an automated, security-focused review generated by Codex.
Use it as a supplement to human review; false positives are possible.

Scope

  • Exact PR diff: 0a362eef150ab16453993d442a8037baad766797...ad11110b49f4e3074e736eb21f943059970037d2
  • Model: gpt-6.1-sol

💡 Click "edited" above to see earlier reviews for this PR.


Review Summary

Overall Risk: NONE

No concrete security, correctness, or reliability findings in the authorized PR range.

Findings

No concrete security, correctness, or reliability findings were identified.

Notes

  • Review used read-only static inspection. Builds and tests were not executed, and vendored-source provenance was not independently verified against upstream.

Generated by Codex Security Review |
Requested by: @brow |
Workflow run

Creating the signing context randomizes it through the aux scratch slot,
so the first schnorrSign in an isolate overwrote the caller's auxRand
with zeros. Create the context before loading inputs.

Co-authored-by: george <973be878edd6d4f79692a3827f20a6f7fe0ed0362f269ff6b6ba8867033d7d8e@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: Tom Brow <tomb@block.xyz>
@brow

brow commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator Author

🤖 Security review finding (first signature discards auxRand): valid. Addressed in ad11110 by creating the signing context before loading inputs, with a fresh-isolate test on BIP-340 vector 1 that fails without the fix.

@brow

brow commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@github-actions github-actions Bot added codex-security-review-current The posted Codex security review matches its recorded range. and removed codex-security-review-current The posted Codex security review matches its recorded range. labels Oct 28, 2026
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Bravo.

Reviewed commit: ad11110b49

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@kalvinnchau kalvinnchau left a comment

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.

🤖 Reviewed ad11110: signing serialization, FFI buffer/context ownership, key cleanup, and iOS/Android build contracts. No P0–P2 findings. The inline header-dependency P3 is nonblocking. Reported validation: 27 package tests and 340 affected mobile tests passed; mobile analysis clean. Device builds/workflows were not independently exercised.

'$_vendor/src/precomputed_ecmult.c',
'$_vendor/src/precomputed_ecmult_gen.c',
'src/buzz_secp256k1_ecdh.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.

🤖 [P3] Declare vendored headers as build dependencies (nonblocking)

After an initial build, a header-only vendor update (for example src/modules/schnorrsig/main_impl.h) can reuse the old native library in incremental app/test builds. This hook declares four C sources but no header dependencies; pinned native_toolchain_c 0.17.6 registers sources and files under includes, rather than transitive C includes (lib/src/cbuilder/cbuilder.dart:221–236). Add includes: ['$_vendor'] or explicitly register the headers so changes invalidate the asset cache. Fresh builds are unaffected. Confirmed from source contracts; not reproduced with a native build.

@brow
brow merged commit 4ff02bf into main Oct 28, 2026
96 checks passed
@brow
brow deleted the mobile-native-secp256k1 branch October 28, 2026 23:28
brow added a commit that referenced this pull request Oct 29, 2026
Follow-up to #8654, from [Kalvin's review
note](#8654 (comment)).

`native_toolchain_c`'s `CBuilder` registers only the listed C sources
(and files under `includes`) as build-hook dependencies. The
`buzz_secp256k1` hook lists four sources and no headers, so after an
initial build a header-only vendor update (for example
`src/modules/schnorrsig/main_impl.h`) could reuse the old library in
incremental app and test builds. The hook now also registers every `.h`
file under the vendored tree. It does not add include paths, so
compilation is unchanged.

### Validation

- With the change, editing the content of
`third_party/secp256k1/src/modules/schnorrsig/main_impl.h` makes the
next `dart test` rebuild `libbuzz_secp256k1.dylib`. Without it, the
library is reused unchanged.
- The hook output now lists all 62 vendored headers as dependencies.
- Package tests pass (27).

buzz-review-completed (Codex and security review clean; agent
verification above; build-only change, no app behavior to test)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Carl, an automated reviewer, commenting via Wes's GitHub account.

Post-merge review at ad11110: no findings.

  • All 66 vendored files match bitcoin-core/secp256k1 v0.8.0 byte for byte, and the tarball SHA-256 matches VENDORED.md.
  • The build hook defines match upstream defaults, including the precomputed table parameters. The ECDH shim keeps the constant-time path.
  • Secret and keypair scratch are wiped, the signing context is randomized once per isolate, and aux randomness is fresh per signature.
  • All 13 migrated signing sites keep their inputs and the nostr event-id serialization.

Validation gap: device timings and the iOS/Android runtime checks are taken from the PR description and were not reproduced.

ravarora2 added a commit that referenced this pull request Oct 29, 2026
…-handler

* origin/main:
  feat(mobile): unify Messages navigation with Solo and Team filters (#8649)
  Smooth profile avatar editor transitions (#8640)
  chore(desktop): point `just staging` at the new Block staging relay (#8623)
  ci: repeat changed tests ten times on pull requests (#8613)
  fix(mobile): rebuild buzz_secp256k1 when vendored headers change (#8664)
  fix(push-gateway): log which App Attest certificate check failed (#8671)
  fix(mobile): resume reconnect replay from the last live point and spread reconnects (#8659)
  fix(mobile): push every message in your own Solo chats (#8615)
  perf(mobile): sign events with native libsecp256k1 on iOS and Android (#8654)
  fix(relay): revoke invitations and restrict claims (#8237)

Signed-off-by: Ravneet Arora <rarora@squareup.com>
brow added a commit that referenced this pull request Oct 29, 2026
Follows #8654.

Deriving an x-only public key from a secret key with
`nostr.Keys(secret).public` costs about 5.6 ms in pure Dart on an iPhone
SE (2nd gen, Profile). The app does it in 10 places, including relay
config, the signed-event relay's `pubkey` getter, push lease checks,
status updates, and the settings identity row's `build` method. This
routes all of them through `derivePublicKeyHex` from #8654, which takes
16.5 µs on the same device.

Behavior is unchanged for valid keys: output is the same lowercase hex.
For a malformed or out-of-range key the thrown type changes from nostr's
`InvalidKeyException` to `ArgumentError`; every affected site either
catches all errors or rethrows its own `FormatException`/`StateError`,
and nothing in `lib` or `test` matches on the old type.

Sites that need a full `nostr.Keys` object (key generation,
`AccountIdentityStore`) are unchanged.

### Validation

- `derivePublicKeyHex` is checked against `nostr.Keys` for 64 random
keys in #8654's `event_signing_test.dart`.
- Full mobile analyze and test suite pass on this branch after rebasing
onto main, including the package's own tests.
- iPhone 12 mini, Profile build of this stack (with #8656 and #8657 on
top): paired to the internal Buzz community through Desktop; relay auth,
identity row, avatar, and channel/DM/inbox loading all worked under the
derived key.

buzz-review-completed (agent review, Codex and security review clean;
device validation above; human app test not applicable under the mobile
testing exception)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: george <973be878edd6d4f79692a3827f20a6f7fe0ed0362f269ff6b6ba8867033d7d8e@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
brow added a commit that referenced this pull request Oct 29, 2026
`ecdhSharedSecret`, the input to every NIP-44 conversation key
(preferences, read state, reminders, themes, mutes, stars, sections,
sort, pairing, agent observer), used pointycastle's BigInt point
multiplication. That costs about 5.9 ms per call on an iPhone SE (2nd
gen, Profile) and is variable time in the secret scalar. It now calls
libsecp256k1's constant-time `secp256k1_ecdh` through `ecdhX` from #8654
(39.8 µs on the same device), returning the same raw x-coordinate NIP-44
requires. The decoded secret key bytes are zeroed after the call.

Invalid keys now throw `ArgumentError` instead of pointycastle's errors;
callers already treat any throw as a failed derivation.

### Validation

- Added tests: `ecdh_test.dart` compares the native result with the
previous pointycastle algorithm for 32 random key pairs, checks the
NIP-44 v2 `get_conversation_key` vector (secret 1, peer pubkey of secret
2), and checks rejection of a zero secret, an off-curve public key and
wrong lengths.
- Full mobile analyze and test suite pass after rebasing onto main at
da1190e, including the desktop nostr-rs NIP-44 interop vector.
- On a device: an iPhone 12 mini Profile build of the full native stack,
connected to the internal relay.

buzz-review-completed

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: george <973be878edd6d4f79692a3827f20a6f7fe0ed0362f269ff6b6ba8867033d7d8e@sprout-oss.stage.blox.sqprod.co>

This branch was previously deployed

1 inactive deployment
codex-review — ad11110b Deployed Oct 28, 2026 by brow via Run Codex Security Review #10314
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

codex-security-review-current The posted Codex security review matches its recorded range.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants