From 126f79dbb14be5f66eff5a810d92ea68c1503892 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Tue, 31 Mar 2020 09:13:15 +0200 Subject: [PATCH] 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. --- lnwallet/chanfunding/assembler.go | 2 +- lnwallet/chanfunding/psbt_assembler.go | 524 ++++++++++++++++++ lnwallet/chanfunding/psbt_assembler_test.go | 577 ++++++++++++++++++++ 3 files changed, 1102 insertions(+), 1 deletion(-) create mode 100644 lnwallet/chanfunding/psbt_assembler.go create mode 100644 lnwallet/chanfunding/psbt_assembler_test.go diff --git a/lnwallet/chanfunding/assembler.go b/lnwallet/chanfunding/assembler.go index d06de903d..208cab68b 100644 --- a/lnwallet/chanfunding/assembler.go +++ b/lnwallet/chanfunding/assembler.go @@ -126,7 +126,7 @@ type Assembler interface { // FundingTxAssembler is a super-set of the regular Assembler interface that's // also able to provide a fully populated funding transaction via the intents -// that it produuces. +// that it produces. type FundingTxAssembler interface { Assembler diff --git a/lnwallet/chanfunding/psbt_assembler.go b/lnwallet/chanfunding/psbt_assembler.go new file mode 100644 index 000000000..a65cc624a --- /dev/null +++ b/lnwallet/chanfunding/psbt_assembler.go @@ -0,0 +1,524 @@ +package chanfunding + +import ( + "bytes" + "crypto/sha256" + "errors" + "fmt" + "sync" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcutil/psbt" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/keychain" +) + +// PsbtState is a type for the state of the PSBT intent state machine. +type PsbtState uint8 + +const ( + // PsbtShimRegistered denotes a channel funding process has started with + // a PSBT shim attached. This is the default state for a PsbtIntent. We + // don't use iota here because the values have to be in sync with the + // RPC constants. + PsbtShimRegistered PsbtState = 1 + + // PsbtOutputKnown denotes that the local and remote peer have + // negotiated the multisig keys to be used as the channel funding output + // and therefore the PSBT funding process can now start. + PsbtOutputKnown PsbtState = 2 + + // PsbtVerified denotes that a potential PSBT has been presented to the + // intent and passed all checks. The verified PSBT can be given to a/the + // signer(s). + PsbtVerified PsbtState = 3 + + // PsbtFinalized denotes that a fully signed PSBT has been given to the + // intent that looks identical to the previously verified transaction + // but has all witness data added and is therefore completely signed. + PsbtFinalized PsbtState = 4 + + // PsbtFundingTxCompiled denotes that the PSBT processed by this intent + // has been successfully converted into a protocol transaction. It is + // not yet completely certain that the resulting transaction will be + // published because the commitment transactions between the channel + // peers first need to be counter signed. But the job of the intent is + // hereby completed. + PsbtFundingTxCompiled PsbtState = 5 + + // PsbtInitiatorCanceled denotes that the user has canceled the intent. + PsbtInitiatorCanceled PsbtState = 6 + + // PsbtResponderCanceled denotes that the remote peer has canceled the + // funding, likely due to a timeout. + PsbtResponderCanceled PsbtState = 7 +) + +// String returns a string representation of the PsbtState. +func (s PsbtState) String() string { + switch s { + case PsbtShimRegistered: + return "shim_registered" + + case PsbtOutputKnown: + return "output_known" + + case PsbtVerified: + return "verified" + + case PsbtFinalized: + return "finalized" + + case PsbtFundingTxCompiled: + return "funding_tx_compiled" + + case PsbtInitiatorCanceled: + return "user_canceled" + + case PsbtResponderCanceled: + return "remote_canceled" + + default: + return fmt.Sprintf("", s) + } +} + +var ( + // ErrRemoteCanceled is the error that is returned to the user if the + // funding flow was canceled by the remote peer. + ErrRemoteCanceled = errors.New("remote canceled funding, possibly " + + "timed out") + + // ErrUserCanceled is the error that is returned through the PsbtReady + // channel if the user canceled the funding flow. + ErrUserCanceled = errors.New("user canceled funding") +) + +// PsbtIntent is an intent created by the PsbtAssembler which represents a +// funding output to be created by a PSBT. This might be used when a hardware +// wallet, or a channel factory is the entity crafting the funding transaction, +// and not lnd. +type PsbtIntent struct { + // ShimIntent is the wrapped basic intent that contains common fields + // we also use in the PSBT funding case. + ShimIntent + + // State is the current state the intent state machine is in. + State PsbtState + + // BasePsbt is the user-supplied base PSBT the channel output should be + // added to. If this is nil we will create a new, empty PSBT as the base + // for the funding transaction. + BasePsbt *psbt.Packet + + // PendingPsbt is the parsed version of the current PSBT. This can be + // in two stages: If the user has not yet provided any PSBT, this is + // nil. Once the user sends us an unsigned funded PSBT, we verify that + // we have a valid transaction that sends to the channel output PK + // script and has an input large enough to pay for it. We keep this + // verified but not yet signed version around until the fully signed + // transaction is submitted by the user. At that point we make sure the + // inputs and outputs haven't changed to what was previously verified. + // Only witness data should be added after the verification process. + PendingPsbt *psbt.Packet + + // PsbtReady is an error channel the funding manager will listen for + // a signal about the PSBT being ready to continue the funding flow. In + // the normal, happy flow, this channel is only ever closed. If a + // non-nil error is sent through the channel, the funding flow will be + // canceled. + // + // NOTE: This channel must always be buffered. + PsbtReady chan error + + // signalPsbtReady is a Once guard to make sure the PsbtReady channel is + // only closed exactly once. + signalPsbtReady sync.Once + + // netParams are the network parameters used to encode the P2WSH funding + // address. + netParams *chaincfg.Params +} + +// BindKeys sets both the remote and local node's keys that will be used for the +// channel funding multisig output. +func (i *PsbtIntent) BindKeys(localKey *keychain.KeyDescriptor, + remoteKey *btcec.PublicKey) { + + i.localKey = localKey + i.remoteKey = remoteKey + i.State = PsbtOutputKnown +} + +// FundingParams returns the parameters that are necessary to start funding the +// channel output this intent was created for. It returns the P2WSH funding +// address, the exact funding amount and a PSBT packet that contains exactly one +// output that encodes the previous two parameters. +func (i *PsbtIntent) FundingParams() (btcutil.Address, int64, *psbt.Packet, + error) { + + if i.State != PsbtOutputKnown { + return nil, 0, nil, fmt.Errorf("invalid state, got %v "+ + "expected %v", i.State, PsbtOutputKnown) + } + + // The funding output needs to be known already at this point, which + // means we need to have the local and remote multisig keys bound + // already. + witnessScript, out, err := i.FundingOutput() + if err != nil { + return nil, 0, nil, fmt.Errorf("unable to create funding "+ + "output: %v", err) + } + witnessScriptHash := sha256.Sum256(witnessScript) + + // Encode the address in the human readable bech32 format. + addr, err := btcutil.NewAddressWitnessScriptHash( + witnessScriptHash[:], i.netParams, + ) + if err != nil { + return nil, 0, nil, fmt.Errorf("unable to encode address: %v", + err) + } + + // We'll also encode the address/amount in a machine readable raw PSBT + // format. If the user supplied a base PSBT, we'll add the output to + // that one, otherwise we'll create a new one. + packet := i.BasePsbt + if packet == nil { + packet, err = psbt.New(nil, nil, 2, 0, nil) + if err != nil { + return nil, 0, nil, fmt.Errorf("unable to create "+ + "PSBT: %v", err) + } + } + packet.UnsignedTx.TxOut = append(packet.UnsignedTx.TxOut, out) + packet.Outputs = append(packet.Outputs, psbt.POutput{}) + return addr, out.Value, packet, nil +} + +// Verify makes sure the PSBT that is given to the intent has an output that +// sends to the channel funding multisig address with the correct amount. A +// simple check that at least a single input has been specified is performed. +func (i *PsbtIntent) Verify(packet *psbt.Packet) error { + if packet == nil { + return fmt.Errorf("PSBT is nil") + } + if i.State != PsbtOutputKnown { + return fmt.Errorf("invalid state. got %v expected %v", i.State, + PsbtOutputKnown) + } + + // Try to locate the channel funding multisig output. + _, expectedOutput, err := i.FundingOutput() + if err != nil { + return fmt.Errorf("funding output cannot be created: %v", err) + } + outputFound := false + outputSum := int64(0) + for _, out := range packet.UnsignedTx.TxOut { + outputSum += out.Value + if txOutsEqual(out, expectedOutput) { + outputFound = true + } + } + if !outputFound { + return fmt.Errorf("funding output not found in PSBT") + } + + // At least one input needs to be specified and it must be large enough + // to pay for all outputs. We don't want to dive into fee estimation + // here so we just assume that if the input amount exceeds the output + // amount, the chosen fee is sufficient. + if len(packet.UnsignedTx.TxIn) == 0 { + return fmt.Errorf("PSBT has no inputs") + } + sum, err := sumUtxoInputValues(packet) + if err != nil { + return fmt.Errorf("error determining input sum: %v", err) + } + if sum <= outputSum { + return fmt.Errorf("input amount sum must be larger than " + + "output amount sum") + } + + i.PendingPsbt = packet + i.State = PsbtVerified + return nil +} + +// Finalize makes sure the final PSBT that is given to the intent is fully valid +// and signed but still contains the same UTXOs and outputs as the pending +// transaction we previously verified. If everything checks out, the funding +// manager is informed that the channel can now be opened and the funding +// transaction be broadcast. +func (i *PsbtIntent) Finalize(packet *psbt.Packet) error { + if packet == nil { + return fmt.Errorf("PSBT is nil") + } + if i.State != PsbtVerified { + return fmt.Errorf("invalid state. got %v expected %v", i.State, + PsbtVerified) + } + + // Make sure the PSBT itself thinks it's finalized and ready to be + // broadcast. + err := psbt.MaybeFinalizeAll(packet) + if err != nil { + return fmt.Errorf("error finalizing PSBT: %v", err) + } + _, err = psbt.Extract(packet) + if err != nil { + return fmt.Errorf("unable to extract funding TX: %v", err) + } + + // Do a basic check that this is still the same PSBT that we verified in + // the previous step. This is to protect the user from unwanted + // modifications. We only check the outputs and previous outpoints of + // the inputs of the wire transaction because the fields in the PSBT + // part are allowed to change. + if i.PendingPsbt == nil { + return fmt.Errorf("PSBT was not verified first") + } + err = verifyOutputsEqual( + packet.UnsignedTx.TxOut, i.PendingPsbt.UnsignedTx.TxOut, + ) + if err != nil { + return fmt.Errorf("outputs differ from verified PSBT: %v", err) + } + err = verifyInputPrevOutpointsEqual( + packet.UnsignedTx.TxIn, i.PendingPsbt.UnsignedTx.TxIn, + ) + if err != nil { + return fmt.Errorf("inputs differ from verified PSBT: %v", err) + } + + // As far as we can tell, this PSBT is ok to be used as a funding + // transaction. + i.PendingPsbt = packet + i.State = PsbtFinalized + + // Signal the funding manager that it can now finally continue with its + // funding flow as the PSBT is now ready to be converted into a real + // transaction and be published. + i.signalPsbtReady.Do(func() { + close(i.PsbtReady) + }) + return nil +} + +// CompileFundingTx finalizes the previously verified PSBT and returns the +// extracted binary serialized transaction from it. It also prepares the channel +// point for which this funding intent was initiated for. +func (i *PsbtIntent) CompileFundingTx() (*wire.MsgTx, error) { + if i.State != PsbtFinalized { + return nil, fmt.Errorf("invalid state. got %v expected %v", + i.State, PsbtFinalized) + } + + // Make sure the PSBT can be finalized and extracted. + err := psbt.MaybeFinalizeAll(i.PendingPsbt) + if err != nil { + return nil, fmt.Errorf("error finalizing PSBT: %v", err) + } + fundingTx, err := psbt.Extract(i.PendingPsbt) + if err != nil { + return nil, fmt.Errorf("unable to extract funding TX: %v", err) + } + + // Identify our funding outpoint now that we know everything's ready. + _, txOut, err := i.FundingOutput() + if err != nil { + return nil, fmt.Errorf("cannot get funding output: %v", err) + } + ok, idx := input.FindScriptOutputIndex(fundingTx, txOut.PkScript) + if !ok { + return nil, fmt.Errorf("funding output not found in PSBT") + } + i.chanPoint = &wire.OutPoint{ + Hash: fundingTx.TxHash(), + Index: idx, + } + i.State = PsbtFundingTxCompiled + + return fundingTx, nil +} + +// RemoteCanceled informs the listener of the PSBT ready channel that the +// funding has been canceled by the remote peer and that we can no longer +// continue with it. +func (i *PsbtIntent) RemoteCanceled() { + log.Debugf("PSBT funding intent canceled by remote, state=%v", i.State) + i.signalPsbtReady.Do(func() { + i.PsbtReady <- ErrRemoteCanceled + i.State = PsbtResponderCanceled + }) + i.ShimIntent.Cancel() +} + +// Cancel allows the caller to cancel a funding Intent at any time. This will +// return make sure the channel funding flow with the remote peer is failed and +// any reservations are canceled. +// +// NOTE: Part of the chanfunding.Intent interface. +func (i *PsbtIntent) Cancel() { + log.Debugf("PSBT funding intent canceled, state=%v", i.State) + i.signalPsbtReady.Do(func() { + i.PsbtReady <- ErrUserCanceled + i.State = PsbtInitiatorCanceled + }) + i.ShimIntent.Cancel() +} + +// PsbtAssembler is a type of chanfunding.Assembler wherein the funding +// transaction is constructed outside of lnd by using partially signed bitcoin +// transactions (PSBT). +type PsbtAssembler struct { + // fundingAmt is the total amount of coins in the funding output. + fundingAmt btcutil.Amount + + // basePsbt is the user-supplied base PSBT the channel output should be + // added to. + basePsbt *psbt.Packet + + // netParams are the network parameters used to encode the P2WSH funding + // address. + netParams *chaincfg.Params +} + +// NewPsbtAssembler creates a new CannedAssembler from the material required +// to construct a funding output and channel point. An optional base PSBT can +// be supplied which will be used to add the channel output to instead of +// creating a new one. +func NewPsbtAssembler(fundingAmt btcutil.Amount, basePsbt *psbt.Packet, + netParams *chaincfg.Params) *PsbtAssembler { + + return &PsbtAssembler{ + fundingAmt: fundingAmt, + basePsbt: basePsbt, + netParams: netParams, + } +} + +// ProvisionChannel creates a new ShimIntent given the passed funding Request. +// The returned intent is immediately able to provide the channel point and +// funding output as they've already been created outside lnd. +// +// NOTE: This method satisfies the chanfunding.Assembler interface. +func (p *PsbtAssembler) ProvisionChannel(req *Request) (Intent, error) { + // We'll exit out if this field is set as the funding transaction will + // be assembled externally, so we don't influence coin selection. + if req.SubtractFees { + return nil, fmt.Errorf("SubtractFees not supported for PSBT") + } + + intent := &PsbtIntent{ + ShimIntent: ShimIntent{ + localFundingAmt: p.fundingAmt, + }, + State: PsbtShimRegistered, + BasePsbt: p.basePsbt, + PsbtReady: make(chan error, 1), + netParams: p.netParams, + } + + // A simple sanity check to ensure the provisioned request matches the + // re-made shim intent. + if req.LocalAmt+req.RemoteAmt != p.fundingAmt { + return nil, fmt.Errorf("intent doesn't match PSBT "+ + "assembler: local_amt=%v, remote_amt=%v, funding_amt=%v", + req.LocalAmt, req.RemoteAmt, p.fundingAmt) + } + + return intent, nil +} + +// FundingTxAvailable is an empty method that an assembler can implement to +// signal to callers that its able to provide the funding transaction for the +// channel via the intent it returns. +// +// NOTE: This method is a part of the FundingTxAssembler interface. +func (p *PsbtAssembler) FundingTxAvailable() {} + +// A compile-time assertion to ensure PsbtAssembler meets the Assembler +// interface. +var _ Assembler = (*PsbtAssembler)(nil) + +// sumUtxoInputValues tries to extract the sum of all inputs specified in the +// UTXO fields of the PSBT. An error is returned if an input is specified that +// does not contain any UTXO information. +func sumUtxoInputValues(packet *psbt.Packet) (int64, error) { + // We take the TX ins of the unsigned TX as the truth for how many + // inputs there should be, as the fields in the extra data part of the + // PSBT can be empty. + if len(packet.UnsignedTx.TxIn) != len(packet.Inputs) { + return 0, fmt.Errorf("TX input length doesn't match PSBT " + + "input length") + } + inputSum := int64(0) + for idx, in := range packet.Inputs { + switch { + case in.WitnessUtxo != nil: + // Witness UTXOs only need to reference the TxOut. + inputSum += in.WitnessUtxo.Value + + case in.NonWitnessUtxo != nil: + // Non-witness UTXOs reference to the whole transaction + // the UTXO resides in. + utxOuts := in.NonWitnessUtxo.TxOut + txIn := packet.UnsignedTx.TxIn[idx] + inputSum += utxOuts[txIn.PreviousOutPoint.Index].Value + + default: + return 0, fmt.Errorf("input %d has no UTXO information", + idx) + } + } + return inputSum, nil +} + +// txOutsEqual returns true if two transaction outputs are equal. +func txOutsEqual(out1, out2 *wire.TxOut) bool { + if out1 == nil || out2 == nil { + return out1 == out2 + } + return out1.Value == out2.Value && + bytes.Equal(out1.PkScript, out2.PkScript) +} + +// verifyOutputsEqual verifies that the two slices of transaction outputs are +// deep equal to each other. We do the length check and manual loop to provide +// better error messages to the user than just returning "not equal". +func verifyOutputsEqual(outs1, outs2 []*wire.TxOut) error { + if len(outs1) != len(outs2) { + return fmt.Errorf("number of outputs are different") + } + for idx, out := range outs1 { + // There is a byte slice in the output so we can't use the + // equality operator. + if !txOutsEqual(out, outs2[idx]) { + return fmt.Errorf("output %d is different", idx) + } + } + return nil +} + +// verifyInputPrevOutpointsEqual verifies that the previous outpoints of the +// two slices of transaction inputs are deep equal to each other. We do the +// length check and manual loop to provide better error messages to the user +// than just returning "not equal". +func verifyInputPrevOutpointsEqual(ins1, ins2 []*wire.TxIn) error { + if len(ins1) != len(ins2) { + return fmt.Errorf("number of inputs are different") + } + for idx, in := range ins1 { + if in.PreviousOutPoint != ins2[idx].PreviousOutPoint { + return fmt.Errorf("previous outpoint of input %d is "+ + "different", idx) + } + } + return nil +} diff --git a/lnwallet/chanfunding/psbt_assembler_test.go b/lnwallet/chanfunding/psbt_assembler_test.go new file mode 100644 index 000000000..5367ecdbe --- /dev/null +++ b/lnwallet/chanfunding/psbt_assembler_test.go @@ -0,0 +1,577 @@ +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 +}