logs.gokuls.in

3 pull requests merged across 1 repo

Offline-Protocol/offline-protocol-sdk

, closes #471, closes #472. One commit per issue, each independently revertable.

#368: Python relay-answer prefixes

0.27.0 (#453) already corrected the prefix names and attribution at each call site. What was left is the issue's third item: Python held no copy of the relay-answer list and no pin.

  • relay_answer_prefixes.py holds the six prefixes. _inject_group_frame passes every actor through attributable_actor, so an answer reaches the core with no transport identity whatever the call site passes. This matches the central rule the Swift and Kotlin bridges already have.
  • test_relay_answer_prefixes.py pins the six literals. A new injection test proves an attributed answer is still stripped.
  • Every doc that counted three copies now counts four: prefixes.rs, the control-messages chapter, ADR 0004, C5, K7, S7, and the bridge headers. New contract P12 in docs/bridges/python.md.

Deployed apps: no behaviour change for a correct caller.

#471: DiagnosticEvent.level

  • The union is now 'debug' | 'info' | 'warning' | 'error' in types.ts and the RN README. Both platforms emit debug, at about 70 Swift and Kotlin sites.
  • The scan also found that Android's PeripheralGattServer emitted four diagnostics as warn, a spelling nothing declared. They are warning now.
  • react_native_diagnostic_levels_match_every_bridge reads the union and every level literal handed to a diagnostic sink in Swift, Kotlin and Python. It fails in both directions. It has floors on files and literals, so a scan that stops matching cannot pass on nothing.

Deployed apps: the type now matches what already arrives at runtime, but this is not automatically source-compatible. An exhaustive switch or a Record keyed by the level needs a debug entry. A filter comparing against 'warn' for the four GATT diagnostics should compare against 'warning'.

#472: setInterest before start() on React Native

The engine's start-up exchange offers every held space with the interest in force at that moment, and narrowing never deletes what a wider exchange pulled in. React Native's start() opens storage and starts the engine in one call. So the documented "before start()" was rejected natively, and "after start()" let the first exchange ask for everything.

  • DataStore.setInterest holds a declaration made before start(). start() applies it after MLS initialization and before the native start. After that, calls go straight to native.
  • A refused pattern rejects start() with an error naming the space and carrying the native code. The engine is not started, because starting would replicate the space whole.
  • When the data layer is off, the held call is dropped with a warning and start() proceeds. This is checked with a probe, because the engine validates the space name before it checks the layer.
  • destroy() discards the hold. It does so only for an instance that created an engine, since the state is module-wide.
  • Both native destroy() paths now release the data store. It holds a strong reference to the engine, so data calls after destroy() reached the stopped engine instead of rejecting, and on Android the engine stayed alive until garbage collection.
  • Docs updated: data.md, the API reference, the RN integration guide, UPGRADING section 23, and a C7 row. A note in docs/bridges/python.md says that ProtocolManager still runs its first exchange under the default interest. That follow-up is not fixed here.

Deployed apps: an invalid pattern declared before start() used to reject that call and now rejects start(). A call after start() is unchanged. An app that worked around 0.27.0 by calling after start() can move the call back.

Validation

  • cargo clippy --workspace -- -D warnings, cargo test --workspace --lib, RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps, cargo fmt --all -- --check: all clean.
  • Python: the full suite passes (808 passed, 4 skipped).
  • npm run test:js: all 11 harness files pass. The new data-interest-order.test.js has 12 cases.
  • iOS: the CI swiftc -typecheck step, which includes OfflineProtocolModule.swift, passes locally. A negative control proved the run was not vacuous.
  • Android: the module compiles and 556 unit tests pass in a clean copy. The 10 PeerStreamFramingTest failures there come from the copy living outside the repository, where the test cannot find its vector file.
  • Mutation-tested: each literal pin, the central attribution rule, the level guard in three directions, the start ordering (JS harness and Rust guard), the destroy reset and its scoping, the array copy, and the layer-off branch and probe. Every mutant fails a test.
  • Not run: no device or simulator run of the start ordering or the native destroy change.

What this does

mls_mark_key_package_synced deleted a key package's record and left its private init key in the MLS provider. Keeping the key was right, because a Welcome built from the uploaded copy has to open. Deleting the record was not, because the record is the only thing that carries expiry. A published package nobody claimed kept its key for the life of the install (#367).

  • Marking keeps the record and sets synced on it. Nothing has ever set that field, which has been on the bundle since the first MLS commit. The package now expires, and its key is destroyed past the 7-day grace window like any other.
  • A synced package is never handed out. The push path and the peer-less getter both skip it, through the same predicate that already skips a Nostr slot package. A stranger may hold it, and pointing a peer at it too is the shared-key failure ADR 0012 exists to prevent.
  • The pending list holds only packages the app may publish: unclaimed, unreserved and unsynced. It used to return every live package, so an app following the documented upload loop would have marked the engine's own slot and push packages synced too.
  • Marking an unknown, expired or consumed id does nothing. Marking a package the push path already gave a peer is allowed and logged.
  • A source guard refuses any new record-only delete of a key package outside the loader and the purge.

The second commit fixes a separate bug on the same methods. The React Native wrappers read createdAt and isSynced, but both native bridges send createdAtMs, expiresAtMs and synced. JavaScript has always received undefined for both fields. The three key package wrappers now share one mapper, and MlsKeyPackage gains expiresAt.

ADR 0012's gap section, the session lifecycle state machine, the MLS integration guide, the React Native method table and the changelog are updated.

Verification

  • cargo clippy --workspace -- -D warnings, cargo test --workspace --lib, cargo fmt --all -- --check and rustdoc under -D warnings pass. Doctests were not run locally.
  • The React Native typecheck and npm run test:js pass, including the new key-package-mapping.test.js.
  • Mutation: four mutants of the Rust fix and one of the old TypeScript mapping were applied in turn. Each fails at least one new test.
  • The UDL is unchanged, so no bindings were regenerated and the local API guard is untouched.
  • No device run. The change is in the MLS crate and the TypeScript layer, and the native bridges are unchanged.

Not in this PR

  • The website docs repository's mesh-sdk/methods.mdx still describes the old upload loop.
  • The push path can claim a package between an app's list call and its mark. The window is one discovery event wide, it predates this issue, and the outcome is the logged shared-package case above.

Closes #367.

What this does

#496 fixed what #315 reported: the four wheels were all py3-none-any, and the Linux floor is now read from the library. This PR closes the rest of that class. Every claim in a wheel's platform tag is now checked against the library inside it.

  • The glibc floor counts glibc's ABI markers. These are version needs that no symbol carries, so the old --dyn-syms grep could not see them. A library linked with packed relative relocations needs GLIBC_ABI_DT_RELR, which is glibc 2.36 whatever its symbols say. GLIBC_ABI_GNU2_TLS and GLIBC_ABI_DT_X86_64_PLT mean 2.42. The numbers match auditwheel's manylinux policies. A marker the script does not know is refused rather than ignored, and so is GLIBC_PRIVATE.
  • The macOS tag is checked against the library. 14.0 stays the chosen floor: it is the oldest arm64 runner the suite runs on. A library whose minimum macOS is newer than that is refused. So is a universal binary or one built for iOS.
  • The architecture is read from each library's header (ELF, Mach-O, PE) instead of trusted from the matrix row.

None of these markers appear in today's libraries, so the 0.27.0 tags were right. The danger is a toolchain or runner-image change: the tag would go wrong while CI stayed green, because the release runner's pip is newer than the claim.

Verification

  • scripts/tests/test-build-wheel.sh passes 28 checks in an ubuntu:24.04 amd64 container. They include a real DT_RELR library, tagged manylinux_2_36. Canned version listings cover the markers no toolchain emits on demand. Hand-built Mach-O and PE headers let the macOS and Windows checks run on the Linux runner.
  • Mutation: each of the 14 new guards was removed in turn, and each removal fails a test.
  • Rebuilding from the 0.27.0 dry run's real libraries (run 36853692220) gives the same four file names, so the packager and publish jobs see no change.
  • On arm64 Linux the DT_RELR fixture skips: binutils 2.42 ignores -z pack-relative-relocs on aarch64 until 2.43. It runs on the x86_64 Release Packaging job.
  • Not run yet: a workflow_dispatch dry run from this branch with version=0.27.0.

Not in this PR

The 2.34 glibc floor itself. It excludes Ubuntu 20.04, Debian 11 and RHEL 8. Lowering it means building the Linux libraries in a manylinux_2_28 container or with zigbuild, which is a separate decision.

Closes #315.