From 42cce8eeff92f1fb775939e19d41cc105d12522c Mon Sep 17 00:00:00 2001 From: David Burkett Date: Mon, 14 Jul 2025 19:31:21 -0400 Subject: [PATCH 1/3] PSBTv2: Serialization fixes and improved signing logic --- ltcutil/psbt/extractor.go | 64 +++++- ltcutil/psbt/psbt.go | 53 +++-- ltcutil/psbt/signer.go | 41 +--- ltcutil/psbt/signer_test.go | 376 +++++++++++++++++++----------------- 4 files changed, 304 insertions(+), 230 deletions(-) diff --git a/ltcutil/psbt/extractor.go b/ltcutil/psbt/extractor.go index bf0b1af90e..1b84d057ba 100644 --- a/ltcutil/psbt/extractor.go +++ b/ltcutil/psbt/extractor.go @@ -68,11 +68,54 @@ func Extract(p *Packet) (*wire.MsgTx, error) { return finalTx, nil } +func ExtractUnsignedTx(p *Packet) (*wire.MsgTx, error) { + if p.PsbtVersion >= 2 { + tx := new(wire.MsgTx) + tx.Version = p.TxVersion + + // TODO: Compute actual lock time + if p.FallbackLocktime != nil { + tx.LockTime = *p.FallbackLocktime + } + + for _, pi := range p.Inputs { + if !pi.isMWEB() { + txin, err := extractTxIn(&pi, false) + if err != nil { + return nil, err + } + + tx.AddTxIn(txin) + } + } + + for _, output := range p.Outputs { + if !output.isMWEB() { + txout := wire.TxOut{Value: int64(output.Amount), PkScript: output.PKScript} + tx.AddTxOut(&txout) + } + } + + // TODO: Include MWEB + + return tx, nil + } else { + return p.UnsignedTx.Copy(), nil + } +} + func extractV2(p *Packet) (*wire.MsgTx, error) { tx := new(wire.MsgTx) + tx.Version = p.TxVersion + + // TODO: Compute actual lock time + if p.FallbackLocktime != nil { + tx.LockTime = *p.FallbackLocktime + } + for _, pi := range p.Inputs { if !pi.isMWEB() { - txin, err := extractTxIn(&pi) + txin, err := extractTxIn(&pi, true) if err != nil { return nil, err } @@ -157,21 +200,24 @@ func extractV2(p *Packet) (*wire.MsgTx, error) { return tx, nil } -func extractTxIn(pi *PInput) (*wire.TxIn, error) { +func extractTxIn(pi *PInput, includeSignature bool) (*wire.TxIn, error) { if pi.PrevoutHash == nil || pi.PrevoutIndex == nil { return nil, errors.New("input missing previous outpoint info") } var txin wire.TxIn txin.PreviousOutPoint = wire.OutPoint{Hash: *pi.PrevoutHash, Index: *pi.PrevoutIndex} - txin.SignatureScript = pi.FinalScriptSig - if pi.FinalScriptWitness != nil { - witness, err := extractTxWitness(pi.FinalScriptWitness) - if err != nil { - return nil, err - } - txin.Witness = witness + if includeSignature { + txin.SignatureScript = pi.FinalScriptSig + if pi.FinalScriptWitness != nil { + witness, err := extractTxWitness(pi.FinalScriptWitness) + if err != nil { + return nil, err + } + + txin.Witness = witness + } } txin.Sequence = 0xffffffff diff --git a/ltcutil/psbt/psbt.go b/ltcutil/psbt/psbt.go index a77a3260ba..519f7de478 100644 --- a/ltcutil/psbt/psbt.go +++ b/ltcutil/psbt/psbt.go @@ -12,10 +12,11 @@ import ( "encoding/base64" "encoding/binary" "errors" + "io" + "github.com/ltcsuite/ltcd/chaincfg/chainhash" "github.com/ltcsuite/ltcd/ltcutil/hdkeychain" "github.com/ltcsuite/ltcd/ltcutil/mweb/mw" - "io" "github.com/ltcsuite/ltcd/ltcutil" "github.com/ltcsuite/ltcd/wire" @@ -319,16 +320,6 @@ func NewFromRawBytes(r io.Reader, b64 bool) (*Packet, error) { // Next we parse the GLOBAL section. Parse all keys and break after separator for { - kvPair, err := getKVPair(r) - if err != nil { - return nil, err - } - - // If this is separator byte (nil kvPair), this section is done. - if kvPair == nil { - break - } - // According to BIP-0174, := must be unique per map if !globalKeys.addKey(kvPair.keyType, kvPair.keyData) { return nil, ErrDuplicateKey @@ -449,6 +440,16 @@ func NewFromRawBytes(r io.Reader, b64 bool) (*Packet, error) { unknownSlice = append(unknownSlice, newUnknown) } + + kvPair, err = getKVPair(r) + if err != nil { + return nil, err + } + + // If this is separator byte (nil kvPair), this section is done. + if kvPair == nil { + break + } } if psbtVersion == nil || txVersion == nil || inputCount == nil || outputCount == nil || kernelCount == nil { @@ -543,6 +544,13 @@ func (p *Packet) Serialize(w io.Writer) error { if err != nil { return err } + } else { + // Psbt Version + var psbtVersionBytes [4]byte + binary.LittleEndian.PutUint32(psbtVersionBytes[:], uint32(p.PsbtVersion)) + if err := serializeKVPairWithType(w, uint8(VersionType), nil, psbtVersionBytes[:]); err != nil { + return err + } } for _, extPubKey := range p.ExtPubKeys { @@ -801,8 +809,29 @@ func (p *Packet) getPrevOut(i int) (*wire.OutPoint, *chainhash.Hash) { pInput := p.Inputs[i] if pInput.PrevoutHash == nil || pInput.PrevoutIndex == nil { return nil, pInput.MwebOutputId - } + } // TODO: Return synthetic OutPoint? prevout := wire.OutPoint{Hash: *pInput.PrevoutHash, Index: *pInput.PrevoutIndex} return &prevout, nil } + +func (p *Packet) BuildTxOuts() []*wire.TxOut { + if p.PsbtVersion == 0 { + return p.UnsignedTx.TxOut + } + + txouts := make([]*wire.TxOut, len(p.Outputs)) + for idx, pOutput := range p.Outputs { + var txout wire.TxOut + txout.Value = int64(pOutput.Amount) + if pOutput.StealthAddress != nil { + pkScript := append(pOutput.StealthAddress.Scan[:], pOutput.StealthAddress.Spend[:]...) + copy(txout.PkScript[:], pkScript) + } else { + copy(txout.PkScript[:], pOutput.PKScript) + } + txouts[idx] = &txout + } + + return txouts +} diff --git a/ltcutil/psbt/signer.go b/ltcutil/psbt/signer.go index 37ac1372e8..bef1df308f 100644 --- a/ltcutil/psbt/signer.go +++ b/ltcutil/psbt/signer.go @@ -17,7 +17,6 @@ import ( "math/big" "github.com/ltcsuite/ltcd/chaincfg/chainhash" - "github.com/ltcsuite/ltcd/ltcutil/mweb" "github.com/ltcsuite/ltcd/ltcutil/mweb/mw" "github.com/ltcsuite/ltcd/txscript" "github.com/ltcsuite/ltcd/wire" @@ -524,11 +523,10 @@ func signMwebKernel(pk *PKernel) (*mw.BlindingFactor, *mw.SecretKey, error) { return blind, stealthKey, nil } -type AddressIndexLookupFunc func(keychain *mweb.Keychain, stealthAddress *mw.StealthAddress) *uint32 +type OutputKeyDerivationFunc func(spentOutputPk *mw.PublicKey, keyExchangePubKey *mw.PublicKey, sharedSecret *mw.SecretKey) (preBlind *mw.BlindingFactor, outputSpendKey *mw.SecretKey, err error) type BasicMwebInputSigner struct { - Keychain *mweb.Keychain - LookupAddressIndex AddressIndexLookupFunc + DeriveOutputKeys OutputKeyDerivationFunc } func (s BasicMwebInputSigner) SignMwebInput(features wire.MwebInputFeatureBit, spentOutputId chainhash.Hash, spentOutputPk mw.PublicKey, amount uint64, extraData []byte, keyExchangePubKey *mw.PublicKey, spentOutputSharedSecret *mw.SecretKey) (*MwebInputSignatureData, error) { @@ -536,30 +534,13 @@ func (s BasicMwebInputSigner) SignMwebInput(features wire.MwebInputFeatureBit, s return nil, errors.New("stealth key feature bit is required to ensure key safety") } - sharedSecret := spentOutputSharedSecret - if sharedSecret == nil { - if keyExchangePubKey == nil { - return nil, errors.New("key exchange pubkey or shared secret needed") - } - sharedSecretPk := keyExchangePubKey.Mul(s.Keychain.Scan) - sharedSecret = (*mw.SecretKey)(mw.Hashed(mw.HashTagDerive, sharedSecretPk[:])) + preBlind, outputSpendKey, err := s.DeriveOutputKeys(&spentOutputPk, keyExchangePubKey, spentOutputSharedSecret) + if err != nil { + return nil, err } - preBlind := (*mw.BlindingFactor)(mw.Hashed(mw.HashTagBlind, sharedSecret[:])) blind := mw.BlindSwitch(preBlind, amount) - addrB := spentOutputPk.Div((*mw.SecretKey)(mw.Hashed(mw.HashTagOutKey, sharedSecret[:]))) - addrA := addrB.Mul(s.Keychain.Scan) - address := mw.StealthAddress{Scan: addrA, Spend: addrB} - - addrIdx := s.LookupAddressIndex(s.Keychain, &address) - if addrIdx == nil { - return nil, errors.New("address not found") - } - - addrSpendKey := s.Keychain.SpendKey(*addrIdx) - outputSpendKey := addrSpendKey.Mul((*mw.SecretKey)(mw.Hashed(mw.HashTagOutKey, sharedSecret[:]))) - var ephemeralKey mw.SecretKey if _, err := rand.Read(ephemeralKey[:]); err != nil { return nil, err @@ -602,15 +583,3 @@ func (s BasicMwebInputSigner) SignMwebInput(features wire.MwebInputFeatureBit, s inputPubKey: inputPubKey, }, nil } - -func NaiveAddressLookup(keychain *mweb.Keychain, stealthAddress *mw.StealthAddress) *uint32 { - var addrIdx *uint32 - for i := uint32(0); i < uint32(1000); i++ { - iAddr := keychain.Address(i) - if iAddr.Equal(stealthAddress) { - addrIdx = &i - break - } - } - return addrIdx -} diff --git a/ltcutil/psbt/signer_test.go b/ltcutil/psbt/signer_test.go index 4a91f37199..df3b73e8d9 100644 --- a/ltcutil/psbt/signer_test.go +++ b/ltcutil/psbt/signer_test.go @@ -1,189 +1,219 @@ package psbt import ( - "encoding/binary" - "github.com/ltcsuite/ltcd/chaincfg/chainhash" - "github.com/ltcsuite/ltcd/ltcutil" - "github.com/ltcsuite/ltcd/ltcutil/mweb" - "github.com/ltcsuite/ltcd/ltcutil/mweb/mw" - "github.com/ltcsuite/ltcd/wire" - "lukechampine.com/blake3" - "math/big" - "testing" + "encoding/binary" + "errors" + "github.com/ltcsuite/ltcd/chaincfg/chainhash" + "github.com/ltcsuite/ltcd/ltcutil" + "github.com/ltcsuite/ltcd/ltcutil/mweb" + "github.com/ltcsuite/ltcd/ltcutil/mweb/mw" + "github.com/ltcsuite/ltcd/wire" + "lukechampine.com/blake3" + "math/big" + "testing" ) func generateUnsignedPInput(features wire.MwebInputFeatureBit, stealthAddress mw.StealthAddress) *PInput { - amount := ltcutil.Amount(123456) - senderKey, _ := mw.NewSecretKey() - - // Generate 128-bit secret nonce 'n' = Hash128(T_nonce, sender_privkey) - n := new(big.Int).SetBytes(mw.Hashed(mw.HashTagNonce, senderKey[:])[:16]) - - // Calculate unique sending key 's' = H(T_send, A, B, v, n) - h := blake3.New(32, nil) - _ = binary.Write(h, binary.LittleEndian, mw.HashTagSendKey) - _, _ = h.Write(stealthAddress.A()[:]) - _, _ = h.Write(stealthAddress.B()[:]) - _ = binary.Write(h, binary.LittleEndian, uint64(amount)) - _, _ = h.Write(n.FillBytes(make([]byte, 16))) - s := (*mw.SecretKey)(h.Sum(nil)) - - // Derive shared secret 't' = H(T_derive, s*A) - sA := stealthAddress.A().Mul(s) - t := (*mw.SecretKey)(mw.Hashed(mw.HashTagDerive, sA[:])) - - // Construct one-time public key for receiver 'Ko' = H(T_outkey, t)*B - Ko := stealthAddress.B().Mul((*mw.SecretKey)(mw.Hashed(mw.HashTagOutKey, t[:]))) - - // Key exchange public key 'Ke' = s*B - Ke := stealthAddress.B().Mul(s) - - // Calc blinding factor and mask nonce and amount - mask := mw.OutputMaskFromShared(t) - blind := mw.BlindSwitch(mask.Blind, uint64(amount)) - - // Commitment 'C' = r*G + v*H - outputCommit := mw.NewCommitment(blind, uint64(amount)) - - var extradata []byte - if features&wire.MwebInputExtraDataFeatureBit > 0 { - extradata = []byte{0xaa, 0xbb, 0xcc} - } - - var outputId chainhash.Hash - tmp, _ := mw.NewSecretKey() - copy(outputId[:], tmp[:]) - - pi := PInput{ - MwebOutputId: &outputId, - MwebFeatures: &features, - MwebAmount: &amount, - MwebCommit: outputCommit, - MwebOutputPubkey: Ko, - MwebSharedSecret: nil, - MwebKeyExchangePubkey: Ke, - MwebExtraData: extradata, - } - return &pi + amount := ltcutil.Amount(123456) + senderKey, _ := mw.NewSecretKey() + + // Generate 128-bit secret nonce 'n' = Hash128(T_nonce, sender_privkey) + n := new(big.Int).SetBytes(mw.Hashed(mw.HashTagNonce, senderKey[:])[:16]) + + // Calculate unique sending key 's' = H(T_send, A, B, v, n) + h := blake3.New(32, nil) + _ = binary.Write(h, binary.LittleEndian, mw.HashTagSendKey) + _, _ = h.Write(stealthAddress.A()[:]) + _, _ = h.Write(stealthAddress.B()[:]) + _ = binary.Write(h, binary.LittleEndian, uint64(amount)) + _, _ = h.Write(n.FillBytes(make([]byte, 16))) + s := (*mw.SecretKey)(h.Sum(nil)) + + // Derive shared secret 't' = H(T_derive, s*A) + sA := stealthAddress.A().Mul(s) + t := (*mw.SecretKey)(mw.Hashed(mw.HashTagDerive, sA[:])) + + // Construct one-time public key for receiver 'Ko' = H(T_outkey, t)*B + Ko := stealthAddress.B().Mul((*mw.SecretKey)(mw.Hashed(mw.HashTagOutKey, t[:]))) + + // Key exchange public key 'Ke' = s*B + Ke := stealthAddress.B().Mul(s) + + // Calc blinding factor and mask nonce and amount + mask := mw.OutputMaskFromShared(t) + blind := mw.BlindSwitch(mask.Blind, uint64(amount)) + + // Commitment 'C' = r*G + v*H + outputCommit := mw.NewCommitment(blind, uint64(amount)) + + var extradata []byte + if features&wire.MwebInputExtraDataFeatureBit > 0 { + extradata = []byte{0xaa, 0xbb, 0xcc} + } + + var outputId chainhash.Hash + tmp, _ := mw.NewSecretKey() + copy(outputId[:], tmp[:]) + + pi := PInput{ + MwebOutputId: &outputId, + MwebFeatures: &features, + MwebAmount: &amount, + MwebCommit: outputCommit, + MwebOutputPubkey: Ko, + MwebSharedSecret: nil, + MwebKeyExchangePubkey: Ke, + MwebExtraData: extradata, + } + return &pi } func generateUnsignedPOutput(features wire.MwebOutputMessageFeatureBit) *POutput { - var extradata []byte - if features&wire.MwebOutputMessageExtraDataFeatureBit > 0 { - extradata = []byte{0xaa, 0xbb, 0xcc} - } - - amount := ltcutil.Amount(345678) - - scanKey, _ := mw.NewSecretKey() - spendKey, _ := mw.NewSecretKey() - stealthAddress := mw.StealthAddress{Scan: scanKey.PubKey(), Spend: spendKey.PubKey()} - - po := POutput{ - Amount: amount, - StealthAddress: &stealthAddress, - OutputCommit: nil, - MwebFeatures: &features, - SenderPubkey: nil, - OutputPubkey: nil, - MwebStandardFields: nil, - RangeProof: nil, - MwebSignature: nil, - MwebExtraData: extradata, - } - return &po + var extradata []byte + if features&wire.MwebOutputMessageExtraDataFeatureBit > 0 { + extradata = []byte{0xaa, 0xbb, 0xcc} + } + + amount := ltcutil.Amount(345678) + + scanKey, _ := mw.NewSecretKey() + spendKey, _ := mw.NewSecretKey() + stealthAddress := mw.StealthAddress{Scan: scanKey.PubKey(), Spend: spendKey.PubKey()} + + po := POutput{ + Amount: amount, + StealthAddress: &stealthAddress, + OutputCommit: nil, + MwebFeatures: &features, + SenderPubkey: nil, + OutputPubkey: nil, + MwebStandardFields: nil, + RangeProof: nil, + MwebSignature: nil, + MwebExtraData: extradata, + } + return &po } func generateUnsignedPKernel(features wire.MwebKernelFeatureBit) *PKernel { - var fee *ltcutil.Amount - if features&wire.MwebKernelFeeFeatureBit > 0 { - fee_amount := ltcutil.Amount(10000) - fee = &fee_amount - } - var peginAmount *ltcutil.Amount - if features&wire.MwebKernelPeginFeatureBit > 0 { - pegin := ltcutil.Amount(20000) - peginAmount = &pegin - } - var lockHeight *int32 - if features&wire.MwebKernelHeightLockFeatureBit > 0 { - height := int32(40000) - lockHeight = &height - } - var extradata []byte - if features&wire.MwebKernelExtraDataFeatureBit > 0 { - extradata = []byte{0xab, 0xcd, 0xef} - } - var pegouts []*wire.TxOut - if features&wire.MwebKernelPegoutFeatureBit > 0 { - pegouts = []*wire.TxOut{ - { - Value: 100000, - PkScript: []byte{0x76, 0xa9, 0x14, 0x20, 0x88, 0xac}, // basic P2PKH - }, - { - Value: 2000000, - PkScript: []byte{0x76, 0xa9, 0x14, 0x20, 0x88, 0xac}, // basic P2PKH - }, - } - } - - pk := PKernel{ - Features: &features, - ExcessCommitment: nil, - StealthExcess: nil, - Fee: fee, - PeginAmount: peginAmount, - LockHeight: lockHeight, - ExtraData: extradata, - Signature: nil, - PegOuts: pegouts, - Unknowns: nil, - } - return &pk + var fee *ltcutil.Amount + if features&wire.MwebKernelFeeFeatureBit > 0 { + fee_amount := ltcutil.Amount(10000) + fee = &fee_amount + } + var peginAmount *ltcutil.Amount + if features&wire.MwebKernelPeginFeatureBit > 0 { + pegin := ltcutil.Amount(20000) + peginAmount = &pegin + } + var lockHeight *int32 + if features&wire.MwebKernelHeightLockFeatureBit > 0 { + height := int32(40000) + lockHeight = &height + } + var extradata []byte + if features&wire.MwebKernelExtraDataFeatureBit > 0 { + extradata = []byte{0xab, 0xcd, 0xef} + } + var pegouts []*wire.TxOut + if features&wire.MwebKernelPegoutFeatureBit > 0 { + pegouts = []*wire.TxOut{ + { + Value: 100000, + PkScript: []byte{0x76, 0xa9, 0x14, 0x20, 0x88, 0xac}, // basic P2PKH + }, + { + Value: 2000000, + PkScript: []byte{0x76, 0xa9, 0x14, 0x20, 0x88, 0xac}, // basic P2PKH + }, + } + } + + pk := PKernel{ + Features: &features, + ExcessCommitment: nil, + StealthExcess: nil, + Fee: fee, + PeginAmount: peginAmount, + LockHeight: lockHeight, + ExtraData: extradata, + Signature: nil, + PegOuts: pegouts, + Unknowns: nil, + } + return &pk } func TestSignMwebComponents(t *testing.T) { - scanKey, _ := mw.NewSecretKey() - spendKey, _ := mw.NewSecretKey() - mwebKeychain := mweb.Keychain{Scan: scanKey, Spend: spendKey} - - inputFeatures := wire.MwebInputStealthKeyFeatureBit - pi := generateUnsignedPInput(inputFeatures, *mwebKeychain.Address(uint32(10))) - - outputFeatures := wire.MwebOutputMessageStandardFieldsFeatureBit - po := generateUnsignedPOutput(outputFeatures) - - kernelFeatures := wire.MwebKernelStealthExcessFeatureBit | wire.MwebKernelFeeFeatureBit - pk := generateUnsignedPKernel(kernelFeatures) - - packet := &Packet{ - PsbtVersion: 2, - MwebTxOffset: nil, - MwebStealthOffset: nil, - Inputs: []PInput{*pi}, - Outputs: []POutput{*po}, - Kernels: []PKernel{*pk}, - } - - mwebInputSigner := BasicMwebInputSigner{ - Keychain: &mwebKeychain, - LookupAddressIndex: NaiveAddressLookup, - } - signer, err := NewSigner(packet, mwebInputSigner) - if err != nil { - t.Fatalf("NewSigner failed: %v", err) - } - - outcome, err := signer.SignMwebComponents() - if outcome != SignSuccesful || err != nil { - t.Fatalf("SignMwebComponents failed: %v", err) - } - - tx, err := Extract(packet) - if tx == nil || err != nil { - t.Fatalf("Extract failed: %v", err) - } - - // TODO(dburkett) Verify all signatures. + masterScanKey, _ := mw.NewSecretKey() + masterSpendKey, _ := mw.NewSecretKey() + mwebKeychain := mweb.Keychain{Scan: masterScanKey, Spend: masterSpendKey} + + inputFeatures := wire.MwebInputStealthKeyFeatureBit + addrIdx := uint32(10) + pi := generateUnsignedPInput(inputFeatures, *mwebKeychain.Address(addrIdx)) + + outputFeatures := wire.MwebOutputMessageStandardFieldsFeatureBit + po := generateUnsignedPOutput(outputFeatures) + + kernelFeatures := wire.MwebKernelStealthExcessFeatureBit | wire.MwebKernelFeeFeatureBit + pk := generateUnsignedPKernel(kernelFeatures) + + packet := &Packet{ + PsbtVersion: 2, + MwebTxOffset: nil, + MwebStealthOffset: nil, + Inputs: []PInput{*pi}, + Outputs: []POutput{*po}, + Kernels: []PKernel{*pk}, + } + + deriveOutputKeys := func(spentOutputPk *mw.PublicKey, keyExchangePubKey *mw.PublicKey, spentOutputSharedSecret *mw.SecretKey) (*mw.BlindingFactor, *mw.SecretKey, error) { + var preBlind *mw.BlindingFactor + var outputSpendKey *mw.SecretKey + + sharedSecret := spentOutputSharedSecret + if sharedSecret == nil { + if keyExchangePubKey == nil { + return nil, nil, errors.New("key exchange pubkey or shared secret needed") + } + sharedSecretPk := keyExchangePubKey.Mul(mwebKeychain.Scan) + sharedSecret = (*mw.SecretKey)(mw.Hashed(mw.HashTagDerive, sharedSecretPk[:])) + } + + addrB := spentOutputPk.Div((*mw.SecretKey)(mw.Hashed(mw.HashTagOutKey, sharedSecret[:]))) + addrA := addrB.Mul(mwebKeychain.Scan) + address := mw.StealthAddress{Scan: addrA, Spend: addrB} + if !address.Equal(mwebKeychain.Address(addrIdx)) { + return nil, nil, errors.New("address doesn't match") + } + + addrSpendSecret := mwebKeychain.SpendKey(addrIdx) + + // Calculate pre-blind and output spend key + preBlind = (*mw.BlindingFactor)(mw.Hashed(mw.HashTagBlind, sharedSecret[:])) + outputSpendKey = addrSpendSecret.Mul((*mw.SecretKey)(mw.Hashed(mw.HashTagOutKey, sharedSecret[:]))) + + return preBlind, outputSpendKey, nil + } + + mwebInputSigner := BasicMwebInputSigner{ + DeriveOutputKeys: deriveOutputKeys, + } + signer, err := NewSigner(packet, mwebInputSigner) + if err != nil { + t.Fatalf("NewSigner failed: %v", err) + } + + outcome, err := signer.SignMwebComponents() + if outcome != SignSuccesful || err != nil { + t.Fatalf("SignMwebComponents failed: %v", err) + } + + tx, err := Extract(packet) + if tx == nil || err != nil { + t.Fatalf("Extract failed: %v", err) + } + + // TODO(dburkett) Verify all signatures. } From 0edd7be385d216489da8b891b10080c7f37b62f4 Mon Sep 17 00:00:00 2001 From: David Burkett Date: Thu, 17 Jul 2025 15:15:11 -0400 Subject: [PATCH 2/3] small fixes and cleanup --- ltcutil/psbt/extractor.go | 4 +++ ltcutil/psbt/signer.go | 72 +++++++++++---------------------------- 2 files changed, 23 insertions(+), 53 deletions(-) diff --git a/ltcutil/psbt/extractor.go b/ltcutil/psbt/extractor.go index 1b84d057ba..c4fc49c812 100644 --- a/ltcutil/psbt/extractor.go +++ b/ltcutil/psbt/extractor.go @@ -40,6 +40,9 @@ func Extract(p *Packet) (*wire.MsgTx, error) { // First, we'll make a copy of the underlying unsigned transaction (the // initial template) so we don't mutate it during our activates below. finalTx := p.UnsignedTx.Copy() + if finalTx == nil { + return nil, ErrInvalidPsbtFormat + } // For each input, we'll now populate any relevant witness and // sigScript data. @@ -326,6 +329,7 @@ func extractMwebOutput(po *POutput) (*wire.MwebOutput, error) { mwebOutput := &wire.MwebOutput{ Commitment: *po.OutputCommit, SenderPubKey: *po.SenderPubkey, + ReceiverPubKey: *po.OutputPubkey, Message: outputMessage, RangeProof: &rangeProof, RangeProofHash: blake3.Sum256(rangeProof[:]), diff --git a/ltcutil/psbt/signer.go b/ltcutil/psbt/signer.go index bef1df308f..2e952b1975 100644 --- a/ltcutil/psbt/signer.go +++ b/ltcutil/psbt/signer.go @@ -335,21 +335,11 @@ func signMwebOutput(output *POutput) (*mw.BlindingFactor, *mw.SecretKey, error) // Calculate unique sending key 's' = H(T_send, A, B, v, n) h := blake3.New(32, nil) - if err := binary.Write(h, binary.LittleEndian, mw.HashTagSendKey); err != nil { - return nil, nil, err - } - if _, err := h.Write(address.A()[:]); err != nil { - return nil, nil, err - } - if _, err := h.Write(address.B()[:]); err != nil { - return nil, nil, err - } - if err := binary.Write(h, binary.LittleEndian, amount); err != nil { - return nil, nil, err - } - if _, err := h.Write(n.FillBytes(make([]byte, 16))); err != nil { - return nil, nil, err - } + _ = binary.Write(h, binary.LittleEndian, mw.HashTagSendKey) + _, _ = h.Write(address.A()[:]) + _, _ = h.Write(address.B()[:]) + _ = binary.Write(h, binary.LittleEndian, amount) + _, _ = h.Write(n.FillBytes(make([]byte, 16))) s := (*mw.SecretKey)(h.Sum(nil)) // Derive shared secret 't' = H(T_derive, s*A) @@ -397,21 +387,11 @@ func signMwebOutput(output *POutput) (*mw.BlindingFactor, *mw.SecretKey, error) // Sign the output h = blake3.New(32, nil) - if _, err := h.Write(outputCommit[:]); err != nil { - return nil, nil, err - } - if _, err := h.Write(Ks[:]); err != nil { - return nil, nil, err - } - if _, err := h.Write(Ko[:]); err != nil { - return nil, nil, err - } - if _, err := h.Write(message.Hash()[:]); err != nil { - return nil, nil, err - } - if _, err := h.Write(rangeProofHash[:]); err != nil { - return nil, nil, err - } + _, _ = h.Write(outputCommit[:]) + _, _ = h.Write(Ks[:]) + _, _ = h.Write(Ko[:]) + _, _ = h.Write(message.Hash()[:]) + _, _ = h.Write(rangeProofHash[:]) signature := mw.Sign(senderKey, h.Sum(nil)) var encryptedNonce [16]byte @@ -492,13 +472,8 @@ func signMwebKernel(pk *PKernel) (*mw.BlindingFactor, *mw.SecretKey, error) { stealthExcess = *(stealthKey).PubKey() h := blake3.New(32, nil) - if _, err := h.Write(kernelExcess.PubKey()[:]); err != nil { - return nil, nil, err - } - if _, err := h.Write(stealthExcess[:]); err != nil { - return nil, nil, err - } - + _, _ = h.Write(kernelExcess.PubKey()[:]) + _, _ = h.Write(stealthExcess[:]) sigKey = sigKey.Mul((*mw.SecretKey)(h.Sum(nil))). Add(stealthKey) } @@ -542,7 +517,7 @@ func (s BasicMwebInputSigner) SignMwebInput(features wire.MwebInputFeatureBit, s blind := mw.BlindSwitch(preBlind, amount) var ephemeralKey mw.SecretKey - if _, err := rand.Read(ephemeralKey[:]); err != nil { + if _, err = rand.Read(ephemeralKey[:]); err != nil { return nil, err } @@ -550,12 +525,8 @@ func (s BasicMwebInputSigner) SignMwebInput(features wire.MwebInputFeatureBit, s // Hash keys (K_i||K_o) h := blake3.New(32, nil) - if _, err := h.Write(inputPubKey[:]); err != nil { - return nil, err - } - if _, err := h.Write(spentOutputPk[:]); err != nil { - return nil, err - } + _, _ = h.Write(inputPubKey[:]) + _, _ = h.Write(spentOutputPk[:]) keyHash := (*mw.SecretKey)(h.Sum(nil)) // Calculate aggregated key k_agg = k_i + HASH(K_i||K_o) * k_o @@ -563,16 +534,11 @@ func (s BasicMwebInputSigner) SignMwebInput(features wire.MwebInputFeatureBit, s // Hash message h = blake3.New(32, nil) - if err := binary.Write(h, binary.LittleEndian, features); err != nil { - return nil, err - } - if _, err := h.Write(spentOutputId[:]); err != nil { - return nil, err - } + _ = binary.Write(h, binary.LittleEndian, features) + _, _ = h.Write(spentOutputId[:]) + if features&wire.MwebInputExtraDataFeatureBit > 0 { - if err := wire.WriteVarBytes(h, 0, extraData); err != nil { - return nil, err - } + _ = wire.WriteVarBytes(h, 0, extraData) } msgHash := h.Sum(nil) From 8000c1ad7bb905b339ea3bd71b204d119a4f70ae Mon Sep 17 00:00:00 2001 From: David Burkett Date: Thu, 17 Jul 2025 16:35:09 -0400 Subject: [PATCH 3/3] PSBTv2: Fix pegin signing --- ltcutil/mweb/txbuilder.go | 5 +- ltcutil/psbt/extractor.go | 25 +- ltcutil/psbt/finalizer.go | 998 +++++++++++++++++++------------------- 3 files changed, 524 insertions(+), 504 deletions(-) diff --git a/ltcutil/mweb/txbuilder.go b/ltcutil/mweb/txbuilder.go index 39284c6c15..6a8b299619 100644 --- a/ltcutil/mweb/txbuilder.go +++ b/ltcutil/mweb/txbuilder.go @@ -8,6 +8,7 @@ import ( "math/big" "sort" + "github.com/ltcsuite/ltcd/chaincfg/chainhash" "github.com/ltcsuite/ltcd/ltcutil/mweb/mw" "github.com/ltcsuite/ltcd/txscript" "github.com/ltcsuite/ltcd/wire" @@ -284,9 +285,9 @@ func createKernel(blind, stealthBlind *mw.BlindingFactor, return k } -func NewPegin(value uint64, kernel *wire.MwebKernel) *wire.TxOut { +func NewPegin(value uint64, kernelHash *chainhash.Hash) *wire.TxOut { script, _ := txscript.NewScriptBuilder(). AddOp(txscript.MwebPeginWitnessVersion + txscript.OP_1 - 1). - AddData(kernel.Hash()[:]).Script() + AddData(kernelHash[:]).Script() return wire.NewTxOut(int64(value), script) } diff --git a/ltcutil/psbt/extractor.go b/ltcutil/psbt/extractor.go index c4fc49c812..380f6b5268 100644 --- a/ltcutil/psbt/extractor.go +++ b/ltcutil/psbt/extractor.go @@ -15,6 +15,8 @@ import ( "math/big" "sort" + "github.com/ltcsuite/ltcd/chaincfg/chainhash" + "github.com/ltcsuite/ltcd/ltcutil/mweb" "github.com/ltcsuite/ltcd/ltcutil/mweb/mw" "github.com/ltcsuite/ltcd/txscript" "github.com/ltcsuite/ltcd/wire" @@ -92,14 +94,24 @@ func ExtractUnsignedTx(p *Packet) (*wire.MsgTx, error) { } } - for _, output := range p.Outputs { - if !output.isMWEB() { - txout := wire.TxOut{Value: int64(output.Amount), PkScript: output.PKScript} + for _, po := range p.Outputs { + if !po.isMWEB() { + txout := wire.TxOut{Value: int64(po.Amount), PkScript: po.PKScript} tx.AddTxOut(&txout) } } - // TODO: Include MWEB + for _, pk := range p.Kernels { + if pk.PeginAmount != nil { + kernelHash := &chainhash.Hash{} + kernel, _ := extractKernel(&pk) + if kernel != nil { + kernelHash = kernel.Hash() + } + pegin := mweb.NewPegin(uint64(*pk.PeginAmount), kernelHash) + tx.AddTxOut(pegin) + } + } return tx, nil } else { @@ -171,6 +183,11 @@ func extractV2(p *Packet) (*wire.MsgTx, error) { return nil, err } kernels = append(kernels, kernel) + + if kernel.Pegin > 0 { + pegin := mweb.NewPegin(kernel.Pegin, kernel.Hash()) + tx.AddTxOut(pegin) + } } // Sort components before assembling txBody diff --git a/ltcutil/psbt/finalizer.go b/ltcutil/psbt/finalizer.go index e4c338bd75..79f2829bf6 100644 --- a/ltcutil/psbt/finalizer.go +++ b/ltcutil/psbt/finalizer.go @@ -12,181 +12,183 @@ package psbt // multisig and no other custom script. import ( - "bytes" - "fmt" + "bytes" + "fmt" - "github.com/ltcsuite/ltcd/btcec/v2/schnorr" - "github.com/ltcsuite/ltcd/txscript" - "github.com/ltcsuite/ltcd/wire" + "github.com/ltcsuite/ltcd/btcec/v2/schnorr" + "github.com/ltcsuite/ltcd/txscript" + "github.com/ltcsuite/ltcd/wire" ) // isFinalizableWitnessInput returns true if the target input is a witness UTXO // that can be finalized. func isFinalizableWitnessInput(pInput *PInput) bool { - pkScript := pInput.WitnessUtxo.PkScript - - switch { - // If this is a native witness output, then we require both - // the witness script, but not a redeem script. - case txscript.IsWitnessProgram(pkScript): - switch { - case txscript.IsPayToWitnessScriptHash(pkScript): - if pInput.WitnessScript == nil || - pInput.RedeemScript != nil { - - return false - } - - case txscript.IsPayToTaproot(pkScript): - if pInput.TaprootKeySpendSig == nil && - pInput.TaprootScriptSpendSig == nil { - - return false - } - - // For each of the script spend signatures we need a - // corresponding tap script leaf with the control block. - for _, sig := range pInput.TaprootScriptSpendSig { - _, err := findLeafScript(pInput, sig.LeafHash) - if err != nil { - return false - } - } - - default: - // A P2WKH output on the other hand doesn't need - // neither a witnessScript or redeemScript. - if pInput.WitnessScript != nil || - pInput.RedeemScript != nil { - - return false - } - } - - // For nested P2SH inputs, we verify that a witness script is known. - case txscript.IsPayToScriptHash(pkScript): - if pInput.RedeemScript == nil { - return false - } - - // If this is a nested P2SH input, then it must also have a - // witness script, while we don't need one for P2WKH. - if txscript.IsPayToWitnessScriptHash(pInput.RedeemScript) { - if pInput.WitnessScript == nil { - return false - } - } else if txscript.IsPayToWitnessPubKeyHash(pInput.RedeemScript) { - if pInput.WitnessScript != nil { - return false - } - } else { - // unrecognized type - return false - } - - // If this isn't a nested P2SH output or a native witness output, then - // we can't finalize this input as we don't understand it. - default: - return false - } - - return true + pkScript := pInput.WitnessUtxo.PkScript + + switch { + // If this is a native witness output, then we require both + // the witness script, but not a redeem script. + case txscript.IsWitnessProgram(pkScript): + switch { + case txscript.IsPayToWitnessScriptHash(pkScript): + if pInput.WitnessScript == nil || + pInput.RedeemScript != nil { + + return false + } + + case txscript.IsPayToTaproot(pkScript): + if pInput.TaprootKeySpendSig == nil && + pInput.TaprootScriptSpendSig == nil { + + return false + } + + // For each of the script spend signatures we need a + // corresponding tap script leaf with the control block. + for _, sig := range pInput.TaprootScriptSpendSig { + _, err := findLeafScript(pInput, sig.LeafHash) + if err != nil { + return false + } + } + + default: + // A P2WKH output on the other hand doesn't need + // neither a witnessScript or redeemScript. + if pInput.WitnessScript != nil || + pInput.RedeemScript != nil { + + return false + } + } + + // For nested P2SH inputs, we verify that a witness script is known. + case txscript.IsPayToScriptHash(pkScript): + if pInput.RedeemScript == nil { + return false + } + + // If this is a nested P2SH input, then it must also have a + // witness script, while we don't need one for P2WKH. + if txscript.IsPayToWitnessScriptHash(pInput.RedeemScript) { + if pInput.WitnessScript == nil { + return false + } + } else if txscript.IsPayToWitnessPubKeyHash(pInput.RedeemScript) { + if pInput.WitnessScript != nil { + return false + } + } else { + // unrecognized type + return false + } + + // If this isn't a nested P2SH output or a native witness output, then + // we can't finalize this input as we don't understand it. + default: + return false + } + + return true } // isFinalizableLegacyInput returns true of the passed input a legacy input // (non-witness) that can be finalized. func isFinalizableLegacyInput(p *Packet, pInput *PInput, inIndex int) bool { - // If the input has a witness, then it's invalid. - if pInput.WitnessScript != nil { - return false - } - - // Otherwise, we'll verify that we only have a RedeemScript if the prev - // output script is P2SH. - outIndex := p.UnsignedTx.TxIn[inIndex].PreviousOutPoint.Index - if txscript.IsPayToScriptHash(pInput.NonWitnessUtxo.TxOut[outIndex].PkScript) { - if pInput.RedeemScript == nil { - return false - } - } else { - if pInput.RedeemScript != nil { - return false - } - } - - return true + // If the input has a witness, then it's invalid. + if pInput.WitnessScript != nil { + return false + } + + // Otherwise, we'll verify that we only have a RedeemScript if the prev + // output script is P2SH. + outIndex := p.UnsignedTx.TxIn[inIndex].PreviousOutPoint.Index + if txscript.IsPayToScriptHash(pInput.NonWitnessUtxo.TxOut[outIndex].PkScript) { + if pInput.RedeemScript == nil { + return false + } + } else { + if pInput.RedeemScript != nil { + return false + } + } + + return true } // isFinalizable checks whether the structure of the entry for the input of the // psbt.Packet at index inIndex contains sufficient information to finalize // this input. func isFinalizable(p *Packet, inIndex int) bool { - pInput := p.Inputs[inIndex] - - // The input cannot be finalized without any signatures. - if pInput.PartialSigs == nil && pInput.TaprootKeySpendSig == nil && - pInput.TaprootScriptSpendSig == nil { - - return false - } - - // For an input to be finalized, we'll one of two possible top-level - // UTXOs present. Each UTXO type has a distinct set of requirements to - // be considered finalized. - switch { - - // A witness input must be either native P2WSH or nested P2SH with all - // relevant sigScript or witness data populated. - case pInput.WitnessUtxo != nil: - if !isFinalizableWitnessInput(&pInput) { - return false - } - - case pInput.NonWitnessUtxo != nil: - if !isFinalizableLegacyInput(p, &pInput, inIndex) { - return false - } - - // If neither a known UTXO type isn't present at all, then we'll - // return false as we need one of them. - default: - return false - } - - return true + pInput := p.Inputs[inIndex] + + // The input cannot be finalized without any signatures. + if pInput.PartialSigs == nil && pInput.TaprootKeySpendSig == nil && + pInput.TaprootScriptSpendSig == nil { + + return false + } + + // For an input to be finalized, we'll one of two possible top-level + // UTXOs present. Each UTXO type has a distinct set of requirements to + // be considered finalized. + switch { + + // A witness input must be either native P2WSH or nested P2SH with all + // relevant sigScript or witness data populated. + case pInput.WitnessUtxo != nil: + if !isFinalizableWitnessInput(&pInput) { + return false + } + + case pInput.NonWitnessUtxo != nil: + if !isFinalizableLegacyInput(p, &pInput, inIndex) { + return false + } + + // If neither a known UTXO type isn't present at all, then we'll + // return false as we need one of them. + default: + return false + } + + return true } // MaybeFinalize attempts to finalize the input at index inIndex in the PSBT p, // returning true with no error if it succeeds, OR if the input has already // been finalized. func MaybeFinalize(p *Packet, inIndex int) (bool, error) { - pInput := p.Inputs[inIndex] - if pInput.isFinalized() { - return true, nil - } + pInput := p.Inputs[inIndex] + if pInput.isFinalized() { + return true, nil + } - if !isFinalizable(p, inIndex) { - return false, ErrNotFinalizable - } + if !isFinalizable(p, inIndex) { + return false, ErrNotFinalizable + } - if err := Finalize(p, inIndex); err != nil { - return false, err - } + if err := Finalize(p, inIndex); err != nil { + return false, err + } - return true, nil + return true, nil } // MaybeFinalizeAll attempts to finalize all inputs of the psbt.Packet that are // not already finalized, and returns an error if it fails to do so. func MaybeFinalizeAll(p *Packet) error { - for i := range p.Inputs { - success, err := MaybeFinalize(p, i) - if err != nil || !success { - return err - } - } - - return nil + for i := range p.Inputs { + success, err := MaybeFinalize(p, i) + if err != nil || !success { + return err + } + } + + // TODO: Finalize MWEB components, which should remove everything except the extractable component fields + + return nil } // Finalize assumes that the provided psbt.Packet struct has all partial @@ -197,142 +199,142 @@ func MaybeFinalizeAll(p *Packet) error { // are left intact as they may be needed for validation (?). If there is any // invalid or incomplete data, an error is returned. func Finalize(p *Packet, inIndex int) error { - pInput := p.Inputs[inIndex] - - // Depending on the UTXO type, we either attempt to finalize it as a - // witness or legacy UTXO. - switch { - case pInput.WitnessUtxo != nil: - pkScript := pInput.WitnessUtxo.PkScript - - switch { - case txscript.IsPayToTaproot(pkScript): - if err := finalizeTaprootInput(p, inIndex); err != nil { - return err - } - - default: - if err := finalizeWitnessInput(p, inIndex); err != nil { - return err - } - } - - case pInput.NonWitnessUtxo != nil: - if err := finalizeNonWitnessInput(p, inIndex); err != nil { - return err - } - - default: - return ErrInvalidPsbtFormat - } - - // Before returning we sanity check the PSBT to ensure we don't extract - // an invalid transaction or produce an invalid intermediate state. - if err := p.SanityCheck(); err != nil { - return err - } - - return nil + pInput := p.Inputs[inIndex] + + // Depending on the UTXO type, we either attempt to finalize it as a + // witness or legacy UTXO. + switch { + case pInput.WitnessUtxo != nil: + pkScript := pInput.WitnessUtxo.PkScript + + switch { + case txscript.IsPayToTaproot(pkScript): + if err := finalizeTaprootInput(p, inIndex); err != nil { + return err + } + + default: + if err := finalizeWitnessInput(p, inIndex); err != nil { + return err + } + } + + case pInput.NonWitnessUtxo != nil: + if err := finalizeNonWitnessInput(p, inIndex); err != nil { + return err + } + + default: + return ErrInvalidPsbtFormat + } + + // Before returning we sanity check the PSBT to ensure we don't extract + // an invalid transaction or produce an invalid intermediate state. + if err := p.SanityCheck(); err != nil { + return err + } + + return nil } // finalizeNonWitnessInput attempts to create a PsbtInFinalScriptSig field for // the input at index inIndex, and removes all other fields except for the UTXO // field, for an input of type non-witness, or returns an error. func finalizeNonWitnessInput(p *Packet, inIndex int) error { - // If this input has already been finalized, then we'll return an error - // as we can't proceed. - if p.Inputs[inIndex].isFinalized() { - return ErrInputAlreadyFinalized - } - - // Our goal here is to construct a sigScript given the pubkey, - // signature (keytype 02), of which there might be multiple, and the - // redeem script field (keytype 04) if present (note, it is not present - // for p2pkh type inputs). - var sigScript []byte - - pInput := p.Inputs[inIndex] - containsRedeemScript := pInput.RedeemScript != nil - - var ( - pubKeys [][]byte - sigs [][]byte - ) - for _, ps := range pInput.PartialSigs { - pubKeys = append(pubKeys, ps.PubKey) - - sigOK := checkSigHashFlags(ps.Signature, &pInput) - if !sigOK { - return ErrInvalidSigHashFlags - } - - sigs = append(sigs, ps.Signature) - } - - // We have failed to identify at least 1 (sig, pub) pair in the PSBT, - // which indicates it was not ready to be finalized. As a result, we - // can't proceed. - if len(sigs) < 1 || len(pubKeys) < 1 { - return ErrNotFinalizable - } - - // If this input doesn't need a redeem script (P2PKH), then we'll - // construct a simple sigScript that's just the signature then the - // pubkey (OP_CHECKSIG). - var err error - if !containsRedeemScript { - // At this point, we should only have a single signature and - // pubkey. - if len(sigs) != 1 || len(pubKeys) != 1 { - return ErrNotFinalizable - } - - // In this case, our sigScript is just: . - builder := txscript.NewScriptBuilder() - builder.AddData(sigs[0]).AddData(pubKeys[0]) - sigScript, err = builder.Script() - if err != nil { - return err - } - } else { - // This is assumed p2sh multisig Given redeemScript and pubKeys - // we can decide in what order signatures must be appended. - orderedSigs, err := extractKeyOrderFromScript( - pInput.RedeemScript, pubKeys, sigs, - ) - if err != nil { - return err - } - - // At this point, we assume that this is a mult-sig input, so - // we construct our sigScript which looks something like this - // (mind the extra element for the extra multi-sig pop): - // * - // - // TODO(waxwing): the below is specific to the multisig case. - builder := txscript.NewScriptBuilder() - builder.AddOp(txscript.OP_FALSE) - for _, os := range orderedSigs { - builder.AddData(os) - } - builder.AddData(pInput.RedeemScript) - sigScript, err = builder.Script() - if err != nil { - return err - } - } - - // At this point, a sigScript has been constructed. Remove all fields - // other than non-witness utxo (00) and finaliscriptsig (07) - newInput := NewPsbtInput(pInput.NonWitnessUtxo, nil) - newInput.FinalScriptSig = sigScript - - // Overwrite the entry in the input list at the correct index. Note - // that this removes all the other entries in the list for this input - // index. - p.Inputs[inIndex] = *newInput - - return nil + // If this input has already been finalized, then we'll return an error + // as we can't proceed. + if p.Inputs[inIndex].isFinalized() { + return ErrInputAlreadyFinalized + } + + // Our goal here is to construct a sigScript given the pubkey, + // signature (keytype 02), of which there might be multiple, and the + // redeem script field (keytype 04) if present (note, it is not present + // for p2pkh type inputs). + var sigScript []byte + + pInput := p.Inputs[inIndex] + containsRedeemScript := pInput.RedeemScript != nil + + var ( + pubKeys [][]byte + sigs [][]byte + ) + for _, ps := range pInput.PartialSigs { + pubKeys = append(pubKeys, ps.PubKey) + + sigOK := checkSigHashFlags(ps.Signature, &pInput) + if !sigOK { + return ErrInvalidSigHashFlags + } + + sigs = append(sigs, ps.Signature) + } + + // We have failed to identify at least 1 (sig, pub) pair in the PSBT, + // which indicates it was not ready to be finalized. As a result, we + // can't proceed. + if len(sigs) < 1 || len(pubKeys) < 1 { + return ErrNotFinalizable + } + + // If this input doesn't need a redeem script (P2PKH), then we'll + // construct a simple sigScript that's just the signature then the + // pubkey (OP_CHECKSIG). + var err error + if !containsRedeemScript { + // At this point, we should only have a single signature and + // pubkey. + if len(sigs) != 1 || len(pubKeys) != 1 { + return ErrNotFinalizable + } + + // In this case, our sigScript is just: . + builder := txscript.NewScriptBuilder() + builder.AddData(sigs[0]).AddData(pubKeys[0]) + sigScript, err = builder.Script() + if err != nil { + return err + } + } else { + // This is assumed p2sh multisig Given redeemScript and pubKeys + // we can decide in what order signatures must be appended. + orderedSigs, err := extractKeyOrderFromScript( + pInput.RedeemScript, pubKeys, sigs, + ) + if err != nil { + return err + } + + // At this point, we assume that this is a mult-sig input, so + // we construct our sigScript which looks something like this + // (mind the extra element for the extra multi-sig pop): + // * + // + // TODO(waxwing): the below is specific to the multisig case. + builder := txscript.NewScriptBuilder() + builder.AddOp(txscript.OP_FALSE) + for _, os := range orderedSigs { + builder.AddData(os) + } + builder.AddData(pInput.RedeemScript) + sigScript, err = builder.Script() + if err != nil { + return err + } + } + + // At this point, a sigScript has been constructed. Remove all fields + // other than non-witness utxo (00) and finaliscriptsig (07) + newInput := NewPsbtInput(pInput.NonWitnessUtxo, nil) + newInput.FinalScriptSig = sigScript + + // Overwrite the entry in the input list at the correct index. Note + // that this removes all the other entries in the list for this input + // index. + p.Inputs[inIndex] = *newInput + + return nil } // finalizeWitnessInput attempts to create PsbtInFinalScriptSig field and @@ -340,234 +342,234 @@ func finalizeNonWitnessInput(p *Packet, inIndex int) error { // other fields except for the utxo field, for an input of type witness, or // returns an error. func finalizeWitnessInput(p *Packet, inIndex int) error { - // If this input has already been finalized, then we'll return an error - // as we can't proceed. - if p.Inputs[inIndex].isFinalized() { - return ErrInputAlreadyFinalized - } - - // Depending on the actual output type, we'll either populate a - // serializedWitness or a witness as well asa sigScript. - var ( - sigScript []byte - serializedWitness []byte - ) - - pInput := p.Inputs[inIndex] - - // First we'll validate and collect the pubkey+sig pairs from the set - // of partial signatures. - var ( - pubKeys [][]byte - sigs [][]byte - ) - for _, ps := range pInput.PartialSigs { - pubKeys = append(pubKeys, ps.PubKey) - - sigOK := checkSigHashFlags(ps.Signature, &pInput) - if !sigOK { - return ErrInvalidSigHashFlags - - } - - sigs = append(sigs, ps.Signature) - } - - // If at this point, we don't have any pubkey+sig pairs, then we bail - // as we can't proceed. - if len(sigs) == 0 || len(pubKeys) == 0 { - return ErrNotFinalizable - } - - containsRedeemScript := pInput.RedeemScript != nil - cointainsWitnessScript := pInput.WitnessScript != nil - - // If there's no redeem script, then we assume that this is native - // segwit input. - var err error - if !containsRedeemScript { - // If we have only a sigley pubkey+sig pair, and no witness - // script, then we assume this is a P2WKH input. - if len(pubKeys) == 1 && len(sigs) == 1 && - !cointainsWitnessScript { - - serializedWitness, err = writePKHWitness( - sigs[0], pubKeys[0], - ) - if err != nil { - return err - } - } else { - // Otherwise, we must have a witnessScript field, so - // we'll generate a valid multi-sig witness. - // - // NOTE: We tacitly assume multisig. - // - // TODO(roasbeef): need to add custom finalize for - // non-multisig P2WSH outputs (HTLCs, delay outputs, - // etc). - if !cointainsWitnessScript { - return ErrNotFinalizable - } - - serializedWitness, err = getMultisigScriptWitness( - pInput.WitnessScript, pubKeys, sigs, - ) - if err != nil { - return err - } - } - } else { - // Otherwise, we assume that this is a p2wsh multi-sig output, - // which is nested in a p2sh, or a p2wkh nested in a p2sh. - // - // In this case, we'll take the redeem script (the witness - // program in this case), and push it on the stack within the - // sigScript. - builder := txscript.NewScriptBuilder() - builder.AddData(pInput.RedeemScript) - sigScript, err = builder.Script() - if err != nil { - return err - } - - // If don't have a witness script, then we assume this is a - // nested p2wkh output. - if !cointainsWitnessScript { - // Assumed p2sh-p2wkh Here the witness is just (sig, - // pub) as for p2pkh case - if len(sigs) != 1 || len(pubKeys) != 1 { - return ErrNotFinalizable - } - - serializedWitness, err = writePKHWitness( - sigs[0], pubKeys[0], - ) - if err != nil { - return err - } - - } else { - // Otherwise, we assume that this is a p2wsh multi-sig, - // so we generate the proper witness. - serializedWitness, err = getMultisigScriptWitness( - pInput.WitnessScript, pubKeys, sigs, - ) - if err != nil { - return err - } - } - } - - // At this point, a witness has been constructed, and a sigScript (if - // nested; else it's []). Remove all fields other than witness utxo - // (01) and finalscriptsig (07), finalscriptwitness (08). - newInput := NewPsbtInput(nil, pInput.WitnessUtxo) - if len(sigScript) > 0 { - newInput.FinalScriptSig = sigScript - } - - newInput.FinalScriptWitness = serializedWitness - - // Finally, we overwrite the entry in the input list at the correct - // index. - p.Inputs[inIndex] = *newInput - return nil + // If this input has already been finalized, then we'll return an error + // as we can't proceed. + if p.Inputs[inIndex].isFinalized() { + return ErrInputAlreadyFinalized + } + + // Depending on the actual output type, we'll either populate a + // serializedWitness or a witness as well asa sigScript. + var ( + sigScript []byte + serializedWitness []byte + ) + + pInput := p.Inputs[inIndex] + + // First we'll validate and collect the pubkey+sig pairs from the set + // of partial signatures. + var ( + pubKeys [][]byte + sigs [][]byte + ) + for _, ps := range pInput.PartialSigs { + pubKeys = append(pubKeys, ps.PubKey) + + sigOK := checkSigHashFlags(ps.Signature, &pInput) + if !sigOK { + return ErrInvalidSigHashFlags + + } + + sigs = append(sigs, ps.Signature) + } + + // If at this point, we don't have any pubkey+sig pairs, then we bail + // as we can't proceed. + if len(sigs) == 0 || len(pubKeys) == 0 { + return ErrNotFinalizable + } + + containsRedeemScript := pInput.RedeemScript != nil + cointainsWitnessScript := pInput.WitnessScript != nil + + // If there's no redeem script, then we assume that this is native + // segwit input. + var err error + if !containsRedeemScript { + // If we have only a sigley pubkey+sig pair, and no witness + // script, then we assume this is a P2WKH input. + if len(pubKeys) == 1 && len(sigs) == 1 && + !cointainsWitnessScript { + + serializedWitness, err = writePKHWitness( + sigs[0], pubKeys[0], + ) + if err != nil { + return err + } + } else { + // Otherwise, we must have a witnessScript field, so + // we'll generate a valid multi-sig witness. + // + // NOTE: We tacitly assume multisig. + // + // TODO(roasbeef): need to add custom finalize for + // non-multisig P2WSH outputs (HTLCs, delay outputs, + // etc). + if !cointainsWitnessScript { + return ErrNotFinalizable + } + + serializedWitness, err = getMultisigScriptWitness( + pInput.WitnessScript, pubKeys, sigs, + ) + if err != nil { + return err + } + } + } else { + // Otherwise, we assume that this is a p2wsh multi-sig output, + // which is nested in a p2sh, or a p2wkh nested in a p2sh. + // + // In this case, we'll take the redeem script (the witness + // program in this case), and push it on the stack within the + // sigScript. + builder := txscript.NewScriptBuilder() + builder.AddData(pInput.RedeemScript) + sigScript, err = builder.Script() + if err != nil { + return err + } + + // If don't have a witness script, then we assume this is a + // nested p2wkh output. + if !cointainsWitnessScript { + // Assumed p2sh-p2wkh Here the witness is just (sig, + // pub) as for p2pkh case + if len(sigs) != 1 || len(pubKeys) != 1 { + return ErrNotFinalizable + } + + serializedWitness, err = writePKHWitness( + sigs[0], pubKeys[0], + ) + if err != nil { + return err + } + + } else { + // Otherwise, we assume that this is a p2wsh multi-sig, + // so we generate the proper witness. + serializedWitness, err = getMultisigScriptWitness( + pInput.WitnessScript, pubKeys, sigs, + ) + if err != nil { + return err + } + } + } + + // At this point, a witness has been constructed, and a sigScript (if + // nested; else it's []). Remove all fields other than witness utxo + // (01) and finalscriptsig (07), finalscriptwitness (08). + newInput := NewPsbtInput(nil, pInput.WitnessUtxo) + if len(sigScript) > 0 { + newInput.FinalScriptSig = sigScript + } + + newInput.FinalScriptWitness = serializedWitness + + // Finally, we overwrite the entry in the input list at the correct + // index. + p.Inputs[inIndex] = *newInput + return nil } // finalizeTaprootInput attempts to create PsbtInFinalScriptWitness field for // input at index inIndex, and removes all other fields except for the utxo // field, for an input of type p2tr, or returns an error. func finalizeTaprootInput(p *Packet, inIndex int) error { - // If this input has already been finalized, then we'll return an error - // as we can't proceed. - if p.Inputs[inIndex].isFinalized() { - return ErrInputAlreadyFinalized - } - - // Any p2tr input will only have a witness script, no sig script. - var ( - serializedWitness []byte - err error - pInput = &p.Inputs[inIndex] - ) - - // What spend path did we take? - switch { - // Key spend path. - case len(pInput.TaprootKeySpendSig) > 0: - sig := pInput.TaprootKeySpendSig - - // Make sure TaprootKeySpendSig is equal to size of signature, - // if not, we assume that sighash flag was appended to the - // signature. - if len(pInput.TaprootKeySpendSig) == schnorr.SignatureSize { - // Append to the signature if flag is not equal to the - // default sighash (that can be omitted). - if pInput.SighashType != txscript.SigHashDefault { - sigHashType := byte(pInput.SighashType) - sig = append(sig, sigHashType) - } - } - serializedWitness, err = writeWitness(sig) - - // Script spend path. - case len(pInput.TaprootScriptSpendSig) > 0: - var witnessStack wire.TxWitness - - // If there are multiple script spend signatures, we assume they - // are from multiple signing participants for the same leaf - // script that uses OP_CHECKSIGADD for multi-sig. Signing - // multiple possible execution paths at the same time is - // currently not supported by this library. - targetLeafHash := pInput.TaprootScriptSpendSig[0].LeafHash - leafScript, err := findLeafScript(pInput, targetLeafHash) - if err != nil { - return fmt.Errorf("control block for script spend " + - "signature not found") - } - - // The witness stack will contain all signatures, followed by - // the script itself and then the control block. - for idx, scriptSpendSig := range pInput.TaprootScriptSpendSig { - // Make sure that if there are indeed multiple - // signatures, they all reference the same leaf hash. - if !bytes.Equal(scriptSpendSig.LeafHash, targetLeafHash) { - return fmt.Errorf("script spend signature %d "+ - "references different target leaf "+ - "hash than first signature; only one "+ - "script path is supported", idx) - } - - sig := append([]byte{}, scriptSpendSig.Signature...) - if scriptSpendSig.SigHash != txscript.SigHashDefault { - sig = append(sig, byte(scriptSpendSig.SigHash)) - } - witnessStack = append(witnessStack, sig) - } - - // Complete the witness stack with the executed script and the - // serialized control block. - witnessStack = append(witnessStack, leafScript.Script) - witnessStack = append(witnessStack, leafScript.ControlBlock) - - serializedWitness, err = writeWitness(witnessStack...) - - default: - return ErrInvalidPsbtFormat - } - if err != nil { - return err - } - - // At this point, a witness has been constructed. Remove all fields - // other than witness utxo (01) and finalscriptsig (07), - // finalscriptwitness (08). - newInput := NewPsbtInput(nil, pInput.WitnessUtxo) - newInput.FinalScriptWitness = serializedWitness - - // Finally, we overwrite the entry in the input list at the correct - // index. - p.Inputs[inIndex] = *newInput - return nil + // If this input has already been finalized, then we'll return an error + // as we can't proceed. + if p.Inputs[inIndex].isFinalized() { + return ErrInputAlreadyFinalized + } + + // Any p2tr input will only have a witness script, no sig script. + var ( + serializedWitness []byte + err error + pInput = &p.Inputs[inIndex] + ) + + // What spend path did we take? + switch { + // Key spend path. + case len(pInput.TaprootKeySpendSig) > 0: + sig := pInput.TaprootKeySpendSig + + // Make sure TaprootKeySpendSig is equal to size of signature, + // if not, we assume that sighash flag was appended to the + // signature. + if len(pInput.TaprootKeySpendSig) == schnorr.SignatureSize { + // Append to the signature if flag is not equal to the + // default sighash (that can be omitted). + if pInput.SighashType != txscript.SigHashDefault { + sigHashType := byte(pInput.SighashType) + sig = append(sig, sigHashType) + } + } + serializedWitness, err = writeWitness(sig) + + // Script spend path. + case len(pInput.TaprootScriptSpendSig) > 0: + var witnessStack wire.TxWitness + + // If there are multiple script spend signatures, we assume they + // are from multiple signing participants for the same leaf + // script that uses OP_CHECKSIGADD for multi-sig. Signing + // multiple possible execution paths at the same time is + // currently not supported by this library. + targetLeafHash := pInput.TaprootScriptSpendSig[0].LeafHash + leafScript, err := findLeafScript(pInput, targetLeafHash) + if err != nil { + return fmt.Errorf("control block for script spend " + + "signature not found") + } + + // The witness stack will contain all signatures, followed by + // the script itself and then the control block. + for idx, scriptSpendSig := range pInput.TaprootScriptSpendSig { + // Make sure that if there are indeed multiple + // signatures, they all reference the same leaf hash. + if !bytes.Equal(scriptSpendSig.LeafHash, targetLeafHash) { + return fmt.Errorf("script spend signature %d "+ + "references different target leaf "+ + "hash than first signature; only one "+ + "script path is supported", idx) + } + + sig := append([]byte{}, scriptSpendSig.Signature...) + if scriptSpendSig.SigHash != txscript.SigHashDefault { + sig = append(sig, byte(scriptSpendSig.SigHash)) + } + witnessStack = append(witnessStack, sig) + } + + // Complete the witness stack with the executed script and the + // serialized control block. + witnessStack = append(witnessStack, leafScript.Script) + witnessStack = append(witnessStack, leafScript.ControlBlock) + + serializedWitness, err = writeWitness(witnessStack...) + + default: + return ErrInvalidPsbtFormat + } + if err != nil { + return err + } + + // At this point, a witness has been constructed. Remove all fields + // other than witness utxo (01) and finalscriptsig (07), + // finalscriptwitness (08). + newInput := NewPsbtInput(nil, pInput.WitnessUtxo) + newInput.FinalScriptWitness = serializedWitness + + // Finally, we overwrite the entry in the input list at the correct + // index. + p.Inputs[inIndex] = *newInput + return nil }