diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 31b69764a..bc273eeaa 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -118,7 +118,23 @@ exactly-once semantics. See `docs/durable_actor_architecture.md`. ### RPC-over-Mailbox All server communication flows through `serverconn`, which implements unary RPCs (low-latency) and durable event egress (crash-safe) over the mailbox protocol. -Inbound events are dispatched via `EventRouter`. See `docs/mailbox_architecture.md`. +Inbound events are dispatched via `EventRouter`. Registered routes currently +include OOR lifecycle pushes, round progress pushes, and `MethodIncomingVTXO` +(which delivers `arkrpc.IncomingVTXOEvent` notifications to the +`vtxo.IncomingVTXOHandler` actor for local materialization of round-produced +VTXOs owned by the local wallet). See `docs/mailbox_architecture.md`. + +### Data-Driven Script Ownership +Local wallet ownership of a VTXO is resolved at round confirmation time by +looking up its pkScript in a persistent "owned receive scripts" table (the OOR +artifact store). The round FSM calls `OwnedScriptChecker.IsOwnedScript` for +every VTXO in a completed round and only persists the ones the wallet +recognizes. The round actor populates this table via `OwnedScriptRegistrar` +when it builds change/refresh intents and when it accepts a `RegisterIntentMsg` +whose owner key has a non-zero `KeyLocator`. Directed-send recipient keys +intentionally carry a zero `KeyLocator` so they are not registered on the +sender side — the recipient materializes those VTXOs via the incoming VTXO +push path instead. ### Outbox Pattern FSMs emit messages as data (outbox events). The actor runtime dispatches them @@ -140,6 +156,10 @@ corresponding state transition being durable. | `Message` | baselib/actor | Sealed interface for actor messages | | `Ref[Msg, Resp]` | baselib/actor | Typed actor reference (Tell, Ask) | | `ClientWallet` | round | MuSig2 signing + key derivation interface for round participation | +| `OwnedScriptChecker` | round | Data-driven pkScript ownership lookup used by the round FSM at confirmation time (replaces the old `IsOwner` flag) | +| `OwnedScriptRegistrar` | round | Persists locally-owned pkScripts when the round actor builds/accepts VTXO intents so the checker recognizes them on confirmation | +| `IncomingVTXOHandler` | vtxo | Materializes round-produced VTXOs from indexer push notifications when the local wallet owns the receive script | +| `OwnedScriptLookup` | vtxo | Read-only view of the owned receive scripts store used by `IncomingVTXOHandler` | | `VTXOReader` | wallet | Read-only VTXO descriptor access (breaks import cycle) | | `SelectedVTXO` | wallet | Locked VTXO descriptor for transfer inputs (breaks import cycle) | | `TxInfo` | wallet | Confirmed transaction with block hash and height | diff --git a/darepod/AGENTS.md b/darepod/AGENTS.md index f6183abc8..da7a7e992 100644 --- a/darepod/AGENTS.md +++ b/darepod/AGENTS.md @@ -8,17 +8,22 @@ gRPC API. ## Key Types -- `Server` — Main daemon owning wallet, DB, chainsource actor, gRPC server, and ActorSystem. Caches `localMailboxID` (pubkey-derived) and `authSigHex` (Schnorr auth) on the struct for use in response envelopes. +- `Server` — Main daemon owning wallet, DB, chainsource actor, gRPC server, and ActorSystem. Caches `localMailboxID` (pubkey-derived), `authSigHex` (Schnorr auth) and a single `clk` (`clock.Clock`) that all sub-stores share for deterministic time injection. - `RPCServer` — Implements the gRPC `DaemonService` API (Board, ListRounds, WatchRounds, NewOORReceiveScript, SendVTXO, etc.). Includes test hooks for mailbox edge factory and round registration. - `Config` — Daemon configuration (data dir, network, RPC host, wallet type, etc.). Includes `MailboxEdgeFactory` hook for test harness transport interception. - `TriggerRoundRegistration` — Test-hook method that injects a round registration event into the round actor (in `server_round_testhook.go`). +- `GetStoredVTXO` — Harness-only accessor that returns a persisted `vtxo.Descriptor` for a given outpoint directly from the daemon's VTXO store. Lets integration tests inspect partial unroll state without reaching into internal fields. - `WalletState` — Enum (None/Locked/Ready) for wallet lifecycle. - `serverDurableUnaryBuilder` — Implements `serverconn.DurableUnaryRequestBuilder` by delegating to the indexer client with proof-of-control credentials. - `NewOwnedReceiveScriptSigner` — Indexer signer that resolves the wallet key for any persisted owned receive script, then delegates signing to the backend-specific signer. +- `ownedScriptCheckerAdapter` — Wraps `db.OORArtifactPersistenceStore` to satisfy `round.OwnedScriptChecker`. Uses `context.WithoutCancel` so the confirmation-time ownership lookup survives FSM shutdown. Returns `false` on `sql.ErrNoRows`. +- `ownedScriptRegistrarAdapter` — Wraps the same store to satisfy `round.OwnedScriptRegistrar`. Persists pkScripts as `OwnedReceiveScriptSourceWallet` with the operator pubkey and VTXO exit delay from `OperatorTerms`. +- `ownedScriptLookupAdapter` — Wraps the store to satisfy `vtxo.OwnedScriptLookup` for the incoming VTXO handler, converting `db.OwnedReceiveScriptRecord` to `vtxo.OwnedReceiveScript`. - `EnsureDefaultOORReceiveScript` / `CreateOORReceiveScript` — Receive-key lifecycle: derive, register with indexer (proof-of-control), persist ownership record. - `ResolveIncomingMetadataFromIndexer` — Resolves authoritative VTXO lineage metadata from the indexer's `ListVTXOsByScripts` response for incoming materialization. -- `SendVTXO` — RPC handler for in-round directed sends. Validates recipients, resolves destinations via `resolveRecipientOutput`, and delegates to the wallet actor. +- `SendVTXO` — RPC handler for in-round directed sends. Validates recipients (count cap, positive and `MaxSatoshi`-bounded amounts, overflow-safe sum), resolves destinations via `resolveRecipientOutput`, and delegates to the wallet actor. - `resolveRecipientOutput` — Extracts pkScript and client pubkey from an `Output` proto oneof (pubkey or address). Enforces taproot-only for directed sends. +- `registerIncomingVTXOEventRoute` — Registers the `arkrpc.IncomingVTXOEvent` mailbox route under `MethodIncomingVTXO`, dispatching decoded events to the incoming VTXO handler actor via its service key. - `deriveIdentityKeyEarly` — Derives the client's secp256k1 identity key from LND or lwwallet before mailbox transport starts. Propagates wallet-specific errors on failure. - `signMailboxAuth` — Produces Schnorr auth signature. LND path uses tagged Schnorr signing RPC (`withSchnorrTag`); lwwallet path signs locally via `serverconn.SignMailboxAuth`. - `fetchOperatorPubKeyDirect` — Fetches operator pubkey via direct gRPC `GetInfo` call before the mailbox runtime starts. @@ -37,12 +42,16 @@ gRPC API. - Auth headers (Schnorr signature) are injected into all outbound envelopes including response envelopes in `handleInboundRPC`. - TLS client cert generation is skipped in insecure mode. - Per-subsystem logging: configurable log writer, no global mutable loggers. Each subsystem receives its own logger instance. +- All sub-stores share the single `s.clk` clock instance assigned at `NewServer`. New code must not call `clock.NewDefaultClock()` inside `init*` methods — use `s.clk` so tests can inject deterministic time. - Board RPC is non-blocking: delegates to wallet actor and returns immediately. +- `SendVTXO` enforces a hard recipient cap (`maxRecipients = 256`, see TODO #241), rejects per-recipient amounts outside `(0, MaxSatoshi]`, and uses overflow-safe accumulation when summing recipient amounts. Wallet-side validation (`handleSendVTXOs`) repeats these checks as a defense-in-depth boundary. - ListRounds splits pending (in-memory from actor) and persisted (SQL with cursor pagination) rounds. - Server holds a `roundStore` reference for direct SQL queries from the RPC layer. - Actor startup order: VTXO manager starts before round actor and OOR actor, so the manager ref is available for both. The round actor ref in the VTXO manager is lazy (service-key-based, resolved at Tell time). - `mapRoundVTXOManagerMsg` bridges `round.VTXOManagerMsg` → `vtxo.ManagerMsg` via `MapInputRef`. Compile-time assertions enforce that all `round.VTXOManagerMsg` implementors satisfy `vtxo.ManagerMsg`. - OOR receive-key is derived once at startup via `EnsureDefaultOORReceiveScript` and persisted for restart-safe re-registration. The `DurableUnaryBuilder` is wired through `serverconn.ConnectorConfig` so all indexer queries flow through the durable transport path. +- The OOR artifact store backs three different round/vtxo abstractions via the `ownedScript*Adapter` types: `round.OwnedScriptChecker`, `round.OwnedScriptRegistrar`, and `vtxo.OwnedScriptLookup`. There is one logical "owned receive scripts" table; all ownership questions resolve through it. +- The incoming VTXO handler actor (`vtxo.IncomingVTXOHandler`) is registered with the actor system under `vtxo.IncomingVTXOServiceKey()` during `initOORActor`. The mailbox route `MethodIncomingVTXO` decodes `arkrpc.IncomingVTXOEvent` push notifications and dispatches them to this actor for materialization. - In btcwallet mode, neutrino is pre-started before seed availability so P2P sync proceeds in parallel. The `neutrinoSvc` field uses `fn.Option` and is reused by `startBtcwallet` via `NewWithNeutrino`. - The neutrino sync-wait goroutine polls indefinitely (no timeout) to avoid leaving the wallet permanently unready. Progress is logged every 30 seconds. - `ensureRoundExists` in `db/vtxo_store.go` uses check-then-insert (not upsert) because `InsertRound`'s `ON CONFLICT DO UPDATE` would overwrite richer round state. diff --git a/darepod/CLAUDE.md b/darepod/CLAUDE.md index f6183abc8..da7a7e992 100644 --- a/darepod/CLAUDE.md +++ b/darepod/CLAUDE.md @@ -8,17 +8,22 @@ gRPC API. ## Key Types -- `Server` — Main daemon owning wallet, DB, chainsource actor, gRPC server, and ActorSystem. Caches `localMailboxID` (pubkey-derived) and `authSigHex` (Schnorr auth) on the struct for use in response envelopes. +- `Server` — Main daemon owning wallet, DB, chainsource actor, gRPC server, and ActorSystem. Caches `localMailboxID` (pubkey-derived), `authSigHex` (Schnorr auth) and a single `clk` (`clock.Clock`) that all sub-stores share for deterministic time injection. - `RPCServer` — Implements the gRPC `DaemonService` API (Board, ListRounds, WatchRounds, NewOORReceiveScript, SendVTXO, etc.). Includes test hooks for mailbox edge factory and round registration. - `Config` — Daemon configuration (data dir, network, RPC host, wallet type, etc.). Includes `MailboxEdgeFactory` hook for test harness transport interception. - `TriggerRoundRegistration` — Test-hook method that injects a round registration event into the round actor (in `server_round_testhook.go`). +- `GetStoredVTXO` — Harness-only accessor that returns a persisted `vtxo.Descriptor` for a given outpoint directly from the daemon's VTXO store. Lets integration tests inspect partial unroll state without reaching into internal fields. - `WalletState` — Enum (None/Locked/Ready) for wallet lifecycle. - `serverDurableUnaryBuilder` — Implements `serverconn.DurableUnaryRequestBuilder` by delegating to the indexer client with proof-of-control credentials. - `NewOwnedReceiveScriptSigner` — Indexer signer that resolves the wallet key for any persisted owned receive script, then delegates signing to the backend-specific signer. +- `ownedScriptCheckerAdapter` — Wraps `db.OORArtifactPersistenceStore` to satisfy `round.OwnedScriptChecker`. Uses `context.WithoutCancel` so the confirmation-time ownership lookup survives FSM shutdown. Returns `false` on `sql.ErrNoRows`. +- `ownedScriptRegistrarAdapter` — Wraps the same store to satisfy `round.OwnedScriptRegistrar`. Persists pkScripts as `OwnedReceiveScriptSourceWallet` with the operator pubkey and VTXO exit delay from `OperatorTerms`. +- `ownedScriptLookupAdapter` — Wraps the store to satisfy `vtxo.OwnedScriptLookup` for the incoming VTXO handler, converting `db.OwnedReceiveScriptRecord` to `vtxo.OwnedReceiveScript`. - `EnsureDefaultOORReceiveScript` / `CreateOORReceiveScript` — Receive-key lifecycle: derive, register with indexer (proof-of-control), persist ownership record. - `ResolveIncomingMetadataFromIndexer` — Resolves authoritative VTXO lineage metadata from the indexer's `ListVTXOsByScripts` response for incoming materialization. -- `SendVTXO` — RPC handler for in-round directed sends. Validates recipients, resolves destinations via `resolveRecipientOutput`, and delegates to the wallet actor. +- `SendVTXO` — RPC handler for in-round directed sends. Validates recipients (count cap, positive and `MaxSatoshi`-bounded amounts, overflow-safe sum), resolves destinations via `resolveRecipientOutput`, and delegates to the wallet actor. - `resolveRecipientOutput` — Extracts pkScript and client pubkey from an `Output` proto oneof (pubkey or address). Enforces taproot-only for directed sends. +- `registerIncomingVTXOEventRoute` — Registers the `arkrpc.IncomingVTXOEvent` mailbox route under `MethodIncomingVTXO`, dispatching decoded events to the incoming VTXO handler actor via its service key. - `deriveIdentityKeyEarly` — Derives the client's secp256k1 identity key from LND or lwwallet before mailbox transport starts. Propagates wallet-specific errors on failure. - `signMailboxAuth` — Produces Schnorr auth signature. LND path uses tagged Schnorr signing RPC (`withSchnorrTag`); lwwallet path signs locally via `serverconn.SignMailboxAuth`. - `fetchOperatorPubKeyDirect` — Fetches operator pubkey via direct gRPC `GetInfo` call before the mailbox runtime starts. @@ -37,12 +42,16 @@ gRPC API. - Auth headers (Schnorr signature) are injected into all outbound envelopes including response envelopes in `handleInboundRPC`. - TLS client cert generation is skipped in insecure mode. - Per-subsystem logging: configurable log writer, no global mutable loggers. Each subsystem receives its own logger instance. +- All sub-stores share the single `s.clk` clock instance assigned at `NewServer`. New code must not call `clock.NewDefaultClock()` inside `init*` methods — use `s.clk` so tests can inject deterministic time. - Board RPC is non-blocking: delegates to wallet actor and returns immediately. +- `SendVTXO` enforces a hard recipient cap (`maxRecipients = 256`, see TODO #241), rejects per-recipient amounts outside `(0, MaxSatoshi]`, and uses overflow-safe accumulation when summing recipient amounts. Wallet-side validation (`handleSendVTXOs`) repeats these checks as a defense-in-depth boundary. - ListRounds splits pending (in-memory from actor) and persisted (SQL with cursor pagination) rounds. - Server holds a `roundStore` reference for direct SQL queries from the RPC layer. - Actor startup order: VTXO manager starts before round actor and OOR actor, so the manager ref is available for both. The round actor ref in the VTXO manager is lazy (service-key-based, resolved at Tell time). - `mapRoundVTXOManagerMsg` bridges `round.VTXOManagerMsg` → `vtxo.ManagerMsg` via `MapInputRef`. Compile-time assertions enforce that all `round.VTXOManagerMsg` implementors satisfy `vtxo.ManagerMsg`. - OOR receive-key is derived once at startup via `EnsureDefaultOORReceiveScript` and persisted for restart-safe re-registration. The `DurableUnaryBuilder` is wired through `serverconn.ConnectorConfig` so all indexer queries flow through the durable transport path. +- The OOR artifact store backs three different round/vtxo abstractions via the `ownedScript*Adapter` types: `round.OwnedScriptChecker`, `round.OwnedScriptRegistrar`, and `vtxo.OwnedScriptLookup`. There is one logical "owned receive scripts" table; all ownership questions resolve through it. +- The incoming VTXO handler actor (`vtxo.IncomingVTXOHandler`) is registered with the actor system under `vtxo.IncomingVTXOServiceKey()` during `initOORActor`. The mailbox route `MethodIncomingVTXO` decodes `arkrpc.IncomingVTXOEvent` push notifications and dispatches them to this actor for materialization. - In btcwallet mode, neutrino is pre-started before seed availability so P2P sync proceeds in parallel. The `neutrinoSvc` field uses `fn.Option` and is reused by `startBtcwallet` via `NewWithNeutrino`. - The neutrino sync-wait goroutine polls indefinitely (no timeout) to avoid leaving the wallet permanently unready. Progress is logged every 30 seconds. - `ensureRoundExists` in `db/vtxo_store.go` uses check-then-insert (not upsert) because `InsertRound`'s `ON CONFLICT DO UPDATE` would overwrite richer round state. diff --git a/round/AGENTS.md b/round/AGENTS.md index de5266a40..08ed9b0e7 100644 --- a/round/AGENTS.md +++ b/round/AGENTS.md @@ -17,8 +17,10 @@ protocols with MuSig2 signing ceremonies. - `Intents` — Pools of boarding, VTXO, forfeit, and leave requests accumulated before registration. - `IntentPackage` — FSM event wrapping `Intents` for atomic delivery to the round FSM. - `RegisterIntentRequest` — Actor message carrying a pre-composed `IntentPackage` from the wallet. -- `VTXOIntent` — Pre-registration VTXO request carrying `OwnerKey`, `OperatorKey`. For directed sends, `OwnerKey` is the recipient's key (distinct from the sender's `SigningKey`). Ownership is determined at confirmation time via `OwnedScriptChecker`. +- `VTXOIntent` — Pre-registration VTXO request carrying `OwnerKey`, `OperatorKey`. For directed sends, `OwnerKey` is the recipient's key (distinct from the sender's `SigningKey`). Ownership is determined at confirmation time via `OwnedScriptChecker` — there is no `IsOwner` flag on the wire or in local state. - `RoundVTXORequest` — Pairs a `VTXOIntent` with an ephemeral `SigningKey` derived at registration time for MuSig2 tree construction. +- `OwnedScriptChecker` — Interface that answers "does this pkScript belong to the local wallet?" The `InputSigSent → Confirmed` transition calls this for every VTXO in the round to decide which entries `buildOwnedClientVTXOs` persists as spendable local balance. Backed in production by the OOR artifact store (owned receive scripts table). +- `OwnedScriptRegistrar` — Interface used by the round actor when building VTXO intents (refresh change, boarding change, directed-send change outputs) to persist the pkScript + owner key before the round registers. This ensures the `OwnedScriptChecker` recognizes the script when the round confirms. `handleRegisterIntent` also registers any VTXO in an incoming `RegisterIntentMsg` whose `OwnerKey.KeyLocator` is non-zero (remote recipient keys are left with a zero locator and skipped). - `ForfeitSignaturesCollectingState` — State entered after VTXO tree signing when round includes refresh/leave VTXOs. Waits for all expected forfeit signatures before submitting to server. - `ForfeitSignatureResponse` — Carries a VTXO's forfeit signature back from the VTXO actor. - `ConnectorLeafInfo` — Maps a VTXO outpoint to its connector output index and leaf info for forfeit construction. @@ -26,13 +28,14 @@ protocols with MuSig2 signing ceremonies. ## Relationships - **Depends on**: `baselib/protofsm` (FSM engine), `lib/tree` (Merkle trees), `lib/types` (shared domain types), `lib/scripts` (taproot scripts), `wallet` (types: `BoardingAddress`, `BoardingIntent`, `SelectedVTXO`). -- **Depended on by**: `vtxo` (forfeit coordination), `db` (round persistence), `darepod` (wiring). +- **Depended on by**: `vtxo` (forfeit coordination), `db` (round persistence), `darepod` (wiring, owned-script adapters). - **Sends**: - → `serverconn`: `JoinRoundRequest`, `SubmitNoncesRequest`, `SubmitPartialSigRequest`, `SubmitVTXOForfeitSigsToServer` - → `vtxo`: `ForfeitRequestEvent`, `ForfeitConfirmedEvent`, `BlockEpochEvent`, `PendingForfeitEvent`, `SpendReserveEvent`, `SpendCompletedEvent`, `ForfeitReleasedEvent` - → `vtxo` manager: `VTXOCreatedNotification` - → `wallet`: `RegisterConfirmationNotifierRequest` - → `timeout`: `ScheduleTimeoutRequest`, `CancelTimeoutRequest` + - → `OwnedScriptRegistrar` (darepod adapter over OOR artifact store): `RegisterOwnedScript(pkScript, ownerKey)` - **Receives**: - ← `serverconn`: `CommitmentTxBuilt`, `NoncesAggregated`, `OperatorSigned`, `RoundJoined`, `BoardingFailed` - ← `vtxo`: `ForfeitSignatureResponse` (relayed through manager) @@ -54,6 +57,9 @@ protocols with MuSig2 signing ceremonies. - The round actor does not mark VTXOs as PendingForfeit — the wallet/manager admits VTXOs before sending RegisterIntentMsg. - `ClientWallet` provides MuSig2 signing and key derivation; boarding address creation is handled by the wallet actor (not the round FSM). - Persisted VTXO ownership uses `OwnerKey` (not `SigningKey`). For directed sends the sender's signing key participates in MuSig2 tree construction, but the recipient's owner key determines VTXO ownership. +- Local-balance persistence on confirmation is driven by `OwnedScriptChecker.IsOwnedScript(pkScript)`, not by any per-intent boolean. `buildOwnedClientVTXOs` skips any VTXO whose pkScript the checker does not recognize; the client still co-signs its tree path, so foreign recipients in a directed send still get a valid unroll proof. When the checker is nil (tests), every VTXO is treated as owned. +- VTXO pkScripts are registered with `OwnedScriptRegistrar` at intent-build time for change/refresh outputs, and inside `handleRegisterIntent` for any `RegisterIntentMsg` entry with a non-zero `KeyLocator`. Remote recipient keys in directed sends carry a zero `KeyLocator` and are intentionally left unregistered. +- Each client sub-tree in the commitment tree must contain exactly one non-anchor leaf. `buildOwnedClientVTXOs` fails the transition if a signing-key sub-tree yields anything other than one leaf. ## Deep Docs diff --git a/round/CLAUDE.md b/round/CLAUDE.md index de5266a40..08ed9b0e7 100644 --- a/round/CLAUDE.md +++ b/round/CLAUDE.md @@ -17,8 +17,10 @@ protocols with MuSig2 signing ceremonies. - `Intents` — Pools of boarding, VTXO, forfeit, and leave requests accumulated before registration. - `IntentPackage` — FSM event wrapping `Intents` for atomic delivery to the round FSM. - `RegisterIntentRequest` — Actor message carrying a pre-composed `IntentPackage` from the wallet. -- `VTXOIntent` — Pre-registration VTXO request carrying `OwnerKey`, `OperatorKey`. For directed sends, `OwnerKey` is the recipient's key (distinct from the sender's `SigningKey`). Ownership is determined at confirmation time via `OwnedScriptChecker`. +- `VTXOIntent` — Pre-registration VTXO request carrying `OwnerKey`, `OperatorKey`. For directed sends, `OwnerKey` is the recipient's key (distinct from the sender's `SigningKey`). Ownership is determined at confirmation time via `OwnedScriptChecker` — there is no `IsOwner` flag on the wire or in local state. - `RoundVTXORequest` — Pairs a `VTXOIntent` with an ephemeral `SigningKey` derived at registration time for MuSig2 tree construction. +- `OwnedScriptChecker` — Interface that answers "does this pkScript belong to the local wallet?" The `InputSigSent → Confirmed` transition calls this for every VTXO in the round to decide which entries `buildOwnedClientVTXOs` persists as spendable local balance. Backed in production by the OOR artifact store (owned receive scripts table). +- `OwnedScriptRegistrar` — Interface used by the round actor when building VTXO intents (refresh change, boarding change, directed-send change outputs) to persist the pkScript + owner key before the round registers. This ensures the `OwnedScriptChecker` recognizes the script when the round confirms. `handleRegisterIntent` also registers any VTXO in an incoming `RegisterIntentMsg` whose `OwnerKey.KeyLocator` is non-zero (remote recipient keys are left with a zero locator and skipped). - `ForfeitSignaturesCollectingState` — State entered after VTXO tree signing when round includes refresh/leave VTXOs. Waits for all expected forfeit signatures before submitting to server. - `ForfeitSignatureResponse` — Carries a VTXO's forfeit signature back from the VTXO actor. - `ConnectorLeafInfo` — Maps a VTXO outpoint to its connector output index and leaf info for forfeit construction. @@ -26,13 +28,14 @@ protocols with MuSig2 signing ceremonies. ## Relationships - **Depends on**: `baselib/protofsm` (FSM engine), `lib/tree` (Merkle trees), `lib/types` (shared domain types), `lib/scripts` (taproot scripts), `wallet` (types: `BoardingAddress`, `BoardingIntent`, `SelectedVTXO`). -- **Depended on by**: `vtxo` (forfeit coordination), `db` (round persistence), `darepod` (wiring). +- **Depended on by**: `vtxo` (forfeit coordination), `db` (round persistence), `darepod` (wiring, owned-script adapters). - **Sends**: - → `serverconn`: `JoinRoundRequest`, `SubmitNoncesRequest`, `SubmitPartialSigRequest`, `SubmitVTXOForfeitSigsToServer` - → `vtxo`: `ForfeitRequestEvent`, `ForfeitConfirmedEvent`, `BlockEpochEvent`, `PendingForfeitEvent`, `SpendReserveEvent`, `SpendCompletedEvent`, `ForfeitReleasedEvent` - → `vtxo` manager: `VTXOCreatedNotification` - → `wallet`: `RegisterConfirmationNotifierRequest` - → `timeout`: `ScheduleTimeoutRequest`, `CancelTimeoutRequest` + - → `OwnedScriptRegistrar` (darepod adapter over OOR artifact store): `RegisterOwnedScript(pkScript, ownerKey)` - **Receives**: - ← `serverconn`: `CommitmentTxBuilt`, `NoncesAggregated`, `OperatorSigned`, `RoundJoined`, `BoardingFailed` - ← `vtxo`: `ForfeitSignatureResponse` (relayed through manager) @@ -54,6 +57,9 @@ protocols with MuSig2 signing ceremonies. - The round actor does not mark VTXOs as PendingForfeit — the wallet/manager admits VTXOs before sending RegisterIntentMsg. - `ClientWallet` provides MuSig2 signing and key derivation; boarding address creation is handled by the wallet actor (not the round FSM). - Persisted VTXO ownership uses `OwnerKey` (not `SigningKey`). For directed sends the sender's signing key participates in MuSig2 tree construction, but the recipient's owner key determines VTXO ownership. +- Local-balance persistence on confirmation is driven by `OwnedScriptChecker.IsOwnedScript(pkScript)`, not by any per-intent boolean. `buildOwnedClientVTXOs` skips any VTXO whose pkScript the checker does not recognize; the client still co-signs its tree path, so foreign recipients in a directed send still get a valid unroll proof. When the checker is nil (tests), every VTXO is treated as owned. +- VTXO pkScripts are registered with `OwnedScriptRegistrar` at intent-build time for change/refresh outputs, and inside `handleRegisterIntent` for any `RegisterIntentMsg` entry with a non-zero `KeyLocator`. Remote recipient keys in directed sends carry a zero `KeyLocator` and are intentionally left unregistered. +- Each client sub-tree in the commitment tree must contain exactly one non-anchor leaf. `buildOwnedClientVTXOs` fails the transition if a signing-key sub-tree yields anything other than one leaf. ## Deep Docs diff --git a/tools/AGENTS.md b/tools/AGENTS.md index c25df3441..5252d684d 100644 --- a/tools/AGENTS.md +++ b/tools/AGENTS.md @@ -11,8 +11,9 @@ Development tool dependencies (`tools.go` for protoc plugins, sqlc, linters). ## Local Linting -- Run `make install-custom-gcl` from the repo root to build a native - `custom-gcl` binary for the current macOS/Linux host. -- After installation, `make lint-local` and `make lint-changed-local` - reuse that native binary and load the real `ll` plugin instead of the - fallback `lll` approximation. +- **Preferred**: `make lint-native` builds the custom linter via + `go tool golangci-lint custom` and runs it on branch changes. No Docker + required, loads the real `ll` plugin. +- **Alternative**: `make install-custom-gcl` builds a native `custom-gcl` + binary, then `make lint-changed-local` uses it. +- Both native paths are much faster than `make lint` (Docker) on macOS. diff --git a/vtxo/AGENTS.md b/vtxo/AGENTS.md index 656c610ef..6b3c3978f 100644 --- a/vtxo/AGENTS.md +++ b/vtxo/AGENTS.md @@ -7,7 +7,9 @@ coordinates refresh (forfeit + new issuance), coordinates forfeit signing, and tracks cooperative and unilateral spending paths. The Manager actor is the single admission gate for all VTXO operations. The VTXO FSM models lifecycle phases only, not business intent like refresh -versus leave. +versus leave. The package also hosts the `IncomingVTXOHandler` actor, +which materializes round-produced VTXOs from indexer push notifications +when the local wallet owns the receive script. ## Key Types @@ -20,19 +22,26 @@ versus leave. - `FilterOptions` / `FilterDescriptors` — VTXO filtering by expiry status, spend state, etc. - `GetActiveVTXOCountRequest` / `GetActiveVTXOCountResponse` — Ask-message for querying active VTXO count from the manager. - `ManagerMsg` / `ManagerResp` — Type aliases for `actormsg.VTXOManagerMsg` / `actormsg.VTXOManagerResp` (admission types live in `lib/actormsg` to avoid import cycles). +- `IncomingVTXOHandler` — Actor that consumes `arkrpc.IncomingVTXOEvent` push notifications, looks up the receive script in the owned-script store, builds a `Descriptor` (with tapscript derived via `lib/scripts.VTXOTapScript`), persists it via `VTXOSaver`, and tells the manager via `VTXOsMaterializedNotification`. Only `VTXO_EVENT_TYPE_CREATED` events are acted on; unknown event kinds and unowned scripts are silently ignored. Inputs are validated for outpoint shape, pkScript presence, and `int64`/`MaxSatoshi` value bounds before any DB write. +- `IncomingVTXOMsg` / `IncomingVTXOResp` — Actor envelope wrapping an `arkrpc.IncomingVTXOEvent` and the `any`-typed response. +- `IncomingVTXOServiceKey` — Well-known service key (`"incoming-vtxo-handler"`) used by `darepod` to register the actor and by `serverconn.EventRouter` to dispatch routed events. +- `OwnedReceiveScript` / `OwnedScriptLookup` — Read-only view of the owned receive scripts store used by the incoming handler. `LookupOwnedReceiveScript` returns `sql.ErrNoRows` for unknown scripts; the handler treats this as "not ours" and exits cleanly. +- `VTXOSaver` — Narrow persistence interface (`SaveVTXO(ctx, *Descriptor)`) the incoming handler uses; the production implementation is the `db` VTXO store, which serializes a missing tree path as an empty blob. +- `VTXOsMaterializedNotification` — Manager-facing notification carrying already-persisted descriptors; the manager spawns one actor per descriptor without performing another store write. Used by both the OOR receive path and the new incoming round VTXO handler. ## Relationships -- **Depends on**: `baselib/protofsm` (FSM engine), `baselib/actor` (actor system), `lib/tree` (tree paths), `lib/actormsg` (admission message types), `chainsource` (block epochs). -- **Depended on by**: `round` (triggers forfeit requests), `oor` (incoming VTXOs), `wallet` (admission gating), `db` (persistence), `darepod` (wiring). +- **Depends on**: `baselib/protofsm` (FSM engine), `baselib/actor` (actor system), `lib/tree` (tree paths), `lib/scripts` (taproot construction in `IncomingVTXOHandler`), `lib/actormsg` (admission message types), `arkrpc` (`IncomingVTXOEvent`), `chainsource` (block epochs). +- **Depended on by**: `round` (triggers forfeit requests), `oor` (incoming VTXOs), `wallet` (admission gating), `db` (persistence), `darepod` (wiring, owned-script adapters, incoming event route). - **Sends**: - → `round` (via manager relay): `RelayToRoundMsg` wrapping `ForfeitSignatureSubmission` - → `db` (via outbox): `VTXOStatusUpdate` - - → `vtxo` manager: `VTXOTerminatedNotification`, `RelayToRoundMsg` + - → `vtxo` manager: `VTXOTerminatedNotification`, `RelayToRoundMsg`, `VTXOsMaterializedNotification` (from `IncomingVTXOHandler`) - **Receives**: - ← `round`: `ForfeitRequestEvent`, `ForfeitConfirmedEvent`, `ForfeitSignedEvent`, `ForfeitReleasedEvent`, `BlockEpochEvent`, `PendingForfeitEvent`, `SpendReserveEvent`, `SpendReleasedEvent`, `SpendCompletedEvent`, `ResumeVTXOEvent` - ← `wallet` (via `lib/actormsg`): `SelectAndReserveSpendRequest`, `ReleaseSpendRequest`, `CompleteSpendRequest`, `ReserveForfeitRequest`, `ReleaseForfeitRequest`, `SelectAndReserveForfeitRequest` - ← `chainsource` (via Manager): `BlockEpochEvent` + - ← `serverconn` (via `EventRouter` route `MethodIncomingVTXO`): `IncomingVTXOMsg` (wrapping `arkrpc.IncomingVTXOEvent`), routed to `IncomingVTXOHandler` ## Invariants @@ -45,6 +54,9 @@ versus leave. - A VTXO in SpendingState cannot be admitted for cooperative consumption, and vice versa. - Admission types (`SelectAndReserveSpendRequest`, `SelectAndReserveForfeitRequest`, `ReserveForfeitRequest`, etc.) are defined in `lib/actormsg` and re-exported as type aliases to avoid wallet → vtxo → round → wallet import cycles. - `selectAndReserveVTXOs` is a shared helper parameterized by `reserveParams` that serves both the OOR spend and cooperative forfeit coin selection paths, avoiding code duplication. +- `IncomingVTXOHandler` only handles `VTXO_EVENT_TYPE_CREATED` events. Other event kinds, missing/short outpoints, empty pkScripts, oversized values (`> int64` or `> MaxSatoshi`), and tapscript derivation failures all return success without persisting — they cannot crash the actor or block the indexer push stream. Real DB lookup/save errors are surfaced. +- Incoming VTXOs are saved with `Status: VTXOStatusLive` and no `TreePath` (the round commitment tree is not pushed alongside the event); `db.VTXOPersistenceStore.descriptorToInsertParams` accepts an empty tree-path blob to support this. +- The `CommitmentTxID` on a materialized incoming VTXO comes from `IncomingVTXOEvent.CommitmentTxid`, which is the round commitment txid — **not** the leaf txid in the outpoint. - Per-subsystem logging: `ManagerConfig.Log` provides an optional instance logger; falls back to `build.LoggerFromContext` (no global mutable loggers). ## Deep Docs diff --git a/vtxo/CLAUDE.md b/vtxo/CLAUDE.md index 656c610ef..6b3c3978f 100644 --- a/vtxo/CLAUDE.md +++ b/vtxo/CLAUDE.md @@ -7,7 +7,9 @@ coordinates refresh (forfeit + new issuance), coordinates forfeit signing, and tracks cooperative and unilateral spending paths. The Manager actor is the single admission gate for all VTXO operations. The VTXO FSM models lifecycle phases only, not business intent like refresh -versus leave. +versus leave. The package also hosts the `IncomingVTXOHandler` actor, +which materializes round-produced VTXOs from indexer push notifications +when the local wallet owns the receive script. ## Key Types @@ -20,19 +22,26 @@ versus leave. - `FilterOptions` / `FilterDescriptors` — VTXO filtering by expiry status, spend state, etc. - `GetActiveVTXOCountRequest` / `GetActiveVTXOCountResponse` — Ask-message for querying active VTXO count from the manager. - `ManagerMsg` / `ManagerResp` — Type aliases for `actormsg.VTXOManagerMsg` / `actormsg.VTXOManagerResp` (admission types live in `lib/actormsg` to avoid import cycles). +- `IncomingVTXOHandler` — Actor that consumes `arkrpc.IncomingVTXOEvent` push notifications, looks up the receive script in the owned-script store, builds a `Descriptor` (with tapscript derived via `lib/scripts.VTXOTapScript`), persists it via `VTXOSaver`, and tells the manager via `VTXOsMaterializedNotification`. Only `VTXO_EVENT_TYPE_CREATED` events are acted on; unknown event kinds and unowned scripts are silently ignored. Inputs are validated for outpoint shape, pkScript presence, and `int64`/`MaxSatoshi` value bounds before any DB write. +- `IncomingVTXOMsg` / `IncomingVTXOResp` — Actor envelope wrapping an `arkrpc.IncomingVTXOEvent` and the `any`-typed response. +- `IncomingVTXOServiceKey` — Well-known service key (`"incoming-vtxo-handler"`) used by `darepod` to register the actor and by `serverconn.EventRouter` to dispatch routed events. +- `OwnedReceiveScript` / `OwnedScriptLookup` — Read-only view of the owned receive scripts store used by the incoming handler. `LookupOwnedReceiveScript` returns `sql.ErrNoRows` for unknown scripts; the handler treats this as "not ours" and exits cleanly. +- `VTXOSaver` — Narrow persistence interface (`SaveVTXO(ctx, *Descriptor)`) the incoming handler uses; the production implementation is the `db` VTXO store, which serializes a missing tree path as an empty blob. +- `VTXOsMaterializedNotification` — Manager-facing notification carrying already-persisted descriptors; the manager spawns one actor per descriptor without performing another store write. Used by both the OOR receive path and the new incoming round VTXO handler. ## Relationships -- **Depends on**: `baselib/protofsm` (FSM engine), `baselib/actor` (actor system), `lib/tree` (tree paths), `lib/actormsg` (admission message types), `chainsource` (block epochs). -- **Depended on by**: `round` (triggers forfeit requests), `oor` (incoming VTXOs), `wallet` (admission gating), `db` (persistence), `darepod` (wiring). +- **Depends on**: `baselib/protofsm` (FSM engine), `baselib/actor` (actor system), `lib/tree` (tree paths), `lib/scripts` (taproot construction in `IncomingVTXOHandler`), `lib/actormsg` (admission message types), `arkrpc` (`IncomingVTXOEvent`), `chainsource` (block epochs). +- **Depended on by**: `round` (triggers forfeit requests), `oor` (incoming VTXOs), `wallet` (admission gating), `db` (persistence), `darepod` (wiring, owned-script adapters, incoming event route). - **Sends**: - → `round` (via manager relay): `RelayToRoundMsg` wrapping `ForfeitSignatureSubmission` - → `db` (via outbox): `VTXOStatusUpdate` - - → `vtxo` manager: `VTXOTerminatedNotification`, `RelayToRoundMsg` + - → `vtxo` manager: `VTXOTerminatedNotification`, `RelayToRoundMsg`, `VTXOsMaterializedNotification` (from `IncomingVTXOHandler`) - **Receives**: - ← `round`: `ForfeitRequestEvent`, `ForfeitConfirmedEvent`, `ForfeitSignedEvent`, `ForfeitReleasedEvent`, `BlockEpochEvent`, `PendingForfeitEvent`, `SpendReserveEvent`, `SpendReleasedEvent`, `SpendCompletedEvent`, `ResumeVTXOEvent` - ← `wallet` (via `lib/actormsg`): `SelectAndReserveSpendRequest`, `ReleaseSpendRequest`, `CompleteSpendRequest`, `ReserveForfeitRequest`, `ReleaseForfeitRequest`, `SelectAndReserveForfeitRequest` - ← `chainsource` (via Manager): `BlockEpochEvent` + - ← `serverconn` (via `EventRouter` route `MethodIncomingVTXO`): `IncomingVTXOMsg` (wrapping `arkrpc.IncomingVTXOEvent`), routed to `IncomingVTXOHandler` ## Invariants @@ -45,6 +54,9 @@ versus leave. - A VTXO in SpendingState cannot be admitted for cooperative consumption, and vice versa. - Admission types (`SelectAndReserveSpendRequest`, `SelectAndReserveForfeitRequest`, `ReserveForfeitRequest`, etc.) are defined in `lib/actormsg` and re-exported as type aliases to avoid wallet → vtxo → round → wallet import cycles. - `selectAndReserveVTXOs` is a shared helper parameterized by `reserveParams` that serves both the OOR spend and cooperative forfeit coin selection paths, avoiding code duplication. +- `IncomingVTXOHandler` only handles `VTXO_EVENT_TYPE_CREATED` events. Other event kinds, missing/short outpoints, empty pkScripts, oversized values (`> int64` or `> MaxSatoshi`), and tapscript derivation failures all return success without persisting — they cannot crash the actor or block the indexer push stream. Real DB lookup/save errors are surfaced. +- Incoming VTXOs are saved with `Status: VTXOStatusLive` and no `TreePath` (the round commitment tree is not pushed alongside the event); `db.VTXOPersistenceStore.descriptorToInsertParams` accepts an empty tree-path blob to support this. +- The `CommitmentTxID` on a materialized incoming VTXO comes from `IncomingVTXOEvent.CommitmentTxid`, which is the round commitment txid — **not** the leaf txid in the outpoint. - Per-subsystem logging: `ManagerConfig.Log` provides an optional instance logger; falls back to `build.LoggerFromContext` (no global mutable loggers). ## Deep Docs diff --git a/wallet/AGENTS.md b/wallet/AGENTS.md index e0285a24e..c0d3a64ef 100644 --- a/wallet/AGENTS.md +++ b/wallet/AGENTS.md @@ -27,7 +27,7 @@ refresh, leave, OOR spend, and directed send flows. - `CompleteSpendVTXOsRequest` — Tell-message to finalize spend and release locks. - `UnlockVTXOsRequest` — Tell-message to release locked VTXOs on failure. - `SendRecipient` — Describes a single directed send destination (pkscript, amount, recipient client key). -- `SendVTXOsRequest` / `SendVTXOsResponse` — Ask-request for in-round directed sends. Atomically selects and reserves VTXOs via `SelectAndReserveForfeitRequest`, builds forfeit + recipient VTXO intents, and registers with the round actor. Supports dry-run mode for previewing coin selection without committing. +- `SendVTXOsRequest` / `SendVTXOsResponse` — Ask-request for in-round directed sends. Validates each recipient amount is within `(0, MaxSatoshi]` and that the running total never overflows `int64`, atomically selects and reserves VTXOs via `SelectAndReserveForfeitRequest`, builds forfeit + recipient VTXO intents, and registers with the round actor. Supports dry-run mode for previewing coin selection without committing. Reserved VTXOs are released via a deferred cleanup that uses `context.WithoutCancel` so cleanup survives caller disconnect; on success, a `committed` flag is set to skip the release. ## Relationships @@ -50,6 +50,8 @@ refresh, leave, OOR spend, and directed send flows. - Cooperative admission (refresh/leave) must reserve forfeit inputs through the VTXO manager before sending `RegisterIntentMsg` to the round actor. - If round registration fails after successful admission, the wallet releases the forfeit reservation so VTXOs return to LiveState. - Directed sends use `SelectAndReserveForfeitRequest` (cooperative forfeit path) rather than the OOR spend path. The wallet builds recipient VTXOs with the recipient's key as `OwnerKey` and derives a separate ephemeral `SigningKey` for MuSig2 tree construction. +- Local ownership of a round-produced VTXO is no longer tracked with a per-intent `IsOwner` flag. `types.VTXORequest` / `round.VTXOIntent` no longer carry `IsOwner`; at round confirmation time the round FSM asks a `round.OwnedScriptChecker` (backed in production by the OOR owned-receive-scripts store) which pkScripts to persist as local balance. The wallet's only job is to supply the correct `OwnerKey` per intent — local-origin owner keys keep their populated `KeyLocator` so `handleRegisterIntent` registers them via `OwnedScriptRegistrar`, while remote recipients carry a zero `KeyLocator` and are intentionally left unregistered. +- `handleSendVTXOs` uses a `defer`-based release rather than a `releaseAndFail` helper: any error path (including dry-run) falls through to the deferred release, and the `committed` flag is set only after the round actor accepts the intent. Context is preserved via `context.WithoutCancel` so cleanup is not dropped when the caller disconnects. - `VTXOReader` / `VTXODescriptor` / `SelectedVTXO` break the vtxo → round → wallet import cycle by providing wallet-level types that don't reference `vtxo.Descriptor` directly. - Per-subsystem logging via `build.LoggerFromContext` (no global mutable loggers). diff --git a/wallet/CLAUDE.md b/wallet/CLAUDE.md index e0285a24e..c0d3a64ef 100644 --- a/wallet/CLAUDE.md +++ b/wallet/CLAUDE.md @@ -27,7 +27,7 @@ refresh, leave, OOR spend, and directed send flows. - `CompleteSpendVTXOsRequest` — Tell-message to finalize spend and release locks. - `UnlockVTXOsRequest` — Tell-message to release locked VTXOs on failure. - `SendRecipient` — Describes a single directed send destination (pkscript, amount, recipient client key). -- `SendVTXOsRequest` / `SendVTXOsResponse` — Ask-request for in-round directed sends. Atomically selects and reserves VTXOs via `SelectAndReserveForfeitRequest`, builds forfeit + recipient VTXO intents, and registers with the round actor. Supports dry-run mode for previewing coin selection without committing. +- `SendVTXOsRequest` / `SendVTXOsResponse` — Ask-request for in-round directed sends. Validates each recipient amount is within `(0, MaxSatoshi]` and that the running total never overflows `int64`, atomically selects and reserves VTXOs via `SelectAndReserveForfeitRequest`, builds forfeit + recipient VTXO intents, and registers with the round actor. Supports dry-run mode for previewing coin selection without committing. Reserved VTXOs are released via a deferred cleanup that uses `context.WithoutCancel` so cleanup survives caller disconnect; on success, a `committed` flag is set to skip the release. ## Relationships @@ -50,6 +50,8 @@ refresh, leave, OOR spend, and directed send flows. - Cooperative admission (refresh/leave) must reserve forfeit inputs through the VTXO manager before sending `RegisterIntentMsg` to the round actor. - If round registration fails after successful admission, the wallet releases the forfeit reservation so VTXOs return to LiveState. - Directed sends use `SelectAndReserveForfeitRequest` (cooperative forfeit path) rather than the OOR spend path. The wallet builds recipient VTXOs with the recipient's key as `OwnerKey` and derives a separate ephemeral `SigningKey` for MuSig2 tree construction. +- Local ownership of a round-produced VTXO is no longer tracked with a per-intent `IsOwner` flag. `types.VTXORequest` / `round.VTXOIntent` no longer carry `IsOwner`; at round confirmation time the round FSM asks a `round.OwnedScriptChecker` (backed in production by the OOR owned-receive-scripts store) which pkScripts to persist as local balance. The wallet's only job is to supply the correct `OwnerKey` per intent — local-origin owner keys keep their populated `KeyLocator` so `handleRegisterIntent` registers them via `OwnedScriptRegistrar`, while remote recipients carry a zero `KeyLocator` and are intentionally left unregistered. +- `handleSendVTXOs` uses a `defer`-based release rather than a `releaseAndFail` helper: any error path (including dry-run) falls through to the deferred release, and the `committed` flag is set only after the round actor accepts the intent. Context is preserved via `context.WithoutCancel` so cleanup is not dropped when the caller disconnects. - `VTXOReader` / `VTXODescriptor` / `SelectedVTXO` break the vtxo → round → wallet import cycle by providing wallet-level types that don't reference `vtxo.Descriptor` directly. - Per-subsystem logging via `build.LoggerFromContext` (no global mutable loggers).