Merge pull request #1875 from joostjager/resolverstate

rpc+contractcourt: merge the contractResolver state into the pendingchannels RPC response
This commit is contained in:
Olaoluwa Osuntokun 2019-02-01 17:07:53 -08:00 committed by GitHub
commit 6032e47fe8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 346 additions and 136 deletions

@ -566,6 +566,21 @@ func (c *ChainArbitrator) UpdateContractSignals(chanPoint wire.OutPoint,
return nil
}
// GetChannelArbitrator safely returns the channel arbitrator for a given
// channel outpoint.
func (c *ChainArbitrator) GetChannelArbitrator(chanPoint wire.OutPoint) (
*ChannelArbitrator, error) {
c.Lock()
arbitrator, ok := c.activeChannels[chanPoint]
c.Unlock()
if !ok {
return nil, fmt.Errorf("unable to find arbitrator")
}
return arbitrator, nil
}
// forceCloseReq is a request sent from an outside sub-system to the arbitrator
// that watches a particular channel to broadcast the commitment transaction,
// and enter the resolution phase of the channel.

@ -1,11 +1,13 @@
package contractcourt
import (
"bytes"
"errors"
"sync"
"sync/atomic"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/davecgh/go-spew/spew"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/channeldb"
@ -132,6 +134,36 @@ type ChannelArbitratorConfig struct {
ChainArbitratorConfig
}
// ContractReport provides a summary of a commitment tx output.
type ContractReport struct {
// Outpoint is the final output that will be swept back to the wallet.
Outpoint wire.OutPoint
// Incoming indicates whether the htlc was incoming to this channel.
Incoming bool
// Amount is the final value that will be swept in back to the wallet.
Amount btcutil.Amount
// MaturityHeight is the absolute block height that this output will
// mature at.
MaturityHeight uint32
// Stage indicates whether the htlc is in the CLTV-timeout stage (1) or
// the CSV-delay stage (2). A stage 1 htlc's maturity height will be set
// to its expiry height, while a stage 2 htlc's maturity height will be
// set to its confirmation height plus the maturity requirement.
Stage uint32
// LimboBalance is the total number of frozen coins within this
// contract.
LimboBalance btcutil.Amount
// RecoveredBalance is the total value that has been successfully swept
// back to the user's wallet.
RecoveredBalance btcutil.Amount
}
// htlcSet represents the set of active HTLCs on a given commitment
// transaction.
type htlcSet struct {
@ -202,6 +234,10 @@ type ChannelArbitrator struct {
// be able to signal them for shutdown in the case that we shutdown.
activeResolvers []ContractResolver
// activeResolversLock prevents simultaneous read and write to the
// resolvers slice.
activeResolversLock sync.RWMutex
// resolutionSignal is a channel that will be sent upon by contract
// resolvers once their contract has been fully resolved. With each
// send, we'll check to see if the contract is fully resolved.
@ -461,6 +497,33 @@ func supplementTimeoutResolver(r *htlcTimeoutResolver,
return nil
}
// Report returns htlc reports for the active resolvers.
func (c *ChannelArbitrator) Report() []*ContractReport {
c.activeResolversLock.RLock()
defer c.activeResolversLock.RUnlock()
var reports []*ContractReport
for _, resolver := range c.activeResolvers {
r, ok := resolver.(reportingContractResolver)
if !ok {
continue
}
if r.IsResolved() {
continue
}
report := r.report()
if report == nil {
continue
}
reports = append(reports, report)
}
return reports
}
// Stop signals the ChannelArbitrator for a graceful shutdown.
func (c *ChannelArbitrator) Stop() error {
if !atomic.CompareAndSwapInt32(&c.stopped, 0, 1) {
@ -473,9 +536,11 @@ func (c *ChannelArbitrator) Stop() error {
go c.cfg.ChainEvents.Cancel()
}
c.activeResolversLock.RLock()
for _, activeResolver := range c.activeResolvers {
activeResolver.Stop()
}
c.activeResolversLock.RUnlock()
close(c.quit)
c.wg.Wait()
@ -816,7 +881,19 @@ func (c *ChannelArbitrator) stateStep(triggerHeight uint32,
log.Infof("ChannelArbitrator(%v): still awaiting contract "+
"resolution", c.cfg.ChanPoint)
nextState = StateWaitingFullResolution
numUnresolved, err := c.log.FetchUnresolvedContracts()
if err != nil {
return StateError, closeTx, err
}
// If we still have unresolved contracts, then we'll stay alive
// to oversee their resolution.
if len(numUnresolved) != 0 {
nextState = StateWaitingFullResolution
break
}
nextState = StateFullyResolved
// If we start as fully resolved, then we'll end as fully resolved.
case StateFullyResolved:
@ -842,6 +919,9 @@ func (c *ChannelArbitrator) stateStep(triggerHeight uint32,
// launchResolvers updates the activeResolvers list and starts the resolvers.
func (c *ChannelArbitrator) launchResolvers(resolvers []ContractResolver) {
c.activeResolversLock.Lock()
defer c.activeResolversLock.Unlock()
c.activeResolvers = resolvers
for _, contract := range resolvers {
c.wg.Add(1)
@ -1361,6 +1441,25 @@ func (c *ChannelArbitrator) prepContractResolutions(htlcActions ChainActionMap,
return htlcResolvers, msgsToSend, nil
}
// replaceResolver replaces a in the list of active resolvers. If the resolver
// to be replaced is not found, it returns an error.
func (c *ChannelArbitrator) replaceResolver(oldResolver,
newResolver ContractResolver) error {
c.activeResolversLock.Lock()
defer c.activeResolversLock.Unlock()
oldKey := oldResolver.ResolverKey()
for i, r := range c.activeResolvers {
if bytes.Equal(r.ResolverKey(), oldKey) {
c.activeResolvers[i] = newResolver
return nil
}
}
return errors.New("resolver to be replaced not found")
}
// resolveContract is a goroutine tasked with fully resolving an unresolved
// contract. Either the initial contract will be resolved after a single step,
// or the contract will itself create another contract to be resolved. In
@ -1410,6 +1509,7 @@ func (c *ChannelArbitrator) resolveContract(currentContract ContractResolver) {
c.cfg.ChanPoint, currentContract,
nextContract)
// Swap contract in log.
err := c.log.SwapContract(
currentContract, nextContract,
)
@ -1418,6 +1518,17 @@ func (c *ChannelArbitrator) resolveContract(currentContract ContractResolver) {
"contract: %v", err)
}
// Swap contract in resolvers list. This is to
// make sure that reports are queried from the
// new resolver.
err = c.replaceResolver(
currentContract, nextContract,
)
if err != nil {
log.Errorf("unable to replace "+
"contract: %v", err)
}
// As this contract produced another, we'll
// re-assign, so we can continue our resolution
// loop.
@ -1722,29 +1833,22 @@ func (c *ChannelArbitrator) channelAttendant(bestHeight int32) {
log.Infof("ChannelArbitrator(%v): a contract has been "+
"fully resolved!", c.cfg.ChanPoint)
numUnresolved, err := c.log.FetchUnresolvedContracts()
nextState, _, err := c.advanceState(
uint32(bestHeight), chainTrigger,
)
if err != nil {
log.Errorf("unable to query resolved "+
"contracts: %v", err)
log.Errorf("unable to advance state: %v", err)
}
// If we still have unresolved contracts, then we'll
// stay alive to oversee their resolution.
if len(numUnresolved) != 0 {
continue
}
// If we don't have anything further to do after
// advancing our state, then we'll exit.
if nextState == StateFullyResolved {
log.Infof("ChannelArbitrator(%v): all "+
"contracts fully resolved, exiting",
c.cfg.ChanPoint)
log.Infof("ChannelArbitrator(%v): all contracts fully "+
"resolved, exiting", c.cfg.ChanPoint)
// Otherwise, our job is finished here, the contract is
// now fully resolved! We'll mark it as such, then exit
// ourselves.
if err := c.cfg.MarkChannelResolved(); err != nil {
log.Errorf("unable to mark contract "+
"resolved: %v", err)
return
}
return
// We've just received a request to forcibly close out the
// channel. We'll

@ -611,10 +611,7 @@ func TestChannelArbitratorLocalForceClosePendingHtlc(t *testing.T) {
notifier.spendChan <- &chainntnfs.SpendDetail{}
// At this point channel should be marked as resolved.
// It should transition StateWaitingFullResolution ->
// StateFullyResolved, but this isn't happening.
// assertStateTransitions(t, arbLog.newStates, StateFullyResolved)
assertStateTransitions(t, arbLog.newStates, StateFullyResolved)
select {
case <-resolved:

@ -60,6 +60,14 @@ type ContractResolver interface {
Stop()
}
// reportingContractResolver is a ContractResolver that also exposes a report on
// the resolution state of the contract.
type reportingContractResolver interface {
ContractResolver
report() *ContractReport
}
// ResolverKit is meant to be used as a mix-in struct to be embedded within a
// given ContractResolver implementation. It contains all the items that a
// resolver requires to carry out its duties.

@ -6,6 +6,8 @@ import (
"encoding/binary"
"fmt"
"io"
"github.com/btcsuite/btcutil"
)
// htlcIncomingContestResolver is a ContractResolver that's able to resolve an
@ -166,6 +168,27 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) {
}
}
// report returns a report on the resolution state of the contract.
func (h *htlcIncomingContestResolver) report() *ContractReport {
// No locking needed as these values are read-only.
finalAmt := h.htlcAmt.ToSatoshis()
if h.htlcResolution.SignedSuccessTx != nil {
finalAmt = btcutil.Amount(
h.htlcResolution.SignedSuccessTx.TxOut[0].Value,
)
}
return &ContractReport{
Outpoint: h.htlcResolution.ClaimOutpoint,
Incoming: true,
Amount: finalAmt,
MaturityHeight: h.htlcExpiry,
LimboBalance: finalAmt,
Stage: 1,
}
}
// Stop signals the resolver to cancel any current resolution processes, and
// suspend.
//

@ -6,6 +6,7 @@ import (
"io"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/davecgh/go-spew/spew"
"github.com/lightningnetwork/lnd/chainntnfs"
)
@ -108,6 +109,9 @@ func (h *htlcOutgoingContestResolver) Resolve() (ContractResolver, error) {
scriptToWatch []byte
err error
)
// TODO(joostjager): output already set properly in
// lnwallet.newOutgoingHtlcResolution? And script too?
if h.htlcResolution.SignedTimeoutTx == nil {
outPointToWatch = h.htlcResolution.ClaimOutpoint
scriptToWatch = h.htlcResolution.SweepSignDesc.Output.PkScript
@ -235,6 +239,27 @@ func (h *htlcOutgoingContestResolver) Resolve() (ContractResolver, error) {
}
}
// report returns a report on the resolution state of the contract.
func (h *htlcOutgoingContestResolver) report() *ContractReport {
// No locking needed as these values are read-only.
finalAmt := h.htlcAmt.ToSatoshis()
if h.htlcResolution.SignedTimeoutTx != nil {
finalAmt = btcutil.Amount(
h.htlcResolution.SignedTimeoutTx.TxOut[0].Value,
)
}
return &ContractReport{
Outpoint: h.htlcResolution.ClaimOutpoint,
Incoming: false,
Amount: finalAmt,
MaturityHeight: h.htlcResolution.Expiry,
LimboBalance: finalAmt,
Stage: 1,
}
}
// Stop signals the resolver to cancel any current resolution processes, and
// suspend.
//

@ -2803,6 +2803,12 @@ func testChannelForceClosure(net *lntest.NetworkHarness, t *harnessTest) {
assertTxInBlock(t, block, sweepTx.Hash())
// Update current height
_, curHeight, err = net.Miner.Node.GetBestBlock()
if err != nil {
t.Fatalf("unable to get best block height")
}
err = lntest.WaitPredicate(func() bool {
// Now that the commit output has been fully swept, check to see
// that the channel remains open for the pending htlc outputs.
@ -2824,21 +2830,25 @@ func testChannelForceClosure(net *lntest.NetworkHarness, t *harnessTest) {
// The commitment funds will have been recovered after the
// commit txn was included in the last block. The htlc funds
// will not be shown in limbo, since they are still in their
// first stage and the nursery hasn't received them from the
// contract court.
// will be shown in limbo.
forceClose, err := findForceClosedChannel(pendingChanResp, &op)
if err != nil {
predErr = err
return false
}
predErr = checkPendingChannelNumHtlcs(forceClose, 0)
predErr = checkPendingChannelNumHtlcs(forceClose, numInvoices)
if predErr != nil {
return false
}
if forceClose.LimboBalance != 0 {
predErr = fmt.Errorf("expected 0 funds in limbo, "+
"found %d", forceClose.LimboBalance)
predErr = checkPendingHtlcStageAndMaturity(
forceClose, 1, htlcExpiryHeight,
int32(htlcExpiryHeight)-curHeight,
)
if predErr != nil {
return false
}
if forceClose.LimboBalance == 0 {
predErr = fmt.Errorf("expected funds in limbo, found 0")
return false
}

@ -2063,58 +2063,27 @@ func (r *rpcServer) PendingChannels(ctx context.Context,
ClosingTxid: closeTXID,
}
// Query for the maturity state for this force closed
// channel. If we didn't have any time-locked outputs,
// then the nursery may not know of the contract.
nurseryInfo, err := r.server.utxoNursery.NurseryReport(&chanPoint)
if err != nil && err != ErrContractNotFound {
return nil, fmt.Errorf("unable to obtain "+
"nursery report for ChannelPoint(%v): %v",
chanPoint, err)
// Fetch reports from both nursery and resolvers. At the
// moment this is not an atomic snapshot. This is
// planned to be resolved when the nursery is removed
// and channel arbitrator will be the single source for
// these kind of reports.
err := r.nurseryPopulateForceCloseResp(
&chanPoint, currentHeight, forceClose,
)
if err != nil {
return nil, err
}
// If the nursery knows of this channel, then we can
// populate information detailing exactly how much
// funds are time locked and also the height in which
// we can ultimately sweep the funds into the wallet.
if nurseryInfo != nil {
forceClose.LimboBalance = int64(nurseryInfo.limboBalance)
forceClose.RecoveredBalance = int64(nurseryInfo.recoveredBalance)
forceClose.MaturityHeight = nurseryInfo.maturityHeight
// If the transaction has been confirmed, then
// we can compute how many blocks it has left.
if forceClose.MaturityHeight != 0 {
forceClose.BlocksTilMaturity =
int32(forceClose.MaturityHeight) -
currentHeight
}
for _, htlcReport := range nurseryInfo.htlcs {
// TODO(conner) set incoming flag
// appropriately after handling incoming
// incubation
htlc := &lnrpc.PendingHTLC{
Incoming: false,
Amount: int64(htlcReport.amount),
Outpoint: htlcReport.outpoint.String(),
MaturityHeight: htlcReport.maturityHeight,
Stage: htlcReport.stage,
}
if htlc.MaturityHeight != 0 {
htlc.BlocksTilMaturity =
int32(htlc.MaturityHeight) -
currentHeight
}
forceClose.PendingHtlcs = append(forceClose.PendingHtlcs,
htlc)
}
resp.TotalLimboBalance += int64(nurseryInfo.limboBalance)
err = r.arbitratorPopulateForceCloseResp(
&chanPoint, currentHeight, forceClose,
)
if err != nil {
return nil, err
}
resp.TotalLimboBalance += int64(forceClose.LimboBalance)
resp.PendingForceClosingChannels = append(
resp.PendingForceClosingChannels,
forceClose,
@ -2158,6 +2127,101 @@ func (r *rpcServer) PendingChannels(ctx context.Context,
return resp, nil
}
// arbitratorPopulateForceCloseResp populates the pending channels response
// message with channel resolution information from the contract resolvers.
func (r *rpcServer) arbitratorPopulateForceCloseResp(chanPoint *wire.OutPoint,
currentHeight int32,
forceClose *lnrpc.PendingChannelsResponse_ForceClosedChannel) error {
// Query for contract resolvers state.
arbitrator, err := r.server.chainArb.GetChannelArbitrator(*chanPoint)
if err != nil {
return err
}
reports := arbitrator.Report()
for _, report := range reports {
htlc := &lnrpc.PendingHTLC{
Incoming: report.Incoming,
Amount: int64(report.Amount),
Outpoint: report.Outpoint.String(),
MaturityHeight: report.MaturityHeight,
Stage: report.Stage,
}
if htlc.MaturityHeight != 0 {
htlc.BlocksTilMaturity =
int32(htlc.MaturityHeight) - currentHeight
}
forceClose.LimboBalance += int64(report.LimboBalance)
forceClose.RecoveredBalance += int64(report.RecoveredBalance)
forceClose.PendingHtlcs = append(forceClose.PendingHtlcs, htlc)
}
return nil
}
// nurseryPopulateForceCloseResp populates the pending channels response
// message with contract resolution information from utxonursery.
func (r *rpcServer) nurseryPopulateForceCloseResp(chanPoint *wire.OutPoint,
currentHeight int32,
forceClose *lnrpc.PendingChannelsResponse_ForceClosedChannel) error {
// Query for the maturity state for this force closed channel. If we
// didn't have any time-locked outputs, then the nursery may not know of
// the contract.
nurseryInfo, err := r.server.utxoNursery.NurseryReport(chanPoint)
if err == ErrContractNotFound {
return nil
}
if err != nil {
return fmt.Errorf("unable to obtain "+
"nursery report for ChannelPoint(%v): %v",
chanPoint, err)
}
// If the nursery knows of this channel, then we can populate
// information detailing exactly how much funds are time locked and also
// the height in which we can ultimately sweep the funds into the
// wallet.
forceClose.LimboBalance = int64(nurseryInfo.limboBalance)
forceClose.RecoveredBalance = int64(nurseryInfo.recoveredBalance)
forceClose.MaturityHeight = nurseryInfo.maturityHeight
// If the transaction has been confirmed, then we can compute how many
// blocks it has left.
if forceClose.MaturityHeight != 0 {
forceClose.BlocksTilMaturity =
int32(forceClose.MaturityHeight) -
currentHeight
}
for _, htlcReport := range nurseryInfo.htlcs {
// TODO(conner) set incoming flag appropriately after handling
// incoming incubation
htlc := &lnrpc.PendingHTLC{
Incoming: false,
Amount: int64(htlcReport.amount),
Outpoint: htlcReport.outpoint.String(),
MaturityHeight: htlcReport.maturityHeight,
Stage: htlcReport.stage,
}
if htlc.MaturityHeight != 0 {
htlc.BlocksTilMaturity =
int32(htlc.MaturityHeight) -
currentHeight
}
forceClose.PendingHtlcs = append(forceClose.PendingHtlcs,
htlc)
}
return nil
}
// ClosedChannels returns a list of all the channels have been closed.
// This does not include channels that are still in the process of closing.
func (r *rpcServer) ClosedChannels(ctx context.Context,

@ -494,9 +494,7 @@ func (u *utxoNursery) NurseryReport(
utxnLog.Infof("NurseryReport: building nursery report for channel %v",
chanPoint)
report := &contractMaturityReport{
chanPoint: *chanPoint,
}
report := &contractMaturityReport{}
if err := u.cfg.Store.ForChanOutputs(chanPoint, func(k, v []byte) error {
switch {
@ -536,11 +534,18 @@ func (u *utxoNursery) NurseryReport(
case input.CommitmentTimeLock:
report.AddLimboCommitment(&kid)
// An HTLC output on our commitment transaction
// where the second-layer transaction hasn't
// yet confirmed.
case input.HtlcAcceptedSuccessSecondLevel:
// An HTLC output on our commitment transaction
// where the second-layer transaction hasn't
// yet confirmed.
report.AddLimboStage1SuccessHtlc(&kid)
case input.HtlcOfferedRemoteTimeout:
// This is an HTLC output on the
// commitment transaction of the remote
// party. We are waiting for the CLTV
// timelock expire.
report.AddLimboDirectHtlc(&kid)
}
case bytes.HasPrefix(k, kndrPrefix):
@ -1051,10 +1056,6 @@ func (u *utxoNursery) waitForPreschoolConf(kid *kidOutput,
// contractMaturityReport is a report that details the maturity progress of a
// particular force closed contract.
type contractMaturityReport struct {
// chanPoint is the channel point of the original contract that is now
// awaiting maturity within the utxoNursery.
chanPoint wire.OutPoint
// limboBalance is the total number of frozen coins within this
// contract.
limboBalance btcutil.Amount
@ -1063,16 +1064,6 @@ type contractMaturityReport struct {
// back to the user's wallet.
recoveredBalance btcutil.Amount
// localAmount is the local value of the commitment output.
localAmount btcutil.Amount
// confHeight is the block height that this output originally confirmed.
confHeight uint32
// maturityRequirement is the input age required for this output to
// reach maturity.
maturityRequirement uint32
// maturityHeight is the absolute block height that this output will
// mature at.
maturityHeight uint32
@ -1090,13 +1081,6 @@ type htlcMaturityReport struct {
// amount is the final value that will be swept in back to the wallet.
amount btcutil.Amount
// confHeight is the block height that this output originally confirmed.
confHeight uint32
// maturityRequirement is the input age required for this output to
// reach maturity.
maturityRequirement uint32
// maturityHeight is the absolute block height that this output will
// mature at.
maturityHeight uint32
@ -1113,10 +1097,6 @@ type htlcMaturityReport struct {
func (c *contractMaturityReport) AddLimboCommitment(kid *kidOutput) {
c.limboBalance += kid.Amount()
c.localAmount += kid.Amount()
c.confHeight = kid.ConfHeight()
c.maturityRequirement = kid.BlocksToMaturity()
// If the confirmation height is set, then this means the contract has
// been confirmed, and we know the final maturity height.
if kid.ConfHeight() != 0 {
@ -1129,9 +1109,6 @@ func (c *contractMaturityReport) AddLimboCommitment(kid *kidOutput) {
func (c *contractMaturityReport) AddRecoveredCommitment(kid *kidOutput) {
c.recoveredBalance += kid.Amount()
c.localAmount += kid.Amount()
c.confHeight = kid.ConfHeight()
c.maturityRequirement = kid.BlocksToMaturity()
c.maturityHeight = kid.BlocksToMaturity() + kid.ConfHeight()
}
@ -1144,7 +1121,6 @@ func (c *contractMaturityReport) AddLimboStage1TimeoutHtlc(baby *babyOutput) {
c.htlcs = append(c.htlcs, htlcMaturityReport{
outpoint: *baby.OutPoint(),
amount: baby.Amount(),
confHeight: baby.ConfHeight(),
maturityHeight: baby.expiry,
stage: 1,
})
@ -1152,14 +1128,13 @@ func (c *contractMaturityReport) AddLimboStage1TimeoutHtlc(baby *babyOutput) {
// AddLimboDirectHtlc adds a direct HTLC on the commitment transaction of the
// remote party to the maturity report. This a CLTV time-locked output that
// hasn't yet expired.
// has or hasn't expired yet.
func (c *contractMaturityReport) AddLimboDirectHtlc(kid *kidOutput) {
c.limboBalance += kid.Amount()
htlcReport := htlcMaturityReport{
outpoint: *kid.OutPoint(),
amount: kid.Amount(),
confHeight: kid.ConfHeight(),
maturityHeight: kid.absoluteMaturity,
stage: 2,
}
@ -1174,11 +1149,9 @@ func (c *contractMaturityReport) AddLimboStage1SuccessHtlc(kid *kidOutput) {
c.limboBalance += kid.Amount()
c.htlcs = append(c.htlcs, htlcMaturityReport{
outpoint: *kid.OutPoint(),
amount: kid.Amount(),
confHeight: kid.ConfHeight(),
maturityRequirement: kid.BlocksToMaturity(),
stage: 1,
outpoint: *kid.OutPoint(),
amount: kid.Amount(),
stage: 1,
})
}
@ -1188,11 +1161,9 @@ func (c *contractMaturityReport) AddLimboStage2Htlc(kid *kidOutput) {
c.limboBalance += kid.Amount()
htlcReport := htlcMaturityReport{
outpoint: *kid.OutPoint(),
amount: kid.Amount(),
confHeight: kid.ConfHeight(),
maturityRequirement: kid.BlocksToMaturity(),
stage: 2,
outpoint: *kid.OutPoint(),
amount: kid.Amount(),
stage: 2,
}
// If the confirmation height is set, then this means the first stage
@ -1211,11 +1182,9 @@ func (c *contractMaturityReport) AddRecoveredHtlc(kid *kidOutput) {
c.recoveredBalance += kid.Amount()
c.htlcs = append(c.htlcs, htlcMaturityReport{
outpoint: *kid.OutPoint(),
amount: kid.Amount(),
confHeight: kid.ConfHeight(),
maturityRequirement: kid.BlocksToMaturity(),
maturityHeight: kid.ConfHeight() + kid.BlocksToMaturity(),
outpoint: *kid.OutPoint(),
amount: kid.Amount(),
maturityHeight: kid.ConfHeight() + kid.BlocksToMaturity(),
})
}

@ -664,12 +664,7 @@ func incubateTestOutput(t *testing.T, nursery *utxoNursery,
if onLocalCommitment {
expectedStage = 1
}
// TODO(joostjager): Nursery is currently not reporting this limbo
// balance.
if onLocalCommitment {
assertNurseryReport(t, nursery, 1, expectedStage, 10000)
}
assertNurseryReport(t, nursery, 1, expectedStage, 10000)
return outgoingRes
}