feat: port payjoin core to no_std#1615
Conversation
Coverage Report for CI Build 27779438342Coverage decreased (-0.6%) to 84.654%Details
Uncovered Changes
Coverage Regressions56 previously-covered lines in 6 files lost coverage.
Coverage Stats
💛 - Coveralls |
|
Great to see this take off here. My biggest question is about |
benalleng
left a comment
There was a problem hiding this comment.
Looks like a good start, though there are some feature organizations I have some questions about.
249f524 to
04f34f6
Compare
853bb38 to
c0c2149
Compare
After reviewing the code, |
504491c to
082564e
Compare
69d72e6 to
37bb1e8
Compare
benalleng
left a comment
There was a problem hiding this comment.
Tested on a RPI Pico 2. It is a limited test that proves compilation and the ShortId round trip to begin the payjoin process.
$ sudo nix develop ~/rust-payjoin#embedded -c bash -c 'picotool uf2 convert --family absolute ~/payjoin-pico2-test/target/thumbv8m.main-none-eabihf/release/payjoin-pico2-test -t elf /tmp/main.uf2 && picotool load --ignore-partitions -x /tmp/main.uf2'
warning: Git tree '~/rust-payjoin' is dirty
Loading into Flash: [==============================] 100%
The device was rebooted to start the application.
$ cat /dev/ttyACM0
=== payjoin no_std (v2) on RP2350 ===
ShortId bytes: 4242424242424242
ShortId bech32m: GFPYYSJZGFPYY
round-trip ok: true
mailbox id bytes: 7f2f54ff94459f3a
mailbox id bech32m: 0UH4FLU5GK0N5
=== done ===I created this repo to demonstrate my steps to get here. https://github.com/benalleng/payjoin-pico2
|
I did add an additional target |
Thanks for testing this against the branch directly, and for putting together the payjoin-pico2-test repo as a reference. One note on target coverage: this is |
On |
|
Tested on a WeAct STM32F411CEU6 Black Pill (Cortex-M4F, thumbv7em-none-eabihf), the PR's declared CI target. $ sudo $(which dfu-util) -a 0 -s 0x08000000:leave -D target/thumbv7em-none-eabihf/release/payjoin-blackpill-test
dfu-util 0.11
Opening DFU capable USB device...
Device ID 0483:df11
Downloading element to address = 0x08000000, size = 134132
Erase [=========================] 100% 134132 bytes
Erase done.
Download [=========================] 100% 134132 bytes
Download done.
File downloaded successfully
Submitting leave request...
Transitioning to dfuMANIFEST stateThe test exercises ShortId round-trip and SHA256 → mailbox ID derivation on bare-metal, with no std. I created https://github.com/caarloshenriq/payjoin-blackpill-test to document the steps. |
The v1 feature required std unconditionally, even though the protocol itself has no networking or OS dependency: it is plain byte-in/byte-out request/response handling, making it the natural fit for the no_std embedded receiver/sender work in this branch. Port the v1 send and receive paths to alloc-only: - Change the v1 feature to depend on alloc instead of std. - Gate the handful of genuinely std-only APIs (PjUri construction, SenderBuilder::new) behind std, leaving SenderBuilder::from_parts and PjParam::parse as the no_std entry points. - Replace bitcoin's Psbt FromStr/Display, which pulls in std through bitcoin's base64 feature, with direct use of a no_std-configured base64 crate on top of PSBT's existing binary (de)serialization. Shared with the v2 receive path, which used the same std-only entry point. - Fix several imports and cfg gates that were tied to std without a real std dependency (FeeRate, Version, FromStr, format!, and the query-string parsing in Params::from_query_str). - Simplify from_query_str's query parsing to avoid a percent-decoding dependency that is itself std-only; BIP78 query parameters don't need percent-decoding in practice. - Add a no_std fallback for ResponseError parsing, since the well-known JSON error format requires serde_json (std-only); it reports a generic parse failure instead of the decoded reason.
Add payjoin/tests/e2e.rs with two independent round trip tests, split into cfg-gated submodules since v1 and v2 have different feature requirements: - v1: runs entirely in memory under the no_std-compatible alloc,v1 feature set. Drives the full sender and receiver typestate chain with no OHTTP or directory involved, since v1 is plain request/response bytes. This is the test that actually exercises the no_std surface end to end, not just at compile time. - v2: cannot run under that restricted feature set, since the sender side of v2 still requires v2-ohttp/std. Instead this proves that the receiver side, driven by a real OHTTP-encapsulated request from a real sender, walks the same typestate chain already verified to compile for thumbv7em-none-eabihf. Uses a small in-memory stand-in for the directory and OHTTP relay (decapsulate, route GET/POST against a single-message mailbox, re-encapsulate) instead of the real local servers tests/integration.rs already spins up, so it stays fast and dependency-free. Both tests have the receiver contribute a real input from its own UTXO before finalizing, rather than only fee-bumping the original PSBT, so they exercise an actual payjoin (combined inputs from both parties) and not just protocol plumbing. Wire both into contrib/test.sh: the v2 module runs as part of the --all-features e2e run, and v1 gets its own explicit --no-default-features --features alloc,v1 invocation, since that's the only place the no_std guarantee is actually exercised at runtime rather than just checked at compile time.
37bb1e8 to
641926c
Compare
Summary
This is a implementation of
no_stdsupport for thepayjoincrate,enabling its use on embedded devices
As discussed in #942, running payjoin logic on a hardware signer enables
stronger security guarantees: the device can verify the fallback transaction,
compare it against the payjoin proposal, and only sign previously-approved
inputs — without trusting the host machine.
Feature Architecture
A new
v2-stdfeature was introduced to separate the state machine logicfrom networking dependencies:
allocv2v2-stdv2+ networking (url,ohttp,hpke,bhttp,http)stdtokio,serde_json,bitcoin/base64Verified Build Targets
cargo build -p payjoin --no-default-features --features v2,alloccargo build -p payjoin --no-default-features --features v2,alloc --target thumbv7em-none-eabihf-p payjoincargo build -p payjoin --no-default-features --features v2,stdNotes for Reviewers
Please review commit by commit:
refactor: introduce no_std/alloc feature split in payjoin coreThe main structural change — replaces
std::withcore::/alloc::and gates std-only deps behind#[cfg(feature = "std")]or#[cfg(feature = "v2-std")].fix: gate v2 std-only code behind cfg featuresExtends gating to v2 send/receive and persist. Key decisions:
HasReplyableErrornow carriesfallback_txin both configs to preserve fallback through replay;MaybeSuccessTransition::deconstructusesSaveinstead ofSaveAndCloseon success.fix: update payjoin-ffi for no_std feature splitMinimal FFI updates to match new
AsyncSessionPersisterbounds.fix: restore OHTTP test constants and enable v2 feature in test utilsKEM,KEY_ID,SYMMETRICwere dropped upstream without updating internal tests. Restores them inpayjoin-test-utils/src/v2.rs.chore: update CI, lock files and flake for no_std targetsAdds
thumbv7em-none-eabihfto CI and ARM cross-toolchain to the Nix dev shell.AI Assistance
This implementation was developed with AI assistance (Claude, Anthropic).
Pull Request Checklist
Please confirm the following before requesting review:
AI
in the body of this PR.