Skip to content

multi: unilateral-exit preparatory fixes and package-submission infra (1/5) - #260

Merged
Roasbeef merged 3 commits into
mainfrom
unroll-01-prep
Apr 18, 2026
Merged

multi: unilateral-exit preparatory fixes and package-submission infra (1/5)#260
Roasbeef merged 3 commits into
mainfrom
unroll-01-prep

Conversation

@Roasbeef

Copy link
Copy Markdown
Member

Summary

This is part 1 of 5 in a stacked split of #235. The original PR
bundled 15 commits across five distinct concerns into a single review.
This PR isolates the three preparatory commits so they can land
independently of the unroll subsystem itself.

Scope:

  • 188bd32multi: populate round metadata on ClientVTXO at confirmation. Fixes a VTXO-metadata race where callers reading
    between the round FSM's initial save and the VTXO manager's
    follow-up upsert would see zero CommitmentTxID, BatchExpiry, and
    CreatedHeight. Populates the fields from BoardingConfirmed in
    the first save; the manager's upsert becomes a no-op.

  • 362588cmulti: improve durable actor and DB transaction failure logging. Adds a Log option to DurableActorConfig so actors can
    surface warn/error through the caller's logger. Logs message type
    and delivery ID on Tell failures (makes silent nack loops visible).
    Adds WarnS to TransactionExecutor for begin/body/commit/retry
    exhaustion.

  • c2e1d51multi: add package submission and harness infrastructure. Adds SubmitPackageRequest/Response to
    chainsource with backend forwarding. Adds SubmitPackage to
    LNDBackend (via optional PackageSubmitter backed by bitcoind
    RPC). Adds Esplora package relay to lwwallet. Adds
    PackageSubmitter to LNDBackendFromLndClientConfig and daemon
    config. 15s timeout on RegisterConfirmationsNtfn, 10s on
    ConfActor backend registration. Exports
    GetLNDClientConn/BitcoindRPCUser/BitcoindRPCPass and adds
    BitcoindPackageSubmitter for itest package submission.

Stack

# Branch PR Scope
1/5 unroll-01-prep this PR preparatory fixes + infra
2/5 unroll-02-plan (stacked on 1) lib/recovery + unrollplan
3/5 unroll-03-txconfirm (stacked on 2) txconfirm actor
4/5 unroll-04-core (stacked on 3) vtxo + db + rpc + unroll/
5/5 unroll-05-wire (stacked on 4) daemon wiring + CLI

Supersedes #235.

Authorship

All three commits are authored by @ellemouton; the cherry-picks preserve
the Author: header.

Test plan

  • go build ./cmd/...
  • go vet ./...
  • CI: full unit + lint

@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 introduces support for atomic parent+child transaction package submission (V3 package relay) across various chain backends, including LND and Esplora. It also enhances logging for durable actors and database transactions, adds timeouts to confirmation registrations to prevent hangs, and ensures VTXO metadata is populated early to avoid data races. Feedback focuses on improving the robustness of the new SubmitPackage methods by ensuring they consistently accept and respect context.Context for network calls, avoiding the use of http.DefaultClient in the test harness, and refining error handling when joining transaction results to avoid malformed error strings.

Comment thread chainbackends/lnd.go Outdated
Comment on lines +37 to +38
SubmitPackage(parents []*wire.MsgTx, child *wire.MsgTx,
maxFeeRate *float64) (*btcjson.SubmitPackageResult, error)

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 SubmitPackage method in the PackageSubmitter interface should accept a context.Context as its first parameter. This allows implementations to respect the caller's cancellation signal or timeout, which is especially important for network-bound RPC calls. The calling method LNDBackend.SubmitPackage already receives a context that should be passed down to this interface.

Suggested change
SubmitPackage(parents []*wire.MsgTx, child *wire.MsgTx,
maxFeeRate *float64) (*btcjson.SubmitPackageResult, error)
SubmitPackage(ctx context.Context, parents []*wire.MsgTx, child *wire.MsgTx,
maxFeeRate *float64) (*btcjson.SubmitPackageResult, error)

Comment thread chainbackends/lnd.go
Comment on lines +211 to +213
result, err := b.packageSubmitter.SubmitPackage(
parents, child, nil,
)

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

Pass the ctx to the SubmitPackage call once the interface is updated to support it.

Suggested change
result, err := b.packageSubmitter.SubmitPackage(
parents, child, nil,
)
result, err := b.packageSubmitter.SubmitPackage(
ctx, parents, child, nil,
)

Comment thread chainbackends/lnd.go
Comment on lines +237 to +238
return fmt.Errorf("package not accepted: %s: %w",
result.PackageMsg, errors.Join(txErrors...))

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

If result.PackageMsg indicates failure but txErrors is empty, errors.Join(txErrors...) returns nil. Formatting an error with %w and a nil value can lead to confusing output (e.g., package not accepted: ...: %!w(<nil>)). Consider checking if txErrors is non-empty before joining, or use a pattern similar to the one in lwwallet/esplora.go where the presence of errors is explicitly checked before formatting.

Comment thread harness/bitcoind_package_submitter.go Outdated
Comment on lines +38 to +40
func (s *BitcoindPackageSubmitter) SubmitPackage(
parents []*wire.MsgTx, child *wire.MsgTx,
_ *float64) (*btcjson.SubmitPackageResult, error) {

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 SubmitPackage implementation should be updated to accept and use a context.Context for the HTTP request (using http.NewRequestWithContext). Additionally, it is recommended to avoid using http.DefaultClient as it lacks a timeout and uses shared state; instead, use a locally configured http.Client. Finally, the implementation should check resp.StatusCode (e.g., ensuring it is http.StatusOK) before reading the response body to handle HTTP-level errors like authentication failures gracefully.

Comment thread lwwallet/chain_backend.go Outdated
}
txHexes = append(txHexes, hex.EncodeToString(childBuf.Bytes()))

if err := b.esplora.SubmitPackage(txHexes); err != nil {

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

Pass the ctx to the SubmitPackage call to ensure the network request can be cancelled if the caller's context is done.

Suggested change
if err := b.esplora.SubmitPackage(txHexes); err != nil {
if err := b.esplora.SubmitPackage(ctx, txHexes); err != nil {

Comment thread lwwallet/esplora.go Outdated
// SubmitPackage posts a package of raw transaction hex strings to the
// Esplora /txs/package endpoint for atomic package relay. Transactions must be
// ordered by dependency with parents first and child last.
func (c *EsploraClient) SubmitPackage(txHexes []string) error {

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 SubmitPackage method should accept a context.Context and use it when making the POST request to the Esplora API (via http.NewRequestWithContext). This ensures that the network call respects the caller's timeout or cancellation signal.

Suggested change
func (c *EsploraClient) SubmitPackage(txHexes []string) error {
func (c *EsploraClient) SubmitPackage(ctx context.Context, txHexes []string) error {

@Roasbeef

Copy link
Copy Markdown
Member Author

@claude review this

@codex review this

@claude

claude Bot commented Apr 16, 2026

Copy link
Copy Markdown

Claude finished @Roasbeef's task in 2m 9s —— View job


PR Review

  • Read changed files and gather context
  • Review commit 1: VTXO metadata population at confirmation
  • Review commit 2: Durable actor and DB transaction logging
  • Review commit 3: Package submission and harness infrastructure
  • Synthesize findings and post final review

Summary

The three commits are well-scoped and address distinct concerns. The VTXO metadata fix is correct and straightforward. The logging improvements are solid. The package submission infrastructure mostly looks good but has a handful of issues worth fixing — some of which Gemini already flagged.


Commit 1 — VTXO metadata at confirmation (round/transitions.go)

The fix is correct. Populating CommitmentTxID, BatchExpiry, and CreatedHeight directly on the ClientVTXO structs before SaveVTXOs (round/transitions.go:1940-1944) eliminates the race window.

Bug: dbVTXOToDomainVTXO does not restore these fields from DB

db/round_store.go:1396-1412 returns a ClientVTXO without populating CommitmentTxID, BatchExpiry, or CreatedHeight, even though they are written by domainVTXOToInsertParams (db/round_store.go:1296-1300). After a daemon restart, any code reading VTXOs back from the DB (e.g., the unroll subsystem in PRs 2-5) will see zero values for these fields despite the correct data being on disk. The write path is now fixed — the read path needs the same treatment.

Fix this →


Commit 2 — Durable actor / DB transaction logging (baselib/actor/durable_actor.go)

The Log field addition to DurableActorConfig is clean. The WarnS calls added in processInTransaction now surface the message type and delivery ID, which will make silent-nack loops much easier to trace. No issues.


Commit 3 — Package submission infrastructure

1. PackageSubmitter interface missing context.Context (medium, echoes Gemini)

chainbackends/lnd.go:37-38:

SubmitPackage(parents []*wire.MsgTx, child *wire.MsgTx,
    maxFeeRate *float64) (*btcjson.SubmitPackageResult, error)

The calling site (LNDBackend.SubmitPackage) already receives a ctx and uses it for logging, but cannot thread it to the implementation. For the production BitcoindPackageSubmitter (or any future bitcoind RPC client wrapper), the RPC call will be uncontrollable. The interface should be:

SubmitPackage(ctx context.Context, parents []*wire.MsgTx,
    child *wire.MsgTx, maxFeeRate *float64) (*btcjson.SubmitPackageResult, error)

This also fixes harness/bitcoind_package_submitter.go and chainbackends/lnd.go:211.

2. EsploraClient.SubmitPackage missing context.Context (medium, echoes Gemini)

lwwallet/esplora.go:551:

func (c *EsploraClient) SubmitPackage(txHexes []string) error {

The method uses c.httpClient.Post(...) which has no cancellation. The caller in lwwallet/chain_backend.go:297 has ctx available but cannot pass it. Both the signature and the call site need updating, using http.NewRequestWithContext internally.

3. Malformed error string when txErrors is empty (medium, echoes Gemini)

chainbackends/lnd.go:236-239:

if result.PackageMsg != "success" {
    return fmt.Errorf("package not accepted: %s: %w",
        result.PackageMsg, errors.Join(txErrors...))
}

When txErrors is empty, errors.Join() returns nil. fmt.Errorf("...: %w", nil) produces "package not accepted: <msg>: %!w(<nil>)" — a garbled error string. The lwwallet/esplora.go implementation handles this correctly by branching on len(txErrors) == 0 first. Apply the same pattern here:

if result.PackageMsg != "success" {
    if len(txErrors) == 0 {
        return fmt.Errorf("package not accepted: %s",
            result.PackageMsg)
    }
    return fmt.Errorf("package not accepted: %s: %w",
        result.PackageMsg, errors.Join(txErrors...))
}

Fix this →

4. Harness: http.DefaultClient, no status check, context missing (low–medium)

harness/bitcoind_package_submitter.go:76-93:

  • http.NewRequest should be http.NewRequestWithContext (once the interface gains ctx).
  • http.DefaultClient has no timeout; a bitcoind node under load could hang the test. A local &http.Client{Timeout: 30 * time.Second} is safer.
  • There is no resp.StatusCode check before the json.Unmarshal call. A 401/403 from a misconfigured auth will produce a confusing JSON-parse error rather than a clear "HTTP 401" message. The esplora implementation (lwwallet/esplora.go:571-574) checks status first — apply the same pattern.

5. maxFeeRate silently dropped in harness (low)

harness/bitcoind_package_submitter.go:40: the parameter is discarded (_ *float64) and the RPC hardcodes maxfeerate=0. The comment explains the rationale (CPFP child high feerate), but a reader of the interface won't see this. A brief comment on the interface itself or a named constant would help.

6. Hard-coded timeout magic numbers (low)

chainbackends/lndclient_adapters.go:217 (15 s) and chainsource/conf_actor.go:161 (10 s) are undocumented magic numbers. Named constants (confNtfnRegTimeout, backendRegTimeout) would make intent clearer and be easier to tune.


Verdict

The VTXO metadata race fix and logging improvements are ready to land. For the package submission commit, the context-threading gap (PackageSubmitter interface, EsploraClient.SubmitPackage) and the malformed error string are the only issues worth blocking on before merge; the rest are polish. The missing DB read-back of the new VTXO metadata fields should also be addressed before the unroll subsystem (PRs 2-5) depends on it.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 84d72c1113

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread round/interfaces.go
Comment on lines +449 to +451
// CommitmentTxID is the txid of the round's commitment
// transaction. Populated at confirmation time.
CommitmentTxID chainhash.Hash

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Populate new ClientVTXO metadata on DB readback

This introduces CommitmentTxID/BatchExpiry/CreatedHeight on ClientVTXO, and SaveVTXOs now persists those values, but db.RoundPersistenceStore.dbVTXOToDomainVTXO still rebuilds ClientVTXO without assigning the new fields. As a result, callers using GetVTXO/ListVTXOs continue receiving zero metadata even after the write path was updated, which undermines the metadata-race fix for round-store readers.

Useful? React with 👍 / 👎.

Comment thread chainbackends/lnd.go
Comment on lines +236 to +238
if result.PackageMsg != "success" {
return fmt.Errorf("package not accepted: %s: %w",
result.PackageMsg, errors.Join(txErrors...))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3 Badge Handle package rejection when txErrors is empty

If PackageMsg != "success" but no per-transaction errors are present, errors.Join(txErrors...) is nil and fmt.Errorf("...: %w", nil) emits %!w(<nil>) in the error string. That malformed error text can occur on package-level rejections without tx-level details and makes diagnostics/error matching brittle; branch to a non-%w error format when no tx errors were collected.

Useful? React with 👍 / 👎.

The round FSM saves VTXOs to the store before the VTXO manager
sends VTXOCreatedNotification. Previously, CommitmentTxID,
BatchExpiry, and CreatedHeight were left zero in the first save
and filled in asynchronously by the manager's second upsert.
This created a race: callers reading the VTXO between the two
saves would see incomplete metadata.

Fix: add CommitmentTxID, BatchExpiry, and CreatedHeight fields
to ClientVTXO and populate them from the BoardingConfirmed event
before SaveVTXOs. The first write is now complete, making the
manager's upsert a harmless no-op.
Add a Log option to DurableActorConfig so actors can surface warning
and error logs through the caller's logger instead of the context
fallback. Log message type and delivery ID on Tell failures so
silent nack loops become visible.

Add WarnS calls to TransactionExecutor for begin, body, commit, and
retry-exhaustion failures so DB transaction errors are observable
without debug-level logging.
Add SubmitPackageRequest/Response to chainsource with backend
forwarding. Add SubmitPackage to LNDBackend with optional
PackageSubmitter interface (backed by bitcoind RPC). Add Esplora
package relay support to lwwallet chain backend.

Add PackageSubmitter field to LNDBackendFromLndClientConfig and
daemon Config for wiring.

Add 15-second timeout to LndClientChainNotifier's
RegisterConfirmationsNtfn to prevent hangs under heavy block load.
Add 10-second timeout to ConfActor's backend registration.

Export GetLNDClientConn, BitcoindRPCUser, BitcoindRPCPass from the
harness. Add BitcoindPackageSubmitter for itest package submission
via direct bitcoind JSON-RPC. Export WalletKit() on BoardingBackend.
@Roasbeef
Roasbeef merged commit c56f1f3 into main Apr 18, 2026
16 checks passed
ellemouton pushed a commit that referenced this pull request May 22, 2026
…-04-21-0

docs: nightly doc-gardening sweep 2026-04-21
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.

2 participants