From cdcb9f83455b162be4e052a991748bdec46bd73a Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Wed, 6 Mar 2019 18:19:57 -0800 Subject: [PATCH] loopdb: make new loopdb package to house persistent storage --- client/README.md | 96 ------ client/state_type.go | 17 - client/store_interface.go | 65 ---- loopdb/codec.go | 8 + loopdb/interface.go | 27 ++ loopdb/log.go | 44 +++ client/store_meta.go => loopdb/meta.go | 17 +- loopdb/store.go | 307 ++++++++++++++++++ {client => loopdb}/store_test.go | 85 +++-- loopdb/swapcontract.go | 41 +++ .../uncharge_state.go => loopdb/swapstate.go | 45 ++- client/store.go => loopdb/uncharge.go | 263 +++------------ 12 files changed, 581 insertions(+), 434 deletions(-) delete mode 100644 client/README.md delete mode 100644 client/state_type.go delete mode 100644 client/store_interface.go create mode 100644 loopdb/codec.go create mode 100644 loopdb/interface.go create mode 100644 loopdb/log.go rename client/store_meta.go => loopdb/meta.go (89%) create mode 100644 loopdb/store.go rename {client => loopdb}/store_test.go (50%) create mode 100644 loopdb/swapcontract.go rename client/uncharge_state.go => loopdb/swapstate.go (77%) rename client/store.go => loopdb/uncharge.go (50%) diff --git a/client/README.md b/client/README.md deleted file mode 100644 index caa7c12..0000000 --- a/client/README.md +++ /dev/null @@ -1,96 +0,0 @@ -# Swaplet - -## Uncharge swap (off -> on-chain) - -``` - swapcli uncharge 500 - | - | - v - .-----------------------------. - | Swap CLI | - | ./cmd/swapcli | - | | - | | - | .-------------------. | .--------------. .---------------. - | | Swap Client (lib) | | | LND node | | Bitcoin node | - | | ./ |<-------------| |-------------------| | - | | | | | | on-chain | | - | | |------------->| | htlc | | - | | | | off-chain | | | | - | '-------------------' | htlc '--------------' '---------------' - '-----------------|-----------' | ^ - | | | - | v | - | .--. .--. - | _ -( )- _ _ -( )- _ - | .--,( ),--. .--,( ),--. - initiate| _.-( )-._ _.-( )-._ - swap | ( LIGHTNING NETWORK ) ( BITCOIN NETWORK ) - | '-._( )_.-' '-._( )_.-' - | '__,( ),__' '__,( ),__' - | - ._(__)_. - - ._(__)_. - - | | ^ - | | | - v v | - .--------------------. off-chain .--------------. .---------------. - | Swap Server | htlc | LND node | | Bitcoin node | - | |<-------------| | | | - | | | | on-chain | | - | | | | htlc | | - | |--------------| |----------------->| | - | | | | | | - '--------------------' '--------------' '---------------' - -``` - -## Setup - -LND and the swaplet are using go modules. Make sure that the `GO111MODULE` env variable is set to `on`. - -In order to execute a swap, LND needs to be rebuilt with sub servers enabled. - -### LND - -* Checkout branch `master` - -- `make install tags="signrpc walletrpc chainrpc"` to build and install lnd with required sub-servers enabled. - -- Make sure there are no macaroons in the lnd dir `~/.lnd/data/chain/bitcoin/mainnet`. If there are, lnd has been started before and in that case, it could be that `admin.macaroon` doesn't contain signer permission. Delete `macaroons.db` and `*.macaroon`. - - DO NOT DELETE `wallet.db` ! - -- Start lnd - -### Swaplet -- `git clone git@gitlab.com:lightning-labs/swaplet.git` -- `cd swaplet/cmd` -- `go install ./...` - -## Execute a swap - -* Swaps are executed by a client daemon process. Run: - - `swapd` - - By default `swapd` attempts to connect to an lnd instance running on `localhost:10009` and reads the macaroon and tls certificate from `~/.lnd`. This can be altered using command line flags. See `swapd --help`. - - `swapd` only listens on localhost and uses an unencrypted and unauthenticated connection. - -* To initiate a swap, run: - - `swapcli uncharge ` - - When the swap is initiated successfully, `swapd` will see the process through. - -* To query and track the swap status, run `swapcli` without arguments. - -## Resume -When `swapd` is terminated (or killed) for whatever reason, it will pickup pending swaps after a restart. - -Information about pending swaps is stored persistently in the swap database. Its location is `~/.swaplet//swapclient.db`. - -## Multiple simultaneous swaps - -It is possible to execute multiple swaps simultaneously. - diff --git a/client/state_type.go b/client/state_type.go deleted file mode 100644 index dd74753..0000000 --- a/client/state_type.go +++ /dev/null @@ -1,17 +0,0 @@ -package client - -// SwapStateType defines the types of swap states that exist. Every swap state -// defined as type SwapState above, falls into one of these SwapStateType -// categories. -type SwapStateType uint8 - -const ( - // StateTypePending indicates that the swap is still pending. - StateTypePending SwapStateType = iota - - // StateTypeSuccess indicates that the swap has completed successfully. - StateTypeSuccess - - // StateTypeFail indicates that the swap has failed. - StateTypeFail -) diff --git a/client/store_interface.go b/client/store_interface.go deleted file mode 100644 index 1f3fce6..0000000 --- a/client/store_interface.go +++ /dev/null @@ -1,65 +0,0 @@ -package client - -import ( - "time" - - "github.com/lightningnetwork/lnd/lntypes" -) - -// swapClientStore provides persistent storage for swaps. -type swapClientStore interface { - // getUnchargeSwaps returns all swaps currently in the store. - getUnchargeSwaps() ([]*PersistentUncharge, error) - - // createUncharge adds an initiated swap to the store. - createUncharge(hash lntypes.Hash, swap *UnchargeContract) error - - // updateUncharge stores a swap updateUncharge. - updateUncharge(hash lntypes.Hash, time time.Time, state SwapState) error -} - -// PersistentUnchargeEvent contains the dynamic data of a swap. -type PersistentUnchargeEvent struct { - State SwapState - Time time.Time -} - -// PersistentUncharge is a combination of the contract and the updates. -type PersistentUncharge struct { - Hash lntypes.Hash - - Contract *UnchargeContract - Events []*PersistentUnchargeEvent -} - -// State returns the most recent state of this swap. -func (s *PersistentUncharge) State() SwapState { - lastUpdate := s.LastUpdate() - if lastUpdate == nil { - return StateInitiated - } - - return lastUpdate.State -} - -// LastUpdate returns the most recent update of this swap. -func (s *PersistentUncharge) LastUpdate() *PersistentUnchargeEvent { - eventCount := len(s.Events) - - if eventCount == 0 { - return nil - } - - lastEvent := s.Events[eventCount-1] - return lastEvent -} - -// LastUpdateTime returns the last update time of this swap. -func (s *PersistentUncharge) LastUpdateTime() time.Time { - lastUpdate := s.LastUpdate() - if lastUpdate == nil { - return s.Contract.InitiationTime - } - - return lastUpdate.Time -} diff --git a/loopdb/codec.go b/loopdb/codec.go new file mode 100644 index 0000000..990f164 --- /dev/null +++ b/loopdb/codec.go @@ -0,0 +1,8 @@ +package loopdb + +// itob returns an 8-byte big endian representation of v. +func itob(v uint64) []byte { + b := make([]byte, 8) + byteOrder.PutUint64(b, v) + return b +} diff --git a/loopdb/interface.go b/loopdb/interface.go new file mode 100644 index 0000000..55503e4 --- /dev/null +++ b/loopdb/interface.go @@ -0,0 +1,27 @@ +package loopdb + +import ( + "time" + + "github.com/lightningnetwork/lnd/lntypes" +) + +// SwapStore is the priamry database interface used by the loopd system. It +// houses informatino for all pending completed/failed swaps. +type SwapStore interface { + // FetchUnchargeSwaps returns all swaps currently in the store. + FetchUnchargeSwaps() ([]*PersistentUncharge, error) + + // CreateUncharge adds an initiated swap to the store. + CreateUncharge(hash lntypes.Hash, swap *UnchargeContract) error + + // UpdateUncharge stores a swap updateUncharge. This appends to the + // event log for a particular swap as it goes through the various + // stages in its lifetime. + UpdateUncharge(hash lntypes.Hash, time time.Time, state SwapState) error + + // Close closes the underlying database. + Close() error +} + +// TODO(roasbeef): back up method in interface? diff --git a/loopdb/log.go b/loopdb/log.go new file mode 100644 index 0000000..73cc81d --- /dev/null +++ b/loopdb/log.go @@ -0,0 +1,44 @@ +package loopdb + +import ( + "github.com/btcsuite/btclog" +) + +// log is a logger that is initialized with no output filters. This means +// the package will not perform any logging by default until the caller +// requests it. +var log btclog.Logger + +// The default amount of logging is none. +func init() { + DisableLog() +} + +// DisableLog disables all library log output. Logging output is disabled +// by default until either UseLogger or SetLogWriter are called. +func DisableLog() { + log = btclog.Disabled +} + +// UseLogger uses a specified Logger to output package logging info. +// This should be used in preference to SetLogWriter if the caller is also +// using btclog. +func UseLogger(logger btclog.Logger) { + log = logger +} + +// logClosure is used to provide a closure over expensive logging operations so +// don't have to be performed when the logging level doesn't warrant it. +type logClosure func() string + +// String invokes the underlying function and returns the result. +func (c logClosure) String() string { + return c() +} + +// newLogClosure returns a new closure over a function that returns a string +// which itself provides a Stringer interface so that it can be used with the +// logging system. +func newLogClosure(c func() string) logClosure { + return logClosure(c) +} diff --git a/client/store_meta.go b/loopdb/meta.go similarity index 89% rename from client/store_meta.go rename to loopdb/meta.go index 658a5a3..a1d9a18 100644 --- a/client/store_meta.go +++ b/loopdb/meta.go @@ -1,4 +1,4 @@ -package client +package loopdb import ( "errors" @@ -82,7 +82,7 @@ func syncVersions(db *bbolt.DB) error { return err } - logger.Infof("Checking for schema update: latest_version=%v, "+ + log.Infof("Checking for schema update: latest_version=%v, "+ "db_version=%v", latestDBVersion, currentVersion) switch { @@ -91,7 +91,7 @@ func syncVersions(db *bbolt.DB) error { // user is probably trying to revert to a prior version of lnd. We fail // here to prevent reversions and unintended corruption. case currentVersion > latestDBVersion: - logger.Errorf("Refusing to revert from db_version=%d to "+ + log.Errorf("Refusing to revert from db_version=%d to "+ "lower version=%d", currentVersion, latestDBVersion) @@ -103,16 +103,17 @@ func syncVersions(db *bbolt.DB) error { return nil } - logger.Infof("Performing database schema migration") + log.Infof("Performing database schema migration") - // Otherwise we execute the migrations serially within a single database - // transaction to ensure the migration is atomic. + // Otherwise we execute the migrations serially within a single + // database transaction to ensure the migration is atomic. return db.Update(func(tx *bbolt.Tx) error { for v := currentVersion; v < latestDBVersion; v++ { - logger.Infof("Applying migration #%v", v+1) + log.Infof("Applying migration #%v", v+1) + migration := migrations[v] if err := migration(tx); err != nil { - logger.Infof("Unable to apply migration #%v", + log.Infof("Unable to apply migration #%v", v+1) return err } diff --git a/loopdb/store.go b/loopdb/store.go new file mode 100644 index 0000000..2578832 --- /dev/null +++ b/loopdb/store.go @@ -0,0 +1,307 @@ +package loopdb + +import ( + "encoding/binary" + "errors" + "fmt" + "os" + "path/filepath" + "time" + + "github.com/coreos/bbolt" + "github.com/lightningnetwork/lnd/lntypes" +) + +var ( + // dbFileName is the default file name of the client-side loop sub-swap + // database. + dbFileName = "loop.db" + + // unchargeSwapsBucketKey is a bucket that contains all swaps that are + // currently pending or completed. This bucket is keyed by the + // swaphash, and leads to a nested sub-bucket that houses information + // for that swap. + // + // maps: swapHash -> swapBucket + unchargeSwapsBucketKey = []byte("uncharge-swaps") + + // unchargeUpdatesBucketKey is a bucket that contains all updates + // pertaining to a swap. This is a sub-bucket of the swap bucket for a + // particular swap. This list only ever grows. + // + // path: unchargeUpdatesBucket -> swapBucket[hash] -> updateBucket + // + // maps: updateNumber -> time || state + updatesBucketKey = []byte("updates") + + // contractKey is the key that stores the serialized swap contract. It + // is nested within the sub-bucket for each active swap. + // + // path: unchargeUpdatesBucket -> swapBucket[hash] + // + // value: time || rawSwapState + contractKey = []byte("contract") + + byteOrder = binary.BigEndian + + keyLength = 33 +) + +// fileExists returns true if the file exists, and false otherwise. +func fileExists(path string) bool { + if _, err := os.Stat(path); err != nil { + if os.IsNotExist(err) { + return false + } + } + + return true +} + +// boltSwapStore stores swap data in boltdb. +type boltSwapStore struct { + db *bbolt.DB +} + +// A compile-time flag to ensure that boltSwapStore implements the SwapStore +// interface. +var _ = (*boltSwapStore)(nil) + +// newBoltSwapStore creates a new client swap store. +func newBoltSwapStore(dbPath string) (*boltSwapStore, error) { + + // If the target path for the swap store doesn't exist, then we'll + // create it now before we proceed. + if !fileExists(dbPath) { + if err := os.MkdirAll(dbPath, 0700); err != nil { + return nil, err + } + } + + // Now that we know that path exists, we'll open up bolt, which + // implements our default swap store. + path := filepath.Join(dbPath, dbFileName) + bdb, err := bbolt.Open(path, 0600, nil) + if err != nil { + return nil, err + } + + // We'll create all the buckets we need if this is the first time we're + // starting up. If they already exist, then these calls will be noops. + err = bdb.Update(func(tx *bbolt.Tx) error { + _, err := tx.CreateBucketIfNotExists(unchargeSwapsBucketKey) + if err != nil { + return err + } + _, err = tx.CreateBucketIfNotExists(updatesBucketKey) + if err != nil { + return err + } + _, err = tx.CreateBucketIfNotExists(metaBucket) + if err != nil { + return err + } + return nil + }) + if err != nil { + return nil, err + } + + // Finally, before we start, we'll sync the DB versions to pick up any + // possible DB migrations. + err = syncVersions(bdb) + if err != nil { + return nil, err + } + + return &boltSwapStore{ + db: bdb, + }, nil +} + +// FetchUnchargeSwaps returns all swaps currently in the store. +// +// NOTE: Part of the loopdb.SwapStore interface. +func (s *boltSwapStore) FetchUnchargeSwaps() ([]*PersistentUncharge, error) { + var swaps []*PersistentUncharge + + err := s.db.View(func(tx *bbolt.Tx) error { + // First, we'll grab our main loop out swap bucket key. + rootBucket := tx.Bucket(unchargeSwapsBucketKey) + if rootBucket == nil { + return errors.New("bucket does not exist") + } + + // We'll now traverse the root bucket for all active swaps. The + // primary key is the swap hash itself. + return rootBucket.ForEach(func(swapHash, v []byte) error { + // Only go into things that we know are sub-bucket + // keys. + if v != nil { + return nil + } + + // From the root bucket, we'll grab the next swap + // bucket for this swap from its swaphash. + swapBucket := rootBucket.Bucket(swapHash) + if swapBucket == nil { + return fmt.Errorf("swap bucket %x not found", + swapHash) + } + + // With the main swap bucket obtained, we'll grab the + // raw swap contract bytes and decode it. + contractBytes := swapBucket.Get(contractKey) + if contractBytes == nil { + return errors.New("contract not found") + } + contract, err := deserializeUnchargeContract( + contractBytes, + ) + if err != nil { + return err + } + + // Once we have the raw swap, we'll also need to decode + // each of the past updates to the swap itself. + stateBucket := swapBucket.Bucket(updatesBucketKey) + if stateBucket == nil { + return errors.New("updates bucket not found") + } + + // De serialize and collect each swap update into our + // slice of swap events. + var updates []*PersistentUnchargeEvent + err = stateBucket.ForEach(func(k, v []byte) error { + event, err := deserializeUnchargeUpdate(v) + if err != nil { + return err + } + + updates = append(updates, event) + return nil + }) + if err != nil { + return err + } + + var hash lntypes.Hash + copy(hash[:], swapHash) + + swap := PersistentUncharge{ + Contract: contract, + Hash: hash, + Events: updates, + } + + swaps = append(swaps, &swap) + return nil + }) + }) + if err != nil { + return nil, err + } + + return swaps, nil +} + +// CreateUncharge adds an initiated swap to the store. +// +// NOTE: Part of the loopdb.SwapStore interface. +func (s *boltSwapStore) CreateUncharge(hash lntypes.Hash, + swap *UnchargeContract) error { + + // If the hash doesn't match the pre-image, then this is an invalid + // swap so we'll bail out early. + if hash != swap.Preimage.Hash() { + return errors.New("hash and preimage do not match") + } + + // Otherwise, we'll create a new swap within the database. + return s.db.Update(func(tx *bbolt.Tx) error { + // First, we'll grab the root bucket that houses all of our + // main swaps. + rootBucket, err := tx.CreateBucketIfNotExists( + unchargeSwapsBucketKey, + ) + if err != nil { + return err + } + + // If the swap already exists, then we'll exit as we don't want + // to override a swap. + if rootBucket.Get(hash[:]) != nil { + return fmt.Errorf("swap %v already exists", + swap.Preimage) + } + + // From the root bucket, we'll make a new sub swap bucket using + // the swap hash. + swapBucket, err := rootBucket.CreateBucket(hash[:]) + if err != nil { + return err + } + + // With out swap bucket created, we'll serialize and store the + // swap itself. + contract, err := serializeUnchargeContract(swap) + if err != nil { + return err + } + if err := swapBucket.Put(contractKey, contract); err != nil { + return err + } + + // Finally, we'll create an empty updates bucket for this swap + // to track any future updates to the swap itself. + _, err = swapBucket.CreateBucket(updatesBucketKey) + return err + }) +} + +// UpdateUncharge stores a swap updateUncharge. This appends to the event log +// for a particular swap as it goes through the various stages in its lifetime. +// +// NOTE: Part of the loopdb.SwapStore interface. +func (s *boltSwapStore) UpdateUncharge(hash lntypes.Hash, time time.Time, + state SwapState) error { + + return s.db.Update(func(tx *bbolt.Tx) error { + // Starting from the root bucket, we'll traverse the bucket + // hierarchy all the way down to the swap bucket, and the + // update sub-bucket within that. + rootBucket := tx.Bucket(unchargeSwapsBucketKey) + if rootBucket == nil { + return errors.New("bucket does not exist") + } + swapBucket := rootBucket.Bucket(hash[:]) + if swapBucket == nil { + return errors.New("swap not found") + } + updateBucket := swapBucket.Bucket(updatesBucketKey) + if updateBucket == nil { + return errors.New("udpate bucket not found") + } + + // Each update for this swap will get a new monotonically + // increasing ID number that we'll obtain now. + id, err := updateBucket.NextSequence() + if err != nil { + return err + } + + // With the ID obtained, we'll write out this new update value. + updateValue, err := serializeUnchargeUpdate(time, state) + if err != nil { + return err + } + return updateBucket.Put(itob(id), updateValue) + }) +} + +// Close closes the underlying database. +// +// NOTE: Part of the loopdb.SwapStore interface. +func (s *boltSwapStore) Close() error { + return s.db.Close() +} diff --git a/client/store_test.go b/loopdb/store_test.go similarity index 50% rename from client/store_test.go rename to loopdb/store_test.go index 1780797..58017f6 100644 --- a/client/store_test.go +++ b/loopdb/store_test.go @@ -1,48 +1,67 @@ -package client +package loopdb import ( "crypto/sha256" "io/ioutil" + "os" "reflect" "testing" "time" "github.com/lightninglabs/loop/test" + "github.com/lightningnetwork/lnd/lntypes" ) -func TestStore(t *testing.T) { +var ( + senderKey = [33]byte{ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, + } + + receiverKey = [33]byte{ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, + } + + testPreimage = lntypes.Preimage([32]byte{ + 1, 1, 1, 1, 2, 2, 2, 2, + 3, 3, 3, 3, 4, 4, 4, 4, + 1, 1, 1, 1, 2, 2, 2, 2, + 3, 3, 3, 3, 4, 4, 4, 4, + }) + + testTime = time.Date(2018, time.January, 9, 14, 00, 00, 0, time.UTC) +) +// TestBoltSwapStore tests all the basic functionality of the current bbolt +// swap store. +func TestBoltSwapStore(t *testing.T) { tempDirName, err := ioutil.TempDir("", "clientstore") if err != nil { t.Fatal(err) } + defer os.RemoveAll(tempDirName) - store, err := newBoltSwapClientStore(tempDirName) + store, err := newBoltSwapStore(tempDirName) if err != nil { t.Fatal(err) } - swaps, err := store.getUnchargeSwaps() + // First, verify that an empty database has no active swaps. + swaps, err := store.FetchUnchargeSwaps() if err != nil { t.Fatal(err) } - if len(swaps) != 0 { t.Fatal("expected empty store") } destAddr := test.GetDestAddr(t, 0) - - senderKey := [33]byte{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2} - - receiverKey := [33]byte{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3} - hash := sha256.Sum256(testPreimage[:]) - initiationTime := time.Date(2018, 11, 1, 0, 0, 0, 0, time.UTC) + // Next, we'll make a new pending swap that we'll insert into the + // database shortly. pendingSwap := UnchargeContract{ SwapContract: SwapContract{ AmountRequested: 100, @@ -66,10 +85,12 @@ func TestStore(t *testing.T) { SweepConfTarget: 2, } + // checkSwap is a test helper function that'll assert the state of a + // swap. checkSwap := func(expectedState SwapState) { t.Helper() - swaps, err := store.getUnchargeSwaps() + swaps, err := store.FetchUnchargeSwaps() if err != nil { t.Fatal(err) } @@ -90,42 +111,48 @@ func TestStore(t *testing.T) { } } - err = store.createUncharge(hash, &pendingSwap) - if err != nil { + // If we create a new swap, then it should show up as being initialized + // right after. + if err := store.CreateUncharge(hash, &pendingSwap); err != nil { t.Fatal(err) } - checkSwap(StateInitiated) - err = store.createUncharge(hash, &pendingSwap) - if err == nil { + // Trying to make the same swap again should result in an error. + if err := store.CreateUncharge(hash, &pendingSwap); err == nil { t.Fatal("expected error on storing duplicate") } - checkSwap(StateInitiated) - if err := store.updateUncharge(hash, testTime, StatePreimageRevealed); err != nil { + // Next, we'll update to the next state of the pre-image being + // revealed. The state should be reflected here again. + err = store.UpdateUncharge( + hash, testTime, StatePreimageRevealed, + ) + if err != nil { t.Fatal(err) } - checkSwap(StatePreimageRevealed) - if err := store.updateUncharge(hash, testTime, StateFailInsufficientValue); err != nil { + // Next, we'll update to the final state to ensure that the state is + // properly updated. + err = store.UpdateUncharge( + hash, testTime, StateFailInsufficientValue, + ) + if err != nil { t.Fatal(err) } - checkSwap(StateFailInsufficientValue) - err = store.close() - if err != nil { + if err := store.Close(); err != nil { t.Fatal(err) } - // Reopen store - store, err = newBoltSwapClientStore(tempDirName) + // If we re-open the same store, then the state of the current swap + // should be the same. + store, err = newBoltSwapStore(tempDirName) if err != nil { t.Fatal(err) } - checkSwap(StateFailInsufficientValue) } diff --git a/loopdb/swapcontract.go b/loopdb/swapcontract.go new file mode 100644 index 0000000..f9a3b7b --- /dev/null +++ b/loopdb/swapcontract.go @@ -0,0 +1,41 @@ +package loopdb + +import ( + "time" + + "github.com/btcsuite/btcutil" + "github.com/lightningnetwork/lnd/lntypes" +) + +// SwapContract contains the base data that is serialized to persistent storage +// for pending swaps. +type SwapContract struct { + Preimage lntypes.Preimage + AmountRequested btcutil.Amount + + PrepayInvoice string + + SenderKey [33]byte + ReceiverKey [33]byte + + CltvExpiry int32 + + // MaxPrepayRoutingFee is the maximum off-chain fee in msat that may be + // paid for the prepayment to the server. + MaxPrepayRoutingFee btcutil.Amount + + // MaxSwapFee is the maximum we are willing to pay the server for the + // swap. + MaxSwapFee btcutil.Amount + + // MaxMinerFee is the maximum in on-chain fees that we are willing to + // spend. + MaxMinerFee btcutil.Amount + + // InitiationHeight is the block height at which the swap was + // initiated. + InitiationHeight int32 + + // InitiationTime is the time at which the swap was initiated. + InitiationTime time.Time +} diff --git a/client/uncharge_state.go b/loopdb/swapstate.go similarity index 77% rename from client/uncharge_state.go rename to loopdb/swapstate.go index 23a776a..5b27870 100644 --- a/client/uncharge_state.go +++ b/loopdb/swapstate.go @@ -1,4 +1,4 @@ -package client +package loopdb // SwapState indicates the current state of a swap. type SwapState uint8 @@ -7,7 +7,7 @@ const ( // StateInitiated is the initial state of a swap. At that point, the // initiation call to the server has been made and the payment process // has been started for the swap and prepayment invoices. - StateInitiated SwapState = iota + StateInitiated SwapState = 0 // StatePreimageRevealed is reached when the sweep tx publication is // first attempted. From that point on, we should consider the preimage @@ -15,39 +15,55 @@ const ( // confirmed. This state will mostly coalesce with StateHtlcConfirmed, // except in the case where we wait for fees to come down before we // sweep. - StatePreimageRevealed + StatePreimageRevealed = 1 // StateSuccess is the final swap state that is reached when the sweep // tx has the required confirmation depth (SweepConfDepth) and the // server pulled the off-chain htlc. - StateSuccess + StateSuccess = 2 // StateFailOffchainPayments indicates that it wasn't possible to find routes // for one or both of the off-chain payments to the server that // satisfied the payment restrictions (fee and timelock limits). - StateFailOffchainPayments + StateFailOffchainPayments = 3 // StateFailTimeout indicates that the on-chain htlc wasn't confirmed before // its expiry or confirmed too late (MinPreimageRevealDelta violated). - StateFailTimeout + StateFailTimeout = 4 // StateFailSweepTimeout indicates that the on-chain htlc wasn't swept before // the server revoked the htlc. The server didn't pull the off-chain // htlc (even though it could have) and we timed out the off-chain htlc // ourselves. No funds lost. - StateFailSweepTimeout + StateFailSweepTimeout = 5 // StateFailInsufficientValue indicates that the published on-chain htlc had // a value lower than the requested amount. - StateFailInsufficientValue + StateFailInsufficientValue = 6 // StateFailTemporary indicates that the swap cannot progress because // of an internal error. This is not a final state. Manual intervention // (like a restart) is required to solve this problem. - StateFailTemporary + StateFailTemporary = 7 // StateHtlcPublished means that the client published the on-chain htlc. - StateHtlcPublished + StateHtlcPublished = 8 +) + +// SwapStateType defines the types of swap states that exist. Every swap state +// defined as type SwapState above, falls into one of these SwapStateType +// categories. +type SwapStateType uint8 + +const ( + // StateTypePending indicates that the swap is still pending. + StateTypePending SwapStateType = 0 + + // StateTypeSuccess indicates that the swap has completed successfully. + StateTypeSuccess = 1 + + // StateTypeFail indicates that the swap has failed. + StateTypeFail = 2 ) // Type returns the type of the SwapState it is called on. @@ -65,24 +81,33 @@ func (s SwapState) Type() SwapStateType { return StateTypeFail } +// String returns a string representation of the swap's state. func (s SwapState) String() string { switch s { case StateInitiated: return "Initiated" + case StatePreimageRevealed: return "PreimageRevealed" + case StateSuccess: return "Success" + case StateFailOffchainPayments: return "FailOffchainPayments" + case StateFailTimeout: return "FailTimeout" + case StateFailSweepTimeout: return "FailSweepTimeout" + case StateFailInsufficientValue: return "FailInsufficientValue" + case StateFailTemporary: return "FailTemporary" + default: return "Unknown" } diff --git a/client/store.go b/loopdb/uncharge.go similarity index 50% rename from client/store.go rename to loopdb/uncharge.go index 3783f51..8d69da4 100644 --- a/client/store.go +++ b/loopdb/uncharge.go @@ -1,240 +1,92 @@ -package client +package loopdb import ( "bytes" "encoding/binary" - "errors" "fmt" "io" - "os" - "path/filepath" "time" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" - "github.com/coreos/bbolt" - "github.com/lightninglabs/loop/utils" "github.com/lightningnetwork/lnd/lntypes" ) -var ( - dbFileName = "swapclient.db" +// UnchargeContract contains the data that is serialized to persistent storage +// for pending swaps. +type UnchargeContract struct { + SwapContract - // unchargeSwapsBucketKey is a bucket that contains all swaps that are - // currently pending or completed. - // - // maps: swap_hash -> UnchargeContract - unchargeSwapsBucketKey = []byte("uncharge-swaps") + DestAddr btcutil.Address - // unchargeUpdatesBucketKey is a bucket that contains all updates - // pertaining to a swap. This list only ever grows. - // - // maps: update_nr -> time | state - updatesBucketKey = []byte("updates") + SwapInvoice string - // contractKey is the key that stores the serialized swap contract. - contractKey = []byte("contract") + // MaxSwapRoutingFee is the maximum off-chain fee in msat that may be + // paid for the swap payment to the server. + MaxSwapRoutingFee btcutil.Amount - byteOrder = binary.BigEndian + // SweepConfTarget specifies the targeted confirmation target for the + // client sweep tx. + SweepConfTarget int32 - keyLength = 33 -) - -// boltSwapClientStore stores swap data in boltdb. -type boltSwapClientStore struct { - db *bbolt.DB + // UnchargeChannel is the channel to uncharge. If zero, any channel may + // be used. + UnchargeChannel *uint64 } -// newBoltSwapClientStore creates a new client swap store. -func newBoltSwapClientStore(dbPath string) (*boltSwapClientStore, error) { - if !utils.FileExists(dbPath) { - if err := os.MkdirAll(dbPath, 0700); err != nil { - return nil, err - } - } - path := filepath.Join(dbPath, dbFileName) - bdb, err := bbolt.Open(path, 0600, nil) - if err != nil { - return nil, err - } - - err = bdb.Update(func(tx *bbolt.Tx) error { - _, err := tx.CreateBucketIfNotExists(unchargeSwapsBucketKey) - if err != nil { - return err - } - _, err = tx.CreateBucketIfNotExists(updatesBucketKey) - if err != nil { - return err - } - _, err = tx.CreateBucketIfNotExists(metaBucket) - if err != nil { - return err - } - return nil - }) - if err != nil { - return nil, err - } - - err = syncVersions(bdb) - if err != nil { - return nil, err - } +// PersistentUnchargeEvent contains the dynamic data of a swap. +type PersistentUnchargeEvent struct { + // State is the new state for this swap as a result of this event. + State SwapState - return &boltSwapClientStore{ - db: bdb, - }, nil + // Time is the time that this swap had its state changed. + Time time.Time } -// getUnchargeSwaps returns all swaps currently in the store. -func (s *boltSwapClientStore) getUnchargeSwaps() ([]*PersistentUncharge, error) { - var swaps []*PersistentUncharge - err := s.db.View(func(tx *bbolt.Tx) error { - - bucket := tx.Bucket(unchargeSwapsBucketKey) - if bucket == nil { - return errors.New("bucket does not exist") - } - - err := bucket.ForEach(func(k, _ []byte) error { - swapBucket := bucket.Bucket(k) - if swapBucket == nil { - return fmt.Errorf("swap bucket %x not found", - k) - } - - contractBytes := swapBucket.Get(contractKey) - if contractBytes == nil { - return errors.New("contract not found") - } - - contract, err := deserializeUnchargeContract( - contractBytes, - ) - if err != nil { - return err - } - - stateBucket := swapBucket.Bucket(updatesBucketKey) - if stateBucket == nil { - return errors.New("updates bucket not found") - } - var updates []*PersistentUnchargeEvent - err = stateBucket.ForEach(func(k, v []byte) error { - event, err := deserializeUnchargeUpdate(v) - if err != nil { - return err - } - updates = append(updates, event) - return nil - }) - if err != nil { - return err - } - - var hash lntypes.Hash - copy(hash[:], k) - - swap := PersistentUncharge{ - Contract: contract, - Hash: hash, - Events: updates, - } - - swaps = append(swaps, &swap) - return nil - }) - if err != nil { - return err - } +// PersistentUncharge is a combination of the contract and the updates. +type PersistentUncharge struct { + // Hash is the hash that uniquely identifies this swap. + Hash lntypes.Hash - return nil - }) - if err != nil { - return nil, err - } + // Contract is the active contract for this swap. It describes the + // precise details of the swap including the final fee, CLTV value, + // etc. + Contract *UnchargeContract - return swaps, nil + // Events are each of the state transitions that this swap underwent. + Events []*PersistentUnchargeEvent } -// createUncharge adds an initiated swap to the store. -func (s *boltSwapClientStore) createUncharge(hash lntypes.Hash, - swap *UnchargeContract) error { - - if hash != swap.Preimage.Hash() { - return errors.New("hash and preimage do not match") +// State returns the most recent state of this swap. +func (s *PersistentUncharge) State() SwapState { + lastUpdate := s.LastUpdate() + if lastUpdate == nil { + return StateInitiated } - return s.db.Update(func(tx *bbolt.Tx) error { - bucket, err := tx.CreateBucketIfNotExists(unchargeSwapsBucketKey) - if err != nil { - return err - } - - if bucket.Get(hash[:]) != nil { - return fmt.Errorf("swap %v already exists", swap.Preimage) - } - - // Create bucket for swap. - swapBucket, err := bucket.CreateBucket(hash[:]) - if err != nil { - return err - } + return lastUpdate.State +} - contract, err := serializeUnchargeContract(swap) - if err != nil { - return err - } +// LastUpdate returns the most recent update of this swap. +func (s *PersistentUncharge) LastUpdate() *PersistentUnchargeEvent { + eventCount := len(s.Events) - // Store contact. - if err := swapBucket.Put(contractKey, contract); err != nil { - return err - } + if eventCount == 0 { + return nil + } - // Create empty updates bucket. - _, err = swapBucket.CreateBucket(updatesBucketKey) - return err - }) + lastEvent := s.Events[eventCount-1] + return lastEvent } -// updateUncharge stores a swap updateUncharge. -func (s *boltSwapClientStore) updateUncharge(hash lntypes.Hash, time time.Time, - state SwapState) error { - - return s.db.Update(func(tx *bbolt.Tx) error { - bucket := tx.Bucket(unchargeSwapsBucketKey) - if bucket == nil { - return errors.New("bucket does not exist") - } - - swapBucket := bucket.Bucket(hash[:]) - if swapBucket == nil { - return errors.New("swap not found") - } - - updateBucket := swapBucket.Bucket(updatesBucketKey) - if updateBucket == nil { - return errors.New("udpate bucket not found") - } - - id, err := updateBucket.NextSequence() - if err != nil { - return err - } - - updateValue, err := serializeUnchargeUpdate(time, state) - if err != nil { - return err - } - - return updateBucket.Put(itob(id), updateValue) - }) -} +// LastUpdateTime returns the last update time of this swap. +func (s *PersistentUncharge) LastUpdateTime() time.Time { + lastUpdate := s.LastUpdate() + if lastUpdate == nil { + return s.Contract.InitiationTime + } -// Close closes the underlying bolt db. -func (s *boltSwapClientStore) close() error { - return s.db.Close() + return lastUpdate.Time } func deserializeUnchargeContract(value []byte) (*UnchargeContract, error) { @@ -463,10 +315,3 @@ func deserializeUnchargeUpdate(value []byte) (*PersistentUnchargeEvent, error) { return update, nil } - -// itob returns an 8-byte big endian representation of v. -func itob(v uint64) []byte { - b := make([]byte, 8) - byteOrder.PutUint64(b, v) - return b -}