perf(mobile): verify owned-agent registrations natively (#8938)

## Summary

`loadOwnedAgentDirectory` checks the signature of each kind:30177 agent
registration before it trusts the registration's `model`. It did this
with `nostr.Event.fromJson`, which runs the `nostr` package's pure-Dart
Schnorr code on the UI isolate.

This change uses `verifiedNostrEventFromJson`, which the app's other
verification sites already use. It makes the same timestamp, event id
and BIP-340 signature checks with the bundled libsecp256k1. It also
requires lowercase hex for the pubkey and signature, which relay events
already use.

A registration that fails verification still lists its agent without a
model, as before.

## Measurements

- **Synthetic** (Mac, `flutter test`, 20 signed registrations, warm):
pure Dart 163 ms, native 2.6 ms.
- **Device** (iPhone 12 mini, Profile build, first channel open after
launch, first 5 s; runs not matched): `loadOwnedAgentDirectory`
inclusive CPU was 71 ms before (69 ms in `Event._assertValid`) and 2 ms
after.

## Testing

- New test: a registration with a wrong signature still lists the agent,
with no model. The correctly signed registration keeps its model.
- `just mobile-check mobile-test` passes (pre-push hook).

🤖 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>
This commit is contained in:
Tom Brow
2026-11-03 08:00:26 -08:00
committed by GitHub
parent cad7f1d53d
commit eee75a1c7b
2 changed files with 40 additions and 2 deletions

View File

@@ -1,8 +1,9 @@
import 'dart:async';
import 'dart:convert';
import 'package:nostr/nostr.dart' as nostr;
import 'package:hooks_riverpod/hooks_riverpod.dart';
import '../crypto/event_signing.dart';
import '../crypto/nip_oa.dart';
import '../mentions/agent_identity_provider.dart';
import '../relay/relay.dart';
@@ -99,7 +100,9 @@ Future<List<OwnedAgentDirectoryEntry>> loadOwnedAgentDirectory(
String? model;
try {
final registration = registrations[event.pubkey]!;
nostr.Event.fromJson(jsonEncode(registration.toJson()));
// Native libsecp256k1 verification; the pure-Dart codec took
// several milliseconds per registration on the UI isolate.
verifiedNostrEventFromJson(jsonEncode(registration.toJson()));
final data = jsonDecode(registration.content);
if (data is Map && data['model'] is String) {
final value = (data['model'] as String).trim();

View File

@@ -3,6 +3,7 @@ import 'package:buzz/shared/relay/relay.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:nostr/nostr.dart' as nostr;
import '../../helpers/remote_agents_fixture.dart';
NostrEvent registration(
@@ -137,6 +138,40 @@ void main() {
]);
});
test('a registration whose signature fails keeps the agent without its '
'model', () async {
final signed = registration(
fakeAgent,
content: '{"name":"Remote","model":"Claude Sonnet"}',
);
// Same id and content, but another key's signature over the id.
final forged = NostrEvent.fromJson({
...signed.toJson(),
'sig': nostr.Event.from(
kind: 1,
content: 'other',
secretKey: fakeAgentKeys.secret,
).sig,
});
final profiles = [
signedAgentProfile(
fakeAgentKeys,
fakeOwner,
content: '{"name":"Remote"}',
),
];
Future<List<OwnedAgentDirectoryEntry>> load(NostrEvent event) =>
loadOwnedAgentDirectory(
fakeOwner.public,
(filter) async => filter.kinds.contains(30177) ? [event] : profiles,
);
expect((await load(signed)).single.model, 'Claude Sonnet');
final found = (await load(forged)).single;
expect(found.pubkey, fakeAgent);
expect(found.model, isNull);
});
test('a registration is listed only with a type runtime and a name', () {
final agent = fakeAgent;
final cases = <(String, NostrEvent, bool)>[