Browse Source

loopdb: make new loopdb package to house persistent storage

pull/9/head
Olaoluwa Osuntokun 2 years ago
parent
commit
cdcb9f8345
12 changed files with 581 additions and 434 deletions
  1. +0
    -96
      client/README.md
  2. +0
    -17
      client/state_type.go
  3. +0
    -65
      client/store_interface.go
  4. +8
    -0
      loopdb/codec.go
  5. +27
    -0
      loopdb/interface.go
  6. +44
    -0
      loopdb/log.go
  7. +9
    -8
      loopdb/meta.go
  8. +307
    -0
      loopdb/store.go
  9. +56
    -29
      loopdb/store_test.go
  10. +41
    -0
      loopdb/swapcontract.go
  11. +35
    -10
      loopdb/swapstate.go
  12. +54
    -209
      loopdb/uncharge.go

+ 0
- 96
client/README.md View File

@ -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 <amt_msat>`
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/<network>/swapclient.db`.
## Multiple simultaneous swaps
It is possible to execute multiple swaps simultaneously.

+ 0
- 17
client/state_type.go View File

@ -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
)

+ 0
- 65
client/store_interface.go View File

@ -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
}

+ 8
- 0
loopdb/codec.go View File

@ -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
}

+ 27
- 0
loopdb/interface.go View File

@ -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?

+ 44
- 0
loopdb/log.go View File

@ -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)
}

client/store_meta.go → loopdb/meta.go View File

@ -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
}

+ 307
- 0
loopdb/store.go View File

@ -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()
}

client/store_test.go → loopdb/store_test.go View File

@ -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)
}

+ 41
- 0
loopdb/swapcontract.go View File

@ -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
}

client/uncharge_state.go → loopdb/swapstate.go View File

@ -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"
}

client/store.go → loopdb/uncharge.go View File

@ -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
}

Loading…
Cancel
Save