diff --git a/funding/manager.go b/funding/manager.go index 648448a0211..b99201004d4 100644 --- a/funding/manager.go +++ b/funding/manager.go @@ -303,9 +303,8 @@ type Config struct { // funds from on-chain transaction outputs into Lightning channels. Wallet *lnwallet.LightningWallet - // PublishTransaction facilitates the process of broadcasting a - // transaction to the network. - PublishTransaction func(*wire.MsgTx, string) error + // PublisherCfg is the config required to initialise the Publisher. + PublisherCfg *PublisherCfg // UpdateLabel updates the label that a transaction has in our wallet, // overwriting any existing labels. @@ -527,6 +526,12 @@ type Manager struct { handleFundingLockedMtx sync.RWMutex handleFundingLockedBarriers map[lnwire.ChannelID]struct{} + // publisher facilitates the process of first checking the sanity and + // standardness of a transaction, then possible testing its acceptance + // to the mempool if one is available, and then broadcasting it to the + // network. + publisher *Publisher + quit chan struct{} wg sync.WaitGroup } @@ -580,6 +585,7 @@ func NewFundingManager(cfg Config) (*Manager, error) { fundingRequests: make(chan *InitFundingMsg, msgBufferSize), localDiscoverySignals: make(map[lnwire.ChannelID]chan struct{}), handleFundingLockedBarriers: make(map[lnwire.ChannelID]struct{}), + publisher: NewPublisher(cfg.PublisherCfg), quit: make(chan struct{}), }, nil } @@ -658,15 +664,13 @@ func (f *Manager) start() error { labels.LabelTypeChannelOpen, nil, ) - err = f.cfg.PublishTransaction( + err = f.publisher.CheckAndPublish( channel.FundingTxn, label, ) - if err != nil { - log.Errorf("Unable to rebroadcast "+ - "funding tx %x for "+ - "ChannelPoint(%v): %v", - fundingTxBuf.Bytes(), - channel.FundingOutpoint, err) + if f.maybeAbortOnStartupTxPublishErr( + err, channel, fundingTxBuf.Bytes(), + ) { + continue } } } @@ -778,6 +782,12 @@ func (f *Manager) failFundingFlow(peer lnpeer.Peer, tempChanID [32]byte, ctx.err <- fundingErr } + f.sendError(peer, tempChanID, fundingErr) +} + +func (f *Manager) sendError(peer lnpeer.Peer, tempChanID [32]byte, + fundingErr error) { + // We only send the exact error if it is part of out whitelisted set of // errors (lnwire.FundingError or lnwallet.ReservationError). var msg lnwire.ErrorData @@ -1049,6 +1059,49 @@ func (f *Manager) stateStep(channel *channeldb.OpenChannel, return fmt.Errorf("undefined channelState: %v", channelState) } +// failPendingChannel creates a close summary and closes the channel if it's +// initiated by us and it's in pending state. +func (f *Manager) failPendingChannel(pendingChan *channeldb.OpenChannel) error { + // Check the channel is pending. + if !pendingChan.IsPending { + return fmt.Errorf("cannot fail a non-pending channel: %v", + pendingChan.FundingOutpoint, + ) + } + + // Check we are the initiator. + if !pendingChan.IsInitiator { + return fmt.Errorf("cannot fail a remote pending channel: %v", + pendingChan.FundingOutpoint, + ) + } + + // Create the close summary. + localBalance := pendingChan.LocalCommitment.LocalBalance.ToSatoshis() + closeInfo := &channeldb.ChannelCloseSummary{ + ChanPoint: pendingChan.FundingOutpoint, + ChainHash: pendingChan.ChainHash, + RemotePub: pendingChan.IdentityPub, + CloseType: channeldb.FundingCanceled, + Capacity: pendingChan.Capacity, + SettledBalance: localBalance, + RemoteCurrentRevocation: pendingChan.RemoteCurrentRevocation, + RemoteNextRevocation: pendingChan.RemoteNextRevocation, + LocalChanConfig: pendingChan.LocalChanCfg, + } + + // Close the channel with us as the initiator because we are + // deciding to exit the funding flow due to an internal error. + if err := pendingChan.CloseChannel( + closeInfo, channeldb.ChanStatusLocalCloseInitiator, + ); err != nil { + log.Errorf("Failed closing channel %v: %v", + pendingChan.FundingOutpoint, err) + return err + } + return nil +} + // advancePendingChannelState waits for a pending channel's funding tx to // confirm, and marks it open in the database when that happens. func (f *Manager) advancePendingChannelState( @@ -2146,19 +2199,13 @@ func (f *Manager) handleFundingSigned(peer lnpeer.Peer, labels.LabelTypeChannelOpen, nil, ) - err = f.cfg.PublishTransaction(fundingTx, label) - if err != nil { - log.Errorf("Unable to broadcast funding tx %x for "+ - "ChannelPoint(%v): %v", fundingTxBuf.Bytes(), - completeChan.FundingOutpoint, err) + err = f.publisher.CheckAndPublish(fundingTx, label) + if f.maybeAbortOnTxPublishErr(err, resCtx, completeChan, + fundingTxBuf.Bytes()) { - // We failed to broadcast the funding transaction, but - // watch the channel regardless, in case the - // transaction made it to the network. We will retry - // broadcast at startup. - // - // TODO(halseth): retry more often? Handle with CPFP? - // Just delete from the DB? + log.Errorf("Aborted funding flow for ChannelPoint(%v)", + fundingPoint) + return } } @@ -3689,3 +3736,125 @@ func (f *Manager) deleteChannelOpeningState(chanPoint *wire.OutPoint) error { outpointBytes.Bytes(), ) } +func (f *Manager) maybeAbortOnTxPublishErr(publishErr error, + resCtx *reservationWithCtx, ch *channeldb.OpenChannel, + fundingTxBytes []byte) bool { + + if publishErr == nil { + return false + } + + log.Errorf("Unable to broadcast funding tx %x for ChannelPoint(%v): %v", + fundingTxBytes, ch.FundingOutpoint, publishErr) + + externallyFunded := resCtx.reservation.IsPsbt() || + resCtx.reservation.IsCannedShim() + + abort := func() bool { + if _, ok := publishErr.(*ErrSanity); ok { + return true + } + + if externallyFunded { + return false + } + + switch publishErr.(type) { + case *ErrStandardness, *ErrMempoolTestAccept: + return true + + case *ErrPublish: + return false + } + + return false + }() + + if !abort { + // We failed to broadcast the funding transaction, but + // watch the channel regardless, in case the + // transaction made it to the network. We will retry + // broadcast at startup. + // + // TODO(halseth): retry more often? Handle with CPFP? + // Just delete from the DB? + return false + } + + err := f.failPendingChannel(ch) + if err != nil { + log.Errorf("Unable to close channel for ChannelPoint(%v): %v", + fundingTxBytes, err) + } + + err = resCtx.reservation.Cancel() + if err != nil { + log.Errorf("Unable to cancel reservation: %v", err) + } + + return true +} + +func (f *Manager) maybeAbortOnStartupTxPublishErr(err error, + ch *channeldb.OpenChannel, fundingTxBytes []byte) bool { + + if err == nil { + return false + } + + log.Errorf("Unable to broadcast funding tx %x for "+ + "ChannelPoint(%v): %v", fundingTxBytes, ch.FundingOutpoint, err) + + var abort bool + + switch err.(type) { + + // If the transaction failed the sanity check, it means that the tx + // does not pass the consensus rules, so we can immediately abort this + // funding flow. + case *ErrSanity: + abort = true + + case *ErrMempoolTestAccept: + // On startup, we no longer have a lock held on the inputs to + // this transaction, so we might run into a double spend error + // here in which case we can fail the funding flow for this + // channel. + if err.(*ErrStandardness).err == lnwallet.ErrDoubleSpend { + abort = true + } + + case *ErrPublish: + // See comment above for ErrMempoolTestAccept. + // TODO(elle): is this ok for neutrino backend? + if err.(*ErrStandardness).err == lnwallet.ErrDoubleSpend { + abort = true + } + + default: + // For all other errors, we continue to monitor the channel + // since at startup we can't be sure if this transaction has + // been broadcast before (by us or externally). + // TODO(elle): should monitor for a while & occasionally attempt + // to rebroadcast. Since on startup, we dont have locks on the + // inputs for this tx, the inputs should eventually be double + // spent in which case we can safety abort. + abort = false + } + + if abort { + err := f.failPendingChannel(ch) + if err != nil { + // If we get an error failing the pending channel, + // then rather dont abort so that we can retry next + // time. + log.Errorf("Unable to close channel for "+ + "ChannelPoint(%v): %v", fundingTxBytes, err, + ) + + abort = false + } + } + + return abort +} diff --git a/funding/manager_test.go b/funding/manager_test.go index 459ea8cf217..d718408f0ee 100644 --- a/funding/manager_test.go +++ b/funding/manager_test.go @@ -436,9 +436,11 @@ func createTestFundingManager(t *testing.T, privKey *btcec.PrivateKey, ReportShortChanID: func(wire.OutPoint) error { return nil }, - PublishTransaction: func(txn *wire.MsgTx, _ string) error { - publTxChan <- txn - return nil + PublisherCfg: &PublisherCfg{ + Publish: func(txn *wire.MsgTx, _ string) error { + publTxChan <- txn + return nil + }, }, UpdateLabel: func(chainhash.Hash, string) error { return nil @@ -546,9 +548,11 @@ func recreateAliceFundingManager(t *testing.T, alice *testNode) { }, DefaultMinHtlcIn: 5, RequiredRemoteMaxValue: oldCfg.RequiredRemoteMaxValue, - PublishTransaction: func(txn *wire.MsgTx, _ string) error { - publishChan <- txn - return nil + PublisherCfg: &PublisherCfg{ + Publish: func(txn *wire.MsgTx, _ string) error { + publishChan <- txn + return nil + }, }, UpdateLabel: func(chainhash.Hash, string) error { return nil @@ -651,6 +655,8 @@ func openChannel(t *testing.T, alice, bob *testNode, localFundingAmt, updateChan chan *lnrpc.OpenStatusUpdate, announceChan bool) ( *wire.OutPoint, *wire.MsgTx) { + t.Helper() + publ := fundChannel( t, alice, bob, localFundingAmt, pushAmt, false, numConfs, updateChan, announceChan, @@ -668,6 +674,28 @@ func fundChannel(t *testing.T, alice, bob *testNode, localFundingAmt, pushAmt btcutil.Amount, subtractFees bool, numConfs uint32, updateChan chan *lnrpc.OpenStatusUpdate, announceChan bool) *wire.MsgTx { + t.Helper() + + acceptChannelResponse := initChannel( + t, alice, bob, localFundingAmt, pushAmt, + subtractFees, updateChan, announceChan, + ) + + signChannel(t, alice, bob, acceptChannelResponse) + + return finishOpenChannel(t, alice, bob, updateChan) + +} + +// initChannel takes the funding process to the point where an +// acceptChannelResponse is returned from Bob. +func initChannel(t *testing.T, alice, bob *testNode, localFundingAmt, + pushAmt btcutil.Amount, subtractFees bool, + updateChan chan *lnrpc.OpenStatusUpdate, + announceChan bool) lnwire.Message { + + t.Helper() + // Create a funding request and start the workflow. errChan := make(chan error, 1) initReq := &InitFundingMsg{ @@ -720,6 +748,17 @@ func fundChannel(t *testing.T, alice, bob *testNode, localFundingAmt, assertNumPendingReservations(t, alice, bobPubKey, 1) assertNumPendingReservations(t, bob, alicePubKey, 1) + return acceptChannelResponse +} + +// signChannel starts when Bob has returned an acceptChannelResponse, takes +// the funding process to the point where the funding transaction is signed by +// both parties. +func signChannel(t *testing.T, alice, bob *testNode, + acceptChannelResponse lnwire.Message) { + + t.Helper() + // Forward the response to Alice. alice.fundingMgr.ProcessFundingMsg(acceptChannelResponse, bob) @@ -738,6 +777,15 @@ func fundChannel(t *testing.T, alice, bob *testNode, localFundingAmt, // Forward the signature to Alice. alice.fundingMgr.ProcessFundingMsg(fundingSigned, bob) +} + +// finishOpenChannel starts when both parties have signed the funding +// transction and takes the funding process to the point where the funding +// transaction is confirmed on-chain. Returns the funding tx. +func finishOpenChannel(t *testing.T, alice, bob *testNode, + updateChan chan *lnrpc.OpenStatusUpdate) *wire.MsgTx { + + t.Helper() // After Alice processes the singleFundingSignComplete message, she will // broadcast the funding transaction to the network. We expect to get a @@ -749,7 +797,7 @@ func fundChannel(t *testing.T, alice, bob *testNode, localFundingAmt, t.Fatalf("alice did not send OpenStatusUpdate_ChanPending") } - _, ok = pendingUpdate.Update.(*lnrpc.OpenStatusUpdate_ChanPending) + _, ok := pendingUpdate.Update.(*lnrpc.OpenStatusUpdate_ChanPending) if !ok { t.Fatal("OpenStatusUpdate was not OpenStatusUpdate_ChanPending") } @@ -3684,3 +3732,44 @@ func testUpfrontFailure(t *testing.T, pkscript []byte, expectErr bool) { require.True(t, ok, "did not receive AcceptChannel") } } + +func TestPublishError(t *testing.T) { + // Ensure that Alice gets an error back when she attempts to publish + // the funding transaction. + alice, bob := setupFundingManagers(t, func(cfg *Config) { + cfg.PublisherCfg = &PublisherCfg{ + TxSanityCheck: func(tx *wire.MsgTx) error { + return fmt.Errorf("sanity check fail") + }, + Publish: func(_ *wire.MsgTx, _ string) error { + return fmt.Errorf("publish error") + }, + } + }) + defer tearDownFundingManagers(t, alice, bob) + + // We will consume the channel updates as we go, so no buffering is + // needed. + updateChan := make(chan *lnrpc.OpenStatusUpdate) + + // Run a channel through the OpenChannel-AcceptChannel steps. + acceptChannelResponse := initChannel( + t, alice, bob, 500000, 0, false, updateChan, true, + ) + + // Run a channel through the FundingCreated-FundingSigned steps. + signChannel(t, alice, bob, acceptChannelResponse) + + // After Alice processes the singleFundingSignComplete message, she will + // attempt to broadcast the funding transaction to the network. We + // expect to get a channel update saying the channel is pending. Note + // that at the moment, this is expected even if an error was returned + // from PublishTransaction. + select { + case <-updateChan: + t.Fatalf("alice sent unexpected update") + case <-time.After(time.Millisecond * 200): + } + + assertNumPendingChannelsRemains(t, alice, 0) +} diff --git a/funding/publisher.go b/funding/publisher.go new file mode 100644 index 00000000000..d6e55dd4541 --- /dev/null +++ b/funding/publisher.go @@ -0,0 +1,155 @@ +package funding + +import ( + "fmt" + + "github.com/btcsuite/btcd/wire" +) + +// PublisherCfg is used to configure the Publisher. +type PublisherCfg struct { + // TxSanityCheck is a context free, static check that verifies some + // basic transaction rules. If an error is returned, it should mean that + // transaction would fail consensus rules. + TxSanityCheck func(tx *wire.MsgTx) error + + // TxStandardnessCheck checks the transaction against some of the main, + // static standardness rules. It is a context check. Failing this check + // means that the transaction would likely fail policy rules but not + // consensus. + TxStandardnessCheck func(tx *wire.MsgTx) error + + // TestMempoolAccept can be set if the chain backend has a mempool + // available. This check uses the context of the current mempool and + // UTXO set. + TestMempoolAccept func(*wire.MsgTx) error + + // Publish will be used to broadcast the transaction to the network + // if it passes all the prior checks. + Publish func(*wire.MsgTx, string) error +} + +// Publisher handles checking the validity of a transaction in various ways +// before attempting to broadcast it. It wraps the errors for each check so +// that decisions can be made based on the type of check that the transaction +// may have failed on. +type Publisher struct { + cfg *PublisherCfg +} + +// NewPublisher constructs a new Publisher instance given the passed config. +func NewPublisher(cfg *PublisherCfg) *Publisher { + return &Publisher{ + cfg: cfg, + } +} + +// CheckAndPublish does various checks of the transaction before attempting +// to publish it. +func (p *Publisher) CheckAndPublish(tx *wire.MsgTx, label string) error { + // First, we do a static sanity check of the transaction. If this fails, + // the transaction would not pass consensus rules. + if p.cfg.TxSanityCheck != nil { + err := p.cfg.TxSanityCheck(tx) + if err != nil { + return &ErrSanity{err} + } + } + + // Next, we do a static standardness check of the transaction. If this + // fails, we can assume that it will be rejected by most mempools but + // there is still a chance that the transaction will eventually make it + // onto the blockchain if it was externally published to a mining + // accelerator. So we can only safely forget about this transaction if + // we know that it was constructed internally. + if p.cfg.TxStandardnessCheck != nil { + err := p.cfg.TxStandardnessCheck(tx) + if err != nil { + return &ErrStandardness{err} + } + } + + // Now, if our chain backend has a mempool instance, we can use it to + // check if the transaction would be accepted. An error here doesn't + // necessarily mean that the transaction would not be accepted to other + // mempools though (for example, maybe the parent of the transaction has + // been evicted from this mempool but not others). If we get an error + // here, we can only safely forget about this transaction if we know + // that it was constructed internally. + if p.cfg.TestMempoolAccept != nil { + err := p.cfg.TestMempoolAccept(tx) + if err != nil { + return &ErrMempoolTestAccept{err} + } + } + + // Finally, we can attempt to broadcast this transaction. If we get an + // error here even after performing all the prior sanity checks, then + // something strange is happening. We should continue to monitor this + // transaction and its inputs. + err := p.cfg.Publish(tx, label) + if err != nil { + return &ErrPublish{err} + } + + return nil +} + +// ErrSanity is the error returned if the sanity check failed. +type ErrSanity struct { + err error +} + +func (e *ErrSanity) Error() string { + return fmt.Sprintf("tx sanity check error: %v", e.err) +} + +// Unwrap returns the underlying error returned from the sanity check function. +func (e *ErrSanity) Unwrap() error { + return e.err +} + +// ErrStandardness is the error returned if the standardness check failed. +type ErrStandardness struct { + err error +} + +func (e *ErrStandardness) Error() string { + return fmt.Sprintf("tx standardness check error: %v", e.err) +} + +// Unwrap returns the underlying error returned from the standardness check +// function. +func (e *ErrStandardness) Unwrap() error { + return e.err +} + +// ErrMempoolTestAccept is the error returned if the testmempoolaccept check +// failed. +type ErrMempoolTestAccept struct { + err error +} + +func (e *ErrMempoolTestAccept) Error() string { + return fmt.Sprintf("test mempool accept error: %v", e.err) +} + +// Unwrap returns the underlying error returned from the testmempoolaccept +// check. +func (e *ErrMempoolTestAccept) Unwrap() error { + return e.err +} + +// ErrPublish is the error returned if the tx broadcast failed. +type ErrPublish struct { + err error +} + +func (e *ErrPublish) Error() string { + return fmt.Sprintf("tx publish error: %v", e.err) +} + +// Unwrap returns the underlying error returned from the publish call. +func (e *ErrPublish) Unwrap() error { + return e.err +} diff --git a/funding/publisher_test.go b/funding/publisher_test.go new file mode 100644 index 00000000000..7123b811c1a --- /dev/null +++ b/funding/publisher_test.go @@ -0,0 +1,97 @@ +package funding + +import ( + "errors" + "fmt" + "testing" + + "github.com/lightningnetwork/lnd/lnwallet" + + "github.com/btcsuite/btcd/wire" + "github.com/stretchr/testify/require" +) + +// TestPublisherErrors ensures that the wrapped errors returned from the +// Publisher can correctly be matched on. +func TestPublisherErrors(t *testing.T) { + errSanity := fmt.Errorf("sanity") + errStandardness := fmt.Errorf("standardness") + errPublish := fmt.Errorf("publish") + + tests := []struct { + cfg *PublisherCfg + expectedPublishErr error + expectedBackendErr error + }{ + { + cfg: &PublisherCfg{ + TxSanityCheck: func(tx *wire.MsgTx) error { + return errSanity + }, + }, + expectedPublishErr: &ErrSanity{}, + expectedBackendErr: errSanity, + }, + { + cfg: &PublisherCfg{ + TxStandardnessCheck: func(tx *wire.MsgTx) error { + return errStandardness + }, + }, + expectedPublishErr: &ErrStandardness{}, + expectedBackendErr: errStandardness, + }, + { + cfg: &PublisherCfg{ + TestMempoolAccept: func(tx *wire.MsgTx) error { + return lnwallet.ErrDoubleSpend + }, + }, + expectedPublishErr: &ErrMempoolTestAccept{}, + expectedBackendErr: lnwallet.ErrDoubleSpend, + }, + { + cfg: &PublisherCfg{ + Publish: func(tx *wire.MsgTx, + label string) error { + + return errPublish + }, + }, + expectedPublishErr: &ErrPublish{}, + expectedBackendErr: errPublish, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + p := NewPublisher(test.cfg) + err := p.CheckAndPublish(nil, "") + + var ( + e error + ok bool + ) + switch test.expectedPublishErr.(type) { + case *ErrSanity: + e, ok = err.(*ErrSanity) + case *ErrStandardness: + e, ok = err.(*ErrStandardness) + case *ErrMempoolTestAccept: + e, ok = err.(*ErrMempoolTestAccept) + case *ErrPublish: + e, ok = err.(*ErrPublish) + } + + if !ok { + t.Fatalf("incorrect publish error") + } + + require.True( + t, errors.Is( + e, test.expectedBackendErr, + ), + ) + }) + } +} diff --git a/go.mod b/go.mod index ae8735c71cf..1cbf83470b1 100644 --- a/go.mod +++ b/go.mod @@ -81,6 +81,8 @@ replace github.com/gogo/protobuf => github.com/gogo/protobuf v1.3.2 // https://github.com/lightninglabs/neutrino/pull/247 is merged. replace github.com/lightninglabs/neutrino => github.com/lightninglabs/neutrino v0.13.2-0.20220209052920-0c79b771272b +replace github.com/btcsuite/btcd => github.com/ellemouton/btcd v0.22.0-beta.0.20220406123313-511b4648786e + // If you change this please also update .github/pull_request_template.md and // docs/INSTALL.md. go 1.16 diff --git a/go.sum b/go.sum index 5f951f7c629..d94b1585c87 100644 --- a/go.sum +++ b/go.sum @@ -69,19 +69,10 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= -github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI= -github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= -github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= -github.com/btcsuite/btcd v0.22.0-beta.0.20220204213055-eaf0459ff879/go.mod h1:osu7EoKiL36UThEgzYPqdRaxeo0NU8VoXqgcnwpey0g= -github.com/btcsuite/btcd v0.22.0-beta.0.20220207191057-4dc4ff7963b4/go.mod h1:7alexyj/lHlOtr2PJK7L/+HDJZpcGDn/pAU98r7DY08= -github.com/btcsuite/btcd v0.22.0-beta.0.20220316175102-8d5c75c28923/go.mod h1:taIcYprAW2g6Z9S0gGUxyR+zDwimyDMK5ePOX+iJ2ds= -github.com/btcsuite/btcd v0.22.0-beta.0.20220330201728-074266215c26 h1:dgH5afJcotX4eXo7+bXp8Z7lOw0FyVxXQwvtkN+jab4= -github.com/btcsuite/btcd v0.22.0-beta.0.20220330201728-074266215c26/go.mod h1:taIcYprAW2g6Z9S0gGUxyR+zDwimyDMK5ePOX+iJ2ds= github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= github.com/btcsuite/btcd/btcec/v2 v2.1.1/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= github.com/btcsuite/btcd/btcec/v2 v2.1.3 h1:xM/n3yIhHAhHy04z4i43C8p4ehixJZMsnrVJkgl+MTE= github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= -github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= github.com/btcsuite/btcd/btcutil v1.1.1 h1:hDcDaXiP0uEzR8Biqo2weECKqEw0uHDZ9ixIWevVQqY= github.com/btcsuite/btcd/btcutil v1.1.1/go.mod h1:nbKlBMNm9FGsdvKvu0essceubPiAcI57pYBNnsLAa34= @@ -93,7 +84,6 @@ github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOF github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= -github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= github.com/btcsuite/btcwallet v0.14.1-0.20220322182735-b0001c262734 h1:gG2UgzXLiMiT4sw74161AEf0LE/mxDM8Ia6TaoV0VBw= github.com/btcsuite/btcwallet v0.14.1-0.20220322182735-b0001c262734/go.mod h1:QN2tl1ipATUQRo9RtgvMHLSspqx7QWsj30qL+7AXuAo= github.com/btcsuite/btcwallet/wallet/txauthor v1.2.1/go.mod h1:/74bubxX5Js48d76nf/TsNabpYp/gndUuJw4chzCmhU= @@ -176,6 +166,8 @@ github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dvyukov/go-fuzz v0.0.0-20210602112143-b1f3d6f4ef4e h1:qTP1telKJHlToHlwPQNmVg4yfMDMHe4Z3SYmzkrvA2M= github.com/dvyukov/go-fuzz v0.0.0-20210602112143-b1f3d6f4ef4e/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= +github.com/ellemouton/btcd v0.22.0-beta.0.20220406123313-511b4648786e h1:yItZwDoYsInJ/9sSdhQE+aduLY6M6YuhURZk+l5nphk= +github.com/ellemouton/btcd v0.22.0-beta.0.20220406123313-511b4648786e/go.mod h1:taIcYprAW2g6Z9S0gGUxyR+zDwimyDMK5ePOX+iJ2ds= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -542,12 +534,10 @@ github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= diff --git a/server.go b/server.go index 4accc6e975b..a8cea1b0f73 100644 --- a/server.go +++ b/server.go @@ -9,12 +9,19 @@ import ( "math/big" prand "math/rand" "net" + "sort" "strconv" "strings" "sync" "sync/atomic" "time" + "github.com/btcsuite/btcd/mempool" + + "github.com/btcsuite/btcwallet/chain" + + "github.com/btcsuite/btcd/blockchain" + "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" @@ -1154,11 +1161,36 @@ func newServer(cfg *Config, listenAddrs []net.Addr, } s.fundingMgr, err = funding.NewFundingManager(funding.Config{ - NoWumboChans: !cfg.ProtocolOptions.Wumbo(), - IDKey: nodeKeyDesc.PubKey, - IDKeyLoc: nodeKeyDesc.KeyLocator, - Wallet: cc.Wallet, - PublishTransaction: cc.Wallet.PublishTransaction, + NoWumboChans: !cfg.ProtocolOptions.Wumbo(), + IDKey: nodeKeyDesc.PubKey, + IDKeyLoc: nodeKeyDesc.KeyLocator, + Wallet: cc.Wallet, + PublisherCfg: &funding.PublisherCfg{ + TxSanityCheck: func(tx *wire.MsgTx) error { + return blockchain.CheckTransactionSanity( + btcutil.NewTx(tx), + ) + }, + TxStandardnessCheck: func(tx *wire.MsgTx) error { + _, height, err := cc.ChainSource.GetBestBlock() + if err != nil { + return err + } + + mpt, err := calcPastMedianTime(s.cc.ChainSource) + if err != nil { + return err + } + + minRelayFee := cc.FeeEstimator.RelayFeePerKW() + + return mempool.CheckTransactionStandard( + btcutil.NewTx(tx), height, mpt, + btcutil.Amount(minRelayFee), 2, + ) + }, + Publish: cc.Wallet.PublishTransaction, + }, UpdateLabel: func(hash chainhash.Hash, label string) error { return cc.Wallet.LabelTransaction(hash, label, true) }, @@ -4351,3 +4383,59 @@ func shouldPeerBootstrap(cfg *Config) bool { // covering the bootstrapping process. return !cfg.NoNetBootstrap && !isDevNetwork } + +// medianTimeBlocks is the number of previous blocks which should be +// used to calculate the median time used to validate block timestamps. +const medianTimeBlocks = 11 + +// calcPastMedianTime calculates the median time of the previous few blocks +// prior to, and including, the block node. +func calcPastMedianTime(chain chain.Interface) (time.Time, error) { + // Create a slice of the previous few block timestamps used to calculate + // the median per the number defined b the constant medianTimeBlocks. + timestamps := make([]int64, 0, medianTimeBlocks) + var ( + hash *chainhash.Hash + height int32 + header *wire.BlockHeader + err error + ) + _, height, err = chain.GetBestBlock() + if err != nil { + return time.Time{}, err + } + for i := 0; i < medianTimeBlocks; i++ { + hash, err = chain.GetBlockHash(int64(height)) + if err != nil { + return time.Time{}, err + } + + header, err = chain.GetBlockHeader(hash) + if err != nil { + return time.Time{}, err + } + + timestamps = append(timestamps, header.Timestamp.Unix()) + height-- + + if height < 0 { + break + } + } + + sort.Slice(timestamps, func(i, j int) bool { + return timestamps[i] < timestamps[j] + }) + + // NOTE: The consensus rules incorrectly calculate the median for even + // numbers of blocks. A true median averages the middle two elements + // for a set with an even number of elements in it. Since the constant + // for the previous number of blocks to be used is odd, this is only an + // issue for a few blocks near the beginning of the chain. + // + // This code follows suit to ensure the same rules are used, however, be + // aware that should the medianTimeBlocks constant ever be changed to an + // even number, this code will be wrong. + medianTimestamp := timestamps[len(timestamps)/2] + return time.Unix(medianTimestamp, 0), nil +} diff --git a/server_test.go b/server_test.go index 50ab6f0fb2a..970b94f046e 100644 --- a/server_test.go +++ b/server_test.go @@ -12,6 +12,7 @@ import ( "crypto/x509" "crypto/x509/pkix" "encoding/pem" + "fmt" "io/ioutil" "math/big" "net" @@ -19,6 +20,11 @@ import ( "testing" "time" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcwallet/chain" + "github.com/stretchr/testify/require" + "github.com/lightningnetwork/lnd/lncfg" ) @@ -249,3 +255,119 @@ func TestShouldPeerBootstrap(t *testing.T) { } } } + +// TestCalcMedianPastTime tests that the calcPastMedianTime function correctly +// calculates the median-past-time from a set of block times. +func TestCalcMedianPastTime(t *testing.T) { + tests := []struct { + times []int64 + expectedMedian int64 + }{ + { + times: []int64{ + 0, 1, 2, 3, 4, 5, + }, + expectedMedian: 3, + }, + { + times: []int64{ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, + }, + expectedMedian: 5, + }, + { + times: []int64{ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, + 14, 15, 16, + }, + expectedMedian: 11, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + chain, err := newMockChainInterface(test.times) + require.NoError(t, err) + + res, err := calcPastMedianTime(chain) + require.NoError(t, err) + require.Equal(t, test.expectedMedian, res.Unix()) + }) + } +} + +type block struct { + hash *chainhash.Hash + timestamp time.Time +} + +type mockChainInterface struct { + blocks []*block + index map[chainhash.Hash]int + + chain.Interface +} + +func newMockChainInterface(times []int64) (*mockChainInterface, error) { + chain := &mockChainInterface{ + blocks: make([]*block, len(times)), + index: make(map[chainhash.Hash]int), + } + + var b [32]byte + for i, t := range times { + _, err := rand.Read(b[:]) + if err != nil { + return nil, err + } + + hash, err := chainhash.NewHash(b[:]) + if err != nil { + return nil, err + } + + chain.blocks[i] = &block{ + hash: hash, + timestamp: time.Unix(t, 0), + } + chain.index[*hash] = i + } + + return chain, nil +} + +func (m *mockChainInterface) GetBestBlock() (*chainhash.Hash, int32, error) { + if len(m.blocks) == 0 { + return nil, 0, fmt.Errorf("empty blockchain") + } + + index := len(m.blocks) - 1 + block := m.blocks[index] + + return block.hash, int32(index), nil +} + +func (m *mockChainInterface) GetBlockHeader( + hash *chainhash.Hash) (*wire.BlockHeader, error) { + + index, ok := m.index[*hash] + if !ok { + return nil, fmt.Errorf("block not found") + } + + block := m.blocks[index] + + return &wire.BlockHeader{ + Timestamp: block.timestamp, + }, nil +} + +func (m *mockChainInterface) GetBlockHash(height int64) (*chainhash.Hash, + error) { + + if len(m.blocks)-1 < int(height) { + return nil, fmt.Errorf("block with given height does not exist") + } + + return m.blocks[height].hash, nil +}