mirror of
https://github.com/block/buzz.git
synced 2026-11-12 04:12:21 +00:00
## Summary Two pure-Dart crypto paths that run on the UI isolate during normal use: - **NIP-98 body hashes.** Every relay HTTP query (`RelaySessionNotifier._postForEvents`) hashes its request body for the NIP-98 auth header with pointycastle's `SHA256Digest`. This switches that hash, the media upload hash and the NIP-OA preimage hash to `package:crypto`'s `sha256`, which is already a dependency. The output is identical. - **Public key derivation.** `AccountIdentityStore.publicKeyForNsec` runs on every relay auth assertion and derived the key with the `nostr` package's pure-Dart curve math. It now uses the native `derivePublicKeyHex` (libsecp256k1) with the same validation. The default-agent writer now signs with the native `signNostrEvent` instead of `nostr.Event.from`. Left as they were: `nostr.Keys` construction for signing keys (nostr 2.0.0 always derives in Dart, with no way to pass a known public key), and `nostr.Keys.generate()` on the rare key-creation paths. ## Measurements - **Device** (iPhone 12 mini, Profile build, #4things captures): NIP-98 hashing took 17 to 24 ms of UI CPU per 10 to 20 s capture, mostly from channel notices queries. `publicKeyForNsec` took about 7 ms per relay auth assertion. - **Synthetic** (Mac, AOT, same output): SHA-256 of a 1 KB body takes 39.9 µs with pointycastle and 6.6 µs with `package:crypto`. 50 KB: 1,518 µs vs 293 µs. 1 MB: 29.6 ms vs 6.1 ms. - **Device A/B after the change** (iPhone 12 mini, Profile/AOT, Tom's Block community, two app resumes per build from the home screen, 15 s windows; A is `origin/main`3d26ed7, B is A plus this PR and [#8966](https://github.com/block/buzz/pull/8966)): NIP-98 signing per resume went from 42 / 50 ms to 9 / 13 ms of UI CPU, and all SHA-256 work per resume from 74 / 73 ms to 33 / 33 ms. Dart public-key derivation through `nostr.Keys` (left as is, see above) still takes 15 to 23 ms per resume. - **Same A/B on current `main`** (14037e3): pointycastle SHA-256 per resume went from 39 / 45 ms to 7 / 0 ms, and NIP-98 header building from 4 / 5 ms to 2 / 0 ms. `nostr.Keys` derivation: 12 to 17 ms per resume on both builds. ## Testing - New known-vector tests for the NIP-98 header hash: an empty body, `abc`, and the one-million-`a` FIPS vector. They also check the event's kind, method, signature and pubkey. - New `publicKeyForNsec` tests: the generator-point vector, matching the pure-Dart derivation, and rejecting invalid keys (scalar 0 and n, garbage, an npub). - These are correctness tests only. A timing test would be flaky, so none would fail if the old pure-Dart code came back. - `just mobile-check mobile-test` passes (pre-push hook). - Automated device run in place of manual testing: the B build above on the 12 mini, launched and resumed twice into a signed-in Block community, loading the channel list, channel notices and read state over authenticated relay queries (NIP-98 headers and relay auth assertions on every request). Not exercised on the device: media upload and the default-agent writer. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Tom Brow <tomb@squareup.com> Co-authored-by: Tom Brow <tomb@squareup.com> Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
71 lines
2.2 KiB
Dart
71 lines
2.2 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:buzz/shared/crypto/event_signing.dart';
|
|
import 'package:buzz/shared/relay/relay_session.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:nostr/nostr.dart' as nostr;
|
|
|
|
void main() {
|
|
final keys = nostr.Keys(
|
|
'b7e151628aed2a6abf7158809cf4f3c762e7160f38b4da56a784d9045190cfef',
|
|
);
|
|
|
|
Map<String, dynamic> decode(String header) {
|
|
expect(header, startsWith('Nostr '));
|
|
return jsonDecode(utf8.decode(base64.decode(header.substring(6))))
|
|
as Map<String, dynamic>;
|
|
}
|
|
|
|
String payloadTag(Map<String, dynamic> event) =>
|
|
(event['tags'] as List).firstWhere((t) => t[0] == 'payload')[1] as String;
|
|
|
|
test(
|
|
'payload tag is the lowercase SHA-256 hex of the body (known vectors)',
|
|
() {
|
|
final vectors = {
|
|
'': 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
|
|
'abc':
|
|
'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad',
|
|
};
|
|
for (final entry in vectors.entries) {
|
|
final header = buildNip98AuthHeader(
|
|
method: 'post',
|
|
url: 'https://relay.example/query',
|
|
bodyBytes: utf8.encode(entry.key),
|
|
nsec: keys.nsec,
|
|
);
|
|
final event = decode(header);
|
|
expect(payloadTag(event), entry.value);
|
|
expect(event['kind'], 27235);
|
|
expect(event['pubkey'], keys.public);
|
|
expect(
|
|
(event['tags'] as List).firstWhere((t) => t[0] == 'method')[1],
|
|
'POST',
|
|
);
|
|
expect(
|
|
verifySchnorrHex(
|
|
publicKey: event['pubkey'] as String,
|
|
messageHex: event['id'] as String,
|
|
signature: event['sig'] as String,
|
|
),
|
|
isTrue,
|
|
);
|
|
}
|
|
},
|
|
);
|
|
|
|
test('hashes bodies longer than one SHA-256 block identically', () {
|
|
// 1,000,000 repetitions of "a" (FIPS 180 test vector).
|
|
final header = buildNip98AuthHeader(
|
|
method: 'POST',
|
|
url: 'https://relay.example/query',
|
|
bodyBytes: List.filled(1000000, 0x61),
|
|
nsec: keys.nsec,
|
|
);
|
|
expect(
|
|
payloadTag(decode(header)),
|
|
'cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0',
|
|
);
|
|
});
|
|
}
|