diff --git a/.gitignore b/.gitignore index d7f19c41c4..290cf5682c 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,8 @@ _testmain.go /lnd-debug /lncli /lncli-debug +/lnd-itest +/lncli-itest # Integration test log files output*.log diff --git a/Makefile b/Makefile index 96218891c9..b0e4ec307e 100644 --- a/Makefile +++ b/Makefile @@ -100,6 +100,11 @@ build: $(GOBUILD) -tags="$(DEV_TAGS)" -o lnd-debug $(LDFLAGS) $(PKG) $(GOBUILD) -tags="$(DEV_TAGS)" -o lncli-debug $(LDFLAGS) $(PKG)/cmd/lncli +build-itest: + @$(call print, "Building itest lnd and lncli.") + $(GOBUILD) -tags="$(ITEST_TAGS)" -o lnd-itest $(LDFLAGS) $(PKG) + $(GOBUILD) -tags="$(ITEST_TAGS)" -o lncli-itest $(LDFLAGS) $(PKG)/cmd/lncli + install: @$(call print, "Installing lnd and lncli.") $(GOINSTALL) -tags="${tags}" $(LDFLAGS) $(PKG) @@ -118,7 +123,7 @@ itest-only: @$(call print, "Running integration tests.") $(ITEST) -itest: btcd build itest-only +itest: btcd build-itest itest-only unit: btcd @$(call print, "Running unit tests.") @@ -181,6 +186,7 @@ rpc: clean: @$(call print, "Cleaning source.$(NC)") $(RM) ./lnd-debug ./lncli-debug + $(RM) ./lnd-itest ./lncli-itest $(RM) -r ./vendor .vendor-new diff --git a/breacharbiter.go b/breacharbiter.go index c7786791a5..45c4bafc1d 100644 --- a/breacharbiter.go +++ b/breacharbiter.go @@ -757,6 +757,7 @@ type breachedOutput struct { outpoint wire.OutPoint witnessType lnwallet.WitnessType signDesc lnwallet.SignDescriptor + confHeight uint32 secondLevelWitnessScript []byte @@ -768,7 +769,8 @@ type breachedOutput struct { func makeBreachedOutput(outpoint *wire.OutPoint, witnessType lnwallet.WitnessType, secondLevelScript []byte, - signDescriptor *lnwallet.SignDescriptor) breachedOutput { + signDescriptor *lnwallet.SignDescriptor, + confHeight uint32) breachedOutput { amount := signDescriptor.Output.Value @@ -778,6 +780,7 @@ func makeBreachedOutput(outpoint *wire.OutPoint, secondLevelWitnessScript: secondLevelScript, witnessType: witnessType, signDesc: *signDescriptor, + confHeight: confHeight, } } @@ -831,6 +834,12 @@ func (bo *breachedOutput) BlocksToMaturity() uint32 { return 0 } +// HeightHint returns the minimum height at which a confirmed spending tx can +// occur. +func (bo *breachedOutput) HeightHint() uint32 { + return bo.confHeight +} + // Add compile-time constraint ensuring breachedOutput implements the Input // interface. var _ sweep.Input = (*breachedOutput)(nil) @@ -878,7 +887,8 @@ func newRetributionInfo(chanPoint *wire.OutPoint, // No second level script as this is a commitment // output. nil, - breachInfo.LocalOutputSignDesc) + breachInfo.LocalOutputSignDesc, + breachInfo.BreachHeight) breachedOutputs = append(breachedOutputs, localOutput) } @@ -895,7 +905,8 @@ func newRetributionInfo(chanPoint *wire.OutPoint, // No second level script as this is a commitment // output. nil, - breachInfo.RemoteOutputSignDesc) + breachInfo.RemoteOutputSignDesc, + breachInfo.BreachHeight) breachedOutputs = append(breachedOutputs, remoteOutput) } @@ -919,7 +930,8 @@ func newRetributionInfo(chanPoint *wire.OutPoint, &breachInfo.HtlcRetributions[i].OutPoint, htlcWitnessType, breachInfo.HtlcRetributions[i].SecondLevelWitnessScript, - &breachInfo.HtlcRetributions[i].SignDesc) + &breachInfo.HtlcRetributions[i].SignDesc, + breachInfo.BreachHeight) breachedOutputs = append(breachedOutputs, htlcOutput) } diff --git a/breacharbiter_test.go b/breacharbiter_test.go index 65a5bc5b40..d0d2cc3daa 100644 --- a/breacharbiter_test.go +++ b/breacharbiter_test.go @@ -1336,7 +1336,7 @@ func createTestArbiter(t *testing.T, contractBreaches chan *ContractBreachEvent, ba := newBreachArbiter(&BreachConfig{ CloseLink: func(_ *wire.OutPoint, _ htlcswitch.ChannelCloseType) {}, DB: db, - Estimator: &lnwallet.StaticFeeEstimator{FeePerKW: 12500}, + Estimator: lnwallet.NewStaticFeeEstimator(12500, 0), GenSweepScript: func() ([]byte, error) { return nil, nil }, ContractBreaches: contractBreaches, Signer: signer, @@ -1476,7 +1476,7 @@ func createInitChannels(revocationWindow int) (*lnwallet.LightningChannel, *lnwa return nil, nil, nil, err } - estimator := &lnwallet.StaticFeeEstimator{FeePerKW: 12500} + estimator := lnwallet.NewStaticFeeEstimator(12500, 0) feePerKw, err := estimator.EstimateFeePerKW(1) if err != nil { return nil, nil, nil, err diff --git a/chainregistry.go b/chainregistry.go index cce2696efd..56137b73f4 100644 --- a/chainregistry.go +++ b/chainregistry.go @@ -150,9 +150,9 @@ func newChainControlFromConfig(cfg *config, chanDB *channeldb.DB, FeeRate: cfg.Bitcoin.FeeRate, TimeLockDelta: cfg.Bitcoin.TimeLockDelta, } - cc.feeEstimator = lnwallet.StaticFeeEstimator{ - FeePerKW: defaultBitcoinStaticFeePerKW, - } + cc.feeEstimator = lnwallet.NewStaticFeeEstimator( + defaultBitcoinStaticFeePerKW, 0, + ) case litecoinChain: cc.routingPolicy = htlcswitch.ForwardingPolicy{ MinHTLC: cfg.Litecoin.MinHTLC, @@ -160,9 +160,9 @@ func newChainControlFromConfig(cfg *config, chanDB *channeldb.DB, FeeRate: cfg.Litecoin.FeeRate, TimeLockDelta: cfg.Litecoin.TimeLockDelta, } - cc.feeEstimator = lnwallet.StaticFeeEstimator{ - FeePerKW: defaultLitecoinStaticFeePerKW, - } + cc.feeEstimator = lnwallet.NewStaticFeeEstimator( + defaultLitecoinStaticFeePerKW, 0, + ) default: return nil, nil, fmt.Errorf("Default routing policy for "+ "chain %v is unknown", registeredChains.PrimaryChain()) diff --git a/contractcourt/contract_resolvers.go b/contractcourt/contract_resolvers.go index 71ea72dc13..6d4609da40 100644 --- a/contractcourt/contract_resolvers.go +++ b/contractcourt/contract_resolvers.go @@ -462,6 +462,7 @@ func (h *htlcSuccessResolver) Resolve() (ContractResolver, error) { &h.htlcResolution.ClaimOutpoint, &h.htlcResolution.SweepSignDesc, h.htlcResolution.Preimage[:], + h.broadcastHeight, ) // With the input created, we can now generate the full @@ -471,6 +472,8 @@ func (h *htlcSuccessResolver) Resolve() (ContractResolver, error) { // TODO: Set tx lock time to current block height // instead of zero. Will be taken care of once sweeper // implementation is complete. + // + // TODO: Use time-based sweeper and result chan. var err error h.sweepTx, err = h.Sweeper.CreateSweepTx( []sweep.Input{&input}, sweepConfTarget, 0, @@ -1254,6 +1257,7 @@ func (c *commitSweepResolver) Resolve() (ContractResolver, error) { &c.commitResolution.SelfOutPoint, lnwallet.CommitmentNoDelay, &c.commitResolution.SelfOutputSignDesc, + c.broadcastHeight, ) // With out input constructed, we'll now request that the @@ -1263,6 +1267,8 @@ func (c *commitSweepResolver) Resolve() (ContractResolver, error) { // TODO: Set tx lock time to current block height instead of // zero. Will be taken care of once sweeper implementation is // complete. + // + // TODO: Use time-based sweeper and result chan. c.sweepTx, err = c.Sweeper.CreateSweepTx( []sweep.Input{&input}, sweepConfTarget, 0, ) diff --git a/fundingmanager_test.go b/fundingmanager_test.go index c182d01508..84886ea9a3 100644 --- a/fundingmanager_test.go +++ b/fundingmanager_test.go @@ -231,7 +231,7 @@ func createTestFundingManager(t *testing.T, privKey *btcec.PrivateKey, addr *lnwire.NetAddress, tempTestDir string) (*testNode, error) { netParams := activeNetParams.Params - estimator := lnwallet.StaticFeeEstimator{FeePerKW: 62500} + estimator := lnwallet.NewStaticFeeEstimator(62500, 0) chainNotifier := &mockNotifier{ oneConfChannel: make(chan *chainntnfs.TxConfirmation, 1), @@ -250,7 +250,9 @@ func createTestFundingManager(t *testing.T, privKey *btcec.PrivateKey, signer := &mockSigner{ key: alicePrivKey, } - bio := &mockChainIO{} + bio := &mockChainIO{ + bestHeight: fundingBroadcastHeight, + } dbDir := filepath.Join(tempTestDir, "cdb") cdb, err := channeldb.Open(dbDir) diff --git a/htlcswitch/link_test.go b/htlcswitch/link_test.go index 517196db5f..5fbaa765ad 100644 --- a/htlcswitch/link_test.go +++ b/htlcswitch/link_test.go @@ -1795,7 +1795,7 @@ func TestChannelLinkBandwidthConsistency(t *testing.T) { coreLink.cfg.HodlMask = hodl.MaskFromFlags(hodl.ExitSettle) coreLink.cfg.DebugHTLC = true - estimator := &lnwallet.StaticFeeEstimator{FeePerKW: 6000} + estimator := lnwallet.NewStaticFeeEstimator(6000, 0) feePerKw, err := estimator.EstimateFeePerKW(1) if err != nil { t.Fatalf("unable to query fee estimator: %v", err) @@ -2206,7 +2206,7 @@ func TestChannelLinkBandwidthConsistencyOverflow(t *testing.T) { aliceMsgs = coreLink.cfg.Peer.(*mockPeer).sentMsgs ) - estimator := &lnwallet.StaticFeeEstimator{FeePerKW: 6000} + estimator := lnwallet.NewStaticFeeEstimator(6000, 0) feePerKw, err := estimator.EstimateFeePerKW(1) if err != nil { t.Fatalf("unable to query fee estimator: %v", err) @@ -2453,7 +2453,7 @@ func TestChannelLinkTrimCircuitsPending(t *testing.T) { // Compute the static fees that will be used to determine the // correctness of Alice's bandwidth when forwarding HTLCs. - estimator := &lnwallet.StaticFeeEstimator{FeePerKW: 6000} + estimator := lnwallet.NewStaticFeeEstimator(6000, 0) feePerKw, err := estimator.EstimateFeePerKW(1) if err != nil { t.Fatalf("unable to query fee estimator: %v", err) @@ -2731,7 +2731,7 @@ func TestChannelLinkTrimCircuitsNoCommit(t *testing.T) { // Compute the static fees that will be used to determine the // correctness of Alice's bandwidth when forwarding HTLCs. - estimator := &lnwallet.StaticFeeEstimator{FeePerKW: 6000} + estimator := lnwallet.NewStaticFeeEstimator(6000, 0) feePerKw, err := estimator.EstimateFeePerKW(1) if err != nil { t.Fatalf("unable to query fee estimator: %v", err) @@ -2989,7 +2989,7 @@ func TestChannelLinkBandwidthChanReserve(t *testing.T) { aliceMsgs = coreLink.cfg.Peer.(*mockPeer).sentMsgs ) - estimator := &lnwallet.StaticFeeEstimator{FeePerKW: 6000} + estimator := lnwallet.NewStaticFeeEstimator(6000, 0) feePerKw, err := estimator.EstimateFeePerKW(1) if err != nil { t.Fatalf("unable to query fee estimator: %v", err) diff --git a/htlcswitch/mock.go b/htlcswitch/mock.go index 58ccc8b18e..bb260156a6 100644 --- a/htlcswitch/mock.go +++ b/htlcswitch/mock.go @@ -75,6 +75,10 @@ func (m *mockFeeEstimator) EstimateFeePerKW( } } +func (m *mockFeeEstimator) RelayFeePerKW() lnwallet.SatPerKWeight { + return 1e3 +} + func (m *mockFeeEstimator) Start() error { return nil } diff --git a/htlcswitch/test_utils.go b/htlcswitch/test_utils.go index c29619f5b2..ab15aa8b86 100644 --- a/htlcswitch/test_utils.go +++ b/htlcswitch/test_utils.go @@ -273,7 +273,7 @@ func createTestChannel(alicePrivKey, bobPrivKey []byte, return nil, nil, nil, nil, err } - estimator := &lnwallet.StaticFeeEstimator{FeePerKW: 6000} + estimator := lnwallet.NewStaticFeeEstimator(6000, 0) feePerKw, err := estimator.EstimateFeePerKW(1) if err != nil { return nil, nil, nil, nil, err diff --git a/lntest/node.go b/lntest/node.go index 811c3a3836..d518dd957f 100644 --- a/lntest/node.go +++ b/lntest/node.go @@ -281,7 +281,7 @@ func (hn *HarnessNode) start(lndError chan<- error) error { args := hn.cfg.genArgs() args = append(args, fmt.Sprintf("--profile=%d", 9000+hn.NodeID)) - hn.cmd = exec.Command("./lnd-debug", args...) + hn.cmd = exec.Command("./lnd-itest", args...) // Redirect stderr output to buffer var errb bytes.Buffer diff --git a/lnwallet/channel_test.go b/lnwallet/channel_test.go index e3cd3e15c0..5eba3bc45c 100644 --- a/lnwallet/channel_test.go +++ b/lnwallet/channel_test.go @@ -1131,7 +1131,7 @@ func TestHTLCSigNumber(t *testing.T) { } // Calculate two values that will be below and above Bob's dust limit. - estimator := &StaticFeeEstimator{FeePerKW: 6000} + estimator := NewStaticFeeEstimator(6000, 0) feePerKw, err := estimator.EstimateFeePerKW(1) if err != nil { t.Fatalf("unable to get fee: %v", err) diff --git a/lnwallet/fee_estimator.go b/lnwallet/fee_estimator.go index 1f63a19ff6..76acc63a5d 100644 --- a/lnwallet/fee_estimator.go +++ b/lnwallet/fee_estimator.go @@ -59,22 +59,50 @@ type FeeEstimator interface { // Stop stops any spawned goroutines and cleans up the resources used // by the fee estimator. Stop() error + + // RelayFeePerKW returns the minimum fee rate required for transactions + // to be relayed. This is also the basis for calculation of the dust + // limit. + RelayFeePerKW() SatPerKWeight } // StaticFeeEstimator will return a static value for all fee calculation // requests. It is designed to be replaced by a proper fee calculation -// implementation. +// implementation. The fees are not accessible directly, because changing them +// would not be thread safe. type StaticFeeEstimator struct { - // FeePerKW is the static fee rate in satoshis-per-vbyte that will be + // feePerKW is the static fee rate in satoshis-per-vbyte that will be // returned by this fee estimator. - FeePerKW SatPerKWeight + feePerKW SatPerKWeight + + // relayFee is the minimum fee rate required for transactions to be + // relayed. + relayFee SatPerKWeight +} + +// NewStaticFeeEstimator returns a new static fee estimator instance. +func NewStaticFeeEstimator(feePerKW, + relayFee SatPerKWeight) *StaticFeeEstimator { + + return &StaticFeeEstimator{ + feePerKW: feePerKW, + relayFee: relayFee, + } } // EstimateFeePerKW will return a static value for fee calculations. // // NOTE: This method is part of the FeeEstimator interface. func (e StaticFeeEstimator) EstimateFeePerKW(numBlocks uint32) (SatPerKWeight, error) { - return e.FeePerKW, nil + return e.feePerKW, nil +} + +// RelayFeePerKW returns the minimum fee rate required for transactions to be +// relayed. +// +// NOTE: This method is part of the FeeEstimator interface. +func (e StaticFeeEstimator) RelayFeePerKW() SatPerKWeight { + return e.relayFee } // Start signals the FeeEstimator to start any processes or goroutines @@ -206,6 +234,14 @@ func (b *BtcdFeeEstimator) EstimateFeePerKW(numBlocks uint32) (SatPerKWeight, er return feeEstimate, nil } +// RelayFeePerKW returns the minimum fee rate required for transactions to be +// relayed. +// +// NOTE: This method is part of the FeeEstimator interface. +func (b *BtcdFeeEstimator) RelayFeePerKW() SatPerKWeight { + return b.minFeePerKW +} + // fetchEstimate returns a fee estimate for a transaction to be confirmed in // confTarget blocks. The estimate is returned in sat/kw. func (b *BtcdFeeEstimator) fetchEstimate(confTarget uint32) (SatPerKWeight, error) { @@ -359,6 +395,14 @@ func (b *BitcoindFeeEstimator) EstimateFeePerKW(numBlocks uint32) (SatPerKWeight return feeEstimate, nil } +// RelayFeePerKW returns the minimum fee rate required for transactions to be +// relayed. +// +// NOTE: This method is part of the FeeEstimator interface. +func (b *BitcoindFeeEstimator) RelayFeePerKW() SatPerKWeight { + return b.minFeePerKW +} + // fetchEstimate returns a fee estimate for a transaction to be confirmed in // confTarget blocks. The estimate is returned in sat/kw. func (b *BitcoindFeeEstimator) fetchEstimate(confTarget uint32) (SatPerKWeight, error) { diff --git a/lnwallet/fee_estimator_test.go b/lnwallet/fee_estimator_test.go index d4608b9b28..77d60916cf 100644 --- a/lnwallet/fee_estimator_test.go +++ b/lnwallet/fee_estimator_test.go @@ -74,9 +74,7 @@ func TestStaticFeeEstimator(t *testing.T) { const feePerKw = lnwallet.FeePerKwFloor - feeEstimator := &lnwallet.StaticFeeEstimator{ - FeePerKW: feePerKw, - } + feeEstimator := lnwallet.NewStaticFeeEstimator(feePerKw, 0) if err := feeEstimator.Start(); err != nil { t.Fatalf("unable to start fee estimator: %v", err) } diff --git a/lnwallet/interface_test.go b/lnwallet/interface_test.go index 23fc296b9a..c95b82a00c 100644 --- a/lnwallet/interface_test.go +++ b/lnwallet/interface_test.go @@ -368,7 +368,7 @@ func createTestWallet(tempTestDir string, miningNode *rpctest.Harness, WalletController: wc, Signer: signer, ChainIO: bio, - FeeEstimator: lnwallet.StaticFeeEstimator{FeePerKW: 2500}, + FeeEstimator: lnwallet.NewStaticFeeEstimator(2500, 0), DefaultConstraints: channeldb.ChannelConstraints{ DustLimit: 500, MaxPendingAmount: lnwire.NewMSatFromSatoshis(btcutil.SatoshiPerBitcoin) * 100, @@ -2440,7 +2440,7 @@ func runTests(t *testing.T, walletDriver *lnwallet.WalletDriver, } case "neutrino": - feeEstimator = lnwallet.StaticFeeEstimator{FeePerKW: 62500} + feeEstimator = lnwallet.NewStaticFeeEstimator(62500, 0) // Set some package-level variable to speed up // operation for tests. diff --git a/lnwallet/test_utils.go b/lnwallet/test_utils.go index 60e98d3bf0..db2cbb1089 100644 --- a/lnwallet/test_utils.go +++ b/lnwallet/test_utils.go @@ -229,7 +229,7 @@ func CreateTestChannels() (*LightningChannel, *LightningChannel, func(), error) return nil, nil, nil, err } - estimator := &StaticFeeEstimator{FeePerKW: 6000} + estimator := NewStaticFeeEstimator(6000, 0) feePerKw, err := estimator.EstimateFeePerKW(1) if err != nil { return nil, nil, nil, err diff --git a/lnwallet/witnessgen.go b/lnwallet/witnessgen.go index 708df7176e..00c3fab094 100644 --- a/lnwallet/witnessgen.go +++ b/lnwallet/witnessgen.go @@ -71,6 +71,44 @@ const ( HtlcSecondLevelRevoke WitnessType = 9 ) +// Stirng returns a human readable version of the target WitnessType. +func (wt WitnessType) String() string { + switch wt { + case CommitmentTimeLock: + return "CommitmentTimeLock" + + case CommitmentNoDelay: + return "CommitmentNoDelay" + + case CommitmentRevoke: + return "CommitmentRevoke" + + case HtlcOfferedRevoke: + return "HtlcOfferedRevoke" + + case HtlcAcceptedRevoke: + return "HtlcAcceptedRevoke" + + case HtlcOfferedTimeoutSecondLevel: + return "HtlcOfferedTimeoutSecondLevel" + + case HtlcAcceptedSuccessSecondLevel: + return "HtlcAcceptedSuccessSecondLevel" + + case HtlcOfferedRemoteTimeout: + return "HtlcOfferedRemoteTimeout" + + case HtlcAcceptedRemoteSuccess: + return "HtlcAcceptedRemoteSuccess" + + case HtlcSecondLevelRevoke: + return "HtlcSecondLevelRevoke" + + default: + return fmt.Sprintf("Unknown WitnessType: %v", uint32(wt)) + } +} + // 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. diff --git a/mock.go b/mock.go index 34276beed5..b323dcc716 100644 --- a/mock.go +++ b/mock.go @@ -168,10 +168,12 @@ func (m *mockSpendNotifier) Spend(outpoint *wire.OutPoint, height int32, } } -type mockChainIO struct{} +type mockChainIO struct { + bestHeight int32 +} -func (*mockChainIO) GetBestBlock() (*chainhash.Hash, int32, error) { - return activeNetParams.GenesisHash, fundingBroadcastHeight, nil +func (m *mockChainIO) GetBestBlock() (*chainhash.Hash, int32, error) { + return activeNetParams.GenesisHash, m.bestHeight, nil } func (*mockChainIO) GetUtxo(op *wire.OutPoint, _ []byte, diff --git a/nursery_store.go b/nursery_store.go index 95ce5847e7..eec8e74a99 100644 --- a/nursery_store.go +++ b/nursery_store.go @@ -23,21 +23,6 @@ import ( // // utxn/ // | -// | LAST PURGED + FINALIZED HEIGHTS -// | -// | Each nursery store tracks a "last graduated height", which records the -// | most recent block height for which the nursery store has successfully -// | graduated all outputs. It also tracks a "last finalized height", which -// | records the last block height that the nursery attempted to graduate -// | If a finalized height has kindergarten outputs, the sweep txn for these -// | outputs will be stored in the height bucket. This ensure that the same -// | txid will be used after restarts. Otherwise, the nursery will be unable -// | to recover the txid of kindergarten sweep transaction it has already -// | broadcast. -// | -// ├── last-finalized-height-key: -// ├── last-graduated-height-key: -// | // | CHANNEL INDEX // | // | The channel index contains a directory for each channel that has a @@ -72,10 +57,7 @@ import ( // | relative file path: // | e.g. // // | that can be queried in the channel index to retrieve the serialized -// | output. If a height bucket is less than or equal to the current last -// | finalized height and has a non-zero number of kindergarten outputs, a -// | height bucket will also contain the finalized kindergarten sweep txn -// | under the "finalized-kndr-txn" key. +// | output. // | // └── height-index-key/ //    ├── / <- HEIGHT BUCKET @@ -84,12 +66,15 @@ import ( // | |    └── : "" // |   ├── / // | |    └── : "" -//    | └── finalized-kndr-txn: "" | //    └── / //    └── / //    └── : "" //    └── : "" +// TODO(joostjager): Add database migration to clean up now unused last +// graduated height and finalized txes. This also prevents people downgrading +// and surprising the downgraded nursery with missing data. + // NurseryStore abstracts the persistent storage layer for the utxo nursery. // Concretely, it stores commitment and htlc outputs until any time-bounded // constraints have fully matured. The store exposes methods for enumerating its @@ -110,47 +95,30 @@ type NurseryStore interface { CribToKinder(*babyOutput) error // PreschoolToKinder atomically moves a kidOutput from the preschool - // bucket to the kindergarten bucket. This transition should be - // executed after receiving confirmation of the preschool output. - // Incoming HTLC's we need to go to the second-layer to claim, and also - // our commitment outputs fall into this class. - PreschoolToKinder(*kidOutput) error - - // GraduateKinder atomically moves the kindergarten class at the - // provided height into the graduated status. This involves removing the - // kindergarten entries from both the height and channel indexes, and - // cleaning up the finalized kindergarten sweep txn. The height bucket - // will be opportunistically pruned from the height index as outputs are + // bucket to the kindergarten bucket. This transition should be executed + // after receiving confirmation of the preschool output. Incoming HTLC's + // we need to go to the second-layer to claim, and also our commitment + // outputs fall into this class. + // + // An additional parameter specifies the last graduated height. This is + // used in case of late registration. It schedules the output for sweep + // at the next epoch even though it has already expired earlier. + PreschoolToKinder(kid *kidOutput, lastGradHeight uint32) error + + // GraduateKinder atomically moves an output at the provided height into + // the graduated status. This involves removing the kindergarten entries + // from both the height and channel indexes. The height bucket will be + // opportunistically pruned from the height index as outputs are // removed. - GraduateKinder(height uint32) error + GraduateKinder(height uint32, output *kidOutput) error // FetchPreschools returns a list of all outputs currently stored in // the preschool bucket. FetchPreschools() ([]kidOutput, error) // FetchClass returns a list of kindergarten and crib outputs whose - // timelocks expire at the given height. If the kindergarten class at - // this height hash been finalized previously, via FinalizeKinder, it - // will also returns the finalized kindergarten sweep txn. - FetchClass(height uint32) (*wire.MsgTx, []kidOutput, []babyOutput, error) - - // FinalizeKinder accepts a block height and the kindergarten sweep txn - // computed for this height. Upon startup, we will rebroadcast any - // finalized kindergarten txns instead of signing a new txn, as this - // result in a different txid from a preceding broadcast. - FinalizeKinder(height uint32, tx *wire.MsgTx) error - - // LastFinalizedHeight returns the last block height for which the - // nursery store finalized a kindergarten class. - LastFinalizedHeight() (uint32, error) - - // GraduateHeight records the provided height as the last height for - // which the nursery store successfully graduated all outputs. - GraduateHeight(height uint32) error - - // LastGraduatedHeight returns the last block height for which the - // nursery store successfully graduated all outputs. - LastGraduatedHeight() (uint32, error) + // timelocks expire at the given height. + FetchClass(height uint32) ([]kidOutput, []babyOutput, error) // HeightsBelowOrEqual returns the lowest non-empty heights in the // height index, that exist at or below the provided upper bound. @@ -181,14 +149,6 @@ var ( // the root-level, chain-segmented bucket for each nursery store. utxnChainPrefix = []byte("utxn") - // lastFinalizedHeightKey is a static key used to locate nursery store's - // last finalized height. - lastFinalizedHeightKey = []byte("last-finalized-height") - - // lastGraduatedHeightKey is a static key used to retrieve the height of - // the last bucket that successfully graduated all outputs. - lastGraduatedHeightKey = []byte("last-graduated-height") - // channelIndexKey is a static key used to lookup the bucket containing // all of the nursery's active channels. channelIndexKey = []byte("channel-index") @@ -197,10 +157,6 @@ var ( // containing all heights for which the nursery will need to take // action. heightIndexKey = []byte("height-index") - - // finalizedKndrTxnKey is a static key that can be used to locate a - // finalized kindergarten sweep txn. - finalizedKndrTxnKey = []byte("finalized-kndr-txn") ) // Defines the state prefixes that will be used to persistently track an @@ -415,7 +371,9 @@ func (ns *nurseryStore) CribToKinder(bby *babyOutput) error { // PreschoolToKinder atomically moves a kidOutput from the preschool bucket to // the kindergarten bucket. This transition should be executed after receiving // confirmation of the preschool output's commitment transaction. -func (ns *nurseryStore) PreschoolToKinder(kid *kidOutput) error { +func (ns *nurseryStore) PreschoolToKinder(kid *kidOutput, + lastGradHeight uint32) error { + return ns.db.Update(func(tx *bbolt.Tx) error { // Create or retrieve the channel bucket corresponding to the // kid output's origin channel point. @@ -478,13 +436,6 @@ func (ns *nurseryStore) PreschoolToKinder(kid *kidOutput) error { maturityHeight = kid.ConfHeight() + kid.BlocksToMaturity() } - // In the case of a Late Registration, we've already graduated - // the class that this kid is destined for. So we'll bump its - // height by one to ensure we don't forget to graduate it. - lastGradHeight, err := ns.getLastGraduatedHeight(tx) - if err != nil { - return err - } if maturityHeight <= lastGradHeight { utxnLog.Debugf("Late Registration for kid output=%v "+ "detected: class_height=%v, "+ @@ -515,108 +466,66 @@ func (ns *nurseryStore) PreschoolToKinder(kid *kidOutput) error { }) } -// GraduateKinder atomically moves the kindergarten class at the provided height -// into the graduated status. This involves removing the kindergarten entries -// from both the height and channel indexes, and cleaning up the finalized -// kindergarten sweep txn. The height bucket will be opportunistically pruned -// from the height index as outputs are removed. -func (ns *nurseryStore) GraduateKinder(height uint32) error { +// GraduateKinder atomically moves an output at the provided height into the +// graduated status. This involves removing the kindergarten entries from both +// the height and channel indexes. The height bucket will be opportunistically +// pruned from the height index as outputs are removed. +func (ns *nurseryStore) GraduateKinder(height uint32, kid *kidOutput) error { return ns.db.Update(func(tx *bbolt.Tx) error { - // Since all kindergarten outputs at a particular height are - // swept in a single txn, we can now safely delete the finalized - // txn, since it has already been broadcast and confirmed. hghtBucket := ns.getHeightBucket(tx, height) if hghtBucket == nil { // Nothing to delete, bucket has already been removed. return nil } - // Remove the finalized kindergarten txn, we do this before - // removing the outputs so that the extra entry doesn't prevent - // the height bucket from being opportunistically pruned below. - if err := hghtBucket.Delete(finalizedKndrTxnKey); err != nil { - return err - } - - // For each kindergarten found output, delete its entry from the + // For the kindergarten output, delete its entry from the // height and channel index, and create a new grad output in the // channel index. - return ns.forEachHeightPrefix(tx, kndrPrefix, height, - func(v []byte) error { - var kid kidOutput - err := kid.Decode(bytes.NewReader(v)) - if err != nil { - return err - } - - outpoint := kid.OutPoint() - chanPoint := kid.OriginChanPoint() - - // Construct the key under which the output is - // currently stored height and channel indexes. - pfxOutputKey, err := prefixOutputKey(kndrPrefix, - outpoint) - if err != nil { - return err - } - - // Remove the grad output's entry in the height - // index. - err = ns.removeOutputFromHeight(tx, height, - chanPoint, pfxOutputKey) - if err != nil { - return err - } - - chanBucket := ns.getChannelBucket(tx, - chanPoint) - if chanBucket == nil { - return ErrContractNotFound - } - - // Remove previous output with kindergarten - // prefix. - err = chanBucket.Delete(pfxOutputKey) - if err != nil { - return err - } + outpoint := kid.OutPoint() + chanPoint := kid.OriginChanPoint() - // Convert kindergarten key to graduate key. - copy(pfxOutputKey, gradPrefix) + // Construct the key under which the output is + // currently stored height and channel indexes. + pfxOutputKey, err := prefixOutputKey(kndrPrefix, + outpoint) + if err != nil { + return err + } - var gradBuffer bytes.Buffer - if err := kid.Encode(&gradBuffer); err != nil { - return err - } + // Remove the grad output's entry in the height + // index. + err = ns.removeOutputFromHeight(tx, height, + chanPoint, pfxOutputKey) + if err != nil { + return err + } - // Insert serialized output into channel bucket - // using graduate-prefixed key. - return chanBucket.Put(pfxOutputKey, - gradBuffer.Bytes()) - }, - ) - }) -} + chanBucket := ns.getChannelBucket(tx, + chanPoint) + if chanBucket == nil { + return ErrContractNotFound + } -// FinalizeKinder accepts a block height and a finalized kindergarten sweep -// transaction, persisting the transaction at the appropriate height bucket. The -// nursery store's last finalized height is also updated with the provided -// height. -func (ns *nurseryStore) FinalizeKinder(height uint32, - finalTx *wire.MsgTx) error { + // Remove previous output with kindergarten + // prefix. + err = chanBucket.Delete(pfxOutputKey) + if err != nil { + return err + } - return ns.db.Update(func(tx *bbolt.Tx) error { - return ns.finalizeKinder(tx, height, finalTx) - }) -} + // Convert kindergarten key to graduate key. + copy(pfxOutputKey, gradPrefix) -// GraduateHeight persists the provided height as the nursery store's last -// graduated height. -func (ns *nurseryStore) GraduateHeight(height uint32) error { + var gradBuffer bytes.Buffer + if err := kid.Encode(&gradBuffer); err != nil { + return err + } - return ns.db.Update(func(tx *bbolt.Tx) error { - return ns.putLastGraduatedHeight(tx, height) + // Insert serialized output into channel bucket + // using graduate-prefixed key. + return chanBucket.Put(pfxOutputKey, + gradBuffer.Bytes()) }) } @@ -625,23 +534,15 @@ func (ns *nurseryStore) GraduateHeight(height uint32) error { // FetchClass returns a list of the kindergarten and crib outputs whose timeouts // are expiring func (ns *nurseryStore) FetchClass( - height uint32) (*wire.MsgTx, []kidOutput, []babyOutput, error) { + height uint32) ([]kidOutput, []babyOutput, error) { // Construct list of all crib and kindergarten outputs that need to be // processed at the provided block height. - var finalTx *wire.MsgTx var kids []kidOutput var babies []babyOutput if err := ns.db.View(func(tx *bbolt.Tx) error { - - var err error - finalTx, err = ns.getFinalizedTxn(tx, height) - if err != nil { - return err - } - // Append each crib output to our list of babyOutputs. - if err = ns.forEachHeightPrefix(tx, cribPrefix, height, + if err := ns.forEachHeightPrefix(tx, cribPrefix, height, func(buf []byte) error { // We will attempt to deserialize all outputs @@ -683,10 +584,10 @@ func (ns *nurseryStore) FetchClass( }) }); err != nil { - return nil, nil, nil, err + return nil, nil, err } - return finalTx, kids, babies, nil + return kids, babies, nil } // FetchPreschools returns a list of all outputs currently stored in the @@ -938,32 +839,6 @@ func (ns *nurseryStore) RemoveChannel(chanPoint *wire.OutPoint) error { }) } -// LastFinalizedHeight returns the last block height for which the nursery -// store has finalized a kindergarten class. -func (ns *nurseryStore) LastFinalizedHeight() (uint32, error) { - var lastFinalizedHeight uint32 - err := ns.db.View(func(tx *bbolt.Tx) error { - var err error - lastFinalizedHeight, err = ns.getLastFinalizedHeight(tx) - return err - }) - - return lastFinalizedHeight, err -} - -// LastGraduatedHeight returns the last block height for which the nursery -// store has successfully graduated all outputs. -func (ns *nurseryStore) LastGraduatedHeight() (uint32, error) { - var lastGraduatedHeight uint32 - err := ns.db.View(func(tx *bbolt.Tx) error { - var err error - lastGraduatedHeight, err = ns.getLastGraduatedHeight(tx) - return err - }) - - return lastGraduatedHeight, err -} - // Helper Methods // enterCrib accepts a new htlc output that the nursery will incubate through @@ -994,6 +869,8 @@ func (ns *nurseryStore) enterCrib(tx *bbolt.Tx, baby *babyOutput) error { // Next, retrieve or create the height-channel bucket located in the // height bucket corresponding to the baby output's CLTV expiry height. + + // TODO: Handle late registration. hghtChanBucket, err := ns.createHeightChanBucket(tx, baby.expiry, chanPoint) if err != nil { @@ -1332,145 +1209,6 @@ func (ns *nurseryStore) forChanOutputs(tx *bbolt.Tx, chanPoint *wire.OutPoint, return chanBucket.ForEach(callback) } -// getLastFinalizedHeight is a helper method that retrieves the last height for -// which the database finalized its persistent state. -func (ns *nurseryStore) getLastFinalizedHeight(tx *bbolt.Tx) (uint32, error) { - // Retrieve the chain bucket associated with the given nursery store. - chainBucket := tx.Bucket(ns.pfxChainKey) - if chainBucket == nil { - return 0, nil - } - - // Lookup the last finalized height in the top-level chain bucket. - heightBytes := chainBucket.Get(lastFinalizedHeightKey) - if heightBytes == nil { - // We have never finalized, return height 0. - return 0, nil - } - - // If the resulting bytes are not sized like a uint32, then we have - // never finalized, so we return 0. - - // Otherwise, parse the bytes and return the last finalized height. - return byteOrder.Uint32(heightBytes), nil -} - -// finalizeKinder records a finalized kindergarten sweep txn to the given height -// bucket. It also updates the nursery store's last finalized height, so that we -// do not finalize the same height twice. If the finalized txn is nil, i.e. if -// the height has no kindergarten outputs, the height will be marked as -// finalized, and we skip the process of writing the txn. When the class is -// loaded, a nil value will be returned if no txn has been written to a -// finalized height bucket. -func (ns *nurseryStore) finalizeKinder(tx *bbolt.Tx, height uint32, - finalTx *wire.MsgTx) error { - - // TODO(conner) ensure height is greater that current finalized height. - - // 1. Write the last finalized height to the chain bucket. - - // Ensure that the chain bucket for this nursery store exists. - chainBucket, err := tx.CreateBucketIfNotExists(ns.pfxChainKey) - if err != nil { - return err - } - - // Serialize the provided last-finalized height, and store it in the - // top-level chain bucket for this nursery store. - var lastHeightBytes [4]byte - byteOrder.PutUint32(lastHeightBytes[:], height) - - err = chainBucket.Put(lastFinalizedHeightKey, lastHeightBytes[:]) - if err != nil { - return err - } - - // 2. Write the finalized txn in the appropriate height bucket. - - // If there is no finalized txn, we have nothing to do. - if finalTx == nil { - return nil - } - - // Otherwise serialize the finalized txn and write it to the height - // bucket. - hghtBucket := ns.getHeightBucket(tx, height) - if hghtBucket == nil { - return nil - } - - var finalTxnBuf bytes.Buffer - if err := finalTx.Serialize(&finalTxnBuf); err != nil { - return err - } - - return hghtBucket.Put(finalizedKndrTxnKey, finalTxnBuf.Bytes()) -} - -// getFinalizedTxn retrieves the finalized kindergarten sweep txn at the given -// height, returning nil if one was not found. -func (ns *nurseryStore) getFinalizedTxn(tx *bbolt.Tx, - height uint32) (*wire.MsgTx, error) { - - hghtBucket := ns.getHeightBucket(tx, height) - if hghtBucket == nil { - // No class to finalize. - return nil, nil - } - - finalTxBytes := hghtBucket.Get(finalizedKndrTxnKey) - if finalTxBytes == nil { - // No finalized txn for this height. - return nil, nil - } - - // Otherwise, deserialize and return the finalized transaction. - txn := &wire.MsgTx{} - if err := txn.Deserialize(bytes.NewReader(finalTxBytes)); err != nil { - return nil, err - } - - return txn, nil -} - -// getLastGraduatedHeight is a helper method that retrieves the last height for -// which the database graduated all outputs successfully. -func (ns *nurseryStore) getLastGraduatedHeight(tx *bbolt.Tx) (uint32, error) { - // Retrieve the chain bucket associated with the given nursery store. - chainBucket := tx.Bucket(ns.pfxChainKey) - if chainBucket == nil { - return 0, nil - } - - // Lookup the last graduated height in the top-level chain bucket. - heightBytes := chainBucket.Get(lastGraduatedHeightKey) - if heightBytes == nil { - // We have never graduated before, return height 0. - return 0, nil - } - - // Otherwise, parse the bytes and return the last graduated height. - return byteOrder.Uint32(heightBytes), nil -} - -// pubLastGraduatedHeight is a helper method that writes the provided height under -// the last graduated height key. -func (ns *nurseryStore) putLastGraduatedHeight(tx *bbolt.Tx, height uint32) error { - - // Ensure that the chain bucket for this nursery store exists. - chainBucket, err := tx.CreateBucketIfNotExists(ns.pfxChainKey) - if err != nil { - return err - } - - // Serialize the provided last-graduated height, and store it in the - // top-level chain bucket for this nursery store. - var lastHeightBytes [4]byte - byteOrder.PutUint32(lastHeightBytes[:], height) - - return chainBucket.Put(lastGraduatedHeightKey, lastHeightBytes[:]) -} - // errBucketNotEmpty signals that an attempt to prune a particular // bucket failed because it still has active outputs. var errBucketNotEmpty = errors.New("bucket is not empty, cannot be pruned") @@ -1541,7 +1279,8 @@ func (ns *nurseryStore) pruneHeight(tx *bbolt.Tx, height uint32) (bool, error) { // attempt to remove each one if they are empty, keeping track of the // number of height-channel buckets that still have active outputs. if err := hghtBucket.ForEach(func(chanBytes, v []byte) error { - // Skip the finalized txn key. + // Skip the finalized txn key if it still exists from a previous + // db version. if v != nil { return nil } diff --git a/nursery_store_test.go b/nursery_store_test.go index 4617e5e0eb..0c20595800 100644 --- a/nursery_store_test.go +++ b/nursery_store_test.go @@ -88,8 +88,6 @@ func TestNurseryStoreInit(t *testing.T) { assertNumChannels(t, ns, 0) assertNumPreschools(t, ns, 0) - assertLastFinalizedHeight(t, ns, 0) - assertLastGraduatedHeight(t, ns, 0) } // TestNurseryStoreIncubate tests the primary state transitions taken by outputs @@ -172,7 +170,7 @@ func TestNurseryStoreIncubate(t *testing.T) { // Now, move the commitment output to the kindergarten // bucket. - err = ns.PreschoolToKinder(test.commOutput) + err = ns.PreschoolToKinder(test.commOutput, 0) if err != test.err { t.Fatalf("unable to move commitment output from "+ "pscl to kndr: %v", err) @@ -212,7 +210,7 @@ func TestNurseryStoreIncubate(t *testing.T) { maturityHeight := test.commOutput.ConfHeight() + test.commOutput.BlocksToMaturity() - err = ns.GraduateKinder(maturityHeight) + err = ns.GraduateKinder(maturityHeight, test.commOutput) if err != nil { t.Fatalf("unable to graduate kindergarten class at "+ "height %d: %v", maturityHeight, err) @@ -286,7 +284,8 @@ func TestNurseryStoreIncubate(t *testing.T) { maturityHeight := htlcOutput.ConfHeight() + htlcOutput.BlocksToMaturity() - err = ns.GraduateKinder(maturityHeight) + err = ns.GraduateKinder(maturityHeight, + &htlcOutput.kidOutput) if err != nil { t.Fatalf("unable to graduate htlc output "+ "from kndr to grad: %v", err) @@ -333,93 +332,6 @@ func TestNurseryStoreIncubate(t *testing.T) { } } -// TestNurseryStoreFinalize tests that kindergarten sweep transactions are -// properly persisted, and that the last finalized height is being set -// accordingly. -func TestNurseryStoreFinalize(t *testing.T) { - cdb, cleanUp, err := makeTestDB() - if err != nil { - t.Fatalf("unable to open channel db: %v", err) - } - defer cleanUp() - - ns, err := newNurseryStore(&bitcoinTestnetGenesis, cdb) - if err != nil { - t.Fatalf("unable to open nursery store: %v", err) - } - - kid := &kidOutputs[3] - - // Compute the maturity height at which to enter the commitment output. - maturityHeight := kid.ConfHeight() + kid.BlocksToMaturity() - - // Since we haven't finalized before, we should see a last finalized - // height of 0. - assertLastFinalizedHeight(t, ns, 0) - - // Begin incubating the commitment output, which will be placed in the - // preschool bucket. - err = ns.Incubate([]kidOutput{*kid}, nil) - if err != nil { - t.Fatalf("unable to incubate commitment output: %v", err) - } - - // Then move the commitment output to the kindergarten bucket, so that - // the output is registered in the height index. - err = ns.PreschoolToKinder(kid) - if err != nil { - t.Fatalf("unable to move pscl output to kndr: %v", err) - } - - // We should still see a last finalized height of 0, since no classes - // have been graduated. - assertLastFinalizedHeight(t, ns, 0) - - // Now, iteratively finalize all heights below the maturity height, - // ensuring that the last finalized height is properly persisted, and - // that the finalized transactions are all nil. - for i := 0; i < int(maturityHeight); i++ { - err = ns.FinalizeKinder(uint32(i), nil) - if err != nil { - t.Fatalf("unable to finalize kndr at height=%d: %v", - i, err) - } - assertLastFinalizedHeight(t, ns, uint32(i)) - assertFinalizedTxn(t, ns, uint32(i), nil) - } - - // As we have now finalized all heights below the maturity height, we - // should still see the commitment output in the kindergarten bucket at - // its maturity height. - assertKndrAtMaturityHeight(t, ns, kid) - - // Now, finalize the kindergarten sweep transaction at the maturity - // height. - err = ns.FinalizeKinder(maturityHeight, timeoutTx) - if err != nil { - t.Fatalf("unable to finalize kndr at height=%d: %v", - maturityHeight, err) - } - - // The nursery store should now see the maturity height finalized, and - // the finalized kindergarten sweep txn should be returned at this - // height. - assertLastFinalizedHeight(t, ns, maturityHeight) - assertFinalizedTxn(t, ns, maturityHeight, timeoutTx) - - // Lastly, continue to finalize heights above the maturity height. Each - // should report having a nil finalized kindergarten sweep txn. - for i := maturityHeight + 1; i < maturityHeight+10; i++ { - err = ns.FinalizeKinder(uint32(i), nil) - if err != nil { - t.Fatalf("unable to finalize kndr at height=%d: %v", - i, err) - } - assertLastFinalizedHeight(t, ns, uint32(i)) - assertFinalizedTxn(t, ns, uint32(i), nil) - } -} - // TestNurseryStoreGraduate verifies that the nursery store properly removes // populated entries from the height index as it is purged, and that the last // purged height is set appropriately. @@ -441,9 +353,6 @@ func TestNurseryStoreGraduate(t *testing.T) { // height index. maturityHeight := kid.ConfHeight() + kid.BlocksToMaturity() - // Since we have never purged, the last purged height should be 0. - assertLastGraduatedHeight(t, ns, 0) - // First, add a commitment output to the nursery store, which is // initially inserted in the preschool bucket. err = ns.Incubate([]kidOutput{*kid}, nil) @@ -453,7 +362,7 @@ func TestNurseryStoreGraduate(t *testing.T) { // Then, move the commitment output to the kindergarten bucket, such // that it resides in the height index at its maturity height. - err = ns.PreschoolToKinder(kid) + err = ns.PreschoolToKinder(kid, 0) if err != nil { t.Fatalf("unable to move pscl output to kndr: %v", err) } @@ -462,12 +371,6 @@ func TestNurseryStoreGraduate(t *testing.T) { // checking that each class is now empty, and that the last purged // height is set correctly. for i := 0; i < int(maturityHeight); i++ { - err = ns.GraduateHeight(uint32(i)) - if err != nil { - t.Fatalf("unable to purge height=%d: %v", i, err) - } - - assertLastGraduatedHeight(t, ns, uint32(i)) assertHeightIsPurged(t, ns, uint32(i)) } @@ -475,27 +378,7 @@ func TestNurseryStoreGraduate(t *testing.T) { // height. assertKndrAtMaturityHeight(t, ns, kid) - // Finalize the kindergarten transaction, ensuring that it is a non-nil - // value. - err = ns.FinalizeKinder(maturityHeight, timeoutTx) - if err != nil { - t.Fatalf("unable to finalize kndr at height=%d: %v", - maturityHeight, err) - } - - // Verify that the maturity height has now been finalized. - assertLastFinalizedHeight(t, ns, maturityHeight) - assertFinalizedTxn(t, ns, maturityHeight, timeoutTx) - - // Finally, purge the non-empty maturity height, and check that returned - // class is empty. - err = ns.GraduateHeight(maturityHeight) - if err != nil { - t.Fatalf("unable to set graduated height=%d: %v", maturityHeight, - err) - } - - err = ns.GraduateKinder(maturityHeight) + err = ns.GraduateKinder(maturityHeight, kid) if err != nil { t.Fatalf("unable to graduate kindergarten outputs at height=%d: "+ "%v", maturityHeight, err) @@ -528,36 +411,6 @@ func assertNumChanOutputs(t *testing.T, ns NurseryStore, } } -// assertLastFinalizedHeight checks that the nursery stores last finalized -// height matches the expected height. -func assertLastFinalizedHeight(t *testing.T, ns NurseryStore, - expected uint32) { - - lfh, err := ns.LastFinalizedHeight() - if err != nil { - t.Fatalf("unable to get last finalized height: %v", err) - } - - if lfh != expected { - t.Fatalf("expected last finalized height to be %d, got %d", - expected, lfh) - } -} - -// assertLastGraduatedHeight checks that the nursery stores last purged height -// matches the expected height. -func assertLastGraduatedHeight(t *testing.T, ns NurseryStore, expected uint32) { - lgh, err := ns.LastGraduatedHeight() - if err != nil { - t.Fatalf("unable to get last graduated height: %v", err) - } - - if lgh != expected { - t.Fatalf("expected last graduated height to be %d, got %d", - expected, lgh) - } -} - // assertNumPreschools loads all preschool outputs and verifies their count // matches the expected number. func assertNumPreschools(t *testing.T, ns NurseryStore, expected int) { @@ -592,16 +445,12 @@ func assertNumChannels(t *testing.T, ns NurseryStore, expected int) { func assertHeightIsPurged(t *testing.T, ns NurseryStore, height uint32) { - finalTx, kndrOutputs, cribOutputs, err := ns.FetchClass(height) + kndrOutputs, cribOutputs, err := ns.FetchClass(height) if err != nil { t.Fatalf("unable to retrieve class at height=%d: %v", height, err) } - if finalTx != nil { - t.Fatalf("height=%d not purged, final txn should be nil", height) - } - if kndrOutputs != nil { t.Fatalf("height=%d not purged, kndr outputs should be nil", height) } @@ -617,7 +466,7 @@ func assertCribAtExpiryHeight(t *testing.T, ns NurseryStore, htlcOutput *babyOutput) { expiryHeight := htlcOutput.expiry - _, _, cribOutputs, err := ns.FetchClass(expiryHeight) + _, cribOutputs, err := ns.FetchClass(expiryHeight) if err != nil { t.Fatalf("unable to retrieve class at height=%d: %v", expiryHeight, err) @@ -639,7 +488,7 @@ func assertCribNotAtExpiryHeight(t *testing.T, ns NurseryStore, htlcOutput *babyOutput) { expiryHeight := htlcOutput.expiry - _, _, cribOutputs, err := ns.FetchClass(expiryHeight) + _, cribOutputs, err := ns.FetchClass(expiryHeight) if err != nil { t.Fatalf("unable to retrieve class at height %d: %v", expiryHeight, err) @@ -653,25 +502,6 @@ func assertCribNotAtExpiryHeight(t *testing.T, ns NurseryStore, } } -// assertFinalizedTxn loads the class at the given height and compares the -// returned finalized txn to that in the class. It is safe to presented a nil -// expected transaction. -func assertFinalizedTxn(t *testing.T, ns NurseryStore, height uint32, - exFinalTx *wire.MsgTx) { - - finalTx, _, _, err := ns.FetchClass(height) - if err != nil { - t.Fatalf("unable to fetch class at height=%d: %v", height, - err) - } - - if !reflect.DeepEqual(finalTx, exFinalTx) { - t.Fatalf("expected finalized txn at height=%d "+ - "to be %v, got %v", height, finalTx.TxHash(), - exFinalTx.TxHash()) - } -} - // assertKndrAtMaturityHeight loads the class at the provided height and // verifies that the provided kid output is one of the kindergarten outputs // returned. @@ -680,7 +510,7 @@ func assertKndrAtMaturityHeight(t *testing.T, ns NurseryStore, maturityHeight := kndrOutput.ConfHeight() + kndrOutput.BlocksToMaturity() - _, kndrOutputs, _, err := ns.FetchClass(maturityHeight) + kndrOutputs, _, err := ns.FetchClass(maturityHeight) if err != nil { t.Fatalf("unable to retrieve class at height %d: %v", maturityHeight, err) @@ -705,7 +535,7 @@ func assertKndrNotAtMaturityHeight(t *testing.T, ns NurseryStore, maturityHeight := kndrOutput.ConfHeight() + kndrOutput.BlocksToMaturity() - _, kndrOutputs, _, err := ns.FetchClass(maturityHeight) + kndrOutputs, _, err := ns.FetchClass(maturityHeight) if err != nil { t.Fatalf("unable to retrieve class at height %d: %v", maturityHeight, err) diff --git a/peer_test.go b/peer_test.go index 86ba3c1643..ccb7cd41e0 100644 --- a/peer_test.go +++ b/peer_test.go @@ -157,7 +157,7 @@ func TestPeerChannelClosureAcceptFeeInitiator(t *testing.T) { dummyDeliveryScript), } - estimator := lnwallet.StaticFeeEstimator{FeePerKW: 12500} + estimator := lnwallet.NewStaticFeeEstimator(12500, 0) feePerKw, err := estimator.EstimateFeePerKW(1) if err != nil { t.Fatalf("unable to query fee estimator: %v", err) @@ -447,7 +447,7 @@ func TestPeerChannelClosureFeeNegotiationsInitiator(t *testing.T) { msg: respShutdown, } - estimator := lnwallet.StaticFeeEstimator{FeePerKW: 12500} + estimator := lnwallet.NewStaticFeeEstimator(12500, 0) initiatorIdealFeeRate, err := estimator.EstimateFeePerKW(1) if err != nil { t.Fatalf("unable to query fee estimator: %v", err) diff --git a/server.go b/server.go index c792122bfd..c6ae09b629 100644 --- a/server.go +++ b/server.go @@ -17,8 +17,6 @@ import ( "sync/atomic" "time" - "github.com/lightningnetwork/lnd/sweep" - "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/connmgr" @@ -40,6 +38,7 @@ import ( "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/nat" "github.com/lightningnetwork/lnd/routing" + "github.com/lightningnetwork/lnd/sweep" "github.com/lightningnetwork/lnd/ticker" "github.com/lightningnetwork/lnd/tor" ) @@ -156,6 +155,8 @@ type server struct { utxoNursery *utxoNursery + sweeper *sweep.UtxoSweeper + chainArb *contractcourt.ChainArbitrator sphinx *htlcswitch.OnionProcessor @@ -597,24 +598,45 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB, cc *chainControl, return nil, err } - sweeper := sweep.New(&sweep.UtxoSweeperConfig{ + srvrLog.Tracef("Sweeper batch window duration: %v", + sweep.DefaultBatchWindowDuration) + + sweeperStore, err := sweep.NewSweeperStore( + chanDB, activeNetParams.GenesisHash, + ) + if err != nil { + srvrLog.Errorf("unable to create sweeper store: %v", err) + return nil, err + } + + s.sweeper = sweep.New(&sweep.UtxoSweeperConfig{ Estimator: cc.feeEstimator, GenSweepScript: func() ([]byte, error) { return newSweepPkScript(cc.wallet) }, - Signer: cc.wallet.Cfg.Signer, + Signer: cc.wallet.Cfg.Signer, + PublishTransaction: cc.wallet.PublishTransaction, + NewBatchTimer: func() <-chan time.Time { + return time.NewTimer(sweep.DefaultBatchWindowDuration).C + }, + SweepTxConfTarget: 6, + Notifier: cc.chainNotifier, + ChainIO: cc.chainIO, + Store: sweeperStore, + MaxInputsPerTx: sweep.DefaultMaxInputsPerTx, + MaxSweepAttempts: sweep.DefaultMaxSweepAttempts, + NextAttemptDeltaFunc: sweep.DefaultNextAttemptDeltaFunc, }) s.utxoNursery = newUtxoNursery(&NurseryConfig{ ChainIO: cc.chainIO, ConfDepth: 1, - SweepTxConfTarget: 6, FetchClosedChannels: chanDB.FetchClosedChannels, FetchClosedChannel: chanDB.FetchClosedChannel, Notifier: cc.chainNotifier, PublishTransaction: cc.wallet.PublishTransaction, Store: utxnStore, - Sweeper: sweeper, + Sweeper: s.sweeper, }) // Construct a closure that wraps the htlcswitch's CloseLink method. @@ -706,7 +728,7 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB, cc *chainControl, DisableChannel: func(op wire.OutPoint) error { return s.announceChanStatus(op, true) }, - Sweeper: sweeper, + Sweeper: s.sweeper, }, chanDB) s.breachArbiter = newBreachArbiter(&BreachConfig{ @@ -963,6 +985,9 @@ func (s *server) Start() error { if err := s.htlcSwitch.Start(); err != nil { return err } + if err := s.sweeper.Start(); err != nil { + return err + } if err := s.utxoNursery.Start(); err != nil { return err } @@ -1050,6 +1075,7 @@ func (s *server) Stop() error { s.breachArbiter.Stop() s.authGossiper.Stop() s.chainArb.Stop() + s.sweeper.Stop() s.cc.wallet.Shutdown() s.cc.chainView.Stop() s.connMgr.Stop() diff --git a/sweep/backend_mock_test.go b/sweep/backend_mock_test.go new file mode 100644 index 0000000000..43699be309 --- /dev/null +++ b/sweep/backend_mock_test.go @@ -0,0 +1,110 @@ +package sweep + +import ( + "sync" + + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/lnwallet" +) + +// mockBackend simulates a chain backend for realistic behaviour in unit tests +// around double spends. +type mockBackend struct { + lock sync.Mutex + + notifier *MockNotifier + + confirmedSpendInputs map[wire.OutPoint]struct{} + + unconfirmedTxes map[chainhash.Hash]*wire.MsgTx + unconfirmedSpendInputs map[wire.OutPoint]struct{} +} + +func newMockBackend(notifier *MockNotifier) *mockBackend { + return &mockBackend{ + notifier: notifier, + unconfirmedTxes: make(map[chainhash.Hash]*wire.MsgTx), + confirmedSpendInputs: make(map[wire.OutPoint]struct{}), + unconfirmedSpendInputs: make(map[wire.OutPoint]struct{}), + } +} + +func (b *mockBackend) publishTransaction(tx *wire.MsgTx) error { + b.lock.Lock() + defer b.lock.Unlock() + + txHash := tx.TxHash() + if _, ok := b.unconfirmedTxes[txHash]; ok { + // Tx already exists + testLog.Tracef("mockBackend duplicate tx %v", tx.TxHash()) + return lnwallet.ErrDoubleSpend + } + + for _, in := range tx.TxIn { + if _, ok := b.unconfirmedSpendInputs[in.PreviousOutPoint]; ok { + // Double spend + testLog.Tracef("mockBackend double spend tx %v", tx.TxHash()) + return lnwallet.ErrDoubleSpend + } + + if _, ok := b.confirmedSpendInputs[in.PreviousOutPoint]; ok { + // Already included in block + testLog.Tracef("mockBackend already in block tx %v", tx.TxHash()) + return lnwallet.ErrDoubleSpend + } + } + + b.unconfirmedTxes[txHash] = tx + for _, in := range tx.TxIn { + b.unconfirmedSpendInputs[in.PreviousOutPoint] = struct{}{} + } + + testLog.Tracef("mockBackend publish tx %v", tx.TxHash()) + + return nil +} + +func (b *mockBackend) deleteUnconfirmed(txHash chainhash.Hash) { + b.lock.Lock() + defer b.lock.Unlock() + + tx, ok := b.unconfirmedTxes[txHash] + if !ok { + // Tx already exists + testLog.Errorf("mockBackend delete tx not existing %v", txHash) + return + } + + testLog.Tracef("mockBackend delete tx %v", tx.TxHash()) + delete(b.unconfirmedTxes, txHash) + for _, in := range tx.TxIn { + delete(b.unconfirmedSpendInputs, in.PreviousOutPoint) + } +} + +func (b *mockBackend) mine() { + b.lock.Lock() + defer b.lock.Unlock() + + notifications := make(map[wire.OutPoint]*wire.MsgTx) + for _, tx := range b.unconfirmedTxes { + testLog.Tracef("mockBackend mining tx %v", tx.TxHash()) + for _, in := range tx.TxIn { + b.confirmedSpendInputs[in.PreviousOutPoint] = struct{}{} + notifications[in.PreviousOutPoint] = tx + } + } + b.unconfirmedSpendInputs = make(map[wire.OutPoint]struct{}) + b.unconfirmedTxes = make(map[chainhash.Hash]*wire.MsgTx) + + for outpoint, tx := range notifications { + testLog.Tracef("mockBackend delivering spend ntfn for %v", + outpoint) + b.notifier.SpendOutpoint(outpoint, *tx) + } +} + +func (b *mockBackend) isDone() bool { + return len(b.unconfirmedTxes) == 0 +} diff --git a/sweep/defaults.go b/sweep/defaults.go new file mode 100644 index 0000000000..d9b5c9231d --- /dev/null +++ b/sweep/defaults.go @@ -0,0 +1,14 @@ +// +build !rpctest + +package sweep + +import ( + "time" +) + +var ( + // DefaultBatchWindowDuration specifies duration of the sweep batch + // window. The sweep is held back during the batch window to allow more + // inputs to be added and thereby lower the fee per input. + DefaultBatchWindowDuration = 30 * time.Second +) diff --git a/sweep/defaults_rpctest.go b/sweep/defaults_rpctest.go new file mode 100644 index 0000000000..6d027ab6f7 --- /dev/null +++ b/sweep/defaults_rpctest.go @@ -0,0 +1,17 @@ +// +build rpctest + +package sweep + +import ( + "time" +) + +var ( + // DefaultBatchWindowDuration specifies duration of the sweep batch + // window. The sweep is held back during the batch window to allow more + // inputs to be added and thereby lower the fee per input. + // + // To speed up integration tests waiting for a sweep to happen, the + // batch window is shortened. + DefaultBatchWindowDuration = 2 * time.Second +) diff --git a/sweep/fee_estimator_mock_test.go b/sweep/fee_estimator_mock_test.go new file mode 100644 index 0000000000..8f3eb44bbc --- /dev/null +++ b/sweep/fee_estimator_mock_test.go @@ -0,0 +1,62 @@ +package sweep + +import ( + "github.com/lightningnetwork/lnd/lnwallet" + "sync" +) + +// mockFeeEstimator implements a mock fee estimator. It closely resembles +// lnwallet.StaticFeeEstimator with the addition that fees can be changed for +// testing purposes in a thread safe manner. +type mockFeeEstimator struct { + feePerKW lnwallet.SatPerKWeight + + relayFee lnwallet.SatPerKWeight + + lock sync.Mutex +} + +func newMockFeeEstimator(feePerKW, + relayFee lnwallet.SatPerKWeight) *mockFeeEstimator { + + return &mockFeeEstimator{ + feePerKW: feePerKW, + relayFee: relayFee, + } +} + +func (e *mockFeeEstimator) updateFees(feePerKW, + relayFee lnwallet.SatPerKWeight) { + + e.lock.Lock() + defer e.lock.Unlock() + + e.feePerKW = feePerKW + e.relayFee = relayFee +} + +func (e *mockFeeEstimator) EstimateFeePerKW(numBlocks uint32) ( + lnwallet.SatPerKWeight, error) { + + e.lock.Lock() + defer e.lock.Unlock() + + return e.feePerKW, nil +} + +func (e *mockFeeEstimator) RelayFeePerKW() lnwallet.SatPerKWeight { + e.lock.Lock() + defer e.lock.Unlock() + + return e.relayFee +} + +func (e *mockFeeEstimator) Start() error { + return nil +} + +func (e *mockFeeEstimator) Stop() error { + return nil +} + +var _ lnwallet.FeeEstimator = (*mockFeeEstimator)(nil) diff --git a/sweep/input.go b/sweep/input.go index 6cf09760dd..57ee793937 100644 --- a/sweep/input.go +++ b/sweep/input.go @@ -33,12 +33,17 @@ type Input interface { // the output can be spent. For non-CSV locked inputs this is always // zero. BlocksToMaturity() uint32 + + // HeightHint returns the minimum height at which a confirmed spending + // tx can occur. + HeightHint() uint32 } type inputKit struct { outpoint wire.OutPoint witnessType lnwallet.WitnessType signDesc lnwallet.SignDescriptor + heightHint uint32 } // OutPoint returns the breached output's identifier that is to be included as @@ -59,6 +64,12 @@ func (i *inputKit) SignDesc() *lnwallet.SignDescriptor { return &i.signDesc } +// HeightHint returns the minimum height at which a confirmed spending +// tx can occur. +func (i *inputKit) HeightHint() uint32 { + return i.heightHint +} + // BaseInput contains all the information needed to sweep a basic output // (CSV/CLTV/no time lock) type BaseInput struct { @@ -68,13 +79,14 @@ type BaseInput struct { // MakeBaseInput assembles a new BaseInput that can be used to construct a // sweep transaction. func MakeBaseInput(outpoint *wire.OutPoint, witnessType lnwallet.WitnessType, - signDescriptor *lnwallet.SignDescriptor) BaseInput { + signDescriptor *lnwallet.SignDescriptor, heightHint uint32) BaseInput { return BaseInput{ inputKit{ outpoint: *outpoint, witnessType: witnessType, signDesc: *signDescriptor, + heightHint: heightHint, }, } } @@ -113,13 +125,14 @@ type HtlcSucceedInput struct { // construct a sweep transaction. func MakeHtlcSucceedInput(outpoint *wire.OutPoint, signDescriptor *lnwallet.SignDescriptor, - preimage []byte) HtlcSucceedInput { + preimage []byte, heightHint uint32) HtlcSucceedInput { return HtlcSucceedInput{ inputKit: inputKit{ outpoint: *outpoint, witnessType: lnwallet.HtlcAcceptedRemoteSuccess, signDesc: *signDescriptor, + heightHint: heightHint, }, preimage: preimage, } diff --git a/sweep/store.go b/sweep/store.go new file mode 100644 index 0000000000..ef6ba99c94 --- /dev/null +++ b/sweep/store.go @@ -0,0 +1,247 @@ +package sweep + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/coreos/bbolt" + "github.com/lightningnetwork/lnd/channeldb" +) + +var ( + // lastTxBucketKey is the key that points to a bucket containing a + // single item storing the last published tx. + // + // maps: lastTxKey -> serialized_tx + lastTxBucketKey = []byte("sweeper-last-tx") + + // lastTxKey is the fixed key under which the serialized tx is stored. + lastTxKey = []byte("last-tx") + + // txHashesBucketKey is the key that points to a bucket containing the + // hashes of all sweep txes that were published successfully. + // + // maps: txHash -> empty slice + txHashesBucketKey = []byte("sweeper-tx-hashes") + + // utxnChainPrefix is the bucket prefix for nursery buckets. + utxnChainPrefix = []byte("utxn") + + // utxnHeightIndexKey is the sub bucket where the nursery stores the + // height index. + utxnHeightIndexKey = []byte("height-index") + + // utxnFinalizedKndrTxnKey is a static key that can be used to locate + // the nursery finalized kindergarten sweep txn. + utxnFinalizedKndrTxnKey = []byte("finalized-kndr-txn") + + byteOrder = binary.BigEndian +) + +// SweeperStore stores published txes. +type SweeperStore interface { + // IsOurTx determines whether a tx is published by us, based on its + // hash. + IsOurTx(hash chainhash.Hash) (bool, error) + + // NotifyPublishTx signals that we are about to publish a tx. + NotifyPublishTx(*wire.MsgTx) error + + // GetLastPublishedTx returns the last tx that we called NotifyPublishTx + // for. + GetLastPublishedTx() (*wire.MsgTx, error) +} + +type sweeperStore struct { + db *channeldb.DB +} + +// NewSweeperStore returns a new store instance. +func NewSweeperStore(db *channeldb.DB, chainHash *chainhash.Hash) ( + SweeperStore, error) { + + err := db.Update(func(tx *bbolt.Tx) error { + _, err := tx.CreateBucketIfNotExists( + lastTxBucketKey, + ) + if err != nil { + return err + } + + if tx.Bucket(txHashesBucketKey) != nil { + return nil + } + + txHashesBucket, err := tx.CreateBucket(txHashesBucketKey) + if err != nil { + return err + } + + // Use non-existence of tx hashes bucket as a signal to migrate + // nursery finalized txes. + err = migrateTxHashes(tx, txHashesBucket, chainHash) + + return err + }) + if err != nil { + return nil, err + } + + return &sweeperStore{ + db: db, + }, nil +} + +// migrateTxHashes migrates nursery finalized txes to the tx hashes bucket. This +// is not implemented as a database migration, to keep the downgrade path open. +func migrateTxHashes(tx *bbolt.Tx, txHashesBucket *bbolt.Bucket, + chainHash *chainhash.Hash) error { + + log.Infof("Migrating UTXO nursery finalized TXIDs") + + // Compose chain bucket key. + var b bytes.Buffer + if _, err := b.Write(utxnChainPrefix); err != nil { + return err + } + + if _, err := b.Write(chainHash[:]); err != nil { + return err + } + + // Get chain bucket if exists. + chainBucket := tx.Bucket(b.Bytes()) + if chainBucket == nil { + return nil + } + + // Retrieve the existing height index. + hghtIndex := chainBucket.Bucket(utxnHeightIndexKey) + if hghtIndex == nil { + return nil + } + + // Retrieve all heights. + err := hghtIndex.ForEach(func(k, v []byte) error { + heightBucket := hghtIndex.Bucket(k) + if heightBucket == nil { + return nil + } + + // Get finalized tx for height. + txBytes := heightBucket.Get(utxnFinalizedKndrTxnKey) + if txBytes == nil { + return nil + } + + // Deserialize and skip tx if it cannot be deserialized. + tx := &wire.MsgTx{} + err := tx.Deserialize(bytes.NewReader(txBytes)) + if err != nil { + log.Warnf("Cannot deserialize utxn tx") + return nil + } + + // Calculate hash. + hash := tx.TxHash() + + // Insert utxn tx hash in hashes bucket. + log.Debugf("Inserting nursery tx %v in hash list "+ + "(height=%v)", hash, byteOrder.Uint32(k)) + + return txHashesBucket.Put(hash[:], []byte{}) + }) + if err != nil { + return err + } + + return nil +} + +// NotifyPublishTx signals that we are about to publish a tx. +func (s *sweeperStore) NotifyPublishTx(sweepTx *wire.MsgTx) error { + return s.db.Update(func(tx *bbolt.Tx) error { + lastTxBucket := tx.Bucket(lastTxBucketKey) + if lastTxBucket == nil { + return errors.New("last tx bucket does not exist") + } + + txHashesBucket := tx.Bucket(txHashesBucketKey) + if txHashesBucket == nil { + return errors.New("tx hashes bucket does not exist") + } + + var b bytes.Buffer + if err := sweepTx.Serialize(&b); err != nil { + return err + } + + if err := lastTxBucket.Put(lastTxKey, b.Bytes()); err != nil { + return err + } + + hash := sweepTx.TxHash() + + return txHashesBucket.Put(hash[:], []byte{}) + }) +} + +// GetLastPublishedTx returns the last tx that we called NotifyPublishTx +// for. +func (s *sweeperStore) GetLastPublishedTx() (*wire.MsgTx, error) { + var sweepTx *wire.MsgTx + + err := s.db.View(func(tx *bbolt.Tx) error { + lastTxBucket := tx.Bucket(lastTxBucketKey) + if lastTxBucket == nil { + return errors.New("last tx bucket does not exist") + } + + sweepTxRaw := lastTxBucket.Get(lastTxKey) + if sweepTxRaw == nil { + return nil + } + + sweepTx = &wire.MsgTx{} + txReader := bytes.NewReader(sweepTxRaw) + if err := sweepTx.Deserialize(txReader); err != nil { + return fmt.Errorf("tx deserialize: %v", err) + } + + return nil + }) + if err != nil { + return nil, err + } + + return sweepTx, nil +} + +// IsOurTx determines whether a tx is published by us, based on its +// hash. +func (s *sweeperStore) IsOurTx(hash chainhash.Hash) (bool, error) { + var ours bool + + err := s.db.View(func(tx *bbolt.Tx) error { + txHashesBucket := tx.Bucket(txHashesBucketKey) + if txHashesBucket == nil { + return errors.New("tx hashes bucket does not exist") + } + + ours = txHashesBucket.Get(hash[:]) != nil + + return nil + }) + if err != nil { + return false, err + } + + return ours, nil +} + +// Compile-time constraint to ensure sweeperStore implements SweeperStore. +var _ SweeperStore = (*sweeperStore)(nil) diff --git a/sweep/store_mock.go b/sweep/store_mock.go new file mode 100644 index 0000000000..ba8b2f2bbc --- /dev/null +++ b/sweep/store_mock.go @@ -0,0 +1,45 @@ +package sweep + +import ( + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" +) + +// MockSweeperStore is a mock implementation of sweeper store. This type is +// exported, because it is currently used in nursery tests too. +type MockSweeperStore struct { + lastTx *wire.MsgTx + ourTxes map[chainhash.Hash]struct{} +} + +// NewMockSweeperStore returns a new instance. +func NewMockSweeperStore() *MockSweeperStore { + return &MockSweeperStore{ + ourTxes: make(map[chainhash.Hash]struct{}), + } +} + +// IsOurTx determines whether a tx is published by us, based on its +// hash. +func (s *MockSweeperStore) IsOurTx(hash chainhash.Hash) (bool, error) { + _, ok := s.ourTxes[hash] + return ok, nil +} + +// NotifyPublishTx signals that we are about to publish a tx. +func (s *MockSweeperStore) NotifyPublishTx(tx *wire.MsgTx) error { + txHash := tx.TxHash() + s.ourTxes[txHash] = struct{}{} + s.lastTx = tx + + return nil +} + +// GetLastPublishedTx returns the last tx that we called NotifyPublishTx +// for. +func (s *MockSweeperStore) GetLastPublishedTx() (*wire.MsgTx, error) { + return s.lastTx, nil +} + +// Compile-time constraint to ensure MockSweeperStore implements SweeperStore. +var _ SweeperStore = (*MockSweeperStore)(nil) diff --git a/sweep/store_test.go b/sweep/store_test.go new file mode 100644 index 0000000000..23714c781b --- /dev/null +++ b/sweep/store_test.go @@ -0,0 +1,153 @@ +package sweep + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/channeldb" +) + +// makeTestDB creates a new instance of the ChannelDB for testing purposes. A +// callback which cleans up the created temporary directories is also returned +// and intended to be executed after the test completes. +func makeTestDB() (*channeldb.DB, func(), error) { + // First, create a temporary directory to be used for the duration of + // this test. + tempDirName, err := ioutil.TempDir("", "channeldb") + if err != nil { + return nil, nil, err + } + + // Next, create channeldb for the first time. + cdb, err := channeldb.Open(tempDirName) + if err != nil { + return nil, nil, err + } + + cleanUp := func() { + cdb.Close() + os.RemoveAll(tempDirName) + } + + return cdb, cleanUp, nil +} + +// TestStore asserts that the store persists the presented data to disk and is +// able to retrieve it again. +func TestStore(t *testing.T) { + t.Run("bolt", func(t *testing.T) { + + // Create new store. + cdb, cleanUp, err := makeTestDB() + if err != nil { + t.Fatalf("unable to open channel db: %v", err) + } + defer cleanUp() + + if err != nil { + t.Fatal(err) + } + + testStore(t, func() (SweeperStore, error) { + var chain chainhash.Hash + return NewSweeperStore(cdb, &chain) + }) + }) + t.Run("mock", func(t *testing.T) { + store := NewMockSweeperStore() + + testStore(t, func() (SweeperStore, error) { + // Return same store, because the mock has no real + // persistence. + return store, nil + }) + }) +} + +func testStore(t *testing.T, createStore func() (SweeperStore, error)) { + store, err := createStore() + if err != nil { + t.Fatal(err) + } + + // Initially we expect the store not to have a last published tx. + retrievedTx, err := store.GetLastPublishedTx() + if err != nil { + t.Fatal(err) + } + if retrievedTx != nil { + t.Fatal("expected no last published tx") + } + + // Notify publication of tx1 + tx1 := wire.MsgTx{} + tx1.AddTxIn(&wire.TxIn{ + PreviousOutPoint: wire.OutPoint{ + Index: 1, + }, + }) + + err = store.NotifyPublishTx(&tx1) + if err != nil { + t.Fatal(err) + } + + // Notify publication of tx2 + tx2 := wire.MsgTx{} + tx2.AddTxIn(&wire.TxIn{ + PreviousOutPoint: wire.OutPoint{ + Index: 2, + }, + }) + + err = store.NotifyPublishTx(&tx2) + if err != nil { + t.Fatal(err) + } + + // Recreate the sweeper store + store, err = createStore() + if err != nil { + t.Fatal(err) + } + + // Assert that last published tx2 is present. + retrievedTx, err = store.GetLastPublishedTx() + if err != nil { + t.Fatal(err) + } + + if tx2.TxHash() != retrievedTx.TxHash() { + t.Fatal("txes do not match") + } + + // Assert that both txes are recognized as our own. + ours, err := store.IsOurTx(tx1.TxHash()) + if err != nil { + t.Fatal(err) + } + if !ours { + t.Fatal("expected tx to be ours") + } + + ours, err = store.IsOurTx(tx2.TxHash()) + if err != nil { + t.Fatal(err) + } + if !ours { + t.Fatal("expected tx to be ours") + } + + // An different hash should be reported on as not being ours. + var unknownHash chainhash.Hash + ours, err = store.IsOurTx(unknownHash) + if err != nil { + t.Fatal(err) + } + if ours { + t.Fatal("expected tx to be not ours") + } +} diff --git a/sweep/sweeper.go b/sweep/sweeper.go index a4c5a439af..3829d27e6a 100644 --- a/sweep/sweeper.go +++ b/sweep/sweeper.go @@ -1,24 +1,88 @@ package sweep import ( - "github.com/btcsuite/btcd/blockchain" - "github.com/btcsuite/btcd/txscript" + "errors" + "fmt" + "math/rand" + "sync" + "sync/atomic" + "time" + "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" + "github.com/davecgh/go-spew/spew" + "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/lnwallet" ) -// UtxoSweeper provides the functionality to generate sweep txes. The plan is -// to extend UtxoSweeper in the future to also manage the actual sweeping -// process by itself. +var ( + // ErrRemoteSpend is returned in case an output that we try to sweep is + // confirmed in a tx of the remote party. + ErrRemoteSpend = errors.New("remote party swept utxo") + + // ErrTooManyAttempts is returned in case sweeping an output has failed + // for the configured max number of attempts. + ErrTooManyAttempts = errors.New("sweep failed after max attempts") + + // DefaultMaxSweepAttempts specifies the default maximum number of times + // an input is included in a publish attempt before giving up and + // returning an error to the caller. + DefaultMaxSweepAttempts = 10 +) + +// pendingInput is created when an input reaches the main loop for the first +// time. It tracks all relevant state that is needed for sweeping. +type pendingInput struct { + // listeners is a list of channels over which the final outcome of the + // sweep needs to be broadcasted. + listeners []chan Result + + // input is the original struct that contains the input and sign + // descriptor. + input Input + + // ntfnRegCancel is populated with a function that cancels the chain + // notifier spend registration. + ntfnRegCancel func() + + // minPublishHeight indicates the minimum block height at which this + // input may be (re)published. + minPublishHeight int32 + + // publishAttempts records the number of attempts that have already been + // made to sweep this tx. + publishAttempts int +} + +// UtxoSweeper is responsible for sweeping outputs back into the wallet type UtxoSweeper struct { + started uint32 // To be used atomically. + stopped uint32 // To be used atomically. + cfg *UtxoSweeperConfig + + newInputs chan *sweepInputMessage + spendChan chan *chainntnfs.SpendDetail + + pendingInputs map[wire.OutPoint]*pendingInput + + // timer is the channel that signals expiry of the sweep batch timer. + timer <-chan time.Time + + testSpendChan chan wire.OutPoint + + currentOutputScript []byte + + relayFeePerKW lnwallet.SatPerKWeight + + quit chan struct{} + wg sync.WaitGroup } // UtxoSweeperConfig contains dependencies of UtxoSweeper. type UtxoSweeperConfig struct { - // GenSweepScript generates a P2WKH script belonging to the wallet - // where funds can be swept. + // GenSweepScript generates a P2WKH script belonging to the wallet where + // funds can be swept. GenSweepScript func() ([]byte, error) // Estimator is used when crafting sweep transactions to estimate the @@ -26,209 +90,694 @@ type UtxoSweeperConfig struct { // transaction. Estimator lnwallet.FeeEstimator + // PublishTransaction facilitates the process of broadcasting a signed + // transaction to the appropriate network. + PublishTransaction func(*wire.MsgTx) error + + // NewBatchTimer creates a channel that will be sent on when a certain + // time window has passed. During this time window, new inputs can still + // be added to the sweep tx that is about to be generated. + NewBatchTimer func() <-chan time.Time + + // Notifier is an instance of a chain notifier we'll use to watch for + // certain on-chain events. + Notifier chainntnfs.ChainNotifier + + // ChainIO is used to determine the current block height. + ChainIO lnwallet.BlockChainIO + + // Store stores the published sweeper txes. + Store SweeperStore + // Signer is used by the sweeper to generate valid witnesses at the // time the incubated outputs need to be spent. Signer lnwallet.Signer + + // SweepTxConfTarget assigns a confirmation target for sweep txes on + // which the fee calculation will be based. + SweepTxConfTarget uint32 + + // MaxInputsPerTx specifies the default maximum number of inputs allowed + // in a single sweep tx. If more need to be swept, multiple txes are + // created and published. + MaxInputsPerTx int + + // MaxSweepAttempts specifies the maximum number of times an input is + // included in a publish attempt before giving up and returning an error + // to the caller. + MaxSweepAttempts int + + // NextAttemptDeltaFunc returns given the number of already attempted + // sweeps, how many blocks to wait before retrying to sweep. + NextAttemptDeltaFunc func(int) int32 +} + +// Result is the struct that is pushed through the result channel. Callers can +// use this to be informed of the final sweep result. In case of a remote +// spend, Err will be ErrRemoteSpend. +type Result struct { + // Err is the final result of the sweep. It is nil when the input is + // swept successfully by us. ErrRemoteSpend is returned when another + // party took the input. + Err error + + // Tx is the transaction that spent the input. + Tx *wire.MsgTx } -// New returns a new UtxoSweeper instance. +// sweepInputMessage structs are used in the internal channel between the +// SweepInput call and the sweeper main loop. +type sweepInputMessage struct { + input Input + resultChan chan Result +} + +// New returns a new Sweeper instance. func New(cfg *UtxoSweeperConfig) *UtxoSweeper { + return &UtxoSweeper{ - cfg: cfg, + cfg: cfg, + newInputs: make(chan *sweepInputMessage), + spendChan: make(chan *chainntnfs.SpendDetail), + quit: make(chan struct{}), + pendingInputs: make(map[wire.OutPoint]*pendingInput), } } -// CreateSweepTx accepts a list of inputs and signs and generates a txn that -// spends from them. This method also makes an accurate fee estimate before -// generating the required witnesses. -// -// The created transaction has a single output sending all the funds back to -// the source wallet, after accounting for the fee estimate. -// -// The value of currentBlockHeight argument will be set as the tx locktime. -// This function assumes that all CLTV inputs will be unlocked after -// currentBlockHeight. Reasons not to use the maximum of all actual CLTV expiry -// values of the inputs: -// -// - Make handling re-orgs easier. -// - Thwart future possible fee sniping attempts. -// - Make us blend in with the bitcoind wallet. -func (s *UtxoSweeper) CreateSweepTx(inputs []Input, confTarget uint32, - currentBlockHeight uint32) (*wire.MsgTx, error) { +// Start starts the process of constructing and publish sweep txes. +func (s *UtxoSweeper) Start() error { + if !atomic.CompareAndSwapUint32(&s.started, 0, 1) { + return nil + } - // Generate the receiving script to which the funds will be swept. - pkScript, err := s.cfg.GenSweepScript() + log.Tracef("Sweeper starting") + + // Retrieve last published tx from database. + lastTx, err := s.cfg.Store.GetLastPublishedTx() if err != nil { - return nil, err + return fmt.Errorf("get last published tx: %v", err) } - // Using the txn weight estimate, compute the required txn fee. - feePerKw, err := s.cfg.Estimator.EstimateFeePerKW(confTarget) + // Republish in case the previous call crashed lnd. We don't care about + // the return value, because inputs will be re-offered and retried + // anyway. The only reason we republish here is to prevent the corner + // case where lnd goes into a restart loop because of a crashing publish + // tx where we keep deriving new output script. By publishing and + // possibly crashing already now, we haven't derived a new output script + // yet. + if lastTx != nil { + log.Debugf("Publishing last tx %v", lastTx.TxHash()) + + // Error can be ignored. Because we are starting up, there are + // no pending inputs to update based on the publish result. + err := s.cfg.PublishTransaction(lastTx) + if err != nil && err != lnwallet.ErrDoubleSpend { + log.Errorf("last tx publish: %v", err) + } + } + + // Retrieve relay fee for dust limit calculation. Assume that this will + // not change from here on. + s.relayFeePerKW = s.cfg.Estimator.RelayFeePerKW() + + // Register for block epochs to retry sweeping every block. + bestHash, bestHeight, err := s.cfg.ChainIO.GetBestBlock() if err != nil { - return nil, err + return fmt.Errorf("get best block: %v", err) + } + + log.Debugf("Best height: %v", bestHeight) + + blockEpochs, err := s.cfg.Notifier.RegisterBlockEpochNtfn( + &chainntnfs.BlockEpoch{ + Height: bestHeight, + Hash: bestHash, + }, + ) + if err != nil { + return fmt.Errorf("register block epoch ntfn: %v", err) } - inputs, txWeight, csvCount, cltvCount := s.getWeightEstimate(inputs) - log.Infof("Creating sweep transaction for %v inputs (%v CSV, %v CLTV) "+ - "using %v sat/kw", len(inputs), csvCount, cltvCount, - int64(feePerKw)) + // Start sweeper main loop. + s.wg.Add(1) + go func() { + defer blockEpochs.Cancel() + defer s.wg.Done() - txFee := feePerKw.FeeForWeight(txWeight) + err := s.collector(blockEpochs.Epochs, bestHeight) + if err != nil { + log.Errorf("sweeper stopped: %v", err) + } + }() - // Sum up the total value contained in the inputs. - var totalSum btcutil.Amount - for _, o := range inputs { - totalSum += btcutil.Amount(o.SignDesc().Output.Value) + return nil +} + +// Stop stops sweeper from listening to block epochs and constructing sweep +// txes. +func (s *UtxoSweeper) Stop() error { + if !atomic.CompareAndSwapUint32(&s.stopped, 0, 1) { + return nil } - // Sweep as much possible, after subtracting txn fees. - sweepAmt := int64(totalSum - txFee) + log.Debugf("Sweeper shutting down") - // Create the sweep transaction that we will be building. We use - // version 2 as it is required for CSV. The txn will sweep the amount - // after fees to the pkscript generated above. - sweepTx := wire.NewMsgTx(2) - sweepTx.AddTxOut(&wire.TxOut{ - PkScript: pkScript, - Value: sweepAmt, - }) + close(s.quit) + s.wg.Wait() - sweepTx.LockTime = currentBlockHeight + log.Debugf("Sweeper shut down") - // Add all inputs to the sweep transaction. Ensure that for each - // csvInput, we set the sequence number properly. - for _, input := range inputs { - sweepTx.AddTxIn(&wire.TxIn{ - PreviousOutPoint: *input.OutPoint(), - Sequence: input.BlocksToMaturity(), - }) + return nil +} + +// SweepInput sweeps inputs back into the wallet. The inputs will be batched and +// swept after the batch time window ends. +// +// NOTE: Extreme care needs to be taken that input isn't changed externally. +// Because it is an interface and we don't know what is exactly behind it, we +// cannot make a local copy in sweeper. +func (s *UtxoSweeper) SweepInput(input Input) (chan Result, error) { + if input == nil || input.OutPoint() == nil || input.SignDesc() == nil { + return nil, errors.New("nil input received") } - // Before signing the transaction, check to ensure that it meets some - // basic validity requirements. - // - // TODO(conner): add more control to sanity checks, allowing us to - // delay spending "problem" outputs, e.g. possibly batching with other - // classes if fees are too low. - btx := btcutil.NewTx(sweepTx) - if err := blockchain.CheckTransactionSanity(btx); err != nil { - return nil, err + log.Infof("Sweep request received: out_point=%v, witness_type=%v, "+ + "time_lock=%v, size=%v", input.OutPoint(), input.WitnessType(), + input.BlocksToMaturity(), + btcutil.Amount(input.SignDesc().Output.Value)) + + sweeperInput := &sweepInputMessage{ + input: input, + resultChan: make(chan Result, 1), } - hashCache := txscript.NewTxSigHashes(sweepTx) + // Deliver input to main event loop. + select { + case s.newInputs <- sweeperInput: + case <-s.quit: + return nil, fmt.Errorf("sweeper shutting down") + } - // With all the inputs in place, use each output's unique witness - // function to generate the final witness required for spending. - addWitness := func(idx int, tso Input) error { - witness, err := tso.BuildWitness( - s.cfg.Signer, sweepTx, hashCache, idx, - ) - if err != nil { - return err + return sweeperInput.resultChan, nil +} + +// collector is the sweeper main loop. It processes new inputs, spend +// notifications and counts down to publication of the sweep tx. +func (s *UtxoSweeper) collector(blockEpochs <-chan *chainntnfs.BlockEpoch, + bestHeight int32) error { + + for { + select { + // A new inputs is offered to the sweeper. We check to see if we + // are already trying to sweep this input and if not, set up a + // listener for spend and schedule a sweep. + case input := <-s.newInputs: + outpoint := *input.input.OutPoint() + pendInput, pending := s.pendingInputs[outpoint] + if pending { + log.Debugf("Already pending input %v received", + outpoint) + + // Add additional result channel to signal + // spend of this input. + pendInput.listeners = append( + pendInput.listeners, input.resultChan, + ) + continue + } + + // Create a new pendingInput and initialize the + // listeners slice with the passed in result channel. If + // this input is offered for sweep again, the result + // channel will be appended to this slice. + pendInput = &pendingInput{ + listeners: []chan Result{input.resultChan}, + input: input.input, + minPublishHeight: bestHeight, + } + s.pendingInputs[outpoint] = pendInput + + // Start watching for spend of this input, either by us + // or the remote party. + cancel, err := s.waitForSpend( + outpoint, + input.input.SignDesc().Output.PkScript, + input.input.HeightHint(), + ) + if err != nil { + err := fmt.Errorf("wait for spend: %v", err) + s.signalAndRemove(&outpoint, Result{Err: err}) + continue + } + pendInput.ntfnRegCancel = cancel + + // Check to see if with this new input a sweep tx can be + // formed. + if err := s.scheduleSweep(bestHeight); err != nil { + log.Errorf("schedule sweep: %v", err) + } + + // A spend of one of our inputs is detected. Signal sweep + // results to the caller(s). + case spend := <-s.spendChan: + // For testing purposes. + if s.testSpendChan != nil { + s.testSpendChan <- *spend.SpentOutPoint + } + + // Query store to find out if we every published this + // tx. + spendHash := *spend.SpenderTxHash + isOurTx, err := s.cfg.Store.IsOurTx(spendHash) + if err != nil { + log.Errorf("cannot determine if tx %v "+ + "is ours: %v", spendHash, err, + ) + continue + } + + log.Debugf("Detected spend related to in flight inputs "+ + "(is_ours=%v): %v", + newLogClosure(func() string { + return spew.Sdump(spend.SpendingTx) + }), isOurTx, + ) + + // Signal sweep results for inputs in this confirmed + // tx. + for _, txIn := range spend.SpendingTx.TxIn { + outpoint := txIn.PreviousOutPoint + + // Check if this input is known to us. It could + // probably be unknown if we canceled the + // registration, deleted from pendingInputs but + // the ntfn was in-flight already. Or this could + // be not one of our inputs. + _, ok := s.pendingInputs[outpoint] + if !ok { + continue + } + + // Return either a nil or a remote spend result. + var err error + if !isOurTx { + err = ErrRemoteSpend + } + + // Signal result channels. + s.signalAndRemove(&outpoint, Result{ + Tx: spend.SpendingTx, + Err: err, + }) + } + + // Now that an input of ours is spent, we can try to + // resweep the remaining inputs. + if err := s.scheduleSweep(bestHeight); err != nil { + log.Errorf("schedule sweep: %v", err) + } + + // The timer expires and we are going to (re)sweep. + case <-s.timer: + log.Debugf("Sweep timer expired") + + // Set timer to nil so we know that a new timer needs to + // be started when new inputs arrive. + s.timer = nil + + // Retrieve fee estimate for input filtering and final + // tx fee calculation. + satPerKW, err := s.cfg.Estimator.EstimateFeePerKW( + s.cfg.SweepTxConfTarget, + ) + if err != nil { + log.Errorf("estimate fee: %v", err) + continue + } + + // Examine pending inputs and try to construct lists of + // inputs. + inputLists, err := s.getInputLists(bestHeight, satPerKW) + if err != nil { + log.Errorf("get input lists: %v", err) + continue + } + + // Sweep selected inputs. + for _, inputs := range inputLists { + err := s.sweep(inputs, satPerKW, bestHeight) + if err != nil { + log.Errorf("sweep: %v", err) + } + } + + // A new block comes in. Things may have changed, so we retry a + // sweep. + case epoch, ok := <-blockEpochs: + if !ok { + return nil + } + + bestHeight = epoch.Height + + log.Debugf("New blocks: height=%v, sha=%v", + epoch.Height, epoch.Hash) + + if err := s.scheduleSweep(bestHeight); err != nil { + log.Errorf("schedule sweep: %v", err) + } + + case <-s.quit: + return nil } + } +} + +// scheduleSweep starts the sweep timer to create an opportunity for more inputs +// to be added. +func (s *UtxoSweeper) scheduleSweep(currentHeight int32) error { + // The timer is already ticking, no action needed for the sweep to + // happen. + if s.timer != nil { + log.Debugf("Timer still ticking") + return nil + } + + // Retrieve fee estimate for input filtering and final tx fee + // calculation. + satPerKW, err := s.cfg.Estimator.EstimateFeePerKW( + s.cfg.SweepTxConfTarget, + ) + if err != nil { + return fmt.Errorf("estimate fee: %v", err) + } + + // Examine pending inputs and try to construct lists of inputs. + inputLists, err := s.getInputLists(currentHeight, satPerKW) + if err != nil { + return fmt.Errorf("get input lists: %v", err) + } - sweepTx.TxIn[idx].Witness = witness + log.Infof("Sweep candidates at height=%v, yield %v distinct txns", + currentHeight, len(inputLists)) + // If there are no input sets, there is nothing sweepable and we can + // return without starting the timer. + if len(inputLists) == 0 { return nil } - // Finally we'll attach a valid witness to each csv and cltv input - // within the sweeping transaction. - for i, input := range inputs { - if err := addWitness(i, input); err != nil { - return nil, err - } + // Start sweep timer to create opportunity for more inputs to be added + // before a tx is constructed. + s.timer = s.cfg.NewBatchTimer() + + log.Debugf("Sweep timer started") + + return nil +} + +// signalAndRemove notifies the listeners of the final result of the input +// sweep. It cancels any pending spend notification and removes the input from +// the list of pending inputs. When this function returns, the sweeper has +// completely forgotten about the input. +func (s *UtxoSweeper) signalAndRemove(outpoint *wire.OutPoint, result Result) { + pendInput := s.pendingInputs[*outpoint] + listeners := pendInput.listeners + + if result.Err == nil { + log.Debugf("Dispatching sweep success for %v to %v listeners", + outpoint, len(listeners), + ) + } else { + log.Debugf("Dispatching sweep error for %v to %v listeners: %v", + outpoint, len(listeners), result.Err, + ) } - return sweepTx, nil + // Signal all listeners. Channel is buffered. Because we only send once + // on every channel, it should never block. + for _, resultChan := range listeners { + resultChan <- result + } + + // Cancel spend notification with chain notifier. This is not necessary + // in case of a success, except for that a reorg could still happen. + if pendInput.ntfnRegCancel != nil { + log.Debugf("Canceling spend ntfn for %v", outpoint) + + pendInput.ntfnRegCancel() + } + + // Inputs are no longer pending after result has been sent. + delete(s.pendingInputs, *outpoint) } -// getWeightEstimate returns a weight estimate for the given inputs. -// Additionally, it returns counts for the number of csv and cltv inputs. -func (s *UtxoSweeper) getWeightEstimate(inputs []Input) ([]Input, int64, int, int) { - // We initialize a weight estimator so we can accurately asses the - // amount of fees we need to pay for this sweep transaction. +// getInputLists goes through all pending inputs and constructs sweep lists, +// each up to the configured maximum number of inputs. Negative yield inputs are +// skipped. Transactions with an output below the dust limit are not published. +// Those inputs remain pending and will be bundled with future inputs if +// possible. +func (s *UtxoSweeper) getInputLists(currentHeight int32, + satPerKW lnwallet.SatPerKWeight) ([]inputSet, error) { + + // Filter for inputs that need to be swept. Create two lists: all + // sweepable inputs and a list containing only the new, never tried + // inputs. // - // TODO(roasbeef): can be more intelligent about buffering outputs to - // be more efficient on-chain. - var weightEstimate lnwallet.TxWeightEstimator - - // Our sweep transaction will pay to a single segwit p2wkh address, - // ensure it contributes to our weight estimate. - weightEstimate.AddP2WKHOutput() - - // For each output, use its witness type to determine the estimate - // weight of its witness, and add it to the proper set of spendable - // outputs. - var ( - sweepInputs []Input - csvCount, cltvCount int + // We want to create as large a tx as possible, so we return a final set + // list that starts with sets created from all inputs. However, there is + // a chance that those txes will not publish, because they already + // contain inputs that failed before. Therefore we also add sets + // consisting of only new inputs to the list, to make sure that new + // inputs are given a good, isolated chance of being published. + var newInputs, retryInputs []Input + for _, input := range s.pendingInputs { + // Skip inputs that have a minimum publish height that is not + // yet reached. + if input.minPublishHeight > currentHeight { + continue + } + + // Add input to the either one of the lists. + if input.publishAttempts == 0 { + newInputs = append(newInputs, input.input) + } else { + retryInputs = append(retryInputs, input.input) + } + } + + // If there is anything to retry, combine it with the new inputs and + // form input sets. + var allSets []inputSet + if len(retryInputs) > 0 { + var err error + allSets, err = generateInputPartitionings( + append(retryInputs, newInputs...), + s.relayFeePerKW, satPerKW, + s.cfg.MaxInputsPerTx, + ) + if err != nil { + return nil, fmt.Errorf("input partitionings: %v", err) + } + } + + // Create sets for just the new inputs. + newSets, err := generateInputPartitionings( + newInputs, + s.relayFeePerKW, satPerKW, + s.cfg.MaxInputsPerTx, ) - for i := range inputs { - input := inputs[i] - - switch input.WitnessType() { - - // Outputs on a remote commitment transaction that pay directly - // to us. - case lnwallet.CommitmentNoDelay: - weightEstimate.AddP2WKHInput() - sweepInputs = append(sweepInputs, input) - - // Outputs on a past commitment transaction that pay directly - // to us. - case lnwallet.CommitmentTimeLock: - weightEstimate.AddWitnessInput( - lnwallet.ToLocalTimeoutWitnessSize, - ) - sweepInputs = append(sweepInputs, input) - csvCount++ + if err != nil { + return nil, fmt.Errorf("input partitionings: %v", err) + } - // Outgoing second layer HTLC's that have confirmed within the - // chain, and the output they produced is now mature enough to - // sweep. - case lnwallet.HtlcOfferedTimeoutSecondLevel: - weightEstimate.AddWitnessInput( - lnwallet.ToLocalTimeoutWitnessSize, - ) - sweepInputs = append(sweepInputs, input) - csvCount++ + log.Debugf("Sweep candidates at height=%v: total_num_pending=%v, "+ + "total_num_new=%v", currentHeight, len(allSets), len(newSets)) - // Incoming second layer HTLC's that have confirmed within the - // chain, and the output they produced is now mature enough to - // sweep. - case lnwallet.HtlcAcceptedSuccessSecondLevel: - weightEstimate.AddWitnessInput( - lnwallet.ToLocalTimeoutWitnessSize, - ) - sweepInputs = append(sweepInputs, input) - csvCount++ - - // An HTLC on the commitment transaction of the remote party, - // that has had its absolute timelock expire. - case lnwallet.HtlcOfferedRemoteTimeout: - weightEstimate.AddWitnessInput( - lnwallet.AcceptedHtlcTimeoutWitnessSize, - ) - sweepInputs = append(sweepInputs, input) - cltvCount++ - - // An HTLC on the commitment transaction of the remote party, - // that can be swept with the preimage. - case lnwallet.HtlcAcceptedRemoteSuccess: - weightEstimate.AddWitnessInput( - lnwallet.OfferedHtlcSuccessWitnessSize, - ) - sweepInputs = append(sweepInputs, input) + // Append the new sets at the end of the list, because those tx likely + // have a higher fee per input. + return append(allSets, newSets...), nil +} + +// sweep takes a set of preselected inputs, creates a sweep tx and publishes the +// tx. The output address is only marked as used if the publish succeeds. +func (s *UtxoSweeper) sweep(inputs inputSet, + satPerKW lnwallet.SatPerKWeight, currentHeight int32) error { + + var err error + + // Generate output script if no unused script available. + if s.currentOutputScript == nil { + s.currentOutputScript, err = s.cfg.GenSweepScript() + if err != nil { + return fmt.Errorf("gen sweep script: %v", err) + } + } + + // Create sweep tx. + tx, err := createSweepTx( + inputs, s.currentOutputScript, + uint32(currentHeight), satPerKW, s.cfg.Signer, + ) + if err != nil { + return fmt.Errorf("create sweep tx: %v", err) + } + + // Add tx before publication, so that we will always know that a spend + // by this tx is ours. Otherwise if the publish doesn't return, but did + // publish, we loose track of this tx. Even republication on startup + // doesn't prevent this, because that call returns a double spend error + // then and would also not add the hash to the store. + err = s.cfg.Store.NotifyPublishTx(tx) + if err != nil { + return fmt.Errorf("notify publish tx: %v", err) + } + + // Publish sweep tx. + log.Debugf("Publishing sweep tx %v, num_inputs=%v, height=%v", + tx.TxHash(), len(tx.TxIn), currentHeight) + + log.Tracef("Sweep tx at height=%v: %v", currentHeight, + newLogClosure(func() string { + return spew.Sdump(tx) + }), + ) + + err = s.cfg.PublishTransaction(tx) + + // In case of an unexpected error, don't try to recover. + if err != nil && err != lnwallet.ErrDoubleSpend { + return fmt.Errorf("publish tx: %v", err) + } + + // Keep outputScript in case of an error, so that it can be reused for + // the next tx and causes no address inflation. + if err == nil { + s.currentOutputScript = nil + } + + // Reschedule sweep. + for _, input := range tx.TxIn { + pi, ok := s.pendingInputs[input.PreviousOutPoint] + if !ok { + // It can be that the input has been removed because it + // exceed the maximum number of attempts in a previous + // input set. + continue + } + + // Record another publish attempt. + pi.publishAttempts++ + + // We don't care what the result of the publish call was. Even + // if it is published successfully, it can still be that it + // needs to be retried. Call NextAttemptDeltaFunc to calculate + // when to resweep this input. + nextAttemptDelta := s.cfg.NextAttemptDeltaFunc( + pi.publishAttempts, + ) + + pi.minPublishHeight = currentHeight + nextAttemptDelta + + log.Debugf("Rescheduling input %v after %v attempts at "+ + "height %v (delta %v)", input.PreviousOutPoint, + pi.publishAttempts, pi.minPublishHeight, + nextAttemptDelta) + + if pi.publishAttempts >= s.cfg.MaxSweepAttempts { + // Signal result channels sweep result. + s.signalAndRemove(&input.PreviousOutPoint, Result{ + Err: ErrTooManyAttempts, + }) + } + } + return nil +} + +// waitForSpend registers a spend notification with the chain notifier. It +// returns a cancel function that can be used to cancel the registration. +func (s *UtxoSweeper) waitForSpend(outpoint wire.OutPoint, + script []byte, heightHint uint32) (func(), error) { - default: - log.Warnf("kindergarten output in nursery store "+ - "contains unexpected witness type: %v", - input.WitnessType()) + log.Debugf("Wait for spend of %v", outpoint) + + spendEvent, err := s.cfg.Notifier.RegisterSpendNtfn( + &outpoint, script, heightHint, + ) + if err != nil { + return nil, fmt.Errorf("register spend ntfn: %v", err) + } + + s.wg.Add(1) + go func() { + defer s.wg.Done() + select { + case spend, ok := <-spendEvent.Spend: + if !ok { + log.Debugf("Spend ntfn for %v canceled", + outpoint) + return + } + + log.Debugf("Delivering spend ntfn for %v", + outpoint) + select { + case s.spendChan <- spend: + log.Debugf("Delivered spend ntfn for %v", + outpoint) + + case <-s.quit: + } + case <-s.quit: } + }() + + return spendEvent.Cancel, nil +} + +// CreateSweepTx accepts a list of inputs and signs and generates a txn that +// spends from them. This method also makes an accurate fee estimate before +// generating the required witnesses. +// +// The created transaction has a single output sending all the funds back to +// the source wallet, after accounting for the fee estimate. +// +// The value of currentBlockHeight argument will be set as the tx locktime. +// This function assumes that all CLTV inputs will be unlocked after +// currentBlockHeight. Reasons not to use the maximum of all actual CLTV expiry +// values of the inputs: +// +// - Make handling re-orgs easier. +// - Thwart future possible fee sniping attempts. +// - Make us blend in with the bitcoind wallet. +func (s *UtxoSweeper) CreateSweepTx(inputs []Input, confTarget uint32, + currentBlockHeight uint32) (*wire.MsgTx, error) { + + feePerKw, err := s.cfg.Estimator.EstimateFeePerKW(confTarget) + if err != nil { + return nil, err } - txWeight := int64(weightEstimate.Weight()) + // Generate the receiving script to which the funds will be swept. + pkScript, err := s.cfg.GenSweepScript() + if err != nil { + return nil, err + } + + return createSweepTx( + inputs, pkScript, currentBlockHeight, feePerKw, s.cfg.Signer, + ) +} + +// DefaultNextAttemptDeltaFunc is the default calculation for next sweep attempt +// scheduling. It implements exponential back-off with some randomness. This is +// to prevent a stuck tx (for example because fee is too low and can't be bumped +// in btcd) from blocking all other retried inputs in the same tx. +func DefaultNextAttemptDeltaFunc(attempts int) int32 { + return 1 + rand.Int31n(1<= height { + continue + } + + log.Debugf("Notifying height %v to listener", height) + + select { + case epochChan <- &chainntnfs.BlockEpoch{ + Height: height, + }: + case <-time.After(defaultTestTimeout): + pprof.Lookup("goroutine").WriteTo(os.Stdout, 1) + + m.t.Fatal("epoch event not consumed") + } + } +} + +// ConfirmTx simulates a tx confirming. +func (m *MockNotifier) ConfirmTx(txid *chainhash.Hash, height uint32) error { + confirm := &chainntnfs.TxConfirmation{ + BlockHeight: height, + } + select { + case m.getConfChannel(txid) <- confirm: + case <-time.After(defaultTestTimeout): + return fmt.Errorf("confirmation not consumed") + } + return nil +} + +// SpendOutpoint simulates a utxo being spent. +func (m *MockNotifier) SpendOutpoint(outpoint wire.OutPoint, + spendingTx wire.MsgTx) { + + log.Debugf("Spending outpoint %v", outpoint) + + m.mutex.Lock() + defer m.mutex.Unlock() + + channels, ok := m.spendChan[outpoint] + if ok { + for _, channel := range channels { + m.sendSpend(channel, &outpoint, &spendingTx) + } + } + + m.spends[outpoint] = &spendingTx +} + +func (m *MockNotifier) sendSpend(channel chan *chainntnfs.SpendDetail, + outpoint *wire.OutPoint, + spendingTx *wire.MsgTx) { + + spenderTxHash := spendingTx.TxHash() + channel <- &chainntnfs.SpendDetail{ + SpenderTxHash: &spenderTxHash, + SpendingTx: spendingTx, + SpentOutPoint: outpoint, + } +} + +// RegisterConfirmationsNtfn registers for tx confirm notifications. +func (m *MockNotifier) RegisterConfirmationsNtfn(txid *chainhash.Hash, + _ []byte, numConfs, heightHint uint32) (*chainntnfs.ConfirmationEvent, + error) { + + return &chainntnfs.ConfirmationEvent{ + Confirmed: m.getConfChannel(txid), + }, nil +} + +func (m *MockNotifier) getConfChannel( + txid *chainhash.Hash) chan *chainntnfs.TxConfirmation { + + m.mutex.Lock() + defer m.mutex.Unlock() + + channel, ok := m.confChannel[*txid] + if ok { + return channel + } + channel = make(chan *chainntnfs.TxConfirmation) + m.confChannel[*txid] = channel + + return channel +} + +// RegisterBlockEpochNtfn registers a block notification. +func (m *MockNotifier) RegisterBlockEpochNtfn( + bestBlock *chainntnfs.BlockEpoch) (*chainntnfs.BlockEpochEvent, error) { + + log.Tracef("Mock block ntfn registered") + + m.mutex.Lock() + epochChan := make(chan *chainntnfs.BlockEpoch, 0) + bestHeight := int32(0) + if bestBlock != nil { + bestHeight = bestBlock.Height + } + m.epochChan[epochChan] = bestHeight + m.mutex.Unlock() + + return &chainntnfs.BlockEpochEvent{ + Epochs: epochChan, + Cancel: func() { + log.Tracef("Mock block ntfn cancelled") + m.mutex.Lock() + delete(m.epochChan, epochChan) + m.mutex.Unlock() + }, + }, nil +} + +// Start the notifier. +func (m *MockNotifier) Start() error { + return nil +} + +// Stop the notifier. +func (m *MockNotifier) Stop() error { + return nil +} + +// RegisterSpendNtfn registers for spend notifications. +func (m *MockNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint, + _ []byte, heightHint uint32) (*chainntnfs.SpendEvent, error) { + + // Add channel to global spend ntfn map. + m.mutex.Lock() + + channels, ok := m.spendChan[*outpoint] + if !ok { + channels = make([]chan *chainntnfs.SpendDetail, 0) + } + + channel := make(chan *chainntnfs.SpendDetail, 1) + channels = append(channels, channel) + m.spendChan[*outpoint] = channels + + // Check if this output has already been spent. + spendingTx, spent := m.spends[*outpoint] + + m.mutex.Unlock() + + // If output has been spent already, signal now. Do this outside the + // lock to prevent a dead lock. + if spent { + m.sendSpend(channel, outpoint, spendingTx) + } + + return &chainntnfs.SpendEvent{ + Spend: channel, + Cancel: func() { + log.Infof("Cancelling RegisterSpendNtfn for %v", + outpoint) + + m.mutex.Lock() + defer m.mutex.Unlock() + channels := m.spendChan[*outpoint] + for i, c := range channels { + if c == channel { + channels[i] = channels[len(channels)-1] + m.spendChan[*outpoint] = + channels[:len(channels)-1] + } + } + + close(channel) + + log.Infof("Spend ntfn channel closed for %v", + outpoint) + }, + }, nil +} + +type mockChainIO struct{} + +func (m *mockChainIO) GetBestBlock() (*chainhash.Hash, int32, error) { + return nil, mockChainIOHeight, nil +} + +func (m *mockChainIO) GetUtxo(op *wire.OutPoint, pkScript []byte, + heightHint uint32) (*wire.TxOut, error) { + + return nil, nil +} + +func (m *mockChainIO) GetBlockHash(blockHeight int64) (*chainhash.Hash, error) { + return nil, nil +} + +func (m *mockChainIO) GetBlock(blockHash *chainhash.Hash) (*wire.MsgBlock, error) { + return nil, nil +} diff --git a/sweep/txgenerator.go b/sweep/txgenerator.go new file mode 100644 index 0000000000..e173706a29 --- /dev/null +++ b/sweep/txgenerator.go @@ -0,0 +1,335 @@ +package sweep + +import ( + "fmt" + "sort" + + "github.com/btcsuite/btcd/blockchain" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcwallet/wallet/txrules" + "github.com/lightningnetwork/lnd/lnwallet" +) + +var ( + // DefaultMaxInputsPerTx specifies the default maximum number of inputs + // allowed in a single sweep tx. If more need to be swept, multiple txes + // are created and published. + DefaultMaxInputsPerTx = 100 +) + +// inputSet is a set of inputs that can be used as the basis to generate a tx +// on. +type inputSet []Input + +// generateInputPartitionings goes through all given inputs and constructs sets +// of inputs that can be used to generate a sensible transaction. Each set +// contains up to the configured maximum number of inputs. Negative yield +// inputs are skipped. No input sets with a total value after fees below the +// dust limit are returned. +func generateInputPartitionings(sweepableInputs []Input, + relayFeePerKW, feePerKW lnwallet.SatPerKWeight, + maxInputsPerTx int) ([]inputSet, error) { + + // Calculate dust limit based on the P2WPKH output script of the sweep + // txes. + dustLimit := txrules.GetDustThreshold( + lnwallet.P2WPKHSize, + btcutil.Amount(relayFeePerKW.FeePerKVByte()), + ) + + // Sort input by yield. We will start constructing input sets starting + // with the highest yield inputs. This is to prevent the construction + // of a set with an output below the dust limit, causing the sweep + // process to stop, while there are still higher value inputs + // available. It also allows us to stop evaluating more inputs when the + // first input in this ordering is encountered with a negative yield. + // + // Yield is calculated as the difference between value and added fee + // for this input. The fee calculation excludes fee components that are + // common to all inputs, as those wouldn't influence the order. The + // single component that is differentiating is witness size. + // + // For witness size, the upper limit is taken. The actual size depends + // on the signature length, which is not known yet at this point. + yields := make(map[wire.OutPoint]int64) + for _, input := range sweepableInputs { + size, err := getInputWitnessSizeUpperBound(input) + if err != nil { + return nil, fmt.Errorf( + "failed adding input weight: %v", err) + } + + yields[*input.OutPoint()] = input.SignDesc().Output.Value - + int64(feePerKW.FeeForWeight(int64(size))) + } + + sort.Slice(sweepableInputs, func(i, j int) bool { + return yields[*sweepableInputs[i].OutPoint()] > + yields[*sweepableInputs[j].OutPoint()] + }) + + // Select blocks of inputs up to the configured maximum number. + var sets []inputSet + for len(sweepableInputs) > 0 { + // Get the maximum number of inputs from sweepableInputs that + // we can use to create a positive yielding set from. + count, outputValue := getPositiveYieldInputs( + sweepableInputs, maxInputsPerTx, feePerKW, + ) + + // If there are no positive yield inputs left, we can stop + // here. + if count == 0 { + return sets, nil + } + + // If the output value of this block of inputs does not reach + // the dust limit, stop sweeping. Because of the sorting, + // continuing with the remaining inputs will only lead to sets + // with a even lower output value. + if outputValue < dustLimit { + log.Debugf("Set value %v below dust limit of %v", + outputValue, dustLimit) + return sets, nil + } + + log.Infof("Candidate sweep set of size=%v, has yield=%v", + count, outputValue) + + sets = append(sets, sweepableInputs[:count]) + sweepableInputs = sweepableInputs[count:] + } + + return sets, nil +} + +// getPositiveYieldInputs returns the maximum of a number n for which holds +// that the inputs [0,n) of sweepableInputs have a positive yield. +// Additionally, the total values of these inputs minus the fee is returned. +// +// TODO(roasbeef): Consider including some negative yield inputs too to clean +// up the utxo set even if it costs us some fees up front. In the spirit of +// minimizing any negative externalities we cause for the Bitcoin system as a +// whole. +func getPositiveYieldInputs(sweepableInputs []Input, maxInputs int, + feePerKW lnwallet.SatPerKWeight) (int, btcutil.Amount) { + + var weightEstimate lnwallet.TxWeightEstimator + + // Add the sweep tx output to the weight estimate. + weightEstimate.AddP2WKHOutput() + + var total, outputValue btcutil.Amount + for idx, input := range sweepableInputs { + // Can ignore error, because it has already been checked when + // calculating the yields. + size, _ := getInputWitnessSizeUpperBound(input) + + // Keep a running weight estimate of the input set. + weightEstimate.AddWitnessInput(size) + + newTotal := total + btcutil.Amount(input.SignDesc().Output.Value) + + weight := weightEstimate.Weight() + fee := feePerKW.FeeForWeight(int64(weight)) + + // Calculate the output value if the current input would be + // added to the set. + newOutputValue := newTotal - fee + + // If adding this input makes the total output value of the set + // decrease, this is a negative yield input. It shouldn't be + // added to the set. We return the current index as the number + // of inputs, so the current input is being excluded. + if newOutputValue <= outputValue { + return idx, outputValue + } + + // Update running values. + total = newTotal + outputValue = newOutputValue + + // Stop if max inputs is reached. + if idx == maxInputs-1 { + return maxInputs, outputValue + } + } + + // We could add all inputs to the set, so return them all. + return len(sweepableInputs), outputValue +} + +// createSweepTx builds a signed tx spending the inputs to a the output script. +func createSweepTx(inputs []Input, outputPkScript []byte, + currentBlockHeight uint32, feePerKw lnwallet.SatPerKWeight, + signer lnwallet.Signer) (*wire.MsgTx, error) { + + inputs, txWeight, csvCount, cltvCount := getWeightEstimate(inputs) + + log.Infof("Creating sweep transaction for %v inputs (%v CSV, %v CLTV) "+ + "using %v sat/kw", len(inputs), csvCount, cltvCount, + int64(feePerKw)) + + txFee := feePerKw.FeeForWeight(txWeight) + + // Sum up the total value contained in the inputs. + var totalSum btcutil.Amount + for _, o := range inputs { + totalSum += btcutil.Amount(o.SignDesc().Output.Value) + } + + // Sweep as much possible, after subtracting txn fees. + sweepAmt := int64(totalSum - txFee) + + // Create the sweep transaction that we will be building. We use + // version 2 as it is required for CSV. The txn will sweep the amount + // after fees to the pkscript generated above. + sweepTx := wire.NewMsgTx(2) + sweepTx.AddTxOut(&wire.TxOut{ + PkScript: outputPkScript, + Value: sweepAmt, + }) + + sweepTx.LockTime = currentBlockHeight + + // Add all inputs to the sweep transaction. Ensure that for each + // csvInput, we set the sequence number properly. + for _, input := range inputs { + sweepTx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: *input.OutPoint(), + Sequence: input.BlocksToMaturity(), + }) + } + + // Before signing the transaction, check to ensure that it meets some + // basic validity requirements. + // + // TODO(conner): add more control to sanity checks, allowing us to + // delay spending "problem" outputs, e.g. possibly batching with other + // classes if fees are too low. + btx := btcutil.NewTx(sweepTx) + if err := blockchain.CheckTransactionSanity(btx); err != nil { + return nil, err + } + + hashCache := txscript.NewTxSigHashes(sweepTx) + + // With all the inputs in place, use each output's unique witness + // function to generate the final witness required for spending. + addWitness := func(idx int, tso Input) error { + witness, err := tso.BuildWitness( + signer, sweepTx, hashCache, idx, + ) + if err != nil { + return err + } + + sweepTx.TxIn[idx].Witness = witness + + return nil + } + + // Finally we'll attach a valid witness to each csv and cltv input + // within the sweeping transaction. + for i, input := range inputs { + if err := addWitness(i, input); err != nil { + return nil, err + } + } + + return sweepTx, nil +} + +// getInputWitnessSizeUpperBound returns the maximum length of the witness for +// the given input if it would be included in a tx. +func getInputWitnessSizeUpperBound(input Input) (int, error) { + switch input.WitnessType() { + + // Outputs on a remote commitment transaction that pay directly + // to us. + case lnwallet.CommitmentNoDelay: + return lnwallet.P2WKHWitnessSize, nil + + // Outputs on a past commitment transaction that pay directly + // to us. + case lnwallet.CommitmentTimeLock: + return lnwallet.ToLocalTimeoutWitnessSize, nil + + // Outgoing second layer HTLC's that have confirmed within the + // chain, and the output they produced is now mature enough to + // sweep. + case lnwallet.HtlcOfferedTimeoutSecondLevel: + return lnwallet.ToLocalTimeoutWitnessSize, nil + + // Incoming second layer HTLC's that have confirmed within the + // chain, and the output they produced is now mature enough to + // sweep. + case lnwallet.HtlcAcceptedSuccessSecondLevel: + return lnwallet.ToLocalTimeoutWitnessSize, nil + + // An HTLC on the commitment transaction of the remote party, + // that has had its absolute timelock expire. + case lnwallet.HtlcOfferedRemoteTimeout: + return lnwallet.AcceptedHtlcTimeoutWitnessSize, nil + + // An HTLC on the commitment transaction of the remote party, + // that can be swept with the preimage. + case lnwallet.HtlcAcceptedRemoteSuccess: + return lnwallet.OfferedHtlcSuccessWitnessSize, nil + + } + + return 0, fmt.Errorf("unexpected witness type: %v", input.WitnessType()) +} + +// getWeightEstimate returns a weight estimate for the given inputs. +// Additionally, it returns counts for the number of csv and cltv inputs. +func getWeightEstimate(inputs []Input) ([]Input, int64, int, int) { + // We initialize a weight estimator so we can accurately asses the + // amount of fees we need to pay for this sweep transaction. + // + // TODO(roasbeef): can be more intelligent about buffering outputs to + // be more efficient on-chain. + var weightEstimate lnwallet.TxWeightEstimator + + // Our sweep transaction will pay to a single segwit p2wkh address, + // ensure it contributes to our weight estimate. + weightEstimate.AddP2WKHOutput() + + // For each output, use its witness type to determine the estimate + // weight of its witness, and add it to the proper set of spendable + // outputs. + var ( + sweepInputs []Input + csvCount, cltvCount int + ) + for i := range inputs { + input := inputs[i] + + size, err := getInputWitnessSizeUpperBound(input) + if err != nil { + log.Warn(err) + + // Skip inputs for which no weight estimate can be + // given. + continue + } + weightEstimate.AddWitnessInput(size) + + switch input.WitnessType() { + case lnwallet.CommitmentTimeLock, + lnwallet.HtlcOfferedTimeoutSecondLevel, + lnwallet.HtlcAcceptedSuccessSecondLevel: + csvCount++ + case lnwallet.HtlcOfferedRemoteTimeout: + cltvCount++ + } + sweepInputs = append(sweepInputs, input) + } + + txWeight := int64(weightEstimate.Weight()) + + return sweepInputs, txWeight, csvCount, cltvCount +} diff --git a/test_utils.go b/test_utils.go index 283efabcd9..eb6b3dc269 100644 --- a/test_utils.go +++ b/test_utils.go @@ -202,7 +202,7 @@ func createTestPeer(notifier chainntnfs.ChainNotifier, return nil, nil, nil, nil, err } - estimator := &lnwallet.StaticFeeEstimator{FeePerKW: 12500} + estimator := lnwallet.NewStaticFeeEstimator(12500, 0) feePerKw, err := estimator.EstimateFeePerKW(1) if err != nil { return nil, nil, nil, nil, err @@ -316,7 +316,9 @@ func createTestPeer(notifier chainntnfs.ChainNotifier, } bobPool.Start() - chainIO := &mockChainIO{} + chainIO := &mockChainIO{ + bestHeight: fundingBroadcastHeight, + } wallet := &lnwallet.LightningWallet{ WalletController: &mockWalletController{ rootKey: aliceKeyPriv, diff --git a/utxonursery.go b/utxonursery.go index 3f8cc2f34f..5285127e9e 100644 --- a/utxonursery.go +++ b/utxonursery.go @@ -72,17 +72,6 @@ import ( // the utxo nursery will sweep all KNDR outputs scheduled for that height // using a single txn. // -// NOTE: Due to the fact that KNDR outputs can be dynamically aggregated and -// swept, we make precautions to finalize the KNDR outputs at a particular -// height on our first attempt to sweep it. Finalizing involves signing the -// sweep transaction and persisting it in the nursery store, and recording -// the last finalized height. Any attempts to replay an already finalized -// height will result in broadcasting the already finalized txn, ensuring the -// nursery does not broadcast different txids for the same batch of KNDR -// outputs. The reason txids may change is due to the probabilistic nature of -// generating the pkscript in the sweep txn's output, even if the set of -// inputs remains static across attempts. -// // - GRAD (kidOutput) outputs are KNDR outputs that have successfully been // swept into the user's wallet. A channel is considered mature once all of // its outputs, including two-stage htlcs, have entered the GRAD state, @@ -183,10 +172,6 @@ type NurseryConfig struct { // determining outputs in the chain as confirmed. ConfDepth uint32 - // SweepTxConfTarget assigns a confirmation target for sweep txes on - // which the fee calculation will be based. - SweepTxConfTarget uint32 - // FetchClosedChannels provides access to a user's channels, such that // they can be marked fully closed after incubation has concluded. FetchClosedChannels func(pendingOnly bool) ( @@ -254,25 +239,22 @@ func (u *utxoNursery) Start() error { utxnLog.Tracef("Starting UTXO nursery") - // 1. Start watching for new blocks, as this will drive the nursery - // store's state machine. - - // Register with the notifier to receive notifications for each newly - // connected block. We register immediately on 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.cfg.Notifier.RegisterBlockEpochNtfn(nil) + // Retrieve the currently best known block. This is needed to have the + // state machine catch up with the blocks we missed when we were down. + bestHash, bestHeight, err := u.cfg.ChainIO.GetBestBlock() if err != nil { return err } + // Set best known height to schedule late registrations properly. + atomic.StoreUint32(&u.bestHeight, uint32(bestHeight)) + // 2. Flush all fully-graduated channels from the pipeline. // Load any pending close channels, which represents the super set of // all channels that may still be incubating. pendingCloseChans, err := u.cfg.FetchClosedChannels(true) if err != nil { - newBlockChan.Cancel() return err } @@ -281,7 +263,6 @@ func (u *utxoNursery) Start() error { for _, pendingClose := range pendingCloseChans { err := u.closeAndRemoveIfMature(&pendingClose.ChanPoint) if err != nil { - newBlockChan.Cancel() return err } } @@ -289,15 +270,6 @@ func (u *utxoNursery) Start() error { // TODO(conner): check if any fully closed channels can be removed from // utxn. - // Query the nursery store for the lowest block height we could be - // incubating, which is taken to be the last height for which the - // database was purged. - lastGraduatedHeight, err := u.cfg.Store.LastGraduatedHeight() - if err != nil { - newBlockChan.Cancel() - return err - } - // 2. Restart spend ntfns for any preschool outputs, which are waiting // for the force closed commitment txn to confirm, or any second-layer // HTLC success transactions. @@ -306,15 +278,24 @@ func (u *utxoNursery) Start() error { // point forward, we must close the nursery's quit channel if we detect // any failures during startup to ensure they terminate. if err := u.reloadPreschool(); err != nil { - newBlockChan.Cancel() close(u.quit) return err } - // 3. Replay all crib and kindergarten outputs from last pruned to - // current best height. - if err := u.reloadClasses(lastGraduatedHeight); err != nil { - newBlockChan.Cancel() + // 3. Replay all crib and kindergarten outputs up to the current best + // height. + if err := u.reloadClasses(uint32(bestHeight)); err != nil { + close(u.quit) + return err + } + + // Start watching for new blocks, as this will drive the nursery store's + // state machine. + newBlockChan, err := u.cfg.Notifier.RegisterBlockEpochNtfn(&chainntnfs.BlockEpoch{ + Height: bestHeight, + Hash: bestHash, + }) + if err != nil { close(u.quit) return err } @@ -672,123 +653,44 @@ func (u *utxoNursery) reloadPreschool() error { // reloadClasses reinitializes any height-dependent state transitions for which // the utxonursery has not received confirmation, and replays the graduation of -// all kindergarten and crib outputs for heights that have not been finalized. +// all kindergarten and crib outputs for all heights up to the current block. // This allows the nursery to reinitialize all state to continue sweeping -// outputs, even in the event that we missed blocks while offline. -// reloadClasses is called during the startup of the UTXO Nursery. -func (u *utxoNursery) reloadClasses(lastGradHeight uint32) error { - // Begin by loading all of the still-active heights up to and including - // the last height we successfully graduated. - activeHeights, err := u.cfg.Store.HeightsBelowOrEqual(lastGradHeight) +// outputs, even in the event that we missed blocks while offline. reloadClasses +// is called during the startup of the UTXO Nursery. +func (u *utxoNursery) reloadClasses(bestHeight uint32) error { + // Loading all active heights up to and including the current block. + activeHeights, err := u.cfg.Store.HeightsBelowOrEqual( + uint32(bestHeight)) if err != nil { return err } - if len(activeHeights) > 0 { - utxnLog.Infof("Re-registering confirmations for %d already "+ - "graduated heights below height=%d", len(activeHeights), - lastGradHeight) + // Return early if nothing to sweep. + if len(activeHeights) == 0 { + return nil } + utxnLog.Infof("(Re)-sweeping %d heights below height=%d", + len(activeHeights), bestHeight) + // Attempt to re-register notifications for any outputs still at these // heights. for _, classHeight := range activeHeights { - utxnLog.Debugf("Attempting to regraduate outputs at height=%v", + utxnLog.Debugf("Attempting to sweep outputs at height=%v", classHeight) - if err = u.regraduateClass(classHeight); err != nil { - utxnLog.Errorf("Failed to regraduate outputs at "+ + if err = u.graduateClass(classHeight); err != nil { + utxnLog.Errorf("Failed to sweep outputs at "+ "height=%v: %v", classHeight, err) return err } } - // Get the most recently mined block. - _, bestHeight, err := u.cfg.ChainIO.GetBestBlock() - if err != nil { - return err - } - - // If we haven't yet seen any registered force closes, or we're already - // caught up with the current best chain, then we can exit early. - if lastGradHeight == 0 || uint32(bestHeight) == lastGradHeight { - return nil - } - - utxnLog.Infof("Processing outputs from missed blocks. Starting with "+ - "blockHeight=%v, to current blockHeight=%v", lastGradHeight, - bestHeight) - - // Loop through and check for graduating outputs at each of the missed - // block heights. - for curHeight := lastGradHeight + 1; curHeight <= uint32(bestHeight); curHeight++ { - utxnLog.Debugf("Attempting to graduate outputs at height=%v", - curHeight) - - if err := u.graduateClass(curHeight); err != nil { - utxnLog.Errorf("Failed to graduate outputs at "+ - "height=%v: %v", curHeight, err) - return err - } - } - utxnLog.Infof("UTXO Nursery is now fully synced") return nil } -// regraduateClass handles the steps involved in re-registering for -// confirmations for all still-active outputs at a particular height. This is -// used during restarts to ensure that any still-pending state transitions are -// properly registered, so they can be driven by the chain notifier. No -// transactions or signing are done as a result of this step. -func (u *utxoNursery) regraduateClass(classHeight uint32) error { - // Fetch all information about the crib and kindergarten outputs at - // this height. In addition to the outputs, we also retrieve the - // finalized kindergarten sweep txn, which will be nil if we have not - // attempted this height before, or if no kindergarten outputs exist at - // this height. - finalTx, kgtnOutputs, cribOutputs, err := u.cfg.Store.FetchClass( - classHeight) - if err != nil { - return err - } - - if finalTx != nil { - utxnLog.Infof("Re-registering confirmation for kindergarten "+ - "sweep transaction at height=%d ", classHeight) - - err = u.sweepMatureOutputs(classHeight, finalTx, kgtnOutputs) - if err != nil { - utxnLog.Errorf("Failed to re-register for kindergarten "+ - "sweep transaction at height=%d: %v", - classHeight, err) - return err - } - } - - if len(cribOutputs) == 0 { - return nil - } - - utxnLog.Infof("Re-registering confirmation for first-stage HTLC "+ - "outputs at height=%d ", classHeight) - - // Now, we broadcast all pre-signed htlc txns from the crib outputs at - // this height. There is no need to finalize these txns, since the txid - // is predetermined when signed in the wallet. - for i := range cribOutputs { - err := u.sweepCribOutput(classHeight, &cribOutputs[i]) - if err != nil { - utxnLog.Errorf("Failed to re-register first-stage "+ - "HTLC output %v", cribOutputs[i].OutPoint()) - return err - } - } - - return nil -} - // incubator is tasked with driving all state transitions that are dependent on // the current height of the blockchain. As new blocks arrive, the incubator // will attempt spend outputs at the latest height. The asynchronous @@ -821,6 +723,11 @@ func (u *utxoNursery) incubator(newBlockChan *chainntnfs.BlockEpochEvent) { // as signing and broadcasting a sweep txn that spends // from all kindergarten outputs at this height. height := uint32(epoch.Height) + + // Update best known block height for late registrations + // to be scheduled properly. + atomic.StoreUint32(&u.bestHeight, height) + if err := u.graduateClass(height); err != nil { utxnLog.Errorf("error while graduating "+ "class at height=%d: %v", height, err) @@ -843,14 +750,9 @@ func (u *utxoNursery) graduateClass(classHeight uint32) error { u.mu.Lock() defer u.mu.Unlock() - u.bestHeight = classHeight - // Fetch all information about the crib and kindergarten outputs at - // this height. In addition to the outputs, we also retrieve the - // finalized kindergarten sweep txn, which will be nil if we have not - // attempted this height before, or if no kindergarten outputs exist at // this height. - finalTx, kgtnOutputs, cribOutputs, err := u.cfg.Store.FetchClass( + kgtnOutputs, cribOutputs, err := u.cfg.Store.FetchClass( classHeight) if err != nil { return err @@ -859,64 +761,11 @@ func (u *utxoNursery) graduateClass(classHeight uint32) error { utxnLog.Infof("Attempting to graduate height=%v: num_kids=%v, "+ "num_babies=%v", classHeight, len(kgtnOutputs), len(cribOutputs)) - // Load the last finalized height, so we can determine if the - // kindergarten sweep txn should be crafted. - lastFinalizedHeight, err := u.cfg.Store.LastFinalizedHeight() - if err != nil { - return err - } - - // If we haven't processed this height before, we finalize the - // graduating kindergarten outputs, by signing a sweep transaction that - // spends from them. This txn is persisted such that we never broadcast - // a different txn for the same height. This allows us to recover from - // failures, and watch for the correct txid. - if classHeight > lastFinalizedHeight { - // If this height has never been finalized, we have never - // generated a sweep txn for this height. Generate one if there - // are kindergarten outputs or cltv crib outputs to be spent. - if len(kgtnOutputs) > 0 { - sweepInputs := make([]sweep.Input, len(kgtnOutputs)) - for i := range kgtnOutputs { - sweepInputs[i] = &kgtnOutputs[i] - } - - finalTx, err = u.cfg.Sweeper.CreateSweepTx( - sweepInputs, u.cfg.SweepTxConfTarget, - classHeight, - ) - if err != nil { - utxnLog.Errorf("Failed to create sweep txn at "+ - "height=%d", classHeight) - return err - } - } - - // Persist the kindergarten sweep txn to the nursery store. It - // is safe to store a nil finalTx, which happens if there are - // no graduating kindergarten outputs. - err = u.cfg.Store.FinalizeKinder(classHeight, finalTx) - if err != nil { - utxnLog.Errorf("Failed to finalize kindergarten at "+ - "height=%d", classHeight) - - return err - } - - // Log if the finalized transaction is non-trivial. - if finalTx != nil { - utxnLog.Infof("Finalized kindergarten at height=%d ", - classHeight) - } - } - - // Now that the kindergarten sweep txn has either been finalized or - // restored, broadcast the txn, and set up notifications that will - // transition the swept kindergarten outputs and cltvCrib into - // graduated outputs. - if finalTx != nil { - err := u.sweepMatureOutputs(classHeight, finalTx, kgtnOutputs) - if err != nil { + // Offer the outputs to the sweeper and set up notifications that will + // transition the swept kindergarten outputs and cltvCrib into graduated + // outputs. + if len(kgtnOutputs) > 0 { + if err := u.sweepMatureOutputs(classHeight, kgtnOutputs); err != nil { utxnLog.Errorf("Failed to sweep %d kindergarten "+ "outputs at height=%d: %v", len(kgtnOutputs), classHeight, err) @@ -925,8 +774,7 @@ func (u *utxoNursery) graduateClass(classHeight uint32) error { } // Now, we broadcast all pre-signed htlc txns from the csv crib outputs - // at this height. There is no need to finalize these txns, since the - // txid is predetermined when signed in the wallet. + // at this height. for i := range cribOutputs { err := u.sweepCribOutput(classHeight, &cribOutputs[i]) if err != nil { @@ -937,62 +785,32 @@ func (u *utxoNursery) graduateClass(classHeight uint32) error { } } - return u.cfg.Store.GraduateHeight(classHeight) + return nil } // sweepMatureOutputs generates and broadcasts the transaction that transfers // control of funds from a prior channel commitment transaction to the user's // wallet. The outputs swept were previously time locked (either absolute or // relative), but are not mature enough to sweep into the wallet. -func (u *utxoNursery) sweepMatureOutputs(classHeight uint32, finalTx *wire.MsgTx, +func (u *utxoNursery) sweepMatureOutputs(classHeight uint32, kgtnOutputs []kidOutput) error { - utxnLog.Infof("Sweeping %v CSV-delayed outputs with sweep tx "+ - "(txid=%v): %v", len(kgtnOutputs), - finalTx.TxHash(), newLogClosure(func() string { - return spew.Sdump(finalTx) - }), - ) - - // With the sweep transaction fully signed, broadcast the transaction - // to the network. Additionally, we can stop tracking these outputs as - // they've just been swept. - err := u.cfg.PublishTransaction(finalTx) - if err != nil && err != lnwallet.ErrDoubleSpend { - utxnLog.Errorf("unable to broadcast sweep tx: %v, %v", - err, spew.Sdump(finalTx)) - return err - } - - return u.registerSweepConf(finalTx, kgtnOutputs, classHeight) -} - -// registerSweepConf is responsible for registering a finalized kindergarten -// sweep transaction for confirmation notifications. If the confirmation was -// successfully registered, a goroutine will be spawned that waits for the -// confirmation, and graduates the provided kindergarten class within the -// nursery store. -func (u *utxoNursery) registerSweepConf(finalTx *wire.MsgTx, - kgtnOutputs []kidOutput, heightHint uint32) error { + utxnLog.Infof("Sweeping %v CSV-delayed outputs with sweep tx for "+ + "height %v", len(kgtnOutputs), classHeight) - finalTxID := finalTx.TxHash() + for _, output := range kgtnOutputs { + // Create local copy to prevent pointer to loop variable to be + // passed in with disastruous consequences. + local := output - confChan, err := u.cfg.Notifier.RegisterConfirmationsNtfn( - &finalTxID, finalTx.TxOut[0].PkScript, u.cfg.ConfDepth, - heightHint, - ) - if err != nil { - utxnLog.Errorf("unable to register notification for "+ - "sweep confirmation: %v", finalTxID) - return err + resultChan, err := u.cfg.Sweeper.SweepInput(&local) + if err != nil { + return err + } + u.wg.Add(1) + go u.waitForSweepConf(classHeight, &output, resultChan) } - utxnLog.Infof("Registering sweep tx %v for confs at height=%d", - finalTxID, heightHint) - - u.wg.Add(1) - go u.waitForSweepConf(heightHint, kgtnOutputs, confChan) - return nil } @@ -1002,16 +820,30 @@ func (u *utxoNursery) registerSweepConf(finalTx *wire.MsgTx, // to mark any mature channels as fully closed in channeldb. // NOTE(conner): this method MUST be called as a go routine. func (u *utxoNursery) waitForSweepConf(classHeight uint32, - kgtnOutputs []kidOutput, confChan *chainntnfs.ConfirmationEvent) { + output *kidOutput, resultChan chan sweep.Result) { defer u.wg.Done() select { - case _, ok := <-confChan.Confirmed: + case result, ok := <-resultChan: if !ok { - utxnLog.Errorf("Notification chan closed, can't"+ - " advance %v graduating outputs", - len(kgtnOutputs)) + utxnLog.Errorf("Notification chan closed, can't" + + " advance graduating output") + return + } + + // In case of a remote spend, still graduate the output. There + // is no way to sweep it anymore. + if result.Err == sweep.ErrRemoteSpend { + utxnLog.Infof("Output %v was spend by remote party", + output.OutPoint()) + break + } + + if result.Err != nil { + utxnLog.Errorf("Failed to sweep %v at "+ + "height=%d", output.OutPoint(), + classHeight) return } @@ -1024,32 +856,23 @@ func (u *utxoNursery) waitForSweepConf(classHeight uint32, // TODO(conner): add retry logic? - // Mark the confirmed kindergarten outputs as graduated. - if err := u.cfg.Store.GraduateKinder(classHeight); err != nil { - utxnLog.Errorf("Unable to graduate %v kindergarten outputs: "+ - "%v", len(kgtnOutputs), err) + // Mark the confirmed kindergarten output as graduated. + if err := u.cfg.Store.GraduateKinder(classHeight, output); err != nil { + utxnLog.Errorf("Unable to graduate kindergarten output %v: %v", + output.OutPoint(), err) return } - utxnLog.Infof("Graduated %d kindergarten outputs from height=%d", - len(kgtnOutputs), classHeight) - - // Iterate over the kid outputs and construct a set of all channel - // points to which they belong. - var possibleCloses = make(map[wire.OutPoint]struct{}) - for _, kid := range kgtnOutputs { - possibleCloses[*kid.OriginChanPoint()] = struct{}{} - - } + utxnLog.Infof("Graduated kindergarten output from height=%d", + classHeight) - // Attempt to close each channel, only doing so if all of the channel's + // Attempt to close the channel, only doing so if all of the channel's // outputs have been graduated. - for chanPoint := range possibleCloses { - if err := u.closeAndRemoveIfMature(&chanPoint); err != nil { - utxnLog.Errorf("Failed to close and remove channel %v", - chanPoint) - return - } + chanPoint := output.OriginChanPoint() + if err := u.closeAndRemoveIfMature(chanPoint); err != nil { + utxnLog.Errorf("Failed to close and remove channel %v", + *chanPoint) + return } } @@ -1216,7 +1039,8 @@ func (u *utxoNursery) waitForPreschoolConf(kid *kidOutput, outputType = "Commitment" } - err := u.cfg.Store.PreschoolToKinder(kid) + bestHeight := atomic.LoadUint32(&u.bestHeight) + err := u.cfg.Store.PreschoolToKinder(kid, bestHeight) if err != nil { utxnLog.Errorf("Unable to move %v output "+ "from preschool to kindergarten bucket: %v", @@ -1554,8 +1378,6 @@ type kidOutput struct { // NOTE: This will only be set for: outgoing HTLC's on the commitment // transaction of the remote party. absoluteMaturity uint32 - - confHeight uint32 } func makeKidOutput(outpoint, originChanPoint *wire.OutPoint, @@ -1569,9 +1391,14 @@ func makeKidOutput(outpoint, originChanPoint *wire.OutPoint, isHtlc := (witnessType == lnwallet.HtlcAcceptedSuccessSecondLevel || witnessType == lnwallet.HtlcOfferedRemoteTimeout) + // heightHint can be safely set to zero here, because after this + // function returns, nursery will set a proper confirmation height in + // waitForTimeoutConf or waitForPreschoolConf. + heightHint := uint32(0) + return kidOutput{ breachedOutput: makeBreachedOutput( - outpoint, witnessType, nil, signDescriptor, + outpoint, witnessType, nil, signDescriptor, heightHint, ), isHtlc: isHtlc, originChanPoint: *originChanPoint, diff --git a/utxonursery_test.go b/utxonursery_test.go index db0ebd9a33..1584edb4dc 100644 --- a/utxonursery_test.go +++ b/utxonursery_test.go @@ -9,8 +9,9 @@ import ( "github.com/lightningnetwork/lnd/sweep" "io/ioutil" "math" + "os" "reflect" - "sync" + "runtime/pprof" "testing" "time" @@ -19,7 +20,6 @@ import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" - "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/lnwallet" ) @@ -206,10 +206,10 @@ var ( amt: btcutil.Amount(13e7), outpoint: outPoints[1], witnessType: lnwallet.CommitmentTimeLock, + confHeight: uint32(1000), }, originChanPoint: outPoints[0], blocksToMaturity: uint32(42), - confHeight: uint32(1000), }, { @@ -217,10 +217,10 @@ var ( amt: btcutil.Amount(24e7), outpoint: outPoints[2], witnessType: lnwallet.CommitmentTimeLock, + confHeight: uint32(1000), }, originChanPoint: outPoints[0], blocksToMaturity: uint32(42), - confHeight: uint32(1000), }, { @@ -228,10 +228,10 @@ var ( amt: btcutil.Amount(2e5), outpoint: outPoints[3], witnessType: lnwallet.CommitmentTimeLock, + confHeight: uint32(500), }, originChanPoint: outPoints[0], blocksToMaturity: uint32(28), - confHeight: uint32(500), }, { @@ -239,10 +239,10 @@ var ( amt: btcutil.Amount(10e6), outpoint: outPoints[4], witnessType: lnwallet.CommitmentTimeLock, + confHeight: uint32(500), }, originChanPoint: outPoints[0], blocksToMaturity: uint32(28), - confHeight: uint32(500), }, } @@ -396,11 +396,14 @@ func TestBabyOutputSerialization(t *testing.T) { type nurseryTestContext struct { nursery *utxoNursery - notifier *nurseryMockNotifier + notifier *sweep.MockNotifier + chainIO *mockChainIO publishChan chan wire.MsgTx store *nurseryStoreInterceptor restart func() bool receiveTx func() wire.MsgTx + sweeper *sweep.UtxoSweeper + timeoutChan chan chan time.Time t *testing.T } @@ -430,17 +433,50 @@ func createNurseryTestContext(t *testing.T, // test. storeIntercepter := newNurseryStoreInterceptor(store) - notifier := newNurseryMockNotifier(t) + notifier := sweep.NewMockNotifier(t) - sweeper := sweep.New(&sweep.UtxoSweeperConfig{ + publishChan := make(chan wire.MsgTx, 1) + publishFunc := func(tx *wire.MsgTx, source string) error { + utxnLog.Tracef("Publishing tx %v by %v", tx.TxHash(), source) + publishChan <- *tx + return nil + } + + timeoutChan := make(chan chan time.Time) + + chainIO := &mockChainIO{ + bestHeight: 0, + } + + sweeperStore := sweep.NewMockSweeperStore() + + sweeperCfg := &sweep.UtxoSweeperConfig{ GenSweepScript: func() ([]byte, error) { return []byte{}, nil }, Estimator: &lnwallet.StaticFeeEstimator{}, Signer: &nurseryMockSigner{}, - }) + Notifier: notifier, + PublishTransaction: func(tx *wire.MsgTx) error { + return publishFunc(tx, "sweeper") + }, + NewBatchTimer: func() <-chan time.Time { + c := make(chan time.Time, 1) + timeoutChan <- c + return c + }, + ChainIO: chainIO, + Store: sweeperStore, + MaxInputsPerTx: 10, + MaxSweepAttempts: 5, + NextAttemptDeltaFunc: func(int) int32 { return 1 }, + } + + sweeper := sweep.New(sweeperCfg) - cfg := NurseryConfig{ + sweeper.Start() + + nurseryCfg := NurseryConfig{ Notifier: notifier, FetchClosedChannels: func(pendingOnly bool) ( []*channeldb.ChannelCloseSummary, error) { @@ -453,54 +489,91 @@ func createNurseryTestContext(t *testing.T, }, nil }, Store: storeIntercepter, - ChainIO: &mockChainIO{}, + ChainIO: chainIO, Sweeper: sweeper, + PublishTransaction: func(tx *wire.MsgTx) error { + return publishFunc(tx, "nursery") + }, } - publishChan := make(chan wire.MsgTx, 1) - cfg.PublishTransaction = func(tx *wire.MsgTx) error { - t.Logf("Publishing tx %v", tx.TxHash()) - publishChan <- *tx - return nil - } - - nursery := newUtxoNursery(&cfg) + nursery := newUtxoNursery(&nurseryCfg) nursery.Start() ctx := &nurseryTestContext{ nursery: nursery, notifier: notifier, + chainIO: chainIO, store: storeIntercepter, publishChan: publishChan, + sweeper: sweeper, + timeoutChan: timeoutChan, t: t, } - ctx.restart = func() bool { - return checkStartStop(func() { - ctx.nursery.Stop() - // Simulate lnd restart. - ctx.nursery = newUtxoNursery(ctx.nursery.cfg) - ctx.nursery.Start() - }) - } - ctx.receiveTx = func() wire.MsgTx { var tx wire.MsgTx select { case tx = <-ctx.publishChan: + utxnLog.Debugf("Published tx %v", tx.TxHash()) return tx - case <-time.After(5 * time.Second): + case <-time.After(defaultTestTimeout): + pprof.Lookup("goroutine").WriteTo(os.Stdout, 1) + t.Fatalf("tx not published") } return tx } + ctx.restart = func() bool { + return checkStartStop(func() { + utxnLog.Tracef("Restart sweeper and nursery") + // Simulate lnd restart. + ctx.nursery.Stop() + + // Also restart sweeper to test behaviour as one unit. + // + // TODO(joostjager): Mock sweeper to test nursery in + // isolation. + ctx.sweeper.Stop() + + // Find out if there is a last tx stored. If so, we + // expect it to be republished on startup. + hasLastTx, err := sweeperCfg.Store.GetLastPublishedTx() + if err != nil { + t.Fatal(err) + } + + // Restart sweeper. + ctx.sweeper = sweep.New(sweeperCfg) + ctx.sweeper.Start() + + // Receive last tx if expected. + if hasLastTx != nil { + utxnLog.Debugf("Expecting republish") + ctx.receiveTx() + } else { + utxnLog.Debugf("Expecting no republish") + } + + /// Restart nursery. + nurseryCfg.Sweeper = ctx.sweeper + ctx.nursery = newUtxoNursery(&nurseryCfg) + ctx.nursery.Start() + + }) + } + // Start with testing an immediate restart. ctx.restart() return ctx } +func (ctx *nurseryTestContext) notifyEpoch(height int32) { + ctx.chainIO.bestHeight = height + ctx.notifier.NotifyEpoch(height) +} + func (ctx *nurseryTestContext) finish() { // Add a final restart point in this state ctx.restart() @@ -556,6 +629,8 @@ func (ctx *nurseryTestContext) finish() { if len(activeHeights) > 0 { ctx.t.Fatalf("Expected height index to be empty") } + + ctx.sweeper.Stop() } func createOutgoingRes(onLocalCommitment bool) *lnwallet.OutgoingHtlcResolution { @@ -703,6 +778,8 @@ func testRestartLoop(t *testing.T, test func(*testing.T, return true } + utxnLog.Debugf("Skipping restart point %v", + currentStartStopIdx) return false } @@ -739,7 +816,7 @@ func testNurseryOutgoingHtlcSuccessOnLocal(t *testing.T, ctx.restart() // Notify arrival of block where HTLC CLTV expires. - ctx.notifier.notifyEpoch(125) + ctx.notifyEpoch(125) // This should trigger nursery to publish the timeout tx. ctx.receiveTx() @@ -751,7 +828,7 @@ func testNurseryOutgoingHtlcSuccessOnLocal(t *testing.T, // Confirm the timeout tx. This should promote the HTLC to KNDR state. timeoutTxHash := outgoingRes.SignedTimeoutTx.TxHash() - if err := ctx.notifier.confirmTx(&timeoutTxHash, 126); err != nil { + if err := ctx.notifier.ConfirmTx(&timeoutTxHash, 126); err != nil { t.Fatal(err) } @@ -765,7 +842,7 @@ func testNurseryOutgoingHtlcSuccessOnLocal(t *testing.T, ctx.restart() // Notify arrival of block where second level HTLC unlocks. - ctx.notifier.notifyEpoch(128) + ctx.notifyEpoch(128) // Check final sweep into wallet. testSweepHtlc(t, ctx) @@ -790,7 +867,7 @@ func testNurseryOutgoingHtlcSuccessOnRemote(t *testing.T, // resolving remote commitment tx. // // TODO(joostjager): This is probably not correct? - err := ctx.notifier.confirmTx(&outgoingRes.ClaimOutpoint.Hash, 124) + err := ctx.notifier.ConfirmTx(&outgoingRes.ClaimOutpoint.Hash, 124) if err != nil { t.Fatal(err) } @@ -805,7 +882,7 @@ func testNurseryOutgoingHtlcSuccessOnRemote(t *testing.T, ctx.restart() // Notify arrival of block where HTLC CLTV expires. - ctx.notifier.notifyEpoch(125) + ctx.notifyEpoch(125) // Check final sweep into wallet. testSweepHtlc(t, ctx) @@ -840,7 +917,7 @@ func testNurseryCommitSuccessOnLocal(t *testing.T, ctx.restart() // Notify confirmation of the commitment tx. - err = ctx.notifier.confirmTx(&commitRes.SelfOutPoint.Hash, 124) + err = ctx.notifier.ConfirmTx(&commitRes.SelfOutPoint.Hash, 124) if err != nil { t.Fatal(err) } @@ -855,7 +932,7 @@ func testNurseryCommitSuccessOnLocal(t *testing.T, ctx.restart() // Notify arrival of block where commit output CSV expires. - ctx.notifier.notifyEpoch(126) + ctx.notifyEpoch(126) // Check final sweep into wallet. testSweep(t, ctx, func() { @@ -876,27 +953,28 @@ func testSweepHtlc(t *testing.T, ctx *nurseryTestContext) { func testSweep(t *testing.T, ctx *nurseryTestContext, afterPublishAssert func()) { + // Wait for nursery to publish the sweep tx. + ctx.tick() sweepTx := ctx.receiveTx() if ctx.restart() { - // Restart will trigger rebroadcast of sweep tx. - sweepTx = ctx.receiveTx() + // Nursery reoffers its input. Sweeper needs a tick to create the sweep + // tx. + ctx.tick() + ctx.receiveTx() } afterPublishAssert() // Confirm the sweep tx. - sweepTxHash := sweepTx.TxHash() - err := ctx.notifier.confirmTx(&sweepTxHash, 129) - if err != nil { - t.Fatal(err) - } + ctx.notifier.SpendOutpoint(sweepTx.TxIn[0].PreviousOutPoint, sweepTx) // Wait for output to be promoted in store to GRAD. select { case <-ctx.store.graduateKinderChan: case <-time.After(defaultTestTimeout): + pprof.Lookup("goroutine").WriteTo(os.Stdout, 1) t.Fatalf("output not graduated") } @@ -907,6 +985,19 @@ func testSweep(t *testing.T, ctx *nurseryTestContext, assertNurseryReportUnavailable(t, ctx.nursery) } +func (ctx *nurseryTestContext) tick() { + select { + case c := <-ctx.timeoutChan: + select { + case c <- time.Time{}: + case <-time.After(defaultTestTimeout): + ctx.t.Fatal("tick timeout - tick not consumed") + } + case <-time.After(defaultTestTimeout): + ctx.t.Fatal("tick timeout - no new timer created") + } +} + type nurseryStoreInterceptor struct { ns NurseryStore @@ -941,16 +1032,18 @@ func (i *nurseryStoreInterceptor) CribToKinder(babyOutput *babyOutput) error { return err } -func (i *nurseryStoreInterceptor) PreschoolToKinder(kidOutput *kidOutput) error { - err := i.ns.PreschoolToKinder(kidOutput) +func (i *nurseryStoreInterceptor) PreschoolToKinder(kidOutput *kidOutput, + lastGradHeight uint32) error { + + err := i.ns.PreschoolToKinder(kidOutput, lastGradHeight) i.preschoolToKinderChan <- struct{}{} return err } -func (i *nurseryStoreInterceptor) GraduateKinder(height uint32) error { - err := i.ns.GraduateKinder(height) +func (i *nurseryStoreInterceptor) GraduateKinder(height uint32, kid *kidOutput) error { + err := i.ns.GraduateKinder(height, kid) i.graduateKinderChan <- struct{}{} @@ -961,30 +1054,12 @@ func (i *nurseryStoreInterceptor) FetchPreschools() ([]kidOutput, error) { return i.ns.FetchPreschools() } -func (i *nurseryStoreInterceptor) FetchClass(height uint32) (*wire.MsgTx, +func (i *nurseryStoreInterceptor) FetchClass(height uint32) ( []kidOutput, []babyOutput, error) { return i.ns.FetchClass(height) } -func (i *nurseryStoreInterceptor) FinalizeKinder(height uint32, - tx *wire.MsgTx) error { - - return i.ns.FinalizeKinder(height, tx) -} - -func (i *nurseryStoreInterceptor) LastFinalizedHeight() (uint32, error) { - return i.ns.LastFinalizedHeight() -} - -func (i *nurseryStoreInterceptor) GraduateHeight(height uint32) error { - return i.ns.GraduateHeight(height) -} - -func (i *nurseryStoreInterceptor) LastGraduatedHeight() (uint32, error) { - return i.ns.LastGraduatedHeight() -} - func (i *nurseryStoreInterceptor) HeightsBelowOrEqual(height uint32) ( []uint32, error) { @@ -1025,92 +1100,3 @@ func (m *nurseryMockSigner) ComputeInputScript(tx *wire.MsgTx, return &lnwallet.InputScript{}, nil } - -type nurseryMockNotifier struct { - confChannel map[chainhash.Hash]chan *chainntnfs.TxConfirmation - epochChan chan *chainntnfs.BlockEpoch - spendChan chan *chainntnfs.SpendDetail - mutex sync.RWMutex - t *testing.T -} - -func newNurseryMockNotifier(t *testing.T) *nurseryMockNotifier { - return &nurseryMockNotifier{ - confChannel: make(map[chainhash.Hash]chan *chainntnfs.TxConfirmation), - epochChan: make(chan *chainntnfs.BlockEpoch), - spendChan: make(chan *chainntnfs.SpendDetail), - t: t, - } -} - -func (m *nurseryMockNotifier) notifyEpoch(height int32) { - select { - case m.epochChan <- &chainntnfs.BlockEpoch{ - Height: height, - }: - case <-time.After(defaultTestTimeout): - m.t.Fatal("epoch event not consumed") - } -} - -func (m *nurseryMockNotifier) confirmTx(txid *chainhash.Hash, height uint32) error { - confirm := &chainntnfs.TxConfirmation{ - BlockHeight: height, - } - select { - case m.getConfChannel(txid) <- confirm: - case <-time.After(defaultTestTimeout): - return fmt.Errorf("confirmation not consumed") - } - return nil -} - -func (m *nurseryMockNotifier) RegisterConfirmationsNtfn(txid *chainhash.Hash, - _ []byte, numConfs, heightHint uint32) (*chainntnfs.ConfirmationEvent, - error) { - - return &chainntnfs.ConfirmationEvent{ - Confirmed: m.getConfChannel(txid), - }, nil -} - -func (m *nurseryMockNotifier) getConfChannel( - txid *chainhash.Hash) chan *chainntnfs.TxConfirmation { - - m.mutex.Lock() - defer m.mutex.Unlock() - - channel, ok := m.confChannel[*txid] - if ok { - return channel - } - channel = make(chan *chainntnfs.TxConfirmation) - m.confChannel[*txid] = channel - - return channel -} - -func (m *nurseryMockNotifier) RegisterBlockEpochNtfn( - bestBlock *chainntnfs.BlockEpoch) (*chainntnfs.BlockEpochEvent, error) { - return &chainntnfs.BlockEpochEvent{ - Epochs: m.epochChan, - Cancel: func() {}, - }, nil -} - -func (m *nurseryMockNotifier) Start() error { - return nil -} - -func (m *nurseryMockNotifier) Stop() error { - return nil -} - -func (m *nurseryMockNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint, - _ []byte, heightHint uint32) (*chainntnfs.SpendEvent, error) { - - return &chainntnfs.SpendEvent{ - Spend: m.spendChan, - Cancel: func() {}, - }, nil -}