mirror of https://github.com/lightninglabs/loop
loopdb: make new loopdb package to house persistent storage
parent
74cf29a9cf
commit
cdcb9f8345
@ -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.
|
||||
|
@ -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
|
||||
)
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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?
|
@ -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)
|
||||
}
|
@ -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()
|
||||
}
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue