mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
Include https://github.com/lightninglabs/lndclient/pull/280 multi: migrate to btcd v2 modules + add WalletKit.SubmitPackage Migrate Loop imports and update Aperture and Taproot Assets to compatible revisions so this commit remains green on its own.
387 lines
9.7 KiB
Go
387 lines
9.7 KiB
Go
package address
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
btcaddr "github.com/btcsuite/btcd/address/v2"
|
|
"github.com/btcsuite/btcd/btcec/v2"
|
|
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
|
"github.com/btcsuite/btcd/chaincfg/v2"
|
|
"github.com/btcsuite/btcd/wire/v2"
|
|
"github.com/lightninglabs/lndclient"
|
|
"github.com/lightninglabs/loop/staticaddr/script"
|
|
"github.com/lightninglabs/loop/staticaddr/version"
|
|
"github.com/lightninglabs/loop/swap"
|
|
staticaddressrpc "github.com/lightninglabs/loop/swapserverrpc"
|
|
"github.com/lightningnetwork/lnd/input"
|
|
"github.com/lightningnetwork/lnd/keychain"
|
|
"github.com/lightningnetwork/lnd/lnwallet"
|
|
)
|
|
|
|
const (
|
|
// maxStaticAddressCSVExpiry is the maximum CSV delay that we accept
|
|
// from the server for a static address timeout path: 200 days at 144
|
|
// blocks per day.
|
|
maxStaticAddressCSVExpiry = uint32(200 * 144)
|
|
)
|
|
|
|
// ManagerConfig holds the configuration for the address manager.
|
|
type ManagerConfig struct {
|
|
// AddressClient is the client that communicates with the loop server
|
|
// to manage static addresses.
|
|
AddressClient staticaddressrpc.StaticAddressServerClient
|
|
|
|
// FetchL402 is the function used to fetch the l402 token.
|
|
FetchL402 func(context.Context) error
|
|
|
|
// Store is the database store that is used to store static address
|
|
// related records.
|
|
Store Store
|
|
|
|
// WalletKit is the wallet client that is used to derive new keys from
|
|
// lnd's wallet.
|
|
WalletKit lndclient.WalletKitClient
|
|
|
|
// ChainParams is the chain configuration(mainnet, testnet...) this
|
|
// manager uses.
|
|
ChainParams *chaincfg.Params
|
|
|
|
// ChainNotifier is the chain notifier that is used to listen for new
|
|
// blocks.
|
|
ChainNotifier lndclient.ChainNotifierClient
|
|
}
|
|
|
|
// Manager manages the address state machines.
|
|
type Manager struct {
|
|
sync.Mutex
|
|
|
|
cfg *ManagerConfig
|
|
|
|
currentHeight atomic.Int32
|
|
}
|
|
|
|
// NewManager creates a new address manager.
|
|
func NewManager(cfg *ManagerConfig, currentHeight int32) (*Manager, error) {
|
|
if currentHeight <= 0 {
|
|
return nil, fmt.Errorf("invalid current height %d",
|
|
currentHeight)
|
|
}
|
|
|
|
m := &Manager{
|
|
cfg: cfg,
|
|
}
|
|
m.currentHeight.Store(currentHeight)
|
|
|
|
return m, nil
|
|
}
|
|
|
|
// Run runs the address manager.
|
|
func (m *Manager) Run(ctx context.Context, initChan chan struct{}) error {
|
|
newBlockChan, newBlockErrChan, err :=
|
|
m.cfg.ChainNotifier.RegisterBlockEpochNtfn(ctx)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Communicate to the caller that the address manager has completed its
|
|
// initialization.
|
|
close(initChan)
|
|
|
|
for {
|
|
select {
|
|
case currentHeight := <-newBlockChan:
|
|
m.currentHeight.Store(currentHeight)
|
|
|
|
case err = <-newBlockErrChan:
|
|
return err
|
|
|
|
case <-ctx.Done():
|
|
// Signal subroutines that the manager is exiting.
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
}
|
|
|
|
// NewAddress creates a new static address with the server or returns an
|
|
// existing one.
|
|
func (m *Manager) NewAddress(ctx context.Context) (*btcaddr.AddressTaproot,
|
|
int64, error) {
|
|
|
|
// If there's already a static address in the database, we can return
|
|
// it.
|
|
m.Lock()
|
|
addresses, err := m.cfg.Store.GetAllStaticAddresses(ctx)
|
|
if err != nil {
|
|
m.Unlock()
|
|
|
|
return nil, 0, err
|
|
}
|
|
if len(addresses) > 0 {
|
|
clientPubKey := addresses[0].ClientPubkey
|
|
serverPubKey := addresses[0].ServerPubkey
|
|
expiry := int64(addresses[0].Expiry)
|
|
|
|
defer m.Unlock()
|
|
|
|
address, err := m.GetTaprootAddress(
|
|
clientPubKey, serverPubKey, expiry,
|
|
)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
return address, expiry, nil
|
|
}
|
|
m.Unlock()
|
|
|
|
// We are fetching a new L402 token from the server. There is one static
|
|
// address per L402 token allowed.
|
|
err = m.cfg.FetchL402(ctx)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
clientPubKey, err := m.cfg.WalletKit.DeriveNextKey(
|
|
ctx, swap.StaticAddressKeyFamily,
|
|
)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
// Send our clientPubKey to the server and wait for the server to
|
|
// respond with he serverPubKey and the static address CSV expiry.
|
|
protocolVersion := version.CurrentRPCProtocolVersion()
|
|
resp, err := m.cfg.AddressClient.ServerNewAddress(
|
|
ctx, &staticaddressrpc.ServerNewAddressRequest{
|
|
ProtocolVersion: protocolVersion,
|
|
ClientKey: clientPubKey.PubKey.SerializeCompressed(), //nolint:lll
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
if resp == nil {
|
|
return nil, 0, fmt.Errorf("missing server new address response")
|
|
}
|
|
|
|
serverParams := resp.GetParams()
|
|
if err := validateServerAddressParams(serverParams); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
serverPubKey, err := btcec.ParsePubKey(serverParams.GetServerKey())
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
staticAddress, err := script.NewStaticAddress(
|
|
input.MuSig2Version100RC2, int64(serverParams.Expiry),
|
|
clientPubKey.PubKey, serverPubKey,
|
|
)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
pkScript, err := staticAddress.StaticAddressScript()
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
// Create the static address from the parameters the server provided and
|
|
// store all parameters in the database.
|
|
addrParams := &script.Parameters{
|
|
ClientPubkey: clientPubKey.PubKey,
|
|
ServerPubkey: serverPubKey,
|
|
PkScript: pkScript,
|
|
Expiry: serverParams.Expiry,
|
|
KeyLocator: keychain.KeyLocator{
|
|
Family: clientPubKey.Family,
|
|
Index: clientPubKey.Index,
|
|
},
|
|
ProtocolVersion: version.AddressProtocolVersion(
|
|
protocolVersion,
|
|
),
|
|
InitiationHeight: m.currentHeight.Load(),
|
|
}
|
|
err = m.cfg.Store.CreateStaticAddress(ctx, addrParams)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
// Import the static address tapscript into our lnd wallet, so we can
|
|
// track unspent outputs of it.
|
|
tapScript := input.TapscriptFullTree(
|
|
staticAddress.InternalPubKey, *staticAddress.TimeoutLeaf,
|
|
)
|
|
addr, err := m.cfg.WalletKit.ImportTaprootScript(ctx, tapScript)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
log.Infof("Imported static address taproot script to lnd wallet: %v",
|
|
addr)
|
|
|
|
address, err := m.GetTaprootAddress(
|
|
clientPubKey.PubKey, serverPubKey, int64(serverParams.Expiry),
|
|
)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
return address, int64(serverParams.Expiry), nil
|
|
}
|
|
|
|
// validateServerAddressParams validates the server-controlled static address
|
|
// parameters before they are committed into the address script or database.
|
|
func validateServerAddressParams(
|
|
params *staticaddressrpc.ServerAddressParameters) error {
|
|
|
|
if params == nil {
|
|
return fmt.Errorf("missing server address parameters")
|
|
}
|
|
|
|
serverKey := params.GetServerKey()
|
|
if len(serverKey) == 0 {
|
|
return fmt.Errorf("missing server public key")
|
|
}
|
|
if !btcec.IsCompressedPubKey(serverKey) {
|
|
return fmt.Errorf("server public key is not a compressed " +
|
|
"secp256k1 public key")
|
|
}
|
|
|
|
expiry := params.GetExpiry()
|
|
switch {
|
|
case expiry == 0:
|
|
return fmt.Errorf("static address CSV expiry must be non-zero")
|
|
|
|
case expiry&^wire.SequenceLockTimeMask != 0:
|
|
return fmt.Errorf("static address expiry does not fit into "+
|
|
"CSV: %x", expiry)
|
|
|
|
case expiry > maxStaticAddressCSVExpiry:
|
|
return fmt.Errorf("static address CSV expiry %v exceeds "+
|
|
"maximum %v", expiry, maxStaticAddressCSVExpiry)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetTaprootAddress returns a taproot address for the given client and server
|
|
// public keys and expiry.
|
|
func (m *Manager) GetTaprootAddress(clientPubkey, serverPubkey *btcec.PublicKey,
|
|
expiry int64) (*btcaddr.AddressTaproot, error) {
|
|
|
|
staticAddress, err := script.NewStaticAddress(
|
|
input.MuSig2Version100RC2, expiry, clientPubkey, serverPubkey,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return btcaddr.NewAddressTaproot(
|
|
schnorr.SerializePubKey(staticAddress.TaprootKey),
|
|
m.cfg.ChainParams,
|
|
)
|
|
}
|
|
|
|
// ListUnspentRaw returns a list of utxos at the static address.
|
|
func (m *Manager) ListUnspentRaw(ctx context.Context, minConfs,
|
|
maxConfs int32) (*btcaddr.AddressTaproot, []*lnwallet.Utxo, error) {
|
|
|
|
addresses, err := m.cfg.Store.GetAllStaticAddresses(ctx)
|
|
switch {
|
|
case err != nil:
|
|
return nil, nil, err
|
|
|
|
case len(addresses) == 0:
|
|
return nil, nil, nil
|
|
|
|
case len(addresses) > 1:
|
|
return nil, nil, fmt.Errorf("more than one address found")
|
|
}
|
|
|
|
staticAddress := addresses[0]
|
|
|
|
// List all unspent utxos the wallet sees, regardless of the number of
|
|
// confirmations.
|
|
utxos, err := m.cfg.WalletKit.ListUnspent(
|
|
ctx, minConfs, maxConfs,
|
|
)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Filter the list of lnd's unspent utxos for the pkScript of our static
|
|
// address.
|
|
var filteredUtxos []*lnwallet.Utxo
|
|
for _, utxo := range utxos {
|
|
if bytes.Equal(utxo.PkScript, staticAddress.PkScript) {
|
|
filteredUtxos = append(filteredUtxos, utxo)
|
|
}
|
|
}
|
|
|
|
taprootAddress, err := m.GetTaprootAddress(
|
|
staticAddress.ClientPubkey, staticAddress.ServerPubkey,
|
|
int64(staticAddress.Expiry),
|
|
)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return taprootAddress, filteredUtxos, nil
|
|
}
|
|
|
|
// GetStaticAddressParameters returns the parameters of the static address.
|
|
func (m *Manager) GetStaticAddressParameters(ctx context.Context) (
|
|
*script.Parameters, error) {
|
|
|
|
params, err := m.cfg.Store.GetAllStaticAddresses(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(params) == 0 {
|
|
return nil, fmt.Errorf("no static address parameters found")
|
|
}
|
|
|
|
return params[0], nil
|
|
}
|
|
|
|
// GetStaticAddress returns a taproot address for the given client and server
|
|
// public keys and expiry.
|
|
func (m *Manager) GetStaticAddress(ctx context.Context) (*script.StaticAddress,
|
|
error) {
|
|
|
|
params, err := m.GetStaticAddressParameters(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
address, err := script.NewStaticAddress(
|
|
input.MuSig2Version100RC2, int64(params.Expiry),
|
|
params.ClientPubkey, params.ServerPubkey,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return address, nil
|
|
}
|
|
|
|
// ListUnspent returns a list of utxos at the static address.
|
|
func (m *Manager) ListUnspent(ctx context.Context, minConfs,
|
|
maxConfs int32) ([]*lnwallet.Utxo, error) {
|
|
|
|
_, utxos, err := m.ListUnspentRaw(ctx, minConfs, maxConfs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return utxos, nil
|
|
}
|