Skip to content

chore: adopt Paykit v0.1.1-rc6 - #131

Draft
ben-kaufman wants to merge 5 commits into
masterfrom
chore/paykit-rc6
Draft

ben-kaufman wants to merge 5 commits into
masterfrom
chore/paykit-rc6

Conversation

@ben-kaufman

@ben-kaufman ben-kaufman commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Caution

Do not merge or deploy this until a Bitkit release with Paykit rc6 is out (synonymdev/bitkit-android#1803 and synonymdev/bitkit-ios#1237) and Bitkit users have updated.

rc6 writes hosted SDK state in a newer format. Once this server writes a Creator's state, Bitkit builds on Paykit rc5 or older, including Bitkit 3.1.0, can no longer load it, and there is no downgrade.

Summary

Move paykit-lib and paykit-sdk from the crates.io =0.1.0 release to the paykit-rs v0.1.1-rc6 tag (commit 9919198a8a810652945f7366defacae8c3135632), and adopt what the server should use from 0.1.0..rc6. Paykit release candidates are only published as Git tags, so the pin follows the locks-core pattern.

  • Pin: Cargo.toml uses the tag. The Cargo.lock diff is limited to the two Paykit packages and their new libc and pubky-common edges. pubky and pubky-testnet stay at =0.16.2, and pubky-noise stays at 0.1.0. The local Docker source script, its test and docs/local-locks-demo.md follow the pin. The demo build uses the tag's commit, so a moved tag cannot change it.
  • Late-fetched Acceptances (paykit-rs#234): rc6 no longer marks a payee's request invalid when the Acceptance was stored at or after the proposal expiry. It derives accepted, or proof_submitted after a Payment Proof, from the same stored events. A row the server stored as invalid_conflict for that reason then got a projection with equal source cursors and a new state, which the equal-cursor guard refused on every projection. That conflict failed status, drain and rotation reads for every invoice of the Creator. The projection now carries acceptance_recorded_at, and invalid_conflict may take accepted or proof_submitted at equal cursors only when that time is at or after the proposal expiry. Every other equal-cursor change still conflicts.
  • SSE-assisted private intake (paykit-rs#243): not adopted, see Out of scope. docs/architecture.md records why. PrivateIntakeModeConflict gets its own SDK failure label, and tests pin that such a refused receive fails the transport pass and never counts as an empty inbox.
  • Stored state (paykit-rs#243): rc6 reads hosted state written by 0.1.0 and writes SDK state blob version 4, which Paykit releases before rc6 cannot read. The README now says to confirm every other app on a Creator's identity, such as Bitkit, runs an rc6 or later Paykit build before deploying.
  • Proof size (paykit-rs#238): tests measure the largest Payment Proof a reader can send for the server's USDT method with payment_proof_event_len: 847 bytes with the server's UUID Payment References, under the 1000-byte limit.
  • Guarded preparation claim (paykit-rs#208): the SDK payment example and the reader wallet in the Allowance e2e test use claim_payment_request_for_preparation and check the claimed proposal.

Paykit changes since 0.1.0

Change Server
#243 SSE-assisted private intake Not adopted, see Out of scope
#243 stored SDK state migration Hosted state from 0.1.0 loads. Upgrade order documented. Checked across versions, see Verification
#268, server #128 TLS roots Already in the lockfile: webpki-roots 1.0.11, webpki-root-certs 1.0.11, rustls-webpki 0.103.16. Unchanged here
#237 sign-up and sign-in errors, signup_code_rejected, republish_identity Not needed. The server never signs up. Setup uses the external auth flow and import_session, whose errors did not change. The reader demo maps every sign-in failure to one result
#238 message size preflight, is_message_too_large Proposals were already measured at admission with the same serializer and limit, and an oversized proposal is still a permanent handoff failure. payment_proof_event_len is adopted for the USDT proof budget
#234 proposal_recorded_at, acceptance_recorded_at, payee attribution rule acceptance_recorded_at is adopted for late Acceptances. Bitcoin uses invoice-specific addresses. The USDT not-before bound is in #122
#234 late Acceptance no longer invalid Adopted
#213 accept_pending_allowance_proposals Left to #122, which adopts it and enables Allowance intake
#230, #232, #235 payment ledger and backup restore guidance Not needed. The server never pays, keeps no payment accounting, and does not export or restore SDK backups
#244, #248 grant observation and captured grant logout Not needed. The server never logs out Creator grants. A changed session is replaced by reauthentication
#252 Encrypted Link sends check write slots before publishing No API change. One extra HEAD per send. No server send path has a time budget it affects
#208 claim_payment_request_for_preparation Adopted in the example and the e2e reader wallet
#202, #203 combined scheduling and request reads Not needed. Transport maintenance must read pending outbound work after its receives, and lifecycle reads cover all counterparties
#233 public endpoint sync semantics Not needed. The server does not call sync_public_endpoints
#231, #249 block, contact removal and Private Payment List reports Not needed. The server does not block peers, remove contacts or publish Private Payment Lists
#210, #211 lock diagnostics and lease expiry Applies automatically, no API change
#241, #204, stateRevision Swift, Kotlin and React Native only

Protocol and integration impact

  • No wire format, message kind, storage path or capability change. A 0.1.0 wallet still receives requests the rc6 server sends.
  • Hosted SDK state moves to blob version 4 on the server's first write. Apps on a Paykit release before rc6 then fail on that state, and there is no downgrade. Deploy only after every app that uses a Creator's identity runs rc6 or later.
  • After the upgrade, a request stored as invalid_conflict only because its Acceptance was fetched after the proposal expiry reports accepted or proof_submitted instead of 409 conflict.
  • No exposed server struct, config key or HTTP contract changes. Package versions stay at 0.1.1-rc2.

Out of scope

  • SSE-assisted private intake. Its supervisor holds one lease over the whole identity-wide state. While it is live, polling any established Linked Peer of the identity fails with PrivateIntakeModeConflict on every SDK handle sharing that state, and only the supervisor's owner can drain what it buffers. Bitkit shares each Creator's state, and Paykit's Swift and Kotlin bindings do not expose the supervisor, so a server supervisor would stop the Creator's wallet from receiving. The status, drain and rotation contracts also need a receive made for each answer, while the supervisor reports stream state and an audit up to about a minute old. Adopting it would need a platform lifecycle contract in Paykit and a redesign of server freshness, not a version bump.
  • Allowance intake in production and the USDT proposal-time bound. feat: allowance intake in production #122 adopts accept_pending_allowance_proposals, lifts the allowance_intake guard and applies proposal_recorded_at to USDT. It pins rc5. Whichever PR merges second takes the other's pin.
  • No release preparation, tag, deploy or Docker image build.

Verification

Run with Rust 1.91.1 on the final source. The test suites ran on a tree that differs from the head only in two README lines.

  • cargo fmt --all --check: pass
  • cargo clippy --locked --workspace --all-targets -- -D warnings, and the same with --all-features: pass
  • cargo test --locked -p paykit-server -- --test-threads=1: pass, 500 tests
  • cargo test --locked -p paykit-server --example paykit-companion-auth -- --test-threads=1: pass, 14 tests
  • cargo test --locked -p paykit-server-e2e -- --test-threads=1 against a disposable PostgreSQL 16 container: pass, 243 tests, 3 ignored live tests
  • git diff --check: pass
  • scripts/test-prepare-local-docker-sources.sh in the Dockerfile's Debian runtime image: pass. The prep script also runs cleanly on the real Cargo.toml and Cargo.lock.
  • A scratch program outside this repository linked paykit-sdk 0.1.0 and rc6 against one local Pubky testnet. With 0.1.0 it linked a paykit-server Creator and a reader, proposed two requests, and had the server fetch one Acceptance after its proposal expiry, which 0.1.0 stored as InvalidConflict. rc6 then loaded that hosted state: both requests were Accepted, the late one with the same stream cursor, outbound cursor and event time and an acceptance_recorded_at after the expiry, and the link was still Linked. rc6 sent a third request that the 0.1.0 wallet received. After that write, 0.1.0 failed to load the Creator's state with a storage error. A 0.1.0 backup decoded in rc6 both from its frozen postcard form and from JSON.

New tests:

  • an_acceptance_fetched_after_proposal_expiry_leaves_invalid_conflict and invalid_conflict_stays_without_a_late_fetched_acceptance (domain)
  • projection_carries_the_acceptance_record_time and only_an_acceptance_fetched_after_expiry_moves_a_stored_invalid_request (projection)
  • a_stored_invalid_request_takes_the_state_derived_for_a_late_fetched_acceptance and a_stored_invalid_request_stays_invalid_without_a_late_fetched_acceptance (PostgreSQL)
  • a_peer_owned_by_sse_intake_is_a_named_receive_failure and a_peer_owned_by_sse_intake_is_neither_contention_nor_an_empty_inbox
  • the_largest_usdt_payment_proof_fits_one_private_message and the_usdt_payment_proof_budget_catches_a_proof_that_does_not_fit

Not run: the ignored live Pubky, Electrum and Marketplace harness tests, sdk-example/run.sh (the example compiles, and the e2e reader wallet runs the same guarded claim), and Docker image builds.

🤖 Generated with Claude Code

ben-kaufman and others added 5 commits November 6, 2026 11:57
Move paykit-lib and paykit-sdk from the crates.io 0.1.0 release to the
paykit-rs v0.1.1-rc6 Git tag (commit 9919198). Paykit release candidates
are only published as Git tags. The lockfile change is limited to the two
Paykit packages and their new libc and pubky-common edges. The TLS crates
stay at webpki-roots 1.0.11, webpki-root-certs 1.0.11 and rustls-webpki
0.103.16. The pubky and pubky-testnet pins stay at 0.16.2.

The only API change the server meets is the two new PaymentRequestRecord
record time fields in test fixtures.

Point the local Docker source substitution and the local demo build at
the tag and its commit, and document that this Paykit release stores
hosted state in a format earlier Paykit releases cannot read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paykit before v0.1.1-rc6 marked a payee's Payment Request invalid when
it stored an otherwise-valid Acceptance at or after the proposal expiry,
such as an in-time Acceptance the server fetched late. v0.1.1-rc6 records
that Acceptance and derives accepted, or proof_submitted once a Payment
Proof arrived, from the same stored events.

A lifecycle row stored as invalid_conflict for that reason then meets a
projection with the same source cursors, a new state and a new state
event. The equal-cursor guard refused it with a conflict on every
projection, which failed status, drain and rotation reads for every
invoice of that Creator.

Carry the SDK's acceptance_recorded_at in the lifecycle projection and
let invalid_conflict take accepted or proof_submitted at equal cursors
only when the Acceptance was recorded at or after the proposal expiry.
Every other equal-cursor change still conflicts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paykit v0.1.1-rc6 adds opt-in SSE-assisted private intake for Rust
runtimes. The server keeps polling. The intake supervisor holds one
lease over the whole identity-wide state, polling every established
Linked Peer of the identity fails while it is live, and only its owner
can drain what it buffers. Bitkit shares each Creator's state and its
Swift and Kotlin bindings can only poll, so a server supervisor would
stop the Creator's wallet from receiving. Status reads also need a
receive made for the answer, not the supervisor's last audit.

Name the PrivateIntakeModeConflict error in SDK failure labels, and pin
that a receive refused for that reason fails the transport pass and is
never read as an empty inbox.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A payee must budget for a proof profile before offering it. Measure the
largest Payment Proof a reader's wallet can send for the server's USDT
method with paykit_sdk::payment_proof_event_len from v0.1.1-rc6, using
the longest App ID, an Allowance ID, the ERC-20 hash and signature
lengths and a 78-digit receipt log index. It is 847 bytes with the
server's UUID Payment References, under the 1000-byte limit. The same
proof with a 256-character reference does not fit, which the second
test pins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paykit v0.1.1-rc2 added claim_payment_request_for_preparation, which
rejects a blocked counterparty before App Registry access and rechecks
that in the claim transaction. Paykit's guidance is to use it before
preparing payment and to compare the claimed proposal with the approved
terms.

The local SDK payment example and the reader wallet in the Allowance
end-to-end test now use it and check that they claimed the proposal
whose terms they evaluated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant