126f79dbb1
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.
578 lines
16 KiB
Go
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, ¶ms)
|
|
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[:], ¶ms,
|
|
)
|
|
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[:], ¶ms,
|
|
)
|
|
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, ¶ms)
|
|
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, ¶ms)
|
|
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, ¶ms)
|
|
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
|
|
}
|