Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions cmd/wavecli/waveclicommands/devrpc/registry_generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion db/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const (
// daemon.
//
// NOTE: This MUST be updated when a new migration is added.
LatestMigrationVersion uint = 15
LatestMigrationVersion uint = 16
)

// MigrationTarget is a functional option that can be passed to applyMigrations
Expand Down
19 changes: 17 additions & 2 deletions db/pending_intent_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,10 +153,23 @@ func upsertPendingIntentDetail(ctx context.Context, q PendingIntentStore,

switch p := intent.Payload.(type) {
case *wallet.BoardIntentPayload:
// Store nil rather than a zero-length slice when there is no
// custom policy so the columns round-trip as NULL on Postgres
// (the x'' BYTEA pitfall) and the pk_script length CHECK holds.
var policyTemplate, pkScript []byte
if len(p.PolicyTemplate) > 0 {
policyTemplate = p.PolicyTemplate
}
if len(p.PkScript) > 0 {
pkScript = p.PkScript
}

err := q.UpsertPendingBoardIntent(
ctx, sqlc.UpsertPendingBoardIntentParams{
IntentID: intent.ID[:],
TargetVtxoCount: int32(p.TargetVTXOCount),
IntentID: intent.ID[:],
TargetVtxoCount: int32(p.TargetVTXOCount),
VtxoPolicyTemplate: policyTemplate,
PkScript: pkScript,
},
)
if err != nil {
Expand Down Expand Up @@ -315,6 +328,8 @@ func listPendingBoardIntents(ctx context.Context, q PendingIntentStore,
ID: id,
Payload: &wallet.BoardIntentPayload{
TargetVTXOCount: uint32(row.TargetVtxoCount),
PolicyTemplate: row.VtxoPolicyTemplate,
PkScript: row.PkScript,
},
RequestedAt: row.RequestedAtUnix,
Anchors: anchors[id],
Expand Down
43 changes: 43 additions & 0 deletions db/pending_intent_store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,49 @@ func TestPendingIntentUpsertListRoundtrip(t *testing.T) {
require.EqualValues(t, 150, gotBoard[0].RequestedAt)
}

// TestPendingIntentBoardCustomPolicyRoundtrip verifies that a board intent's
// custom VTXO policy template and pinned pk_script persist to their typed
// columns and list back byte-for-byte, so restart replay recreates the same
// custom-owned output. This exercises the full persistence path: the
// 000016 migration columns, the sqlc upsert/list, and the store's nil-vs-empty
// handling.
func TestPendingIntentBoardCustomPolicyRoundtrip(t *testing.T) {
t.Parallel()

ctx := t.Context()
h := newPendingIntentStoreForTest(t)

opA := wire.OutPoint{Hash: chainhash.Hash{0xd1}, Index: 0}

payload := &wallet.BoardIntentPayload{
TargetVTXOCount: 4,
PolicyTemplate: []byte{
0x01,
0xde,
0xad,
0xbe,
0xef,
},
PkScript: append(
[]byte{0x51, 0x20}, make([]byte, 32)...,
),
}
board := makePendingIntent(payload, 100, opA)
require.NoError(t, h.store.UpsertPendingIntent(ctx, board))

got, err := h.store.ListPendingIntents(
ctx, wallet.PendingIntentKindBoard,
)
require.NoError(t, err)
require.Len(t, got, 1)
require.Equal(t, board.Payload, got[0].Payload)

gotPayload, ok := got[0].Payload.(*wallet.BoardIntentPayload)
require.True(t, ok)
require.Equal(t, payload.PolicyTemplate, gotPayload.PolicyTemplate)
require.Equal(t, payload.PkScript, gotPayload.PkScript)
}

// TestPendingIntentAnchorRebindSweepsOrphan verifies that a newer intent
// claiming an older intent's anchors rebinds them, and that an older parent
// left with zero anchors is swept in the same transaction.
Expand Down
3 changes: 3 additions & 0 deletions db/sqlc/migrations/000016_board_intent_policy.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ALTER TABLE pending_board_intents DROP COLUMN pk_script;

ALTER TABLE pending_board_intents DROP COLUMN vtxo_policy_template;
15 changes: 15 additions & 0 deletions db/sqlc/migrations/000016_board_intent_policy.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-- Add the custom-policy replay parameters to the Board pending-intent detail
-- table. When a Board RPC pins a vtxo_policy_template (for example to board
-- directly into a VTXO owned by an external FROST aggregate key), the template
-- and its optional pinned pk_script must survive a restart so replay recreates
-- the same custom output instead of silently re-boarding into the standard
-- collaborative shape. Both columns are nullable (NULL for board intents
-- persisted before this migration and for the legacy standard-policy path); a
-- NULL template selects the standard collaborative policy with a freshly
-- derived owner key.
ALTER TABLE pending_board_intents
ADD COLUMN vtxo_policy_template BLOB;

ALTER TABLE pending_board_intents
ADD COLUMN pk_script BLOB
CHECK (pk_script IS NULL OR length(pk_script) > 0);
6 changes: 4 additions & 2 deletions db/sqlc/models.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 30 additions & 11 deletions db/sqlc/pending_intents.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 8 additions & 4 deletions db/sqlc/queries/pending_intents.sql
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,14 @@ SET requested_at_unix = excluded.requested_at_unix,
-- name: UpsertPendingBoardIntent :exec
INSERT INTO pending_board_intents (
intent_id,
target_vtxo_count
) VALUES ($1, $2)
target_vtxo_count,
vtxo_policy_template,
pk_script
) VALUES ($1, $2, $3, $4)
ON CONFLICT (intent_id) DO UPDATE
SET target_vtxo_count = excluded.target_vtxo_count;
SET target_vtxo_count = excluded.target_vtxo_count,
vtxo_policy_template = excluded.vtxo_policy_template,
pk_script = excluded.pk_script;

-- name: UpsertPendingSendIntent :exec
INSERT INTO pending_send_intents (
Expand Down Expand Up @@ -56,7 +60,7 @@ SET intent_id = excluded.intent_id;
-- retired and must not be re-submitted on restart.
SELECT
i.intent_id, i.requested_at_unix,
b.target_vtxo_count
b.target_vtxo_count, b.vtxo_policy_template, b.pk_script
FROM pending_intents i
JOIN pending_board_intents b ON b.intent_id = i.intent_id
WHERE i.kind = 'board' AND i.status = 'pending'
Expand Down
3 changes: 2 additions & 1 deletion db/sqlc/schemas/generated_schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -1058,7 +1058,8 @@ CREATE TABLE pending_board_intents (
-- the confirmed boarding balance into one VTXO, non-zero fans it out.
target_vtxo_count INTEGER NOT NULL DEFAULT 0
CHECK (target_vtxo_count >= 0)
);
, vtxo_policy_template BLOB, pk_script BLOB
CHECK (pk_script IS NULL OR length(pk_script) > 0));

CREATE TABLE pending_intent_anchors (
-- The anchored outpoint. For kind='board' this is a confirmed boarding
Expand Down
14 changes: 14 additions & 0 deletions lib/actormsg/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,20 @@ type TriggerBoardMsg struct {
// board later once headroom frees up. Nil when the full balance
// boards.
Change *types.LeaveRequest

// PolicyTemplate optionally pins the serialized arkscript policy for
// every boarded VTXO output. Nil selects the standard collaborative
// policy with a freshly derived owner key (the legacy behavior); when
// set, the round actor builds each boarded output from this template
// verbatim instead of synthesizing one, letting a client board directly
// into a custom-owned VTXO (e.g. one owned by an external FROST
// aggregate key).
PolicyTemplate []byte

// PkScript optionally pins the taproot output script for the boarded
// VTXOs. Only valid alongside PolicyTemplate; empty means derive it
// from the template.
PkScript []byte
}

// RoundReceivable implements the RoundReceivable marker interface.
Expand Down
9 changes: 9 additions & 0 deletions lib/types/boarding.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,15 @@ type VTXORequest struct {
// need the key locator for signing operations.
SigningKey keychain.KeyDescriptor

// ExternalTreeSigner marks this VTXO's tree-signing (MuSig2 cosigner)
// key as living outside this daemon: the round FSM must not derive a
// wallet key for it and must route its tree nonce and partial-signature
// production to an external party (e.g. an aggregate FROST key the
// client controls off-box) instead of the local wallet signer. When
// set, SigningKey.PubKey must be the external cosigner public key; the
// key locator is ignored because the key is not wallet-derivable.
ExternalTreeSigner bool

// Origin classifies how a locally-owned VTXO came into
// existence (boarding, refresh, or participant transfer). It
// is set by the wallet at intent-composition time and flows
Expand Down
Loading
Loading