lnd.xprv/lnwallet/chanfunding/psbt_assembler_test.go
Oliver Gugger 126f79dbb1
chanfunding: add PSBT assembler and intent
We add a new funding assembler and intent type that handle channel
funding through the use of a PSBT. The PsbtIntent is in itself a
simple state machine that can be stepped through the process of
assembling the required information for the funding output, verifying
a user supplied PSBT for correctness, accepting a fully signed PSBT
and then assembling the funding wire message.
2020-03-31 09:17:23 +02:00

578 lines
16 KiB
Go

package chanfunding
import (
"bytes"
"crypto/sha256"
"fmt"
"reflect"
"sync"
"testing"
"time"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcutil/psbt"
"github.com/davecgh/go-spew/spew"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
)
var (
localPrivkey = []byte{1, 2, 3, 4, 5, 6}
remotePrivkey = []byte{6, 5, 4, 3, 2, 1}
chanCapacity btcutil.Amount = 644000
params = chaincfg.RegressionNetParams
defaultTimeout = 50 * time.Millisecond
)
// TestPsbtIntent tests the basic happy path of the PSBT assembler and intent.
func TestPsbtIntent(t *testing.T) {
t.Parallel()
// Create a simple assembler and ask it to provision a channel to get
// the funding intent.
a := NewPsbtAssembler(chanCapacity, nil, &params)
intent, err := a.ProvisionChannel(&Request{LocalAmt: chanCapacity})
if err != nil {
t.Fatalf("error provisioning channel: %v", err)
}
psbtIntent, ok := intent.(*PsbtIntent)
if !ok {
t.Fatalf("intent was not a PsbtIntent")
}
if psbtIntent.State != PsbtShimRegistered {
t.Fatalf("unexpected state. got %d wanted %d", psbtIntent.State,
PsbtShimRegistered)
}
// The first step with the intent is that the funding manager starts
// negotiating with the remote peer and they accept. By accepting, they
// send over their multisig key that's going to be used for the funding
// output. With that known, we can start crafting a PSBT.
_, localPubkey := btcec.PrivKeyFromBytes(btcec.S256(), localPrivkey)
_, remotePubkey := btcec.PrivKeyFromBytes(btcec.S256(), remotePrivkey)
psbtIntent.BindKeys(
&keychain.KeyDescriptor{PubKey: localPubkey}, remotePubkey,
)
if psbtIntent.State != PsbtOutputKnown {
t.Fatalf("unexpected state. got %d wanted %d", psbtIntent.State,
PsbtOutputKnown)
}
// Make sure the output script address is correct.
script, _, err := input.GenFundingPkScript(
localPubkey.SerializeCompressed(),
remotePubkey.SerializeCompressed(), int64(chanCapacity),
)
if err != nil {
t.Fatalf("error calculating script: %v", err)
}
witnessScriptHash := sha256.Sum256(script)
addr, err := btcutil.NewAddressWitnessScriptHash(
witnessScriptHash[:], &params,
)
if err != nil {
t.Fatalf("unable to encode address: %v", err)
}
fundingAddr, amt, pendingPsbt, err := psbtIntent.FundingParams()
if err != nil {
t.Fatalf("unable to get funding params: %v", err)
}
if addr.EncodeAddress() != fundingAddr.EncodeAddress() {
t.Fatalf("unexpected address. got %s wanted %s", fundingAddr,
addr)
}
if amt != int64(chanCapacity) {
t.Fatalf("unexpected amount. got %d wanted %d", amt,
chanCapacity)
}
// Parse and check the returned PSBT packet.
if pendingPsbt == nil {
t.Fatalf("expected pending PSBT to be returned")
}
if len(pendingPsbt.UnsignedTx.TxOut) != 1 {
t.Fatalf("unexpected number of outputs. got %d wanted %d",
len(pendingPsbt.UnsignedTx.TxOut), 1)
}
txOut := pendingPsbt.UnsignedTx.TxOut[0]
if !bytes.Equal(txOut.PkScript[2:], witnessScriptHash[:]) {
t.Fatalf("unexpected PK script in output. got %x wanted %x",
txOut.PkScript[2:], witnessScriptHash)
}
if txOut.Value != int64(chanCapacity) {
t.Fatalf("unexpected value in output. got %d wanted %d",
txOut.Value, chanCapacity)
}
// Add an input to the pending TX to simulate it being funded.
pendingPsbt.UnsignedTx.TxIn = []*wire.TxIn{
{PreviousOutPoint: wire.OutPoint{Index: 0}},
}
pendingPsbt.Inputs = []psbt.PInput{
{WitnessUtxo: &wire.TxOut{Value: int64(chanCapacity + 1)}},
}
// Verify the dummy PSBT with the intent.
err = psbtIntent.Verify(pendingPsbt)
if err != nil {
t.Fatalf("error verifying pending PSBT: %v", err)
}
if psbtIntent.State != PsbtVerified {
t.Fatalf("unexpected state. got %d wanted %d", psbtIntent.State,
PsbtVerified)
}
// Add some fake witness data to the transaction so it thinks it's
// signed.
pendingPsbt.Inputs[0].WitnessUtxo = &wire.TxOut{
Value: int64(chanCapacity) * 2,
PkScript: []byte{99, 99, 99},
}
pendingPsbt.Inputs[0].FinalScriptSig = []byte{88, 88, 88}
pendingPsbt.Inputs[0].FinalScriptWitness = []byte{2, 0, 0}
// If we call Finalize, the intent will signal to the funding manager
// that it can continue with the funding flow. We want to make sure
// the signal arrives.
var wg sync.WaitGroup
errChan := make(chan error, 1)
wg.Add(1)
go func() {
defer wg.Done()
select {
case err := <-psbtIntent.PsbtReady:
errChan <- err
case <-time.After(defaultTimeout):
errChan <- fmt.Errorf("timed out")
}
}()
err = psbtIntent.Finalize(pendingPsbt)
if err != nil {
t.Fatalf("error finalizing pending PSBT: %v", err)
}
wg.Wait()
// We should have a nil error in our channel now.
err = <-errChan
if err != nil {
t.Fatalf("unexpected error after finalize: %v", err)
}
if psbtIntent.State != PsbtFinalized {
t.Fatalf("unexpected state. got %d wanted %d", psbtIntent.State,
PsbtFinalized)
}
// Make sure the funding transaction can be compiled.
_, err = psbtIntent.CompileFundingTx()
if err != nil {
t.Fatalf("error compiling funding TX from PSBT: %v", err)
}
if psbtIntent.State != PsbtFundingTxCompiled {
t.Fatalf("unexpected state. got %d wanted %d", psbtIntent.State,
PsbtFundingTxCompiled)
}
}
// TestPsbtIntentBasePsbt tests that a channel funding output can be appended to
// a given base PSBT in the funding flow.
func TestPsbtIntentBasePsbt(t *testing.T) {
t.Parallel()
// First create a dummy PSBT with a single output.
pendingPsbt, err := psbt.New(
[]*wire.OutPoint{{}}, []*wire.TxOut{
{Value: 999, PkScript: []byte{99, 88, 77}},
}, 2, 0, []uint32{0},
)
if err != nil {
t.Fatalf("unable to create dummy PSBT")
}
// Generate the funding multisig keys and the address so we can compare
// it to the output of the intent.
_, localPubkey := btcec.PrivKeyFromBytes(btcec.S256(), localPrivkey)
_, remotePubkey := btcec.PrivKeyFromBytes(btcec.S256(), remotePrivkey)
// Make sure the output script address is correct.
script, _, err := input.GenFundingPkScript(
localPubkey.SerializeCompressed(),
remotePubkey.SerializeCompressed(), int64(chanCapacity),
)
if err != nil {
t.Fatalf("error calculating script: %v", err)
}
witnessScriptHash := sha256.Sum256(script)
addr, err := btcutil.NewAddressWitnessScriptHash(
witnessScriptHash[:], &params,
)
if err != nil {
t.Fatalf("unable to encode address: %v", err)
}
// Now as the next step, create a new assembler/intent pair with a base
// PSBT to see that we can add an additional output to it.
a := NewPsbtAssembler(chanCapacity, pendingPsbt, &params)
intent, err := a.ProvisionChannel(&Request{LocalAmt: chanCapacity})
if err != nil {
t.Fatalf("error provisioning channel: %v", err)
}
psbtIntent, ok := intent.(*PsbtIntent)
if !ok {
t.Fatalf("intent was not a PsbtIntent")
}
psbtIntent.BindKeys(
&keychain.KeyDescriptor{PubKey: localPubkey}, remotePubkey,
)
newAddr, amt, twoOutPsbt, err := psbtIntent.FundingParams()
if err != nil {
t.Fatalf("unable to get funding params: %v", err)
}
if addr.EncodeAddress() != newAddr.EncodeAddress() {
t.Fatalf("unexpected address. got %s wanted %s", newAddr,
addr)
}
if amt != int64(chanCapacity) {
t.Fatalf("unexpected amount. got %d wanted %d", amt,
chanCapacity)
}
if len(twoOutPsbt.UnsignedTx.TxOut) != 2 {
t.Fatalf("unexpected number of outputs. got %d wanted %d",
len(twoOutPsbt.UnsignedTx.TxOut), 2)
}
if len(twoOutPsbt.UnsignedTx.TxIn) != 1 {
t.Fatalf("unexpected number of inputs. got %d wanted %d",
len(twoOutPsbt.UnsignedTx.TxIn), 1)
}
txOld := pendingPsbt.UnsignedTx
txNew := twoOutPsbt.UnsignedTx
prevoutEqual := reflect.DeepEqual(
txOld.TxIn[0].PreviousOutPoint, txNew.TxIn[0].PreviousOutPoint,
)
if !prevoutEqual {
t.Fatalf("inputs changed. got %s wanted %s",
spew.Sdump(txOld.TxIn[0].PreviousOutPoint),
spew.Sdump(txNew.TxIn[0].PreviousOutPoint))
}
if !reflect.DeepEqual(txOld.TxOut[0], txNew.TxOut[0]) {
t.Fatalf("existing output changed. got %v wanted %v",
txOld.TxOut[0], txNew.TxOut[0])
}
}
// TestPsbtVerify tests the PSBT verification process more deeply than just
// the happy path.
func TestPsbtVerify(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
expectedErr string
doVerify func(int64, *psbt.Packet, *PsbtIntent) error
}{
{
name: "nil packet",
expectedErr: "PSBT is nil",
doVerify: func(amt int64, p *psbt.Packet,
i *PsbtIntent) error {
return i.Verify(nil)
},
},
{
name: "wrong state",
expectedErr: "invalid state. got user_canceled " +
"expected output_known",
doVerify: func(amt int64, p *psbt.Packet,
i *PsbtIntent) error {
i.State = PsbtInitiatorCanceled
return i.Verify(p)
},
},
{
name: "output not found, value wrong",
expectedErr: "funding output not found in PSBT",
doVerify: func(amt int64, p *psbt.Packet,
i *PsbtIntent) error {
p.UnsignedTx.TxOut[0].Value = 123
return i.Verify(p)
},
},
{
name: "output not found, pk script wrong",
expectedErr: "funding output not found in PSBT",
doVerify: func(amt int64, p *psbt.Packet,
i *PsbtIntent) error {
p.UnsignedTx.TxOut[0].PkScript = []byte{1, 2, 3}
return i.Verify(p)
},
},
{
name: "no inputs",
expectedErr: "PSBT has no inputs",
doVerify: func(amt int64, p *psbt.Packet,
i *PsbtIntent) error {
return i.Verify(p)
},
},
{
name: "input(s) too small",
expectedErr: "input amount sum must be larger than " +
"output amount sum",
doVerify: func(amt int64, p *psbt.Packet,
i *PsbtIntent) error {
p.UnsignedTx.TxIn = []*wire.TxIn{{}}
p.Inputs = []psbt.PInput{{
WitnessUtxo: &wire.TxOut{
Value: int64(chanCapacity),
},
}}
return i.Verify(p)
},
},
{
name: "input correct",
expectedErr: "",
doVerify: func(amt int64, p *psbt.Packet,
i *PsbtIntent) error {
txOut := &wire.TxOut{
Value: int64(chanCapacity/2) + 1,
}
p.UnsignedTx.TxIn = []*wire.TxIn{
{},
{
PreviousOutPoint: wire.OutPoint{
Index: 0,
},
},
}
p.Inputs = []psbt.PInput{
{
WitnessUtxo: txOut,
},
{
NonWitnessUtxo: &wire.MsgTx{
TxOut: []*wire.TxOut{
txOut,
},
},
}}
return i.Verify(p)
},
},
}
// Create a simple assembler and ask it to provision a channel to get
// the funding intent.
a := NewPsbtAssembler(chanCapacity, nil, &params)
intent, err := a.ProvisionChannel(&Request{LocalAmt: chanCapacity})
if err != nil {
t.Fatalf("error provisioning channel: %v", err)
}
psbtIntent := intent.(*PsbtIntent)
// Bind our test keys to get the funding parameters.
_, localPubkey := btcec.PrivKeyFromBytes(btcec.S256(), localPrivkey)
_, remotePubkey := btcec.PrivKeyFromBytes(btcec.S256(), remotePrivkey)
psbtIntent.BindKeys(
&keychain.KeyDescriptor{PubKey: localPubkey}, remotePubkey,
)
// Loop through all our test cases.
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
// Reset the state from a previous test and create a new
// pending PSBT that we can manipulate.
psbtIntent.State = PsbtOutputKnown
_, amt, pendingPsbt, err := psbtIntent.FundingParams()
if err != nil {
t.Fatalf("unable to get funding params: %v", err)
}
err = tc.doVerify(amt, pendingPsbt, psbtIntent)
if err != nil && tc.expectedErr != "" &&
err.Error() != tc.expectedErr {
t.Fatalf("unexpected error, got '%v' wanted "+
"'%v'", err, tc.expectedErr)
}
})
}
}
// TestPsbtFinalize tests the PSBT finalization process more deeply than just
// the happy path.
func TestPsbtFinalize(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
expectedErr string
doFinalize func(int64, *psbt.Packet, *PsbtIntent) error
}{
{
name: "nil packet",
expectedErr: "PSBT is nil",
doFinalize: func(amt int64, p *psbt.Packet,
i *PsbtIntent) error {
return i.Finalize(nil)
},
},
{
name: "wrong state",
expectedErr: "invalid state. got user_canceled " +
"expected verified",
doFinalize: func(amt int64, p *psbt.Packet,
i *PsbtIntent) error {
i.State = PsbtInitiatorCanceled
return i.Finalize(p)
},
},
{
name: "not verified first",
expectedErr: "PSBT was not verified first",
doFinalize: func(amt int64, p *psbt.Packet,
i *PsbtIntent) error {
i.State = PsbtVerified
i.PendingPsbt = nil
return i.Finalize(p)
},
},
{
name: "output value changed",
expectedErr: "outputs differ from verified PSBT: " +
"output 0 is different",
doFinalize: func(amt int64, p *psbt.Packet,
i *PsbtIntent) error {
p.UnsignedTx.TxOut[0].Value = 123
return i.Finalize(p)
},
},
{
name: "output pk script changed",
expectedErr: "outputs differ from verified PSBT: " +
"output 0 is different",
doFinalize: func(amt int64, p *psbt.Packet,
i *PsbtIntent) error {
p.UnsignedTx.TxOut[0].PkScript = []byte{3, 2, 1}
return i.Finalize(p)
},
},
{
name: "input previous outpoint index changed",
expectedErr: "inputs differ from verified PSBT: " +
"previous outpoint of input 0 is different",
doFinalize: func(amt int64, p *psbt.Packet,
i *PsbtIntent) error {
p.UnsignedTx.TxIn[0].PreviousOutPoint.Index = 0
return i.Finalize(p)
},
},
{
name: "input previous outpoint hash changed",
expectedErr: "inputs differ from verified PSBT: " +
"previous outpoint of input 0 is different",
doFinalize: func(amt int64, p *psbt.Packet,
i *PsbtIntent) error {
prevout := &p.UnsignedTx.TxIn[0].PreviousOutPoint
prevout.Hash = chainhash.Hash{77, 88, 99, 11}
return i.Finalize(p)
},
},
}
// Create a simple assembler and ask it to provision a channel to get
// the funding intent.
a := NewPsbtAssembler(chanCapacity, nil, &params)
intent, err := a.ProvisionChannel(&Request{LocalAmt: chanCapacity})
if err != nil {
t.Fatalf("error provisioning channel: %v", err)
}
psbtIntent := intent.(*PsbtIntent)
// Bind our test keys to get the funding parameters.
_, localPubkey := btcec.PrivKeyFromBytes(btcec.S256(), localPrivkey)
_, remotePubkey := btcec.PrivKeyFromBytes(btcec.S256(), remotePrivkey)
psbtIntent.BindKeys(
&keychain.KeyDescriptor{PubKey: localPubkey}, remotePubkey,
)
// Loop through all our test cases.
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
// Reset the state from a previous test and create a new
// pending PSBT that we can manipulate.
psbtIntent.State = PsbtOutputKnown
_, amt, pendingPsbt, err := psbtIntent.FundingParams()
if err != nil {
t.Fatalf("unable to get funding params: %v", err)
}
// We need to have a simulated transaction here that is
// fully funded and signed.
pendingPsbt.UnsignedTx.TxIn = []*wire.TxIn{{
PreviousOutPoint: wire.OutPoint{
Index: 1,
Hash: chainhash.Hash{1, 2, 3},
},
}}
pendingPsbt.Inputs = []psbt.PInput{{
WitnessUtxo: &wire.TxOut{
Value: int64(chanCapacity) + 1,
PkScript: []byte{1, 2, 3},
},
FinalScriptWitness: []byte{0x01, 0x00},
}}
err = psbtIntent.Verify(pendingPsbt)
if err != nil {
t.Fatalf("error verifying PSBT: %v", err)
}
// Deep clone the PSBT so we don't modify the pending
// one that was registered during Verify.
pendingPsbt = clonePsbt(t, pendingPsbt)
err = tc.doFinalize(amt, pendingPsbt, psbtIntent)
if (err == nil && tc.expectedErr != "") ||
(err != nil && err.Error() != tc.expectedErr) {
t.Fatalf("unexpected error, got '%v' wanted "+
"'%v'", err, tc.expectedErr)
}
})
}
}
// clonePsbt creates a clone of a PSBT packet by serializing then de-serializing
// it.
func clonePsbt(t *testing.T, p *psbt.Packet) *psbt.Packet {
var buf bytes.Buffer
err := p.Serialize(&buf)
if err != nil {
t.Fatalf("error serializing PSBT: %v", err)
}
newPacket, err := psbt.NewFromRawBytes(&buf, false)
if err != nil {
t.Fatalf("error unserializing PSBT: %v", err)
}
return newPacket
}