Skip to content

multi: extract darepocli commands, add darepod RunWithContext - #174

Merged
Roasbeef merged 22 commits into
mainfrom
darepotest-repl
Mar 12, 2026
Merged

multi: extract darepocli commands, add darepod RunWithContext#174
Roasbeef merged 22 commits into
mainfrom
darepotest-repl

Conversation

@sputn1ck

Copy link
Copy Markdown
Member

Summary

  • Extract darepocli commands from package main into importable
    darepoclicommands sub-package with exported NewRootCmd(),
    enabling embedding in the darepotest REPL.
  • Add RunWithContext(ctx) to darepod.Server as a context-based
    alternative to RunUntilShutdown(interceptor), allowing test
    harnesses and the REPL to manage multiple concurrent darepod
    instances without the process-global signal handler.
  • Add /darepocli binary to .gitignore.

Test plan

  • go build ./cmd/darepocli/ — existing binary still compiles
  • go build ./darepod/ — package compiles with new API
  • Existing darepocli commands work identically (thin wrapper)
  • make unit pkg=internal/repl — REPL tests pass
  • Manual: darepotest repl starts darepod via RunWithContext

Copilot AI review requested due to automatic review settings March 11, 2026 16:30
@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the modularity and testability of the darepo-client project. By extracting the darepocli commands into a dedicated sub-package, the CLI functionality can now be easily embedded and reused, particularly in test harnesses and REPL environments. The introduction of RunWithContext provides a more flexible and context-aware mechanism for managing darepod instances, moving away from process-global signal handling. Additionally, a new RPC and CLI command facilitate the boarding process for UTXOs.

Highlights

  • CLI Modularity: The darepocli command-line interface has been refactored into a new, importable darepoclicommands sub-package, making its commands reusable and enabling embedding in test environments.
  • Daemon Lifecycle Management: A new RunWithContext(ctx) method was added to darepod.Server, providing a context-based daemon lifecycle management alternative to the signal-based RunUntilShutdown, which is beneficial for test harnesses and REPLs.
  • New RPC and CLI Command: A new Board RPC and corresponding darepocli board command were introduced to trigger the registration of confirmed boarding UTXOs.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • .gitignore
    • Added /darepocli to ignore the generated binary.
  • cmd/darepocli/client.go
    • Renamed to cmd/darepocli/darepoclicommands/client.go and refactored to darepoclicommands package.
  • cmd/darepocli/cmd_getinfo.go
    • Renamed to cmd/darepocli/darepoclicommands/cmd_getinfo.go and refactored to darepoclicommands package.
  • cmd/darepocli/cmd_mcp.go
    • Renamed to cmd/darepocli/darepoclicommands/cmd_mcp.go and refactored to darepoclicommands package.
  • cmd/darepocli/cmd_schema.go
    • Renamed to cmd/darepocli/darepoclicommands/cmd_schema.go, refactored to darepoclicommands package, and updated printError to PrintError.
  • cmd/darepocli/cmd_send.go
    • Renamed to cmd/darepocli/darepoclicommands/cmd_send.go and refactored to darepoclicommands package.
  • cmd/darepocli/cmd_vtxos.go
    • Renamed to cmd/darepocli/darepoclicommands/cmd_vtxos.go, refactored to darepoclicommands package, and updated printError to PrintError.
  • cmd/darepocli/cmd_wallet.go
    • Renamed to cmd/darepocli/darepoclicommands/cmd_wallet.go and refactored to darepoclicommands package.
  • cmd/darepocli/darepoclicommands/cmd_board.go
    • Added new file defining the board subcommand for darepocli.
  • cmd/darepocli/darepoclicommands/root.go
    • Added new file defining NewRootCmd and PrintError for the darepocli commands.
  • cmd/darepocli/format.go
    • Renamed to cmd/darepocli/darepoclicommands/format.go and refactored to darepoclicommands package.
  • cmd/darepocli/json_input.go
    • Renamed to cmd/darepocli/darepoclicommands/json_input.go and refactored to darepoclicommands package.
  • cmd/darepocli/main.go
    • Modified to import and utilize the new darepoclicommands package for CLI execution and error handling.
  • cmd/darepocli/schema_registry.go
    • Renamed to cmd/darepocli/darepoclicommands/schema_registry.go and refactored to darepoclicommands package.
  • daemonrpc/daemon.pb.go
    • Generated new BoardRequest and BoardResponse protobuf messages and updated service descriptor.
  • daemonrpc/daemon.proto
    • Added BoardRequest and BoardResponse messages and a new Board RPC to the DaemonService.
  • daemonrpc/daemon_grpc.pb.go
    • Updated gRPC client and server interfaces to include the new Board RPC.
  • daemonrpc/daemon_mailboxrpc.pb.go
    • Implemented mailbox server and client handlers for the new Board RPC.
  • darepod/logging.go
    • Added SetupLoggersWithShutdownFn to support context-based daemon shutdown.
  • darepod/rpc_server.go
    • Imported round package and implemented the Board RPC handler.
  • darepod/server.go
    • Refactored daemon startup logic into a shared run method, introduced RunWithContext for context-managed daemon lifecycle, and updated RunUntilShutdown to use the new run method.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request effectively refactors the darepocli commands into a reusable package and introduces a context-aware RunWithContext method for the darepod server, which is a great improvement for testing and embedding. The new board command is also a welcome addition.

My review includes a couple of suggestions for improvement:

  • In darepod/rpc_server.go, I've suggested handling a specific error case in the Board RPC to provide a more informative response to the client, aligning with the API definition.
  • In darepod/logging.go, I've pointed out some code duplication that could be refactored to improve long-term maintainability.

Overall, the changes are well-structured and move the project in a good direction.

Comment thread darepod/rpc_server.go Outdated
Comment on lines +572 to +576
if result.IsErr() {
return nil, status.Errorf(codes.Internal,
"board registration failed: %v",
result.Err())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The Board RPC can fail if there are no boarding UTXOs available, with an error message containing no inputs to register. Currently, this is returned as a generic internal error to the client. To adhere to the API contract defined in the protobuf file (which specifies a no_boarding_utxos status), this specific error case should be handled to return a BoardResponse with the appropriate status.

While string matching on the error is not ideal, it's a pragmatic short-term solution. A TODO to use a typed error in the future would be a good addition.

if result.IsErr() {
		err := result.Err()
		// TODO: Use a typed error once available from the round actor.
		if strings.Contains(err.Error(), "no inputs to register") {
			log.InfoS(ctx, "Board registration skipped: no boarding UTXOs")
			return &daemonrpc.BoardResponse{
				Status: "no_boarding_utxos",
			}, nil
		}

		return nil, status.Errorf(codes.Internal,
			"board registration failed: %v", err)
	}

Comment thread darepod/logging.go Outdated
Comment on lines +110 to +130
subsystems := []struct {
name string
useLogger func(btclog.Logger)
}{
{Subsystem, UseLogger},
{actor.Subsystem, actor.UseLogger},
{round.Subsystem, round.UseLogger},
{oor.Subsystem, oor.UseLogger},
{vtxo.Subsystem, vtxo.UseLogger},
{wallet.Subsystem, wallet.UseLogger},
{lwwallet.Subsystem, lwwallet.UseLogger},
{serverconn.Subsystem, serverconn.UseLogger},
{chainbackends.Subsystem, chainbackends.UseLogger},
{
chainbackends.LndClientSubsystem,
chainbackends.UseLndClientLogger,
},
{lndbackend.Subsystem, lndbackend.UseLogger},
{indexer.Subsystem, indexer.UseLogger},
{db.Subsystem, db.UseLogger},
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The subsystems slice defined here is a duplicate of the one in the (now unused) SetupLoggers function. To improve maintainability and avoid having to update two lists in the future, consider extracting this slice into a package-level variable. Both SetupLoggers and SetupLoggersWithShutdownFn can then iterate over this shared slice.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR modularizes darepocli so it can be embedded (e.g., in the REPL), adds a context-driven lifecycle entrypoint for darepod.Server, and introduces a new Board RPC/CLI command for triggering round registration.

Changes:

  • Extract darepocli cobra command tree into cmd/darepocli/darepoclicommands with exported NewRootCmd() and PrintError().
  • Add Server.RunWithContext(ctx) and refactor shared startup into Server.run(ctx, shutdownFn).
  • Add Board RPC (proto + generated stubs) and corresponding server/CLI implementations; ignore /darepocli binary in .gitignore.

Reviewed changes

Copilot reviewed 20 out of 21 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
darepod/server.go Refactors daemon startup/shutdown to support context-managed lifecycle via RunWithContext.
darepod/logging.go Adds SetupLoggersWithShutdownFn to decouple logging-triggered shutdown from signal.Interceptor.
darepod/rpc_server.go Adds Board RPC handler that triggers round registration via the round actor.
daemonrpc/daemon.proto Defines new Board RPC and request/response messages.
daemonrpc/daemon.pb.go Regenerated protobuf types to include BoardRequest/BoardResponse and service descriptor updates.
daemonrpc/daemon_grpc.pb.go Regenerated gRPC client/server stubs to include Board.
daemonrpc/daemon_mailboxrpc.pb.go Regenerated mailbox RPC router/client to include Board.
cmd/darepocli/main.go Converts binary into a thin wrapper around darepoclicommands.NewRootCmd().
cmd/darepocli/darepoclicommands/root.go New importable root command + shared PrintError; wires in new board subcommand.
cmd/darepocli/darepoclicommands/cmd_board.go Adds darepocli board command that calls the new Board RPC.
cmd/darepocli/darepoclicommands/schema_registry.go Moves schema registry into darepoclicommands package.
cmd/darepocli/darepoclicommands/json_input.go Moves JSON parsing utilities into darepoclicommands package.
cmd/darepocli/darepoclicommands/format.go Moves formatting helpers into darepoclicommands package.
cmd/darepocli/darepoclicommands/client.go Moves daemon client connection logic into darepoclicommands package.
cmd/darepocli/darepoclicommands/cmd_getinfo.go Repackages existing command under darepoclicommands.
cmd/darepocli/darepoclicommands/cmd_wallet.go Repackages existing command under darepoclicommands.
cmd/darepocli/darepoclicommands/cmd_vtxos.go Repackages existing command; updates printError -> PrintError.
cmd/darepocli/darepoclicommands/cmd_send.go Repackages existing command under darepoclicommands.
cmd/darepocli/darepoclicommands/cmd_schema.go Repackages existing command; updates printError -> PrintError.
cmd/darepocli/darepoclicommands/cmd_mcp.go Repackages existing command under darepoclicommands.
.gitignore Adds /darepocli binary to ignore list.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread darepod/rpc_server.go Outdated
Comment on lines +569 to +582
future := roundRef.Ask(ctx, msg)
result := future.Await(ctx)

if result.IsErr() {
return nil, status.Errorf(codes.Internal,
"board registration failed: %v",
result.Err())
}

log.InfoS(ctx, "Board registration triggered")

return &daemonrpc.BoardResponse{
Status: "registered",
}, nil

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Board currently always returns status "registered" on success and maps any round-actor error to codes.Internal. If there is no pending round (or no confirmed boarding intents), the round actor can return errors like "no pending round for event *round.RegistrationRequested" which are expected/valid states and should map to a user-facing result (e.g., status "no_boarding_utxos" per daemon.proto, or codes.FailedPrecondition) instead of Internal.

Copilot uses AI. Check for mistakes.
Comment thread darepod/server.go Outdated
// callers manage daemon lifecycle via context cancellation instead
// of requiring a signal.Interceptor (which is process-global).
func (s *Server) RunWithContext(ctx context.Context) error {
return s.run(ctx, func() {})

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RunWithContext passes a no-op shutdownFn into run(), which means critical log events (via SubLoggerManager's shutdown callback) can no longer trigger a shutdown when using the context-based entrypoint. Consider deriving a child context with context.WithCancel(ctx) inside RunWithContext and passing the cancel func as shutdownFn, so both caller cancellation and critical logs can stop the daemon.

Suggested change
return s.run(ctx, func() {})
ctx, cancel := context.WithCancel(ctx)
defer cancel()
return s.run(ctx, cancel)

Copilot uses AI. Check for mistakes.
Comment thread darepod/rpc_server.go Outdated
Comment on lines +563 to +564
// ServerMessageNotification, matching the pattern used by
// the systest client (systest/client.go:TriggerRegistration).

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment references systest/client.go:TriggerRegistration, but there is no systest/client.go in the repo. Please update the reference to an existing file/location (or remove it) so future readers can find the pattern being referenced.

Suggested change
// ServerMessageNotification, matching the pattern used by
// the systest client (systest/client.go:TriggerRegistration).
// ServerMessageNotification, following the standard client
// interaction pattern for the round service.

Copilot uses AI. Check for mistakes.
Comment thread darepod/rpc_server.go Outdated
// to acknowledge the JoinRoundRequest via the mailbox before giving
// up. This must be long enough for the durable egress path and one
// mailbox round-trip.
const boardJoinTimeout = 30 * time.Second

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should instead be a non-blocking call. Then we'll add a CLI rounds command that allows a user to check on pending/requested rounds.

Comment thread darepod/rpc_server.go Outdated
defer assemblyTicker.Stop()

var reachedAssembly bool
for !reachedAssembly {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can simplify this a lot, instead we can just go through the wallet entirely here. Then have it send the message to the round actor as needed re the VTXO requests.

Comment thread darepod/rpc_server.go Outdated

for {
select {
case <-joinCtx.Done():

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, we can remove this, then make a rounds CLI command that lets the caller list/query the active rounds (and the state of the round), and also watch to be notified when a round concludes or advances.

Comment thread darepod/server.go

// JoinAck: server accepted the client's join request.
addRoundRoute(
roundpb.MethodJoinAck,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@Roasbeef

Copy link
Copy Markdown
Member

@claude review this

@claude

claude Bot commented Mar 12, 2026

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

sputn1ck and others added 12 commits March 11, 2026 22:45
Add Board RPC with BoardRequest/BoardResponse messages to the
DaemonService proto. This enables clients to trigger round
registration for confirmed boarding UTXOs via the daemon's gRPC
API. Regenerated Go stubs via make rpc.
Implement the Board RPC handler in darepod that triggers round
registration by sending RegistrationRequested to the round actor
via its service key. Add the corresponding darepocli board CLI
command and register it in the root command.

The implementation mirrors the pattern used in
systest/client.go:TriggerRegistration, sending a
ServerMessageNotification with RegistrationRequested to the
round actor via Ask.
Move all command implementations from package main into a new
darepoclicommands sub-package with an exported NewRootCmd()
constructor. This allows other packages (e.g., darepotest repl)
to import and embed the darepocli command tree without compiling
a separate binary.

The main.go becomes a thin wrapper that calls
darepoclicommands.NewRootCmd().Execute().
Refactor RunUntilShutdown to extract the core startup logic into
a shared run(ctx, shutdownFn) method, then add RunWithContext as
a context-based entry point that avoids the process-global
signal.Interceptor.

This allows test harnesses and the darepotest REPL to manage
multiple concurrent darepod instances via context cancellation
instead of fighting over the singleton OS signal handler.
Move the DAREPOD_WALLET_PASSWORD environment variable check before
the stdin pipe detection in readPassword(). This prevents the
function from blocking on a pipe that never produces data (e.g.
when running inside the darepotest REPL which pipes stdin through
readline).
Add Service and Method fields to the serverconn TLV wire
format so that outbound mailbox envelopes carry the
fully-qualified protobuf service and method names needed
for dispatch on the operator side. The round actor's
processOutbox extracts these from the new RpcRouted
interface that client outbox messages implement.

Without routing metadata the operator's clientconn ingress
loop silently drops envelopes because it cannot match them
to a handler.
Several fixes to make the full boarding flow work:

- Add signing_key field (field 6) to VTXORequest proto so
  the server can verify BIP-322 join-auth messages.
- Serialize SigningKey in JoinRoundRequest.ToProto() and add
  RpcService/RpcMethod routing methods to all client outbox
  message types.
- Add MethodJoinAck constant and register the JoinAck
  event route in darepod so the client FSM receives operator
  acknowledgment.
- Rewrite Board RPC to fetch boarding balance, compute VTXO
  amounts, register output requests, and poll the FSM until
  the operator acknowledges the join (RoundJoined state).
- Set default MaxOperatorFee in round client config.
The tree node constructors (NewLeafNode, NewBranchNode)
compute FinalKey via MuSig2 key aggregation, but proto
deserialization bypasses them. Without FinalKey, signature
verification in VerifySigned fails with a nil-pointer
dereference after the operator sends aggregated signatures.

Compute FinalKey for each deserialized node that has
cosigners, copying the slice first since MuSig2
AggregateKeys sorts in-place. Update the round-trip test
to clear FinalKey on both sides since it is a derived
field not carried in the proto.
Add an exported method that connects to the harness LND
node, requests a P2WKH address, faucets the given amount,
and mines 6 blocks to confirm. This allows callers like
darepotest to ensure the operator has funds for commitment
transactions without manual docker exec steps.
Replace the standalone RpcRouted interface with a ServiceMethod()
method on the ServerMessage interface, reusing the existing
mailboxrpc.ServiceMethod type that the unary facade, codegen stubs,
and EventRouter already use. This simplifies the outbound event path
by removing the optional type assertion in processOutbox and making
routing metadata mandatory for all server-bound messages.

The OOR transport messages (SendSubmitPackageRequest,
SendFinalizePackageRequest, SendIncomingAckRequest) gain
ServiceMethod() implementations, fixing a gap where their outbound
envelopes previously had empty routing metadata. Local-only OOR
types (MarkInputsSpentRequest, ScheduleRetryRequest) no longer
satisfy ServerMessage since they lack routing and are never sent
to the server.

Client-to-server method name constants are added to roundpb and
oorpb alongside the existing server-to-client push event constants.
Extract the repeated subsystem registration list into a package-level
allSubsystems slice. Both SetupLoggers and SetupLoggersWithShutdownFn
now iterate over this single source of truth, so adding a new subsystem
requires only one edit instead of maintaining two identical lists that
can drift apart.
@Roasbeef

Copy link
Copy Markdown
Member

@claude review this

@claude

claude Bot commented Mar 12, 2026

Copy link
Copy Markdown

Claude finished @Roasbeef's task in 4m 1s —— View job


Code Review

This PR is in good shape overall — the darepoclicommands extraction is clean and the RunWithContext refactor is well-structured. The allSubsystems deduplication and the Board non-blocking redesign are both improvements. A few issues worth addressing before merge:


Bugs

1. RunWithContext leaks context on normal shutdowndarepod/server.go:228

func (s *Server) RunWithContext(ctx context.Context) error {
    ctx, cancel := context.WithCancel(ctx)
    return s.run(ctx, cancel)   // no defer cancel()
}

cancel is passed as shutdownFn to run() so critical log events can trigger it, but if run() returns through any path that doesn't call cancel (e.g., an early error return before actors start, or normal context cancellation propagating through ctx.Done()), the derived child context leaks. RunUntilShutdown correctly does defer cancel() after context.WithCancel. This needs the same:

func (s *Server) RunWithContext(ctx context.Context) error {
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()
    return s.run(ctx, cancel)
}

Fix this →


2. Board RPC missing walletRef.IsSome() guarddarepod/rpc_server.go:563

// requireWalletReady() checks the walletReady channel...
if err := r.requireWalletReady(); err != nil { return nil, err }

// ...but walletRef is set at step 9, while the gRPC server
// starts at step 6 and walletReady is closed at step 1 (lnd mode).
wRef := r.server.walletRef.UnsafeFromSome()  // can panic!

In lnd mode, markWalletReady() is called in step 1 (initLndBackend), the gRPC server starts listening in step 6, and walletRef is set in step 9 (initWalletActor). There's a window between steps 6 and 9 where a caller can pass the requireWalletReady() check and then hit a panic on UnsafeFromSome(). Every other handler that needs walletRef (e.g., SendVTXO line 360, GetBalance line 154) does an explicit walletRef.IsSome() check. Board should match that pattern.
Fix this →


3. Board RPC returns codes.Internal for "no confirmed boarding balance"darepod/rpc_server.go:572

The proto comment (daemon.proto:368) explicitly documents "no_boarding_utxos" as a valid status value:

// status is "registered" on success, "no_boarding_utxos" if
// there are no confirmed boarding UTXOs available.
string status = 1;

But when wallet.handleBoard returns fn.Err because totalBalance == 0 (wallet.go:741), the RPC handler wraps it as codes.Internal. This should instead be a success response with status: "no_boarding_utxos" since it's an expected, non-error condition. Right now the proto field is dead — the "no_boarding_utxos" status can never reach the caller.


4. Silent Tell failure in wallet.handleBoard still returns successwallet/wallet.go:771

When roundRef.Tell() fails, the code logs a warning and falls through to return a BoardResponse with status: "registered". The RPC caller gets success even though the board was never queued. If Tell fails (e.g., actor system overloaded or shutting down), this should return an error, not a success response.


Style / Logging

5. WarnS call passes error without keywallet/wallet.go:774

// Wrong — error as positional variadic argument
a.logger(ctx).WarnS(ctx, "Failed to forward board to round actor", err)

Per the project's structured-logging guidelines, errors should be passed as a key-value pair:

a.logger(ctx).WarnS(ctx, "Failed to forward board to round actor",
    btclog.Fmt("err", "%v", err))

The nil in the actorSystem == nil branch (line 783) has the same issue — WarnS with nil as the error argument. Use slog.String("reason", "no actor system") or similar instead of nil.


Design Observations

6. WatchRounds only observes in-memory FSM state

The polling loop in WatchRounds calls queryRoundStates which only queries the live round actor (pending/in-progress rounds). When a round transitions to ConfirmedState it's removed from the actor's in-memory map and written to DB. A client watching for round completion would see the round disappear from the stream with no final "confirmed" notification — it would just stop appearing. The docs and proto comment should make this explicit, or the implementation should emit a final transition event before the round is removed from the actor. This limitation also means the streaming command can't replace a simple "poll ListRounds until status is confirmed" loop.

7. prevStates key collision for temp rounds in WatchRounds

key := info.RoundId
if info.IsTemp {
    key = "temp:" + key  // RoundId is "" for temp rounds → always "temp:"
}

If there were ever multiple concurrent temp rounds, they'd all map to "temp:" and only the last one's state would be tracked. Not a problem today since there's one primary FSM, but worth a comment that this assumption is intentional.


Minor / Nits

  • cmd_board.go: parseRequest(cmd, req, nil) — since BoardRequest has no fields, the parseRequest call is a no-op. Either drop it or add a comment explaining why it's there for consistency.
  • ListRounds cursor: the next-page token is correctly set to the last item in the truncated page. Looks correct.
  • The allSubsystems deduplication in logging.go is a clean improvement — addressed Gemini's earlier feedback.
  • The SetupLoggers function now looks unused since run() calls SetupLoggersWithShutdownFn directly in all paths. If it's only kept for external callers, a comment to that effect would help.

RunWithContext was passing a no-op func(){} as the shutdownFn to
s.run(), which meant critical-error paths that call shutdownFn to
trigger graceful shutdown were silently ignored. Derive a child
context with context.WithCancel and pass the cancel func instead,
matching the semantics of RunUntilShutdown which wires the
signal.Interceptor's RequestShutdown.
This commit restructures the boarding flow so that the Board RPC is
a thin, non-blocking gateway: it validates that the wallet is ready,
fetches operator terms, and delegates the entire flow to the wallet
actor via a BoardRequest message.

The wallet actor's new handleBoard handler fetches the confirmed
boarding balance from its store, computes the VTXO output amount
(balance minus operator fee), validates it exceeds the dust limit,
and then sends a TriggerBoardMsg to the round actor. This message
combines the two previously separate steps (RegisterVTXORequests +
RegistrationRequested) into one atomic Tell.

The round actor's handleTriggerBoard handler receives the amounts,
builds VTXO requests via buildVTXORequest, registers an
IntentPackage on the assembling round, and triggers registration
via RegistrationRequested — all within a single handler.

The old Board RPC had two blocking polling loops: one waiting for
PendingRoundAssembly and another waiting for RoundJoined, each
with 30-second timeouts. These are replaced by a single Ask to the
wallet actor that returns immediately. Callers can use
ListRounds/WatchRounds (added in a subsequent commit) to observe
round progress.
Define the proto schema for round observability: RoundState enum
with all 14 FSM states, RoundVTXOInfo for per-round VTXO details,
and RoundInfo combining round_id, state, is_temp, and vtxos.

Add ListRoundsRequest with page_size, page_token, and
persisted_only fields. Pending (in-memory) rounds are always
returned and do not count against the page limit. Add
WatchRoundsRequest/Response for server-streaming state updates.

On the database side, add a ListRoundsPaginated SQL query using
cursor-based pagination (WHERE round_id > cursor ORDER BY
round_id ASC LIMIT N) so we never load all rounds into memory.
Regenerated with protoc v5.28.0 / protoc-gen-go v1.36.6 after
adding RoundState enum, RoundVTXOInfo, RoundInfo, ListRounds,
and WatchRounds to daemon.proto.
Regenerated via make sqlc after adding the ListRoundsPaginated
query to db/sqlc/queries/round.sql.
Add SQL-backed pagination for persisted rounds via
RoundPersistenceStore.ListRoundsPaginated, which returns
lightweight RoundSummary structs (round ID, status, and VTXO
outpoints/amounts) without deserializing full commitment
transactions or tree data.

The ListRounds RPC splits round sources: pending (in-memory) rounds
are fetched from the round actor and always returned on the first
page, while persisted rounds (input_sig_sent, confirmed) come from
SQL with cursor-based pagination. Each persisted round includes its
VTXOs via ListVTXOsByRound. The persisted_only flag skips in-memory
rounds entirely.

WatchRounds opens a server-streaming connection that polls the
round actor at 500ms intervals and pushes RoundInfo messages
whenever a round's FSM state changes, enabling real-time
observability without blocking RPCs.

The Server struct now stores a roundStore reference so the RPC
layer can query persisted rounds directly without routing through
the actor system.
Add a rounds parent command with list and watch subcommands for
round FSM observability.

rounds list calls the ListRounds RPC and prints the JSON response.
It supports --persisted-only, --page-size, and --page-token flags
for pagination control. rounds watch opens a WatchRounds streaming
connection and prints each state update as it arrives.
Test cursor-based pagination across 5 rounds: empty database
returns empty, page_size limiting works correctly, cursor advances
through pages, VTXOs are attached to the correct rounds (2 VTXOs
each for the first two rounds), and status transitions from
input_sig_sent to confirmed after FinalizeRound are reflected in
subsequent queries.
Update CLAUDE.md/AGENTS.md for packages affected by this PR:

- darepod: add RPCServer type, Board non-blocking invariant,
  ListRounds split (pending vs persisted), roundStore reference.
- round: add TriggerBoardMsg receive from wallet via lib/actormsg.
- wallet: add BoardRequest/BoardResponse types, TriggerBoardMsg send.
- db: add RoundSummary/VTXOSummary types, ListRoundsPaginated method.
- lib: add TriggerBoardMsg and other cross-package message types to
  lib/actormsg section.
- cmd/darepocli/darepoclicommands: new CLAUDE.md with command table.
Address golangci-lint findings: remove unnecessary int32 conversion
in ListRounds, use errors.Is for io.EOF check in rounds watch,
add nolint:funlen to processOutbox dispatch loop, wrap long comment
in actormsg, lowercase error string in cmd_board, remove leading
newline in cmd_wallet, and drop stale nolint:funlen from
RunUntilShutdown.
@Roasbeef
Roasbeef merged commit 747b67c into main Mar 12, 2026
15 of 16 checks passed
darioAnongba added a commit that referenced this pull request Aug 7, 2026
The commitment transition accepts one funding source per boarded
input: per-source amounts, proofs, witnesses, and signing plans, with
the request total pinned to the sources' sum. The tree root source
becomes a V1 compact path whose additional confirmed bases carry the
co-inputs' lineages, bound to the transition by the path verifier.
Requires tap-sdk multi-base proof paths (#174).
darioAnongba added a commit that referenced this pull request Aug 11, 2026
The commitment transition accepts one funding source per boarded
input: per-source amounts, proofs, witnesses, and signing plans, with
the request total pinned to the sources' sum. The tree root source
becomes a V1 compact path whose additional confirmed bases carry the
co-inputs' lineages, bound to the transition by the path verifier.
Requires tap-sdk multi-base proof paths (#174).
darioAnongba added a commit that referenced this pull request Aug 12, 2026
The commitment transition accepts one funding source per boarded
input: per-source amounts, proofs, witnesses, and signing plans, with
the request total pinned to the sources' sum. The tree root source
becomes a V1 compact path whose additional confirmed bases carry the
co-inputs' lineages, bound to the transition by the path verifier.
Requires tap-sdk multi-base proof paths (#174).
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.

3 participants