From dcf21513523e1023906759493cb6148db67f719c Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Mon, 4 Mar 2024 14:02:31 +0100 Subject: [PATCH] staticaddr: deposit interface and sql store --- staticaddr/deposit.go | 66 ++++++++++++++ staticaddr/interface.go | 16 ++++ staticaddr/sql_store.go | 191 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 273 insertions(+) create mode 100644 staticaddr/deposit.go diff --git a/staticaddr/deposit.go b/staticaddr/deposit.go new file mode 100644 index 0000000000..5802cc44ba --- /dev/null +++ b/staticaddr/deposit.go @@ -0,0 +1,66 @@ +package staticaddr + +import ( + "crypto/rand" + "fmt" + + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/loop/fsm" +) + +// ID is a unique identifier for a deposit. +type ID [IdLength]byte + +// FromByteSlice creates a deposit id from a byte slice. +func (r *ID) FromByteSlice(b []byte) error { + if len(b) != IdLength { + return fmt.Errorf("deposit id must be 32 bytes, got %d, %x", + len(b), b) + } + + copy(r[:], b) + + return nil +} + +// Deposit bundles an utxo at a static address together with manager-relevant +// data. +type Deposit struct { + // ID is the unique identifier of the deposit. + ID ID + + // State is the current state of the deposit. + State fsm.StateType + + // The outpoint of the deposit. + wire.OutPoint + + // Value is the amount of the deposit. + Value btcutil.Amount + + // ConfirmationHeight is the absolute height at which the deposit was + // first confirmed. + ConfirmationHeight int64 + + // TimeOutSweepPkScript is the pk script that is used to sweep the + // deposit to after it is expired. + TimeOutSweepPkScript []byte +} + +// IsPending returns true if the deposit is pending. +func (d *Deposit) IsPending() bool { + return !d.IsFinal() +} + +// IsFinal returns true if the deposit is final. +func (d *Deposit) IsFinal() bool { + return d.State == SweptExpiredDeposit || d.State == Failed +} + +// GetRandomDepositID generates a random deposit ID. +func GetRandomDepositID() (ID, error) { + var id ID + _, err := rand.Read(id[:]) + return id, err +} diff --git a/staticaddr/interface.go b/staticaddr/interface.go index 8b424fbca9..1231fef591 100644 --- a/staticaddr/interface.go +++ b/staticaddr/interface.go @@ -13,6 +13,10 @@ var ( ErrAddressNotFound = fmt.Errorf("address not found") ) +const ( + IdLength = 32 +) + // AddressStore is the database interface that is used to store and retrieve // static addresses. type AddressStore interface { @@ -29,6 +33,18 @@ type AddressStore interface { // GetAllStaticAddresses retrieves all static addresses from the store. GetAllStaticAddresses(ctx context.Context) ( []*AddressParameters, error) + + // CreateDeposit inserts a new deposit into the store. + CreateDeposit(ctx context.Context, deposit *Deposit) error + + // UpdateDeposit updates the deposit in the database. + UpdateDeposit(ctx context.Context, deposit *Deposit) error + + // GetDeposit retrieves a deposit with depositID from the database. + GetDeposit(ctx context.Context, depositID ID) (*Deposit, error) + + // AllDeposits retrieves all deposits from the store. + AllDeposits(ctx context.Context) ([]*Deposit, error) } // AddressParameters holds all the necessary information for the 2-of-2 multisig diff --git a/staticaddr/sql_store.go b/staticaddr/sql_store.go index 7f14f7c5e5..418555c385 100644 --- a/staticaddr/sql_store.go +++ b/staticaddr/sql_store.go @@ -2,18 +2,26 @@ package staticaddr import ( "context" + "database/sql" "errors" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" "github.com/jackc/pgx/v4" + "github.com/lightninglabs/loop/fsm" "github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/loopdb/sqlc" + "github.com/lightningnetwork/lnd/clock" "github.com/lightningnetwork/lnd/keychain" ) // SqlStore is the backing store for static addresses. type SqlStore struct { baseDB *loopdb.BaseDB + + clock clock.Clock } // NewSqlStore constructs a new SQLStore from a BaseDB. The BaseDB is agnostic @@ -21,6 +29,8 @@ type SqlStore struct { func NewSqlStore(db *loopdb.BaseDB) *SqlStore { return &SqlStore{ baseDB: db, + + clock: clock.NewDefaultClock(), } } @@ -112,6 +122,179 @@ func (s *SqlStore) GetAllStaticAddresses(ctx context.Context) ( return result, nil } +// CreateDeposit creates a static address record in the database. +func (s *SqlStore) CreateDeposit(ctx context.Context, deposit *Deposit) error { + createArgs := sqlc.CreateDepositParams{ + DepositID: deposit.ID[:], + TxHash: deposit.Hash[:], + OutIndex: int32(deposit.Index), + Amount: int64(deposit.Value), + ConfirmationHeight: deposit.ConfirmationHeight, + TimeOutSweepPkScript: deposit.TimeOutSweepPkScript, + } + + updateArgs := sqlc.InsertDepositUpdateParams{ + DepositID: deposit.ID[:], + UpdateTimestamp: s.clock.Now().UTC(), + UpdateState: string(deposit.State), + } + + return s.baseDB.ExecTx(ctx, &loopdb.SqliteTxOptions{}, + func(q *sqlc.Queries) error { + err := q.CreateDeposit(ctx, createArgs) + if err != nil { + return err + } + + return q.InsertDepositUpdate(ctx, updateArgs) + }) +} + +// UpdateDeposit updates the deposit in the database. +func (s *SqlStore) UpdateDeposit(ctx context.Context, deposit *Deposit) error { + insertUpdateArgs := sqlc.InsertDepositUpdateParams{ + DepositID: deposit.ID[:], + UpdateTimestamp: s.clock.Now().UTC(), + UpdateState: string(deposit.State), + } + + var ( + txHash = deposit.Hash[:] + outIndex = sql.NullInt32{ + Int32: int32(deposit.Index), + Valid: true, + } + ) + + updateArgs := sqlc.UpdateDepositParams{ + DepositID: deposit.ID[:], + TxHash: txHash, + OutIndex: outIndex.Int32, + ConfirmationHeight: marshalSqlNullInt64( + deposit.ConfirmationHeight, + ).Int64, + } + + return s.baseDB.ExecTx(ctx, &loopdb.SqliteTxOptions{}, + func(q *sqlc.Queries) error { + err := q.UpdateDeposit(ctx, updateArgs) + if err != nil { + return err + } + + return q.InsertDepositUpdate(ctx, insertUpdateArgs) + }) +} + +// GetDeposit retrieves the deposit from the database. +func (s *SqlStore) GetDeposit(ctx context.Context, id ID) (*Deposit, error) { + var deposit *Deposit + err := s.baseDB.ExecTx(ctx, loopdb.NewSqlReadOpts(), + func(q *sqlc.Queries) error { + var err error + row, err := q.GetDeposit(ctx, id[:]) + if err != nil { + return err + } + + updates, err := q.GetDepositUpdates(ctx, id[:]) + if err != nil { + return err + } + + if len(updates) == 0 { + return errors.New("no deposit updates") + } + + deposit, err = s.toDeposit(row, updates[len(updates)-1]) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return nil, err + } + + return deposit, nil +} + +// AllDeposits retrieves all known deposits to our static address. +func (s *SqlStore) AllDeposits(ctx context.Context) ([]*Deposit, error) { + var allDeposits []*Deposit + + err := s.baseDB.ExecTx(ctx, loopdb.NewSqlReadOpts(), + func(q *sqlc.Queries) error { + var err error + + deposits, err := q.AllDeposits(ctx) + if err != nil { + return err + } + + for _, deposit := range deposits { + updates, err := q.GetDepositUpdates( + ctx, deposit.DepositID, + ) + if err != nil { + return err + } + + if len(updates) == 0 { + return errors.New("no deposit updates") + } + + lastUpdate := updates[len(updates)-1] + + d, err := s.toDeposit(deposit, lastUpdate) + if err != nil { + return err + } + + allDeposits = append(allDeposits, d) + } + + return nil + }) + if err != nil { + return nil, err + } + + return allDeposits, nil +} + +// toDeposit converts an sql deposit to a deposit. +func (s *SqlStore) toDeposit(row sqlc.Deposit, + lastUpdate sqlc.DepositUpdate) (*Deposit, error) { + + id := ID{} + err := id.FromByteSlice(row.DepositID) + if err != nil { + return nil, err + } + + var txHash *chainhash.Hash + if row.TxHash != nil { + txHash, err = chainhash.NewHash(row.TxHash) + if err != nil { + return nil, err + } + } + + return &Deposit{ + ID: id, + State: fsm.StateType(lastUpdate.UpdateState), + OutPoint: wire.OutPoint{ + Hash: *txHash, + Index: uint32(row.OutIndex), + }, + Value: btcutil.Amount(row.Amount), + ConfirmationHeight: row.ConfirmationHeight, + TimeOutSweepPkScript: row.TimeOutSweepPkScript, + }, nil +} + // Close closes the database connection. func (s *SqlStore) Close() { s.baseDB.DB.Close() @@ -144,3 +327,11 @@ func (s *SqlStore) toAddressParameters(row sqlc.StaticAddress) ( ProtocolVersion: AddressProtocolVersion(row.ProtocolVersion), }, nil } + +// marshalSqlNullInt64 converts an int64 to a sql.NullInt64. +func marshalSqlNullInt64(i int64) sql.NullInt64 { + return sql.NullInt64{ + Int64: i, + Valid: i != 0, + } +}