mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
loopdb: make new loopdb package to house persistent storage
This commit is contained in:
parent
74cf29a9cf
commit
cdcb9f8345
12 changed files with 589 additions and 442 deletions
8
loopdb/codec.go
Normal file
8
loopdb/codec.go
Normal 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
loopdb/interface.go
Normal file
27
loopdb/interface.go
Normal 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
loopdb/log.go
Normal file
44
loopdb/log.go
Normal 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)
|
||||
}
|
||||
124
loopdb/meta.go
Normal file
124
loopdb/meta.go
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
package loopdb
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/coreos/bbolt"
|
||||
)
|
||||
|
||||
var (
|
||||
// metaBucket stores all the meta information concerning the state of
|
||||
// the database.
|
||||
metaBucket = []byte("metadata")
|
||||
|
||||
// dbVersionKey is a boltdb key and it's used for storing/retrieving
|
||||
// current database version.
|
||||
dbVersionKey = []byte("dbp")
|
||||
|
||||
// ErrDBReversion is returned when detecting an attempt to revert to a
|
||||
// prior database version.
|
||||
ErrDBReversion = fmt.Errorf("channel db cannot revert to prior version")
|
||||
)
|
||||
|
||||
// migration is a function which takes a prior outdated version of the database
|
||||
// instances and mutates the key/bucket structure to arrive at a more
|
||||
// up-to-date version of the database.
|
||||
type migration func(tx *bbolt.Tx) error
|
||||
|
||||
var (
|
||||
// dbVersions is storing all versions of database. If current version
|
||||
// of database don't match with latest version this list will be used
|
||||
// for retrieving all migration function that are need to apply to the
|
||||
// current db.
|
||||
migrations = []migration{}
|
||||
|
||||
latestDBVersion = uint32(len(migrations))
|
||||
)
|
||||
|
||||
// getDBVersion retrieves the current db version.
|
||||
func getDBVersion(db *bbolt.DB) (uint32, error) {
|
||||
var version uint32
|
||||
|
||||
err := db.View(func(tx *bbolt.Tx) error {
|
||||
metaBucket := tx.Bucket(metaBucket)
|
||||
if metaBucket == nil {
|
||||
return errors.New("bucket does not exist")
|
||||
}
|
||||
|
||||
data := metaBucket.Get(dbVersionKey)
|
||||
// If no version key found, assume version is 0.
|
||||
if data != nil {
|
||||
version = byteOrder.Uint32(data)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return version, nil
|
||||
}
|
||||
|
||||
// getDBVersion updates the current db version.
|
||||
func setDBVersion(tx *bbolt.Tx, version uint32) error {
|
||||
metaBucket := tx.Bucket(metaBucket)
|
||||
if metaBucket == nil {
|
||||
return errors.New("bucket does not exist")
|
||||
}
|
||||
|
||||
scratch := make([]byte, 4)
|
||||
byteOrder.PutUint32(scratch, version)
|
||||
return metaBucket.Put(dbVersionKey, scratch)
|
||||
}
|
||||
|
||||
// syncVersions function is used for safe db version synchronization. It
|
||||
// applies migration functions to the current database and recovers the
|
||||
// previous state of db if at least one error/panic appeared during migration.
|
||||
func syncVersions(db *bbolt.DB) error {
|
||||
currentVersion, err := getDBVersion(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Infof("Checking for schema update: latest_version=%v, "+
|
||||
"db_version=%v", latestDBVersion, currentVersion)
|
||||
|
||||
switch {
|
||||
|
||||
// If the database reports a higher version that we are aware of, the
|
||||
// user is probably trying to revert to a prior version of lnd. We fail
|
||||
// here to prevent reversions and unintended corruption.
|
||||
case currentVersion > latestDBVersion:
|
||||
log.Errorf("Refusing to revert from db_version=%d to "+
|
||||
"lower version=%d", currentVersion,
|
||||
latestDBVersion)
|
||||
|
||||
return ErrDBReversion
|
||||
|
||||
// If the current database version matches the latest version number,
|
||||
// then we don't need to perform any migrations.
|
||||
case currentVersion == latestDBVersion:
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Infof("Performing database schema migration")
|
||||
|
||||
// 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++ {
|
||||
log.Infof("Applying migration #%v", v+1)
|
||||
|
||||
migration := migrations[v]
|
||||
if err := migration(tx); err != nil {
|
||||
log.Infof("Unable to apply migration #%v",
|
||||
v+1)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return setDBVersion(tx, latestDBVersion)
|
||||
})
|
||||
}
|
||||
307
loopdb/store.go
Normal file
307
loopdb/store.go
Normal 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()
|
||||
}
|
||||
158
loopdb/store_test.go
Normal file
158
loopdb/store_test.go
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
package loopdb
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/lightninglabs/loop/test"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
)
|
||||
|
||||
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 := newBoltSwapStore(tempDirName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// 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)
|
||||
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,
|
||||
Preimage: testPreimage,
|
||||
CltvExpiry: 144,
|
||||
SenderKey: senderKey,
|
||||
PrepayInvoice: "prepayinvoice",
|
||||
ReceiverKey: receiverKey,
|
||||
MaxMinerFee: 10,
|
||||
MaxSwapFee: 20,
|
||||
MaxPrepayRoutingFee: 40,
|
||||
InitiationHeight: 99,
|
||||
|
||||
// Convert to/from unix to remove timezone, so that it
|
||||
// doesn't interfere with DeepEqual.
|
||||
InitiationTime: time.Unix(0, initiationTime.UnixNano()),
|
||||
},
|
||||
DestAddr: destAddr,
|
||||
SwapInvoice: "swapinvoice",
|
||||
MaxSwapRoutingFee: 30,
|
||||
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.FetchUnchargeSwaps()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(swaps) != 1 {
|
||||
t.Fatal("expected pending swap in store")
|
||||
}
|
||||
|
||||
swap := swaps[0].Contract
|
||||
if !reflect.DeepEqual(swap, &pendingSwap) {
|
||||
t.Fatal("invalid pending swap data")
|
||||
}
|
||||
|
||||
if swaps[0].State() != expectedState {
|
||||
t.Fatalf("expected state %v, but got %v",
|
||||
expectedState, swaps[0].State(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
// 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)
|
||||
|
||||
// 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)
|
||||
|
||||
// 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)
|
||||
|
||||
if err := store.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// 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
loopdb/swapcontract.go
Normal file
41
loopdb/swapcontract.go
Normal 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
|
||||
}
|
||||
114
loopdb/swapstate.go
Normal file
114
loopdb/swapstate.go
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
package loopdb
|
||||
|
||||
// SwapState indicates the current state of a swap.
|
||||
type SwapState uint8
|
||||
|
||||
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 = 0
|
||||
|
||||
// StatePreimageRevealed is reached when the sweep tx publication is
|
||||
// first attempted. From that point on, we should consider the preimage
|
||||
// to no longer be secret and we need to do all we can to get the sweep
|
||||
// confirmed. This state will mostly coalesce with StateHtlcConfirmed,
|
||||
// except in the case where we wait for fees to come down before we
|
||||
// sweep.
|
||||
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 = 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 = 3
|
||||
|
||||
// StateFailTimeout indicates that the on-chain htlc wasn't confirmed before
|
||||
// its expiry or confirmed too late (MinPreimageRevealDelta violated).
|
||||
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 = 5
|
||||
|
||||
// StateFailInsufficientValue indicates that the published on-chain htlc had
|
||||
// a value lower than the requested amount.
|
||||
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 = 7
|
||||
|
||||
// StateHtlcPublished means that the client published the on-chain htlc.
|
||||
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.
|
||||
func (s SwapState) Type() SwapStateType {
|
||||
if s == StateInitiated || s == StateHtlcPublished ||
|
||||
s == StatePreimageRevealed || s == StateFailTemporary {
|
||||
|
||||
return StateTypePending
|
||||
}
|
||||
|
||||
if s == StateSuccess {
|
||||
return StateTypeSuccess
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
317
loopdb/uncharge.go
Normal file
317
loopdb/uncharge.go
Normal file
|
|
@ -0,0 +1,317 @@
|
|||
package loopdb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
)
|
||||
|
||||
// UnchargeContract contains the data that is serialized to persistent storage
|
||||
// for pending swaps.
|
||||
type UnchargeContract struct {
|
||||
SwapContract
|
||||
|
||||
DestAddr btcutil.Address
|
||||
|
||||
SwapInvoice string
|
||||
|
||||
// MaxSwapRoutingFee is the maximum off-chain fee in msat that may be
|
||||
// paid for the swap payment to the server.
|
||||
MaxSwapRoutingFee btcutil.Amount
|
||||
|
||||
// SweepConfTarget specifies the targeted confirmation target for the
|
||||
// client sweep tx.
|
||||
SweepConfTarget int32
|
||||
|
||||
// UnchargeChannel is the channel to uncharge. If zero, any channel may
|
||||
// be used.
|
||||
UnchargeChannel *uint64
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// Time is the time that this swap had its state changed.
|
||||
Time time.Time
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// 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
|
||||
|
||||
// Events are each of the state transitions that this swap underwent.
|
||||
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
|
||||
}
|
||||
|
||||
func deserializeUnchargeContract(value []byte) (*UnchargeContract, error) {
|
||||
r := bytes.NewReader(value)
|
||||
|
||||
contract, err := deserializeContract(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
swap := UnchargeContract{
|
||||
SwapContract: *contract,
|
||||
}
|
||||
|
||||
addr, err := wire.ReadVarString(r, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
swap.DestAddr, err = btcutil.DecodeAddress(addr, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
swap.SwapInvoice, err = wire.ReadVarString(r, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := binary.Read(r, byteOrder, &swap.SweepConfTarget); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := binary.Read(r, byteOrder, &swap.MaxSwapRoutingFee); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var unchargeChannel uint64
|
||||
if err := binary.Read(r, byteOrder, &unchargeChannel); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if unchargeChannel != 0 {
|
||||
swap.UnchargeChannel = &unchargeChannel
|
||||
}
|
||||
|
||||
return &swap, nil
|
||||
}
|
||||
|
||||
func serializeUnchargeContract(swap *UnchargeContract) (
|
||||
[]byte, error) {
|
||||
|
||||
var b bytes.Buffer
|
||||
|
||||
serializeContract(&swap.SwapContract, &b)
|
||||
|
||||
addr := swap.DestAddr.String()
|
||||
if err := wire.WriteVarString(&b, 0, addr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := wire.WriteVarString(&b, 0, swap.SwapInvoice); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := binary.Write(&b, byteOrder, swap.SweepConfTarget); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := binary.Write(&b, byteOrder, swap.MaxSwapRoutingFee); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var unchargeChannel uint64
|
||||
if swap.UnchargeChannel != nil {
|
||||
unchargeChannel = *swap.UnchargeChannel
|
||||
}
|
||||
if err := binary.Write(&b, byteOrder, unchargeChannel); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
func deserializeContract(r io.Reader) (*SwapContract, error) {
|
||||
swap := SwapContract{}
|
||||
var err error
|
||||
var unixNano int64
|
||||
if err := binary.Read(r, byteOrder, &unixNano); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
swap.InitiationTime = time.Unix(0, unixNano)
|
||||
|
||||
if err := binary.Read(r, byteOrder, &swap.Preimage); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
binary.Read(r, byteOrder, &swap.AmountRequested)
|
||||
|
||||
swap.PrepayInvoice, err = wire.ReadVarString(r, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
n, err := r.Read(swap.SenderKey[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if n != keyLength {
|
||||
return nil, fmt.Errorf("sender key has invalid length")
|
||||
}
|
||||
|
||||
n, err = r.Read(swap.ReceiverKey[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if n != keyLength {
|
||||
return nil, fmt.Errorf("receiver key has invalid length")
|
||||
}
|
||||
|
||||
if err := binary.Read(r, byteOrder, &swap.CltvExpiry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := binary.Read(r, byteOrder, &swap.MaxMinerFee); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := binary.Read(r, byteOrder, &swap.MaxSwapFee); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := binary.Read(r, byteOrder, &swap.MaxPrepayRoutingFee); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := binary.Read(r, byteOrder, &swap.InitiationHeight); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &swap, nil
|
||||
}
|
||||
|
||||
func serializeContract(swap *SwapContract, b *bytes.Buffer) error {
|
||||
if err := binary.Write(b, byteOrder, swap.InitiationTime.UnixNano()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := binary.Write(b, byteOrder, swap.Preimage); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := binary.Write(b, byteOrder, swap.AmountRequested); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := wire.WriteVarString(b, 0, swap.PrepayInvoice); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n, err := b.Write(swap.SenderKey[:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n != keyLength {
|
||||
return fmt.Errorf("sender key has invalid length")
|
||||
}
|
||||
|
||||
n, err = b.Write(swap.ReceiverKey[:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n != keyLength {
|
||||
return fmt.Errorf("receiver key has invalid length")
|
||||
}
|
||||
|
||||
if err := binary.Write(b, byteOrder, swap.CltvExpiry); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := binary.Write(b, byteOrder, swap.MaxMinerFee); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := binary.Write(b, byteOrder, swap.MaxSwapFee); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := binary.Write(b, byteOrder, swap.MaxPrepayRoutingFee); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := binary.Write(b, byteOrder, swap.InitiationHeight); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func serializeUnchargeUpdate(time time.Time, state SwapState) (
|
||||
[]byte, error) {
|
||||
|
||||
var b bytes.Buffer
|
||||
|
||||
if err := binary.Write(&b, byteOrder, time.UnixNano()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := binary.Write(&b, byteOrder, state); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
func deserializeUnchargeUpdate(value []byte) (*PersistentUnchargeEvent, error) {
|
||||
update := &PersistentUnchargeEvent{}
|
||||
|
||||
r := bytes.NewReader(value)
|
||||
|
||||
var unixNano int64
|
||||
if err := binary.Read(r, byteOrder, &unixNano); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
update.Time = time.Unix(0, unixNano)
|
||||
|
||||
if err := binary.Read(r, byteOrder, &update.State); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return update, nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue