Merge pull request #3561 from Roasbeef/chan-validation-cleanup
chanvalidate: create new channel validation package
This commit is contained in:
commit
c03fb8aca0
@ -969,7 +969,7 @@ func (f *fundingManager) stateStep(channel *channeldb.OpenChannel,
|
||||
func (f *fundingManager) advancePendingChannelState(
|
||||
channel *channeldb.OpenChannel, pendingChanID [32]byte) error {
|
||||
|
||||
shortChanID, err := f.waitForFundingWithTimeout(channel)
|
||||
confChannel, err := f.waitForFundingWithTimeout(channel)
|
||||
if err == ErrConfirmationTimeout {
|
||||
// We'll get a timeout if the number of blocks mined
|
||||
// since the channel was initiated reaches
|
||||
@ -1031,9 +1031,9 @@ func (f *fundingManager) advancePendingChannelState(
|
||||
// Success, funding transaction was confirmed.
|
||||
chanID := lnwire.NewChanIDFromOutPoint(&channel.FundingOutpoint)
|
||||
fndgLog.Debugf("ChannelID(%v) is now fully confirmed! "+
|
||||
"(shortChanID=%v)", chanID, shortChanID)
|
||||
"(shortChanID=%v)", chanID, confChannel.shortChanID)
|
||||
|
||||
err = f.handleFundingConfirmation(channel, *shortChanID)
|
||||
err = f.handleFundingConfirmation(channel, confChannel)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to handle funding "+
|
||||
"confirmation for ChannelPoint(%v): %v",
|
||||
@ -1792,15 +1792,28 @@ func (f *fundingManager) handleFundingSigned(fmsg *fundingSignedMsg) {
|
||||
go f.advanceFundingState(completeChan, pendingChanID, resCtx.updates)
|
||||
}
|
||||
|
||||
// confirmedChannel wraps a confirmed funding transaction, as well as the short
|
||||
// channel ID which identifies that channel into a single struct. We'll use
|
||||
// this to pass around the final state of a channel after it has been
|
||||
// confirmed.
|
||||
type confirmedChannel struct {
|
||||
// shortChanID expresses where in the block the funding transaction was
|
||||
// located.
|
||||
shortChanID lnwire.ShortChannelID
|
||||
|
||||
// fundingTx is the funding transaction that created the channel.
|
||||
fundingTx *wire.MsgTx
|
||||
}
|
||||
|
||||
// waitForFundingWithTimeout is a wrapper around waitForFundingConfirmation and
|
||||
// waitForTimeout that will return ErrConfirmationTimeout if we are not the
|
||||
// channel initiator and the maxWaitNumBlocksFundingConf has passed from the
|
||||
// funding broadcast height. In case of confirmation, the short channel ID of
|
||||
// the channel will be returned.
|
||||
// the channel and the funding transaction will be returned.
|
||||
func (f *fundingManager) waitForFundingWithTimeout(
|
||||
ch *channeldb.OpenChannel) (*lnwire.ShortChannelID, error) {
|
||||
ch *channeldb.OpenChannel) (*confirmedChannel, error) {
|
||||
|
||||
confChan := make(chan *lnwire.ShortChannelID)
|
||||
confChan := make(chan *confirmedChannel)
|
||||
timeoutChan := make(chan error, 1)
|
||||
cancelChan := make(chan struct{})
|
||||
|
||||
@ -1816,8 +1829,6 @@ func (f *fundingManager) waitForFundingWithTimeout(
|
||||
}
|
||||
defer close(cancelChan)
|
||||
|
||||
var shortChanID *lnwire.ShortChannelID
|
||||
var ok bool
|
||||
select {
|
||||
case err := <-timeoutChan:
|
||||
if err != nil {
|
||||
@ -1830,12 +1841,12 @@ func (f *fundingManager) waitForFundingWithTimeout(
|
||||
// startup.
|
||||
return nil, ErrFundingManagerShuttingDown
|
||||
|
||||
case shortChanID, ok = <-confChan:
|
||||
case confirmedChannel, ok := <-confChan:
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("waiting for funding" +
|
||||
"confirmation failed")
|
||||
}
|
||||
return shortChanID, nil
|
||||
return confirmedChannel, nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -1864,7 +1875,7 @@ func makeFundingScript(channel *channeldb.OpenChannel) ([]byte, error) {
|
||||
// NOTE: This MUST be run as a goroutine.
|
||||
func (f *fundingManager) waitForFundingConfirmation(
|
||||
completeChan *channeldb.OpenChannel, cancelChan <-chan struct{},
|
||||
confChan chan<- *lnwire.ShortChannelID) {
|
||||
confChan chan<- *confirmedChannel) {
|
||||
|
||||
defer f.wg.Done()
|
||||
defer close(confChan)
|
||||
@ -1924,22 +1935,8 @@ func (f *fundingManager) waitForFundingConfirmation(
|
||||
}
|
||||
|
||||
fundingPoint := completeChan.FundingOutpoint
|
||||
chanID := lnwire.NewChanIDFromOutPoint(&fundingPoint)
|
||||
if int(fundingPoint.Index) >= len(confDetails.Tx.TxOut) {
|
||||
fndgLog.Warnf("Funding point index does not exist for "+
|
||||
"ChannelPoint(%v)", completeChan.FundingOutpoint)
|
||||
return
|
||||
}
|
||||
|
||||
outputAmt := btcutil.Amount(confDetails.Tx.TxOut[fundingPoint.Index].Value)
|
||||
if outputAmt != completeChan.Capacity {
|
||||
fndgLog.Warnf("Invalid output value for ChannelPoint(%v)",
|
||||
completeChan.FundingOutpoint)
|
||||
return
|
||||
}
|
||||
|
||||
fndgLog.Infof("ChannelPoint(%v) is now active: ChannelID(%x)",
|
||||
fundingPoint, chanID[:])
|
||||
fundingPoint, lnwire.NewChanIDFromOutPoint(&fundingPoint))
|
||||
|
||||
// With the block height and the transaction index known, we can
|
||||
// construct the compact chanID which is used on the network to unique
|
||||
@ -1951,7 +1948,10 @@ func (f *fundingManager) waitForFundingConfirmation(
|
||||
}
|
||||
|
||||
select {
|
||||
case confChan <- &shortChanID:
|
||||
case confChan <- &confirmedChannel{
|
||||
shortChanID: shortChanID,
|
||||
fundingTx: confDetails.Tx,
|
||||
}:
|
||||
case <-f.quit:
|
||||
return
|
||||
}
|
||||
@ -2022,7 +2022,7 @@ func (f *fundingManager) waitForTimeout(completeChan *channeldb.OpenChannel,
|
||||
// for this channel.
|
||||
func (f *fundingManager) handleFundingConfirmation(
|
||||
completeChan *channeldb.OpenChannel,
|
||||
shortChanID lnwire.ShortChannelID) error {
|
||||
confChannel *confirmedChannel) error {
|
||||
|
||||
fundingPoint := completeChan.FundingOutpoint
|
||||
chanID := lnwire.NewChanIDFromOutPoint(&fundingPoint)
|
||||
@ -2030,13 +2030,24 @@ func (f *fundingManager) handleFundingConfirmation(
|
||||
// TODO(roasbeef): ideally persistent state update for chan above
|
||||
// should be abstracted
|
||||
|
||||
// Now that that the channel has been fully confirmed, we'll request
|
||||
// that the wallet fully verify this channel to ensure that it can be
|
||||
// used.
|
||||
err := f.cfg.Wallet.ValidateChannel(completeChan, confChannel.fundingTx)
|
||||
if err != nil {
|
||||
// TODO(roasbeef): delete chan state?
|
||||
return fmt.Errorf("unable to validate channel: %v", err)
|
||||
}
|
||||
|
||||
// The funding transaction now being confirmed, we add this channel to
|
||||
// the fundingManager's internal persistent state machine that we use
|
||||
// to track the remaining process of the channel opening. This is
|
||||
// useful to resume the opening process in case of restarts. We set the
|
||||
// opening state before we mark the channel opened in the database,
|
||||
// such that we can receover from one of the db writes failing.
|
||||
err := f.saveChannelOpeningState(&fundingPoint, markedOpen, &shortChanID)
|
||||
err = f.saveChannelOpeningState(
|
||||
&fundingPoint, markedOpen, &confChannel.shortChanID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error setting channel state to markedOpen: %v",
|
||||
err)
|
||||
@ -2044,7 +2055,8 @@ func (f *fundingManager) handleFundingConfirmation(
|
||||
|
||||
// Now that the channel has been fully confirmed and we successfully
|
||||
// saved the opening state, we'll mark it as open within the database.
|
||||
if err := completeChan.MarkAsOpen(shortChanID); err != nil {
|
||||
err = completeChan.MarkAsOpen(confChannel.shortChanID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error setting channel pending flag to false: "+
|
||||
"%v", err)
|
||||
}
|
||||
|
@ -4947,7 +4947,7 @@ func (lc *LightningChannel) getSignedCommitTx() (*wire.MsgTx, error) {
|
||||
// Fetch the current commitment transaction, along with their signature
|
||||
// for the transaction.
|
||||
localCommit := lc.channelState.LocalCommitment
|
||||
commitTx := localCommit.CommitTx
|
||||
commitTx := localCommit.CommitTx.Copy()
|
||||
theirSig := append(localCommit.CommitSig, byte(txscript.SigHashAll))
|
||||
|
||||
// With this, we then generate the full witness so the caller can
|
||||
|
205
lnwallet/chanvalidate/validate.go
Normal file
205
lnwallet/chanvalidate/validate.go
Normal file
@ -0,0 +1,205 @@
|
||||
package chanvalidate
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInvalidOutPoint is returned when the ChanLocator is unable to
|
||||
// find the target outpoint.
|
||||
ErrInvalidOutPoint = fmt.Errorf("output meant to create channel cannot " +
|
||||
"be found")
|
||||
|
||||
// ErrWrongPkScript is returned when the alleged funding transaction is
|
||||
// found to have an incorrect pkSript.
|
||||
ErrWrongPkScript = fmt.Errorf("wrong pk script")
|
||||
|
||||
// ErrInvalidSize is returned when the alleged funding transaction
|
||||
// output has the wrong size (channel capacity).
|
||||
ErrInvalidSize = fmt.Errorf("channel has wrong size")
|
||||
)
|
||||
|
||||
// ErrScriptValidateError is returned when Script VM validation fails for an
|
||||
// alleged channel output.
|
||||
type ErrScriptValidateError struct {
|
||||
err error
|
||||
}
|
||||
|
||||
// Error returns a human readable string describing the error.
|
||||
func (e *ErrScriptValidateError) Error() string {
|
||||
return fmt.Sprintf("script validation failed: %v", e.err)
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying wrapped VM execution failure error.
|
||||
func (e *ErrScriptValidateError) Unwrap() error {
|
||||
return e.err
|
||||
}
|
||||
|
||||
// ChanLocator abstracts away obtaining the output that created the channel, as
|
||||
// well as validating its existence given the funding transaction. We need
|
||||
// this as there are several ways (outpoint, short chan ID) to identify the
|
||||
// output of a channel given the funding transaction.
|
||||
type ChanLocator interface {
|
||||
// Locate attempts to locate the funding output within the funding
|
||||
// transaction. It also returns the final out point of the channel
|
||||
// which uniquely identifies the output which creates the channel. If
|
||||
// the target output cannot be found, or cannot exist on the funding
|
||||
// transaction, then an error is to be returned.
|
||||
Locate(*wire.MsgTx) (*wire.TxOut, *wire.OutPoint, error)
|
||||
}
|
||||
|
||||
// OutPointChanLocator is an implementation of the ChanLocator that can be used
|
||||
// when one already knows the expected chan point.
|
||||
type OutPointChanLocator struct {
|
||||
// ChanPoint is the expected chan point.
|
||||
ChanPoint wire.OutPoint
|
||||
}
|
||||
|
||||
// Locate attempts to locate the funding output within the passed funding
|
||||
// transaction.
|
||||
//
|
||||
// NOTE: Part of the ChanLocator interface.
|
||||
func (o *OutPointChanLocator) Locate(fundingTx *wire.MsgTx) (
|
||||
*wire.TxOut, *wire.OutPoint, error) {
|
||||
|
||||
// If the expected index is greater than the amount of output in the
|
||||
// transaction, then we'll reject this channel as it's invalid.
|
||||
if int(o.ChanPoint.Index) >= len(fundingTx.TxOut) {
|
||||
return nil, nil, ErrInvalidOutPoint
|
||||
}
|
||||
|
||||
// As an extra sanity check, we'll also ensure the txid hash matches.
|
||||
fundingHash := fundingTx.TxHash()
|
||||
if !bytes.Equal(fundingHash[:], o.ChanPoint.Hash[:]) {
|
||||
return nil, nil, ErrInvalidOutPoint
|
||||
}
|
||||
|
||||
return fundingTx.TxOut[o.ChanPoint.Index], &o.ChanPoint, nil
|
||||
}
|
||||
|
||||
// ShortChanIDChanLocator is an implementation of the ChanLocator that can be
|
||||
// used when one only knows the short channel ID of a channel. This should be
|
||||
// used in contexts when one is verifying a 3rd party channel.
|
||||
type ShortChanIDChanLocator struct {
|
||||
// ID is the short channel ID of the target channel.
|
||||
ID lnwire.ShortChannelID
|
||||
}
|
||||
|
||||
// Locate attempts to locate the funding output within the passed funding
|
||||
// transaction.
|
||||
//
|
||||
// NOTE: Part of the ChanLocator interface.
|
||||
func (s *ShortChanIDChanLocator) Locate(fundingTx *wire.MsgTx) (
|
||||
*wire.TxOut, *wire.OutPoint, error) {
|
||||
|
||||
// If the expected index is greater than the amount of output in the
|
||||
// transaction, then we'll reject this channel as it's invalid.
|
||||
outputIndex := s.ID.TxPosition
|
||||
if int(outputIndex) >= len(fundingTx.TxOut) {
|
||||
return nil, nil, ErrInvalidOutPoint
|
||||
}
|
||||
|
||||
chanPoint := wire.OutPoint{
|
||||
Hash: fundingTx.TxHash(),
|
||||
Index: uint32(outputIndex),
|
||||
}
|
||||
|
||||
return fundingTx.TxOut[outputIndex], &chanPoint, nil
|
||||
}
|
||||
|
||||
// CommitmentContext is optional validation context that can be passed into the
|
||||
// main Validate for self-owned channel. The information in this context allows
|
||||
// us to fully verify out initial commitment spend based on the on-chain state
|
||||
// of the funding output.
|
||||
type CommitmentContext struct {
|
||||
// Value is the known size of the channel.
|
||||
Value btcutil.Amount
|
||||
|
||||
// FullySignedCommitTx is the fully signed commitment transaction. This
|
||||
// should include a valid witness.
|
||||
FullySignedCommitTx *wire.MsgTx
|
||||
}
|
||||
|
||||
// Context is the main validation contxet. For a given channel, all fields but
|
||||
// the optional CommitCtx should be populated based on existing
|
||||
// known-to-be-valid parameters.
|
||||
type Context struct {
|
||||
// Locator is a concrete implementation of the ChanLocator interface.
|
||||
Locator ChanLocator
|
||||
|
||||
// MultiSigPkScript is the fully serialized witness script of the
|
||||
// multi-sig output. This is the final witness program that should be
|
||||
// found in the funding output.
|
||||
MultiSigPkScript []byte
|
||||
|
||||
// FundingTx is channel funding transaction as found confirmed in the
|
||||
// chain.
|
||||
FundingTx *wire.MsgTx
|
||||
|
||||
// CommitCtx is an optional additional set of validation context
|
||||
// required to validate a self-owned channel. If present, then a full
|
||||
// Script VM validation will be performed.
|
||||
CommitCtx *CommitmentContext
|
||||
}
|
||||
|
||||
// Validate given the specified context, this function validates that the
|
||||
// alleged channel is well formed, and spendable (if the optional CommitCtx is
|
||||
// specified). If this method returns an error, then the alleged channel is
|
||||
// invalid and should be abandoned immediately.
|
||||
func Validate(ctx *Context) (*wire.OutPoint, error) {
|
||||
// First, we'll attempt to locate the target outpoint in the funding
|
||||
// transaction. If this returns an error, then we know that the
|
||||
// outpoint doesn't actually exist, so we'll exit early.
|
||||
fundingOutput, chanPoint, err := ctx.Locator.Locate(
|
||||
ctx.FundingTx,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// The scripts should match up exactly, otherwise the channel is
|
||||
// invalid.
|
||||
fundingScript := fundingOutput.PkScript
|
||||
if !bytes.Equal(ctx.MultiSigPkScript, fundingScript) {
|
||||
return nil, ErrWrongPkScript
|
||||
}
|
||||
|
||||
// If there's no commitment context, then we're done here as this is a
|
||||
// 3rd party channel.
|
||||
if ctx.CommitCtx == nil {
|
||||
return chanPoint, nil
|
||||
}
|
||||
|
||||
// Now that we know this is our channel, we'll verify the amount of the
|
||||
// created output against our expected size of the channel.
|
||||
fundingValue := fundingOutput.Value
|
||||
if btcutil.Amount(fundingValue) != ctx.CommitCtx.Value {
|
||||
return nil, ErrInvalidSize
|
||||
}
|
||||
|
||||
// If we reach this point, then all other checks have succeeded, so
|
||||
// we'll now attempt a full Script VM execution to ensure that we're
|
||||
// able to close the channel using this initial state.
|
||||
vm, err := txscript.NewEngine(
|
||||
ctx.MultiSigPkScript, ctx.CommitCtx.FullySignedCommitTx,
|
||||
0, txscript.StandardVerifyFlags, nil, nil, fundingValue,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Finally, we'll attempt to verify our full spend, if this fails then
|
||||
// the channel is definitely invalid.
|
||||
err = vm.Execute()
|
||||
if err != nil {
|
||||
return nil, &ErrScriptValidateError{err: err}
|
||||
}
|
||||
|
||||
return chanPoint, nil
|
||||
}
|
307
lnwallet/chanvalidate/validate_test.go
Normal file
307
lnwallet/chanvalidate/validate_test.go
Normal file
@ -0,0 +1,307 @@
|
||||
package chanvalidate
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
)
|
||||
|
||||
var (
|
||||
aliceKey = chainhash.Hash{
|
||||
0xb7, 0x94, 0x38, 0x5f, 0x2d, 0x1e, 0xf7, 0xab,
|
||||
0x4d, 0x92, 0x73, 0xd1, 0x90, 0x63, 0x81, 0xb4,
|
||||
0x4f, 0x2f, 0x6f, 0x25, 0x18, 0xa3, 0xef, 0xb9,
|
||||
0x64, 0x49, 0x18, 0x83, 0x31, 0x98, 0x47, 0x53,
|
||||
}
|
||||
bobKey = chainhash.Hash{
|
||||
0xb7, 0x94, 0x38, 0x5f, 0x2d, 0x1e, 0xf7, 0xab,
|
||||
0x4d, 0x92, 0x73, 0xd1, 0x90, 0x63, 0x81, 0xb4,
|
||||
0x4f, 0x2f, 0x6f, 0x25, 0x98, 0xa3, 0xef, 0xb9,
|
||||
0x69, 0x49, 0x18, 0x83, 0x31, 0x98, 0x47, 0x53,
|
||||
}
|
||||
|
||||
alicePriv, alicePub = btcec.PrivKeyFromBytes(btcec.S256(), aliceKey[:])
|
||||
bobPriv, bobPub = btcec.PrivKeyFromBytes(btcec.S256(), bobKey[:])
|
||||
)
|
||||
|
||||
// channelTestCtx holds shared context that will be used in all tests cases
|
||||
// below.
|
||||
type channelTestCtx struct {
|
||||
fundingTx *wire.MsgTx
|
||||
|
||||
invalidCommitTx, validCommitTx *wire.MsgTx
|
||||
|
||||
chanPoint wire.OutPoint
|
||||
cid lnwire.ShortChannelID
|
||||
|
||||
fundingScript []byte
|
||||
}
|
||||
|
||||
// newChannelTestCtx creates a new channelCtx for use in the validation tests
|
||||
// below. This creates a fake funding transaction, as well as an invalid and
|
||||
// valid commitment transaction.
|
||||
func newChannelTestCtx(chanSize int64) (*channelTestCtx, error) {
|
||||
multiSigScript, err := input.GenMultiSigScript(
|
||||
alicePub.SerializeCompressed(), bobPub.SerializeCompressed(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pkScript, err := input.WitnessScriptHash(multiSigScript)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fundingOutput := wire.TxOut{
|
||||
Value: chanSize,
|
||||
PkScript: pkScript,
|
||||
}
|
||||
|
||||
fundingTx := &wire.MsgTx{
|
||||
TxIn: []*wire.TxIn{
|
||||
{},
|
||||
},
|
||||
TxOut: []*wire.TxOut{
|
||||
&fundingOutput,
|
||||
{
|
||||
Value: 9999,
|
||||
PkScript: bytes.Repeat([]byte{'a'}, 32),
|
||||
},
|
||||
{
|
||||
Value: 99999,
|
||||
PkScript: bytes.Repeat([]byte{'b'}, 32),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fundingTxHash := fundingTx.TxHash()
|
||||
|
||||
commitTx := &wire.MsgTx{
|
||||
TxIn: []*wire.TxIn{
|
||||
{
|
||||
PreviousOutPoint: wire.OutPoint{
|
||||
Hash: fundingTxHash,
|
||||
Index: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
TxOut: []*wire.TxOut{
|
||||
&fundingOutput,
|
||||
},
|
||||
}
|
||||
|
||||
sigHashes := txscript.NewTxSigHashes(commitTx)
|
||||
aliceSig, err := txscript.RawTxInWitnessSignature(
|
||||
commitTx, sigHashes, 0, chanSize,
|
||||
multiSigScript, txscript.SigHashAll, alicePriv,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bobSig, err := txscript.RawTxInWitnessSignature(
|
||||
commitTx, sigHashes, 0, chanSize,
|
||||
multiSigScript, txscript.SigHashAll, bobPriv,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
commitTx.TxIn[0].Witness = input.SpendMultiSig(
|
||||
multiSigScript, alicePub.SerializeCompressed(), aliceSig,
|
||||
bobPub.SerializeCompressed(), bobSig,
|
||||
)
|
||||
|
||||
invalidCommitTx := commitTx.Copy()
|
||||
invalidCommitTx.TxIn[0].PreviousOutPoint.Index = 2
|
||||
|
||||
return &channelTestCtx{
|
||||
fundingTx: fundingTx,
|
||||
validCommitTx: commitTx,
|
||||
invalidCommitTx: invalidCommitTx,
|
||||
chanPoint: wire.OutPoint{
|
||||
Hash: fundingTxHash,
|
||||
Index: 0,
|
||||
},
|
||||
cid: lnwire.ShortChannelID{
|
||||
TxPosition: 0,
|
||||
},
|
||||
fundingScript: pkScript,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TestValidate ensures that the Validate method is able to detect all cases of
|
||||
// invalid channels, and properly accept invalid channels.
|
||||
func TestValidate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
chanSize := int64(1000000)
|
||||
channelCtx, err := newChannelTestCtx(chanSize)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to make channel context: %v", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
// expectedErr is the error we expect, this should be nil if
|
||||
// the channel is valid.
|
||||
expectedErr error
|
||||
|
||||
// locator is how the Validate method should find the target
|
||||
// outpoint.
|
||||
locator ChanLocator
|
||||
|
||||
// chanPoint is the expected final out point.
|
||||
chanPoint wire.OutPoint
|
||||
|
||||
// chanScript is the funding pkScript.
|
||||
chanScript []byte
|
||||
|
||||
// fundingTx is the funding transaction to use in the test.
|
||||
fundingTx *wire.MsgTx
|
||||
|
||||
// commitTx is the commitment transaction to use in the test,
|
||||
// this is optional.
|
||||
commitTx *wire.MsgTx
|
||||
|
||||
// expectedValue is the value of the funding transaction we
|
||||
// should expect. This is only required if commitTx is non-nil.
|
||||
expectedValue int64
|
||||
}{
|
||||
// Short chan ID channel locator, unable to find target
|
||||
// outpoint.
|
||||
{
|
||||
expectedErr: ErrInvalidOutPoint,
|
||||
locator: &ShortChanIDChanLocator{
|
||||
ID: lnwire.NewShortChanIDFromInt(9),
|
||||
},
|
||||
fundingTx: &wire.MsgTx{},
|
||||
},
|
||||
|
||||
// Chan point based channel locator, unable to find target
|
||||
// outpoint.
|
||||
{
|
||||
expectedErr: ErrInvalidOutPoint,
|
||||
locator: &OutPointChanLocator{
|
||||
ChanPoint: wire.OutPoint{
|
||||
Index: 99,
|
||||
},
|
||||
},
|
||||
fundingTx: &wire.MsgTx{},
|
||||
},
|
||||
|
||||
// Invalid pkScript match on mined funding transaction, chan
|
||||
// point based locator.
|
||||
{
|
||||
expectedErr: ErrWrongPkScript,
|
||||
locator: &OutPointChanLocator{
|
||||
ChanPoint: channelCtx.chanPoint,
|
||||
},
|
||||
chanScript: bytes.Repeat([]byte("a"), 32),
|
||||
fundingTx: channelCtx.fundingTx,
|
||||
},
|
||||
|
||||
// Invalid pkScript match on mined funding transaction, short
|
||||
// chan ID based locator.
|
||||
{
|
||||
expectedErr: ErrWrongPkScript,
|
||||
locator: &ShortChanIDChanLocator{
|
||||
ID: channelCtx.cid,
|
||||
},
|
||||
chanScript: bytes.Repeat([]byte("a"), 32),
|
||||
fundingTx: channelCtx.fundingTx,
|
||||
},
|
||||
|
||||
// Invalid amount on funding transaction.
|
||||
{
|
||||
expectedErr: ErrInvalidSize,
|
||||
locator: &OutPointChanLocator{
|
||||
ChanPoint: channelCtx.chanPoint,
|
||||
},
|
||||
chanScript: channelCtx.fundingScript,
|
||||
fundingTx: channelCtx.fundingTx,
|
||||
expectedValue: 555,
|
||||
commitTx: channelCtx.validCommitTx,
|
||||
},
|
||||
|
||||
// Validation failure on final commitment transaction
|
||||
{
|
||||
expectedErr: &ErrScriptValidateError{},
|
||||
locator: &OutPointChanLocator{
|
||||
ChanPoint: channelCtx.chanPoint,
|
||||
},
|
||||
chanScript: channelCtx.fundingScript,
|
||||
fundingTx: channelCtx.fundingTx,
|
||||
expectedValue: chanSize,
|
||||
commitTx: channelCtx.invalidCommitTx,
|
||||
},
|
||||
|
||||
// Fully valid 3rd party verification.
|
||||
{
|
||||
expectedErr: nil,
|
||||
locator: &OutPointChanLocator{
|
||||
ChanPoint: channelCtx.chanPoint,
|
||||
},
|
||||
chanScript: channelCtx.fundingScript,
|
||||
fundingTx: channelCtx.fundingTx,
|
||||
chanPoint: channelCtx.chanPoint,
|
||||
},
|
||||
|
||||
// Fully valid self-channel verification.
|
||||
{
|
||||
expectedErr: nil,
|
||||
locator: &OutPointChanLocator{
|
||||
ChanPoint: channelCtx.chanPoint,
|
||||
},
|
||||
chanScript: channelCtx.fundingScript,
|
||||
fundingTx: channelCtx.fundingTx,
|
||||
expectedValue: chanSize,
|
||||
commitTx: channelCtx.validCommitTx,
|
||||
chanPoint: channelCtx.chanPoint,
|
||||
},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
ctx := &Context{
|
||||
Locator: testCase.locator,
|
||||
MultiSigPkScript: testCase.chanScript,
|
||||
FundingTx: testCase.fundingTx,
|
||||
}
|
||||
|
||||
if testCase.commitTx != nil {
|
||||
ctx.CommitCtx = &CommitmentContext{
|
||||
Value: btcutil.Amount(
|
||||
testCase.expectedValue,
|
||||
),
|
||||
FullySignedCommitTx: testCase.commitTx,
|
||||
}
|
||||
}
|
||||
|
||||
chanPoint, err := Validate(ctx)
|
||||
if err != testCase.expectedErr {
|
||||
_, ok := testCase.expectedErr.(*ErrScriptValidateError)
|
||||
_, scriptErr := err.(*ErrScriptValidateError)
|
||||
if ok && scriptErr {
|
||||
continue
|
||||
}
|
||||
|
||||
t.Fatalf("test #%v: validation failed: expected %v, "+
|
||||
"got %v", i, testCase.expectedErr, err)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if *chanPoint != testCase.chanPoint {
|
||||
t.Fatalf("test #%v: wrong outpoint: want %v, got %v",
|
||||
i, testCase.chanPoint, chanPoint)
|
||||
}
|
||||
}
|
||||
}
|
@ -21,6 +21,7 @@ import (
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chanvalidate"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/shachain"
|
||||
)
|
||||
@ -1636,3 +1637,58 @@ func coinSelectSubtractFees(feeRate SatPerKWeight, amt,
|
||||
|
||||
return selectedUtxos, outputAmt, changeAmt, nil
|
||||
}
|
||||
|
||||
// ValidateChannel will attempt to fully validate a newly mined channel, given
|
||||
// its funding transaction and existing channel state. If this method returns
|
||||
// an error, then the mined channel is invalid, and shouldn't be used.
|
||||
func (l *LightningWallet) ValidateChannel(channelState *channeldb.OpenChannel,
|
||||
fundingTx *wire.MsgTx) error {
|
||||
|
||||
// First, we'll obtain a fully signed commitment transaction so we can
|
||||
// pass into it on the chanvalidate package for verification.
|
||||
channel, err := NewLightningChannel(l.Cfg.Signer, channelState, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
signedCommitTx, err := channel.getSignedCommitTx()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We'll also need the multi-sig witness script itself so the
|
||||
// chanvalidate package can check it for correctness against the
|
||||
// funding transaction, and also commitment validity.
|
||||
localKey := channelState.LocalChanCfg.MultiSigKey.PubKey
|
||||
remoteKey := channelState.RemoteChanCfg.MultiSigKey.PubKey
|
||||
witnessScript, err := input.GenMultiSigScript(
|
||||
localKey.SerializeCompressed(),
|
||||
remoteKey.SerializeCompressed(),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pkScript, err := input.WitnessScriptHash(witnessScript)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Finally, we'll pass in all the necessary context needed to fully
|
||||
// validate that this channel is indeed what we expect, and can be
|
||||
// used.
|
||||
_, err = chanvalidate.Validate(&chanvalidate.Context{
|
||||
Locator: &chanvalidate.OutPointChanLocator{
|
||||
ChanPoint: channelState.FundingOutpoint,
|
||||
},
|
||||
MultiSigPkScript: pkScript,
|
||||
FundingTx: fundingTx,
|
||||
CommitCtx: &chanvalidate.CommitmentContext{
|
||||
Value: channel.Capacity,
|
||||
FullySignedCommitTx: signedCommitTx,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chanvalidate"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/multimutex"
|
||||
"github.com/lightningnetwork/lnd/routing/chainview"
|
||||
@ -1174,9 +1175,9 @@ func (r *ChannelRouter) processUpdate(msg interface{}) error {
|
||||
// to obtain the full funding outpoint that's encoded within
|
||||
// the channel ID.
|
||||
channelID := lnwire.NewShortChanIDFromInt(msg.ChannelID)
|
||||
fundingPoint, _, err := r.fetchChanPoint(&channelID)
|
||||
fundingTx, err := r.fetchFundingTx(&channelID)
|
||||
if err != nil {
|
||||
return errors.Errorf("unable to fetch chan point for "+
|
||||
return errors.Errorf("unable to fetch funding tx for "+
|
||||
"chan_id=%v: %v", msg.ChannelID, err)
|
||||
}
|
||||
|
||||
@ -1189,7 +1190,22 @@ func (r *ChannelRouter) processUpdate(msg interface{}) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fundingPkScript, err := input.WitnessScriptHash(witnessScript)
|
||||
pkScript, err := input.WitnessScriptHash(witnessScript)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Next we'll validate that this channel is actually well
|
||||
// formed. If this check fails, then this channel either
|
||||
// doesn't exist, or isn't the one that was meant to be created
|
||||
// according to the passed channel proofs.
|
||||
fundingPoint, err := chanvalidate.Validate(&chanvalidate.Context{
|
||||
Locator: &chanvalidate.ShortChanIDChanLocator{
|
||||
ID: channelID,
|
||||
},
|
||||
MultiSigPkScript: pkScript,
|
||||
FundingTx: fundingTx,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -1197,6 +1213,10 @@ func (r *ChannelRouter) processUpdate(msg interface{}) error {
|
||||
// Now that we have the funding outpoint of the channel, ensure
|
||||
// that it hasn't yet been spent. If so, then this channel has
|
||||
// been closed so we'll ignore it.
|
||||
fundingPkScript, err := input.WitnessScriptHash(witnessScript)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
chanUtxo, err := r.cfg.Chain.GetUtxo(
|
||||
fundingPoint, fundingPkScript, channelID.BlockHeight,
|
||||
r.quit,
|
||||
@ -1207,16 +1227,6 @@ func (r *ChannelRouter) processUpdate(msg interface{}) error {
|
||||
msg.ChannelID, fundingPoint, err)
|
||||
}
|
||||
|
||||
// By checking the equality of witness pkscripts we checks that
|
||||
// funding witness script is multisignature lock which contains
|
||||
// both local and remote public keys which was declared in
|
||||
// channel edge and also that the announced channel value is
|
||||
// right.
|
||||
if !bytes.Equal(fundingPkScript, chanUtxo.PkScript) {
|
||||
return errors.Errorf("pkScript mismatch: expected %x, "+
|
||||
"got %x", fundingPkScript, chanUtxo.PkScript)
|
||||
}
|
||||
|
||||
// TODO(roasbeef): this is a hack, needs to be removed
|
||||
// after commitment fees are dynamic.
|
||||
msg.Capacity = btcutil.Amount(chanUtxo.Value)
|
||||
@ -1339,25 +1349,24 @@ func (r *ChannelRouter) processUpdate(msg interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// fetchChanPoint retrieves the original outpoint which is encoded within the
|
||||
// channelID. This method also return the public key script for the target
|
||||
// transaction.
|
||||
// fetchFundingTx returns the funding transaction identified by the passed
|
||||
// short channel ID.
|
||||
//
|
||||
// TODO(roasbeef): replace with call to GetBlockTransaction? (would allow to
|
||||
// later use getblocktxn)
|
||||
func (r *ChannelRouter) fetchChanPoint(
|
||||
chanID *lnwire.ShortChannelID) (*wire.OutPoint, *wire.TxOut, error) {
|
||||
func (r *ChannelRouter) fetchFundingTx(
|
||||
chanID *lnwire.ShortChannelID) (*wire.MsgTx, error) {
|
||||
|
||||
// First fetch the block hash by the block number encoded, then use
|
||||
// that hash to fetch the block itself.
|
||||
blockNum := int64(chanID.BlockHeight)
|
||||
blockHash, err := r.cfg.Chain.GetBlockHash(blockNum)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
fundingBlock, err := r.cfg.Chain.GetBlock(blockHash)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// As a sanity check, ensure that the advertised transaction index is
|
||||
@ -1365,21 +1374,12 @@ func (r *ChannelRouter) fetchChanPoint(
|
||||
// block.
|
||||
numTxns := uint32(len(fundingBlock.Transactions))
|
||||
if chanID.TxIndex > numTxns-1 {
|
||||
return nil, nil, fmt.Errorf("tx_index=#%v is out of range "+
|
||||
"(max_index=%v), network_chan_id=%v\n", chanID.TxIndex,
|
||||
numTxns-1, spew.Sdump(chanID))
|
||||
return nil, fmt.Errorf("tx_index=#%v is out of range "+
|
||||
"(max_index=%v), network_chan_id=%v", chanID.TxIndex,
|
||||
numTxns-1, chanID)
|
||||
}
|
||||
|
||||
// Finally once we have the block itself, we seek to the targeted
|
||||
// transaction index to obtain the funding output and txout.
|
||||
fundingTx := fundingBlock.Transactions[chanID.TxIndex]
|
||||
outPoint := &wire.OutPoint{
|
||||
Hash: fundingTx.TxHash(),
|
||||
Index: uint32(chanID.TxPosition),
|
||||
}
|
||||
txOut := fundingTx.TxOut[chanID.TxPosition]
|
||||
|
||||
return outPoint, txOut, nil
|
||||
return fundingBlock.Transactions[chanID.TxIndex], nil
|
||||
}
|
||||
|
||||
// routingMsg couples a routing related routing topology update to the
|
||||
|
Loading…
Reference in New Issue
Block a user