From 55ff94a30abd866feb0c174d0b745e901e90cd90 Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Tue, 6 Jul 2021 17:28:12 +0900 Subject: [PATCH 1/4] chaincfg/chainhash: Add a DoubleHashBRaw function Instead of receiving a byte slice as the argument, this new function receives a hash.Hash, giving the option for callers to write directly into a hash and save on the byte slice allocation. --- chaincfg/chainhash/hashfuncs.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/chaincfg/chainhash/hashfuncs.go b/chaincfg/chainhash/hashfuncs.go index bf74f73c39..cd254119ae 100644 --- a/chaincfg/chainhash/hashfuncs.go +++ b/chaincfg/chainhash/hashfuncs.go @@ -5,7 +5,10 @@ package chainhash -import "crypto/sha256" +import ( + "crypto/sha256" + "hash" +) // HashB calculates hash(b) and returns the resulting bytes. func HashB(b []byte) []byte { @@ -31,3 +34,11 @@ func DoubleHashH(b []byte) Hash { first := sha256.Sum256(b) return Hash(sha256.Sum256(first[:])) } + +// DoubleHashBRaw calculates hash(hash(h)) and returns the resulting bytes. +func DoubleHashBRaw(h hash.Hash) []byte { + first := h.Sum(nil) + h.Reset() + h.Write(first) + return h.Sum(nil) +} From e869ffd5ccd1bfc90002968991bf0d352e14738e Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Tue, 6 Jul 2021 17:33:20 +0900 Subject: [PATCH 2/4] wire: Export function writeOutPoint --- wire/bench_test.go | 2 +- wire/msgtx.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/wire/bench_test.go b/wire/bench_test.go index f6637d4210..c183df56e5 100644 --- a/wire/bench_test.go +++ b/wire/bench_test.go @@ -200,7 +200,7 @@ func BenchmarkWriteOutPoint(b *testing.B) { Index: 0, } for i := 0; i < b.N; i++ { - writeOutPoint(ioutil.Discard, 0, 0, op) + WriteOutPoint(ioutil.Discard, 0, 0, op) } } diff --git a/wire/msgtx.go b/wire/msgtx.go index 1e2f69fad4..6cd591e3be 100644 --- a/wire/msgtx.go +++ b/wire/msgtx.go @@ -930,9 +930,9 @@ func readOutPoint(r io.Reader, pver uint32, version int32, op *OutPoint) error { return err } -// writeOutPoint encodes op to the bitcoin protocol encoding for an OutPoint +// WriteOutPoint encodes op to the bitcoin protocol encoding for an OutPoint // to w. -func writeOutPoint(w io.Writer, pver uint32, version int32, op *OutPoint) error { +func WriteOutPoint(w io.Writer, pver uint32, version int32, op *OutPoint) error { _, err := w.Write(op.Hash[:]) if err != nil { return err @@ -992,7 +992,7 @@ func readTxIn(r io.Reader, pver uint32, version int32, ti *TxIn) error { // writeTxIn encodes ti to the bitcoin protocol encoding for a transaction // input (TxIn) to w. func writeTxIn(w io.Writer, pver uint32, version int32, ti *TxIn) error { - err := writeOutPoint(w, pver, version, &ti.PreviousOutPoint) + err := WriteOutPoint(w, pver, version, &ti.PreviousOutPoint) if err != nil { return err } From ddcd4ca8a170278444c963ca13b0d84888fbaa56 Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Tue, 6 Jul 2021 17:38:48 +0900 Subject: [PATCH 3/4] txscript: Memory efficient calcSignatureHash() This new calcSignatureHash function behaves more like the calcWitnessSigHash function, which was more memory efficient as it didn't create a temporary tx just to use the tx serialize function that already existed. This results in significant memory savings that show up on pprof. --- txscript/script.go | 123 ++++++++++++++++++++++++++------------------- 1 file changed, 71 insertions(+), 52 deletions(-) diff --git a/txscript/script.go b/txscript/script.go index 92a50e3761..ec2d3ab241 100644 --- a/txscript/script.go +++ b/txscript/script.go @@ -6,6 +6,7 @@ package txscript import ( "bytes" + "crypto/sha256" "encoding/binary" "fmt" "time" @@ -598,66 +599,84 @@ func calcSignatureHash(script []parsedOpcode, hashType SigHashType, tx *wire.Msg // Remove all instances of OP_CODESEPARATOR from the script. script = removeOpcode(script, OP_CODESEPARATOR) - // Make a shallow copy of the transaction, zeroing out the script for - // all inputs that are not currently being processed. - txCopy := shallowCopyTx(tx) - for i := range txCopy.TxIn { - if i == idx { - // UnparseScript cannot fail here because removeOpcode - // above only returns a valid script. - sigScript, _ := unparseScript(script) - txCopy.TxIn[idx].SignatureScript = sigScript - } else { - txCopy.TxIn[i].SignatureScript = nil - } - } - - switch hashType & sigHashMask { - case SigHashNone: - txCopy.TxOut = txCopy.TxOut[0:0] // Empty slice. - for i := range txCopy.TxIn { - if i != idx { - txCopy.TxIn[i].Sequence = 0 + // The calculated checksum here will be the sigHash. We'll be adding necessary + // data in for different types of signature hash types. + sigHash := sha256.New() + binary.Write(sigHash, binary.LittleEndian, uint32(tx.Version)) + + // Add inputs to the hash. + if (hashType & SigHashAnyOneCanPay) == 0 { + txInCount := uint64(len(tx.TxIn)) + wire.WriteVarInt(sigHash, 0, txInCount) + for i := range tx.TxIn { + sigHash.Write(tx.TxIn[i].PreviousOutPoint.Hash[:]) + binary.Write(sigHash, binary.LittleEndian, tx.TxIn[i].PreviousOutPoint.Index) + + // If the txIn is the specified idx, write the actual sigScript and the sequence. + if i == idx { + rawScript, _ := unparseScript(script) + wire.WriteVarBytes(sigHash, 0, rawScript) + binary.Write(sigHash, binary.LittleEndian, tx.TxIn[i].Sequence) + } else { + wire.WriteVarBytes(sigHash, 0, nil) + // For SigHashNone and SigHashSingle, don't write the actual sequence and + // just write 0. + if hashType&sigHashMask == SigHashNone || hashType&sigHashMask == SigHashSingle { + binary.Write(sigHash, binary.LittleEndian, uint32(0)) + } else { + binary.Write(sigHash, binary.LittleEndian, tx.TxIn[i].Sequence) + } } } + } else { + // Write count of 1 + wire.WriteVarInt(sigHash, 0, uint64(1)) + wire.WriteOutPoint(sigHash, 0, 0, &tx.TxIn[idx].PreviousOutPoint) + rawScript, _ := unparseScript(script) + wire.WriteVarBytes(sigHash, 0, rawScript) + binary.Write(sigHash, binary.LittleEndian, tx.TxIn[idx].Sequence) + } + + // Add outputs to the hash. + if hashType&sigHashMask == SigHashNone { + // Write count of 0 for SigHashNone + wire.WriteVarInt(sigHash, 0, uint64(0)) + } else if hashType&sigHashMask == SigHashSingle { + // Write count of all txOuts. We count all txOuts up til the idx specified. + wire.WriteVarInt(sigHash, 0, uint64(idx+1)) + + // Make empty txOut that'll be used for all txOuts except for the + // specified idx. + to := wire.TxOut{ + Value: -1, + PkScript: nil, + } - case SigHashSingle: - // Resize output array to up to and including requested index. - txCopy.TxOut = txCopy.TxOut[:idx+1] - - // All but current output get zeroed out. + // For all txOuts before the specified idx, write empty txOuts. for i := 0; i < idx; i++ { - txCopy.TxOut[i].Value = -1 - txCopy.TxOut[i].PkScript = nil + wire.WriteTxOut(sigHash, 0, 0, &to) } - // Sequence on all other inputs is 0, too. - for i := range txCopy.TxIn { - if i != idx { - txCopy.TxIn[i].Sequence = 0 - } + // Finally write the value and the pkscript. + binary.Write(sigHash, binary.LittleEndian, uint64(tx.TxOut[idx].Value)) + wire.WriteVarBytes(sigHash, 0, tx.TxOut[idx].PkScript) + } else { + txOutCount := uint64(len(tx.TxOut)) + wire.WriteVarInt(sigHash, 0, txOutCount) + for i := range tx.TxOut { + binary.Write(sigHash, binary.LittleEndian, uint64(tx.TxOut[i].Value)) + wire.WriteVarBytes(sigHash, 0, tx.TxOut[i].PkScript) } + } - default: - // Consensus treats undefined hashtypes like normal SigHashAll - // for purposes of hash generation. - fallthrough - case SigHashOld: - fallthrough - case SigHashAll: - // Nothing special here. - } - if hashType&SigHashAnyOneCanPay != 0 { - txCopy.TxIn = txCopy.TxIn[idx : idx+1] - } - - // The final hash is the double sha256 of both the serialized modified - // transaction and the hash type (encoded as a 4-byte little-endian - // value) appended. - wbuf := bytes.NewBuffer(make([]byte, 0, txCopy.SerializeSizeStripped()+4)) - txCopy.SerializeNoWitness(wbuf) - binary.Write(wbuf, binary.LittleEndian, hashType) - return chainhash.DoubleHashB(wbuf.Bytes()) + // Finally, write out the transaction's locktime, and the sig hash type. + var bLockTime [4]byte + binary.LittleEndian.PutUint32(bLockTime[:], tx.LockTime) + sigHash.Write(bLockTime[:]) + var bHashType [4]byte + binary.LittleEndian.PutUint32(bHashType[:], uint32(hashType)) + sigHash.Write(bHashType[:]) + return chainhash.DoubleHashBRaw(sigHash) } // asSmallInt returns the passed opcode, which must be true according to From 50f8996cee45588740c85dd083e23c27fd6c94e6 Mon Sep 17 00:00:00 2001 From: Calvin Kim Date: Tue, 6 Jul 2021 17:44:25 +0900 Subject: [PATCH 4/4] txscript: Use DoubleHashBRaw for calcWitnessSignatureHash Changes up calcWitnessSignatureHash to use the DoubleHashBRaw function, saving on the byte slice allocation that would have happened otherwise. --- txscript/script.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/txscript/script.go b/txscript/script.go index ec2d3ab241..527a00e072 100644 --- a/txscript/script.go +++ b/txscript/script.go @@ -419,7 +419,7 @@ func calcWitnessSignatureHash(subScript []parsedOpcode, sigHashes *TxSigHashes, // We'll utilize this buffer throughout to incrementally calculate // the signature hash for this transaction. - var sigHash bytes.Buffer + sigHash := sha256.New() // First write out, then encode the transaction's version number. var bVersion [4]byte @@ -474,7 +474,7 @@ func calcWitnessSignatureHash(subScript []parsedOpcode, sigHashes *TxSigHashes, // the original script, with all code separators removed, // serialized with a var int length prefix. rawScript, _ := unparseScript(subScript) - wire.WriteVarBytes(&sigHash, 0, rawScript) + wire.WriteVarBytes(sigHash, 0, rawScript) } // Next, add the input amount, and sequence number of the input being @@ -510,7 +510,7 @@ func calcWitnessSignatureHash(subScript []parsedOpcode, sigHashes *TxSigHashes, binary.LittleEndian.PutUint32(bHashType[:], uint32(hashType)) sigHash.Write(bHashType[:]) - return chainhash.DoubleHashB(sigHash.Bytes()), nil + return chainhash.DoubleHashBRaw(sigHash), nil } // CalcWitnessSigHash computes the sighash digest for the specified input of