mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
loopd: map static address loop-ins to swap status
This commit is contained in:
parent
df7dfba12a
commit
34cf98e8e8
4 changed files with 409 additions and 6 deletions
|
|
@ -411,6 +411,8 @@ func (s *swapClientServer) marshallSwap(ctx context.Context,
|
|||
}
|
||||
|
||||
var swapType looprpc.SwapType
|
||||
staticLoopInState := looprpc.
|
||||
StaticAddressLoopInSwapState_UNKNOWN_STATIC_ADDRESS_SWAP_STATE
|
||||
var (
|
||||
htlcAddress string
|
||||
htlcAddressP2TR string
|
||||
|
|
@ -437,6 +439,27 @@ func (s *swapClientServer) marshallSwap(ctx context.Context,
|
|||
lastHop = loopSwap.LastHop[:]
|
||||
}
|
||||
|
||||
case swap.TypeStaticAddressLoopIn:
|
||||
// Static loop-ins surface their precise FSM state through the
|
||||
// optional oneof and keep the reconstructed HTLC P2WSH address,
|
||||
// not the reusable static address.
|
||||
swapType = looprpc.SwapType_STATIC_LOOP_IN
|
||||
staticLoopInState = toClientStaticAddressLoopInState(
|
||||
loopSwap.StaticAddressLoopInState,
|
||||
)
|
||||
|
||||
if loopSwap.HtlcAddressP2WSH == nil {
|
||||
return nil, errors.New(
|
||||
"missing static address loop-in P2WSH HTLC address",
|
||||
)
|
||||
}
|
||||
htlcAddressP2WSH = loopSwap.HtlcAddressP2WSH.EncodeAddress()
|
||||
htlcAddress = htlcAddressP2WSH
|
||||
|
||||
if loopSwap.LastHop != nil {
|
||||
lastHop = loopSwap.LastHop[:]
|
||||
}
|
||||
|
||||
case swap.TypeOut:
|
||||
swapType = looprpc.SwapType_LOOP_OUT
|
||||
if loopSwap.HtlcAddressP2WSH != nil {
|
||||
|
|
@ -478,7 +501,7 @@ func (s *swapClientServer) marshallSwap(ctx context.Context,
|
|||
return nil, errors.New("unknown swap type")
|
||||
}
|
||||
|
||||
return &looprpc.SwapStatus{
|
||||
rpcSwap := &looprpc.SwapStatus{
|
||||
Amt: int64(loopSwap.AmountRequested),
|
||||
Id: loopSwap.SwapHash.String(),
|
||||
IdBytes: loopSwap.SwapHash[:],
|
||||
|
|
@ -497,7 +520,15 @@ func (s *swapClientServer) marshallSwap(ctx context.Context,
|
|||
LastHop: lastHop,
|
||||
OutgoingChanSet: outGoingChanSet,
|
||||
AssetInfo: assetInfo,
|
||||
}, nil
|
||||
}
|
||||
if swapType == looprpc.SwapType_STATIC_LOOP_IN {
|
||||
rpcSwap.StaticLoopInStateOptional =
|
||||
&looprpc.SwapStatus_StaticLoopInState{
|
||||
StaticLoopInState: staticLoopInState,
|
||||
}
|
||||
}
|
||||
|
||||
return rpcSwap, nil
|
||||
}
|
||||
|
||||
// Monitor will return a stream of swap updates for currently active swaps.
|
||||
|
|
@ -2089,6 +2120,8 @@ func (s *swapClientServer) ListStaticAddressSwaps(ctx context.Context,
|
|||
}, nil
|
||||
}
|
||||
|
||||
// staticAddressLoopInTimestamp converts a non-zero timestamp to Unix nano
|
||||
// form and preserves zero timestamps as zero.
|
||||
func staticAddressLoopInTimestamp(t time.Time) int64 {
|
||||
if t.IsZero() {
|
||||
return 0
|
||||
|
|
@ -2114,6 +2147,141 @@ func staticAddressLoopInSwapServerCost(swp *loopin.StaticAddressLoopIn) int64 {
|
|||
}
|
||||
}
|
||||
|
||||
// staticAddressLoopInSwapInfos loads the static-address loop-in manager swaps
|
||||
// and converts them to client-facing swap info records.
|
||||
func (s *swapClientServer) staticAddressLoopInSwapInfos(
|
||||
ctx context.Context) ([]*loop.SwapInfo, error) {
|
||||
|
||||
if s.staticLoopInManager == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
staticSwaps, err := s.staticLoopInManager.GetAllSwaps(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
swapInfos := make([]*loop.SwapInfo, 0, len(staticSwaps))
|
||||
for _, swp := range staticSwaps {
|
||||
if swp == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
swapInfo, err := s.staticAddressLoopInSwapInfo(ctx, swp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
swapInfos = append(swapInfos, swapInfo)
|
||||
}
|
||||
|
||||
return swapInfos, nil
|
||||
}
|
||||
|
||||
// staticAddressLoopInSwapInfo converts one static-address loop-in into swap
|
||||
// info using the daemon's current chain parameters.
|
||||
func (s *swapClientServer) staticAddressLoopInSwapInfo(_ context.Context,
|
||||
swp *loopin.StaticAddressLoopIn) (*loop.SwapInfo, error) {
|
||||
|
||||
chainParams, err := s.network.ChainParams()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting chain params")
|
||||
}
|
||||
|
||||
return staticAddressLoopInSwapInfoWithChainParams(swp, chainParams)
|
||||
}
|
||||
|
||||
// staticAddressLoopInSwapInfoWithChainParams converts one static-address
|
||||
// loop-in into swap info, including its reconstructed V2 P2WSH HTLC address.
|
||||
func staticAddressLoopInSwapInfoWithChainParams(
|
||||
swp *loopin.StaticAddressLoopIn,
|
||||
chainParams *chaincfg.Params) (*loop.SwapInfo, error) {
|
||||
|
||||
htlcAddress, err := staticAddressLoopInHtlcAddress(swp, chainParams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var lastHop *route.Vertex
|
||||
if len(swp.LastHop) > 0 {
|
||||
vertex, err := route.NewVertexFromBytes(swp.LastHop)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lastHop = &vertex
|
||||
}
|
||||
|
||||
amount := swp.TotalDepositAmount()
|
||||
if swp.SelectedAmount > 0 {
|
||||
amount = swp.SelectedAmount
|
||||
}
|
||||
|
||||
lastUpdate := swp.LastUpdateTime
|
||||
if lastUpdate.IsZero() {
|
||||
lastUpdate = swp.InitiationTime
|
||||
}
|
||||
|
||||
return &loop.SwapInfo{
|
||||
SwapStateData: loopdb.SwapStateData{
|
||||
// Mirror ListStaticAddressSwaps by reporting only the persisted
|
||||
// client-visible server cost. On-chain and off-chain costs stay
|
||||
// zero until static loop-ins persist real fee data.
|
||||
Cost: loopdb.SwapCost{
|
||||
Server: btcutil.Amount(
|
||||
staticAddressLoopInSwapServerCost(swp),
|
||||
),
|
||||
},
|
||||
},
|
||||
SwapContract: loopdb.SwapContract{
|
||||
AmountRequested: amount,
|
||||
CltvExpiry: swp.HtlcCltvExpiry,
|
||||
MaxSwapFee: swp.MaxSwapFee,
|
||||
InitiationTime: swp.InitiationTime,
|
||||
Label: swp.Label,
|
||||
ProtocolVersion: loopdb.ProtocolVersion(
|
||||
swp.ProtocolVersion,
|
||||
),
|
||||
},
|
||||
LastUpdate: lastUpdate,
|
||||
SwapHash: swp.SwapHash,
|
||||
SwapType: swap.TypeStaticAddressLoopIn,
|
||||
StaticAddressLoopInState: swp.GetState(),
|
||||
HtlcAddressP2WSH: htlcAddress,
|
||||
LastHop: lastHop,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// staticAddressLoopInHtlcAddress reconstructs the V2 P2WSH HTLC address from
|
||||
// the static-address loop-in's client and server keys.
|
||||
func staticAddressLoopInHtlcAddress(swp *loopin.StaticAddressLoopIn,
|
||||
chainParams *chaincfg.Params) (btcutil.Address, error) {
|
||||
|
||||
if swp.ClientPubkey == nil {
|
||||
return nil, errors.New("missing static address loop-in client HTLC key")
|
||||
}
|
||||
if swp.ServerPubkey == nil {
|
||||
return nil, errors.New("missing static address loop-in server HTLC key")
|
||||
}
|
||||
|
||||
htlc, err := swap.NewHtlcV2(
|
||||
swp.HtlcCltvExpiry, pubkeyTo33ByteSlice(swp.ClientPubkey),
|
||||
pubkeyTo33ByteSlice(swp.ServerPubkey), swp.SwapHash, chainParams,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("construct static address loop-in HTLC: %w", err)
|
||||
}
|
||||
|
||||
return htlc.Address, nil
|
||||
}
|
||||
|
||||
// pubkeyTo33ByteSlice converts a compressed public key to a fixed 33-byte
|
||||
// array.
|
||||
func pubkeyTo33ByteSlice(pubkey *btcec.PublicKey) [33]byte {
|
||||
var pubkeyBytes [33]byte
|
||||
copy(pubkeyBytes[:], pubkey.SerializeCompressed())
|
||||
|
||||
return pubkeyBytes
|
||||
}
|
||||
|
||||
// GetStaticAddressSummary returns a summary of static address-related
|
||||
// information. Amongst deposits and withdrawals and their total values, it also
|
||||
// includes a list of detailed deposit information filtered by their state.
|
||||
|
|
@ -2420,6 +2588,8 @@ func toClientDepositState(state fsm.StateType) looprpc.DepositState {
|
|||
}
|
||||
}
|
||||
|
||||
// toClientStaticAddressLoopInState maps the static-address loop-in FSM state
|
||||
// to the RPC enum exposed to clients.
|
||||
func toClientStaticAddressLoopInState(
|
||||
state fsm.StateType) looprpc.StaticAddressLoopInSwapState {
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
|
|
@ -25,6 +26,7 @@ import (
|
|||
"github.com/lightninglabs/loop/staticaddr/script"
|
||||
"github.com/lightninglabs/loop/swap"
|
||||
mock_lnd "github.com/lightninglabs/loop/test"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
|
|
@ -501,6 +503,222 @@ func TestListStaticAddressSwapsPopulatesTimingAndCosts(t *testing.T) {
|
|||
)
|
||||
}
|
||||
|
||||
// TestStaticAddressLoopInMarshallUsesStaticTypeAndP2WSH protects the RPC
|
||||
// mapping invariant that static loop-ins expose their static type, static
|
||||
// state, and P2WSH HTLC address without leaking a taproot HTLC address.
|
||||
func TestStaticAddressLoopInMarshallUsesStaticTypeAndP2WSH(t *testing.T) {
|
||||
server := &swapClientServer{}
|
||||
loopSwap := &loop.SwapInfo{
|
||||
SwapStateData: loopdb.SwapStateData{
|
||||
State: loopdb.StateInitiated,
|
||||
},
|
||||
SwapContract: loopdb.SwapContract{
|
||||
InitiationTime: time.Now(),
|
||||
},
|
||||
LastUpdate: time.Now(),
|
||||
SwapHash: lntypes.Hash{1},
|
||||
SwapType: swap.TypeStaticAddressLoopIn,
|
||||
StaticAddressLoopInState: loopin.SignHtlcTx,
|
||||
HtlcAddressP2WSH: testnetAddr,
|
||||
}
|
||||
|
||||
rpcSwap, err := server.marshallSwap(t.Context(), loopSwap)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, looprpc.SwapType_STATIC_LOOP_IN, rpcSwap.Type)
|
||||
require.Equal(
|
||||
t, looprpc.StaticAddressLoopInSwapState_SIGN_HTLC_TX,
|
||||
rpcSwap.GetStaticLoopInState(),
|
||||
)
|
||||
require.Equal(t, looprpc.SwapState_INITIATED, rpcSwap.State)
|
||||
require.Equal(t, testnetAddr.EncodeAddress(), rpcSwap.HtlcAddressP2Wsh)
|
||||
require.Empty(t, rpcSwap.HtlcAddressP2Tr)
|
||||
}
|
||||
|
||||
// TestStaticAddressLoopInMarshallFailuresLeaveLegacyFieldsDefault asserts that
|
||||
// static loop-in failures keep default legacy fields while preserving the
|
||||
// precise static state.
|
||||
func TestStaticAddressLoopInMarshallFailuresLeaveLegacyFieldsDefault(
|
||||
t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
state fsm.StateType
|
||||
wantStaticState looprpc.StaticAddressLoopInSwapState
|
||||
}{
|
||||
{
|
||||
name: "failed",
|
||||
state: loopin.Failed,
|
||||
wantStaticState: looprpc.
|
||||
StaticAddressLoopInSwapState_FAILED_STATIC_ADDRESS_SWAP,
|
||||
},
|
||||
{
|
||||
name: "succeeded transitioning failed",
|
||||
state: loopin.SucceededTransitioningFailed,
|
||||
wantStaticState: looprpc.
|
||||
StaticAddressLoopInSwapState_SUCCEEDED_TRANSITIONING_FAILED,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
server, staticLoopIn := newGenericStaticLoopInServer(t)
|
||||
staticLoopIn.SetState(test.state)
|
||||
loopSwap, err := server.staticAddressLoopInSwapInfo(
|
||||
t.Context(), staticLoopIn,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
rpcSwap, err := server.marshallSwap(t.Context(), loopSwap)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, looprpc.SwapState_INITIATED, rpcSwap.State)
|
||||
require.Equal(
|
||||
t, looprpc.FailureReason_FAILURE_REASON_NONE,
|
||||
rpcSwap.FailureReason,
|
||||
)
|
||||
require.Equal(
|
||||
t, test.wantStaticState,
|
||||
rpcSwap.GetStaticLoopInState(),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestStaticAddressLoopInMarshallRejectsMissingHtlcAddress protects the
|
||||
// fail-closed HTLC-address invariant for static loop-ins missing the P2WSH
|
||||
// address required by the client-facing RPC representation.
|
||||
func TestStaticAddressLoopInMarshallRejectsMissingHtlcAddress(t *testing.T) {
|
||||
_, taprootAddress := newTestStaticAddressParams(t)
|
||||
server := &swapClientServer{}
|
||||
loopSwap := &loop.SwapInfo{
|
||||
SwapStateData: loopdb.SwapStateData{
|
||||
State: loopdb.StateInitiated,
|
||||
},
|
||||
SwapContract: loopdb.SwapContract{
|
||||
InitiationTime: time.Now(),
|
||||
},
|
||||
LastUpdate: time.Now(),
|
||||
SwapHash: lntypes.Hash{1},
|
||||
SwapType: swap.TypeStaticAddressLoopIn,
|
||||
StaticAddressLoopInState: loopin.SignHtlcTx,
|
||||
HtlcAddressP2TR: taprootAddress,
|
||||
}
|
||||
|
||||
_, err := server.marshallSwap(t.Context(), loopSwap)
|
||||
require.ErrorContains(t, err, "missing static address loop-in P2WSH HTLC address")
|
||||
}
|
||||
|
||||
// TestStaticAddressLoopInSwapInfoFailsClosedWhenHtlcKeysMissing protects the
|
||||
// HTLC-address construction invariant that missing cooperative keys must not
|
||||
// produce monitorable swap info.
|
||||
func TestStaticAddressLoopInSwapInfoFailsClosedWhenHtlcKeysMissing(t *testing.T) {
|
||||
server, staticLoopIn := newGenericStaticLoopInServer(t)
|
||||
staticLoopIn.ClientPubkey = nil
|
||||
|
||||
_, err := server.staticAddressLoopInSwapInfo(t.Context(), staticLoopIn)
|
||||
require.ErrorContains(
|
||||
t, err, "missing static address loop-in client HTLC key",
|
||||
)
|
||||
}
|
||||
|
||||
func newGenericStaticLoopInServer(t *testing.T) (*swapClientServer,
|
||||
*loopin.StaticAddressLoopIn) {
|
||||
|
||||
server, staticLoopIn, _ := newGenericStaticLoopInServerWithStore(t)
|
||||
|
||||
return server, staticLoopIn
|
||||
}
|
||||
|
||||
func newTestStaticAddressParams(t *testing.T) (*script.Parameters,
|
||||
*btcutil.AddressTaproot) {
|
||||
|
||||
t.Helper()
|
||||
|
||||
const staticAddressExpiry = uint32(25)
|
||||
|
||||
_, staticClientPubkey := mock_lnd.CreateKey(12)
|
||||
_, staticServerPubkey := mock_lnd.CreateKey(13)
|
||||
staticAddress, err := script.NewStaticAddress(
|
||||
input.MuSig2Version100RC2, int64(staticAddressExpiry),
|
||||
staticClientPubkey, staticServerPubkey,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
staticPkScript, err := staticAddress.StaticAddressScript()
|
||||
require.NoError(t, err)
|
||||
|
||||
taprootAddress, err := btcutil.NewAddressTaproot(
|
||||
schnorr.SerializePubKey(staticAddress.TaprootKey),
|
||||
&chaincfg.TestNet3Params,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
return &script.Parameters{
|
||||
ClientPubkey: staticClientPubkey,
|
||||
ServerPubkey: staticServerPubkey,
|
||||
Expiry: staticAddressExpiry,
|
||||
PkScript: staticPkScript,
|
||||
}, taprootAddress
|
||||
}
|
||||
|
||||
func newGenericStaticLoopInServerWithStore(t *testing.T) (*swapClientServer,
|
||||
*loopin.StaticAddressLoopIn, *mockStaticAddressLoopInStore) {
|
||||
|
||||
t.Helper()
|
||||
|
||||
_, clientPubkey := mock_lnd.CreateKey(10)
|
||||
_, serverPubkey := mock_lnd.CreateKey(11)
|
||||
addressParams, _ := newTestStaticAddressParams(t)
|
||||
depositOutpoint := wire.OutPoint{
|
||||
Hash: chainhash.Hash{12, 13, 14},
|
||||
Index: 2,
|
||||
}
|
||||
staticDeposit := &deposit.Deposit{
|
||||
OutPoint: depositOutpoint,
|
||||
Value: 51_000,
|
||||
}
|
||||
lastHop := route.Vertex{7, 8, 9}
|
||||
|
||||
staticLoopIn := &loopin.StaticAddressLoopIn{
|
||||
SwapHash: lntypes.Hash{1, 2, 3},
|
||||
HtlcCltvExpiry: 700,
|
||||
InitiationTime: time.Unix(100, 0).UTC(),
|
||||
LastUpdateTime: time.Unix(200, 0).UTC(),
|
||||
Label: "static-loop-in",
|
||||
ClientPubkey: clientPubkey,
|
||||
ServerPubkey: serverPubkey,
|
||||
LastHop: lastHop[:],
|
||||
QuotedSwapFee: 1_111,
|
||||
SelectedAmount: 50_000,
|
||||
DepositOutpoints: []string{depositOutpoint.String()},
|
||||
Deposits: []*deposit.Deposit{staticDeposit},
|
||||
AddressParams: addressParams,
|
||||
}
|
||||
staticLoopIn.SetState(loopin.PaymentReceived)
|
||||
|
||||
depositStore := &mockDepositStore{
|
||||
byOutpoint: map[string]*deposit.Deposit{
|
||||
depositOutpoint.String(): staticDeposit,
|
||||
},
|
||||
}
|
||||
loopInStore := &mockStaticAddressLoopInStore{
|
||||
swaps: []*loopin.StaticAddressLoopIn{staticLoopIn},
|
||||
}
|
||||
staticLoopInManager, err := loopin.NewManager(&loopin.Config{
|
||||
Store: loopInStore,
|
||||
DepositManager: deposit.NewManager(&deposit.ManagerConfig{
|
||||
Store: depositStore,
|
||||
}),
|
||||
}, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
return &swapClientServer{
|
||||
network: lndclient.NetworkTestnet,
|
||||
swaps: make(map[lntypes.Hash]loop.SwapInfo),
|
||||
staticLoopInManager: staticLoopInManager,
|
||||
}, staticLoopIn, loopInStore
|
||||
}
|
||||
|
||||
// mockStaticAddressLoopInStore is a minimal in-memory loop-in store for RPC
|
||||
// response mapping tests.
|
||||
type mockStaticAddressLoopInStore struct {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue