utxonursery: added persistence to transaction output states
Moved transaction states from in-memory maps to persistent BoltDB buckets. This allows channel force closes to operate reliably if the daemon is shut down and restarted at any point during the forced channel closure process.
This commit is contained in:
parent
90ed23e6aa
commit
aa04f82a15
@ -21,6 +21,11 @@ const (
|
|||||||
notifierType = "btcd"
|
notifierType = "btcd"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrChainNotifierShuttingDown = errors.New("chainntnfs: system interrupt " +
|
||||||
|
"while attempting to register for spend notification.")
|
||||||
|
)
|
||||||
|
|
||||||
// chainUpdate encapsulates an update to the current main chain. This struct is
|
// chainUpdate encapsulates an update to the current main chain. This struct is
|
||||||
// used as an element within an unbounded queue in order to avoid blocking the
|
// used as an element within an unbounded queue in order to avoid blocking the
|
||||||
// main rpc dispatch rule.
|
// main rpc dispatch rule.
|
||||||
@ -549,8 +554,7 @@ func (b *BtcdNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint) (*chainntnfs.S
|
|||||||
|
|
||||||
select {
|
select {
|
||||||
case <-b.quit:
|
case <-b.quit:
|
||||||
return nil, errors.New("chainntnfs: system interrupt while " +
|
return nil, ErrChainNotifierShuttingDown
|
||||||
"attempting to register for spend notification.")
|
|
||||||
case b.notificationRegistry <- ntfn:
|
case b.notificationRegistry <- ntfn:
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -611,8 +615,7 @@ func (b *BtcdNotifier) RegisterConfirmationsNtfn(txid *wire.ShaHash,
|
|||||||
|
|
||||||
select {
|
select {
|
||||||
case <-b.quit:
|
case <-b.quit:
|
||||||
return nil, errors.New("chainntnfs: system interrupt while " +
|
return nil, ErrChainNotifierShuttingDown
|
||||||
"attempting to register for confirmation notification.")
|
|
||||||
case b.notificationRegistry <- ntfn:
|
case b.notificationRegistry <- ntfn:
|
||||||
return &chainntnfs.ConfirmationEvent{
|
return &chainntnfs.ConfirmationEvent{
|
||||||
Confirmed: ntfn.finConf,
|
Confirmed: ntfn.finConf,
|
||||||
|
70
lnd_test.go
70
lnd_test.go
@ -172,7 +172,7 @@ func openChannelAndAssert(t *harnessTest, net *networkHarness, ctx context.Conte
|
|||||||
func closeChannelAndAssert(t *harnessTest, net *networkHarness, ctx context.Context,
|
func closeChannelAndAssert(t *harnessTest, net *networkHarness, ctx context.Context,
|
||||||
node *lightningNode, fundingChanPoint *lnrpc.ChannelPoint, force bool) *wire.ShaHash {
|
node *lightningNode, fundingChanPoint *lnrpc.ChannelPoint, force bool) *wire.ShaHash {
|
||||||
|
|
||||||
closeUpdates, err := net.CloseChannel(ctx, node, fundingChanPoint, force)
|
closeUpdates, _, err := net.CloseChannel(ctx, node, fundingChanPoint, force)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to close channel: %v", err)
|
t.Fatalf("unable to close channel: %v", err)
|
||||||
}
|
}
|
||||||
@ -275,11 +275,13 @@ func testChannelBalance(net *networkHarness, t *harnessTest) {
|
|||||||
// force closes the channel after some cursory assertions. Within the test, two
|
// force closes the channel after some cursory assertions. Within the test, two
|
||||||
// transactions should be broadcast on-chain, the commitment transaction itself
|
// transactions should be broadcast on-chain, the commitment transaction itself
|
||||||
// (which closes the channel), and the sweep transaction a few blocks later
|
// (which closes the channel), and the sweep transaction a few blocks later
|
||||||
// once the output(s) become mature.
|
// once the output(s) become mature. This test also includes several restarts
|
||||||
|
// to ensure that the transaction output states are persisted throughout
|
||||||
|
// the forced closure process.
|
||||||
//
|
//
|
||||||
// TODO(roabeef): also add an unsettled HTLC before force closing.
|
// TODO(roasbeef): also add an unsettled HTLC before force closing.
|
||||||
func testChannelForceClosure(net *networkHarness, t *harnessTest) {
|
func testChannelForceClosure(net *networkHarness, t *harnessTest) {
|
||||||
timeout := time.Duration(time.Second * 5)
|
timeout := time.Duration(time.Second * 10)
|
||||||
ctxb := context.Background()
|
ctxb := context.Background()
|
||||||
|
|
||||||
// First establish a channel ween with a capacity of 100k satoshis
|
// First establish a channel ween with a capacity of 100k satoshis
|
||||||
@ -306,28 +308,63 @@ func testChannelForceClosure(net *networkHarness, t *harnessTest) {
|
|||||||
// the channel. This will also assert that the commitment transaction
|
// the channel. This will also assert that the commitment transaction
|
||||||
// was immediately broadcast in order to fulfill the force closure
|
// was immediately broadcast in order to fulfill the force closure
|
||||||
// request.
|
// request.
|
||||||
closeUpdate, err := net.CloseChannel(ctxb, net.Alice, chanPoint, true)
|
_, closingTxID, err := net.CloseChannel(ctxb, net.Alice, chanPoint, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to execute force channel closure: %v", err)
|
t.Fatalf("unable to execute force channel closure: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The several restarts in this test are intended to ensure that when a
|
||||||
|
// channel is force-closed, the UTXO nursery has persisted the state of
|
||||||
|
// the channel in the closure process and will recover the correct state
|
||||||
|
// when the system comes back on line. This restart tests state
|
||||||
|
// persistence at the beginning of the process, when the commitment
|
||||||
|
// transaction has been broadcast but not yet confirmed in a block.
|
||||||
|
if err := net.RestartNode(net.Alice, nil); err != nil {
|
||||||
|
t.Fatalf("Node restart failed: %v", err)
|
||||||
|
}
|
||||||
// Mine a block which should confirm the commitment transaction
|
// Mine a block which should confirm the commitment transaction
|
||||||
// broadcast as a result of the force closure.
|
// broadcast as a result of the force closure.
|
||||||
if _, err := net.Miner.Node.Generate(1); err != nil {
|
if _, err := net.Miner.Node.Generate(1); err != nil {
|
||||||
t.Fatalf("unable to generate block: %v", err)
|
t.Fatalf("unable to generate block: %v", err)
|
||||||
}
|
}
|
||||||
ctxt, _ = context.WithTimeout(ctxb, timeout)
|
|
||||||
closingTxID, err := net.WaitForChannelClose(ctxt, closeUpdate)
|
// The following sleep provides time for the UTXO nursery to move the
|
||||||
if err != nil {
|
// output from the preschool to the kindergarten database buckets
|
||||||
t.Fatalf("error while waiting for channel close: %v", err)
|
// prior to RestartNode() being triggered. Without this sleep, the
|
||||||
|
// database update may fail, causing the UTXO nursery to retry the move
|
||||||
|
// operation upon restart. This will change the blockheights from what
|
||||||
|
// is expected by the test.
|
||||||
|
// TODO(bvu): refactor out this sleep.
|
||||||
|
duration := time.Millisecond * 300
|
||||||
|
time.Sleep(duration)
|
||||||
|
|
||||||
|
// The following restart is intended to ensure that outputs from the
|
||||||
|
// force close commitment transaction have been persisted once the
|
||||||
|
// transaction has been confirmed, but before the outputs are spendable
|
||||||
|
// (the "kindergarten" bucket.)
|
||||||
|
if err := net.RestartNode(net.Alice, nil); err != nil {
|
||||||
|
t.Fatalf("Node restart failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Currently within the codebase, the default CSV is 4 relative blocks.
|
// Currently within the codebase, the default CSV is 4 relative blocks.
|
||||||
// So generate exactly 4 new blocks.
|
// For the persistence test, we generate three blocks, then trigger
|
||||||
|
// a restart and then generate the final block that should trigger
|
||||||
|
// the creation of the sweep transaction.
|
||||||
// TODO(roasbeef): should check default value in config here instead,
|
// TODO(roasbeef): should check default value in config here instead,
|
||||||
// or make delay a param
|
// or make delay a param
|
||||||
const defaultCSV = 4
|
const defaultCSV = 4
|
||||||
if _, err := net.Miner.Node.Generate(defaultCSV); err != nil {
|
if _, err := net.Miner.Node.Generate(defaultCSV - 1); err != nil {
|
||||||
|
t.Fatalf("unable to mine blocks: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The following restart checks to ensure that outputs in the kindergarten
|
||||||
|
// bucket are persisted while waiting for the required number of
|
||||||
|
// confirmations to be reported.
|
||||||
|
if err := net.RestartNode(net.Alice, nil); err != nil {
|
||||||
|
t.Fatalf("Node restart failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := net.Miner.Node.Generate(1); err != nil {
|
||||||
t.Fatalf("unable to mine blocks: %v", err)
|
t.Fatalf("unable to mine blocks: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -336,20 +373,21 @@ func testChannelForceClosure(net *networkHarness, t *harnessTest) {
|
|||||||
// broadcast.
|
// broadcast.
|
||||||
var sweepingTXID *wire.ShaHash
|
var sweepingTXID *wire.ShaHash
|
||||||
var mempool []*wire.ShaHash
|
var mempool []*wire.ShaHash
|
||||||
|
mempoolTimeout := time.After(3 * time.Second)
|
||||||
|
checkMempoolTick := time.Tick(100 * time.Millisecond)
|
||||||
mempoolPoll:
|
mempoolPoll:
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-time.After(time.Second * 5):
|
case <-mempoolTimeout:
|
||||||
t.Fatalf("sweep tx not found in mempool")
|
t.Fatalf("sweep tx not found in mempool")
|
||||||
default:
|
case <-checkMempoolTick:
|
||||||
mempool, err = net.Miner.Node.GetRawMempool()
|
mempool, err = net.Miner.Node.GetRawMempool()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to fetch node's mempool: %v", err)
|
t.Fatalf("unable to fetch node's mempool: %v", err)
|
||||||
}
|
}
|
||||||
if len(mempool) == 0 {
|
if len(mempool) != 0 {
|
||||||
continue
|
break mempoolPoll
|
||||||
}
|
}
|
||||||
break mempoolPoll
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -734,7 +734,7 @@ func (n *networkHarness) WaitForChannelOpen(ctx context.Context,
|
|||||||
// pending, then an error is returned.
|
// pending, then an error is returned.
|
||||||
func (n *networkHarness) CloseChannel(ctx context.Context,
|
func (n *networkHarness) CloseChannel(ctx context.Context,
|
||||||
lnNode *lightningNode, cp *lnrpc.ChannelPoint,
|
lnNode *lightningNode, cp *lnrpc.ChannelPoint,
|
||||||
force bool) (lnrpc.Lightning_CloseChannelClient, error) {
|
force bool) (lnrpc.Lightning_CloseChannelClient, *wire.ShaHash, error) {
|
||||||
|
|
||||||
closeReq := &lnrpc.CloseChannelRequest{
|
closeReq := &lnrpc.CloseChannelRequest{
|
||||||
ChannelPoint: cp,
|
ChannelPoint: cp,
|
||||||
@ -742,11 +742,11 @@ func (n *networkHarness) CloseChannel(ctx context.Context,
|
|||||||
}
|
}
|
||||||
closeRespStream, err := lnNode.CloseChannel(ctx, closeReq)
|
closeRespStream, err := lnNode.CloseChannel(ctx, closeReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to close channel: %v", err)
|
return nil, nil, fmt.Errorf("unable to close channel: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
errChan := make(chan error)
|
errChan := make(chan error)
|
||||||
fin := make(chan struct{})
|
fin := make(chan *wire.ShaHash)
|
||||||
go func() {
|
go func() {
|
||||||
// Consume the "channel close" update in order to wait for the closing
|
// Consume the "channel close" update in order to wait for the closing
|
||||||
// transaction to be broadcast, then wait for the closing tx to be seen
|
// transaction to be broadcast, then wait for the closing tx to be seen
|
||||||
@ -772,20 +772,19 @@ func (n *networkHarness) CloseChannel(ctx context.Context,
|
|||||||
errChan <- err
|
errChan <- err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
fin <- closeTxid
|
||||||
close(fin)
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Wait until either the deadline for the context expires, an error
|
// Wait until either the deadline for the context expires, an error
|
||||||
// occurs, or the channel close update is received.
|
// occurs, or the channel close update is received.
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return nil, fmt.Errorf("timeout reached before channel close " +
|
return nil, nil, fmt.Errorf("timeout reached before channel close " +
|
||||||
"initiated")
|
"initiated")
|
||||||
case err := <-errChan:
|
case err := <-errChan:
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
case <-fin:
|
case closeTxid := <-fin:
|
||||||
return closeRespStream, nil
|
return closeRespStream, closeTxid, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,7 +108,7 @@ func newServer(listenAddrs []string, notifier chainntnfs.ChainNotifier,
|
|||||||
chanDB: chanDB,
|
chanDB: chanDB,
|
||||||
|
|
||||||
invoices: newInvoiceRegistry(chanDB),
|
invoices: newInvoiceRegistry(chanDB),
|
||||||
utxoNursery: newUtxoNursery(notifier, wallet),
|
utxoNursery: newUtxoNursery(chanDB, notifier, wallet),
|
||||||
htlcSwitch: newHtlcSwitch(),
|
htlcSwitch: newHtlcSwitch(),
|
||||||
|
|
||||||
identityPriv: privKey,
|
identityPriv: privKey,
|
||||||
|
918
utxonursery.go
918
utxonursery.go
@ -1,18 +1,85 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/boltdb/bolt"
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet"
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
|
"github.com/roasbeef/btcd/btcec"
|
||||||
"github.com/roasbeef/btcd/txscript"
|
"github.com/roasbeef/btcd/txscript"
|
||||||
"github.com/roasbeef/btcd/wire"
|
"github.com/roasbeef/btcd/wire"
|
||||||
"github.com/roasbeef/btcutil"
|
"github.com/roasbeef/btcutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// preschoolBucket stores outputs from commitment transactions that
|
||||||
|
// have been broadcast, but not yet confirmed. This set of outputs is
|
||||||
|
// persisted in case the system is shut down between the time when
|
||||||
|
// the commitment has been broadcast and the time the transaction
|
||||||
|
// has been confirmed on the blockchain.
|
||||||
|
preschoolBucket = []byte("psc")
|
||||||
|
|
||||||
|
// kindergartenBucket stores outputs from commitment transactions that
|
||||||
|
// have received an initial confirmation, but which aren't yet spendable
|
||||||
|
// because they require additional confirmations enforced by Check
|
||||||
|
// Sequence Verify. Once required additional confirmations have been reported,
|
||||||
|
// a sweep transaction will be created to move the funds out of these
|
||||||
|
// outputs. After a further six confirmations have been reported, the outputs
|
||||||
|
// will be deleted from this bucket. The purpose of this additional wait
|
||||||
|
// time is to ensure that a block reorganization doesn't result in the
|
||||||
|
// sweep transaction getting re-organized out of the chain.
|
||||||
|
kindergartenBucket = []byte("kdg")
|
||||||
|
|
||||||
|
// lastGraduatedHeightKey is used to persist the last blockheight that
|
||||||
|
// has been checked for graduating outputs. When the nursery is restarted,
|
||||||
|
// lastGraduatedHeightKey is used to determine the point from which it's
|
||||||
|
// necessary to catch up.
|
||||||
|
lastGraduatedHeightKey = []byte("lgh")
|
||||||
|
|
||||||
|
byteOrder = binary.BigEndian
|
||||||
|
)
|
||||||
|
|
||||||
|
// witnessType determines how an output's witness will be generated. The default
|
||||||
|
// commitmentTimeLock type will generate a witness that will allow spending of a
|
||||||
|
// time-locked transaction enforced by CheckSequenceVerify.
|
||||||
|
type witnessType uint16
|
||||||
|
|
||||||
|
const (
|
||||||
|
commitmentTimeLock witnessType = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
// witnessGenerator represents a function which is able to generate the final
|
||||||
|
// witness for a particular public key script. This function acts as an
|
||||||
|
// abstraction layer, hiding the details of the underlying script from the
|
||||||
|
// utxoNursery.
|
||||||
|
type witnessGenerator func(tx *wire.MsgTx, hc *txscript.TxSigHashes, inputIndex int) ([][]byte, error)
|
||||||
|
|
||||||
|
// generateFunc will return the witnessGenerator function that a kidOutput uses to
|
||||||
|
// generate the witness for a sweep transaction. Currently there is only one witnessType
|
||||||
|
// but this will be expanded.
|
||||||
|
func (wt *witnessType) generateFunc(signer *lnwallet.Signer, descriptor *lnwallet.SignDescriptor) witnessGenerator {
|
||||||
|
switch *wt {
|
||||||
|
case commitmentTimeLock:
|
||||||
|
return func(tx *wire.MsgTx, hc *txscript.TxSigHashes, inputIndex int) ([][]byte, error) {
|
||||||
|
desc := descriptor
|
||||||
|
desc.SigHashes = hc
|
||||||
|
desc.InputIndex = inputIndex
|
||||||
|
|
||||||
|
return lnwallet.CommitSpendTimeout(*signer, desc, tx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// utxoNursery is a system dedicated to incubating time-locked outputs created
|
// utxoNursery is a system dedicated to incubating time-locked outputs created
|
||||||
// by the broadcast of a commitment transaction either by us, or the remote
|
// by the broadcast of a commitment transaction either by us, or the remote
|
||||||
// peer. The nursery accepts outputs and "incubates" them until they've reached
|
// peer. The nursery accepts outputs and "incubates" them until they've reached
|
||||||
@ -27,14 +94,10 @@ type utxoNursery struct {
|
|||||||
notifier chainntnfs.ChainNotifier
|
notifier chainntnfs.ChainNotifier
|
||||||
wallet *lnwallet.LightningWallet
|
wallet *lnwallet.LightningWallet
|
||||||
|
|
||||||
db channeldb.DB
|
db *channeldb.DB
|
||||||
|
|
||||||
requests chan *incubationRequest
|
requests chan *incubationRequest
|
||||||
|
|
||||||
// TODO(roasbeef): persist to disk afterwards
|
|
||||||
unstagedOutputs map[wire.OutPoint]*immatureOutput
|
|
||||||
stagedOutputs map[uint32][]*immatureOutput
|
|
||||||
|
|
||||||
started uint32
|
started uint32
|
||||||
stopped uint32
|
stopped uint32
|
||||||
quit chan struct{}
|
quit chan struct{}
|
||||||
@ -43,16 +106,15 @@ type utxoNursery struct {
|
|||||||
|
|
||||||
// newUtxoNursery creates a new instance of the utxoNursery from a
|
// newUtxoNursery creates a new instance of the utxoNursery from a
|
||||||
// ChainNotifier and LightningWallet instance.
|
// ChainNotifier and LightningWallet instance.
|
||||||
func newUtxoNursery(notifier chainntnfs.ChainNotifier,
|
func newUtxoNursery(db *channeldb.DB, notifier chainntnfs.ChainNotifier,
|
||||||
wallet *lnwallet.LightningWallet) *utxoNursery {
|
wallet *lnwallet.LightningWallet) *utxoNursery {
|
||||||
|
|
||||||
return &utxoNursery{
|
return &utxoNursery{
|
||||||
notifier: notifier,
|
notifier: notifier,
|
||||||
wallet: wallet,
|
wallet: wallet,
|
||||||
requests: make(chan *incubationRequest),
|
requests: make(chan *incubationRequest),
|
||||||
unstagedOutputs: make(map[wire.OutPoint]*immatureOutput),
|
db: db,
|
||||||
stagedOutputs: make(map[uint32][]*immatureOutput),
|
quit: make(chan struct{}),
|
||||||
quit: make(chan struct{}),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,13 +127,114 @@ func (u *utxoNursery) Start() error {
|
|||||||
|
|
||||||
utxnLog.Tracef("Starting UTXO nursery")
|
utxnLog.Tracef("Starting UTXO nursery")
|
||||||
|
|
||||||
|
if err := u.reloadPreschool(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register with the notifier to receive notifications for each newly
|
||||||
|
// connected block. We register during startup to ensure that no blocks
|
||||||
|
// are missed while we are handling blocks that were missed during the
|
||||||
|
// time the UTXO nursery was unavailable.
|
||||||
|
newBlockChan, err := u.notifier.RegisterBlockEpochNtfn()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := u.catchUpKindergarten(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
u.wg.Add(1)
|
u.wg.Add(1)
|
||||||
go u.incubator()
|
go u.incubator(newBlockChan)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop gracefully shutsdown any lingering goroutines launched during normal
|
// reloadPreschool re-initializes the chain notifier with all of the outputs
|
||||||
|
// that had been saved to the "preschool" database bucket prior to shutdown.
|
||||||
|
func (u *utxoNursery) reloadPreschool() error {
|
||||||
|
err := u.db.View(func(tx *bolt.Tx) error {
|
||||||
|
psclBucket := tx.Bucket(preschoolBucket)
|
||||||
|
if psclBucket == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := psclBucket.ForEach(func(outputBytes, kidBytes []byte) error {
|
||||||
|
psclOutput, err := deserializeKidOutput(bytes.NewBuffer(kidBytes))
|
||||||
|
|
||||||
|
outpoint := psclOutput.outPoint
|
||||||
|
sourceTxid := outpoint.Hash
|
||||||
|
|
||||||
|
confChan, err := u.notifier.RegisterConfirmationsNtfn(&sourceTxid, 1)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
utxnLog.Infof("Preschool outpoint %v re-registered for confirmation "+
|
||||||
|
"notification.", psclOutput.outPoint)
|
||||||
|
go psclOutput.waitForPromotion(u.db, confChan)
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// catchUpKindergarten handles the graduation of kindergarten outputs from
|
||||||
|
// blocks that were missed while the UTXO Nursery was down or offline.
|
||||||
|
// graduateMissedBlocks is called during the startup of the UTXO Nursery.
|
||||||
|
func (u *utxoNursery) catchUpKindergarten() error {
|
||||||
|
var lastGraduatedHeight uint32
|
||||||
|
|
||||||
|
// Query the database for the most recently processed block
|
||||||
|
err := u.db.View(func(tx *bolt.Tx) error {
|
||||||
|
kgtnBucket := tx.Bucket(kindergartenBucket)
|
||||||
|
if kgtnBucket == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
heightBytes := kgtnBucket.Get(lastGraduatedHeightKey)
|
||||||
|
if heightBytes == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lastGraduatedHeight = byteOrder.Uint32(heightBytes)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the most recently mined block
|
||||||
|
_, bestHeight, err := u.wallet.ChainIO.GetBestBlock()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop through and check for graduating outputs at each of the missed
|
||||||
|
// block heights.
|
||||||
|
if lastGraduatedHeight != 0 {
|
||||||
|
graduationHeight := lastGraduatedHeight + 1
|
||||||
|
|
||||||
|
utxnLog.Infof("Processing outputs from missed blocks. Starting with "+
|
||||||
|
"blockheight: %v, to current blockheight: %v", graduationHeight,
|
||||||
|
bestHeight)
|
||||||
|
|
||||||
|
for graduationHeight <= uint32(bestHeight) {
|
||||||
|
if err := u.graduateKindergarten(graduationHeight); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
graduationHeight = graduationHeight + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop gracefully shuts down any lingering goroutines launched during normal
|
||||||
// operation of the utxoNursery.
|
// operation of the utxoNursery.
|
||||||
func (u *utxoNursery) Stop() error {
|
func (u *utxoNursery) Stop() error {
|
||||||
if !atomic.CompareAndSwapUint32(&u.stopped, 0, 1) {
|
if !atomic.CompareAndSwapUint32(&u.stopped, 0, 1) {
|
||||||
@ -86,87 +249,94 @@ func (u *utxoNursery) Stop() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// incubator is tasked with watching over all immature outputs until they've
|
// kidOutput represents an output that's waiting for a required blockheight
|
||||||
// reached "maturity", after which they'll be swept into the underlying wallet
|
// before its funds will be available to be moved into the user's wallet.
|
||||||
// in batches within a single transaction. Immature outputs can be divided into
|
// The struct includes a witnessGenerator closure which will be used to
|
||||||
// three stages: early stage, mid stage, and final stage. During the early
|
// generate the witness required to sweep the output once it's mature.
|
||||||
// stage, the transaction containing the output has not yet been confirmed.
|
// TODO(roasbeef): make into interface? can't gob functions
|
||||||
// Once the txn creating the output is confirmed, then output moves to the mid
|
type kidOutput struct {
|
||||||
// stage wherein a dedicated goroutine waits until it has reached "maturity".
|
amt btcutil.Amount
|
||||||
// Once an output is mature, it will be swept into the wallet at the earlier
|
outPoint wire.OutPoint
|
||||||
// possible height.
|
|
||||||
func (u *utxoNursery) incubator() {
|
|
||||||
defer u.wg.Done()
|
|
||||||
|
|
||||||
// Register with the notifier to receive notifications for each newly
|
witnessFunc witnessGenerator
|
||||||
// connected block.
|
|
||||||
newBlocks, err := u.notifier.RegisterBlockEpochNtfn()
|
// TODO(roasbeef): using block timeouts everywhere currently, will need
|
||||||
if err != nil {
|
// to modify logic later to account for MTP based timeouts.
|
||||||
utxnLog.Errorf("unable to register for block epoch "+
|
blocksToMaturity uint32
|
||||||
"notifications: %v", err)
|
confHeight uint32
|
||||||
|
|
||||||
|
signDescriptor *lnwallet.SignDescriptor
|
||||||
|
witnessType witnessType
|
||||||
|
}
|
||||||
|
|
||||||
|
// incubationRequest is a request to the utxoNursery to incubate a set of
|
||||||
|
// outputs until their mature, finally sweeping them into the wallet once
|
||||||
|
// available.
|
||||||
|
type incubationRequest struct {
|
||||||
|
outputs []*kidOutput
|
||||||
|
}
|
||||||
|
|
||||||
|
// incubateOutputs sends a request to utxoNursery to incubate the outputs
|
||||||
|
// defined within the summary of a closed channel. Individually, as all outputs
|
||||||
|
// reach maturity they'll be swept back into the wallet.
|
||||||
|
func (u *utxoNursery) incubateOutputs(closeSummary *lnwallet.ForceCloseSummary) {
|
||||||
|
outputAmt := btcutil.Amount(closeSummary.SelfOutputSignDesc.Output.Value)
|
||||||
|
selfOutput := &kidOutput{
|
||||||
|
amt: outputAmt,
|
||||||
|
outPoint: closeSummary.SelfOutpoint,
|
||||||
|
blocksToMaturity: closeSummary.SelfOutputMaturity,
|
||||||
|
signDescriptor: closeSummary.SelfOutputSignDesc,
|
||||||
|
witnessType: commitmentTimeLock,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Outputs that are transitioning from early to mid-stage are sent over
|
u.requests <- &incubationRequest{
|
||||||
// this channel by each output's dedicated watcher goroutine.
|
outputs: []*kidOutput{selfOutput},
|
||||||
midStageOutputs := make(chan *immatureOutput)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// incubator is tasked with watching over all outputs from channel closes as they
|
||||||
|
// transition from being broadcast (at which point they move into the "preschool
|
||||||
|
// state"), then confirmed and waiting for the necessary number of blocks to
|
||||||
|
// be confirmed (as specified as kidOutput.blocksToMaturity and enforced by
|
||||||
|
// CheckSequenceVerify). When the necessary block height has been reached, the
|
||||||
|
// output has "matured" and the waitForGraduation function will generate a
|
||||||
|
// sweep transaction to move funds from the commitment transaction into the
|
||||||
|
// user's wallet.
|
||||||
|
func (u *utxoNursery) incubator(newBlockChan *chainntnfs.BlockEpochEvent) {
|
||||||
|
defer u.wg.Done()
|
||||||
|
|
||||||
out:
|
out:
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case earlyStagers := <-u.requests:
|
case preschoolRequest := <-u.requests:
|
||||||
utxnLog.Infof("Incubating %v new outputs",
|
utxnLog.Infof("Incubating %v new outputs",
|
||||||
len(earlyStagers.outputs))
|
len(preschoolRequest.outputs))
|
||||||
|
|
||||||
for _, immatureUtxo := range earlyStagers.outputs {
|
for _, output := range preschoolRequest.outputs {
|
||||||
outpoint := immatureUtxo.outPoint
|
sourceTxid := output.outPoint.Hash
|
||||||
sourceTXID := outpoint.Hash
|
|
||||||
|
|
||||||
// Register for a confirmation once the
|
if err := output.enterPreschool(u.db); err != nil {
|
||||||
// generating txn has been confirmed.
|
utxnLog.Errorf("unable to add kidOutput to preschool: %v, %v ",
|
||||||
confChan, err := u.notifier.RegisterConfirmationsNtfn(&sourceTXID, 1)
|
output, err)
|
||||||
if err != nil {
|
|
||||||
utxnLog.Errorf("unable to register for confirmations "+
|
|
||||||
"for txid: %v", sourceTXID)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(roasbeef): should be an on-disk
|
// Register for a notification that will trigger graduation from
|
||||||
// at-least-once task queue
|
// preschool to kindergarten when the channel close transaction
|
||||||
u.unstagedOutputs[outpoint] = immatureUtxo
|
// has been confirmed.
|
||||||
|
confChan, err := u.notifier.RegisterConfirmationsNtfn(&sourceTxid, 1)
|
||||||
|
if err != nil {
|
||||||
|
utxnLog.Errorf("unable to register output for confirmation: %v",
|
||||||
|
sourceTxid)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Launch a dedicated goroutine which will send
|
// Launch a dedicated goroutine that will move the output from
|
||||||
// the output back to the incubator once the
|
// the preschool bucket to the kindergarten bucket once the
|
||||||
// source txn has been confirmed.
|
// channel close transaction has been confirmed.
|
||||||
go func() {
|
go output.waitForPromotion(u.db, confChan)
|
||||||
confDetails, ok := <-confChan.Confirmed
|
|
||||||
if !ok {
|
|
||||||
utxnLog.Errorf("notification chan "+
|
|
||||||
"closed, can't advance output %v", outpoint)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
confHeight := uint32(confDetails.BlockHeight)
|
|
||||||
utxnLog.Infof("Outpoint %v confirmed in "+
|
|
||||||
"block %v moving to mid-stage",
|
|
||||||
outpoint, confHeight)
|
|
||||||
immatureUtxo.confHeight = confHeight
|
|
||||||
midStageOutputs <- immatureUtxo
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
// TODO(roasbeef): rename to preschool and kindergarden
|
case epoch, ok := <-newBlockChan.Epochs:
|
||||||
case midUtxo := <-midStageOutputs:
|
|
||||||
// The transaction creating the output has been
|
|
||||||
// created, so we move it from early stage to
|
|
||||||
// mid-stage.
|
|
||||||
delete(u.unstagedOutputs, midUtxo.outPoint)
|
|
||||||
|
|
||||||
// TODO(roasbeef): your off-by-one sense are tingling...
|
|
||||||
maturityHeight := midUtxo.confHeight + midUtxo.blocksToMaturity
|
|
||||||
u.stagedOutputs[maturityHeight] = append(u.stagedOutputs[maturityHeight], midUtxo)
|
|
||||||
|
|
||||||
utxnLog.Infof("Outpoint %v now mid-stage, will mature "+
|
|
||||||
"at height %v (delay of %v)", midUtxo.outPoint,
|
|
||||||
maturityHeight, midUtxo.blocksToMaturity)
|
|
||||||
case epoch, ok := <-newBlocks.Epochs:
|
|
||||||
// If the epoch channel has been closed, then the
|
// If the epoch channel has been closed, then the
|
||||||
// ChainNotifier is exiting which means the daemon is
|
// ChainNotifier is exiting which means the daemon is
|
||||||
// as well. Therefore, we exit early also in order to
|
// as well. Therefore, we exit early also in order to
|
||||||
@ -175,59 +345,244 @@ out:
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// A new block has just been connected, check to see if
|
if err := u.graduateKindergarten(uint32(epoch.Height)); err != nil {
|
||||||
// we have any new outputs that can be swept into the
|
utxnLog.Errorf("error while graduating kindergarten outputs: %v", err)
|
||||||
// wallet.
|
|
||||||
newHeight := uint32(epoch.Height)
|
|
||||||
matureOutputs, ok := u.stagedOutputs[newHeight]
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
utxnLog.Infof("New block: height=%v hash=%v, "+
|
|
||||||
"sweeping %v mature outputs", newHeight,
|
|
||||||
epoch.Hash, len(matureOutputs))
|
|
||||||
|
|
||||||
// Create a transation which sweeps all the newly
|
|
||||||
// mature outputs into a output controlled by the
|
|
||||||
// wallet.
|
|
||||||
// TODO(roasbeef): can be more intelligent about
|
|
||||||
// buffering outputs to be more efficient on-chain.
|
|
||||||
sweepTx, err := u.createSweepTx(matureOutputs)
|
|
||||||
if err != nil {
|
|
||||||
// TODO(roasbeef): retry logic?
|
|
||||||
utxnLog.Errorf("unable to create sweep tx: %v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
utxnLog.Infof("Sweeping %v time-locked outputs "+
|
|
||||||
"with sweep tx: %v", len(matureOutputs),
|
|
||||||
newLogClosure(func() string {
|
|
||||||
return spew.Sdump(sweepTx)
|
|
||||||
}))
|
|
||||||
|
|
||||||
// With the sweep transaction fully signed, broadcast
|
|
||||||
// the transaction to the network. Additionally, we can
|
|
||||||
// stop tracking these outputs as they've just been
|
|
||||||
// sweeped.
|
|
||||||
err = u.wallet.PublishTransaction(sweepTx)
|
|
||||||
if err != nil {
|
|
||||||
utxnLog.Errorf("unable to broadcast sweep tx: %v, %v",
|
|
||||||
err, spew.Sdump(sweepTx))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
delete(u.stagedOutputs, newHeight)
|
|
||||||
case <-u.quit:
|
case <-u.quit:
|
||||||
break out
|
break out
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// enterPreschool is the first stage in the process of transferring funds from
|
||||||
|
// a force closed channel into the user's wallet. When an output is in the
|
||||||
|
// "preschool" stage, the daemon is waiting for the initial confirmation of the
|
||||||
|
// commitment transaction.
|
||||||
|
func (k *kidOutput) enterPreschool(db *channeldb.DB) error {
|
||||||
|
err := db.Update(func(tx *bolt.Tx) error {
|
||||||
|
psclBucket, err := tx.CreateBucketIfNotExists(preschoolBucket)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var outpointBytes bytes.Buffer
|
||||||
|
if err := writeOutpoint(&outpointBytes, &k.outPoint); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var kidBytes bytes.Buffer
|
||||||
|
if err := serializeKidOutput(&kidBytes, k); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := psclBucket.Put(outpointBytes.Bytes(), kidBytes.Bytes()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
utxnLog.Infof("Outpoint %v now in preschool, waiting for "+
|
||||||
|
"initial confirmation", k.outPoint)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// waitForPromotion is intended to be run as a goroutine that will wait until
|
||||||
|
// a channel force close commitment transaction has been included in a
|
||||||
|
// confirmed block. Once the transaction has been confirmed (as reported by
|
||||||
|
// the Chain Notifier), waitForPromotion will delete the output from the
|
||||||
|
// "preschool" database bucket and atomically add it to the "kindergarten"
|
||||||
|
// database bucket. This is the second step in the output incubation process.
|
||||||
|
func (k *kidOutput) waitForPromotion(db *channeldb.DB, confChan *chainntnfs.ConfirmationEvent) {
|
||||||
|
txConfirmation, ok := <-confChan.Confirmed
|
||||||
|
if !ok {
|
||||||
|
utxnLog.Errorf("notification chan "+
|
||||||
|
"closed, can't advance output %v", k.outPoint)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utxnLog.Infof("Outpoint %v confirmed in block %v moving to kindergarten",
|
||||||
|
k.outPoint, txConfirmation.BlockHeight)
|
||||||
|
|
||||||
|
k.confHeight = uint32(txConfirmation.BlockHeight)
|
||||||
|
|
||||||
|
// The following block deletes a kidOutput from the preschool database bucket
|
||||||
|
// and adds it to the kindergarten database bucket which is keyed by block
|
||||||
|
// height. Keys and values are serialized into byte array form prior to
|
||||||
|
// database insertion.
|
||||||
|
err := db.Update(func(tx *bolt.Tx) error {
|
||||||
|
psclBucket := tx.Bucket(preschoolBucket)
|
||||||
|
if psclBucket == nil {
|
||||||
|
return errors.New("unable to open preschool bucket")
|
||||||
|
}
|
||||||
|
|
||||||
|
var outpointBytes bytes.Buffer
|
||||||
|
if err := writeOutpoint(&outpointBytes, &k.outPoint); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := psclBucket.Delete(outpointBytes.Bytes()); err != nil {
|
||||||
|
utxnLog.Errorf("unable to delete kindergarten output from "+
|
||||||
|
"preschool bucket: %v", k.outPoint)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
kgtnBucket, err := tx.CreateBucketIfNotExists(kindergartenBucket)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
maturityHeight := k.confHeight +
|
||||||
|
k.blocksToMaturity
|
||||||
|
heightBytes := make([]byte, 4)
|
||||||
|
byteOrder.PutUint32(heightBytes, uint32(maturityHeight))
|
||||||
|
|
||||||
|
var existingOutputs []byte
|
||||||
|
if results := kgtnBucket.Get(heightBytes); results != nil {
|
||||||
|
existingOutputs = results
|
||||||
|
}
|
||||||
|
|
||||||
|
b := bytes.NewBuffer(existingOutputs)
|
||||||
|
if err := serializeKidOutput(b, k); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := kgtnBucket.Put(heightBytes, b.Bytes()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
utxnLog.Infof("Outpoint %v now in kindergarten, will mature "+
|
||||||
|
"at height %v (delay of %v)", k.outPoint,
|
||||||
|
maturityHeight, k.blocksToMaturity)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
utxnLog.Errorf("unable to move kid output from preschool bucket "+
|
||||||
|
"to kindergarten bucket: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// graduateKindergarten handles the steps involed with moving funds
|
||||||
|
// from a force close commitment transaction into a user's wallet after the output
|
||||||
|
// from the commitment transaction has become spendable. graduateKindergarten
|
||||||
|
// is called both when a new block notification has been received and also
|
||||||
|
// at startup in order to process graduations from blocks missed while the
|
||||||
|
// UTXO nursery was offline.
|
||||||
|
func (u *utxoNursery) graduateKindergarten(blockHeight uint32) error {
|
||||||
|
kgtnOutputs, err := fetchGraduatingOutputs(u.db, u.wallet, blockHeight)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(kgtnOutputs) > 0 {
|
||||||
|
if err := sweepGraduatingOutputs(u.wallet, kgtnOutputs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteHeight := blockHeight - 6
|
||||||
|
if err := deleteGraduatedOutputs(u.db, deleteHeight); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := putLastHeightGraduated(u.db, blockHeight); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchGraduatingOutputs checks the "kindergarten" database bucket whenever a
|
||||||
|
// new block is received in order to determine if commitment transaction
|
||||||
|
// outputs have become newly spendable. If fetchGraduatingOutputs finds
|
||||||
|
// outputs that are ready for "graduation," it passes them on to be swept.
|
||||||
|
// This is the third step in the output incubation process.
|
||||||
|
func fetchGraduatingOutputs(db *channeldb.DB, wallet *lnwallet.LightningWallet, blockHeight uint32) ([]*kidOutput, error) {
|
||||||
|
var results []byte
|
||||||
|
|
||||||
|
err := db.View(func(tx *bolt.Tx) error {
|
||||||
|
// A new block has just been connected, check to see if
|
||||||
|
// we have any new outputs that can be swept into the
|
||||||
|
// wallet.
|
||||||
|
kgtnBucket := tx.Bucket(kindergartenBucket)
|
||||||
|
if kgtnBucket == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
heightBytes := make([]byte, 4)
|
||||||
|
byteOrder.PutUint32(heightBytes, blockHeight)
|
||||||
|
|
||||||
|
results = kgtnBucket.Get(heightBytes)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(results) > 0 {
|
||||||
|
kgtnOutputs, err := deserializeKidList(bytes.NewBuffer(results))
|
||||||
|
if err != nil {
|
||||||
|
utxnLog.Errorf("error while deserializing list of kidOutputs: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, kgtnOutput := range kgtnOutputs {
|
||||||
|
kgtnOutput.witnessFunc =
|
||||||
|
kgtnOutput.witnessType.generateFunc(&wallet.Signer, kgtnOutput.signDescriptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
utxnLog.Infof("New block: height=%v, sweeping %v mature outputs",
|
||||||
|
blockHeight, len(kgtnOutputs))
|
||||||
|
|
||||||
|
return kgtnOutputs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sweepGraduatingOutputs generates and broadcasts the transaction that
|
||||||
|
// transfers control of funds from a channel commitment transaction to the
|
||||||
|
// user's wallet.
|
||||||
|
func sweepGraduatingOutputs(wallet *lnwallet.LightningWallet, kgtnOutputs []*kidOutput) error {
|
||||||
|
// Create a transation which sweeps all the newly
|
||||||
|
// mature outputs into a output controlled by the
|
||||||
|
// wallet.
|
||||||
|
// TODO(roasbeef): can be more intelligent about
|
||||||
|
// buffering outputs to be more efficient on-chain.
|
||||||
|
sweepTx, err := createSweepTx(wallet, kgtnOutputs)
|
||||||
|
if err != nil {
|
||||||
|
// TODO(roasbeef): retry logic?
|
||||||
|
utxnLog.Errorf("unable to create sweep tx: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
utxnLog.Infof("Sweeping %v time-locked outputs "+
|
||||||
|
"with sweep tx: %v", len(kgtnOutputs),
|
||||||
|
newLogClosure(func() string {
|
||||||
|
return spew.Sdump(sweepTx)
|
||||||
|
}))
|
||||||
|
|
||||||
|
// With the sweep transaction fully signed, broadcast
|
||||||
|
// the transaction to the network. Additionally, we can
|
||||||
|
// stop tracking these outputs as they've just been
|
||||||
|
// sweeped.
|
||||||
|
err = wallet.PublishTransaction(sweepTx)
|
||||||
|
if err != nil {
|
||||||
|
utxnLog.Errorf("unable to broadcast sweep tx: %v, %v",
|
||||||
|
err, spew.Sdump(sweepTx))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// createSweepTx creates a final sweeping transaction with all witnesses in
|
// createSweepTx creates a final sweeping transaction with all witnesses in
|
||||||
// place for all inputs. The created transaction has a single output sending
|
// place for all inputs. The created transaction has a single output sending
|
||||||
// all the funds back to the source wallet.
|
// all the funds back to the source wallet.
|
||||||
func (u *utxoNursery) createSweepTx(matureOutputs []*immatureOutput) (*wire.MsgTx, error) {
|
func createSweepTx(wallet *lnwallet.LightningWallet, matureOutputs []*kidOutput) (*wire.MsgTx, error) {
|
||||||
pkScript, err := newSweepPkScript(u.wallet)
|
pkScript, err := newSweepPkScript(wallet)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -265,65 +620,72 @@ func (u *utxoNursery) createSweepTx(matureOutputs []*immatureOutput) (*wire.MsgT
|
|||||||
|
|
||||||
txIn.Witness = witness
|
txIn.Witness = witness
|
||||||
}
|
}
|
||||||
|
|
||||||
return sweepTx, nil
|
return sweepTx, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// witnessGenerator represents a function which is able to generate the final
|
// deleteGraduatedOutputs removes outputs from the kindergarten database bucket
|
||||||
// witness for a particular public key script. This function acts as an
|
// when six blockchain confirmations have passed since the outputs were swept.
|
||||||
// abstraction layer, hiding the details of the underlying script from the
|
// We wait for six confirmations to ensure that the outputs will be swept if a
|
||||||
// utxoNursery.
|
// chain reorganization occurs. This is the final step in the output incubation
|
||||||
type witnessGenerator func(tx *wire.MsgTx, hc *txscript.TxSigHashes, inputIndex int) ([][]byte, error)
|
// process.
|
||||||
|
func deleteGraduatedOutputs(db *channeldb.DB, deleteHeight uint32) error {
|
||||||
|
err := db.Update(func(tx *bolt.Tx) error {
|
||||||
|
kgtnBucket := tx.Bucket(kindergartenBucket)
|
||||||
|
if kgtnBucket == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// immatureOutput encapsulates an immature output. The struct includes a
|
heightBytes := make([]byte, 4)
|
||||||
// witnessGenerator closure which will be used to generate the witness required
|
byteOrder.PutUint32(heightBytes, uint32(deleteHeight))
|
||||||
// to sweep the output once it's mature.
|
results := kgtnBucket.Get(heightBytes)
|
||||||
// TODO(roasbeef): make into interface? can't gob functions
|
if results == nil {
|
||||||
type immatureOutput struct {
|
return nil
|
||||||
amt btcutil.Amount
|
}
|
||||||
outPoint wire.OutPoint
|
|
||||||
|
|
||||||
witnessFunc witnessGenerator
|
sweptOutputs, err := deserializeKidList(bytes.NewBuffer(results))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// TODO(roasbeef): using block timeouts everywhere currently, will need
|
if err := kgtnBucket.Delete(heightBytes); err != nil {
|
||||||
// to modify logic later to account for MTP based timeouts.
|
return err
|
||||||
blocksToMaturity uint32
|
}
|
||||||
confHeight uint32
|
|
||||||
|
utxnLog.Info("Deleting %v swept outputs from kindergarten bucket "+
|
||||||
|
"at block height: %v", len(sweptOutputs), deleteHeight)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// incubationRequest is a request to the utxoNursery to incubate a set of
|
// putLastHeightGraduated persists the most recently processed blockheight
|
||||||
// outputs until their mature, finally sweeping them into the wallet once
|
// to the database. This blockheight is used during restarts to determine if
|
||||||
// available.
|
// blocks were missed while the UTXO Nursery was offline.
|
||||||
type incubationRequest struct {
|
func putLastHeightGraduated(db *channeldb.DB, blockheight uint32) error {
|
||||||
outputs []*immatureOutput
|
err := db.Update(func(tx *bolt.Tx) error {
|
||||||
}
|
kgtnBucket, err := tx.CreateBucketIfNotExists(kindergartenBucket)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// incubateOutputs sends a request to utxoNursery to incubate the outputs
|
heightBytes := make([]byte, 4)
|
||||||
// defined within the summary of a closed channel. Individually, as all outputs
|
byteOrder.PutUint32(heightBytes, blockheight)
|
||||||
// reach maturity they'll be swept back into the wallet.
|
if err := kgtnBucket.Put(lastGraduatedHeightKey, heightBytes); err != nil {
|
||||||
func (u *utxoNursery) incubateOutputs(closeSummary *lnwallet.ForceCloseSummary) {
|
return err
|
||||||
// TODO(roasbeef): should use factory func here based on an interface
|
}
|
||||||
// * interface type stored on disk next to record
|
|
||||||
// * spend here also assumes delay is blocked bsaed, and in range
|
|
||||||
witnessFunc := func(tx *wire.MsgTx, hc *txscript.TxSigHashes, inputIndex int) ([][]byte, error) {
|
|
||||||
desc := closeSummary.SelfOutputSignDesc
|
|
||||||
desc.SigHashes = hc
|
|
||||||
desc.InputIndex = inputIndex
|
|
||||||
|
|
||||||
return lnwallet.CommitSpendTimeout(u.wallet.Signer, desc, tx)
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
outputAmt := btcutil.Amount(closeSummary.SelfOutputSignDesc.Output.Value)
|
return nil
|
||||||
selfOutput := &immatureOutput{
|
|
||||||
amt: outputAmt,
|
|
||||||
outPoint: closeSummary.SelfOutpoint,
|
|
||||||
witnessFunc: witnessFunc,
|
|
||||||
blocksToMaturity: closeSummary.SelfOutputMaturity,
|
|
||||||
}
|
|
||||||
|
|
||||||
u.requests <- &incubationRequest{
|
|
||||||
outputs: []*immatureOutput{selfOutput},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// newSweepPkScript creates a new public key script which should be used to
|
// newSweepPkScript creates a new public key script which should be used to
|
||||||
@ -338,3 +700,219 @@ func newSweepPkScript(wallet lnwallet.WalletController) ([]byte, error) {
|
|||||||
|
|
||||||
return txscript.PayToAddrScript(sweepAddr)
|
return txscript.PayToAddrScript(sweepAddr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// deserializedKidList takes a sequence of serialized kid outputs and returns a
|
||||||
|
// slice of kidOutput structs.
|
||||||
|
func deserializeKidList(r io.Reader) ([]*kidOutput, error) {
|
||||||
|
var kidOutputs []*kidOutput
|
||||||
|
|
||||||
|
for {
|
||||||
|
kidOutput, err := deserializeKidOutput(r)
|
||||||
|
if err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
kidOutputs = append(kidOutputs, kidOutput)
|
||||||
|
}
|
||||||
|
|
||||||
|
return kidOutputs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// serializeKidOutput converts a KidOutput struct into a form
|
||||||
|
// suitable for on-disk database storage. Note that the signDescriptor
|
||||||
|
// struct field is included so that the output's witness can be generated
|
||||||
|
// by createSweepTx() when the output becomes spendable.
|
||||||
|
func serializeKidOutput(w io.Writer, kid *kidOutput) error {
|
||||||
|
var scratch [8]byte
|
||||||
|
byteOrder.PutUint64(scratch[:], uint64(kid.amt))
|
||||||
|
if _, err := w.Write(scratch[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writeOutpoint(w, &kid.outPoint); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
byteOrder.PutUint32(scratch[:4], kid.blocksToMaturity)
|
||||||
|
if _, err := w.Write(scratch[:4]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
byteOrder.PutUint32(scratch[:4], kid.confHeight)
|
||||||
|
if _, err := w.Write(scratch[:4]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
byteOrder.PutUint16(scratch[:2], uint16(kid.witnessType))
|
||||||
|
if _, err := w.Write(scratch[:2]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
serializedPubKey := kid.signDescriptor.PubKey.SerializeCompressed()
|
||||||
|
if err := wire.WriteVarBytes(w, 0, serializedPubKey); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := wire.WriteVarBytes(w, 0, kid.signDescriptor.PrivateTweak); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := wire.WriteVarBytes(w, 0, kid.signDescriptor.WitnessScript); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writeTxOut(w, kid.signDescriptor.Output); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
byteOrder.PutUint32(scratch[:4], uint32(kid.signDescriptor.HashType))
|
||||||
|
if _, err := w.Write(scratch[:4]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// deserializeKidOutput takes a byte array representation of a kidOutput
|
||||||
|
// and converts it to an struct. Note that the witnessFunc method isn't added
|
||||||
|
// during deserialization and must be added later based on the value of the
|
||||||
|
// witnessType field.
|
||||||
|
func deserializeKidOutput(r io.Reader) (*kidOutput, error) {
|
||||||
|
scratch := make([]byte, 8)
|
||||||
|
|
||||||
|
kid := &kidOutput{}
|
||||||
|
|
||||||
|
if _, err := r.Read(scratch[:]); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
kid.amt = btcutil.Amount(byteOrder.Uint64(scratch[:]))
|
||||||
|
|
||||||
|
if err := readOutpoint(io.LimitReader(r, 40), &kid.outPoint); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := r.Read(scratch[:4]); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
kid.blocksToMaturity = byteOrder.Uint32(scratch[:4])
|
||||||
|
|
||||||
|
if _, err := r.Read(scratch[:4]); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
kid.confHeight = byteOrder.Uint32(scratch[:4])
|
||||||
|
|
||||||
|
if _, err := r.Read(scratch[:2]); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
kid.witnessType = witnessType(byteOrder.Uint16(scratch[:2]))
|
||||||
|
|
||||||
|
kid.signDescriptor = &lnwallet.SignDescriptor{}
|
||||||
|
|
||||||
|
descKeyBytes, err := wire.ReadVarBytes(r, 0, 34, "descKeyBytes")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
descKey, err := btcec.ParsePubKey(descKeyBytes, btcec.S256())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
kid.signDescriptor.PubKey = descKey
|
||||||
|
|
||||||
|
descPrivateTweak, err := wire.ReadVarBytes(r, 0, 32, "privateTweak")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
kid.signDescriptor.PrivateTweak = descPrivateTweak
|
||||||
|
|
||||||
|
descWitnessScript, err := wire.ReadVarBytes(r, 0, 100, "witnessScript")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
kid.signDescriptor.WitnessScript = descWitnessScript
|
||||||
|
|
||||||
|
descTxOut := &wire.TxOut{}
|
||||||
|
if err := readTxOut(r, descTxOut); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
kid.signDescriptor.Output = descTxOut
|
||||||
|
|
||||||
|
if _, err := r.Read(scratch[:4]); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
kid.signDescriptor.HashType = txscript.SigHashType(byteOrder.Uint32(scratch[:4]))
|
||||||
|
|
||||||
|
return kid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(bvu): copied from channeldb, remove repetition
|
||||||
|
func writeOutpoint(w io.Writer, o *wire.OutPoint) error {
|
||||||
|
// TODO(roasbeef): make all scratch buffers on the stack
|
||||||
|
scratch := make([]byte, 4)
|
||||||
|
|
||||||
|
// TODO(roasbeef): write raw 32 bytes instead of wasting the extra
|
||||||
|
// byte.
|
||||||
|
if err := wire.WriteVarBytes(w, 0, o.Hash[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
byteOrder.PutUint32(scratch, o.Index)
|
||||||
|
if _, err := w.Write(scratch); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(bvu): copied from channeldb, remove repetition
|
||||||
|
func readOutpoint(r io.Reader, o *wire.OutPoint) error {
|
||||||
|
scratch := make([]byte, 4)
|
||||||
|
|
||||||
|
txid, err := wire.ReadVarBytes(r, 0, 32, "prevout")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
copy(o.Hash[:], txid)
|
||||||
|
|
||||||
|
if _, err := r.Read(scratch); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
o.Index = byteOrder.Uint32(scratch)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeTxOut(w io.Writer, txo *wire.TxOut) error {
|
||||||
|
scratch := make([]byte, 8)
|
||||||
|
|
||||||
|
byteOrder.PutUint64(scratch, uint64(txo.Value))
|
||||||
|
if _, err := w.Write(scratch); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := wire.WriteVarBytes(w, 0, txo.PkScript); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readTxOut(r io.Reader, txo *wire.TxOut) error {
|
||||||
|
scratch := make([]byte, 8)
|
||||||
|
|
||||||
|
if _, err := r.Read(scratch); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
txo.Value = int64(byteOrder.Uint64(scratch))
|
||||||
|
|
||||||
|
pkScript, err := wire.ReadVarBytes(r, 0, 80, "pkScript")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
txo.PkScript = pkScript
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
244
utxonursery_test.go
Normal file
244
utxonursery_test.go
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
|
"github.com/roasbeef/btcd/btcec"
|
||||||
|
"github.com/roasbeef/btcd/txscript"
|
||||||
|
"github.com/roasbeef/btcd/wire"
|
||||||
|
"github.com/roasbeef/btcutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
outPoints = []wire.OutPoint{
|
||||||
|
wire.OutPoint{
|
||||||
|
Hash: [wire.HashSize]byte{
|
||||||
|
0x51, 0xb6, 0x37, 0xd8, 0xfc, 0xd2, 0xc6, 0xda,
|
||||||
|
0x48, 0x59, 0xe6, 0x96, 0x31, 0x13, 0xa1, 0x17,
|
||||||
|
0x2d, 0xe7, 0x93, 0xe4, 0xb7, 0x25, 0xb8, 0x4d,
|
||||||
|
0x1f, 0xb, 0x4c, 0xf9, 0x9e, 0xc5, 0x8c, 0xe9,
|
||||||
|
},
|
||||||
|
Index: 9,
|
||||||
|
},
|
||||||
|
wire.OutPoint{
|
||||||
|
Hash: [wire.HashSize]byte{
|
||||||
|
0xb7, 0x94, 0x38, 0x5f, 0x2d, 0x1e, 0xf7, 0xab,
|
||||||
|
0x4d, 0x92, 0x73, 0xd1, 0x90, 0x63, 0x81, 0xb4,
|
||||||
|
0x4f, 0x2f, 0x6f, 0x25, 0x88, 0xa3, 0xef, 0xb9,
|
||||||
|
0x6a, 0x49, 0x18, 0x83, 0x31, 0x98, 0x47, 0x53,
|
||||||
|
},
|
||||||
|
Index: 49,
|
||||||
|
},
|
||||||
|
wire.OutPoint{
|
||||||
|
Hash: [wire.HashSize]byte{
|
||||||
|
0x81, 0xb6, 0x37, 0xd8, 0xfc, 0xd2, 0xc6, 0xda,
|
||||||
|
0x63, 0x59, 0xe6, 0x96, 0x31, 0x13, 0xa1, 0x17,
|
||||||
|
0xd, 0xe7, 0x95, 0xe4, 0xb7, 0x25, 0xb8, 0x4d,
|
||||||
|
0x1e, 0xb, 0x4c, 0xfd, 0x9e, 0xc5, 0x8c, 0xe9,
|
||||||
|
},
|
||||||
|
Index: 23,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
keys = [][]byte{
|
||||||
|
[]byte{0x04, 0x11, 0xdb, 0x93, 0xe1, 0xdc, 0xdb, 0x8a,
|
||||||
|
0x01, 0x6b, 0x49, 0x84, 0x0f, 0x8c, 0x53, 0xbc, 0x1e,
|
||||||
|
0xb6, 0x8a, 0x38, 0x2e, 0x97, 0xb1, 0x48, 0x2e, 0xca,
|
||||||
|
0xd7, 0xb1, 0x48, 0xa6, 0x90, 0x9a, 0x5c, 0xb2, 0xe0,
|
||||||
|
0xea, 0xdd, 0xfb, 0x84, 0xcc, 0xf9, 0x74, 0x44, 0x64,
|
||||||
|
0xf8, 0x2e, 0x16, 0x0b, 0xfa, 0x9b, 0x8b, 0x64, 0xf9,
|
||||||
|
0xd4, 0xc0, 0x3f, 0x99, 0x9b, 0x86, 0x43, 0xf6, 0x56,
|
||||||
|
0xb4, 0x12, 0xa3,
|
||||||
|
},
|
||||||
|
[]byte{0x07, 0x11, 0xdb, 0x93, 0xe1, 0xdc, 0xdb, 0x8a,
|
||||||
|
0x01, 0x6b, 0x49, 0x84, 0x0f, 0x8c, 0x53, 0xbc, 0x1e,
|
||||||
|
0xb6, 0x8a, 0x38, 0x2e, 0x97, 0xb1, 0x48, 0x2e, 0xca,
|
||||||
|
0xd7, 0xb1, 0x48, 0xa6, 0x90, 0x9a, 0x5c, 0xb2, 0xe0,
|
||||||
|
0xea, 0xdd, 0xfb, 0x84, 0xcc, 0xf9, 0x74, 0x44, 0x64,
|
||||||
|
0xf8, 0x2e, 0x16, 0x0b, 0xfa, 0x9b, 0x8b, 0x64, 0xf9,
|
||||||
|
0xd4, 0xc0, 0x3f, 0x99, 0x9b, 0x86, 0x43, 0xf6, 0x56,
|
||||||
|
0xb4, 0x12, 0xa3,
|
||||||
|
},
|
||||||
|
[]byte{0x02, 0xce, 0x0b, 0x14, 0xfb, 0x84, 0x2b, 0x1b,
|
||||||
|
0xa5, 0x49, 0xfd, 0xd6, 0x75, 0xc9, 0x80, 0x75, 0xf1,
|
||||||
|
0x2e, 0x9c, 0x51, 0x0f, 0x8e, 0xf5, 0x2b, 0xd0, 0x21,
|
||||||
|
0xa9, 0xa1, 0xf4, 0x80, 0x9d, 0x3b, 0x4d,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
signDescriptors = []lnwallet.SignDescriptor{
|
||||||
|
lnwallet.SignDescriptor{
|
||||||
|
PrivateTweak: []byte{
|
||||||
|
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
|
||||||
|
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
|
||||||
|
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
|
||||||
|
0x02, 0x02, 0x02, 0x02, 0x02,
|
||||||
|
},
|
||||||
|
WitnessScript: []byte{
|
||||||
|
0x00, 0x14, 0xee, 0x91, 0x41, 0x7e, 0x85, 0x6c, 0xde,
|
||||||
|
0x10, 0xa2, 0x91, 0x1e, 0xdc, 0xbd, 0xbd, 0x69, 0xe2,
|
||||||
|
0xef, 0xb5, 0x71, 0x48,
|
||||||
|
},
|
||||||
|
Output: &wire.TxOut{
|
||||||
|
Value: 5000000000,
|
||||||
|
PkScript: []byte{
|
||||||
|
0x41, // OP_DATA_65
|
||||||
|
0x04, 0xd6, 0x4b, 0xdf, 0xd0, 0x9e, 0xb1, 0xc5,
|
||||||
|
0xfe, 0x29, 0x5a, 0xbd, 0xeb, 0x1d, 0xca, 0x42,
|
||||||
|
0x81, 0xbe, 0x98, 0x8e, 0x2d, 0xa0, 0xb6, 0xc1,
|
||||||
|
0xc6, 0xa5, 0x9d, 0xc2, 0x26, 0xc2, 0x86, 0x24,
|
||||||
|
0xe1, 0x81, 0x75, 0xe8, 0x51, 0xc9, 0x6b, 0x97,
|
||||||
|
0x3d, 0x81, 0xb0, 0x1c, 0xc3, 0x1f, 0x04, 0x78,
|
||||||
|
0x34, 0xbc, 0x06, 0xd6, 0xd6, 0xed, 0xf6, 0x20,
|
||||||
|
0xd1, 0x84, 0x24, 0x1a, 0x6a, 0xed, 0x8b, 0x63,
|
||||||
|
0xa6, // 65-byte signature
|
||||||
|
0xac, // OP_CHECKSIG
|
||||||
|
},
|
||||||
|
},
|
||||||
|
HashType: txscript.SigHashAll,
|
||||||
|
},
|
||||||
|
lnwallet.SignDescriptor{
|
||||||
|
PrivateTweak: []byte{
|
||||||
|
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
|
||||||
|
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
|
||||||
|
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
|
||||||
|
0x02, 0x02, 0x02, 0x02, 0x02,
|
||||||
|
},
|
||||||
|
WitnessScript: []byte{
|
||||||
|
0x00, 0x14, 0xee, 0x91, 0x41, 0x7e, 0x85, 0x6c, 0xde,
|
||||||
|
0x10, 0xa2, 0x91, 0x1e, 0xdc, 0xbd, 0xbd, 0x69, 0xe2,
|
||||||
|
0xef, 0xb5, 0x71, 0x48,
|
||||||
|
},
|
||||||
|
Output: &wire.TxOut{
|
||||||
|
Value: 5000000000,
|
||||||
|
PkScript: []byte{
|
||||||
|
0x41, // OP_DATA_65
|
||||||
|
0x04, 0xd6, 0x4b, 0xdf, 0xd0, 0x9e, 0xb1, 0xc5,
|
||||||
|
0xfe, 0x29, 0x5a, 0xbd, 0xeb, 0x1d, 0xca, 0x42,
|
||||||
|
0x81, 0xbe, 0x98, 0x8e, 0x2d, 0xa0, 0xb6, 0xc1,
|
||||||
|
0xc6, 0xa5, 0x9d, 0xc2, 0x26, 0xc2, 0x86, 0x24,
|
||||||
|
0xe1, 0x81, 0x75, 0xe8, 0x51, 0xc9, 0x6b, 0x97,
|
||||||
|
0x3d, 0x81, 0xb0, 0x1c, 0xc3, 0x1f, 0x04, 0x78,
|
||||||
|
0x34, 0xbc, 0x06, 0xd6, 0xd6, 0xed, 0xf6, 0x20,
|
||||||
|
0xd1, 0x84, 0x24, 0x1a, 0x6a, 0xed, 0x8b, 0x63,
|
||||||
|
0xa6, // 65-byte signature
|
||||||
|
0xac, // OP_CHECKSIG
|
||||||
|
},
|
||||||
|
},
|
||||||
|
HashType: txscript.SigHashAll,
|
||||||
|
},
|
||||||
|
lnwallet.SignDescriptor{
|
||||||
|
PrivateTweak: []byte{
|
||||||
|
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
|
||||||
|
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
|
||||||
|
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
|
||||||
|
0x02, 0x02, 0x02, 0x02, 0x02,
|
||||||
|
},
|
||||||
|
WitnessScript: []byte{
|
||||||
|
0x00, 0x14, 0xee, 0x91, 0x41, 0x7e, 0x85, 0x6c, 0xde,
|
||||||
|
0x10, 0xa2, 0x91, 0x1e, 0xdc, 0xbd, 0xbd, 0x69, 0xe2,
|
||||||
|
0xef, 0xb5, 0x71, 0x48,
|
||||||
|
},
|
||||||
|
Output: &wire.TxOut{
|
||||||
|
Value: 5000000000,
|
||||||
|
PkScript: []byte{
|
||||||
|
0x41, // OP_DATA_65
|
||||||
|
0x04, 0xd6, 0x4b, 0xdf, 0xd0, 0x9e, 0xb1, 0xc5,
|
||||||
|
0xfe, 0x29, 0x5a, 0xbd, 0xeb, 0x1d, 0xca, 0x42,
|
||||||
|
0x81, 0xbe, 0x98, 0x8e, 0x2d, 0xa0, 0xb6, 0xc1,
|
||||||
|
0xc6, 0xa5, 0x9d, 0xc2, 0x26, 0xc2, 0x86, 0x24,
|
||||||
|
0xe1, 0x81, 0x75, 0xe8, 0x51, 0xc9, 0x6b, 0x97,
|
||||||
|
0x3d, 0x81, 0xb0, 0x1c, 0xc3, 0x1f, 0x04, 0x78,
|
||||||
|
0x34, 0xbc, 0x06, 0xd6, 0xd6, 0xed, 0xf6, 0x20,
|
||||||
|
0xd1, 0x84, 0x24, 0x1a, 0x6a, 0xed, 0x8b, 0x63,
|
||||||
|
0xa6, // 65-byte signature
|
||||||
|
0xac, // OP_CHECKSIG
|
||||||
|
},
|
||||||
|
},
|
||||||
|
HashType: txscript.SigHashAll,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
kidOutputs = []kidOutput{
|
||||||
|
kidOutput{
|
||||||
|
amt: btcutil.Amount(13e7),
|
||||||
|
outPoint: outPoints[0],
|
||||||
|
blocksToMaturity: uint32(100),
|
||||||
|
confHeight: uint32(1770001),
|
||||||
|
},
|
||||||
|
|
||||||
|
kidOutput{
|
||||||
|
amt: btcutil.Amount(24e7),
|
||||||
|
outPoint: outPoints[1],
|
||||||
|
blocksToMaturity: uint32(50),
|
||||||
|
confHeight: uint32(22342321),
|
||||||
|
},
|
||||||
|
|
||||||
|
kidOutput{
|
||||||
|
amt: btcutil.Amount(2e5),
|
||||||
|
outPoint: outPoints[2],
|
||||||
|
blocksToMaturity: uint32(12),
|
||||||
|
confHeight: uint32(34241),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAddSerializedKidsToList(t *testing.T) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
kid := &kidOutputs[i]
|
||||||
|
descriptor := &signDescriptors[i]
|
||||||
|
pk, err := btcec.ParsePubKey(keys[i], btcec.S256())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to parse pub key: %v", keys[i])
|
||||||
|
}
|
||||||
|
descriptor.PubKey = pk
|
||||||
|
kid.signDescriptor = descriptor
|
||||||
|
|
||||||
|
if err := serializeKidOutput(&b, &kidOutputs[i]); err != nil {
|
||||||
|
t.Fatalf("unable to serialize and add kid output to list: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kidList, err := deserializeKidList(&b)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to deserialize kid output list: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
if !reflect.DeepEqual(&kidOutputs[i], kidList[i]) {
|
||||||
|
t.Fatalf("kidOutputs don't match \n%+v\n%+v", &kidOutputs[i], kidList[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSerializeKidOutput(t *testing.T) {
|
||||||
|
kid := &kidOutputs[0]
|
||||||
|
descriptor := &signDescriptors[0]
|
||||||
|
pk, err := btcec.ParsePubKey(keys[0], btcec.S256())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to parse pub key: %v", keys[0])
|
||||||
|
}
|
||||||
|
descriptor.PubKey = pk
|
||||||
|
kid.signDescriptor = descriptor
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
|
||||||
|
if err := serializeKidOutput(&b, kid); err != nil {
|
||||||
|
t.Fatalf("unable to serialize kid output: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
deserializedKid, err := deserializeKidOutput(&b)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(kid, deserializedKid) {
|
||||||
|
t.Fatalf("kidOutputs don't match %+v vs %+v", kid, deserializedKid)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user