pool/account/interfaces.go
Oliver Gugger 30c7ef6531
multi: bump lnd+lndclient compile time dependency
This commit bumps the compile time dependency of lnd and lndclient to
the 0.16.x branch. This makes the code forward compatible but does not
yet change anything with respect to the minimum required version the
user needs to run.
2023-03-08 17:42:04 +01:00

677 lines
24 KiB
Go

package account
import (
"context"
"errors"
"fmt"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcwallet/wtxmgr"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/pool/poolscript"
"github.com/lightninglabs/pool/terms"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
)
var (
// ErrNoPendingBatch is an error returned when we attempt to retrieve
// the ID of a pending batch, but one does not exist.
ErrNoPendingBatch = errors.New("no pending batch found")
)
// Reservation contains information about the different keys required for to
// create a new account.
type Reservation struct {
// AuctioneerKey is the base auctioneer's key in the 2-of-2 multi-sig
// construction of a CLM account. This key will never be included in the
// account script, but rather it will be tweaked with the per-batch
// trader key to prevent script reuse and provide plausible deniability
// between account outputs to third parties.
AuctioneerKey *btcec.PublicKey
// InitialBatchKey is the initial batch key that is used to tweak the
// trader key of an account.
InitialBatchKey *btcec.PublicKey
}
// Version represents the version of an account.
type Version uint8
const (
// VersionInitialNoVersion is the initial version any legacy account has
// that technically wasn't versioned at all. The version field isn't
// even serialized for those accounts.
VersionInitialNoVersion Version = 0
// VersionTaprootEnabled is the version that introduced account
// versioning and the upgrade to Taproot (with MuSig2 multi-sig).
VersionTaprootEnabled Version = 1
)
// String returns the string representation of the version.
func (v Version) String() string {
switch v {
case VersionInitialNoVersion:
return "account_p2wsh"
case VersionTaprootEnabled:
return "account_p2tr"
default:
return fmt.Sprintf("unknown <%d>", v)
}
}
// ScriptVersion returns the version of the pool script used by this account
// version.
func (v Version) ScriptVersion() poolscript.Version {
switch v {
case VersionTaprootEnabled:
return poolscript.VersionTaprootMuSig2
default:
return poolscript.VersionWitnessScript
}
}
// ValidateVersion ensures that a given version is a valid and known version.
func ValidateVersion(version Version) error {
switch version {
case VersionInitialNoVersion, VersionTaprootEnabled:
return nil
default:
return fmt.Errorf("unknown version <%d>", version)
}
}
// State describes the different possible states of an account.
type State uint8
// NOTE: We avoid the use of iota as these can be persisted to disk.
const (
// StateInitiated denotes the initial state of an account. When an
// account is in this state, then it should be funded with a
// transaction.
StateInitiated State = 0
// StatePendingOpen denotes that we've broadcast the account's funding
// transaction and are currently waiting for its confirmation.
StatePendingOpen State = 1
// StatePendingUpdate denotes that the account has undergone an update
// on-chain as part of a trader modification and we are currently
// waiting for its confirmation.
StatePendingUpdate State = 2
// StateOpen denotes that the account's funding transaction has been
// included in the chain with sufficient depth.
StateOpen State = 3
// StateExpired denotes that the chain has reached an account's
// expiration height. An account in this state can still be used if
// renewed.
StateExpired State = 4
// StatePendingClosed denotes that an account was fully spent by a
// transaction broadcast by the trader and is pending its confirmation.
StatePendingClosed State = 5
// StateClosed denotes that an account was closed by a transaction
// broadcast by the trader that fully spent the account. An account in
// this state can no longer be used.
StateClosed State = 6
// StateCanceledAfterRecovery denotes that the account was attempted to
// be recovered but failed because the opening transaction wasn't found
// by lnd. This could be because it was never published or it never
// confirmed. Then the funds are SAFU and the account can be considered
// to never have been opened in the first place.
StateCanceledAfterRecovery State = 7
// StatePendingBatch denotes an account that recently participated in a
// batch and is not yet confirmed.
StatePendingBatch State = 8
// StateExpiredPendingUpdate denotes that the chain has reached an
// account's expiration height while the account had a pending update
// that has yet to confirm. This state exists to ensure an account can
// only be renewed once confirmed and expired.
StateExpiredPendingUpdate State = 9
)
// String returns a human-readable description of an account's state.
func (s State) String() string {
switch s {
case StateInitiated:
return "StateInitiated"
case StatePendingOpen:
return "StatePendingOpen"
case StatePendingUpdate:
return "StatePendingUpdate"
case StateOpen:
return "StateOpen"
case StateExpired:
return "StateExpired"
case StatePendingClosed:
return "StatePendingClosed"
case StateClosed:
return "StateClosed"
case StateCanceledAfterRecovery:
return "StateCanceledAfterRecovery"
case StatePendingBatch:
return "StatePendingBatch"
case StateExpiredPendingUpdate:
return "StateExpiredPendingUpdate"
default:
return "unknown"
}
}
// IsActive returns true if the state is considered to be an active account
// state.
func (s State) IsActive() bool {
switch s {
case StateClosed, StateCanceledAfterRecovery:
return false
default:
return true
}
}
// Account encapsulates all of the details of a CLM account on-chain from
// the trader's perspective.
type Account struct {
// Value is the value of the account reflected in on-chain output that
// backs the existence of an account.
Value btcutil.Amount
// Expiry is the expiration block height of an account. After this
// point, the trader is able to withdraw the funds from their account
// without cooperation of the auctioneer.
Expiry uint32
// TraderKey is the base trader's key in the 2-of-2 multi-sig
// construction of a CLM account. This key will never be included in the
// account script, but rather it will be tweaked with the per-batch key
// and the account secret to prevent script reuse and provide plausible
// deniability between account outputs to third parties.
TraderKey *keychain.KeyDescriptor
// AuctioneerKey is the base auctioneer's key in the 2-of-2 multi-sig
// construction of a CLM account. This key will never be included in the
// account script, but rather it will be tweaked with the per-batch
// trader key to prevent script reuse and provide plausible deniability
// between account outputs to third parties.
AuctioneerKey *btcec.PublicKey
// BatchKey is the batch key that is used to tweak the trader key of an
// account with, along with the secret. This will be incremented by the
// curve's base point each time the account is modified or participates
// in a cleared batch to prevent output script reuse for accounts
// on-chain.
BatchKey *btcec.PublicKey
// Secret is a static shared secret between the trader and the
// auctioneer that is used to tweak the trader key of an account with,
// along with the batch key. This ensures that only the trader and
// auctioneer are able to successfully identify every past/future output
// of an account.
Secret [32]byte
// State describes the state of the account.
State State
// HeightHint is the earliest height in the chain at which we can find
// the account output in a block.
HeightHint uint32
// OutPoint is the outpoint of the output used to fund the account. This
// only exists once the account has reached StatePendingOpen.
OutPoint wire.OutPoint
// LatestTx is the latest transaction of an account.
//
// NOTE: This is only nil within the StateInitiated phase. There are no
// guarantees as to whether the transaction has its witness populated.
LatestTx *wire.MsgTx
// Version is the version of the account.
Version Version
}
const (
// DefaultFundingConfTarget is the default value used for the account
// funding/init target number of blocks to confirmation. We choose a
// very high value of one week to arrive at essentially 1 sat/vByte
// which used to be the previous default when creating the transaction.
DefaultFundingConfTarget uint32 = 144 * 7
)
// Output returns the current on-chain output associated with the account.
func (a *Account) Output() (*wire.TxOut, error) {
script, err := poolscript.AccountScript(
a.Version.ScriptVersion(), a.Expiry, a.TraderKey.PubKey,
a.AuctioneerKey, a.BatchKey, a.Secret,
)
if err != nil {
return nil, err
}
return &wire.TxOut{
Value: int64(a.Value),
PkScript: script,
}, nil
}
// NextOutputScript returns the next on-chain output script that is to be
// associated with the account. This is done by using the next batch key, which
// results from incrementing the current one by its curve's base point.
func (a *Account) NextOutputScript() ([]byte, error) {
nextBatchKey := poolscript.IncrementKey(a.BatchKey)
return poolscript.AccountScript(
a.Version.ScriptVersion(), a.Expiry, a.TraderKey.PubKey,
a.AuctioneerKey, nextBatchKey, a.Secret,
)
}
// CopyPubKey creates a copy of a public key.
func CopyPubKey(pub *btcec.PublicKey) *btcec.PublicKey {
newPubKey, _ := btcec.ParsePubKey(pub.SerializeCompressed())
return newPubKey
}
// Copy returns a deep copy of the account with the given modifiers applied.
func (a *Account) Copy(modifiers ...Modifier) *Account {
accountCopy := &Account{
Value: a.Value,
Expiry: a.Expiry,
TraderKey: &keychain.KeyDescriptor{
KeyLocator: a.TraderKey.KeyLocator,
PubKey: CopyPubKey(a.TraderKey.PubKey),
},
AuctioneerKey: CopyPubKey(a.AuctioneerKey),
BatchKey: CopyPubKey(a.BatchKey),
Secret: a.Secret,
State: a.State,
HeightHint: a.HeightHint,
OutPoint: a.OutPoint,
Version: a.Version,
}
if a.State != StateInitiated {
accountCopy.LatestTx = a.LatestTx.Copy()
}
for _, modifier := range modifiers {
modifier(accountCopy)
}
return accountCopy
}
// Modifier abstracts the modification of an account through a function.
type Modifier func(*Account)
// StateModifier is a functional option that modifies the state of an account.
func StateModifier(state State) Modifier {
return func(account *Account) {
account.State = state
}
}
// ValueModifier is a functional option that modifies the value of an account.
func ValueModifier(value btcutil.Amount) Modifier {
return func(account *Account) {
account.Value = value
}
}
// ExpiryModifier is a functional option that modifies the expiry of an account.
func ExpiryModifier(expiry uint32) Modifier {
return func(account *Account) {
account.Expiry = expiry
}
}
// IncrementBatchKey is a functional option that increments the batch key of an
// account by adding the curve's base point.
func IncrementBatchKey() Modifier {
return func(account *Account) {
account.BatchKey = poolscript.IncrementKey(account.BatchKey)
}
}
// OutPointModifier is a functional option that modifies the outpoint of an
// account.
func OutPointModifier(op wire.OutPoint) Modifier {
return func(account *Account) {
account.OutPoint = op
}
}
// HeightHintModifier is a functional option that modifies the height hint of an
// account.
func HeightHintModifier(heightHint uint32) Modifier {
return func(account *Account) {
account.HeightHint = heightHint
}
}
// LatestTxModifier is a functional option that modifies the latest transaction
// of an account.
func LatestTxModifier(tx *wire.MsgTx) Modifier {
return func(account *Account) {
account.LatestTx = tx
}
}
// VersionModifier is a functional option that modifies the version of an
// account.
func VersionModifier(version Version) Modifier {
return func(account *Account) {
account.Version = version
}
}
// Store is responsible for storing and retrieving account information reliably.
type Store interface {
// AddAccount adds a record for the account to the database.
AddAccount(*Account) error
// UpdateAccount updates an account in the database according to the
// given modifiers.
UpdateAccount(*Account, ...Modifier) error
// Account retrieves the account associated with the given trader key
// from the database.
Account(*btcec.PublicKey) (*Account, error)
// Accounts retrieves all existing accounts.
Accounts() ([]*Account, error)
// PendingBatch determines whether we currently have a pending batch.
// If a batch doesn't exist, ErrNoPendingBatch is returned.
PendingBatch() error
// MarkBatchComplete marks the batch with the given ID as complete,
// indicating that the staged account updates can be applied to disk.
MarkBatchComplete() error
// LockID retrieves the global lock ID we'll use to lock any outputs
// when performing coin selection.
LockID() (wtxmgr.LockID, error)
}
// Auctioneer provides us with the different ways we are able to communicate
// with our auctioneer during the process of opening/closing/modifying accounts.
type Auctioneer interface {
// ReserveAccount reserves an account of the specified value with the
// auctioneer. The auctioneer checks the account value against current
// min/max values configured. If the value is valid, it returns the
// public key we should use for them in our 2-of-2 multi-sig
// construction. To address an edge case in the account recovery where
// the trader crashes before confirming the account with the auctioneer,
// we also send the trader key and expiry along with the reservation.
ReserveAccount(context.Context, btcutil.Amount, uint32,
*btcec.PublicKey, Version) (*Reservation, error)
// InitAccount initializes an account with the auctioneer such that it
// can be used once fully confirmed.
InitAccount(context.Context, *Account) error
// ModifyAccount sends an intent to the auctioneer that we'd like to
// modify the account with the associated trader key. The auctioneer's
// signature is returned, allowing us to broadcast a transaction
// spending from the account allowing our modifications to take place.
// If the account spend is a MuSig2 spend, then the trader's nonces must
// be sent and the server's nonces are returned upon success. The inputs
// and outputs provided should exclude the account input being spent and
// the account output potentially being recreated, since the auctioneer
// can construct those themselves. If no modifiers are present, then the
// auctioneer will interpret the request as an account closure. The
// previous outputs must always contain the UTXO information for _every_
// input of the transaction, so inputs+account_input.
ModifyAccount(ctx context.Context, acct *Account, inputs []*wire.TxIn,
outputs []*wire.TxOut, modifiers []Modifier,
traderNonces []byte, previousOutputs []*wire.TxOut) ([]byte,
[]byte, error)
// StartAccountSubscription opens a stream to the server and subscribes
// to all updates that concern the given account, including all orders
// that spend from that account. Only a single stream is ever open to
// the server, so a second call to this method will send a second
// subscription over the same stream, multiplexing all messages into the
// same connection. A stream can be long-lived, so this can be called
// for every account as soon as it's confirmed open. This method will
// return as soon as the authentication was successful. Messages sent
// from the server can then be received on the FromServerChan channel.
StartAccountSubscription(context.Context, *keychain.KeyDescriptor) error
// Terms returns the current dynamic auctioneer terms like max account
// size, max order duration in blocks and the auction fee schedule.
Terms(ctx context.Context) (*terms.AuctioneerTerms, error)
}
// TxSource is a source that provides us with transactions previously broadcast
// by us.
type TxSource interface {
// ListTransactions returns all known transactions of the backing lnd
// node. It takes a start and end block height which can be used to
// limit the block range that we query over. These values can be left
// as zero to include all blocks. To include unconfirmed transactions
// in the query, endHeight must be set to -1.
ListTransactions(ctx context.Context, startHeight, endHeight int32,
opts ...lndclient.ListTransactionsOption) (
[]lndclient.Transaction, error)
}
// TxFeeEstimator is a type that provides us with a realistic fee estimation to
// send coins in a transaction.
type TxFeeEstimator interface {
// EstimateFeeToP2WSH estimates the total chain fees in satoshis to send
// the given amount to a single P2WSH output with the given target
// confirmation.
EstimateFeeToP2WSH(ctx context.Context, amt btcutil.Amount,
confTarget int32) (btcutil.Amount, error)
}
// FeeExpr represents the different ways a transaction fee can be expressed in
// terms of a transaction's resulting outputs.
type FeeExpr interface {
// CloseOutputs is the list of outputs that should be used for the
// closing transaction of an account based on the concrete fee
// expression implementation.
CloseOutputs(btcutil.Amount, witnessType) ([]*wire.TxOut, error)
}
// OutputWithFee signals that a single transaction output along with a fee rate
// is used to determine the transaction fee.
type OutputWithFee struct {
// PkScript is the destination output script. Note that this may be nil,
// in which case a wallet-derived P2WKH script should be used.
PkScript []byte
// FeeRate is the accompanying fee rate to use to determine the
// transaction fee.
FeeRate chainfee.SatPerKWeight
}
func (o *OutputWithFee) CloseOutputs(accountValue btcutil.Amount,
witnessType witnessType) ([]*wire.TxOut, error) {
// Calculate the transaction's weight to determine its fee according to
// the provided fee rate. The transaction will contain one P2WSH input
// (the account input) and one output.
var weightEstimator input.TxWeightEstimator
// Determine the appropriate witness size based on the input and output
// type.
witnessSize, err := witnessType.witnessSize()
if err != nil {
return nil, err
}
weightEstimator.AddWitnessInput(witnessSize)
pkScript, err := txscript.ParsePkScript(o.PkScript)
if err != nil {
return nil, err
}
// We'll also note the dust limit for each output script type along the
// way to determine if the output can even be created.
var dustLimit btcutil.Amount
switch pkScript.Class() {
case txscript.WitnessV0PubKeyHashTy:
weightEstimator.AddP2WKHOutput()
dustLimit = lnwallet.DustLimitForSize(
input.P2WPKHSize,
)
case txscript.ScriptHashTy:
weightEstimator.AddP2SHOutput()
dustLimit = lnwallet.DustLimitForSize(
input.P2SHSize,
)
case txscript.WitnessV0ScriptHashTy:
weightEstimator.AddP2WSHOutput()
dustLimit = lnwallet.DustLimitForSize(
input.P2WSHSize,
)
case txscript.WitnessV1TaprootTy:
weightEstimator.AddP2TROutput()
dustLimit = lnwallet.DustLimitForSize(
input.P2TRSize,
)
}
fee := o.FeeRate.FeeForWeight(int64(weightEstimator.Weight()))
outputValue := accountValue - fee
if outputValue < dustLimit {
return nil, fmt.Errorf("closing to output %x with %v results "+
"in dust", pkScript, o.FeeRate)
}
return []*wire.TxOut{{
Value: int64(outputValue),
PkScript: pkScript.Script(),
}}, nil
}
// OutputsWithImplicitFee signals that the transaction fee is implicitly defined
// by the output values provided, i.e., the fee is determined by subtracting
// the total output value from the total input value.
type OutputsWithImplicitFee []*wire.TxOut
// Outputs returns the set of outputs.
func (o OutputsWithImplicitFee) Outputs() []*wire.TxOut {
return o
}
// Outputs is the list of outputs that should be used for the closing
// transaction of an account using an implicit fee expression.
func (o OutputsWithImplicitFee) CloseOutputs(accountValue btcutil.Amount,
witnessType witnessType) ([]*wire.TxOut, error) {
return o, nil
}
// Manager is the interface a manager implements to deal with the accounts.
type Manager interface {
// Start resumes all account on-chain operation after a restart.
Start() error
// Stop safely stops any ongoing operations within the Manager.
Stop()
// QuoteAccount returns the expected fee rate and total miner fee to send to an
// account funding output with the given confTarget.
QuoteAccount(ctx context.Context, value btcutil.Amount,
confTarget uint32) (chainfee.SatPerKWeight, btcutil.Amount, error)
// InitAccount handles a request to create a new account with the provided
// parameters.
InitAccount(ctx context.Context, value btcutil.Amount, version Version,
feeRate chainfee.SatPerKWeight, expiry,
bestHeight uint32) (*Account, error)
// WatchMatchedAccounts resumes accounts that were just matched in a batch and
// are expecting the batch transaction to confirm as their next account output.
// This will cancel all previous spend and conf watchers of all accounts
// involved in the batch.
WatchMatchedAccounts(ctx context.Context,
matchedAccounts []*btcec.PublicKey) error
// HandleAccountConf takes the necessary steps after detecting the confirmation
// of an account on-chain.
HandleAccountConf(traderKey *btcec.PublicKey,
confDetails *chainntnfs.TxConfirmation) error
// HandleAccountSpend handles the different spend paths of an account. If an
// account is spent by the expiration path, it'll always be marked as closed
// thereafter. If it is spent by the cooperative path with the auctioneer, then
// the account will only remain open if the spending transaction recreates the
// account with the expected next account script. Otherwise, it is also marked
// as closed. In case of multiple consecutive batches with the same account, we
// only track the spend of the latest batch, after it confirmed. So the account
// output in the spend transaction should always match our database state if
// it was a cooperative spend.
HandleAccountSpend(traderKey *btcec.PublicKey,
spendDetails *chainntnfs.SpendDetail) error
// HandleAccountExpiry marks an account as expired within the database.
HandleAccountExpiry(traderKey *btcec.PublicKey,
height uint32) error
// DepositAccount attempts to deposit funds into the account associated with the
// given trader key such that the new account value is met using inputs sourced
// from the backing lnd node's wallet. If needed, a change output that does back
// to lnd may be added to the deposit transaction.
DepositAccount(ctx context.Context, traderKey *btcec.PublicKey,
depositAmount btcutil.Amount, feeRate chainfee.SatPerKWeight,
bestHeight, expiryHeight uint32, newVersion Version) (*Account,
*wire.MsgTx, error)
// WithdrawAccount attempts to withdraw funds from the account associated with
// the given trader key into the provided outputs.
WithdrawAccount(ctx context.Context, traderKey *btcec.PublicKey,
outputs []*wire.TxOut, feeRate chainfee.SatPerKWeight,
bestHeight, expiryHeight uint32, newVersion Version) (*Account,
*wire.MsgTx, error)
// RenewAccount updates the expiration of an open/expired account. This will
// always require a signature from the auctioneer, even after the account has
// expired, to ensure the auctioneer is aware the account is being renewed.
RenewAccount(ctx context.Context, traderKey *btcec.PublicKey,
newExpiry uint32, feeRate chainfee.SatPerKWeight,
bestHeight uint32, newVersion Version) (*Account, *wire.MsgTx,
error)
// BumpAccountFee attempts to bump the fee of an account's most recent
// transaction. This is done by locating an eligible output for lnd to CPFP,
// otherwise the fee bump will not succeed. Further invocations of this call for
// the same account will result in the child being replaced by the higher fee
// transaction (RBF).
BumpAccountFee(ctx context.Context, traderKey *btcec.PublicKey,
newFeeRate chainfee.SatPerKWeight) error
// CloseAccount attempts to close the account associated with the given trader
// key. Closing the account requires a signature of the auctioneer if the
// account has not yet expired. The account funds are swept according to the
// provided fee expression.
CloseAccount(ctx context.Context, traderKey *btcec.PublicKey,
feeExpr FeeExpr, bestHeight uint32) (*wire.MsgTx, error)
// RecoverAccount re-introduces a recovered account into the database and starts
// all watchers necessary depending on the account's state.
RecoverAccount(ctx context.Context, account *Account) error
}