Skip to content

Slatepack: node-independent manual transaction transport#2076

Open
Maxnflaxl wants to merge 10 commits into
masterfrom
feat/slatepack
Open

Slatepack: node-independent manual transaction transport#2076
Maxnflaxl wants to merge 10 commits into
masterfrom
feat/slatepack

Conversation

@Maxnflaxl

@Maxnflaxl Maxnflaxl commented Jul 18, 2026

Copy link
Copy Markdown
Member

Summary

Adds Slatepack, an armored, copy-pastable transport for Beam transaction negotiation that needs no SBBS relay or node connectivity. It revives the node-independent exchange that Beam lost when the cold-wallet feature was removed in Jan 2020 (973d826d5). Closes the long-standing request in #593.

A Slatepack is an ASCII-armored envelope:

BEGINSLATEPACK. <base58 body, whitespace-grouped>. ENDSLATEPACK.

The body is version(1) + type(1) + payload + checksum(4), base58-encoded. It is safe to paste into chat, email, a file, or a QR code, and a damaged or wrong blob fails fast on the checksum / version / type discriminators.

Design

Queued-message endpoint, not Grin-style explicit slate states. A Slatepack wraps one internal negotiation message; importing one feeds it through the same consumer path an SBBS message takes, so the entire existing transaction machinery (simple, max-privacy, offline/voucher) works over the new transport by construction — no duplicated negotiation plumbing.

  • TxParameterID::ManualTransport (public param 119, bool) flags a transaction for manual routing. It is whitelisted in IsTxParameterExternalSettable and propagated to the peer, so the peer also routes its replies manually instead of leaking them back onto SBBS.
  • BaseMessageEndpoint::AcceptsMessage is a new routing hook: the SBBS endpoint skips ManualTransport transactions, and SlatepackEndpoint handles only those — both endpoints run at once without duplicating traffic.
  • SlatepackEndpoint queues each outgoing negotiation message and surfaces it as an armored string via a callback; a pasted Slatepack is decrypted for a preview first (amount / fee / addresses) and only handed to the wallet on explicit confirm.

Security notes

  • The armor adds no crypto of its own. The payload is the same SBBS-encrypted ciphertext produced by the normal proto::Bbs::Encrypt path, so a Slatepack posted in a public channel reveals only its type and size.
  • The network byte is deliberately omitted from the armor (matches SBBS, which carries no network discriminator).

Testing

  • slatepack_test — armor round-trip, whitespace tolerance, and rejection of truncated / checksum-corrupted / wrong-version / wrong-type blobs.
  • slatepack_exchange_test — two wallets complete a full S1/S2 negotiation with no SBBS, exchanging Slatepacks only.

Follow-up

The desktop UI ships as a separate PR against beam-ui that bumps the beam submodule to this branch's merge commit. That PR must not merge until this one does.

Adds TxParameterID::ManualTransport (private, never serialized) and a
virtual BaseMessageEndpoint::AcceptsMessage hook. The SBBS endpoint now
skips transactions flagged for manual transport, so they can be routed
over the upcoming Slatepack armored transport instead. Groundwork for
reviving offline transaction exchange.
Adds slatepack::Armor/Unarmor - a Slatepack-style ASCII-armored envelope
(BEGINSLATEPACK. ... .ENDSLATEPACK.) wrapping a version+type header, an
arbitrary payload, and a 4-byte double-SHA256 error-detection code, base58
encoded and whitespace-grouped for readability. Tolerant of surrounding
text and reflowed whitespace on parse. Includes the TxNegotiation payload
(recipient WalletID + SBBS ciphertext) and its serialization.

The codec is deliberately dependency-free (no Rules/global state) so it
unit-tests against wallet_core alone. Network discrimination is handled
downstream by channel matching, not an armor-header byte.
An IWalletMessageEndpoint that routes ManualTransport-flagged transactions
off SBBS: outgoing negotiation messages are queued and surfaced as armored
Slatepack strings via a callback, and Inject() feeds a pasted Slatepack back
through the standard ProcessMessage path (channel match + decrypt + hand to
the negotiator). Mirrors the WalletNetworkViaBbs structure (own-address
subscription, address-change observer) minus the node/BbsProcessor parts,
reviving the deleted cold-wallet endpoint pattern with armored strings in
place of DB shuttling.
… exchange test

The integration test exposed that a private (>=128) ManualTransport flag
never crosses the wire, so the receiver's side of a manual tx fell back to
SBBS for its S2 reply - the exchange only half-avoided the node. Fix:
 - move ManualTransport to a public tx parameter (119),
 - include it in the negotiation message when set (simple_transaction),
 - whitelist it as externally-settable so the peer adopts it and routes
   its own replies via Slatepack too.

Adds slatepack_exchange_test: two node-connected wallets complete a simple
transfer entirely over armored Slatepacks with no SBBS endpoint carrying the
negotiation, asserting both S1 and S2 crossed the manual transport and the
funds moved.
Registers a SlatepackEndpoint alongside the SBBS one in the live wallet
client; outgoing manual-transport negotiation messages surface via a new
onSlatepackReady(txID, armored) callback, and a pasted Slatepack is fed back
in through the async importSlatepack() -> onSlatepackImportResult(ok, error).
This is the bridge the desktop UI drives.
Import decrypts and previews a Slatepack without continuing the transaction;
the user confirms (Send) or discards (Cancel). Harden SlatepackEndpoint against
cancel/failure notifications and ProcessStoredMessages replays, and report a
decrypted tx summary for the UI.
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