From 602e7816e6f128fe8dde16967804fa015b57297e Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Mon, 3 Feb 2020 12:15:55 +0100 Subject: [PATCH 1/3] cnct: remove unused field in success resolver Persistence of the generated sweep tx was already removed earlier, so this field serves no purpose anymore. --- contractcourt/briefcase_test.go | 1 - contractcourt/htlc_success_resolver.go | 103 +++++++++++-------------- 2 files changed, 44 insertions(+), 60 deletions(-) diff --git a/contractcourt/briefcase_test.go b/contractcourt/briefcase_test.go index 0cccd5a6cb1..1ec05bf59b0 100644 --- a/contractcourt/briefcase_test.go +++ b/contractcourt/briefcase_test.go @@ -283,7 +283,6 @@ func TestContractInsertionRetrieval(t *testing.T) { htlc: channeldb.HTLC{ RHash: testPreimage, }, - sweepTx: nil, } resolvers := []ContractResolver{ &timeoutResolver, diff --git a/contractcourt/htlc_success_resolver.go b/contractcourt/htlc_success_resolver.go index c13c52e87ef..bd3675289c9 100644 --- a/contractcourt/htlc_success_resolver.go +++ b/contractcourt/htlc_success_resolver.go @@ -37,13 +37,6 @@ type htlcSuccessResolver struct { // historical queries to the chain for spends/confirmations. broadcastHeight uint32 - // sweepTx will be non-nil if we've already crafted a transaction to - // sweep a direct HTLC output. This is only a concern if we're sweeping - // from the commitment transaction of the remote party. - // - // TODO(roasbeef): send off to utxobundler - sweepTx *wire.MsgTx - // htlc contains information on the htlc that we are resolving on-chain. htlc channeldb.HTLC @@ -102,60 +95,52 @@ func (h *htlcSuccessResolver) Resolve() (ContractResolver, error) { // If we don't have a success transaction, then this means that this is // an output on the remote party's commitment transaction. if h.htlcResolution.SignedSuccessTx == nil { - // If we don't already have the sweep transaction constructed, - // we'll do so and broadcast it. - if h.sweepTx == nil { - log.Infof("%T(%x): crafting sweep tx for "+ - "incoming+remote htlc confirmed", h, - h.htlc.RHash[:]) - - // Before we can craft out sweeping transaction, we - // need to create an input which contains all the items - // required to add this input to a sweeping transaction, - // and generate a witness. - inp := input.MakeHtlcSucceedInput( - &h.htlcResolution.ClaimOutpoint, - &h.htlcResolution.SweepSignDesc, - h.htlcResolution.Preimage[:], - h.broadcastHeight, - ) - - // With the input created, we can now generate the full - // sweep transaction, that we'll use to move these - // coins back into the backing wallet. - // - // TODO: Set tx lock time to current block height - // instead of zero. Will be taken care of once sweeper - // implementation is complete. - // - // TODO: Use time-based sweeper and result chan. - var err error - h.sweepTx, err = h.Sweeper.CreateSweepTx( - []input.Input{&inp}, - sweep.FeePreference{ - ConfTarget: sweepConfTarget, - }, 0, - ) - if err != nil { - return nil, err - } + log.Infof("%T(%x): crafting sweep tx for "+ + "incoming+remote htlc confirmed", h, + h.htlc.RHash[:]) + + // Before we can craft out sweeping transaction, we + // need to create an input which contains all the items + // required to add this input to a sweeping transaction, + // and generate a witness. + inp := input.MakeHtlcSucceedInput( + &h.htlcResolution.ClaimOutpoint, + &h.htlcResolution.SweepSignDesc, + h.htlcResolution.Preimage[:], + h.broadcastHeight, + ) - log.Infof("%T(%x): crafted sweep tx=%v", h, - h.htlc.RHash[:], spew.Sdump(h.sweepTx)) + // With the input created, we can now generate the full + // sweep transaction, that we'll use to move these + // coins back into the backing wallet. + // + // TODO: Set tx lock time to current block height + // instead of zero. Will be taken care of once sweeper + // implementation is complete. + // + // TODO: Use time-based sweeper and result chan. + sweepTx, err := h.Sweeper.CreateSweepTx( + []input.Input{&inp}, + sweep.FeePreference{ + ConfTarget: sweepConfTarget, + }, 0, + ) + if err != nil { + return nil, err + } - // With the sweep transaction signed, we'll now - // Checkpoint our state. - if err := h.Checkpoint(h); err != nil { - log.Errorf("unable to Checkpoint: %v", err) - return nil, err - } + log.Infof("%T(%x): crafted sweep tx=%v", h, + h.htlc.RHash[:], spew.Sdump(sweepTx)) + + // With the sweep transaction signed, we'll now + // Checkpoint our state. + if err := h.Checkpoint(h); err != nil { + log.Errorf("unable to Checkpoint: %v", err) + return nil, err } - // Regardless of whether an existing transaction was found or newly - // constructed, we'll broadcast the sweep transaction to the - // network. - err := h.PublishTx(h.sweepTx) - if err != nil { + // Broadcast the sweep transaction to the network. + if err := h.PublishTx(sweepTx); err != nil { log.Infof("%T(%x): unable to publish tx: %v", h, h.htlc.RHash[:], err) return nil, err @@ -163,8 +148,8 @@ func (h *htlcSuccessResolver) Resolve() (ContractResolver, error) { // With the sweep transaction broadcast, we'll wait for its // confirmation. - sweepTXID := h.sweepTx.TxHash() - sweepScript := h.sweepTx.TxOut[0].PkScript + sweepTXID := sweepTx.TxHash() + sweepScript := sweepTx.TxOut[0].PkScript confNtfn, err := h.Notifier.RegisterConfirmationsNtfn( &sweepTXID, sweepScript, 1, h.broadcastHeight, ) From 9b6c18136cc805f4fbe7a5433fb05641dff3afff Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Mon, 3 Feb 2020 16:18:39 +0100 Subject: [PATCH 2/3] cnct: do not copy success resolver To avoid complaints of the linter about copying the lock that will be introduced in a next commit. --- contractcourt/briefcase_test.go | 4 ++-- contractcourt/htlc_incoming_contest_resolver.go | 12 ++++++------ contractcourt/htlc_incoming_resolver_test.go | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/contractcourt/briefcase_test.go b/contractcourt/briefcase_test.go index 1ec05bf59b0..d566ba88c44 100644 --- a/contractcourt/briefcase_test.go +++ b/contractcourt/briefcase_test.go @@ -208,7 +208,7 @@ func assertResolversEqual(t *testing.T, originalResolver ContractResolver, case *htlcIncomingContestResolver: diskRes := diskResolver.(*htlcIncomingContestResolver) assertSuccessResEqual( - &ogRes.htlcSuccessResolver, &diskRes.htlcSuccessResolver, + ogRes.htlcSuccessResolver, diskRes.htlcSuccessResolver, ) if ogRes.htlcExpiry != diskRes.htlcExpiry { @@ -311,7 +311,7 @@ func TestContractInsertionRetrieval(t *testing.T) { contestSuccess.htlcResolution.ClaimOutpoint = randOutPoint() resolvers = append(resolvers, &htlcIncomingContestResolver{ htlcExpiry: 100, - htlcSuccessResolver: contestSuccess, + htlcSuccessResolver: &contestSuccess, }) // For quick lookup during the test, we'll create this map which allow diff --git a/contractcourt/htlc_incoming_contest_resolver.go b/contractcourt/htlc_incoming_contest_resolver.go index 98e52500a9a..8622e89dfc8 100644 --- a/contractcourt/htlc_incoming_contest_resolver.go +++ b/contractcourt/htlc_incoming_contest_resolver.go @@ -30,7 +30,7 @@ type htlcIncomingContestResolver struct { // htlcSuccessResolver is the inner resolver that may be utilized if we // learn of the preimage. - htlcSuccessResolver + *htlcSuccessResolver } // newIncomingContestResolver instantiates a new incoming htlc contest resolver. @@ -44,7 +44,7 @@ func newIncomingContestResolver( return &htlcIncomingContestResolver{ htlcExpiry: htlc.RefundTimeout, - htlcSuccessResolver: *success, + htlcSuccessResolver: success, } } @@ -186,7 +186,7 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) { return nil, err } - return &h.htlcSuccessResolver, nil + return h.htlcSuccessResolver, nil } // Create a buffered hodl chan to prevent deadlock. @@ -232,7 +232,7 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) { return nil, err } - return &h.htlcSuccessResolver, nil + return h.htlcSuccessResolver, nil } for { @@ -252,7 +252,7 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) { // We've learned of the preimage and this information // has been added to our inner resolver. We return it so // it can continue contract resolution. - return &h.htlcSuccessResolver, nil + return h.htlcSuccessResolver, nil case hodlItem := <-hodlChan: htlcResolution := hodlItem.(invoices.HtlcResolution) @@ -352,7 +352,7 @@ func newIncomingContestResolverFromReader(r io.Reader, resCfg ResolverConfig) ( if err != nil { return nil, err } - h.htlcSuccessResolver = *successResolver + h.htlcSuccessResolver = successResolver return h, nil } diff --git a/contractcourt/htlc_incoming_resolver_test.go b/contractcourt/htlc_incoming_resolver_test.go index 773b22c003d..81564e26b8d 100644 --- a/contractcourt/htlc_incoming_resolver_test.go +++ b/contractcourt/htlc_incoming_resolver_test.go @@ -261,7 +261,7 @@ func newIncomingResolverTestContext(t *testing.T) *incomingResolverTestContext { }, } resolver := &htlcIncomingContestResolver{ - htlcSuccessResolver: htlcSuccessResolver{ + htlcSuccessResolver: &htlcSuccessResolver{ contractResolverKit: *newContractResolverKit(cfg), htlcResolution: lnwallet.IncomingHtlcResolution{}, htlc: channeldb.HTLC{ From 0bc598ca9eafd40a082df074daca014cc1c7ee73 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Mon, 3 Feb 2020 12:26:14 +0100 Subject: [PATCH 3/3] cnct: remove nursery dependency from success resolver This commit updates the success resolver to (only) use the sweeper and not rely on utxo nursery anymore. This also gets rid of an unnecessary wait that was always performed in the nursery. In addition to that, the resolver is made stateless. This prevents us from persisting the contract report and makes the implementation more robust in general. --- contractcourt/briefcase_test.go | 4 - contractcourt/htlc_success_resolver.go | 257 ++++++++++-------- ...d_multi-hop_htlc_local_chain_claim_test.go | 13 +- 3 files changed, 151 insertions(+), 123 deletions(-) diff --git a/contractcourt/briefcase_test.go b/contractcourt/briefcase_test.go index d566ba88c44..5bf62daf20b 100644 --- a/contractcourt/briefcase_test.go +++ b/contractcourt/briefcase_test.go @@ -176,10 +176,6 @@ func assertResolversEqual(t *testing.T, originalResolver ContractResolver, t.Fatalf("expected %v, got %v", ogRes.outputIncubating, diskRes.outputIncubating) } - if ogRes.resolved != diskRes.resolved { - t.Fatalf("expected %v, got %v", ogRes.resolved, - diskRes.resolved) - } if ogRes.broadcastHeight != diskRes.broadcastHeight { t.Fatalf("expected %v, got %v", ogRes.broadcastHeight, diskRes.broadcastHeight) diff --git a/contractcourt/htlc_success_resolver.go b/contractcourt/htlc_success_resolver.go index bd3675289c9..3c11557970b 100644 --- a/contractcourt/htlc_success_resolver.go +++ b/contractcourt/htlc_success_resolver.go @@ -2,9 +2,12 @@ package contractcourt import ( "encoding/binary" + "fmt" "io" + "sync" "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" "github.com/davecgh/go-spew/spew" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/input" @@ -40,6 +43,13 @@ type htlcSuccessResolver struct { // htlc contains information on the htlc that we are resolving on-chain. htlc channeldb.HTLC + // currentReport stores the current state of the resolver for reporting + // over the rpc interface. + currentReport ContractReport + + // reportLock prevents concurrent access to the resolver report. + reportLock sync.Mutex + contractResolverKit } @@ -48,12 +58,16 @@ func newSuccessResolver(res lnwallet.IncomingHtlcResolution, broadcastHeight uint32, htlc channeldb.HTLC, resCfg ResolverConfig) *htlcSuccessResolver { - return &htlcSuccessResolver{ + r := &htlcSuccessResolver{ contractResolverKit: *newContractResolverKit(resCfg), htlcResolution: res, broadcastHeight: broadcastHeight, htlc: htlc, } + + r.initReport() + + return r } // ResolverKey returns an identifier which should be globally unique for this @@ -87,156 +101,133 @@ func (h *htlcSuccessResolver) ResolverKey() []byte { // // NOTE: Part of the ContractResolver interface. func (h *htlcSuccessResolver) Resolve() (ContractResolver, error) { - // If we're already resolved, then we can exit early. - if h.resolved { - return nil, nil - } + var sweepInput input.Input // If we don't have a success transaction, then this means that this is // an output on the remote party's commitment transaction. - if h.htlcResolution.SignedSuccessTx == nil { - log.Infof("%T(%x): crafting sweep tx for "+ - "incoming+remote htlc confirmed", h, - h.htlc.RHash[:]) - - // Before we can craft out sweeping transaction, we - // need to create an input which contains all the items - // required to add this input to a sweeping transaction, - // and generate a witness. - inp := input.MakeHtlcSucceedInput( - &h.htlcResolution.ClaimOutpoint, - &h.htlcResolution.SweepSignDesc, - h.htlcResolution.Preimage[:], - h.broadcastHeight, - ) + if h.htlcResolution.SignedSuccessTx != nil { + log.Infof("%T(%x): broadcasting second-layer transition tx: %v", + h, h.htlc.RHash[:], spew.Sdump(h.htlcResolution.SignedSuccessTx)) - // With the input created, we can now generate the full - // sweep transaction, that we'll use to move these - // coins back into the backing wallet. - // - // TODO: Set tx lock time to current block height - // instead of zero. Will be taken care of once sweeper - // implementation is complete. + // We'll now broadcast the second layer transaction so we can kick off + // the claiming process. // - // TODO: Use time-based sweeper and result chan. - sweepTx, err := h.Sweeper.CreateSweepTx( - []input.Input{&inp}, - sweep.FeePreference{ - ConfTarget: sweepConfTarget, - }, 0, - ) + // TODO(roasbeef): after changing sighashes send to tx bundler + err := h.PublishTx(h.htlcResolution.SignedSuccessTx) if err != nil { return nil, err } - log.Infof("%T(%x): crafted sweep tx=%v", h, - h.htlc.RHash[:], spew.Sdump(sweepTx)) - - // With the sweep transaction signed, we'll now - // Checkpoint our state. - if err := h.Checkpoint(h); err != nil { - log.Errorf("unable to Checkpoint: %v", err) + // Wait for success tx to confirm. + confHeight, err := h.waitForSecondLevelConf(1) + if err != nil { return nil, err } - // Broadcast the sweep transaction to the network. - if err := h.PublishTx(sweepTx); err != nil { - log.Infof("%T(%x): unable to publish tx: %v", - h, h.htlc.RHash[:], err) - return nil, err - } + // Update reported maturity height and advance to stage two + // (waiting for csv lock). + h.reportLock.Lock() + h.currentReport.MaturityHeight = + confHeight + h.htlcResolution.CsvDelay - 1 + h.currentReport.Stage = 2 + h.reportLock.Unlock() - // With the sweep transaction broadcast, we'll wait for its - // confirmation. - sweepTXID := sweepTx.TxHash() - sweepScript := sweepTx.TxOut[0].PkScript - confNtfn, err := h.Notifier.RegisterConfirmationsNtfn( - &sweepTXID, sweepScript, 1, h.broadcastHeight, - ) + // Wait for csv lock to expire. + _, err = h.waitForSecondLevelConf(h.htlcResolution.CsvDelay) if err != nil { return nil, err } - log.Infof("%T(%x): waiting for sweep tx (txid=%v) to be "+ - "confirmed", h, h.htlc.RHash[:], sweepTXID) - - select { - case _, ok := <-confNtfn.Confirmed: - if !ok { - return nil, errResolverShuttingDown - } - - case <-h.quit: - return nil, errResolverShuttingDown - } - - // Once the transaction has received a sufficient number of - // confirmations, we'll mark ourselves as fully resolved and exit. - h.resolved = true - return nil, h.Checkpoint(h) + // Create final input for the sweeper. + sweepInput = input.NewCsvInput( + &h.htlcResolution.ClaimOutpoint, + input.HtlcAcceptedSuccessSecondLevel, + &h.htlcResolution.SweepSignDesc, + h.broadcastHeight, h.htlcResolution.CsvDelay, + ) + } else { + // Create input to sweep htlc from commitment tx. + inp := input.MakeHtlcSucceedInput( + &h.htlcResolution.ClaimOutpoint, + &h.htlcResolution.SweepSignDesc, + h.htlcResolution.Preimage[:], + h.broadcastHeight, + ) + sweepInput = &inp } - log.Infof("%T(%x): broadcasting second-layer transition tx: %v", - h, h.htlc.RHash[:], spew.Sdump(h.htlcResolution.SignedSuccessTx)) + log.Infof("%T(%x): sweeping output for "+ + "incoming+remote htlc confirmed", h, + h.htlc.RHash[:]) - // We'll now broadcast the second layer transaction so we can kick off - // the claiming process. - // - // TODO(roasbeef): after changing sighashes send to tx bundler - err := h.PublishTx(h.htlcResolution.SignedSuccessTx) + // Offer the created input to the sweeper. + resultChan, err := h.Sweeper.SweepInput( + sweepInput, + sweep.Params{ + Fee: sweep.FeePreference{ + ConfTarget: sweepConfTarget, + }, + }, + ) if err != nil { return nil, err } - // Otherwise, this is an output on our commitment transaction. In this - // case, we'll send it to the incubator, but only if we haven't already - // done so. - if !h.outputIncubating { - log.Infof("%T(%x): incubating incoming htlc output", - h, h.htlc.RHash[:]) - - err := h.IncubateOutputs( - h.ChanPoint, nil, &h.htlcResolution, - h.broadcastHeight, - ) - if err != nil { - return nil, err + // Wait for the sweep result. + select { + case result, ok := <-resultChan: + if !ok { + return nil, errResolverShuttingDown + } + if result.Err != nil { + return nil, result.Err } - h.outputIncubating = true + log.Infof("%T(%x): sweep tx (txid=%v) confirmed", + h, h.htlc.RHash[:], result.Tx) - if err := h.Checkpoint(h); err != nil { - log.Errorf("unable to Checkpoint: %v", err) - return nil, err - } + case <-h.quit: + return nil, errResolverShuttingDown } - // To wrap this up, we'll wait until the second-level transaction has - // been spent, then fully resolve the contract. - spendNtfn, err := h.Notifier.RegisterSpendNtfn( - &h.htlcResolution.ClaimOutpoint, - h.htlcResolution.SweepSignDesc.Output.PkScript, - h.broadcastHeight, + // Funds have been swept and balance is no longer in limbo. + h.reportLock.Lock() + h.currentReport.RecoveredBalance = h.currentReport.LimboBalance + h.currentReport.LimboBalance = 0 + h.reportLock.Unlock() + + // Once the transaction has received a sufficient number of + // confirmations, we'll mark ourselves as fully resolved and exit. + h.resolved = true + return nil, nil +} + +func (h *htlcSuccessResolver) waitForSecondLevelConf(confDepth uint32) ( + uint32, error) { + + txID := h.htlcResolution.SignedSuccessTx.TxHash() + pkScript := h.htlcResolution.SignedSuccessTx.TxOut[0].PkScript + + confChan, err := h.Notifier.RegisterConfirmationsNtfn( + &txID, pkScript, confDepth, h.broadcastHeight, ) if err != nil { - return nil, err + return 0, err } - - log.Infof("%T(%x): waiting for second-level HTLC output to be spent "+ - "after csv_delay=%v", h, h.htlc.RHash[:], h.htlcResolution.CsvDelay) + defer confChan.Cancel() select { - case _, ok := <-spendNtfn.Spend: + case conf, ok := <-confChan.Confirmed: if !ok { - return nil, errResolverShuttingDown + return 0, fmt.Errorf("cannot get confirmation "+ + "for commit tx %v", txID) } + return conf.BlockHeight, nil + case <-h.quit: - return nil, errResolverShuttingDown + return 0, errResolverShuttingDown } - - h.resolved = true - return nil, h.Checkpoint(h) } // Stop signals the resolver to cancel any current resolution processes, and @@ -270,9 +261,13 @@ func (h *htlcSuccessResolver) Encode(w io.Writer) error { if err := binary.Write(w, endian, h.outputIncubating); err != nil { return err } - if err := binary.Write(w, endian, h.resolved); err != nil { + + // This was previously the resolved state of the resolver. Write a dummy + // value, because the resolver is stateless. + if err := binary.Write(w, endian, false); err != nil { return err } + if err := binary.Write(w, endian, h.broadcastHeight); err != nil { return err } @@ -303,9 +298,13 @@ func newSuccessResolverFromReader(r io.Reader, resCfg ResolverConfig) ( if err := binary.Read(r, endian, &h.outputIncubating); err != nil { return nil, err } - if err := binary.Read(r, endian, &h.resolved); err != nil { + + // Read a dummy byte that previously stored the resolved state. + var dummy bool + if err := binary.Read(r, endian, &dummy); err != nil { return nil, err } + if err := binary.Read(r, endian, &h.broadcastHeight); err != nil { return nil, err } @@ -313,6 +312,8 @@ func newSuccessResolverFromReader(r io.Reader, resCfg ResolverConfig) ( return nil, err } + h.initReport() + return h, nil } @@ -331,6 +332,34 @@ func (h *htlcSuccessResolver) HtlcPoint() wire.OutPoint { return h.htlcResolution.HtlcPoint() } +// report returns a report on the resolution state of the contract. +func (h *htlcSuccessResolver) report() *ContractReport { + h.reportLock.Lock() + defer h.reportLock.Unlock() + + copy := h.currentReport + return © +} + +// initReport initializes the pending channels report for this resolver. +func (h *htlcSuccessResolver) initReport() { + amt := btcutil.Amount( + h.htlcResolution.SweepSignDesc.Output.Value, + ) + + // Set the initial report. Because we resolve via the success path, we + // don't need to wait for the htlc to expire and can start in stage 2 + // right away. + h.currentReport = ContractReport{ + Outpoint: h.htlcResolution.ClaimOutpoint, + Type: ReportOutputIncomingHtlc, + Amount: amt, + LimboBalance: amt, + RecoveredBalance: 0, + Stage: 1, + } +} + // A compile time assertion to ensure htlcSuccessResolver meets the // ContractResolver interface. var _ htlcContractResolver = (*htlcSuccessResolver)(nil) diff --git a/lntest/itest/lnd_multi-hop_htlc_local_chain_claim_test.go b/lntest/itest/lnd_multi-hop_htlc_local_chain_claim_test.go index 123359dc49a..118e1cd8f25 100644 --- a/lntest/itest/lnd_multi-hop_htlc_local_chain_claim_test.go +++ b/lntest/itest/lnd_multi-hop_htlc_local_chain_claim_test.go @@ -212,8 +212,10 @@ func testMultiHopHtlcLocalChainClaim(net *lntest.NetworkHarness, t *harnessTest) assertTxInBlock(t, block, txid) } - // Keep track of the second level tx maturity. - carolSecondLevelCSV := uint32(defaultCSV) + // Keep track of the second level tx maturity. The transaction is + // already confirmed, so we don't need to wait for the first block of + // the csv lock. + carolSecondLevelCSV := uint32(defaultCSV) - 1 // When Bob notices Carol's second level transaction in the block, he // will extract the preimage and broadcast a second level tx to claim @@ -287,9 +289,10 @@ func testMultiHopHtlcLocalChainClaim(net *lntest.NetworkHarness, t *harnessTest) } assertTxInBlock(t, block, bobSecondLvlTx) - // Keep track of Bob's second level maturity, and decrement our track - // of Carol's. - bobSecondLevelCSV := uint32(defaultCSV) + // Keep track of Bob's second level maturity, and decrement our track of + // Carol's. Bob's second level tx is already confirmed, so we don't need + // to wait for the first block of that csv lock. + bobSecondLevelCSV := uint32(defaultCSV) - 1 carolSecondLevelCSV-- // Now that the preimage from Bob has hit the chain, restart Alice to