mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-19 09:53:54 +01:00
dbc19b24e5
In this commit, an assertion is added to the bolt 3 commitment tx tests that ensures that the local and remote balances add up to the expected funding amount. Adding this assertion uncovered a borked test vector which is also fixed in this commit.
1001 lines
30 KiB
Go
1001 lines
30 KiB
Go
package lnwallet
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net"
|
|
"sort"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/btcec/v2"
|
|
"github.com/btcsuite/btcd/btcutil"
|
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
|
"github.com/btcsuite/btcd/txscript"
|
|
"github.com/btcsuite/btcd/wire"
|
|
"github.com/lightningnetwork/lnd/channeldb"
|
|
"github.com/lightningnetwork/lnd/input"
|
|
"github.com/lightningnetwork/lnd/keychain"
|
|
"github.com/lightningnetwork/lnd/lntypes"
|
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
|
"github.com/lightningnetwork/lnd/lnwire"
|
|
"github.com/lightningnetwork/lnd/shachain"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
/**
|
|
* This file implements that different types of transactions used in the
|
|
* lightning protocol are created correctly. To do so, the tests use the test
|
|
* vectors defined in Appendix B & C of BOLT 03.
|
|
*/
|
|
|
|
// testContext contains the test parameters defined in Appendix B & C of the
|
|
// BOLT 03 spec.
|
|
type testContext struct {
|
|
localFundingPrivkey *btcec.PrivateKey
|
|
localPaymentBasepointSecret *btcec.PrivateKey
|
|
localDelayedPaymentBasepointSecret *btcec.PrivateKey
|
|
remoteFundingPrivkey *btcec.PrivateKey
|
|
remoteRevocationBasepointSecret *btcec.PrivateKey
|
|
remotePaymentBasepointSecret *btcec.PrivateKey
|
|
|
|
localPerCommitSecret lntypes.Hash
|
|
|
|
fundingTx *btcutil.Tx
|
|
|
|
localCsvDelay uint16
|
|
fundingAmount btcutil.Amount
|
|
dustLimit btcutil.Amount
|
|
commitHeight uint64
|
|
|
|
t *testing.T
|
|
}
|
|
|
|
// newTestContext populates a new testContext struct with the constant
|
|
// parameters defined in the BOLT 03 spec.
|
|
func newTestContext(t *testing.T) *testContext {
|
|
tc := new(testContext)
|
|
|
|
priv := func(v string) *btcec.PrivateKey {
|
|
k, err := privkeyFromHex(v)
|
|
require.NoError(t, err)
|
|
|
|
return k
|
|
}
|
|
|
|
tc.remoteFundingPrivkey = priv("1552dfba4f6cf29a62a0af13c8d6981d36d0ef8d61ba10fb0fe90da7634d7e13")
|
|
tc.remoteRevocationBasepointSecret = priv("2222222222222222222222222222222222222222222222222222222222222222")
|
|
tc.remotePaymentBasepointSecret = priv("4444444444444444444444444444444444444444444444444444444444444444")
|
|
tc.localPaymentBasepointSecret = priv("1111111111111111111111111111111111111111111111111111111111111111")
|
|
tc.localDelayedPaymentBasepointSecret = priv("3333333333333333333333333333333333333333333333333333333333333333")
|
|
tc.localFundingPrivkey = priv("30ff4956bbdd3222d44cc5e8a1261dab1e07957bdac5ae88fe3261ef321f3749")
|
|
|
|
var err error
|
|
tc.localPerCommitSecret, err = lntypes.MakeHashFromStr("1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100")
|
|
require.NoError(t, err)
|
|
|
|
const fundingTxHex = "0200000001adbb20ea41a8423ea937e76e8151636bf6093b70eaff942930d20576600521fd000000006b48304502210090587b6201e166ad6af0227d3036a9454223d49a1f11839c1a362184340ef0240220577f7cd5cca78719405cbf1de7414ac027f0239ef6e214c90fcaab0454d84b3b012103535b32d5eb0a6ed0982a0479bbadc9868d9836f6ba94dd5a63be16d875069184ffffffff028096980000000000220020c015c4a6be010e21657068fc2e6a9d02b27ebe4d490a25846f7237f104d1a3cd20256d29010000001600143ca33c2e4446f4a305f23c80df8ad1afdcf652f900000000"
|
|
tc.fundingTx, err = txFromHex(fundingTxHex)
|
|
require.NoError(t, err)
|
|
|
|
tc.localCsvDelay = 144
|
|
tc.fundingAmount = 10000000
|
|
tc.dustLimit = 546
|
|
tc.commitHeight = 42
|
|
tc.t = t
|
|
|
|
return tc
|
|
}
|
|
|
|
var testHtlcs = []struct {
|
|
incoming bool
|
|
amount lnwire.MilliSatoshi
|
|
expiry uint32
|
|
preimage string
|
|
}{
|
|
{
|
|
incoming: true,
|
|
amount: 1000000,
|
|
expiry: 500,
|
|
preimage: "0000000000000000000000000000000000000000000000000000000000000000",
|
|
},
|
|
{
|
|
incoming: true,
|
|
amount: 2000000,
|
|
expiry: 501,
|
|
preimage: "0101010101010101010101010101010101010101010101010101010101010101",
|
|
},
|
|
{
|
|
incoming: false,
|
|
amount: 2000000,
|
|
expiry: 502,
|
|
preimage: "0202020202020202020202020202020202020202020202020202020202020202",
|
|
},
|
|
{
|
|
incoming: false,
|
|
amount: 3000000,
|
|
expiry: 503,
|
|
preimage: "0303030303030303030303030303030303030303030303030303030303030303",
|
|
},
|
|
{
|
|
incoming: true,
|
|
amount: 4000000,
|
|
expiry: 504,
|
|
preimage: "0404040404040404040404040404040404040404040404040404040404040404",
|
|
},
|
|
}
|
|
|
|
// htlcDesc is a description used to construct each HTLC in each test case.
|
|
type htlcDesc struct {
|
|
RemoteSigHex string
|
|
ResolutionTxHex string
|
|
}
|
|
|
|
type testCase struct {
|
|
Name string
|
|
LocalBalance lnwire.MilliSatoshi
|
|
RemoteBalance lnwire.MilliSatoshi
|
|
FeePerKw btcutil.Amount
|
|
|
|
// UseTestHtlcs defined whether the fixed set of test htlc should be
|
|
// added to the channel before checking the commitment assertions.
|
|
UseTestHtlcs bool
|
|
|
|
HtlcDescs []htlcDesc
|
|
ExpectedCommitmentTxHex string
|
|
RemoteSigHex string
|
|
}
|
|
|
|
// TestCommitmentAndHTLCTransactions checks the test vectors specified in
|
|
// BOLT 03, Appendix C. This deterministically generates commitment and second
|
|
// level HTLC transactions and checks that they match the expected values.
|
|
func TestCommitmentAndHTLCTransactions(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
vectorSets := []struct {
|
|
name string
|
|
jsonFile string
|
|
chanType channeldb.ChannelType
|
|
}{
|
|
{
|
|
name: "legacy",
|
|
chanType: channeldb.SingleFunderBit,
|
|
jsonFile: "test_vectors_legacy.json",
|
|
},
|
|
{
|
|
name: "anchors",
|
|
chanType: channeldb.SingleFunderTweaklessBit |
|
|
channeldb.AnchorOutputsBit,
|
|
jsonFile: "test_vectors_anchors.json",
|
|
},
|
|
}
|
|
|
|
for _, set := range vectorSets {
|
|
set := set
|
|
|
|
var testCases []testCase
|
|
|
|
jsonText, err := ioutil.ReadFile(set.jsonFile)
|
|
require.NoError(t, err)
|
|
|
|
err = json.Unmarshal(jsonText, &testCases)
|
|
require.NoError(t, err)
|
|
|
|
for _, test := range testCases {
|
|
test := test
|
|
name := fmt.Sprintf("%s-%s", set.name, test.Name)
|
|
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run(test.Name, func(t *testing.T) {
|
|
testVectors(t, set.chanType, test)
|
|
})
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// addTestHtlcs adds the test vector htlcs to the update logs of the local and
|
|
// remote node.
|
|
func addTestHtlcs(t *testing.T, remote,
|
|
local *LightningChannel) map[[20]byte]lntypes.Preimage {
|
|
|
|
hash160map := make(map[[20]byte]lntypes.Preimage)
|
|
for _, htlc := range testHtlcs {
|
|
preimage, err := lntypes.MakePreimageFromStr(htlc.preimage)
|
|
require.NoError(t, err)
|
|
|
|
hash := preimage.Hash()
|
|
|
|
// Store ripemd160 hash of the payment hash to later identify
|
|
// resolutions.
|
|
var hash160 [20]byte
|
|
copy(hash160[:], input.Ripemd160H(hash[:]))
|
|
hash160map[hash160] = preimage
|
|
|
|
// Add htlc to the channel.
|
|
chanID := lnwire.NewChanIDFromOutPoint(remote.ChanPoint)
|
|
|
|
msg := &lnwire.UpdateAddHTLC{
|
|
Amount: htlc.amount,
|
|
ChanID: chanID,
|
|
Expiry: htlc.expiry,
|
|
PaymentHash: hash,
|
|
}
|
|
if htlc.incoming {
|
|
htlcID, err := remote.AddHTLC(msg, nil)
|
|
require.NoError(t, err, "unable to add htlc")
|
|
|
|
msg.ID = htlcID
|
|
_, err = local.ReceiveHTLC(msg)
|
|
require.NoError(t, err, "unable to recv htlc")
|
|
} else {
|
|
htlcID, err := local.AddHTLC(msg, nil)
|
|
require.NoError(t, err, "unable to add htlc")
|
|
|
|
msg.ID = htlcID
|
|
_, err = remote.ReceiveHTLC(msg)
|
|
require.NoError(t, err, "unable to recv htlc")
|
|
}
|
|
}
|
|
|
|
return hash160map
|
|
}
|
|
|
|
// testVectors executes a commit dance to end up with the commitment transaction
|
|
// that is described in the test vectors and then asserts that all values are
|
|
// correct.
|
|
func testVectors(t *testing.T, chanType channeldb.ChannelType, test testCase) {
|
|
tc := newTestContext(t)
|
|
|
|
// Balances in the test vectors are before subtraction of in-flight
|
|
// htlcs. Convert to spendable balances.
|
|
remoteBalance := test.RemoteBalance
|
|
localBalance := test.LocalBalance
|
|
|
|
if test.UseTestHtlcs {
|
|
for _, htlc := range testHtlcs {
|
|
if htlc.incoming {
|
|
remoteBalance += htlc.amount
|
|
} else {
|
|
localBalance += htlc.amount
|
|
}
|
|
}
|
|
}
|
|
|
|
// Assert that the remote and local balances add up to the channel
|
|
// capacity.
|
|
require.EqualValues(t, lnwire.NewMSatFromSatoshis(tc.fundingAmount),
|
|
remoteBalance+localBalance)
|
|
|
|
// Set up a test channel on which the test commitment transaction is
|
|
// going to be produced.
|
|
remoteChannel, localChannel := createTestChannelsForVectors(
|
|
tc,
|
|
chanType, test.FeePerKw,
|
|
remoteBalance.ToSatoshis(),
|
|
localBalance.ToSatoshis(),
|
|
)
|
|
|
|
// Add htlcs (if any) to the update logs of both sides and save a hash
|
|
// map that allows us to identify the htlcs in the scripts later on and
|
|
// retrieve the corresponding preimage.
|
|
var hash160map map[[20]byte]lntypes.Preimage
|
|
if test.UseTestHtlcs {
|
|
hash160map = addTestHtlcs(t, remoteChannel, localChannel)
|
|
}
|
|
|
|
// Execute commit dance to arrive at the point where the local node has
|
|
// received the test commitment and the remote signature.
|
|
localSig, localHtlcSigs, _, err := localChannel.SignNextCommitment()
|
|
require.NoError(t, err, "local unable to sign commitment")
|
|
|
|
err = remoteChannel.ReceiveNewCommitment(localSig, localHtlcSigs)
|
|
require.NoError(t, err)
|
|
|
|
revMsg, _, _, err := remoteChannel.RevokeCurrentCommitment()
|
|
require.NoError(t, err)
|
|
|
|
_, _, _, _, err = localChannel.ReceiveRevocation(revMsg)
|
|
require.NoError(t, err)
|
|
|
|
remoteSig, remoteHtlcSigs, _, err := remoteChannel.SignNextCommitment()
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, test.RemoteSigHex,
|
|
hex.EncodeToString(remoteSig.ToSignatureBytes()))
|
|
|
|
for i, sig := range remoteHtlcSigs {
|
|
require.Equal(t, test.HtlcDescs[i].RemoteSigHex,
|
|
hex.EncodeToString(sig.ToSignatureBytes()))
|
|
}
|
|
|
|
err = localChannel.ReceiveNewCommitment(remoteSig, remoteHtlcSigs)
|
|
require.NoError(t, err)
|
|
|
|
_, _, _, err = localChannel.RevokeCurrentCommitment()
|
|
require.NoError(t, err)
|
|
|
|
// Now the local node force closes the channel so that we can inspect
|
|
// its state.
|
|
forceCloseSum, err := localChannel.ForceClose()
|
|
require.NoError(t, err)
|
|
|
|
// Assert that the commitment transaction itself is as expected.
|
|
var txBytes bytes.Buffer
|
|
require.NoError(t, forceCloseSum.CloseTx.Serialize(&txBytes))
|
|
|
|
require.Equal(t, test.ExpectedCommitmentTxHex,
|
|
hex.EncodeToString(txBytes.Bytes()))
|
|
|
|
// Obtain the second level transactions that the local node's channel
|
|
// state machine has produced. Store them in a map indexed by commit tx
|
|
// output index. Also complete the second level transaction with the
|
|
// preimage. This is normally done later in the contract resolver.
|
|
secondLevelTxes := map[uint32]*wire.MsgTx{}
|
|
storeTx := func(index uint32, tx *wire.MsgTx) {
|
|
// Prevent overwrites.
|
|
_, exists := secondLevelTxes[index]
|
|
require.False(t, exists)
|
|
|
|
secondLevelTxes[index] = tx
|
|
}
|
|
|
|
for _, r := range forceCloseSum.HtlcResolutions.IncomingHTLCs {
|
|
successTx := r.SignedSuccessTx
|
|
witnessScript := successTx.TxIn[0].Witness[4]
|
|
var hash160 [20]byte
|
|
copy(hash160[:], witnessScript[69:69+20])
|
|
preimage := hash160map[hash160]
|
|
successTx.TxIn[0].Witness[3] = preimage[:]
|
|
storeTx(r.HtlcPoint().Index, successTx)
|
|
}
|
|
for _, r := range forceCloseSum.HtlcResolutions.OutgoingHTLCs {
|
|
storeTx(r.HtlcPoint().Index, r.SignedTimeoutTx)
|
|
}
|
|
|
|
// Create a list of second level transactions ordered by commit tx
|
|
// output index.
|
|
var keys []uint32
|
|
for k := range secondLevelTxes {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Slice(keys, func(a, b int) bool {
|
|
return keys[a] < keys[b]
|
|
})
|
|
|
|
// Assert that this list matches the test vectors.
|
|
for i, idx := range keys {
|
|
tx := secondLevelTxes[idx]
|
|
var b bytes.Buffer
|
|
err := tx.Serialize(&b)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(
|
|
t,
|
|
test.HtlcDescs[i].ResolutionTxHex,
|
|
hex.EncodeToString(b.Bytes()),
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestCommitTxStateHint(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
stateHintTests := []struct {
|
|
name string
|
|
from uint64
|
|
to uint64
|
|
inputs int
|
|
shouldFail bool
|
|
}{
|
|
{
|
|
name: "states 0 to 1000",
|
|
from: 0,
|
|
to: 1000,
|
|
inputs: 1,
|
|
shouldFail: false,
|
|
},
|
|
{
|
|
name: "states 'maxStateHint-1000' to 'maxStateHint'",
|
|
from: maxStateHint - 1000,
|
|
to: maxStateHint,
|
|
inputs: 1,
|
|
shouldFail: false,
|
|
},
|
|
{
|
|
name: "state 'maxStateHint+1'",
|
|
from: maxStateHint + 1,
|
|
to: maxStateHint + 10,
|
|
inputs: 1,
|
|
shouldFail: true,
|
|
},
|
|
{
|
|
name: "commit transaction with two inputs",
|
|
inputs: 2,
|
|
shouldFail: true,
|
|
},
|
|
}
|
|
|
|
var obfuscator [StateHintSize]byte
|
|
copy(obfuscator[:], testHdSeed[:StateHintSize])
|
|
timeYesterday := uint32(time.Now().Unix() - 24*60*60)
|
|
|
|
for _, test := range stateHintTests {
|
|
commitTx := wire.NewMsgTx(2)
|
|
|
|
// Add supplied number of inputs to the commitment transaction.
|
|
for i := 0; i < test.inputs; i++ {
|
|
commitTx.AddTxIn(&wire.TxIn{})
|
|
}
|
|
|
|
for i := test.from; i <= test.to; i++ {
|
|
stateNum := uint64(i)
|
|
|
|
err := SetStateNumHint(commitTx, stateNum, obfuscator)
|
|
if err != nil && !test.shouldFail {
|
|
t.Fatalf("unable to set state num %v: %v", i, err)
|
|
} else if err == nil && test.shouldFail {
|
|
t.Fatalf("Failed(%v): test should fail but did not", test.name)
|
|
}
|
|
|
|
locktime := commitTx.LockTime
|
|
sequence := commitTx.TxIn[0].Sequence
|
|
|
|
// Locktime should not be less than 500,000,000 and not larger
|
|
// than the time 24 hours ago. One day should provide a good
|
|
// enough buffer for the tests.
|
|
if locktime < 5e8 || locktime > timeYesterday {
|
|
if !test.shouldFail {
|
|
t.Fatalf("The value of locktime (%v) may cause the commitment "+
|
|
"transaction to be unspendable", locktime)
|
|
}
|
|
}
|
|
|
|
if sequence&wire.SequenceLockTimeDisabled == 0 {
|
|
if !test.shouldFail {
|
|
t.Fatalf("Sequence locktime is NOT disabled when it should be")
|
|
}
|
|
}
|
|
|
|
extractedStateNum := GetStateNumHint(commitTx, obfuscator)
|
|
if extractedStateNum != stateNum && !test.shouldFail {
|
|
t.Fatalf("state number mismatched, expected %v, got %v",
|
|
stateNum, extractedStateNum)
|
|
} else if extractedStateNum == stateNum && test.shouldFail {
|
|
t.Fatalf("Failed(%v): test should fail but did not", test.name)
|
|
}
|
|
}
|
|
t.Logf("Passed: %v", test.name)
|
|
}
|
|
}
|
|
|
|
// testSpendValidation ensures that we're able to spend all outputs in the
|
|
// commitment transaction that we create.
|
|
func testSpendValidation(t *testing.T, tweakless bool) {
|
|
// We generate a fake output, and the corresponding txin. This output
|
|
// doesn't need to exist, as we'll only be validating spending from the
|
|
// transaction that references this.
|
|
txid, err := chainhash.NewHash(testHdSeed.CloneBytes())
|
|
require.NoError(t, err, "unable to create txid")
|
|
fundingOut := &wire.OutPoint{
|
|
Hash: *txid,
|
|
Index: 50,
|
|
}
|
|
fakeFundingTxIn := wire.NewTxIn(fundingOut, nil, nil)
|
|
|
|
const channelBalance = btcutil.Amount(1 * 10e8)
|
|
const csvTimeout = 5
|
|
|
|
// We also set up set some resources for the commitment transaction.
|
|
// Each side currently has 1 BTC within the channel, with a total
|
|
// channel capacity of 2BTC.
|
|
aliceKeyPriv, aliceKeyPub := btcec.PrivKeyFromBytes(
|
|
testWalletPrivKey,
|
|
)
|
|
bobKeyPriv, bobKeyPub := btcec.PrivKeyFromBytes(
|
|
bobsPrivKey,
|
|
)
|
|
|
|
revocationPreimage := testHdSeed.CloneBytes()
|
|
commitSecret, commitPoint := btcec.PrivKeyFromBytes(
|
|
revocationPreimage,
|
|
)
|
|
revokePubKey := input.DeriveRevocationPubkey(bobKeyPub, commitPoint)
|
|
|
|
aliceDelayKey := input.TweakPubKey(aliceKeyPub, commitPoint)
|
|
|
|
// Bob will have the channel "force closed" on him, so for the sake of
|
|
// our commitments, if it's tweakless, his key will just be his regular
|
|
// pubkey.
|
|
bobPayKey := input.TweakPubKey(bobKeyPub, commitPoint)
|
|
channelType := channeldb.SingleFunderBit
|
|
if tweakless {
|
|
bobPayKey = bobKeyPub
|
|
channelType = channeldb.SingleFunderTweaklessBit
|
|
}
|
|
|
|
remoteCommitTweak := input.SingleTweakBytes(commitPoint, aliceKeyPub)
|
|
localCommitTweak := input.SingleTweakBytes(commitPoint, bobKeyPub)
|
|
|
|
aliceSelfOutputSigner := &input.MockSigner{
|
|
Privkeys: []*btcec.PrivateKey{aliceKeyPriv},
|
|
}
|
|
|
|
// Calculate the dust limit we'll use for the test.
|
|
dustLimit := DustLimitForSize(input.UnknownWitnessSize)
|
|
|
|
aliceChanCfg := &channeldb.ChannelConfig{
|
|
ChannelConstraints: channeldb.ChannelConstraints{
|
|
DustLimit: dustLimit,
|
|
CsvDelay: csvTimeout,
|
|
},
|
|
}
|
|
|
|
bobChanCfg := &channeldb.ChannelConfig{
|
|
ChannelConstraints: channeldb.ChannelConstraints{
|
|
DustLimit: dustLimit,
|
|
CsvDelay: csvTimeout,
|
|
},
|
|
}
|
|
|
|
// With all the test data set up, we create the commitment transaction.
|
|
// We only focus on a single party's transactions, as the scripts are
|
|
// identical with the roles reversed.
|
|
//
|
|
// This is Alice's commitment transaction, so she must wait a CSV delay
|
|
// of 5 blocks before sweeping the output, while bob can spend
|
|
// immediately with either the revocation key, or his regular key.
|
|
keyRing := &CommitmentKeyRing{
|
|
ToLocalKey: aliceDelayKey,
|
|
RevocationKey: revokePubKey,
|
|
ToRemoteKey: bobPayKey,
|
|
}
|
|
commitmentTx, err := CreateCommitTx(
|
|
channelType, *fakeFundingTxIn, keyRing, aliceChanCfg,
|
|
bobChanCfg, channelBalance, channelBalance, 0, true, 0,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("unable to create commitment transaction: %v", nil)
|
|
}
|
|
|
|
delayOutput := commitmentTx.TxOut[0]
|
|
regularOutput := commitmentTx.TxOut[1]
|
|
|
|
// We're testing an uncooperative close, output sweep, so construct a
|
|
// transaction which sweeps the funds to a random address.
|
|
targetOutput, err := input.CommitScriptUnencumbered(aliceKeyPub)
|
|
require.NoError(t, err, "unable to create target output")
|
|
sweepTx := wire.NewMsgTx(2)
|
|
sweepTx.AddTxIn(wire.NewTxIn(&wire.OutPoint{
|
|
Hash: commitmentTx.TxHash(),
|
|
Index: 0,
|
|
}, nil, nil))
|
|
sweepTx.AddTxOut(&wire.TxOut{
|
|
PkScript: targetOutput,
|
|
Value: 0.5 * 10e8,
|
|
})
|
|
|
|
// First, we'll test spending with Alice's key after the timeout.
|
|
delayScript, err := input.CommitScriptToSelf(
|
|
csvTimeout, aliceDelayKey, revokePubKey,
|
|
)
|
|
require.NoError(t, err, "unable to generate alice delay script")
|
|
sweepTx.TxIn[0].Sequence = input.LockTimeToSequence(false, csvTimeout)
|
|
signDesc := &input.SignDescriptor{
|
|
WitnessScript: delayScript,
|
|
KeyDesc: keychain.KeyDescriptor{
|
|
PubKey: aliceKeyPub,
|
|
},
|
|
SingleTweak: remoteCommitTweak,
|
|
SigHashes: input.NewTxSigHashesV0Only(sweepTx),
|
|
Output: &wire.TxOut{
|
|
Value: int64(channelBalance),
|
|
},
|
|
HashType: txscript.SigHashAll,
|
|
InputIndex: 0,
|
|
}
|
|
aliceWitnessSpend, err := input.CommitSpendTimeout(
|
|
aliceSelfOutputSigner, signDesc, sweepTx,
|
|
)
|
|
require.NoError(t, err, "unable to generate delay commit spend witness")
|
|
sweepTx.TxIn[0].Witness = aliceWitnessSpend
|
|
vm, err := txscript.NewEngine(
|
|
delayOutput.PkScript, sweepTx, 0, txscript.StandardVerifyFlags,
|
|
nil, nil, int64(channelBalance),
|
|
txscript.NewCannedPrevOutputFetcher(nil, 0),
|
|
)
|
|
require.NoError(t, err, "unable to create engine")
|
|
if err := vm.Execute(); err != nil {
|
|
t.Fatalf("spend from delay output is invalid: %v", err)
|
|
}
|
|
|
|
localSigner := &input.MockSigner{Privkeys: []*btcec.PrivateKey{bobKeyPriv}}
|
|
|
|
// Next, we'll test bob spending with the derived revocation key to
|
|
// simulate the scenario when Alice broadcasts this commitment
|
|
// transaction after it's been revoked.
|
|
signDesc = &input.SignDescriptor{
|
|
KeyDesc: keychain.KeyDescriptor{
|
|
PubKey: bobKeyPub,
|
|
},
|
|
DoubleTweak: commitSecret,
|
|
WitnessScript: delayScript,
|
|
SigHashes: input.NewTxSigHashesV0Only(sweepTx),
|
|
Output: &wire.TxOut{
|
|
Value: int64(channelBalance),
|
|
},
|
|
HashType: txscript.SigHashAll,
|
|
InputIndex: 0,
|
|
}
|
|
bobWitnessSpend, err := input.CommitSpendRevoke(localSigner, signDesc,
|
|
sweepTx)
|
|
require.NoError(t, err, "unable to generate revocation witness")
|
|
sweepTx.TxIn[0].Witness = bobWitnessSpend
|
|
vm, err = txscript.NewEngine(
|
|
delayOutput.PkScript, sweepTx, 0, txscript.StandardVerifyFlags,
|
|
nil, nil, int64(channelBalance),
|
|
txscript.NewCannedPrevOutputFetcher(nil, 0),
|
|
)
|
|
require.NoError(t, err, "unable to create engine")
|
|
if err := vm.Execute(); err != nil {
|
|
t.Fatalf("revocation spend is invalid: %v", err)
|
|
}
|
|
|
|
// In order to test the final scenario, we modify the TxIn of the sweep
|
|
// transaction to instead point to the regular output (non delay)
|
|
// within the commitment transaction.
|
|
sweepTx.TxIn[0] = &wire.TxIn{
|
|
PreviousOutPoint: wire.OutPoint{
|
|
Hash: commitmentTx.TxHash(),
|
|
Index: 1,
|
|
},
|
|
}
|
|
|
|
// Finally, we test bob sweeping his output as normal in the case that
|
|
// Alice broadcasts this commitment transaction.
|
|
bobScriptP2WKH, err := input.CommitScriptUnencumbered(bobPayKey)
|
|
require.NoError(t, err, "unable to create bob p2wkh script")
|
|
signDesc = &input.SignDescriptor{
|
|
KeyDesc: keychain.KeyDescriptor{
|
|
PubKey: bobKeyPub,
|
|
},
|
|
WitnessScript: bobScriptP2WKH,
|
|
SigHashes: input.NewTxSigHashesV0Only(sweepTx),
|
|
Output: &wire.TxOut{
|
|
Value: int64(channelBalance),
|
|
PkScript: bobScriptP2WKH,
|
|
},
|
|
HashType: txscript.SigHashAll,
|
|
InputIndex: 0,
|
|
}
|
|
if !tweakless {
|
|
signDesc.SingleTweak = localCommitTweak
|
|
}
|
|
bobRegularSpend, err := input.CommitSpendNoDelay(
|
|
localSigner, signDesc, sweepTx, tweakless,
|
|
)
|
|
require.NoError(t, err, "unable to create bob regular spend")
|
|
sweepTx.TxIn[0].Witness = bobRegularSpend
|
|
vm, err = txscript.NewEngine(
|
|
regularOutput.PkScript,
|
|
sweepTx, 0, txscript.StandardVerifyFlags, nil,
|
|
nil, int64(channelBalance),
|
|
txscript.NewCannedPrevOutputFetcher(bobScriptP2WKH, 0),
|
|
)
|
|
require.NoError(t, err, "unable to create engine")
|
|
if err := vm.Execute(); err != nil {
|
|
t.Fatalf("bob p2wkh spend is invalid: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestCommitmentSpendValidation test the spendability of both outputs within
|
|
// the commitment transaction.
|
|
//
|
|
// The following spending cases are covered by this test:
|
|
// - Alice's spend from the delayed output on her commitment transaction.
|
|
// - Bob's spend from Alice's delayed output when she broadcasts a revoked
|
|
// commitment transaction.
|
|
// - Bob's spend from his unencumbered output within Alice's commitment
|
|
// transaction.
|
|
func TestCommitmentSpendValidation(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// In the modern network, all channels use the new tweakless format,
|
|
// but we also need to support older nodes that want to open channels
|
|
// with the legacy format, so we'll test spending in both scenarios.
|
|
for _, tweakless := range []bool{true, false} {
|
|
tweakless := tweakless
|
|
t.Run(fmt.Sprintf("tweak=%v", tweakless), func(t *testing.T) {
|
|
testSpendValidation(t, tweakless)
|
|
})
|
|
}
|
|
}
|
|
|
|
type mockProducer struct {
|
|
secret chainhash.Hash
|
|
}
|
|
|
|
func (p *mockProducer) AtIndex(uint64) (*chainhash.Hash, error) {
|
|
return &p.secret, nil
|
|
}
|
|
|
|
func (p *mockProducer) Encode(w io.Writer) error {
|
|
_, err := w.Write(p.secret[:])
|
|
return err
|
|
}
|
|
|
|
// createTestChannelsForVectors creates two LightningChannel instances for the
|
|
// test channel that is used to verify the test vectors.
|
|
func createTestChannelsForVectors(tc *testContext, chanType channeldb.ChannelType,
|
|
feeRate btcutil.Amount, remoteBalance, localBalance btcutil.Amount) (
|
|
*LightningChannel, *LightningChannel) {
|
|
|
|
t := tc.t
|
|
|
|
prevOut := &wire.OutPoint{
|
|
Hash: *tc.fundingTx.Hash(),
|
|
Index: 0,
|
|
}
|
|
|
|
fundingTxIn := wire.NewTxIn(prevOut, nil, nil)
|
|
|
|
// Generate random some keys that don't actually matter but need to be
|
|
// set.
|
|
var (
|
|
remoteDummy1, remoteDummy2 *btcec.PrivateKey
|
|
localDummy2, localDummy1 *btcec.PrivateKey
|
|
)
|
|
generateKeys := []**btcec.PrivateKey{
|
|
&remoteDummy1, &remoteDummy2, &localDummy1, &localDummy2,
|
|
}
|
|
for _, keyRef := range generateKeys {
|
|
privkey, err := btcec.NewPrivateKey()
|
|
require.NoError(t, err)
|
|
*keyRef = privkey
|
|
}
|
|
|
|
// Define channel configurations.
|
|
remoteCfg := channeldb.ChannelConfig{
|
|
ChannelConstraints: channeldb.ChannelConstraints{
|
|
DustLimit: tc.dustLimit,
|
|
MaxPendingAmount: lnwire.NewMSatFromSatoshis(
|
|
tc.fundingAmount,
|
|
),
|
|
ChanReserve: 0,
|
|
MinHTLC: 0,
|
|
MaxAcceptedHtlcs: input.MaxHTLCNumber / 2,
|
|
CsvDelay: tc.localCsvDelay,
|
|
},
|
|
MultiSigKey: keychain.KeyDescriptor{
|
|
PubKey: tc.remoteFundingPrivkey.PubKey(),
|
|
},
|
|
PaymentBasePoint: keychain.KeyDescriptor{
|
|
PubKey: tc.remotePaymentBasepointSecret.PubKey(),
|
|
},
|
|
HtlcBasePoint: keychain.KeyDescriptor{
|
|
PubKey: tc.remotePaymentBasepointSecret.PubKey(),
|
|
},
|
|
DelayBasePoint: keychain.KeyDescriptor{
|
|
PubKey: remoteDummy1.PubKey(),
|
|
},
|
|
RevocationBasePoint: keychain.KeyDescriptor{
|
|
PubKey: tc.remoteRevocationBasepointSecret.PubKey(),
|
|
},
|
|
}
|
|
localCfg := channeldb.ChannelConfig{
|
|
ChannelConstraints: channeldb.ChannelConstraints{
|
|
DustLimit: tc.dustLimit,
|
|
MaxPendingAmount: lnwire.NewMSatFromSatoshis(
|
|
tc.fundingAmount,
|
|
),
|
|
ChanReserve: 0,
|
|
MinHTLC: 0,
|
|
MaxAcceptedHtlcs: input.MaxHTLCNumber / 2,
|
|
CsvDelay: tc.localCsvDelay,
|
|
},
|
|
MultiSigKey: keychain.KeyDescriptor{
|
|
PubKey: tc.localFundingPrivkey.PubKey(),
|
|
},
|
|
PaymentBasePoint: keychain.KeyDescriptor{
|
|
PubKey: tc.localPaymentBasepointSecret.PubKey(),
|
|
},
|
|
HtlcBasePoint: keychain.KeyDescriptor{
|
|
PubKey: tc.localPaymentBasepointSecret.PubKey(),
|
|
},
|
|
DelayBasePoint: keychain.KeyDescriptor{
|
|
PubKey: tc.localDelayedPaymentBasepointSecret.PubKey(),
|
|
},
|
|
RevocationBasePoint: keychain.KeyDescriptor{
|
|
PubKey: localDummy1.PubKey(),
|
|
},
|
|
}
|
|
|
|
// Create mock producers to force usage of the test vector commitment
|
|
// point.
|
|
remotePreimageProducer := &mockProducer{
|
|
secret: chainhash.Hash(tc.localPerCommitSecret),
|
|
}
|
|
remoteCommitPoint := input.ComputeCommitmentPoint(
|
|
tc.localPerCommitSecret[:],
|
|
)
|
|
|
|
localPreimageProducer := &mockProducer{
|
|
secret: chainhash.Hash(tc.localPerCommitSecret),
|
|
}
|
|
localCommitPoint := input.ComputeCommitmentPoint(
|
|
tc.localPerCommitSecret[:],
|
|
)
|
|
|
|
// Create temporary databases.
|
|
dbRemote, err := channeldb.Open(t.TempDir())
|
|
require.NoError(t, err)
|
|
|
|
dbLocal, err := channeldb.Open(t.TempDir())
|
|
require.NoError(t, err)
|
|
|
|
// Create the initial commitment transactions for the channel.
|
|
feePerKw := chainfee.SatPerKWeight(feeRate)
|
|
commitWeight := int64(input.CommitWeight)
|
|
if chanType.HasAnchors() {
|
|
commitWeight = input.AnchorCommitWeight
|
|
}
|
|
commitFee := feePerKw.FeeForWeight(commitWeight)
|
|
|
|
var anchorAmt btcutil.Amount
|
|
if chanType.HasAnchors() {
|
|
anchorAmt = 2 * anchorSize
|
|
}
|
|
|
|
remoteCommitTx, localCommitTx, err := CreateCommitmentTxns(
|
|
remoteBalance, localBalance-commitFee,
|
|
&remoteCfg, &localCfg, remoteCommitPoint,
|
|
localCommitPoint, *fundingTxIn, chanType, true, 0,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
// Set up the full channel state.
|
|
|
|
// Subtract one because extra sig exchange will take place during setup
|
|
// to get to the right test point.
|
|
var commitHeight = tc.commitHeight - 1
|
|
|
|
remoteCommit := channeldb.ChannelCommitment{
|
|
CommitHeight: commitHeight,
|
|
LocalBalance: lnwire.NewMSatFromSatoshis(remoteBalance),
|
|
RemoteBalance: lnwire.NewMSatFromSatoshis(localBalance - commitFee - anchorAmt),
|
|
CommitFee: commitFee,
|
|
FeePerKw: btcutil.Amount(feePerKw),
|
|
CommitTx: remoteCommitTx,
|
|
CommitSig: testSigBytes,
|
|
}
|
|
localCommit := channeldb.ChannelCommitment{
|
|
CommitHeight: commitHeight,
|
|
LocalBalance: lnwire.NewMSatFromSatoshis(localBalance - commitFee - anchorAmt),
|
|
RemoteBalance: lnwire.NewMSatFromSatoshis(remoteBalance),
|
|
CommitFee: commitFee,
|
|
FeePerKw: btcutil.Amount(feePerKw),
|
|
CommitTx: localCommitTx,
|
|
CommitSig: testSigBytes,
|
|
}
|
|
|
|
var chanIDBytes [8]byte
|
|
_, err = io.ReadFull(rand.Reader, chanIDBytes[:])
|
|
require.NoError(t, err)
|
|
|
|
shortChanID := lnwire.NewShortChanIDFromInt(
|
|
binary.BigEndian.Uint64(chanIDBytes[:]),
|
|
)
|
|
|
|
remoteChannelState := &channeldb.OpenChannel{
|
|
LocalChanCfg: remoteCfg,
|
|
RemoteChanCfg: localCfg,
|
|
IdentityPub: remoteDummy2.PubKey(),
|
|
FundingOutpoint: *prevOut,
|
|
ShortChannelID: shortChanID,
|
|
ChanType: chanType,
|
|
IsInitiator: false,
|
|
Capacity: tc.fundingAmount,
|
|
RemoteCurrentRevocation: localCommitPoint,
|
|
RevocationProducer: remotePreimageProducer,
|
|
RevocationStore: shachain.NewRevocationStore(),
|
|
LocalCommitment: remoteCommit,
|
|
RemoteCommitment: remoteCommit,
|
|
Db: dbRemote.ChannelStateDB(),
|
|
Packager: channeldb.NewChannelPackager(shortChanID),
|
|
FundingTxn: tc.fundingTx.MsgTx(),
|
|
}
|
|
localChannelState := &channeldb.OpenChannel{
|
|
LocalChanCfg: localCfg,
|
|
RemoteChanCfg: remoteCfg,
|
|
IdentityPub: localDummy2.PubKey(),
|
|
FundingOutpoint: *prevOut,
|
|
ShortChannelID: shortChanID,
|
|
ChanType: chanType,
|
|
IsInitiator: true,
|
|
Capacity: tc.fundingAmount,
|
|
RemoteCurrentRevocation: remoteCommitPoint,
|
|
RevocationProducer: localPreimageProducer,
|
|
RevocationStore: shachain.NewRevocationStore(),
|
|
LocalCommitment: localCommit,
|
|
RemoteCommitment: localCommit,
|
|
Db: dbLocal.ChannelStateDB(),
|
|
Packager: channeldb.NewChannelPackager(shortChanID),
|
|
FundingTxn: tc.fundingTx.MsgTx(),
|
|
}
|
|
|
|
// Create mock signers that can sign for the keys that are used.
|
|
localSigner := &input.MockSigner{Privkeys: []*btcec.PrivateKey{
|
|
tc.localPaymentBasepointSecret, tc.localDelayedPaymentBasepointSecret,
|
|
tc.localFundingPrivkey, localDummy1, localDummy2,
|
|
}}
|
|
|
|
remoteSigner := &input.MockSigner{Privkeys: []*btcec.PrivateKey{
|
|
tc.remoteFundingPrivkey, tc.remoteRevocationBasepointSecret,
|
|
tc.remotePaymentBasepointSecret, remoteDummy1, remoteDummy2,
|
|
}}
|
|
|
|
remotePool := NewSigPool(1, remoteSigner)
|
|
channelRemote, err := NewLightningChannel(
|
|
remoteSigner, remoteChannelState, remotePool,
|
|
)
|
|
require.NoError(t, err)
|
|
require.NoError(t, remotePool.Start())
|
|
|
|
localPool := NewSigPool(1, localSigner)
|
|
channelLocal, err := NewLightningChannel(
|
|
localSigner, localChannelState, localPool,
|
|
)
|
|
require.NoError(t, err)
|
|
require.NoError(t, localPool.Start())
|
|
|
|
// Create state hunt obfuscator for the commitment transaction.
|
|
obfuscator := createStateHintObfuscator(remoteChannelState)
|
|
err = SetStateNumHint(
|
|
remoteCommitTx, commitHeight, obfuscator,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
err = SetStateNumHint(
|
|
localCommitTx, commitHeight, obfuscator,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
// Initialize the database.
|
|
addr := &net.TCPAddr{
|
|
IP: net.ParseIP("127.0.0.1"),
|
|
Port: 18556,
|
|
}
|
|
require.NoError(t, channelRemote.channelState.SyncPending(addr, 101))
|
|
|
|
addr = &net.TCPAddr{
|
|
IP: net.ParseIP("127.0.0.1"),
|
|
Port: 18555,
|
|
}
|
|
require.NoError(t, channelLocal.channelState.SyncPending(addr, 101))
|
|
|
|
// Now that the channel are open, simulate the start of a session by
|
|
// having local and remote extend their revocation windows to each other.
|
|
err = initRevocationWindows(channelRemote, channelLocal)
|
|
require.NoError(t, err)
|
|
|
|
// Return a clean up function that stops goroutines and removes the test
|
|
// databases.
|
|
t.Cleanup(func() {
|
|
dbLocal.Close()
|
|
dbRemote.Close()
|
|
|
|
require.NoError(t, remotePool.Stop())
|
|
require.NoError(t, localPool.Stop())
|
|
})
|
|
|
|
return channelRemote, channelLocal
|
|
}
|