commit
2f17030e8c
2
.gitignore
vendored
2
.gitignore
vendored
@ -28,6 +28,8 @@ _testmain.go
|
|||||||
/lnd-debug
|
/lnd-debug
|
||||||
/lncli
|
/lncli
|
||||||
/lncli-debug
|
/lncli-debug
|
||||||
|
/lnd-itest
|
||||||
|
/lncli-itest
|
||||||
|
|
||||||
# Integration test log files
|
# Integration test log files
|
||||||
output*.log
|
output*.log
|
||||||
|
8
Makefile
8
Makefile
@ -100,6 +100,11 @@ build:
|
|||||||
$(GOBUILD) -tags="$(DEV_TAGS)" -o lnd-debug $(LDFLAGS) $(PKG)
|
$(GOBUILD) -tags="$(DEV_TAGS)" -o lnd-debug $(LDFLAGS) $(PKG)
|
||||||
$(GOBUILD) -tags="$(DEV_TAGS)" -o lncli-debug $(LDFLAGS) $(PKG)/cmd/lncli
|
$(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:
|
install:
|
||||||
@$(call print, "Installing lnd and lncli.")
|
@$(call print, "Installing lnd and lncli.")
|
||||||
$(GOINSTALL) -tags="${tags}" $(LDFLAGS) $(PKG)
|
$(GOINSTALL) -tags="${tags}" $(LDFLAGS) $(PKG)
|
||||||
@ -118,7 +123,7 @@ itest-only:
|
|||||||
@$(call print, "Running integration tests.")
|
@$(call print, "Running integration tests.")
|
||||||
$(ITEST)
|
$(ITEST)
|
||||||
|
|
||||||
itest: btcd build itest-only
|
itest: btcd build-itest itest-only
|
||||||
|
|
||||||
unit: btcd
|
unit: btcd
|
||||||
@$(call print, "Running unit tests.")
|
@$(call print, "Running unit tests.")
|
||||||
@ -181,6 +186,7 @@ rpc:
|
|||||||
clean:
|
clean:
|
||||||
@$(call print, "Cleaning source.$(NC)")
|
@$(call print, "Cleaning source.$(NC)")
|
||||||
$(RM) ./lnd-debug ./lncli-debug
|
$(RM) ./lnd-debug ./lncli-debug
|
||||||
|
$(RM) ./lnd-itest ./lncli-itest
|
||||||
$(RM) -r ./vendor .vendor-new
|
$(RM) -r ./vendor .vendor-new
|
||||||
|
|
||||||
|
|
||||||
|
@ -757,6 +757,7 @@ type breachedOutput struct {
|
|||||||
outpoint wire.OutPoint
|
outpoint wire.OutPoint
|
||||||
witnessType lnwallet.WitnessType
|
witnessType lnwallet.WitnessType
|
||||||
signDesc lnwallet.SignDescriptor
|
signDesc lnwallet.SignDescriptor
|
||||||
|
confHeight uint32
|
||||||
|
|
||||||
secondLevelWitnessScript []byte
|
secondLevelWitnessScript []byte
|
||||||
|
|
||||||
@ -768,7 +769,8 @@ type breachedOutput struct {
|
|||||||
func makeBreachedOutput(outpoint *wire.OutPoint,
|
func makeBreachedOutput(outpoint *wire.OutPoint,
|
||||||
witnessType lnwallet.WitnessType,
|
witnessType lnwallet.WitnessType,
|
||||||
secondLevelScript []byte,
|
secondLevelScript []byte,
|
||||||
signDescriptor *lnwallet.SignDescriptor) breachedOutput {
|
signDescriptor *lnwallet.SignDescriptor,
|
||||||
|
confHeight uint32) breachedOutput {
|
||||||
|
|
||||||
amount := signDescriptor.Output.Value
|
amount := signDescriptor.Output.Value
|
||||||
|
|
||||||
@ -778,6 +780,7 @@ func makeBreachedOutput(outpoint *wire.OutPoint,
|
|||||||
secondLevelWitnessScript: secondLevelScript,
|
secondLevelWitnessScript: secondLevelScript,
|
||||||
witnessType: witnessType,
|
witnessType: witnessType,
|
||||||
signDesc: *signDescriptor,
|
signDesc: *signDescriptor,
|
||||||
|
confHeight: confHeight,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -831,6 +834,12 @@ func (bo *breachedOutput) BlocksToMaturity() uint32 {
|
|||||||
return 0
|
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
|
// Add compile-time constraint ensuring breachedOutput implements the Input
|
||||||
// interface.
|
// interface.
|
||||||
var _ sweep.Input = (*breachedOutput)(nil)
|
var _ sweep.Input = (*breachedOutput)(nil)
|
||||||
@ -878,7 +887,8 @@ func newRetributionInfo(chanPoint *wire.OutPoint,
|
|||||||
// No second level script as this is a commitment
|
// No second level script as this is a commitment
|
||||||
// output.
|
// output.
|
||||||
nil,
|
nil,
|
||||||
breachInfo.LocalOutputSignDesc)
|
breachInfo.LocalOutputSignDesc,
|
||||||
|
breachInfo.BreachHeight)
|
||||||
|
|
||||||
breachedOutputs = append(breachedOutputs, localOutput)
|
breachedOutputs = append(breachedOutputs, localOutput)
|
||||||
}
|
}
|
||||||
@ -895,7 +905,8 @@ func newRetributionInfo(chanPoint *wire.OutPoint,
|
|||||||
// No second level script as this is a commitment
|
// No second level script as this is a commitment
|
||||||
// output.
|
// output.
|
||||||
nil,
|
nil,
|
||||||
breachInfo.RemoteOutputSignDesc)
|
breachInfo.RemoteOutputSignDesc,
|
||||||
|
breachInfo.BreachHeight)
|
||||||
|
|
||||||
breachedOutputs = append(breachedOutputs, remoteOutput)
|
breachedOutputs = append(breachedOutputs, remoteOutput)
|
||||||
}
|
}
|
||||||
@ -919,7 +930,8 @@ func newRetributionInfo(chanPoint *wire.OutPoint,
|
|||||||
&breachInfo.HtlcRetributions[i].OutPoint,
|
&breachInfo.HtlcRetributions[i].OutPoint,
|
||||||
htlcWitnessType,
|
htlcWitnessType,
|
||||||
breachInfo.HtlcRetributions[i].SecondLevelWitnessScript,
|
breachInfo.HtlcRetributions[i].SecondLevelWitnessScript,
|
||||||
&breachInfo.HtlcRetributions[i].SignDesc)
|
&breachInfo.HtlcRetributions[i].SignDesc,
|
||||||
|
breachInfo.BreachHeight)
|
||||||
|
|
||||||
breachedOutputs = append(breachedOutputs, htlcOutput)
|
breachedOutputs = append(breachedOutputs, htlcOutput)
|
||||||
}
|
}
|
||||||
|
@ -1336,7 +1336,7 @@ func createTestArbiter(t *testing.T, contractBreaches chan *ContractBreachEvent,
|
|||||||
ba := newBreachArbiter(&BreachConfig{
|
ba := newBreachArbiter(&BreachConfig{
|
||||||
CloseLink: func(_ *wire.OutPoint, _ htlcswitch.ChannelCloseType) {},
|
CloseLink: func(_ *wire.OutPoint, _ htlcswitch.ChannelCloseType) {},
|
||||||
DB: db,
|
DB: db,
|
||||||
Estimator: &lnwallet.StaticFeeEstimator{FeePerKW: 12500},
|
Estimator: lnwallet.NewStaticFeeEstimator(12500, 0),
|
||||||
GenSweepScript: func() ([]byte, error) { return nil, nil },
|
GenSweepScript: func() ([]byte, error) { return nil, nil },
|
||||||
ContractBreaches: contractBreaches,
|
ContractBreaches: contractBreaches,
|
||||||
Signer: signer,
|
Signer: signer,
|
||||||
@ -1476,7 +1476,7 @@ func createInitChannels(revocationWindow int) (*lnwallet.LightningChannel, *lnwa
|
|||||||
return nil, nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
estimator := &lnwallet.StaticFeeEstimator{FeePerKW: 12500}
|
estimator := lnwallet.NewStaticFeeEstimator(12500, 0)
|
||||||
feePerKw, err := estimator.EstimateFeePerKW(1)
|
feePerKw, err := estimator.EstimateFeePerKW(1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, err
|
||||||
|
@ -150,9 +150,9 @@ func newChainControlFromConfig(cfg *config, chanDB *channeldb.DB,
|
|||||||
FeeRate: cfg.Bitcoin.FeeRate,
|
FeeRate: cfg.Bitcoin.FeeRate,
|
||||||
TimeLockDelta: cfg.Bitcoin.TimeLockDelta,
|
TimeLockDelta: cfg.Bitcoin.TimeLockDelta,
|
||||||
}
|
}
|
||||||
cc.feeEstimator = lnwallet.StaticFeeEstimator{
|
cc.feeEstimator = lnwallet.NewStaticFeeEstimator(
|
||||||
FeePerKW: defaultBitcoinStaticFeePerKW,
|
defaultBitcoinStaticFeePerKW, 0,
|
||||||
}
|
)
|
||||||
case litecoinChain:
|
case litecoinChain:
|
||||||
cc.routingPolicy = htlcswitch.ForwardingPolicy{
|
cc.routingPolicy = htlcswitch.ForwardingPolicy{
|
||||||
MinHTLC: cfg.Litecoin.MinHTLC,
|
MinHTLC: cfg.Litecoin.MinHTLC,
|
||||||
@ -160,9 +160,9 @@ func newChainControlFromConfig(cfg *config, chanDB *channeldb.DB,
|
|||||||
FeeRate: cfg.Litecoin.FeeRate,
|
FeeRate: cfg.Litecoin.FeeRate,
|
||||||
TimeLockDelta: cfg.Litecoin.TimeLockDelta,
|
TimeLockDelta: cfg.Litecoin.TimeLockDelta,
|
||||||
}
|
}
|
||||||
cc.feeEstimator = lnwallet.StaticFeeEstimator{
|
cc.feeEstimator = lnwallet.NewStaticFeeEstimator(
|
||||||
FeePerKW: defaultLitecoinStaticFeePerKW,
|
defaultLitecoinStaticFeePerKW, 0,
|
||||||
}
|
)
|
||||||
default:
|
default:
|
||||||
return nil, nil, fmt.Errorf("Default routing policy for "+
|
return nil, nil, fmt.Errorf("Default routing policy for "+
|
||||||
"chain %v is unknown", registeredChains.PrimaryChain())
|
"chain %v is unknown", registeredChains.PrimaryChain())
|
||||||
|
@ -462,6 +462,7 @@ func (h *htlcSuccessResolver) Resolve() (ContractResolver, error) {
|
|||||||
&h.htlcResolution.ClaimOutpoint,
|
&h.htlcResolution.ClaimOutpoint,
|
||||||
&h.htlcResolution.SweepSignDesc,
|
&h.htlcResolution.SweepSignDesc,
|
||||||
h.htlcResolution.Preimage[:],
|
h.htlcResolution.Preimage[:],
|
||||||
|
h.broadcastHeight,
|
||||||
)
|
)
|
||||||
|
|
||||||
// With the input created, we can now generate the full
|
// 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
|
// TODO: Set tx lock time to current block height
|
||||||
// instead of zero. Will be taken care of once sweeper
|
// instead of zero. Will be taken care of once sweeper
|
||||||
// implementation is complete.
|
// implementation is complete.
|
||||||
|
//
|
||||||
|
// TODO: Use time-based sweeper and result chan.
|
||||||
var err error
|
var err error
|
||||||
h.sweepTx, err = h.Sweeper.CreateSweepTx(
|
h.sweepTx, err = h.Sweeper.CreateSweepTx(
|
||||||
[]sweep.Input{&input}, sweepConfTarget, 0,
|
[]sweep.Input{&input}, sweepConfTarget, 0,
|
||||||
@ -1254,6 +1257,7 @@ func (c *commitSweepResolver) Resolve() (ContractResolver, error) {
|
|||||||
&c.commitResolution.SelfOutPoint,
|
&c.commitResolution.SelfOutPoint,
|
||||||
lnwallet.CommitmentNoDelay,
|
lnwallet.CommitmentNoDelay,
|
||||||
&c.commitResolution.SelfOutputSignDesc,
|
&c.commitResolution.SelfOutputSignDesc,
|
||||||
|
c.broadcastHeight,
|
||||||
)
|
)
|
||||||
|
|
||||||
// With out input constructed, we'll now request that the
|
// 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
|
// TODO: Set tx lock time to current block height instead of
|
||||||
// zero. Will be taken care of once sweeper implementation is
|
// zero. Will be taken care of once sweeper implementation is
|
||||||
// complete.
|
// complete.
|
||||||
|
//
|
||||||
|
// TODO: Use time-based sweeper and result chan.
|
||||||
c.sweepTx, err = c.Sweeper.CreateSweepTx(
|
c.sweepTx, err = c.Sweeper.CreateSweepTx(
|
||||||
[]sweep.Input{&input}, sweepConfTarget, 0,
|
[]sweep.Input{&input}, sweepConfTarget, 0,
|
||||||
)
|
)
|
||||||
|
@ -231,7 +231,7 @@ func createTestFundingManager(t *testing.T, privKey *btcec.PrivateKey,
|
|||||||
addr *lnwire.NetAddress, tempTestDir string) (*testNode, error) {
|
addr *lnwire.NetAddress, tempTestDir string) (*testNode, error) {
|
||||||
|
|
||||||
netParams := activeNetParams.Params
|
netParams := activeNetParams.Params
|
||||||
estimator := lnwallet.StaticFeeEstimator{FeePerKW: 62500}
|
estimator := lnwallet.NewStaticFeeEstimator(62500, 0)
|
||||||
|
|
||||||
chainNotifier := &mockNotifier{
|
chainNotifier := &mockNotifier{
|
||||||
oneConfChannel: make(chan *chainntnfs.TxConfirmation, 1),
|
oneConfChannel: make(chan *chainntnfs.TxConfirmation, 1),
|
||||||
@ -250,7 +250,9 @@ func createTestFundingManager(t *testing.T, privKey *btcec.PrivateKey,
|
|||||||
signer := &mockSigner{
|
signer := &mockSigner{
|
||||||
key: alicePrivKey,
|
key: alicePrivKey,
|
||||||
}
|
}
|
||||||
bio := &mockChainIO{}
|
bio := &mockChainIO{
|
||||||
|
bestHeight: fundingBroadcastHeight,
|
||||||
|
}
|
||||||
|
|
||||||
dbDir := filepath.Join(tempTestDir, "cdb")
|
dbDir := filepath.Join(tempTestDir, "cdb")
|
||||||
cdb, err := channeldb.Open(dbDir)
|
cdb, err := channeldb.Open(dbDir)
|
||||||
|
@ -1795,7 +1795,7 @@ func TestChannelLinkBandwidthConsistency(t *testing.T) {
|
|||||||
coreLink.cfg.HodlMask = hodl.MaskFromFlags(hodl.ExitSettle)
|
coreLink.cfg.HodlMask = hodl.MaskFromFlags(hodl.ExitSettle)
|
||||||
coreLink.cfg.DebugHTLC = true
|
coreLink.cfg.DebugHTLC = true
|
||||||
|
|
||||||
estimator := &lnwallet.StaticFeeEstimator{FeePerKW: 6000}
|
estimator := lnwallet.NewStaticFeeEstimator(6000, 0)
|
||||||
feePerKw, err := estimator.EstimateFeePerKW(1)
|
feePerKw, err := estimator.EstimateFeePerKW(1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to query fee estimator: %v", err)
|
t.Fatalf("unable to query fee estimator: %v", err)
|
||||||
@ -2206,7 +2206,7 @@ func TestChannelLinkBandwidthConsistencyOverflow(t *testing.T) {
|
|||||||
aliceMsgs = coreLink.cfg.Peer.(*mockPeer).sentMsgs
|
aliceMsgs = coreLink.cfg.Peer.(*mockPeer).sentMsgs
|
||||||
)
|
)
|
||||||
|
|
||||||
estimator := &lnwallet.StaticFeeEstimator{FeePerKW: 6000}
|
estimator := lnwallet.NewStaticFeeEstimator(6000, 0)
|
||||||
feePerKw, err := estimator.EstimateFeePerKW(1)
|
feePerKw, err := estimator.EstimateFeePerKW(1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to query fee estimator: %v", err)
|
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
|
// Compute the static fees that will be used to determine the
|
||||||
// correctness of Alice's bandwidth when forwarding HTLCs.
|
// correctness of Alice's bandwidth when forwarding HTLCs.
|
||||||
estimator := &lnwallet.StaticFeeEstimator{FeePerKW: 6000}
|
estimator := lnwallet.NewStaticFeeEstimator(6000, 0)
|
||||||
feePerKw, err := estimator.EstimateFeePerKW(1)
|
feePerKw, err := estimator.EstimateFeePerKW(1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to query fee estimator: %v", err)
|
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
|
// Compute the static fees that will be used to determine the
|
||||||
// correctness of Alice's bandwidth when forwarding HTLCs.
|
// correctness of Alice's bandwidth when forwarding HTLCs.
|
||||||
estimator := &lnwallet.StaticFeeEstimator{FeePerKW: 6000}
|
estimator := lnwallet.NewStaticFeeEstimator(6000, 0)
|
||||||
feePerKw, err := estimator.EstimateFeePerKW(1)
|
feePerKw, err := estimator.EstimateFeePerKW(1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to query fee estimator: %v", err)
|
t.Fatalf("unable to query fee estimator: %v", err)
|
||||||
@ -2989,7 +2989,7 @@ func TestChannelLinkBandwidthChanReserve(t *testing.T) {
|
|||||||
aliceMsgs = coreLink.cfg.Peer.(*mockPeer).sentMsgs
|
aliceMsgs = coreLink.cfg.Peer.(*mockPeer).sentMsgs
|
||||||
)
|
)
|
||||||
|
|
||||||
estimator := &lnwallet.StaticFeeEstimator{FeePerKW: 6000}
|
estimator := lnwallet.NewStaticFeeEstimator(6000, 0)
|
||||||
feePerKw, err := estimator.EstimateFeePerKW(1)
|
feePerKw, err := estimator.EstimateFeePerKW(1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to query fee estimator: %v", err)
|
t.Fatalf("unable to query fee estimator: %v", err)
|
||||||
|
@ -75,6 +75,10 @@ func (m *mockFeeEstimator) EstimateFeePerKW(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockFeeEstimator) RelayFeePerKW() lnwallet.SatPerKWeight {
|
||||||
|
return 1e3
|
||||||
|
}
|
||||||
|
|
||||||
func (m *mockFeeEstimator) Start() error {
|
func (m *mockFeeEstimator) Start() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -273,7 +273,7 @@ func createTestChannel(alicePrivKey, bobPrivKey []byte,
|
|||||||
return nil, nil, nil, nil, err
|
return nil, nil, nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
estimator := &lnwallet.StaticFeeEstimator{FeePerKW: 6000}
|
estimator := lnwallet.NewStaticFeeEstimator(6000, 0)
|
||||||
feePerKw, err := estimator.EstimateFeePerKW(1)
|
feePerKw, err := estimator.EstimateFeePerKW(1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, nil, err
|
return nil, nil, nil, nil, err
|
||||||
|
@ -281,7 +281,7 @@ func (hn *HarnessNode) start(lndError chan<- error) error {
|
|||||||
|
|
||||||
args := hn.cfg.genArgs()
|
args := hn.cfg.genArgs()
|
||||||
args = append(args, fmt.Sprintf("--profile=%d", 9000+hn.NodeID))
|
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
|
// Redirect stderr output to buffer
|
||||||
var errb bytes.Buffer
|
var errb bytes.Buffer
|
||||||
|
@ -1131,7 +1131,7 @@ func TestHTLCSigNumber(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Calculate two values that will be below and above Bob's dust limit.
|
// 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)
|
feePerKw, err := estimator.EstimateFeePerKW(1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to get fee: %v", err)
|
t.Fatalf("unable to get fee: %v", err)
|
||||||
|
@ -59,22 +59,50 @@ type FeeEstimator interface {
|
|||||||
// Stop stops any spawned goroutines and cleans up the resources used
|
// Stop stops any spawned goroutines and cleans up the resources used
|
||||||
// by the fee estimator.
|
// by the fee estimator.
|
||||||
Stop() error
|
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
|
// StaticFeeEstimator will return a static value for all fee calculation
|
||||||
// requests. It is designed to be replaced by a proper 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 {
|
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.
|
// 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.
|
// EstimateFeePerKW will return a static value for fee calculations.
|
||||||
//
|
//
|
||||||
// NOTE: This method is part of the FeeEstimator interface.
|
// NOTE: This method is part of the FeeEstimator interface.
|
||||||
func (e StaticFeeEstimator) EstimateFeePerKW(numBlocks uint32) (SatPerKWeight, error) {
|
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
|
// 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
|
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
|
// fetchEstimate returns a fee estimate for a transaction to be confirmed in
|
||||||
// confTarget blocks. The estimate is returned in sat/kw.
|
// confTarget blocks. The estimate is returned in sat/kw.
|
||||||
func (b *BtcdFeeEstimator) fetchEstimate(confTarget uint32) (SatPerKWeight, error) {
|
func (b *BtcdFeeEstimator) fetchEstimate(confTarget uint32) (SatPerKWeight, error) {
|
||||||
@ -359,6 +395,14 @@ func (b *BitcoindFeeEstimator) EstimateFeePerKW(numBlocks uint32) (SatPerKWeight
|
|||||||
return feeEstimate, nil
|
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
|
// fetchEstimate returns a fee estimate for a transaction to be confirmed in
|
||||||
// confTarget blocks. The estimate is returned in sat/kw.
|
// confTarget blocks. The estimate is returned in sat/kw.
|
||||||
func (b *BitcoindFeeEstimator) fetchEstimate(confTarget uint32) (SatPerKWeight, error) {
|
func (b *BitcoindFeeEstimator) fetchEstimate(confTarget uint32) (SatPerKWeight, error) {
|
||||||
|
@ -74,9 +74,7 @@ func TestStaticFeeEstimator(t *testing.T) {
|
|||||||
|
|
||||||
const feePerKw = lnwallet.FeePerKwFloor
|
const feePerKw = lnwallet.FeePerKwFloor
|
||||||
|
|
||||||
feeEstimator := &lnwallet.StaticFeeEstimator{
|
feeEstimator := lnwallet.NewStaticFeeEstimator(feePerKw, 0)
|
||||||
FeePerKW: feePerKw,
|
|
||||||
}
|
|
||||||
if err := feeEstimator.Start(); err != nil {
|
if err := feeEstimator.Start(); err != nil {
|
||||||
t.Fatalf("unable to start fee estimator: %v", err)
|
t.Fatalf("unable to start fee estimator: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -368,7 +368,7 @@ func createTestWallet(tempTestDir string, miningNode *rpctest.Harness,
|
|||||||
WalletController: wc,
|
WalletController: wc,
|
||||||
Signer: signer,
|
Signer: signer,
|
||||||
ChainIO: bio,
|
ChainIO: bio,
|
||||||
FeeEstimator: lnwallet.StaticFeeEstimator{FeePerKW: 2500},
|
FeeEstimator: lnwallet.NewStaticFeeEstimator(2500, 0),
|
||||||
DefaultConstraints: channeldb.ChannelConstraints{
|
DefaultConstraints: channeldb.ChannelConstraints{
|
||||||
DustLimit: 500,
|
DustLimit: 500,
|
||||||
MaxPendingAmount: lnwire.NewMSatFromSatoshis(btcutil.SatoshiPerBitcoin) * 100,
|
MaxPendingAmount: lnwire.NewMSatFromSatoshis(btcutil.SatoshiPerBitcoin) * 100,
|
||||||
@ -2440,7 +2440,7 @@ func runTests(t *testing.T, walletDriver *lnwallet.WalletDriver,
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "neutrino":
|
case "neutrino":
|
||||||
feeEstimator = lnwallet.StaticFeeEstimator{FeePerKW: 62500}
|
feeEstimator = lnwallet.NewStaticFeeEstimator(62500, 0)
|
||||||
|
|
||||||
// Set some package-level variable to speed up
|
// Set some package-level variable to speed up
|
||||||
// operation for tests.
|
// operation for tests.
|
||||||
|
@ -229,7 +229,7 @@ func CreateTestChannels() (*LightningChannel, *LightningChannel, func(), error)
|
|||||||
return nil, nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
estimator := &StaticFeeEstimator{FeePerKW: 6000}
|
estimator := NewStaticFeeEstimator(6000, 0)
|
||||||
feePerKw, err := estimator.EstimateFeePerKW(1)
|
feePerKw, err := estimator.EstimateFeePerKW(1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, err
|
||||||
|
@ -71,6 +71,44 @@ const (
|
|||||||
HtlcSecondLevelRevoke WitnessType = 9
|
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
|
// WitnessGenerator represents a function which is able to generate the final
|
||||||
// witness for a particular public key script. This function acts as an
|
// witness for a particular public key script. This function acts as an
|
||||||
// abstraction layer, hiding the details of the underlying script.
|
// abstraction layer, hiding the details of the underlying script.
|
||||||
|
8
mock.go
8
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) {
|
func (m *mockChainIO) GetBestBlock() (*chainhash.Hash, int32, error) {
|
||||||
return activeNetParams.GenesisHash, fundingBroadcastHeight, nil
|
return activeNetParams.GenesisHash, m.bestHeight, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*mockChainIO) GetUtxo(op *wire.OutPoint, _ []byte,
|
func (*mockChainIO) GetUtxo(op *wire.OutPoint, _ []byte,
|
||||||
|
337
nursery_store.go
337
nursery_store.go
@ -23,21 +23,6 @@ import (
|
|||||||
//
|
//
|
||||||
// utxn<chain-hash>/
|
// utxn<chain-hash>/
|
||||||
// |
|
// |
|
||||||
// | 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-finalized-height>
|
|
||||||
// ├── last-graduated-height-key: <last-graduated-height>
|
|
||||||
// |
|
|
||||||
// | CHANNEL INDEX
|
// | CHANNEL INDEX
|
||||||
// |
|
// |
|
||||||
// | The channel index contains a directory for each channel that has a
|
// | The channel index contains a directory for each channel that has a
|
||||||
@ -72,10 +57,7 @@ import (
|
|||||||
// | relative file path:
|
// | relative file path:
|
||||||
// | e.g. <chan-point-3>/<prefix><outpoint-2>/
|
// | e.g. <chan-point-3>/<prefix><outpoint-2>/
|
||||||
// | that can be queried in the channel index to retrieve the serialized
|
// | 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
|
// | output.
|
||||||
// | 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.
|
|
||||||
// |
|
// |
|
||||||
// └── height-index-key/
|
// └── height-index-key/
|
||||||
// ├── <height-1>/ <- HEIGHT BUCKET
|
// ├── <height-1>/ <- HEIGHT BUCKET
|
||||||
@ -84,12 +66,15 @@ import (
|
|||||||
// | | └── <state-prefix><outpoint-5>: ""
|
// | | └── <state-prefix><outpoint-5>: ""
|
||||||
// | ├── <chan-point-2>/
|
// | ├── <chan-point-2>/
|
||||||
// | | └── <state-prefix><outpoint-3>: ""
|
// | | └── <state-prefix><outpoint-3>: ""
|
||||||
// | └── finalized-kndr-txn: "" | <kndr-sweep-tnx>
|
|
||||||
// └── <height-2>/
|
// └── <height-2>/
|
||||||
// └── <chan-point-1>/
|
// └── <chan-point-1>/
|
||||||
// └── <state-prefix><outpoint-1>: ""
|
// └── <state-prefix><outpoint-1>: ""
|
||||||
// └── <state-prefix><outpoint-2>: ""
|
// └── <state-prefix><outpoint-2>: ""
|
||||||
|
|
||||||
|
// 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.
|
// NurseryStore abstracts the persistent storage layer for the utxo nursery.
|
||||||
// Concretely, it stores commitment and htlc outputs until any time-bounded
|
// Concretely, it stores commitment and htlc outputs until any time-bounded
|
||||||
// constraints have fully matured. The store exposes methods for enumerating its
|
// constraints have fully matured. The store exposes methods for enumerating its
|
||||||
@ -110,47 +95,30 @@ type NurseryStore interface {
|
|||||||
CribToKinder(*babyOutput) error
|
CribToKinder(*babyOutput) error
|
||||||
|
|
||||||
// PreschoolToKinder atomically moves a kidOutput from the preschool
|
// PreschoolToKinder atomically moves a kidOutput from the preschool
|
||||||
// bucket to the kindergarten bucket. This transition should be
|
// bucket to the kindergarten bucket. This transition should be executed
|
||||||
// executed after receiving confirmation of the preschool output.
|
// after receiving confirmation of the preschool output. Incoming HTLC's
|
||||||
// Incoming HTLC's we need to go to the second-layer to claim, and also
|
// we need to go to the second-layer to claim, and also our commitment
|
||||||
// our commitment outputs fall into this class.
|
// outputs fall into this class.
|
||||||
PreschoolToKinder(*kidOutput) error
|
//
|
||||||
|
// 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 the kindergarten class at the
|
// GraduateKinder atomically moves an output at the provided height into
|
||||||
// provided height into the graduated status. This involves removing the
|
// the graduated status. This involves removing the kindergarten entries
|
||||||
// kindergarten entries from both the height and channel indexes, and
|
// from both the height and channel indexes. The height bucket will be
|
||||||
// cleaning up the finalized kindergarten sweep txn. The height bucket
|
// opportunistically pruned from the height index as outputs are
|
||||||
// will be opportunistically pruned from the height index as outputs are
|
|
||||||
// removed.
|
// removed.
|
||||||
GraduateKinder(height uint32) error
|
GraduateKinder(height uint32, output *kidOutput) error
|
||||||
|
|
||||||
// FetchPreschools returns a list of all outputs currently stored in
|
// FetchPreschools returns a list of all outputs currently stored in
|
||||||
// the preschool bucket.
|
// the preschool bucket.
|
||||||
FetchPreschools() ([]kidOutput, error)
|
FetchPreschools() ([]kidOutput, error)
|
||||||
|
|
||||||
// FetchClass returns a list of kindergarten and crib outputs whose
|
// FetchClass returns a list of kindergarten and crib outputs whose
|
||||||
// timelocks expire at the given height. If the kindergarten class at
|
// timelocks expire at the given height.
|
||||||
// this height hash been finalized previously, via FinalizeKinder, it
|
FetchClass(height uint32) ([]kidOutput, []babyOutput, error)
|
||||||
// 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)
|
|
||||||
|
|
||||||
// HeightsBelowOrEqual returns the lowest non-empty heights in the
|
// HeightsBelowOrEqual returns the lowest non-empty heights in the
|
||||||
// height index, that exist at or below the provided upper bound.
|
// 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.
|
// the root-level, chain-segmented bucket for each nursery store.
|
||||||
utxnChainPrefix = []byte("utxn")
|
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
|
// channelIndexKey is a static key used to lookup the bucket containing
|
||||||
// all of the nursery's active channels.
|
// all of the nursery's active channels.
|
||||||
channelIndexKey = []byte("channel-index")
|
channelIndexKey = []byte("channel-index")
|
||||||
@ -197,10 +157,6 @@ var (
|
|||||||
// containing all heights for which the nursery will need to take
|
// containing all heights for which the nursery will need to take
|
||||||
// action.
|
// action.
|
||||||
heightIndexKey = []byte("height-index")
|
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
|
// 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
|
// PreschoolToKinder atomically moves a kidOutput from the preschool bucket to
|
||||||
// the kindergarten bucket. This transition should be executed after receiving
|
// the kindergarten bucket. This transition should be executed after receiving
|
||||||
// confirmation of the preschool output's commitment transaction.
|
// 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 {
|
return ns.db.Update(func(tx *bbolt.Tx) error {
|
||||||
// Create or retrieve the channel bucket corresponding to the
|
// Create or retrieve the channel bucket corresponding to the
|
||||||
// kid output's origin channel point.
|
// kid output's origin channel point.
|
||||||
@ -478,13 +436,6 @@ func (ns *nurseryStore) PreschoolToKinder(kid *kidOutput) error {
|
|||||||
maturityHeight = kid.ConfHeight() + kid.BlocksToMaturity()
|
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 {
|
if maturityHeight <= lastGradHeight {
|
||||||
utxnLog.Debugf("Late Registration for kid output=%v "+
|
utxnLog.Debugf("Late Registration for kid output=%v "+
|
||||||
"detected: class_height=%v, "+
|
"detected: class_height=%v, "+
|
||||||
@ -515,41 +466,22 @@ func (ns *nurseryStore) PreschoolToKinder(kid *kidOutput) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// GraduateKinder atomically moves the kindergarten class at the provided height
|
// GraduateKinder atomically moves an output at the provided height into the
|
||||||
// into the graduated status. This involves removing the kindergarten entries
|
// graduated status. This involves removing the kindergarten entries from both
|
||||||
// from both the height and channel indexes, and cleaning up the finalized
|
// the height and channel indexes. The height bucket will be opportunistically
|
||||||
// kindergarten sweep txn. The height bucket will be opportunistically pruned
|
// pruned from the height index as outputs are removed.
|
||||||
// from the height index as outputs are removed.
|
func (ns *nurseryStore) GraduateKinder(height uint32, kid *kidOutput) error {
|
||||||
func (ns *nurseryStore) GraduateKinder(height uint32) error {
|
|
||||||
return ns.db.Update(func(tx *bbolt.Tx) 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)
|
hghtBucket := ns.getHeightBucket(tx, height)
|
||||||
if hghtBucket == nil {
|
if hghtBucket == nil {
|
||||||
// Nothing to delete, bucket has already been removed.
|
// Nothing to delete, bucket has already been removed.
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the finalized kindergarten txn, we do this before
|
// For the kindergarten output, delete its entry from the
|
||||||
// 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
|
|
||||||
// height and channel index, and create a new grad output in the
|
// height and channel index, and create a new grad output in the
|
||||||
// channel index.
|
// 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()
|
outpoint := kid.OutPoint()
|
||||||
chanPoint := kid.OriginChanPoint()
|
chanPoint := kid.OriginChanPoint()
|
||||||
|
|
||||||
@ -594,29 +526,6 @@ func (ns *nurseryStore) GraduateKinder(height uint32) error {
|
|||||||
// using graduate-prefixed key.
|
// using graduate-prefixed key.
|
||||||
return chanBucket.Put(pfxOutputKey,
|
return chanBucket.Put(pfxOutputKey,
|
||||||
gradBuffer.Bytes())
|
gradBuffer.Bytes())
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
|
|
||||||
return ns.db.Update(func(tx *bbolt.Tx) error {
|
|
||||||
return ns.finalizeKinder(tx, height, finalTx)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GraduateHeight persists the provided height as the nursery store's last
|
|
||||||
// graduated height.
|
|
||||||
func (ns *nurseryStore) GraduateHeight(height uint32) error {
|
|
||||||
|
|
||||||
return ns.db.Update(func(tx *bbolt.Tx) error {
|
|
||||||
return ns.putLastGraduatedHeight(tx, height)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -625,23 +534,15 @@ func (ns *nurseryStore) GraduateHeight(height uint32) error {
|
|||||||
// FetchClass returns a list of the kindergarten and crib outputs whose timeouts
|
// FetchClass returns a list of the kindergarten and crib outputs whose timeouts
|
||||||
// are expiring
|
// are expiring
|
||||||
func (ns *nurseryStore) FetchClass(
|
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
|
// Construct list of all crib and kindergarten outputs that need to be
|
||||||
// processed at the provided block height.
|
// processed at the provided block height.
|
||||||
var finalTx *wire.MsgTx
|
|
||||||
var kids []kidOutput
|
var kids []kidOutput
|
||||||
var babies []babyOutput
|
var babies []babyOutput
|
||||||
if err := ns.db.View(func(tx *bbolt.Tx) error {
|
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.
|
// 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 {
|
func(buf []byte) error {
|
||||||
|
|
||||||
// We will attempt to deserialize all outputs
|
// We will attempt to deserialize all outputs
|
||||||
@ -683,10 +584,10 @@ func (ns *nurseryStore) FetchClass(
|
|||||||
})
|
})
|
||||||
|
|
||||||
}); err != nil {
|
}); 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
|
// 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
|
// Helper Methods
|
||||||
|
|
||||||
// enterCrib accepts a new htlc output that the nursery will incubate through
|
// 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
|
// Next, retrieve or create the height-channel bucket located in the
|
||||||
// height bucket corresponding to the baby output's CLTV expiry height.
|
// height bucket corresponding to the baby output's CLTV expiry height.
|
||||||
|
|
||||||
|
// TODO: Handle late registration.
|
||||||
hghtChanBucket, err := ns.createHeightChanBucket(tx,
|
hghtChanBucket, err := ns.createHeightChanBucket(tx,
|
||||||
baby.expiry, chanPoint)
|
baby.expiry, chanPoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -1332,145 +1209,6 @@ func (ns *nurseryStore) forChanOutputs(tx *bbolt.Tx, chanPoint *wire.OutPoint,
|
|||||||
return chanBucket.ForEach(callback)
|
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
|
// errBucketNotEmpty signals that an attempt to prune a particular
|
||||||
// bucket failed because it still has active outputs.
|
// bucket failed because it still has active outputs.
|
||||||
var errBucketNotEmpty = errors.New("bucket is not empty, cannot be pruned")
|
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
|
// attempt to remove each one if they are empty, keeping track of the
|
||||||
// number of height-channel buckets that still have active outputs.
|
// number of height-channel buckets that still have active outputs.
|
||||||
if err := hghtBucket.ForEach(func(chanBytes, v []byte) error {
|
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 {
|
if v != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -88,8 +88,6 @@ func TestNurseryStoreInit(t *testing.T) {
|
|||||||
|
|
||||||
assertNumChannels(t, ns, 0)
|
assertNumChannels(t, ns, 0)
|
||||||
assertNumPreschools(t, ns, 0)
|
assertNumPreschools(t, ns, 0)
|
||||||
assertLastFinalizedHeight(t, ns, 0)
|
|
||||||
assertLastGraduatedHeight(t, ns, 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestNurseryStoreIncubate tests the primary state transitions taken by outputs
|
// 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
|
// Now, move the commitment output to the kindergarten
|
||||||
// bucket.
|
// bucket.
|
||||||
err = ns.PreschoolToKinder(test.commOutput)
|
err = ns.PreschoolToKinder(test.commOutput, 0)
|
||||||
if err != test.err {
|
if err != test.err {
|
||||||
t.Fatalf("unable to move commitment output from "+
|
t.Fatalf("unable to move commitment output from "+
|
||||||
"pscl to kndr: %v", err)
|
"pscl to kndr: %v", err)
|
||||||
@ -212,7 +210,7 @@ func TestNurseryStoreIncubate(t *testing.T) {
|
|||||||
maturityHeight := test.commOutput.ConfHeight() +
|
maturityHeight := test.commOutput.ConfHeight() +
|
||||||
test.commOutput.BlocksToMaturity()
|
test.commOutput.BlocksToMaturity()
|
||||||
|
|
||||||
err = ns.GraduateKinder(maturityHeight)
|
err = ns.GraduateKinder(maturityHeight, test.commOutput)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to graduate kindergarten class at "+
|
t.Fatalf("unable to graduate kindergarten class at "+
|
||||||
"height %d: %v", maturityHeight, err)
|
"height %d: %v", maturityHeight, err)
|
||||||
@ -286,7 +284,8 @@ func TestNurseryStoreIncubate(t *testing.T) {
|
|||||||
maturityHeight := htlcOutput.ConfHeight() +
|
maturityHeight := htlcOutput.ConfHeight() +
|
||||||
htlcOutput.BlocksToMaturity()
|
htlcOutput.BlocksToMaturity()
|
||||||
|
|
||||||
err = ns.GraduateKinder(maturityHeight)
|
err = ns.GraduateKinder(maturityHeight,
|
||||||
|
&htlcOutput.kidOutput)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to graduate htlc output "+
|
t.Fatalf("unable to graduate htlc output "+
|
||||||
"from kndr to grad: %v", err)
|
"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
|
// TestNurseryStoreGraduate verifies that the nursery store properly removes
|
||||||
// populated entries from the height index as it is purged, and that the last
|
// populated entries from the height index as it is purged, and that the last
|
||||||
// purged height is set appropriately.
|
// purged height is set appropriately.
|
||||||
@ -441,9 +353,6 @@ func TestNurseryStoreGraduate(t *testing.T) {
|
|||||||
// height index.
|
// height index.
|
||||||
maturityHeight := kid.ConfHeight() + kid.BlocksToMaturity()
|
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
|
// First, add a commitment output to the nursery store, which is
|
||||||
// initially inserted in the preschool bucket.
|
// initially inserted in the preschool bucket.
|
||||||
err = ns.Incubate([]kidOutput{*kid}, nil)
|
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
|
// Then, move the commitment output to the kindergarten bucket, such
|
||||||
// that it resides in the height index at its maturity height.
|
// that it resides in the height index at its maturity height.
|
||||||
err = ns.PreschoolToKinder(kid)
|
err = ns.PreschoolToKinder(kid, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to move pscl output to kndr: %v", err)
|
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
|
// checking that each class is now empty, and that the last purged
|
||||||
// height is set correctly.
|
// height is set correctly.
|
||||||
for i := 0; i < int(maturityHeight); i++ {
|
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))
|
assertHeightIsPurged(t, ns, uint32(i))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -475,27 +378,7 @@ func TestNurseryStoreGraduate(t *testing.T) {
|
|||||||
// height.
|
// height.
|
||||||
assertKndrAtMaturityHeight(t, ns, kid)
|
assertKndrAtMaturityHeight(t, ns, kid)
|
||||||
|
|
||||||
// Finalize the kindergarten transaction, ensuring that it is a non-nil
|
err = ns.GraduateKinder(maturityHeight, kid)
|
||||||
// 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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to graduate kindergarten outputs at height=%d: "+
|
t.Fatalf("unable to graduate kindergarten outputs at height=%d: "+
|
||||||
"%v", maturityHeight, err)
|
"%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
|
// assertNumPreschools loads all preschool outputs and verifies their count
|
||||||
// matches the expected number.
|
// matches the expected number.
|
||||||
func assertNumPreschools(t *testing.T, ns NurseryStore, expected int) {
|
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,
|
func assertHeightIsPurged(t *testing.T, ns NurseryStore,
|
||||||
height uint32) {
|
height uint32) {
|
||||||
|
|
||||||
finalTx, kndrOutputs, cribOutputs, err := ns.FetchClass(height)
|
kndrOutputs, cribOutputs, err := ns.FetchClass(height)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to retrieve class at height=%d: %v",
|
t.Fatalf("unable to retrieve class at height=%d: %v",
|
||||||
height, err)
|
height, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if finalTx != nil {
|
|
||||||
t.Fatalf("height=%d not purged, final txn should be nil", height)
|
|
||||||
}
|
|
||||||
|
|
||||||
if kndrOutputs != nil {
|
if kndrOutputs != nil {
|
||||||
t.Fatalf("height=%d not purged, kndr outputs should be nil", height)
|
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) {
|
htlcOutput *babyOutput) {
|
||||||
|
|
||||||
expiryHeight := htlcOutput.expiry
|
expiryHeight := htlcOutput.expiry
|
||||||
_, _, cribOutputs, err := ns.FetchClass(expiryHeight)
|
_, cribOutputs, err := ns.FetchClass(expiryHeight)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to retrieve class at height=%d: %v",
|
t.Fatalf("unable to retrieve class at height=%d: %v",
|
||||||
expiryHeight, err)
|
expiryHeight, err)
|
||||||
@ -639,7 +488,7 @@ func assertCribNotAtExpiryHeight(t *testing.T, ns NurseryStore,
|
|||||||
htlcOutput *babyOutput) {
|
htlcOutput *babyOutput) {
|
||||||
|
|
||||||
expiryHeight := htlcOutput.expiry
|
expiryHeight := htlcOutput.expiry
|
||||||
_, _, cribOutputs, err := ns.FetchClass(expiryHeight)
|
_, cribOutputs, err := ns.FetchClass(expiryHeight)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to retrieve class at height %d: %v",
|
t.Fatalf("unable to retrieve class at height %d: %v",
|
||||||
expiryHeight, err)
|
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
|
// assertKndrAtMaturityHeight loads the class at the provided height and
|
||||||
// verifies that the provided kid output is one of the kindergarten outputs
|
// verifies that the provided kid output is one of the kindergarten outputs
|
||||||
// returned.
|
// returned.
|
||||||
@ -680,7 +510,7 @@ func assertKndrAtMaturityHeight(t *testing.T, ns NurseryStore,
|
|||||||
|
|
||||||
maturityHeight := kndrOutput.ConfHeight() +
|
maturityHeight := kndrOutput.ConfHeight() +
|
||||||
kndrOutput.BlocksToMaturity()
|
kndrOutput.BlocksToMaturity()
|
||||||
_, kndrOutputs, _, err := ns.FetchClass(maturityHeight)
|
kndrOutputs, _, err := ns.FetchClass(maturityHeight)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to retrieve class at height %d: %v",
|
t.Fatalf("unable to retrieve class at height %d: %v",
|
||||||
maturityHeight, err)
|
maturityHeight, err)
|
||||||
@ -705,7 +535,7 @@ func assertKndrNotAtMaturityHeight(t *testing.T, ns NurseryStore,
|
|||||||
maturityHeight := kndrOutput.ConfHeight() +
|
maturityHeight := kndrOutput.ConfHeight() +
|
||||||
kndrOutput.BlocksToMaturity()
|
kndrOutput.BlocksToMaturity()
|
||||||
|
|
||||||
_, kndrOutputs, _, err := ns.FetchClass(maturityHeight)
|
kndrOutputs, _, err := ns.FetchClass(maturityHeight)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to retrieve class at height %d: %v",
|
t.Fatalf("unable to retrieve class at height %d: %v",
|
||||||
maturityHeight, err)
|
maturityHeight, err)
|
||||||
|
@ -157,7 +157,7 @@ func TestPeerChannelClosureAcceptFeeInitiator(t *testing.T) {
|
|||||||
dummyDeliveryScript),
|
dummyDeliveryScript),
|
||||||
}
|
}
|
||||||
|
|
||||||
estimator := lnwallet.StaticFeeEstimator{FeePerKW: 12500}
|
estimator := lnwallet.NewStaticFeeEstimator(12500, 0)
|
||||||
feePerKw, err := estimator.EstimateFeePerKW(1)
|
feePerKw, err := estimator.EstimateFeePerKW(1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to query fee estimator: %v", err)
|
t.Fatalf("unable to query fee estimator: %v", err)
|
||||||
@ -447,7 +447,7 @@ func TestPeerChannelClosureFeeNegotiationsInitiator(t *testing.T) {
|
|||||||
msg: respShutdown,
|
msg: respShutdown,
|
||||||
}
|
}
|
||||||
|
|
||||||
estimator := lnwallet.StaticFeeEstimator{FeePerKW: 12500}
|
estimator := lnwallet.NewStaticFeeEstimator(12500, 0)
|
||||||
initiatorIdealFeeRate, err := estimator.EstimateFeePerKW(1)
|
initiatorIdealFeeRate, err := estimator.EstimateFeePerKW(1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to query fee estimator: %v", err)
|
t.Fatalf("unable to query fee estimator: %v", err)
|
||||||
|
38
server.go
38
server.go
@ -17,8 +17,6 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/lightningnetwork/lnd/sweep"
|
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/btcec"
|
"github.com/btcsuite/btcd/btcec"
|
||||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
"github.com/btcsuite/btcd/connmgr"
|
"github.com/btcsuite/btcd/connmgr"
|
||||||
@ -40,6 +38,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
"github.com/lightningnetwork/lnd/nat"
|
"github.com/lightningnetwork/lnd/nat"
|
||||||
"github.com/lightningnetwork/lnd/routing"
|
"github.com/lightningnetwork/lnd/routing"
|
||||||
|
"github.com/lightningnetwork/lnd/sweep"
|
||||||
"github.com/lightningnetwork/lnd/ticker"
|
"github.com/lightningnetwork/lnd/ticker"
|
||||||
"github.com/lightningnetwork/lnd/tor"
|
"github.com/lightningnetwork/lnd/tor"
|
||||||
)
|
)
|
||||||
@ -156,6 +155,8 @@ type server struct {
|
|||||||
|
|
||||||
utxoNursery *utxoNursery
|
utxoNursery *utxoNursery
|
||||||
|
|
||||||
|
sweeper *sweep.UtxoSweeper
|
||||||
|
|
||||||
chainArb *contractcourt.ChainArbitrator
|
chainArb *contractcourt.ChainArbitrator
|
||||||
|
|
||||||
sphinx *htlcswitch.OnionProcessor
|
sphinx *htlcswitch.OnionProcessor
|
||||||
@ -597,24 +598,45 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB, cc *chainControl,
|
|||||||
return nil, err
|
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,
|
Estimator: cc.feeEstimator,
|
||||||
GenSweepScript: func() ([]byte, error) {
|
GenSweepScript: func() ([]byte, error) {
|
||||||
return newSweepPkScript(cc.wallet)
|
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{
|
s.utxoNursery = newUtxoNursery(&NurseryConfig{
|
||||||
ChainIO: cc.chainIO,
|
ChainIO: cc.chainIO,
|
||||||
ConfDepth: 1,
|
ConfDepth: 1,
|
||||||
SweepTxConfTarget: 6,
|
|
||||||
FetchClosedChannels: chanDB.FetchClosedChannels,
|
FetchClosedChannels: chanDB.FetchClosedChannels,
|
||||||
FetchClosedChannel: chanDB.FetchClosedChannel,
|
FetchClosedChannel: chanDB.FetchClosedChannel,
|
||||||
Notifier: cc.chainNotifier,
|
Notifier: cc.chainNotifier,
|
||||||
PublishTransaction: cc.wallet.PublishTransaction,
|
PublishTransaction: cc.wallet.PublishTransaction,
|
||||||
Store: utxnStore,
|
Store: utxnStore,
|
||||||
Sweeper: sweeper,
|
Sweeper: s.sweeper,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Construct a closure that wraps the htlcswitch's CloseLink method.
|
// 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 {
|
DisableChannel: func(op wire.OutPoint) error {
|
||||||
return s.announceChanStatus(op, true)
|
return s.announceChanStatus(op, true)
|
||||||
},
|
},
|
||||||
Sweeper: sweeper,
|
Sweeper: s.sweeper,
|
||||||
}, chanDB)
|
}, chanDB)
|
||||||
|
|
||||||
s.breachArbiter = newBreachArbiter(&BreachConfig{
|
s.breachArbiter = newBreachArbiter(&BreachConfig{
|
||||||
@ -963,6 +985,9 @@ func (s *server) Start() error {
|
|||||||
if err := s.htlcSwitch.Start(); err != nil {
|
if err := s.htlcSwitch.Start(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := s.sweeper.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := s.utxoNursery.Start(); err != nil {
|
if err := s.utxoNursery.Start(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -1050,6 +1075,7 @@ func (s *server) Stop() error {
|
|||||||
s.breachArbiter.Stop()
|
s.breachArbiter.Stop()
|
||||||
s.authGossiper.Stop()
|
s.authGossiper.Stop()
|
||||||
s.chainArb.Stop()
|
s.chainArb.Stop()
|
||||||
|
s.sweeper.Stop()
|
||||||
s.cc.wallet.Shutdown()
|
s.cc.wallet.Shutdown()
|
||||||
s.cc.chainView.Stop()
|
s.cc.chainView.Stop()
|
||||||
s.connMgr.Stop()
|
s.connMgr.Stop()
|
||||||
|
110
sweep/backend_mock_test.go
Normal file
110
sweep/backend_mock_test.go
Normal file
@ -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
|
||||||
|
}
|
14
sweep/defaults.go
Normal file
14
sweep/defaults.go
Normal file
@ -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
|
||||||
|
)
|
17
sweep/defaults_rpctest.go
Normal file
17
sweep/defaults_rpctest.go
Normal file
@ -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
|
||||||
|
)
|
62
sweep/fee_estimator_mock_test.go
Normal file
62
sweep/fee_estimator_mock_test.go
Normal file
@ -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)
|
@ -33,12 +33,17 @@ type Input interface {
|
|||||||
// the output can be spent. For non-CSV locked inputs this is always
|
// the output can be spent. For non-CSV locked inputs this is always
|
||||||
// zero.
|
// zero.
|
||||||
BlocksToMaturity() uint32
|
BlocksToMaturity() uint32
|
||||||
|
|
||||||
|
// HeightHint returns the minimum height at which a confirmed spending
|
||||||
|
// tx can occur.
|
||||||
|
HeightHint() uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
type inputKit struct {
|
type inputKit struct {
|
||||||
outpoint wire.OutPoint
|
outpoint wire.OutPoint
|
||||||
witnessType lnwallet.WitnessType
|
witnessType lnwallet.WitnessType
|
||||||
signDesc lnwallet.SignDescriptor
|
signDesc lnwallet.SignDescriptor
|
||||||
|
heightHint uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
// OutPoint returns the breached output's identifier that is to be included as
|
// 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
|
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
|
// BaseInput contains all the information needed to sweep a basic output
|
||||||
// (CSV/CLTV/no time lock)
|
// (CSV/CLTV/no time lock)
|
||||||
type BaseInput struct {
|
type BaseInput struct {
|
||||||
@ -68,13 +79,14 @@ type BaseInput struct {
|
|||||||
// MakeBaseInput assembles a new BaseInput that can be used to construct a
|
// MakeBaseInput assembles a new BaseInput that can be used to construct a
|
||||||
// sweep transaction.
|
// sweep transaction.
|
||||||
func MakeBaseInput(outpoint *wire.OutPoint, witnessType lnwallet.WitnessType,
|
func MakeBaseInput(outpoint *wire.OutPoint, witnessType lnwallet.WitnessType,
|
||||||
signDescriptor *lnwallet.SignDescriptor) BaseInput {
|
signDescriptor *lnwallet.SignDescriptor, heightHint uint32) BaseInput {
|
||||||
|
|
||||||
return BaseInput{
|
return BaseInput{
|
||||||
inputKit{
|
inputKit{
|
||||||
outpoint: *outpoint,
|
outpoint: *outpoint,
|
||||||
witnessType: witnessType,
|
witnessType: witnessType,
|
||||||
signDesc: *signDescriptor,
|
signDesc: *signDescriptor,
|
||||||
|
heightHint: heightHint,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -113,13 +125,14 @@ type HtlcSucceedInput struct {
|
|||||||
// construct a sweep transaction.
|
// construct a sweep transaction.
|
||||||
func MakeHtlcSucceedInput(outpoint *wire.OutPoint,
|
func MakeHtlcSucceedInput(outpoint *wire.OutPoint,
|
||||||
signDescriptor *lnwallet.SignDescriptor,
|
signDescriptor *lnwallet.SignDescriptor,
|
||||||
preimage []byte) HtlcSucceedInput {
|
preimage []byte, heightHint uint32) HtlcSucceedInput {
|
||||||
|
|
||||||
return HtlcSucceedInput{
|
return HtlcSucceedInput{
|
||||||
inputKit: inputKit{
|
inputKit: inputKit{
|
||||||
outpoint: *outpoint,
|
outpoint: *outpoint,
|
||||||
witnessType: lnwallet.HtlcAcceptedRemoteSuccess,
|
witnessType: lnwallet.HtlcAcceptedRemoteSuccess,
|
||||||
signDesc: *signDescriptor,
|
signDesc: *signDescriptor,
|
||||||
|
heightHint: heightHint,
|
||||||
},
|
},
|
||||||
preimage: preimage,
|
preimage: preimage,
|
||||||
}
|
}
|
||||||
|
247
sweep/store.go
Normal file
247
sweep/store.go
Normal file
@ -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)
|
45
sweep/store_mock.go
Normal file
45
sweep/store_mock.go
Normal file
@ -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)
|
153
sweep/store_test.go
Normal file
153
sweep/store_test.go
Normal file
@ -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")
|
||||||
|
}
|
||||||
|
}
|
897
sweep/sweeper.go
897
sweep/sweeper.go
@ -1,24 +1,88 @@
|
|||||||
package sweep
|
package sweep
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/btcsuite/btcd/blockchain"
|
"errors"
|
||||||
"github.com/btcsuite/btcd/txscript"
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
"github.com/btcsuite/btcutil"
|
"github.com/btcsuite/btcutil"
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet"
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UtxoSweeper provides the functionality to generate sweep txes. The plan is
|
var (
|
||||||
// to extend UtxoSweeper in the future to also manage the actual sweeping
|
// ErrRemoteSpend is returned in case an output that we try to sweep is
|
||||||
// process by itself.
|
// 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 {
|
type UtxoSweeper struct {
|
||||||
|
started uint32 // To be used atomically.
|
||||||
|
stopped uint32 // To be used atomically.
|
||||||
|
|
||||||
cfg *UtxoSweeperConfig
|
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.
|
// UtxoSweeperConfig contains dependencies of UtxoSweeper.
|
||||||
type UtxoSweeperConfig struct {
|
type UtxoSweeperConfig struct {
|
||||||
// GenSweepScript generates a P2WKH script belonging to the wallet
|
// GenSweepScript generates a P2WKH script belonging to the wallet where
|
||||||
// where funds can be swept.
|
// funds can be swept.
|
||||||
GenSweepScript func() ([]byte, error)
|
GenSweepScript func() ([]byte, error)
|
||||||
|
|
||||||
// Estimator is used when crafting sweep transactions to estimate the
|
// Estimator is used when crafting sweep transactions to estimate the
|
||||||
@ -26,18 +90,651 @@ type UtxoSweeperConfig struct {
|
|||||||
// transaction.
|
// transaction.
|
||||||
Estimator lnwallet.FeeEstimator
|
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
|
// Signer is used by the sweeper to generate valid witnesses at the
|
||||||
// time the incubated outputs need to be spent.
|
// time the incubated outputs need to be spent.
|
||||||
Signer lnwallet.Signer
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new UtxoSweeper instance.
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
func New(cfg *UtxoSweeperConfig) *UtxoSweeper {
|
||||||
|
|
||||||
return &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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start starts the process of constructing and publish sweep txes.
|
||||||
|
func (s *UtxoSweeper) Start() error {
|
||||||
|
if !atomic.CompareAndSwapUint32(&s.started, 0, 1) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Tracef("Sweeper starting")
|
||||||
|
|
||||||
|
// Retrieve last published tx from database.
|
||||||
|
lastTx, err := s.cfg.Store.GetLastPublishedTx()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get last published tx: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start sweeper main loop.
|
||||||
|
s.wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer blockEpochs.Cancel()
|
||||||
|
defer s.wg.Done()
|
||||||
|
|
||||||
|
err := s.collector(blockEpochs.Epochs, bestHeight)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("sweeper stopped: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("Sweeper shutting down")
|
||||||
|
|
||||||
|
close(s.quit)
|
||||||
|
s.wg.Wait()
|
||||||
|
|
||||||
|
log.Debugf("Sweeper shut down")
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deliver input to main event loop.
|
||||||
|
select {
|
||||||
|
case s.newInputs <- sweeperInput:
|
||||||
|
case <-s.quit:
|
||||||
|
return nil, fmt.Errorf("sweeper shutting down")
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
// 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,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("input partitionings: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("Sweep candidates at height=%v: total_num_pending=%v, "+
|
||||||
|
"total_num_new=%v", currentHeight, len(allSets), len(newSets))
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
|
||||||
|
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
|
// 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
|
// spends from them. This method also makes an accurate fee estimate before
|
||||||
// generating the required witnesses.
|
// generating the required witnesses.
|
||||||
@ -56,179 +753,31 @@ func New(cfg *UtxoSweeperConfig) *UtxoSweeper {
|
|||||||
func (s *UtxoSweeper) CreateSweepTx(inputs []Input, confTarget uint32,
|
func (s *UtxoSweeper) CreateSweepTx(inputs []Input, confTarget uint32,
|
||||||
currentBlockHeight uint32) (*wire.MsgTx, error) {
|
currentBlockHeight uint32) (*wire.MsgTx, error) {
|
||||||
|
|
||||||
|
feePerKw, err := s.cfg.Estimator.EstimateFeePerKW(confTarget)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// Generate the receiving script to which the funds will be swept.
|
// Generate the receiving script to which the funds will be swept.
|
||||||
pkScript, err := s.cfg.GenSweepScript()
|
pkScript, err := s.cfg.GenSweepScript()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Using the txn weight estimate, compute the required txn fee.
|
return createSweepTx(
|
||||||
feePerKw, err := s.cfg.Estimator.EstimateFeePerKW(confTarget)
|
inputs, pkScript, currentBlockHeight, feePerKw, s.cfg.Signer,
|
||||||
if err != nil {
|
|
||||||
return nil, 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))
|
|
||||||
|
|
||||||
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: pkScript,
|
|
||||||
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(
|
|
||||||
s.cfg.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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// getWeightEstimate returns a weight estimate for the given inputs.
|
// DefaultNextAttemptDeltaFunc is the default calculation for next sweep attempt
|
||||||
// Additionally, it returns counts for the number of csv and cltv inputs.
|
// scheduling. It implements exponential back-off with some randomness. This is
|
||||||
func (s *UtxoSweeper) getWeightEstimate(inputs []Input) ([]Input, int64, int, int) {
|
// to prevent a stuck tx (for example because fee is too low and can't be bumped
|
||||||
// We initialize a weight estimator so we can accurately asses the
|
// in btcd) from blocking all other retried inputs in the same tx.
|
||||||
// amount of fees we need to pay for this sweep transaction.
|
func DefaultNextAttemptDeltaFunc(attempts int) int32 {
|
||||||
//
|
return 1 + rand.Int31n(1<<uint(attempts-1))
|
||||||
// TODO(roasbeef): can be more intelligent about buffering outputs to
|
}
|
||||||
// be more efficient on-chain.
|
|
||||||
var weightEstimate lnwallet.TxWeightEstimator
|
// init initializes the random generator for random input rescheduling.
|
||||||
|
func init() {
|
||||||
// Our sweep transaction will pay to a single segwit p2wkh address,
|
rand.Seed(time.Now().Unix())
|
||||||
// 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]
|
|
||||||
|
|
||||||
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++
|
|
||||||
|
|
||||||
// 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++
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
|
|
||||||
default:
|
|
||||||
log.Warnf("kindergarten output in nursery store "+
|
|
||||||
"contains unexpected witness type: %v",
|
|
||||||
input.WitnessType())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
txWeight := int64(weightEstimate.Weight())
|
|
||||||
|
|
||||||
return sweepInputs, txWeight, csvCount, cltvCount
|
|
||||||
}
|
}
|
||||||
|
902
sweep/sweeper_test.go
Normal file
902
sweep/sweeper_test.go
Normal file
@ -0,0 +1,902 @@
|
|||||||
|
package sweep
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"runtime/debug"
|
||||||
|
"runtime/pprof"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec"
|
||||||
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/lightningnetwork/lnd/build"
|
||||||
|
"github.com/lightningnetwork/lnd/keychain"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
testLog = build.NewSubLogger("SWPR_TEST", nil)
|
||||||
|
|
||||||
|
testMaxSweepAttempts = 3
|
||||||
|
|
||||||
|
testMaxInputsPerTx = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
type sweeperTestContext struct {
|
||||||
|
t *testing.T
|
||||||
|
|
||||||
|
sweeper *UtxoSweeper
|
||||||
|
notifier *MockNotifier
|
||||||
|
estimator *mockFeeEstimator
|
||||||
|
backend *mockBackend
|
||||||
|
store *MockSweeperStore
|
||||||
|
|
||||||
|
timeoutChan chan chan time.Time
|
||||||
|
publishChan chan wire.MsgTx
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
spendableInputs []*BaseInput
|
||||||
|
testInputCount int
|
||||||
|
|
||||||
|
testPubKey, _ = btcec.ParsePubKey([]byte{
|
||||||
|
0x04, 0x11, 0xdb, 0x93, 0xe1, 0xdc, 0xdb, 0x8a,
|
||||||
|
0x01, 0x6b, 0x49, 0x84, 0x0f, 0x8c, 0x53, 0xbc, 0x1e,
|
||||||
|
0xb6, 0x8a, 0x38, 0x2e, 0x97, 0xb1, 0x48, 0x2e, 0xca,
|
||||||
|
0xd7, 0xb1, 0x48, 0xa6, 0x90, 0x9a, 0x5c, 0xb2, 0xe0,
|
||||||
|
0xea, 0xdd, 0xfb, 0x84, 0xcc, 0xf9, 0x74, 0x44, 0x64,
|
||||||
|
0xf8, 0x2e, 0x16, 0x0b, 0xfa, 0x9b, 0x8b, 0x64, 0xf9,
|
||||||
|
0xd4, 0xc0, 0x3f, 0x99, 0x9b, 0x86, 0x43, 0xf6, 0x56,
|
||||||
|
0xb4, 0x12, 0xa3,
|
||||||
|
}, btcec.S256())
|
||||||
|
)
|
||||||
|
|
||||||
|
func createTestInput(value int64, witnessType lnwallet.WitnessType) BaseInput {
|
||||||
|
hash := chainhash.Hash{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
byte(testInputCount)}
|
||||||
|
|
||||||
|
input := MakeBaseInput(
|
||||||
|
&wire.OutPoint{
|
||||||
|
Hash: hash,
|
||||||
|
},
|
||||||
|
witnessType,
|
||||||
|
&lnwallet.SignDescriptor{
|
||||||
|
Output: &wire.TxOut{
|
||||||
|
Value: value,
|
||||||
|
},
|
||||||
|
KeyDesc: keychain.KeyDescriptor{
|
||||||
|
PubKey: testPubKey,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
|
||||||
|
testInputCount++
|
||||||
|
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Create a set of test spendable inputs.
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
input := createTestInput(int64(10000+i*500),
|
||||||
|
lnwallet.CommitmentTimeLock)
|
||||||
|
|
||||||
|
spendableInputs = append(spendableInputs, &input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createSweeperTestContext(t *testing.T) *sweeperTestContext {
|
||||||
|
notifier := NewMockNotifier(t)
|
||||||
|
|
||||||
|
store := NewMockSweeperStore()
|
||||||
|
|
||||||
|
backend := newMockBackend(notifier)
|
||||||
|
|
||||||
|
estimator := newMockFeeEstimator(10000, 1000)
|
||||||
|
|
||||||
|
publishChan := make(chan wire.MsgTx, 2)
|
||||||
|
ctx := &sweeperTestContext{
|
||||||
|
notifier: notifier,
|
||||||
|
publishChan: publishChan,
|
||||||
|
t: t,
|
||||||
|
estimator: estimator,
|
||||||
|
backend: backend,
|
||||||
|
store: store,
|
||||||
|
timeoutChan: make(chan chan time.Time, 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
var outputScriptCount byte
|
||||||
|
ctx.sweeper = New(&UtxoSweeperConfig{
|
||||||
|
Notifier: notifier,
|
||||||
|
PublishTransaction: func(tx *wire.MsgTx) error {
|
||||||
|
log.Tracef("Publishing tx %v", tx.TxHash())
|
||||||
|
err := backend.publishTransaction(tx)
|
||||||
|
select {
|
||||||
|
case publishChan <- *tx:
|
||||||
|
case <-time.After(defaultTestTimeout):
|
||||||
|
t.Fatalf("unexpected tx published")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
NewBatchTimer: func() <-chan time.Time {
|
||||||
|
c := make(chan time.Time, 1)
|
||||||
|
ctx.timeoutChan <- c
|
||||||
|
return c
|
||||||
|
},
|
||||||
|
Store: store,
|
||||||
|
Signer: &mockSigner{},
|
||||||
|
SweepTxConfTarget: 1,
|
||||||
|
ChainIO: &mockChainIO{},
|
||||||
|
GenSweepScript: func() ([]byte, error) {
|
||||||
|
script := []byte{outputScriptCount}
|
||||||
|
outputScriptCount++
|
||||||
|
return script, nil
|
||||||
|
},
|
||||||
|
Estimator: estimator,
|
||||||
|
MaxInputsPerTx: testMaxInputsPerTx,
|
||||||
|
MaxSweepAttempts: testMaxSweepAttempts,
|
||||||
|
NextAttemptDeltaFunc: func(attempts int) int32 {
|
||||||
|
// Use delta func without random factor.
|
||||||
|
return 1 << uint(attempts-1)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx.sweeper.Start()
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *sweeperTestContext) tick() {
|
||||||
|
testLog.Trace("Waiting for tick to be consumed")
|
||||||
|
select {
|
||||||
|
case c := <-ctx.timeoutChan:
|
||||||
|
select {
|
||||||
|
case c <- time.Time{}:
|
||||||
|
testLog.Trace("Tick")
|
||||||
|
case <-time.After(defaultTestTimeout):
|
||||||
|
debug.PrintStack()
|
||||||
|
ctx.t.Fatal("tick timeout - tick not consumed")
|
||||||
|
}
|
||||||
|
case <-time.After(defaultTestTimeout):
|
||||||
|
debug.PrintStack()
|
||||||
|
ctx.t.Fatal("tick timeout - no new timer created")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *sweeperTestContext) assertNoNewTimer() {
|
||||||
|
select {
|
||||||
|
case <-ctx.timeoutChan:
|
||||||
|
ctx.t.Fatal("no new timer expected")
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *sweeperTestContext) finish(expectedGoroutineCount int) {
|
||||||
|
// We assume that when finish is called, sweeper has finished all its
|
||||||
|
// goroutines. This implies that the waitgroup is empty.
|
||||||
|
signalChan := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
ctx.sweeper.wg.Wait()
|
||||||
|
close(signalChan)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Simulate exits of the expected number of running goroutines.
|
||||||
|
for i := 0; i < expectedGoroutineCount; i++ {
|
||||||
|
ctx.sweeper.wg.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
// We now expect the Wait to succeed.
|
||||||
|
select {
|
||||||
|
case <-signalChan:
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
pprof.Lookup("goroutine").WriteTo(os.Stdout, 1)
|
||||||
|
|
||||||
|
ctx.t.Fatalf("lingering goroutines detected after test " +
|
||||||
|
"is finished")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore waitgroup state to what it was before.
|
||||||
|
ctx.sweeper.wg.Add(expectedGoroutineCount)
|
||||||
|
|
||||||
|
// Stop sweeper.
|
||||||
|
ctx.sweeper.Stop()
|
||||||
|
|
||||||
|
// We should have consumed and asserted all published transactions in
|
||||||
|
// our unit tests.
|
||||||
|
ctx.assertNoTx()
|
||||||
|
ctx.assertNoNewTimer()
|
||||||
|
if !ctx.backend.isDone() {
|
||||||
|
ctx.t.Fatal("unconfirmed txes remaining")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *sweeperTestContext) assertNoTx() {
|
||||||
|
ctx.t.Helper()
|
||||||
|
select {
|
||||||
|
case <-ctx.publishChan:
|
||||||
|
ctx.t.Fatalf("unexpected transactions published")
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *sweeperTestContext) receiveTx() wire.MsgTx {
|
||||||
|
ctx.t.Helper()
|
||||||
|
var tx wire.MsgTx
|
||||||
|
select {
|
||||||
|
case tx = <-ctx.publishChan:
|
||||||
|
return tx
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
pprof.Lookup("goroutine").WriteTo(os.Stdout, 1)
|
||||||
|
|
||||||
|
ctx.t.Fatalf("tx not published")
|
||||||
|
}
|
||||||
|
return tx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *sweeperTestContext) expectResult(c chan Result, expected error) {
|
||||||
|
ctx.t.Helper()
|
||||||
|
select {
|
||||||
|
case result := <-c:
|
||||||
|
if result.Err != expected {
|
||||||
|
ctx.t.Fatalf("expected %v result, but got %v",
|
||||||
|
expected, result.Err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case <-time.After(defaultTestTimeout):
|
||||||
|
ctx.t.Fatalf("no result received")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSuccess tests the sweeper happy flow.
|
||||||
|
func TestSuccess(t *testing.T) {
|
||||||
|
ctx := createSweeperTestContext(t)
|
||||||
|
|
||||||
|
resultChan, err := ctx.sweeper.SweepInput(spendableInputs[0])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.tick()
|
||||||
|
|
||||||
|
sweepTx := ctx.receiveTx()
|
||||||
|
|
||||||
|
ctx.backend.mine()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case result := <-resultChan:
|
||||||
|
if result.Err != nil {
|
||||||
|
t.Fatalf("expected successful spend, but received "+
|
||||||
|
"error %v instead", result.Err)
|
||||||
|
}
|
||||||
|
if result.Tx.TxHash() != sweepTx.TxHash() {
|
||||||
|
t.Fatalf("expected sweep tx ")
|
||||||
|
}
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatalf("no result received")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.finish(1)
|
||||||
|
|
||||||
|
// Assert that last tx is stored in the database so we can republish
|
||||||
|
// on restart.
|
||||||
|
lastTx, err := ctx.store.GetLastPublishedTx()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if lastTx == nil || sweepTx.TxHash() != lastTx.TxHash() {
|
||||||
|
t.Fatalf("last tx not stored")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDust asserts that inputs that are not big enough to raise above the dust
|
||||||
|
// limit, are held back until the total set does surpass the limit.
|
||||||
|
func TestDust(t *testing.T) {
|
||||||
|
ctx := createSweeperTestContext(t)
|
||||||
|
|
||||||
|
// Sweeping a single output produces a tx of 486 weight units. With the
|
||||||
|
// test fee rate, the sweep tx will pay 4860 sat in fees.
|
||||||
|
//
|
||||||
|
// Create an input so that the output after paying fees is still
|
||||||
|
// positive (400 sat), but less than the dust limit (537 sat) for the
|
||||||
|
// sweep tx output script (P2WPKH).
|
||||||
|
dustInput := createTestInput(5260, lnwallet.CommitmentTimeLock)
|
||||||
|
|
||||||
|
_, err := ctx.sweeper.SweepInput(&dustInput)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// No sweep transaction is expected now. The sweeper should recognize
|
||||||
|
// that the sweep output will not be relayed and not generate the tx.
|
||||||
|
|
||||||
|
// Sweep another input that brings the tx output above the dust limit.
|
||||||
|
largeInput := createTestInput(100000, lnwallet.CommitmentTimeLock)
|
||||||
|
|
||||||
|
_, err = ctx.sweeper.SweepInput(&largeInput)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.tick()
|
||||||
|
|
||||||
|
// The second input brings the sweep output above the dust limit. We
|
||||||
|
// expect a sweep tx now.
|
||||||
|
|
||||||
|
sweepTx := ctx.receiveTx()
|
||||||
|
if len(sweepTx.TxIn) != 2 {
|
||||||
|
t.Fatalf("Expected tx to sweep 2 inputs, but contains %v "+
|
||||||
|
"inputs instead", len(sweepTx.TxIn))
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.backend.mine()
|
||||||
|
|
||||||
|
ctx.finish(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNegativeInput asserts that no inputs with a negative yield are swept.
|
||||||
|
// Negative yield means that the value minus the added fee is negative.
|
||||||
|
func TestNegativeInput(t *testing.T) {
|
||||||
|
ctx := createSweeperTestContext(t)
|
||||||
|
|
||||||
|
// Sweep an input large enough to cover fees, so in any case the tx
|
||||||
|
// output will be above the dust limit.
|
||||||
|
largeInput := createTestInput(100000, lnwallet.CommitmentNoDelay)
|
||||||
|
largeInputResult, err := ctx.sweeper.SweepInput(&largeInput)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sweep an additional input with a negative net yield. The weight of
|
||||||
|
// the HtlcAcceptedRemoteSuccess input type adds more in fees than its
|
||||||
|
// value at the current fee level.
|
||||||
|
negInput := createTestInput(2900, lnwallet.HtlcOfferedRemoteTimeout)
|
||||||
|
negInputResult, err := ctx.sweeper.SweepInput(&negInput)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sweep a third input that has a smaller output than the previous one,
|
||||||
|
// but yields positively because of its lower weight.
|
||||||
|
positiveInput := createTestInput(2800, lnwallet.CommitmentNoDelay)
|
||||||
|
positiveInputResult, err := ctx.sweeper.SweepInput(&positiveInput)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.tick()
|
||||||
|
|
||||||
|
// We expect that a sweep tx is published now, but it should only
|
||||||
|
// contain the large input. The negative input should stay out of sweeps
|
||||||
|
// until fees come down to get a positive net yield.
|
||||||
|
sweepTx1 := ctx.receiveTx()
|
||||||
|
|
||||||
|
if !testTxIns(&sweepTx1, []*wire.OutPoint{
|
||||||
|
largeInput.OutPoint(), positiveInput.OutPoint(),
|
||||||
|
}) {
|
||||||
|
t.Fatal("Tx does not contain expected inputs")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.backend.mine()
|
||||||
|
|
||||||
|
ctx.expectResult(largeInputResult, nil)
|
||||||
|
ctx.expectResult(positiveInputResult, nil)
|
||||||
|
|
||||||
|
// Lower fee rate so that the negative input is no longer negative.
|
||||||
|
ctx.estimator.updateFees(1000, 1000)
|
||||||
|
|
||||||
|
// Create another large input
|
||||||
|
secondLargeInput := createTestInput(100000, lnwallet.CommitmentNoDelay)
|
||||||
|
secondLargeInputResult, err := ctx.sweeper.SweepInput(&secondLargeInput)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.tick()
|
||||||
|
|
||||||
|
sweepTx2 := ctx.receiveTx()
|
||||||
|
if !testTxIns(&sweepTx2, []*wire.OutPoint{
|
||||||
|
secondLargeInput.OutPoint(), negInput.OutPoint(),
|
||||||
|
}) {
|
||||||
|
t.Fatal("Tx does not contain expected inputs")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.backend.mine()
|
||||||
|
|
||||||
|
ctx.expectResult(secondLargeInputResult, nil)
|
||||||
|
ctx.expectResult(negInputResult, nil)
|
||||||
|
|
||||||
|
ctx.finish(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTxIns(tx *wire.MsgTx, inputs []*wire.OutPoint) bool {
|
||||||
|
if len(tx.TxIn) != len(inputs) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
ins := make(map[wire.OutPoint]struct{})
|
||||||
|
for _, in := range tx.TxIn {
|
||||||
|
ins[in.PreviousOutPoint] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, expectedIn := range inputs {
|
||||||
|
if _, ok := ins[*expectedIn]; !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestChunks asserts that large sets of inputs are split into multiple txes.
|
||||||
|
func TestChunks(t *testing.T) {
|
||||||
|
ctx := createSweeperTestContext(t)
|
||||||
|
|
||||||
|
// Sweep five inputs.
|
||||||
|
for _, input := range spendableInputs[:5] {
|
||||||
|
_, err := ctx.sweeper.SweepInput(input)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.tick()
|
||||||
|
|
||||||
|
// We expect two txes to be published because of the max input count of
|
||||||
|
// three.
|
||||||
|
sweepTx1 := ctx.receiveTx()
|
||||||
|
if len(sweepTx1.TxIn) != 3 {
|
||||||
|
t.Fatalf("Expected first tx to sweep 3 inputs, but contains %v "+
|
||||||
|
"inputs instead", len(sweepTx1.TxIn))
|
||||||
|
}
|
||||||
|
|
||||||
|
sweepTx2 := ctx.receiveTx()
|
||||||
|
if len(sweepTx2.TxIn) != 2 {
|
||||||
|
t.Fatalf("Expected first tx to sweep 2 inputs, but contains %v "+
|
||||||
|
"inputs instead", len(sweepTx1.TxIn))
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.backend.mine()
|
||||||
|
|
||||||
|
ctx.finish(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRemoteSpend asserts that remote spends are properly detected and handled
|
||||||
|
// both before the sweep is published as well as after.
|
||||||
|
func TestRemoteSpend(t *testing.T) {
|
||||||
|
t.Run("pre-sweep", func(t *testing.T) {
|
||||||
|
testRemoteSpend(t, false)
|
||||||
|
})
|
||||||
|
t.Run("post-sweep", func(t *testing.T) {
|
||||||
|
testRemoteSpend(t, true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRemoteSpend(t *testing.T, postSweep bool) {
|
||||||
|
ctx := createSweeperTestContext(t)
|
||||||
|
|
||||||
|
resultChan1, err := ctx.sweeper.SweepInput(spendableInputs[0])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resultChan2, err := ctx.sweeper.SweepInput(spendableInputs[1])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spend the input with an unknown tx.
|
||||||
|
remoteTx := &wire.MsgTx{
|
||||||
|
TxIn: []*wire.TxIn{
|
||||||
|
{
|
||||||
|
PreviousOutPoint: *(spendableInputs[0].OutPoint()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err = ctx.backend.publishTransaction(remoteTx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if postSweep {
|
||||||
|
ctx.tick()
|
||||||
|
|
||||||
|
// Tx publication by sweeper returns ErrDoubleSpend. Sweeper
|
||||||
|
// will retry the inputs without reporting a result. It could be
|
||||||
|
// spent by the remote party.
|
||||||
|
ctx.receiveTx()
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.backend.mine()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case result := <-resultChan1:
|
||||||
|
if result.Err != ErrRemoteSpend {
|
||||||
|
t.Fatalf("expected remote spend")
|
||||||
|
}
|
||||||
|
if result.Tx.TxHash() != remoteTx.TxHash() {
|
||||||
|
t.Fatalf("expected remote spend tx")
|
||||||
|
}
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatalf("no result received")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !postSweep {
|
||||||
|
// Assert that the sweeper sweeps the remaining input.
|
||||||
|
ctx.tick()
|
||||||
|
sweepTx := ctx.receiveTx()
|
||||||
|
|
||||||
|
if len(sweepTx.TxIn) != 1 {
|
||||||
|
t.Fatal("expected sweep to only sweep the one remaining output")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.backend.mine()
|
||||||
|
|
||||||
|
ctx.expectResult(resultChan2, nil)
|
||||||
|
|
||||||
|
ctx.finish(1)
|
||||||
|
} else {
|
||||||
|
// Expected sweeper to be still listening for spend of the
|
||||||
|
// error input.
|
||||||
|
ctx.finish(2)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-resultChan2:
|
||||||
|
t.Fatalf("no result expected for error input")
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestIdempotency asserts that offering the same input multiple times is
|
||||||
|
// handled correctly.
|
||||||
|
func TestIdempotency(t *testing.T) {
|
||||||
|
ctx := createSweeperTestContext(t)
|
||||||
|
|
||||||
|
resultChan1, err := ctx.sweeper.SweepInput(spendableInputs[0])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resultChan2, err := ctx.sweeper.SweepInput(spendableInputs[0])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.tick()
|
||||||
|
|
||||||
|
ctx.receiveTx()
|
||||||
|
|
||||||
|
resultChan3, err := ctx.sweeper.SweepInput(spendableInputs[0])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spend the input of the sweep tx.
|
||||||
|
ctx.backend.mine()
|
||||||
|
|
||||||
|
ctx.expectResult(resultChan1, nil)
|
||||||
|
ctx.expectResult(resultChan2, nil)
|
||||||
|
ctx.expectResult(resultChan3, nil)
|
||||||
|
|
||||||
|
// Offer the same input again. The sweeper will register a spend ntfn
|
||||||
|
// for this input. Because the input has already been spent, it will
|
||||||
|
// immediately receive the spend notification with a spending tx hash.
|
||||||
|
// Because the sweeper kept track of all of its sweep txes, it will
|
||||||
|
// recognize the spend as its own.
|
||||||
|
resultChan4, err := ctx.sweeper.SweepInput(spendableInputs[0])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ctx.expectResult(resultChan4, nil)
|
||||||
|
|
||||||
|
// Timer is still running, but spend notification was delivered before
|
||||||
|
// it expired.
|
||||||
|
ctx.tick()
|
||||||
|
|
||||||
|
ctx.finish(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNoInputs asserts that nothing happens if nothing happens.
|
||||||
|
func TestNoInputs(t *testing.T) {
|
||||||
|
ctx := createSweeperTestContext(t)
|
||||||
|
|
||||||
|
// No tx should appear. This is asserted in finish().
|
||||||
|
ctx.finish(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRestart asserts that the sweeper picks up sweeping properly after
|
||||||
|
// a restart.
|
||||||
|
func TestRestart(t *testing.T) {
|
||||||
|
ctx := createSweeperTestContext(t)
|
||||||
|
|
||||||
|
// Sweep input and expect sweep tx.
|
||||||
|
_, err := ctx.sweeper.SweepInput(spendableInputs[0])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ctx.tick()
|
||||||
|
|
||||||
|
ctx.receiveTx()
|
||||||
|
|
||||||
|
// Restart sweeper.
|
||||||
|
ctx.sweeper.Stop()
|
||||||
|
|
||||||
|
ctx.sweeper = New(ctx.sweeper.cfg)
|
||||||
|
ctx.sweeper.Start()
|
||||||
|
|
||||||
|
// Expect last tx to be republished.
|
||||||
|
ctx.receiveTx()
|
||||||
|
|
||||||
|
// Simulate other subsystem (eg contract resolver) re-offering inputs.
|
||||||
|
spendChan1, err := ctx.sweeper.SweepInput(spendableInputs[0])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
spendChan2, err := ctx.sweeper.SweepInput(spendableInputs[1])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spend inputs of sweep txes and verify that spend channels signal
|
||||||
|
// spends.
|
||||||
|
ctx.backend.mine()
|
||||||
|
|
||||||
|
// Sweeper should recognize that its sweep tx of the previous run is
|
||||||
|
// spending the input.
|
||||||
|
select {
|
||||||
|
case result := <-spendChan1:
|
||||||
|
if result.Err != nil {
|
||||||
|
t.Fatalf("expected successful sweep")
|
||||||
|
}
|
||||||
|
case <-time.After(defaultTestTimeout):
|
||||||
|
t.Fatalf("no result received")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timer tick should trigger republishing a sweep for the remaining
|
||||||
|
// input.
|
||||||
|
ctx.tick()
|
||||||
|
|
||||||
|
ctx.receiveTx()
|
||||||
|
|
||||||
|
ctx.backend.mine()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case result := <-spendChan2:
|
||||||
|
if result.Err != nil {
|
||||||
|
t.Fatalf("expected successful sweep")
|
||||||
|
}
|
||||||
|
case <-time.After(defaultTestTimeout):
|
||||||
|
t.Fatalf("no result received")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restart sweeper again. No action is expected.
|
||||||
|
ctx.sweeper.Stop()
|
||||||
|
ctx.sweeper = New(ctx.sweeper.cfg)
|
||||||
|
ctx.sweeper.Start()
|
||||||
|
|
||||||
|
// Expect last tx to be republished.
|
||||||
|
ctx.receiveTx()
|
||||||
|
|
||||||
|
ctx.finish(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRestartRemoteSpend asserts that the sweeper picks up sweeping properly after
|
||||||
|
// a restart with remote spend.
|
||||||
|
func TestRestartRemoteSpend(t *testing.T) {
|
||||||
|
|
||||||
|
ctx := createSweeperTestContext(t)
|
||||||
|
|
||||||
|
// Sweep input.
|
||||||
|
_, err := ctx.sweeper.SweepInput(spendableInputs[0])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sweep another input.
|
||||||
|
_, err = ctx.sweeper.SweepInput(spendableInputs[1])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.tick()
|
||||||
|
|
||||||
|
sweepTx := ctx.receiveTx()
|
||||||
|
|
||||||
|
// Restart sweeper.
|
||||||
|
ctx.sweeper.Stop()
|
||||||
|
|
||||||
|
ctx.sweeper = New(ctx.sweeper.cfg)
|
||||||
|
ctx.sweeper.Start()
|
||||||
|
|
||||||
|
// Expect last tx to be republished.
|
||||||
|
ctx.receiveTx()
|
||||||
|
|
||||||
|
// Replace the sweep tx with a remote tx spending input 1.
|
||||||
|
ctx.backend.deleteUnconfirmed(sweepTx.TxHash())
|
||||||
|
|
||||||
|
remoteTx := &wire.MsgTx{
|
||||||
|
TxIn: []*wire.TxIn{
|
||||||
|
{
|
||||||
|
PreviousOutPoint: *(spendableInputs[1].OutPoint()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err = ctx.backend.publishTransaction(remoteTx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mine remote spending tx.
|
||||||
|
ctx.backend.mine()
|
||||||
|
|
||||||
|
// Simulate other subsystem (eg contract resolver) re-offering input 0.
|
||||||
|
spendChan, err := ctx.sweeper.SweepInput(spendableInputs[0])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expect sweeper to construct a new tx, because input 1 was spend
|
||||||
|
// remotely.
|
||||||
|
ctx.tick()
|
||||||
|
|
||||||
|
ctx.receiveTx()
|
||||||
|
|
||||||
|
ctx.backend.mine()
|
||||||
|
|
||||||
|
ctx.expectResult(spendChan, nil)
|
||||||
|
|
||||||
|
ctx.finish(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRestartConfirmed asserts that the sweeper picks up sweeping properly after
|
||||||
|
// a restart with a confirm of our own sweep tx.
|
||||||
|
func TestRestartConfirmed(t *testing.T) {
|
||||||
|
ctx := createSweeperTestContext(t)
|
||||||
|
|
||||||
|
// Sweep input.
|
||||||
|
_, err := ctx.sweeper.SweepInput(spendableInputs[0])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.tick()
|
||||||
|
|
||||||
|
ctx.receiveTx()
|
||||||
|
|
||||||
|
// Restart sweeper.
|
||||||
|
ctx.sweeper.Stop()
|
||||||
|
|
||||||
|
ctx.sweeper = New(ctx.sweeper.cfg)
|
||||||
|
ctx.sweeper.Start()
|
||||||
|
|
||||||
|
// Expect last tx to be republished.
|
||||||
|
ctx.receiveTx()
|
||||||
|
|
||||||
|
// Mine the sweep tx.
|
||||||
|
ctx.backend.mine()
|
||||||
|
|
||||||
|
// Simulate other subsystem (eg contract resolver) re-offering input 0.
|
||||||
|
spendChan, err := ctx.sweeper.SweepInput(spendableInputs[0])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Here we expect again a successful sweep.
|
||||||
|
ctx.expectResult(spendChan, nil)
|
||||||
|
|
||||||
|
// Timer started but not needed because spend ntfn was sent.
|
||||||
|
ctx.tick()
|
||||||
|
|
||||||
|
ctx.finish(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRestartRepublish asserts that sweeper republishes the last published
|
||||||
|
// tx on restart.
|
||||||
|
func TestRestartRepublish(t *testing.T) {
|
||||||
|
ctx := createSweeperTestContext(t)
|
||||||
|
|
||||||
|
_, err := ctx.sweeper.SweepInput(spendableInputs[0])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.tick()
|
||||||
|
|
||||||
|
sweepTx := ctx.receiveTx()
|
||||||
|
|
||||||
|
// Restart sweeper again. No action is expected.
|
||||||
|
ctx.sweeper.Stop()
|
||||||
|
ctx.sweeper = New(ctx.sweeper.cfg)
|
||||||
|
ctx.sweeper.Start()
|
||||||
|
|
||||||
|
republishedTx := ctx.receiveTx()
|
||||||
|
|
||||||
|
if sweepTx.TxHash() != republishedTx.TxHash() {
|
||||||
|
t.Fatalf("last tx not republished")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mine the tx to conclude the test properly.
|
||||||
|
ctx.backend.mine()
|
||||||
|
|
||||||
|
ctx.finish(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRetry tests the sweeper retry flow.
|
||||||
|
func TestRetry(t *testing.T) {
|
||||||
|
ctx := createSweeperTestContext(t)
|
||||||
|
|
||||||
|
resultChan0, err := ctx.sweeper.SweepInput(spendableInputs[0])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.tick()
|
||||||
|
|
||||||
|
// We expect a sweep to be published.
|
||||||
|
ctx.receiveTx()
|
||||||
|
|
||||||
|
// New block arrives. This should trigger a new sweep attempt timer
|
||||||
|
// start.
|
||||||
|
ctx.notifier.NotifyEpoch(1000)
|
||||||
|
|
||||||
|
// Offer a fresh input.
|
||||||
|
resultChan1, err := ctx.sweeper.SweepInput(spendableInputs[1])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.tick()
|
||||||
|
|
||||||
|
// Two txes are expected to be published, because new and retry inputs
|
||||||
|
// are separated.
|
||||||
|
ctx.receiveTx()
|
||||||
|
ctx.receiveTx()
|
||||||
|
|
||||||
|
ctx.backend.mine()
|
||||||
|
|
||||||
|
ctx.expectResult(resultChan0, nil)
|
||||||
|
ctx.expectResult(resultChan1, nil)
|
||||||
|
|
||||||
|
ctx.finish(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGiveUp asserts that the sweeper gives up on an input if it can't be swept
|
||||||
|
// after a configured number of attempts.a
|
||||||
|
func TestGiveUp(t *testing.T) {
|
||||||
|
ctx := createSweeperTestContext(t)
|
||||||
|
|
||||||
|
resultChan0, err := ctx.sweeper.SweepInput(spendableInputs[0])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.tick()
|
||||||
|
|
||||||
|
// We expect a sweep to be published at height 100 (mockChainIOHeight).
|
||||||
|
ctx.receiveTx()
|
||||||
|
|
||||||
|
// Because of MaxSweepAttemps, two more sweeps will be attempted. We
|
||||||
|
// configured exponential back-off without randomness for the test. The
|
||||||
|
// second attempt, we expect to happen at 101. The third attempt at 103.
|
||||||
|
// At that point, the input is expected to be failed.
|
||||||
|
|
||||||
|
// Second attempt
|
||||||
|
ctx.notifier.NotifyEpoch(101)
|
||||||
|
ctx.tick()
|
||||||
|
ctx.receiveTx()
|
||||||
|
|
||||||
|
// Third attempt
|
||||||
|
ctx.notifier.NotifyEpoch(103)
|
||||||
|
ctx.tick()
|
||||||
|
ctx.receiveTx()
|
||||||
|
|
||||||
|
ctx.expectResult(resultChan0, ErrTooManyAttempts)
|
||||||
|
|
||||||
|
ctx.backend.mine()
|
||||||
|
|
||||||
|
ctx.finish(1)
|
||||||
|
}
|
258
sweep/test_utils.go
Normal file
258
sweep/test_utils.go
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
package sweep
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"runtime/pprof"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
defaultTestTimeout = 5 * time.Second
|
||||||
|
mockChainIOHeight = int32(100)
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockSigner struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockSigner) SignOutputRaw(tx *wire.MsgTx,
|
||||||
|
signDesc *lnwallet.SignDescriptor) ([]byte, error) {
|
||||||
|
|
||||||
|
return []byte{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockSigner) ComputeInputScript(tx *wire.MsgTx,
|
||||||
|
signDesc *lnwallet.SignDescriptor) (*lnwallet.InputScript, error) {
|
||||||
|
|
||||||
|
return &lnwallet.InputScript{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockNotifier simulates the chain notifier for test purposes. This type is
|
||||||
|
// exported because it is used in nursery tests.
|
||||||
|
type MockNotifier struct {
|
||||||
|
confChannel map[chainhash.Hash]chan *chainntnfs.TxConfirmation
|
||||||
|
epochChan map[chan *chainntnfs.BlockEpoch]int32
|
||||||
|
spendChan map[wire.OutPoint][]chan *chainntnfs.SpendDetail
|
||||||
|
spends map[wire.OutPoint]*wire.MsgTx
|
||||||
|
mutex sync.RWMutex
|
||||||
|
t *testing.T
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockNotifier instantiates a new mock notifier.
|
||||||
|
func NewMockNotifier(t *testing.T) *MockNotifier {
|
||||||
|
return &MockNotifier{
|
||||||
|
confChannel: make(map[chainhash.Hash]chan *chainntnfs.TxConfirmation),
|
||||||
|
epochChan: make(map[chan *chainntnfs.BlockEpoch]int32),
|
||||||
|
spendChan: make(map[wire.OutPoint][]chan *chainntnfs.SpendDetail),
|
||||||
|
spends: make(map[wire.OutPoint]*wire.MsgTx),
|
||||||
|
t: t,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotifyEpoch simulates a new epoch arriving.
|
||||||
|
func (m *MockNotifier) NotifyEpoch(height int32) {
|
||||||
|
for epochChan, chanHeight := range m.epochChan {
|
||||||
|
// Only send notifications if the height is greater than the
|
||||||
|
// height the caller passed into the register call.
|
||||||
|
if chanHeight >= 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
|
||||||
|
}
|
335
sweep/txgenerator.go
Normal file
335
sweep/txgenerator.go
Normal file
@ -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
|
||||||
|
}
|
@ -202,7 +202,7 @@ func createTestPeer(notifier chainntnfs.ChainNotifier,
|
|||||||
return nil, nil, nil, nil, err
|
return nil, nil, nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
estimator := &lnwallet.StaticFeeEstimator{FeePerKW: 12500}
|
estimator := lnwallet.NewStaticFeeEstimator(12500, 0)
|
||||||
feePerKw, err := estimator.EstimateFeePerKW(1)
|
feePerKw, err := estimator.EstimateFeePerKW(1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, nil, err
|
return nil, nil, nil, nil, err
|
||||||
@ -316,7 +316,9 @@ func createTestPeer(notifier chainntnfs.ChainNotifier,
|
|||||||
}
|
}
|
||||||
bobPool.Start()
|
bobPool.Start()
|
||||||
|
|
||||||
chainIO := &mockChainIO{}
|
chainIO := &mockChainIO{
|
||||||
|
bestHeight: fundingBroadcastHeight,
|
||||||
|
}
|
||||||
wallet := &lnwallet.LightningWallet{
|
wallet := &lnwallet.LightningWallet{
|
||||||
WalletController: &mockWalletController{
|
WalletController: &mockWalletController{
|
||||||
rootKey: aliceKeyPriv,
|
rootKey: aliceKeyPriv,
|
||||||
|
363
utxonursery.go
363
utxonursery.go
@ -72,17 +72,6 @@ import (
|
|||||||
// the utxo nursery will sweep all KNDR outputs scheduled for that height
|
// the utxo nursery will sweep all KNDR outputs scheduled for that height
|
||||||
// using a single txn.
|
// 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
|
// - GRAD (kidOutput) outputs are KNDR outputs that have successfully been
|
||||||
// swept into the user's wallet. A channel is considered mature once all of
|
// 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,
|
// 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.
|
// determining outputs in the chain as confirmed.
|
||||||
ConfDepth uint32
|
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
|
// FetchClosedChannels provides access to a user's channels, such that
|
||||||
// they can be marked fully closed after incubation has concluded.
|
// they can be marked fully closed after incubation has concluded.
|
||||||
FetchClosedChannels func(pendingOnly bool) (
|
FetchClosedChannels func(pendingOnly bool) (
|
||||||
@ -254,25 +239,22 @@ func (u *utxoNursery) Start() error {
|
|||||||
|
|
||||||
utxnLog.Tracef("Starting UTXO nursery")
|
utxnLog.Tracef("Starting UTXO nursery")
|
||||||
|
|
||||||
// 1. Start watching for new blocks, as this will drive the nursery
|
// Retrieve the currently best known block. This is needed to have the
|
||||||
// store's state machine.
|
// state machine catch up with the blocks we missed when we were down.
|
||||||
|
bestHash, bestHeight, err := u.cfg.ChainIO.GetBestBlock()
|
||||||
// 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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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.
|
// 2. Flush all fully-graduated channels from the pipeline.
|
||||||
|
|
||||||
// Load any pending close channels, which represents the super set of
|
// Load any pending close channels, which represents the super set of
|
||||||
// all channels that may still be incubating.
|
// all channels that may still be incubating.
|
||||||
pendingCloseChans, err := u.cfg.FetchClosedChannels(true)
|
pendingCloseChans, err := u.cfg.FetchClosedChannels(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
newBlockChan.Cancel()
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -281,7 +263,6 @@ func (u *utxoNursery) Start() error {
|
|||||||
for _, pendingClose := range pendingCloseChans {
|
for _, pendingClose := range pendingCloseChans {
|
||||||
err := u.closeAndRemoveIfMature(&pendingClose.ChanPoint)
|
err := u.closeAndRemoveIfMature(&pendingClose.ChanPoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
newBlockChan.Cancel()
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -289,15 +270,6 @@ func (u *utxoNursery) Start() error {
|
|||||||
// TODO(conner): check if any fully closed channels can be removed from
|
// TODO(conner): check if any fully closed channels can be removed from
|
||||||
// utxn.
|
// 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
|
// 2. Restart spend ntfns for any preschool outputs, which are waiting
|
||||||
// for the force closed commitment txn to confirm, or any second-layer
|
// for the force closed commitment txn to confirm, or any second-layer
|
||||||
// HTLC success transactions.
|
// 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
|
// point forward, we must close the nursery's quit channel if we detect
|
||||||
// any failures during startup to ensure they terminate.
|
// any failures during startup to ensure they terminate.
|
||||||
if err := u.reloadPreschool(); err != nil {
|
if err := u.reloadPreschool(); err != nil {
|
||||||
newBlockChan.Cancel()
|
|
||||||
close(u.quit)
|
close(u.quit)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Replay all crib and kindergarten outputs from last pruned to
|
// 3. Replay all crib and kindergarten outputs up to the current best
|
||||||
// current best height.
|
// height.
|
||||||
if err := u.reloadClasses(lastGraduatedHeight); err != nil {
|
if err := u.reloadClasses(uint32(bestHeight)); err != nil {
|
||||||
newBlockChan.Cancel()
|
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)
|
close(u.quit)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -672,123 +653,44 @@ func (u *utxoNursery) reloadPreschool() error {
|
|||||||
|
|
||||||
// reloadClasses reinitializes any height-dependent state transitions for which
|
// reloadClasses reinitializes any height-dependent state transitions for which
|
||||||
// the utxonursery has not received confirmation, and replays the graduation of
|
// 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
|
// This allows the nursery to reinitialize all state to continue sweeping
|
||||||
// outputs, even in the event that we missed blocks while offline.
|
// outputs, even in the event that we missed blocks while offline. reloadClasses
|
||||||
// reloadClasses is called during the startup of the UTXO Nursery.
|
// is called during the startup of the UTXO Nursery.
|
||||||
func (u *utxoNursery) reloadClasses(lastGradHeight uint32) error {
|
func (u *utxoNursery) reloadClasses(bestHeight uint32) error {
|
||||||
// Begin by loading all of the still-active heights up to and including
|
// Loading all active heights up to and including the current block.
|
||||||
// the last height we successfully graduated.
|
activeHeights, err := u.cfg.Store.HeightsBelowOrEqual(
|
||||||
activeHeights, err := u.cfg.Store.HeightsBelowOrEqual(lastGradHeight)
|
uint32(bestHeight))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(activeHeights) > 0 {
|
// Return early if nothing to sweep.
|
||||||
utxnLog.Infof("Re-registering confirmations for %d already "+
|
if len(activeHeights) == 0 {
|
||||||
"graduated heights below height=%d", len(activeHeights),
|
return nil
|
||||||
lastGradHeight)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
utxnLog.Infof("(Re)-sweeping %d heights below height=%d",
|
||||||
|
len(activeHeights), bestHeight)
|
||||||
|
|
||||||
// Attempt to re-register notifications for any outputs still at these
|
// Attempt to re-register notifications for any outputs still at these
|
||||||
// heights.
|
// heights.
|
||||||
for _, classHeight := range activeHeights {
|
for _, classHeight := range activeHeights {
|
||||||
utxnLog.Debugf("Attempting to regraduate outputs at height=%v",
|
utxnLog.Debugf("Attempting to sweep outputs at height=%v",
|
||||||
classHeight)
|
classHeight)
|
||||||
|
|
||||||
if err = u.regraduateClass(classHeight); err != nil {
|
if err = u.graduateClass(classHeight); err != nil {
|
||||||
utxnLog.Errorf("Failed to regraduate outputs at "+
|
utxnLog.Errorf("Failed to sweep outputs at "+
|
||||||
"height=%v: %v", classHeight, err)
|
"height=%v: %v", classHeight, err)
|
||||||
return 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")
|
utxnLog.Infof("UTXO Nursery is now fully synced")
|
||||||
|
|
||||||
return nil
|
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
|
// incubator is tasked with driving all state transitions that are dependent on
|
||||||
// the current height of the blockchain. As new blocks arrive, the incubator
|
// the current height of the blockchain. As new blocks arrive, the incubator
|
||||||
// will attempt spend outputs at the latest height. The asynchronous
|
// 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
|
// as signing and broadcasting a sweep txn that spends
|
||||||
// from all kindergarten outputs at this height.
|
// from all kindergarten outputs at this height.
|
||||||
height := uint32(epoch.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 {
|
if err := u.graduateClass(height); err != nil {
|
||||||
utxnLog.Errorf("error while graduating "+
|
utxnLog.Errorf("error while graduating "+
|
||||||
"class at height=%d: %v", height, err)
|
"class at height=%d: %v", height, err)
|
||||||
@ -843,14 +750,9 @@ func (u *utxoNursery) graduateClass(classHeight uint32) error {
|
|||||||
u.mu.Lock()
|
u.mu.Lock()
|
||||||
defer u.mu.Unlock()
|
defer u.mu.Unlock()
|
||||||
|
|
||||||
u.bestHeight = classHeight
|
|
||||||
|
|
||||||
// Fetch all information about the crib and kindergarten outputs at
|
// 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.
|
// this height.
|
||||||
finalTx, kgtnOutputs, cribOutputs, err := u.cfg.Store.FetchClass(
|
kgtnOutputs, cribOutputs, err := u.cfg.Store.FetchClass(
|
||||||
classHeight)
|
classHeight)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -859,64 +761,11 @@ func (u *utxoNursery) graduateClass(classHeight uint32) error {
|
|||||||
utxnLog.Infof("Attempting to graduate height=%v: num_kids=%v, "+
|
utxnLog.Infof("Attempting to graduate height=%v: num_kids=%v, "+
|
||||||
"num_babies=%v", classHeight, len(kgtnOutputs), len(cribOutputs))
|
"num_babies=%v", classHeight, len(kgtnOutputs), len(cribOutputs))
|
||||||
|
|
||||||
// Load the last finalized height, so we can determine if the
|
// Offer the outputs to the sweeper and set up notifications that will
|
||||||
// kindergarten sweep txn should be crafted.
|
// transition the swept kindergarten outputs and cltvCrib into graduated
|
||||||
lastFinalizedHeight, err := u.cfg.Store.LastFinalizedHeight()
|
// outputs.
|
||||||
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 {
|
if len(kgtnOutputs) > 0 {
|
||||||
sweepInputs := make([]sweep.Input, len(kgtnOutputs))
|
if err := u.sweepMatureOutputs(classHeight, kgtnOutputs); err != nil {
|
||||||
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 {
|
|
||||||
utxnLog.Errorf("Failed to sweep %d kindergarten "+
|
utxnLog.Errorf("Failed to sweep %d kindergarten "+
|
||||||
"outputs at height=%d: %v",
|
"outputs at height=%d: %v",
|
||||||
len(kgtnOutputs), classHeight, err)
|
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
|
// 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
|
// at this height.
|
||||||
// txid is predetermined when signed in the wallet.
|
|
||||||
for i := range cribOutputs {
|
for i := range cribOutputs {
|
||||||
err := u.sweepCribOutput(classHeight, &cribOutputs[i])
|
err := u.sweepCribOutput(classHeight, &cribOutputs[i])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -937,61 +785,31 @@ func (u *utxoNursery) graduateClass(classHeight uint32) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return u.cfg.Store.GraduateHeight(classHeight)
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// sweepMatureOutputs generates and broadcasts the transaction that transfers
|
// sweepMatureOutputs generates and broadcasts the transaction that transfers
|
||||||
// control of funds from a prior channel commitment transaction to the user's
|
// control of funds from a prior channel commitment transaction to the user's
|
||||||
// wallet. The outputs swept were previously time locked (either absolute or
|
// wallet. The outputs swept were previously time locked (either absolute or
|
||||||
// relative), but are not mature enough to sweep into the wallet.
|
// 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 {
|
kgtnOutputs []kidOutput) error {
|
||||||
|
|
||||||
utxnLog.Infof("Sweeping %v CSV-delayed outputs with sweep tx "+
|
utxnLog.Infof("Sweeping %v CSV-delayed outputs with sweep tx for "+
|
||||||
"(txid=%v): %v", len(kgtnOutputs),
|
"height %v", len(kgtnOutputs), classHeight)
|
||||||
finalTx.TxHash(), newLogClosure(func() string {
|
|
||||||
return spew.Sdump(finalTx)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
// With the sweep transaction fully signed, broadcast the transaction
|
for _, output := range kgtnOutputs {
|
||||||
// to the network. Additionally, we can stop tracking these outputs as
|
// Create local copy to prevent pointer to loop variable to be
|
||||||
// they've just been swept.
|
// passed in with disastruous consequences.
|
||||||
err := u.cfg.PublishTransaction(finalTx)
|
local := output
|
||||||
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)
|
resultChan, err := u.cfg.Sweeper.SweepInput(&local)
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
|
|
||||||
finalTxID := finalTx.TxHash()
|
|
||||||
|
|
||||||
confChan, err := u.cfg.Notifier.RegisterConfirmationsNtfn(
|
|
||||||
&finalTxID, finalTx.TxOut[0].PkScript, u.cfg.ConfDepth,
|
|
||||||
heightHint,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utxnLog.Errorf("unable to register notification for "+
|
|
||||||
"sweep confirmation: %v", finalTxID)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
utxnLog.Infof("Registering sweep tx %v for confs at height=%d",
|
|
||||||
finalTxID, heightHint)
|
|
||||||
|
|
||||||
u.wg.Add(1)
|
u.wg.Add(1)
|
||||||
go u.waitForSweepConf(heightHint, kgtnOutputs, confChan)
|
go u.waitForSweepConf(classHeight, &output, resultChan)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -1002,16 +820,30 @@ func (u *utxoNursery) registerSweepConf(finalTx *wire.MsgTx,
|
|||||||
// to mark any mature channels as fully closed in channeldb.
|
// to mark any mature channels as fully closed in channeldb.
|
||||||
// NOTE(conner): this method MUST be called as a go routine.
|
// NOTE(conner): this method MUST be called as a go routine.
|
||||||
func (u *utxoNursery) waitForSweepConf(classHeight uint32,
|
func (u *utxoNursery) waitForSweepConf(classHeight uint32,
|
||||||
kgtnOutputs []kidOutput, confChan *chainntnfs.ConfirmationEvent) {
|
output *kidOutput, resultChan chan sweep.Result) {
|
||||||
|
|
||||||
defer u.wg.Done()
|
defer u.wg.Done()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case _, ok := <-confChan.Confirmed:
|
case result, ok := <-resultChan:
|
||||||
if !ok {
|
if !ok {
|
||||||
utxnLog.Errorf("Notification chan closed, can't"+
|
utxnLog.Errorf("Notification chan closed, can't" +
|
||||||
" advance %v graduating outputs",
|
" advance graduating output")
|
||||||
len(kgtnOutputs))
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1024,33 +856,24 @@ func (u *utxoNursery) waitForSweepConf(classHeight uint32,
|
|||||||
|
|
||||||
// TODO(conner): add retry logic?
|
// TODO(conner): add retry logic?
|
||||||
|
|
||||||
// Mark the confirmed kindergarten outputs as graduated.
|
// Mark the confirmed kindergarten output as graduated.
|
||||||
if err := u.cfg.Store.GraduateKinder(classHeight); err != nil {
|
if err := u.cfg.Store.GraduateKinder(classHeight, output); err != nil {
|
||||||
utxnLog.Errorf("Unable to graduate %v kindergarten outputs: "+
|
utxnLog.Errorf("Unable to graduate kindergarten output %v: %v",
|
||||||
"%v", len(kgtnOutputs), err)
|
output.OutPoint(), err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
utxnLog.Infof("Graduated %d kindergarten outputs from height=%d",
|
utxnLog.Infof("Graduated kindergarten output from height=%d",
|
||||||
len(kgtnOutputs), classHeight)
|
classHeight)
|
||||||
|
|
||||||
// Iterate over the kid outputs and construct a set of all channel
|
// Attempt to close the channel, only doing so if all of the channel's
|
||||||
// points to which they belong.
|
|
||||||
var possibleCloses = make(map[wire.OutPoint]struct{})
|
|
||||||
for _, kid := range kgtnOutputs {
|
|
||||||
possibleCloses[*kid.OriginChanPoint()] = struct{}{}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to close each channel, only doing so if all of the channel's
|
|
||||||
// outputs have been graduated.
|
// outputs have been graduated.
|
||||||
for chanPoint := range possibleCloses {
|
chanPoint := output.OriginChanPoint()
|
||||||
if err := u.closeAndRemoveIfMature(&chanPoint); err != nil {
|
if err := u.closeAndRemoveIfMature(chanPoint); err != nil {
|
||||||
utxnLog.Errorf("Failed to close and remove channel %v",
|
utxnLog.Errorf("Failed to close and remove channel %v",
|
||||||
chanPoint)
|
*chanPoint)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// sweepCribOutput broadcasts the crib output's htlc timeout txn, and sets up a
|
// sweepCribOutput broadcasts the crib output's htlc timeout txn, and sets up a
|
||||||
@ -1216,7 +1039,8 @@ func (u *utxoNursery) waitForPreschoolConf(kid *kidOutput,
|
|||||||
outputType = "Commitment"
|
outputType = "Commitment"
|
||||||
}
|
}
|
||||||
|
|
||||||
err := u.cfg.Store.PreschoolToKinder(kid)
|
bestHeight := atomic.LoadUint32(&u.bestHeight)
|
||||||
|
err := u.cfg.Store.PreschoolToKinder(kid, bestHeight)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utxnLog.Errorf("Unable to move %v output "+
|
utxnLog.Errorf("Unable to move %v output "+
|
||||||
"from preschool to kindergarten bucket: %v",
|
"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
|
// NOTE: This will only be set for: outgoing HTLC's on the commitment
|
||||||
// transaction of the remote party.
|
// transaction of the remote party.
|
||||||
absoluteMaturity uint32
|
absoluteMaturity uint32
|
||||||
|
|
||||||
confHeight uint32
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeKidOutput(outpoint, originChanPoint *wire.OutPoint,
|
func makeKidOutput(outpoint, originChanPoint *wire.OutPoint,
|
||||||
@ -1569,9 +1391,14 @@ func makeKidOutput(outpoint, originChanPoint *wire.OutPoint,
|
|||||||
isHtlc := (witnessType == lnwallet.HtlcAcceptedSuccessSecondLevel ||
|
isHtlc := (witnessType == lnwallet.HtlcAcceptedSuccessSecondLevel ||
|
||||||
witnessType == lnwallet.HtlcOfferedRemoteTimeout)
|
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{
|
return kidOutput{
|
||||||
breachedOutput: makeBreachedOutput(
|
breachedOutput: makeBreachedOutput(
|
||||||
outpoint, witnessType, nil, signDescriptor,
|
outpoint, witnessType, nil, signDescriptor, heightHint,
|
||||||
),
|
),
|
||||||
isHtlc: isHtlc,
|
isHtlc: isHtlc,
|
||||||
originChanPoint: *originChanPoint,
|
originChanPoint: *originChanPoint,
|
||||||
|
@ -9,8 +9,9 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/sweep"
|
"github.com/lightningnetwork/lnd/sweep"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"math"
|
"math"
|
||||||
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sync"
|
"runtime/pprof"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -19,7 +20,6 @@ import (
|
|||||||
"github.com/btcsuite/btcd/txscript"
|
"github.com/btcsuite/btcd/txscript"
|
||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
"github.com/btcsuite/btcutil"
|
"github.com/btcsuite/btcutil"
|
||||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
|
||||||
"github.com/lightningnetwork/lnd/lnwallet"
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -206,10 +206,10 @@ var (
|
|||||||
amt: btcutil.Amount(13e7),
|
amt: btcutil.Amount(13e7),
|
||||||
outpoint: outPoints[1],
|
outpoint: outPoints[1],
|
||||||
witnessType: lnwallet.CommitmentTimeLock,
|
witnessType: lnwallet.CommitmentTimeLock,
|
||||||
|
confHeight: uint32(1000),
|
||||||
},
|
},
|
||||||
originChanPoint: outPoints[0],
|
originChanPoint: outPoints[0],
|
||||||
blocksToMaturity: uint32(42),
|
blocksToMaturity: uint32(42),
|
||||||
confHeight: uint32(1000),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -217,10 +217,10 @@ var (
|
|||||||
amt: btcutil.Amount(24e7),
|
amt: btcutil.Amount(24e7),
|
||||||
outpoint: outPoints[2],
|
outpoint: outPoints[2],
|
||||||
witnessType: lnwallet.CommitmentTimeLock,
|
witnessType: lnwallet.CommitmentTimeLock,
|
||||||
|
confHeight: uint32(1000),
|
||||||
},
|
},
|
||||||
originChanPoint: outPoints[0],
|
originChanPoint: outPoints[0],
|
||||||
blocksToMaturity: uint32(42),
|
blocksToMaturity: uint32(42),
|
||||||
confHeight: uint32(1000),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -228,10 +228,10 @@ var (
|
|||||||
amt: btcutil.Amount(2e5),
|
amt: btcutil.Amount(2e5),
|
||||||
outpoint: outPoints[3],
|
outpoint: outPoints[3],
|
||||||
witnessType: lnwallet.CommitmentTimeLock,
|
witnessType: lnwallet.CommitmentTimeLock,
|
||||||
|
confHeight: uint32(500),
|
||||||
},
|
},
|
||||||
originChanPoint: outPoints[0],
|
originChanPoint: outPoints[0],
|
||||||
blocksToMaturity: uint32(28),
|
blocksToMaturity: uint32(28),
|
||||||
confHeight: uint32(500),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -239,10 +239,10 @@ var (
|
|||||||
amt: btcutil.Amount(10e6),
|
amt: btcutil.Amount(10e6),
|
||||||
outpoint: outPoints[4],
|
outpoint: outPoints[4],
|
||||||
witnessType: lnwallet.CommitmentTimeLock,
|
witnessType: lnwallet.CommitmentTimeLock,
|
||||||
|
confHeight: uint32(500),
|
||||||
},
|
},
|
||||||
originChanPoint: outPoints[0],
|
originChanPoint: outPoints[0],
|
||||||
blocksToMaturity: uint32(28),
|
blocksToMaturity: uint32(28),
|
||||||
confHeight: uint32(500),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -396,11 +396,14 @@ func TestBabyOutputSerialization(t *testing.T) {
|
|||||||
|
|
||||||
type nurseryTestContext struct {
|
type nurseryTestContext struct {
|
||||||
nursery *utxoNursery
|
nursery *utxoNursery
|
||||||
notifier *nurseryMockNotifier
|
notifier *sweep.MockNotifier
|
||||||
|
chainIO *mockChainIO
|
||||||
publishChan chan wire.MsgTx
|
publishChan chan wire.MsgTx
|
||||||
store *nurseryStoreInterceptor
|
store *nurseryStoreInterceptor
|
||||||
restart func() bool
|
restart func() bool
|
||||||
receiveTx func() wire.MsgTx
|
receiveTx func() wire.MsgTx
|
||||||
|
sweeper *sweep.UtxoSweeper
|
||||||
|
timeoutChan chan chan time.Time
|
||||||
t *testing.T
|
t *testing.T
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -430,17 +433,50 @@ func createNurseryTestContext(t *testing.T,
|
|||||||
// test.
|
// test.
|
||||||
storeIntercepter := newNurseryStoreInterceptor(store)
|
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) {
|
GenSweepScript: func() ([]byte, error) {
|
||||||
return []byte{}, nil
|
return []byte{}, nil
|
||||||
},
|
},
|
||||||
Estimator: &lnwallet.StaticFeeEstimator{},
|
Estimator: &lnwallet.StaticFeeEstimator{},
|
||||||
Signer: &nurseryMockSigner{},
|
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 },
|
||||||
|
}
|
||||||
|
|
||||||
cfg := NurseryConfig{
|
sweeper := sweep.New(sweeperCfg)
|
||||||
|
|
||||||
|
sweeper.Start()
|
||||||
|
|
||||||
|
nurseryCfg := NurseryConfig{
|
||||||
Notifier: notifier,
|
Notifier: notifier,
|
||||||
FetchClosedChannels: func(pendingOnly bool) (
|
FetchClosedChannels: func(pendingOnly bool) (
|
||||||
[]*channeldb.ChannelCloseSummary, error) {
|
[]*channeldb.ChannelCloseSummary, error) {
|
||||||
@ -453,54 +489,91 @@ func createNurseryTestContext(t *testing.T,
|
|||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
Store: storeIntercepter,
|
Store: storeIntercepter,
|
||||||
ChainIO: &mockChainIO{},
|
ChainIO: chainIO,
|
||||||
Sweeper: sweeper,
|
Sweeper: sweeper,
|
||||||
|
PublishTransaction: func(tx *wire.MsgTx) error {
|
||||||
|
return publishFunc(tx, "nursery")
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
publishChan := make(chan wire.MsgTx, 1)
|
nursery := newUtxoNursery(&nurseryCfg)
|
||||||
cfg.PublishTransaction = func(tx *wire.MsgTx) error {
|
|
||||||
t.Logf("Publishing tx %v", tx.TxHash())
|
|
||||||
publishChan <- *tx
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
nursery := newUtxoNursery(&cfg)
|
|
||||||
nursery.Start()
|
nursery.Start()
|
||||||
|
|
||||||
ctx := &nurseryTestContext{
|
ctx := &nurseryTestContext{
|
||||||
nursery: nursery,
|
nursery: nursery,
|
||||||
notifier: notifier,
|
notifier: notifier,
|
||||||
|
chainIO: chainIO,
|
||||||
store: storeIntercepter,
|
store: storeIntercepter,
|
||||||
publishChan: publishChan,
|
publishChan: publishChan,
|
||||||
|
sweeper: sweeper,
|
||||||
|
timeoutChan: timeoutChan,
|
||||||
t: t,
|
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 {
|
ctx.receiveTx = func() wire.MsgTx {
|
||||||
var tx wire.MsgTx
|
var tx wire.MsgTx
|
||||||
select {
|
select {
|
||||||
case tx = <-ctx.publishChan:
|
case tx = <-ctx.publishChan:
|
||||||
|
utxnLog.Debugf("Published tx %v", tx.TxHash())
|
||||||
return tx
|
return tx
|
||||||
case <-time.After(5 * time.Second):
|
case <-time.After(defaultTestTimeout):
|
||||||
|
pprof.Lookup("goroutine").WriteTo(os.Stdout, 1)
|
||||||
|
|
||||||
t.Fatalf("tx not published")
|
t.Fatalf("tx not published")
|
||||||
}
|
}
|
||||||
return tx
|
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.
|
// Start with testing an immediate restart.
|
||||||
ctx.restart()
|
ctx.restart()
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ctx *nurseryTestContext) notifyEpoch(height int32) {
|
||||||
|
ctx.chainIO.bestHeight = height
|
||||||
|
ctx.notifier.NotifyEpoch(height)
|
||||||
|
}
|
||||||
|
|
||||||
func (ctx *nurseryTestContext) finish() {
|
func (ctx *nurseryTestContext) finish() {
|
||||||
// Add a final restart point in this state
|
// Add a final restart point in this state
|
||||||
ctx.restart()
|
ctx.restart()
|
||||||
@ -556,6 +629,8 @@ func (ctx *nurseryTestContext) finish() {
|
|||||||
if len(activeHeights) > 0 {
|
if len(activeHeights) > 0 {
|
||||||
ctx.t.Fatalf("Expected height index to be empty")
|
ctx.t.Fatalf("Expected height index to be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx.sweeper.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
func createOutgoingRes(onLocalCommitment bool) *lnwallet.OutgoingHtlcResolution {
|
func createOutgoingRes(onLocalCommitment bool) *lnwallet.OutgoingHtlcResolution {
|
||||||
@ -703,6 +778,8 @@ func testRestartLoop(t *testing.T, test func(*testing.T,
|
|||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
utxnLog.Debugf("Skipping restart point %v",
|
||||||
|
currentStartStopIdx)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -739,7 +816,7 @@ func testNurseryOutgoingHtlcSuccessOnLocal(t *testing.T,
|
|||||||
ctx.restart()
|
ctx.restart()
|
||||||
|
|
||||||
// Notify arrival of block where HTLC CLTV expires.
|
// Notify arrival of block where HTLC CLTV expires.
|
||||||
ctx.notifier.notifyEpoch(125)
|
ctx.notifyEpoch(125)
|
||||||
|
|
||||||
// This should trigger nursery to publish the timeout tx.
|
// This should trigger nursery to publish the timeout tx.
|
||||||
ctx.receiveTx()
|
ctx.receiveTx()
|
||||||
@ -751,7 +828,7 @@ func testNurseryOutgoingHtlcSuccessOnLocal(t *testing.T,
|
|||||||
|
|
||||||
// Confirm the timeout tx. This should promote the HTLC to KNDR state.
|
// Confirm the timeout tx. This should promote the HTLC to KNDR state.
|
||||||
timeoutTxHash := outgoingRes.SignedTimeoutTx.TxHash()
|
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)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -765,7 +842,7 @@ func testNurseryOutgoingHtlcSuccessOnLocal(t *testing.T,
|
|||||||
ctx.restart()
|
ctx.restart()
|
||||||
|
|
||||||
// Notify arrival of block where second level HTLC unlocks.
|
// Notify arrival of block where second level HTLC unlocks.
|
||||||
ctx.notifier.notifyEpoch(128)
|
ctx.notifyEpoch(128)
|
||||||
|
|
||||||
// Check final sweep into wallet.
|
// Check final sweep into wallet.
|
||||||
testSweepHtlc(t, ctx)
|
testSweepHtlc(t, ctx)
|
||||||
@ -790,7 +867,7 @@ func testNurseryOutgoingHtlcSuccessOnRemote(t *testing.T,
|
|||||||
// resolving remote commitment tx.
|
// resolving remote commitment tx.
|
||||||
//
|
//
|
||||||
// TODO(joostjager): This is probably not correct?
|
// 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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -805,7 +882,7 @@ func testNurseryOutgoingHtlcSuccessOnRemote(t *testing.T,
|
|||||||
ctx.restart()
|
ctx.restart()
|
||||||
|
|
||||||
// Notify arrival of block where HTLC CLTV expires.
|
// Notify arrival of block where HTLC CLTV expires.
|
||||||
ctx.notifier.notifyEpoch(125)
|
ctx.notifyEpoch(125)
|
||||||
|
|
||||||
// Check final sweep into wallet.
|
// Check final sweep into wallet.
|
||||||
testSweepHtlc(t, ctx)
|
testSweepHtlc(t, ctx)
|
||||||
@ -840,7 +917,7 @@ func testNurseryCommitSuccessOnLocal(t *testing.T,
|
|||||||
ctx.restart()
|
ctx.restart()
|
||||||
|
|
||||||
// Notify confirmation of the commitment tx.
|
// 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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -855,7 +932,7 @@ func testNurseryCommitSuccessOnLocal(t *testing.T,
|
|||||||
ctx.restart()
|
ctx.restart()
|
||||||
|
|
||||||
// Notify arrival of block where commit output CSV expires.
|
// Notify arrival of block where commit output CSV expires.
|
||||||
ctx.notifier.notifyEpoch(126)
|
ctx.notifyEpoch(126)
|
||||||
|
|
||||||
// Check final sweep into wallet.
|
// Check final sweep into wallet.
|
||||||
testSweep(t, ctx, func() {
|
testSweep(t, ctx, func() {
|
||||||
@ -876,27 +953,28 @@ func testSweepHtlc(t *testing.T, ctx *nurseryTestContext) {
|
|||||||
|
|
||||||
func testSweep(t *testing.T, ctx *nurseryTestContext,
|
func testSweep(t *testing.T, ctx *nurseryTestContext,
|
||||||
afterPublishAssert func()) {
|
afterPublishAssert func()) {
|
||||||
|
|
||||||
// Wait for nursery to publish the sweep tx.
|
// Wait for nursery to publish the sweep tx.
|
||||||
|
ctx.tick()
|
||||||
sweepTx := ctx.receiveTx()
|
sweepTx := ctx.receiveTx()
|
||||||
|
|
||||||
if ctx.restart() {
|
if ctx.restart() {
|
||||||
// Restart will trigger rebroadcast of sweep tx.
|
// Nursery reoffers its input. Sweeper needs a tick to create the sweep
|
||||||
sweepTx = ctx.receiveTx()
|
// tx.
|
||||||
|
ctx.tick()
|
||||||
|
ctx.receiveTx()
|
||||||
}
|
}
|
||||||
|
|
||||||
afterPublishAssert()
|
afterPublishAssert()
|
||||||
|
|
||||||
// Confirm the sweep tx.
|
// Confirm the sweep tx.
|
||||||
sweepTxHash := sweepTx.TxHash()
|
ctx.notifier.SpendOutpoint(sweepTx.TxIn[0].PreviousOutPoint, sweepTx)
|
||||||
err := ctx.notifier.confirmTx(&sweepTxHash, 129)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for output to be promoted in store to GRAD.
|
// Wait for output to be promoted in store to GRAD.
|
||||||
select {
|
select {
|
||||||
case <-ctx.store.graduateKinderChan:
|
case <-ctx.store.graduateKinderChan:
|
||||||
case <-time.After(defaultTestTimeout):
|
case <-time.After(defaultTestTimeout):
|
||||||
|
pprof.Lookup("goroutine").WriteTo(os.Stdout, 1)
|
||||||
t.Fatalf("output not graduated")
|
t.Fatalf("output not graduated")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -907,6 +985,19 @@ func testSweep(t *testing.T, ctx *nurseryTestContext,
|
|||||||
assertNurseryReportUnavailable(t, ctx.nursery)
|
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 {
|
type nurseryStoreInterceptor struct {
|
||||||
ns NurseryStore
|
ns NurseryStore
|
||||||
|
|
||||||
@ -941,16 +1032,18 @@ func (i *nurseryStoreInterceptor) CribToKinder(babyOutput *babyOutput) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *nurseryStoreInterceptor) PreschoolToKinder(kidOutput *kidOutput) error {
|
func (i *nurseryStoreInterceptor) PreschoolToKinder(kidOutput *kidOutput,
|
||||||
err := i.ns.PreschoolToKinder(kidOutput)
|
lastGradHeight uint32) error {
|
||||||
|
|
||||||
|
err := i.ns.PreschoolToKinder(kidOutput, lastGradHeight)
|
||||||
|
|
||||||
i.preschoolToKinderChan <- struct{}{}
|
i.preschoolToKinderChan <- struct{}{}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *nurseryStoreInterceptor) GraduateKinder(height uint32) error {
|
func (i *nurseryStoreInterceptor) GraduateKinder(height uint32, kid *kidOutput) error {
|
||||||
err := i.ns.GraduateKinder(height)
|
err := i.ns.GraduateKinder(height, kid)
|
||||||
|
|
||||||
i.graduateKinderChan <- struct{}{}
|
i.graduateKinderChan <- struct{}{}
|
||||||
|
|
||||||
@ -961,30 +1054,12 @@ func (i *nurseryStoreInterceptor) FetchPreschools() ([]kidOutput, error) {
|
|||||||
return i.ns.FetchPreschools()
|
return i.ns.FetchPreschools()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *nurseryStoreInterceptor) FetchClass(height uint32) (*wire.MsgTx,
|
func (i *nurseryStoreInterceptor) FetchClass(height uint32) (
|
||||||
[]kidOutput, []babyOutput, error) {
|
[]kidOutput, []babyOutput, error) {
|
||||||
|
|
||||||
return i.ns.FetchClass(height)
|
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) (
|
func (i *nurseryStoreInterceptor) HeightsBelowOrEqual(height uint32) (
|
||||||
[]uint32, error) {
|
[]uint32, error) {
|
||||||
|
|
||||||
@ -1025,92 +1100,3 @@ func (m *nurseryMockSigner) ComputeInputScript(tx *wire.MsgTx,
|
|||||||
|
|
||||||
return &lnwallet.InputScript{}, nil
|
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
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user