// Package txauthor provides transaction creation code for wallets. package txauthor import ( "errors" "github.com/p9c/p9/pkg/amt" "github.com/p9c/p9/pkg/chaincfg" "github.com/p9c/p9/pkg/txrules" "github.com/p9c/p9/pkg/txscript" "github.com/p9c/p9/pkg/txsizes" h "github.com/p9c/p9/pkg/util/helpers" "github.com/p9c/p9/pkg/wire" ) type ( // InputSource provides transaction inputs referencing spendable outputs to construct a transaction outputting some // target amount. If the target amount can not be satisified, this can be signaled by returning a total amount less // than the target or by returning a more detailed error implementing InputSourceError. InputSource func(target amt.Amount) ( total amt.Amount, inputs []*wire.TxIn, inputValues []amt.Amount, scripts [][]byte, e error, ) // InputSourceError describes the failure to provide enough input value from unspent transaction outputs to meet a // target amount. A typed error is used so input sources can provide their own implementations describing the reason // for the error, for example, due to spendable policies or locked coins rather than the wallet not having enough // available input value. InputSourceError interface { error InputSourceError() } // Default implementation of InputSourceError. insufficientFundsError struct{} // AuthoredTx holds the state of a newly-created transaction and the change output (if one was added). AuthoredTx struct { Tx *wire.MsgTx PrevScripts [][]byte PrevInputValues []amt.Amount TotalInput amt.Amount ChangeIndex int // negative if no change } // ChangeSource provides P2PKH change output scripts for transaction creation. ChangeSource func() ([]byte, error) // SecretsSource provides private keys and redeem scripts necessary for constructing transaction input signatures. // Secrets are looked up by the corresponding Address for the previous output script. Addresses for lookup are // created using the source's blockchain parameters and means a single SecretsSource can only manage secrets for a // single chain. // // TODO: Rewrite this interface to look up private keys and redeem scripts for pubkeys, pubkey hashes, script // hashes, etc. as separate interface methods. // // This would remove the ChainParams requirement of the interface and could avoid unnecessary conversions from // previous output scripts to Addresses. This can not be done without modifications to the txscript package. SecretsSource interface { txscript.KeyDB txscript.ScriptDB ChainParams() *chaincfg.Params } ) func (insufficientFundsError) InputSourceError() { } func (insufficientFundsError) Error() string { return "insufficient funds available to construct transaction" } // NewUnsignedTransaction creates an unsigned transaction paying to one or more non-change outputs. An appropriate // transaction fee is included based on the transaction size. // // Transaction inputs are chosen from repeated calls to fetchInputs with increasing targets amounts. // // If any remaining output value can be returned to the wallet via a change output without violating mempool dust rules, // a P2WPKH change output is appended to the transaction outputs. Since the change output may not be necessary, // fetchChange is called zero or one times to generate this script. This function must return a P2WPKH script or // smaller, otherwise fee estimation will be incorrect. // // If successful, the transaction, total input value spent, and all previous output scripts are returned. If the input // source was unable to provide enough input value to pay for every output any any necessary fees, an InputSourceError // is returned. // // BUGS: Fee estimation may be off when redeeming non-compressed P2PKH outputs. func NewUnsignedTransaction( outputs []*wire.TxOut, relayFeePerKb amt.Amount, fetchInputs InputSource, fetchChange ChangeSource, ) (*AuthoredTx, error) { targetAmount := h.SumOutputValues(outputs) estimatedSize := txsizes.EstimateVirtualSize(1, 0, 0, outputs, true) targetFee := txrules.FeeForSerializeSize(relayFeePerKb, estimatedSize) for { inputAmount, inputs, inputValues, scripts, e := fetchInputs(targetAmount + targetFee) if e != nil { return nil, e } if inputAmount < targetAmount+targetFee { return nil, insufficientFundsError{} } // We count the types of inputs, which we'll use to estimate the vsize of the transaction. var nested, p2wpkh, p2pkh int for _, /*pkScript*/ _ = range scripts { switch { // // If this is a p2sh output, we assume this is a nested P2WKH. // case txscript.IsPayToScriptHash(pkScript): // nested++ // case txscript.IsPayToWitnessPubKeyHash(pkScript): // p2wpkh++ default: p2pkh++ } } maxSignedSize := txsizes.EstimateVirtualSize( p2pkh, p2wpkh, nested, outputs, true, ) maxRequiredFee := txrules.FeeForSerializeSize(relayFeePerKb, maxSignedSize) remainingAmount := inputAmount - targetAmount if remainingAmount < maxRequiredFee { targetFee = maxRequiredFee continue } unsignedTransaction := &wire.MsgTx{ Version: wire.TxVersion, TxIn: inputs, TxOut: outputs, LockTime: 0, } changeIndex := -1 changeAmount := inputAmount - targetAmount - maxRequiredFee if changeAmount != 0 && !txrules.IsDustAmount( changeAmount, txsizes.P2PKHPkScriptSize, relayFeePerKb, ) { changeScript, e := fetchChange() if e != nil { return nil, e } if len(changeScript) > txsizes.P2PKHPkScriptSize { return nil, errors.New( "fee estimation requires change " + "scripts no larger than P2WPKH output scripts", ) } change := wire.NewTxOut(int64(changeAmount), changeScript) l := len(outputs) unsignedTransaction.TxOut = append(outputs[:l:l], change) changeIndex = l } return &AuthoredTx{ Tx: unsignedTransaction, PrevScripts: scripts, PrevInputValues: inputValues, TotalInput: inputAmount, ChangeIndex: changeIndex, }, nil } } // RandomizeOutputPosition randomizes the position of a transaction's output by swapping it with a random output. The // new index is returned. This should be done before signing. func RandomizeOutputPosition(outputs []*wire.TxOut, index int) int { r := cprng.Int31n(int32(len(outputs))) outputs[r], outputs[index] = outputs[index], outputs[r] return int(r) } // RandomizeChangePosition randomizes the position of an authored transaction's change output. This should be done // before signing. func (tx *AuthoredTx) RandomizeChangePosition() { tx.ChangeIndex = RandomizeOutputPosition(tx.Tx.TxOut, tx.ChangeIndex) } // AddAllInputScripts modifies transaction a transaction by adding inputs // scripts for each input. Previous output scripts being redeemed by each input // are passed in prevPkScripts and the slice length must match the number of // inputs. Private keys and redeem scripts are looked up using a SecretsSource // based on the previous output script. func AddAllInputScripts( tx *wire.MsgTx, prevPkScripts [][]byte, inputValues []amt.Amount, secrets SecretsSource, ) (e error) { inputs := tx.TxIn // hashCache := txscript.NewTxSigHashes(tx) chainParams := secrets.ChainParams() if len(inputs) != len(prevPkScripts) { return errors.New( "tx.TxIn and prevPkScripts slices must " + "have equal length", ) } for i := range inputs { pkScript := prevPkScripts[i] switch { // // If this is a p2sh output, who's script hash pre-image is a witness program, // // then we'll need to use a modified signing function which generates both the // // sigScript, and the witness script. // case txscript.IsPayToScriptHash(pkScript): // e := spendNestedWitnessPubKeyHash(inputs[i], pkScript, // int64(inputValues[i]), chainParams, secrets, // tx, hashCache, i) // if e != nil { // // return e // } // case txscript.IsPayToWitnessPubKeyHash(pkScript): // e := spendWitnessKeyHash(inputs[i], pkScript, // int64(inputValues[i]), chainParams, secrets, // tx, hashCache, i) // if e != nil { // // return e // } default: sigScript := inputs[i].SignatureScript var script []byte script, e = txscript.SignTxOutput( chainParams, tx, i, pkScript, txscript.SigHashAll, secrets, secrets, sigScript, ) if e != nil { return e } inputs[i].SignatureScript = script } } return nil } // spendWitnessKeyHash generates, and sets a valid witness for spending the // // passed pkScript with the specified input amount. The input amount *must* // // correspond to the output value of the previous pkScript, or else verification // // will fail since the new sighash digest algorithm defined in BIP0143 includes // // the input value in the sighash. // func spendWitnessKeyHash(txIn *wire.TxIn, pkScript []byte, // inputValue int64, chainParams *chaincfg.Params, secrets SecretsSource, // tx *wire.MsgTx, hashCache *txscript.TxSigHashes, idx int) (e error) { // // First obtain the key pair associated with this p2wkh address. // _, addrs, _, e = txscript.ExtractPkScriptAddrs(pkScript, // chainParams) // if e != nil { // // return e // } // privKey, compressed, e := secrets.GetKey(addrs[0]) // if e != nil { // // return e // } // pubKey := privKey.PubKey() // // Once we have the key pair, generate a p2wkh address type, respecting the compression type of the generated key. // var pubKeyHash []byte // if compressed { // pubKeyHash = btcaddr.Hash160(pubKey.SerializeCompressed()) // } else { // pubKeyHash = btcaddr.Hash160(pubKey.SerializeUncompressed()) // } // p2wkhAddr, e := util.NewAddressWitnessPubKeyHash(pubKeyHash, chainParams) // if e != nil { // // return e // } // // With the concrete address type, we can now generate the corresponding witness // // program to be used to generate a valid witness which will allow us to spend // // this output. // witnessProgram, e := txscript.PayToAddrScript(p2wkhAddr) // if e != nil { // // return e // } // witnessScript, e := txscript.WitnessSignature(tx, hashCache, idx, // inputValue, witnessProgram, txscript.SigHashAll, privKey, true) // if e != nil { // // return e // } // txIn.Witness = witnessScript // return nil // } // spendNestedWitnessPubKey generates both a sigScript, and valid witness for // // spending the passed pkScript with the specified input amount. The generated // // sigScript is the version 0 p2wkh witness program corresponding to the queried // // key. The witness stack is identical to that of one which spends a regular // // p2wkh output. The input amount *must* correspond to the output value of the // // previous pkScript, or else verification will fail since the new sighash // // digest algorithm defined in BIP0143 includes the input value in the sighash. // func spendNestedWitnessPubKeyHash(txIn *wire.TxIn, pkScript []byte, // inputValue int64, chainParams *chaincfg.Params, secrets SecretsSource, // tx *wire.MsgTx, hashCache *txscript.TxSigHashes, idx int) (e error) { // // First we need to obtain the key pair related to this p2sh output. // _, addrs, _, e = txscript.ExtractPkScriptAddrs(pkScript, // chainParams) // if e != nil { // // return e // } // privKey, compressed, e := secrets.GetKey(addrs[0]) // if e != nil { // // return e // } // pubKey := privKey.PubKey() // var pubKeyHash []byte // if compressed { // pubKeyHash = btcaddr.Hash160(pubKey.SerializeCompressed()) // } else { // pubKeyHash = btcaddr.Hash160(pubKey.SerializeUncompressed()) // } // // Next, we'll generate a valid sigScript that'll allow us to spend the p2sh // // output. The sigScript will contain only a single push of the p2wkh witness // // program corresponding to the matching public key of this address. // p2wkhAddr, e := util.NewAddressWitnessPubKeyHash(pubKeyHash, chainParams) // if e != nil { // // return e // } // witnessProgram, e := txscript.PayToAddrScript(p2wkhAddr) // if e != nil { // // return e // } // bldr := txscript.NewScriptBuilder() // bldr.AddData(witnessProgram) // sigScript, e := bldr.Script() // if e != nil { // // return e // } // txIn.SignatureScript = sigScript // // With the sigScript in place, we'll next generate the proper witness that'll // // allow us to spend the p2wkh output. // witnessScript, e := txscript.WitnessSignature(tx, hashCache, idx, // inputValue, witnessProgram, txscript.SigHashAll, privKey, compressed) // if e != nil { // // return e // } // txIn.Witness = witnessScript // return nil // } // AddAllInputScripts modifies an authored transaction by adding inputs scripts for each input of an authored // transaction. Private keys and redeem scripts are looked up using a SecretsSource based on the previous output script. func (tx *AuthoredTx) AddAllInputScripts(secrets SecretsSource) (e error) { return AddAllInputScripts(tx.Tx, tx.PrevScripts, tx.PrevInputValues, secrets) }