multi: board into custom policies + client-signed VTXO tree - #1043
Draft
Roasbeef wants to merge 13 commits into
Draft
multi: board into custom policies + client-signed VTXO tree#1043Roasbeef wants to merge 13 commits into
Roasbeef wants to merge 13 commits into
Conversation
In this commit, we extend BoardRequest with an optional vtxo_policy_template and a companion pk_script so a client can board directly into a VTXO owned by a custom arkscript policy, instead of boarding into the standard collaborative shape and converting with a follow-up refresh. The motivating case is an owner key held outside the daemon, such as an aggregate FROST key: the template governs ownership while the operator remains the seal-time fee authority. An empty template preserves the legacy behavior, and the pinned script, when supplied, must match the one the template derives.
In this commit, we add the ListPendingTreeSigningRequests and SubmitTreeSignatures RPCs, along with their message set and the TreeSigningRound enum, to let an external party drive the MuSig2 VTXO-tree signing ceremony for a cosigner key it controls off-box (for example an aggregate FROST key). Each request captures one round of the two-round ceremony for one transaction session: the NONCE round asks for a fresh public nonce, and the PARTIAL_SIG round asks for a partial signature over the given sighash under the operator-aggregated combined nonce. The private key never enters the daemon; it only ferries bytes. The gateway config gains the matching REST routes so the new methods are reachable over both transports.
In this commit, we regenerate the gRPC, REST-gateway, and mailbox-RPC stubs (and the devrpc command registry) for the board custom policy fields and the new external tree-signing RPCs added in the preceding two commits. Generated by make rpc; no hand edits.
In this commit, we add migration 000016, which extends the pending_board_intents outbox detail table with a nullable vtxo_policy_template and pk_script column, and update the upsert/list queries to carry them. The columns are nullable so intents persisted before this migration keep working and a NULL template selects the standard collaborative policy. The board intent is written to the outbox before the round adopts it, so a client that boards into a custom-owned VTXO and then restarts must replay the same policy rather than silently re-boarding into the standard shape; persisting the template here is what makes that restart-safe.
In this commit, we regenerate the type-safe query layer and the schema snapshot for the board intent custom policy columns added in the preceding commit. Generated by make sqlc; no hand edits.
In this commit, we carry the custom VTXO policy from the BoardRequest down through the wallet actor, the pending-intent outbox, and restart replay. BoardRequest and BoardIntentPayload gain the policy template and pinned script, TriggerBoardMsg forwards them to the round actor, and the pending-intent store reads and writes the new typed columns. The intent-ID digest is extended with the length-prefixed policy bytes so two boards that differ only by their policy hash to distinct ids and cannot upsert over each other in the outbox. On startup the board replayer carries the persisted policy into the self-issued BoardRequest, so a crash between admission and round seal re-boards into the same custom-owned output rather than the standard shape.
In this commit, we teach the round actor to build a boarded VTXO output from a caller-supplied policy template when the board trigger carries one. buildCustomBoardVTXORequest mirrors the custom-refresh output path: it derives no owner key and registers no owned script, because the policy's owner is external to this daemon, and leaves the owner and client keys zero while the FSM still assigns the ephemeral MuSig2 tree-signing key at registration. When no template is present the actor keeps synthesizing the standard collaborative policy with a freshly derived owner key.
In this commit, we validate an optional board policy template at the RPC boundary and forward it to the wallet actor. validateBoardPolicyTemplate decodes the template and checks it against the operator's terms so the operator co-signs every collaborative leaf and every exit leaf meets the CSV floor, and confirms a pinned pk_script matches the template. A pinned script without a template, or a malformed or unsafe policy, is rejected as InvalidArgument rather than surfacing later as a round failure. The template governs ownership only; the operator remains the seal-time fee authority under the #270 handshake.
In this commit, we cover the board custom policy path at every layer it threads through. The round actor test asserts a custom-policy board builds its output from the template verbatim, deriving no owner key and still assigning the ephemeral tree-signing key. The wallet tests assert the policy survives outbox persistence and restart replay and that two boards differing only by policy hash to distinct intent ids. The store test round-trips the template and pinned script through the 000016 migration columns, and the RPC test exercises the validation boundary.
In this commit, we add an ExternalTreeSigner flag to VTXORequest. It marks a VTXO whose MuSig2 tree-signing (cosigner) key lives outside this daemon, so the round FSM must not derive a wallet key for it and must route its nonce and partial-signature production to an external party rather than the local wallet signer. When set, the signing key's public key is the external cosigner key and its locator is ignored, because the key is not wallet-derivable.
In this commit, we add a daemon-side input.MuSig2Signer that performs no MuSig2 cryptography itself. It stands in for the wallet signer for one external cosigner key and ferries nonce generation and partial-signature production to an ExternalTreeSignerBackend, so the key material never enters this daemon. The proxy identifies the cosigner by public key rather than a wallet key locator, since an external aggregate key is not wallet-derivable, and it captures the operator-aggregated combined nonce via MuSig2RegisterCombinedNonce so round two can carry it to the external party. The signing executor selects this proxy per VTXO when the signing key is marked external, wired through the round config and FSM environment. When a VTXO is not marked external, nothing changes: the wallet signs its tree path as before.
In this commit, we add a daemon-local broker that exposes the round FSM's external tree-signing callbacks to an outside party over RPC. When a VTXO's cosigner key is external, the FSM's proxy signer calls the broker, which parks each call as a pending request, surfaces it through ListPendingTreeSigningRequests, and unblocks it when SubmitTreeSignatures supplies the material. The broker blocks the FSM twice, once for the nonce and once for the partial signature, giving the two rounds distinct request ids so they cannot collide. The broker performs no MuSig2 cryptography: it validates the submitted bytes structurally and relies on the operator's aggregated tree signature, which the FSM already verifies, to reject a bad partial signature downstream. Its state is deliberately daemon-local, so a restart abandons in-flight rounds rather than resuming a stale transcript. The broker is wired onto the server, granted macaroon permissions, and exposed through the REST client.
In this commit, we cover the external tree-signing path. The proxy tests assert it ferries the external party's nonce and partial signature through the two MuSig2 rounds, carries the sighash and aggregate nonce, keeps the session id stable across rounds, and rejects the operations the client never performs. The broker tests drive it exactly as the proxy does, over its list and submit surface, covering the two-round round-trip, distinct request ids, timeout, and idempotent resubmit. The integration test proves the whole daemon-side path over a real VTXO tree: the signing executor drives the proxy, which routes to a backend running a real signer for the cosigner key, and the externally produced nonces and partial signatures cover exactly the transactions on the cosigner's path.
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
In this PR, we make it possible to run a VTXO whose owner (and, if you
want, whose tree-signing key) lives entirely outside the daemon, driven
over the gRPC surface. The motivating case is a client that wants their
VTXO/OOR public key to be an aggregate FROST key: the group does its own
key derivation and signing off-box, and the daemon only ever sees a
pubkey and a finished signature. FROST is invisible to the rest of the
system here, since the group produces an ordinary BIP-340 Schnorr
signature that's indistinguishable on-chain from a single-signer one.
The work splits into two independent parts. Part one lets a client board
straight into a custom-owned VTXO, skipping the board-then-refresh dance.
Part two adds a seam (and two RPCs) so the client can also drive the
covenant tree-signing MuSig2 ceremony for an external cosigner key. The
two parts share only the proto regeneration; everything else is
separable, so see each commit message for the detailed w.r.t the
incremental changes.
Boarding into a custom policy
Today
Boardalways produces a standard 2-of-2 collaborative VTXO witha freshly derived owner key, so getting a custom-owned coin means
boarding and then converting with
RefreshCustomVTXOs. In this PR we addan optional
vtxo_policy_template(and a companionpk_script) toBoardRequestso the boarded outputs adopt that policy verbatim.The template is validated at the RPC boundary against the operator's
terms (operator on every collab leaf, every exit leaf gated by at least
the CSV floor), so a malformed or unsafe policy is rejected up front
rather than surfacing later as a round failure. The template governs
ownership only; the operator stays the seal-time fee authority under the
#270 handshake. The interesting plumbing is the outbox:
Boardisrestart-replayed, so the policy has to survive persistence. We add
migration
000016(nullable columns, so old intents keep working),thread the template through
BoardIntentPayload, and extend theintent-ID digest with the length-prefixed policy bytes so two boards that
differ only by policy can't collide and upsert over each other. On the
round side, a custom-policy board builds its output the same way a custom
refresh does: no owner key is derived, and the FSM still assigns the
ephemeral tree-signing key at registration.
Client-signed tree
There are two independent keys on a VTXO, and only one of them is ever
"yours" to make external: the persistent owner key (family 44), which
signs forfeit/OOR/exit, and the ephemeral per-round tree-cosign key
(family 45), which enforces the sweep covenant. The owner key is fully
handled by the custom policy above plus the existing OOR/forfeit
external-signature seams. This part is about the tree-cosign key, for the
case where you want the external group to control that too.
The core is
externalMuSig2Signer, a daemon-sideinput.MuSig2Signerthat does no MuSig2 crypto itself. It stands in for the wallet signer for
one external cosigner key and ferries the nonce and partial signature to
and from an external party, keyed by pubkey rather than a wallet key
locator (an aggregate key isn't wallet-derivable). It captures the
operator-aggregated combined nonce via
MuSig2RegisterCombinedNonce, soround two can hand it back to the external party. The signing executor
picks this proxy per-VTXO when the signing key is marked external;
nothing changes for ordinary VTXOs.
Behind the proxy sits
treeSignatureBroker, exposed over two new RPCs,ListPendingTreeSigningRequestsandSubmitTreeSignatures. It mirrorsthe existing forfeit-participant broker but blocks the FSM twice, once for
the nonce and once for the partial sig, giving the two rounds distinct
request ids. The broker does no crypto: it validates the submitted bytes
structurally and relies on the operator's aggregated tree signature,
which the FSM already verifies before releasing boarding sigs, to catch a
bad partial sig downstream. Its state is deliberately daemon-local, so a
restart abandons in-flight rounds rather than resuming a stale transcript.
One thing worth calling out for review: externalizing nonce generation
moves the nonce-reuse invariant across the RPC boundary. The external
party alone is responsible for one fresh nonce per session and for never
signing twice against the same nonce. The broker binds the request id to
the session transcript and rejects a divergent re-answer, but it can't
enforce nonce freshness cryptographically. This is noted in the proto and
in the signer's doc comment.
Testing
Both parts are covered end to end. For boarding, the round actor test
asserts a custom-policy board builds its output from the template with no
owner-key derivation, the wallet tests cover outbox persistence + restart
replay + intent-id distinctness, the store test round-trips through the
000016columns, and an RPC test exercises the validation boundary.For tree signing, the proxy tests assert it ferries the material through
both rounds and carries the sighash and aggregate nonce; the broker tests
drive it exactly as the proxy does, over list/submit, covering the
two-round round-trip, distinct ids, timeout, and idempotent resubmit. The
integration test proves the whole daemon-side path over a real VTXO tree:
the executor drives the proxy, which routes to a backend running a real
signer, and the externally produced nonces and partial sigs cover exactly
the transactions on the cosigner's path.
SigningKeysForSpendPath