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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,4 @@ DS_Store
go.work.sum

/tools/custom-gcl
.reviews/
327 changes: 235 additions & 92 deletions arkrpc/indexer.pb.go

Large diffs are not rendered by default.

49 changes: 47 additions & 2 deletions arkrpc/indexer.proto
Original file line number Diff line number Diff line change
Expand Up @@ -472,11 +472,56 @@ message ListVTXOEventsByScriptsResponse {
uint64 next_cursor = 2;
}

// IncomingVTXOEvent is delivered as a mailbox EVENT envelope. It is a durable
// hint that a wallet should reconcile state by polling event feeds.
// VTXOOrigin distinguishes how a VTXO was created so the receiving
// client can apply the correct materialization logic.
enum VTXOOrigin {
// VTXO_ORIGIN_UNSPECIFIED is the default zero value.
VTXO_ORIGIN_UNSPECIFIED = 0;

// VTXO_ORIGIN_IN_ROUND indicates the VTXO was created as part of
// a confirmed round (directed in-round send).
VTXO_ORIGIN_IN_ROUND = 1;

// VTXO_ORIGIN_OOR indicates the VTXO was created via an
// out-of-round transfer.
VTXO_ORIGIN_OOR = 2;
}

// IncomingVTXOEvent is delivered as a mailbox EVENT envelope. For
// VTXO_CREATED events from confirmed rounds, the pk_script and
// value_sat fields carry enough data for the client to materialize
// the VTXO without a follow-up indexer query.
message IncomingVTXOEvent {
uint64 event_id = 1;
VTXOEventType type = 2;
OutPoint outpoint = 3;
VTXOStatus status = 4;

// pk_script is the VTXO output script. Present for CREATED events
// so the client can match against registered receive scripts.
bytes pk_script = 5;

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.

Perhaps we should use an enum/oneof here to add more structure to the proto? So we can more easily distinguiish if this is a new VTXO from an in round send, or an oor.


// value_sat is the VTXO amount in satoshis.
uint64 value_sat = 6;

// round_id identifies the round that created this VTXO.
string round_id = 7;

// batch_expiry_height is the absolute block height at which the
// batch sweep path becomes spendable. The server MUST compute
// this as confirmation_height + sweep_delay before publishing
// the event.
int32 batch_expiry_height = 8;

// relative_expiry is the CSV delay for the unilateral exit path.
uint32 relative_expiry = 9;

// origin indicates how the VTXO was created (in-round send vs
// out-of-round transfer).
VTXOOrigin origin = 10;

// commitment_txid is the transaction ID of the round's
// commitment transaction. This is distinct from the leaf txid
// carried in the outpoint field.
bytes commitment_txid = 11;
}
35 changes: 35 additions & 0 deletions darepod/incoming_vtxo_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package darepod

import (
"context"

"github.com/lightninglabs/darepo-client/db"
"github.com/lightninglabs/darepo-client/vtxo"
)

// ownedScriptLookupAdapter wraps db.OORArtifactPersistenceStore to
// satisfy the vtxo.OwnedScriptLookup interface. It converts the
// db-specific record type to the vtxo-level OwnedReceiveScript.
type ownedScriptLookupAdapter struct {
store *db.OORArtifactPersistenceStore
}

// LookupOwnedReceiveScript delegates to the underlying store and
// converts the result to a vtxo.OwnedReceiveScript.
func (a *ownedScriptLookupAdapter) LookupOwnedReceiveScript(
ctx context.Context,
pkScript []byte) (*vtxo.OwnedReceiveScript, error) {

rec, err := a.store.LookupOwnedReceiveScript(ctx, pkScript)
if err != nil {

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.

Perhaps we could specifically only skip not-found errors (or even just log) otherwise surface the error.

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.

codex: Returning Ok on store/materialization failures makes this mailbox event effectively lossy. IncomingVTXOEvent is delivered via Tell, so once we ack it there is no retry path here; a transient DB issue during lookup/save would silently drop the receive. Could we distinguish not found from real store errors and surface the latter instead of swallowing them?

return nil, err
}

return &vtxo.OwnedReceiveScript{
ClientKey: rec.ClientKey,
OperatorPubKey: rec.OperatorPubKey,
ExitDelay: rec.ExitDelay,
}, nil
}

var _ vtxo.OwnedScriptLookup = (*ownedScriptLookupAdapter)(nil)
86 changes: 86 additions & 0 deletions darepod/owned_script_checker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package darepod

import (
"context"
"database/sql"
"errors"
"fmt"
"time"

"github.com/btcsuite/btcd/btcec/v2"
"github.com/lightninglabs/darepo-client/db"
"github.com/lightninglabs/darepo-client/round"
fn "github.com/lightningnetwork/lnd/fn/v2"
"github.com/lightningnetwork/lnd/keychain"
)

// ownedScriptCheckerAdapter implements round.OwnedScriptChecker by
// looking up pkScripts in the owned_receive_scripts persistence store.
type ownedScriptCheckerAdapter struct {
store *db.OORArtifactPersistenceStore
}

var _ round.OwnedScriptChecker = (*ownedScriptCheckerAdapter)(nil)

// IsOwnedScript returns whether the pkScript is registered as an owned
// receive script in the OOR artifact store. Returns an error for real
// store failures; a not-found result returns false with no error.
func (a *ownedScriptCheckerAdapter) IsOwnedScript(ctx context.Context,

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.

nit: missing godoc.

pkScript []byte) fn.Result[bool] {

if a.store == nil {
return fn.Ok(false)
}

// Use a context that survives cancellation so the DB lookup
// completes even if the caller's context is being torn down
// (e.g., during round confirmation in a shutting-down FSM).
lookupCtx := context.WithoutCancel(ctx)

_, err := a.store.LookupOwnedReceiveScript(lookupCtx, pkScript)

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 don't think this is correct as is, we should instead surface the error and a bool flag if it returns the receive script?

if err != nil {
// Not-found means the script isn't ours.
if errors.Is(err, sql.ErrNoRows) {
return fn.Ok(false)
}

return fn.Err[bool](fmt.Errorf(
"lookup owned receive script: %w", err,
))
}

return fn.Ok(true)
}

// ownedScriptRegistrarAdapter implements round.OwnedScriptRegistrar by
// persisting pkScripts in the owned_receive_scripts table.
type ownedScriptRegistrarAdapter struct {
store *db.OORArtifactPersistenceStore
operatorKey *btcec.PublicKey
exitDelay uint32
}

var _ round.OwnedScriptRegistrar = (*ownedScriptRegistrarAdapter)(nil)

// RegisterOwnedScript persists the pkScript as a locally owned receive
// script in the OOR artifact store.
func (a *ownedScriptRegistrarAdapter) RegisterOwnedScript(

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.

nit: missing godoc

ctx context.Context, pkScript []byte,
ownerKey keychain.KeyDescriptor) error {

if a.store == nil {
return fmt.Errorf("store is nil")
}

return a.store.UpsertOwnedReceiveScript(
ctx, db.OwnedReceiveScriptRecord{
PkScript: pkScript,
ClientKey: ownerKey,
OperatorPubKey: a.operatorKey,
ExitDelay: int64(a.exitDelay),
Source: db.OwnedReceiveScriptSourceWallet,
CreatedAt: time.Now(),
LastUsedAt: fn.None[time.Time](),
},
)
}
24 changes: 22 additions & 2 deletions darepod/rpc_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -635,11 +635,21 @@ func (r *RPCServer) SendVTXO(ctx context.Context,
return nil, err
}

// TODO(#241): Tune this cap based on round tree constraints
// and consider making it configurable.
const maxRecipients = 256

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.

Should make an issue to tune this param later.


if len(req.Recipients) == 0 {
return nil, status.Errorf(codes.InvalidArgument,
"at least one recipient is required")
}

if len(req.Recipients) > maxRecipients {
return nil, status.Errorf(codes.InvalidArgument,
"too many recipients: %d (max %d)",
len(req.Recipients), maxRecipients)
}

// Resolve each recipient's pkScript and client pubkey from
// the proto Output destination.
recipients := make(
Expand All @@ -656,14 +666,24 @@ func (r *RPCServer) SendVTXO(ctx context.Context,
)
}

if out.AmountSat <= 0 {
if out.AmountSat <= 0 ||
out.AmountSat > int64(btcutil.MaxSatoshi) {

return nil, status.Errorf(
codes.InvalidArgument,
"recipient %d: amount must be "+
"positive", i,
"between 1 and %d",
i, int64(btcutil.MaxSatoshi),
)
}

// Overflow-safe addition.
if totalAmount > int64(btcutil.MaxSatoshi)-out.AmountSat {
return nil, status.Errorf(
codes.InvalidArgument,
"total amount overflows max supply")
}

pkScript, clientKey, err := r.resolveRecipientOutput(
out,
)
Expand Down
Loading
Loading