From cfb953cddfd1d90fba89453120ca72d33dc0b7c3 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Sat, 9 Jan 2021 13:12:49 -0600 Subject: [PATCH] blockchain: Remove compression version param. Over the years it has become increasingly obvious that storing multiple versioned formats in the database in an attempt to avoid migrations leads to code that is super hard to reason about and for which it is also difficult to assert correctness. This is the case because it results in a combinatorial explosion of cases that must be handled. For example, as soon as you have 3 versions, you're already up to 8 variants you have to handle properly and test, and it only gets exponentially worse with each new version. Due to this, it is greatly preferred to perform a single migration that handles the conversion logic once. This allows the rest of the code, especially in the critical paths, to work solely with the latest version and therefore it stays much cleaner, easier to validate for correctness, and is generally easier to reason about. With that in mind, this removes the compression version parameter from the functions that deal with serializing and deserializing compressed scripts. Since there is only currently a single version it does not require any migrations. However, since the existing migration code for older versions was passing in the old version parameter, new v1 functions have been added to the upgrade code to ensure stability there. --- blockchain/chain.go | 10 +-- blockchain/chainio.go | 13 ++- blockchain/compress.go | 57 ++++++------- blockchain/compress_test.go | 48 ++++------- blockchain/upgrade.go | 158 ++++++++++++++++++++++++++++++++---- 5 files changed, 195 insertions(+), 91 deletions(-) diff --git a/blockchain/chain.go b/blockchain/chain.go index 81bc7dba34..e65c580e7d 100644 --- a/blockchain/chain.go +++ b/blockchain/chain.go @@ -1917,7 +1917,7 @@ func extractDeploymentIDVersions(params *chaincfg.Params) (map[string]uint32, er // stxosToScriptSource uses the provided block and spent txo information to // create a source of previous transaction scripts and versions spent by the // block. -func stxosToScriptSource(block *dcrutil.Block, stxos []spentTxOut, compressionVersion uint32, isTreasuryEnabled bool, chainParams *chaincfg.Params) scriptSource { +func stxosToScriptSource(block *dcrutil.Block, stxos []spentTxOut, isTreasuryEnabled bool, chainParams *chaincfg.Params) scriptSource { source := make(scriptSource) msgBlock := block.MsgBlock() @@ -1961,7 +1961,7 @@ func stxosToScriptSource(block *dcrutil.Block, stxos []spentTxOut, compressionVe prevOut := &txIn.PreviousOutPoint source[*prevOut] = scriptSourceEntry{ version: stxo.scriptVersion, - script: decompressScript(stxo.pkScript, compressionVersion), + script: decompressScript(stxo.pkScript), } } } @@ -1983,7 +1983,7 @@ func stxosToScriptSource(block *dcrutil.Block, stxos []spentTxOut, compressionVe prevOut := &txIn.PreviousOutPoint source[*prevOut] = scriptSourceEntry{ version: stxo.scriptVersion, - script: decompressScript(stxo.pkScript, compressionVersion), + script: decompressScript(stxo.pkScript), } } } @@ -2033,8 +2033,8 @@ func (q *chainQueryerAdapter) PrevScripts(dbTx database.Tx, block *dcrutil.Block return nil, err } - prevScripts := stxosToScriptSource(block, stxos, currentCompressionVersion, - isTreasuryEnabled, q.chainParams) + prevScripts := stxosToScriptSource(block, stxos, isTreasuryEnabled, + q.chainParams) return prevScripts, nil } diff --git a/blockchain/chainio.go b/blockchain/chainio.go index 7fe7f8fd5d..64cd01649e 100644 --- a/blockchain/chainio.go +++ b/blockchain/chainio.go @@ -553,7 +553,7 @@ func spentTxOutSerializeSize(stxo *spentTxOut) int { const hasAmount = false size += compressedTxOutSize(uint64(stxo.amount), stxo.scriptVersion, - stxo.pkScript, currentCompressionVersion, hasAmount) + stxo.pkScript, hasAmount) if stxo.ticketMinOuts != nil { size += len(stxo.ticketMinOuts.data) @@ -573,7 +573,7 @@ func putSpentTxOut(target []byte, stxo *spentTxOut) int { const hasAmount = false offset += putCompressedTxOut(target[offset:], 0, stxo.scriptVersion, - stxo.pkScript, currentCompressionVersion, hasAmount) + stxo.pkScript, hasAmount) if stxo.ticketMinOuts != nil { copy(target[offset:], stxo.ticketMinOuts.data) @@ -600,7 +600,7 @@ func decodeSpentTxOut(serialized []byte, stxo *spentTxOut, amount int64, // since in Decred we only need pkScript at most due to fraud proofs // already storing the decompressed amount. _, scriptVersion, script, bytesRead, err := - decodeCompressedTxOut(serialized[offset:], currentCompressionVersion, false) + decodeCompressedTxOut(serialized[offset:], false) offset += bytesRead if err != nil { return offset, errDeserialize(fmt.Sprintf("unable to decode "+ @@ -937,7 +937,7 @@ func serializeUtxoEntry(entry *UtxoEntry) ([]byte, error) { serializeSizeVLQ(uint64(entry.blockIndex)) + serializeSizeVLQ(uint64(flags)) + compressedTxOutSize(uint64(entry.amount), entry.scriptVersion, - entry.pkScript, currentCompressionVersion, hasAmount) + entry.pkScript, hasAmount) if entry.ticketMinOuts != nil { size += len(entry.ticketMinOuts.data) @@ -949,7 +949,7 @@ func serializeUtxoEntry(entry *UtxoEntry) ([]byte, error) { offset += putVLQ(serialized[offset:], uint64(entry.blockIndex)) offset += putVLQ(serialized[offset:], uint64(flags)) offset += putCompressedTxOut(serialized[offset:], uint64(entry.amount), - entry.scriptVersion, entry.pkScript, currentCompressionVersion, hasAmount) + entry.scriptVersion, entry.pkScript, hasAmount) if entry.ticketMinOuts != nil { copy(serialized[offset:], entry.ticketMinOuts.data) @@ -986,8 +986,7 @@ func deserializeUtxoEntry(serialized []byte, txOutIndex uint32) (*UtxoEntry, err // Decode the compressed unspent transaction output. amount, scriptVersion, script, bytesRead, err := - decodeCompressedTxOut(serialized[offset:], currentCompressionVersion, - true) + decodeCompressedTxOut(serialized[offset:], true) if err != nil { return nil, errDeserialize(fmt.Sprintf("unable to decode utxo: %v", err)) } diff --git a/blockchain/compress.go b/blockchain/compress.go index 995e0a61a8..ae99122838 100644 --- a/blockchain/compress.go +++ b/blockchain/compress.go @@ -263,8 +263,7 @@ func isPubKey(script []byte) (bool, []byte) { // compressedScriptSize returns the number of bytes the passed script would take // when encoded with the domain specific compression algorithm described above. -func compressedScriptSize(scriptVersion uint16, pkScript []byte, - compressionVersion uint32) int { +func compressedScriptSize(scriptVersion uint16, pkScript []byte) int { // Pay-to-pubkey-hash or pay-to-script-hash script. if isPubKeyHash(pkScript) || isScriptHash(pkScript) { return 21 @@ -286,7 +285,7 @@ func compressedScriptSize(scriptVersion uint16, pkScript []byte, // script, possibly followed by other data, and returns the number of bytes it // occupies taking into account the special encoding of the script size by the // domain specific compression algorithm described above. -func decodeCompressedScriptSize(serialized []byte, compressionVersion uint32) int { +func decodeCompressedScriptSize(serialized []byte) int { scriptSize, bytesRead := deserializeVLQ(serialized) if bytesRead == 0 { return 0 @@ -314,8 +313,7 @@ func decodeCompressedScriptSize(serialized []byte, compressionVersion uint32) in // target byte slice. The target byte slice must be at least large enough to // handle the number of bytes returned by the compressedScriptSize function or // it will panic. -func putCompressedScript(target []byte, scriptVersion uint16, pkScript []byte, - compressionVersion uint32) int { +func putCompressedScript(target []byte, scriptVersion uint16, pkScript []byte) int { if len(target) == 0 { target[0] = 0x00 return 1 @@ -376,8 +374,7 @@ func putCompressedScript(target []byte, scriptVersion uint16, pkScript []byte, // NOTE: The script parameter must already have been proven to be long enough // to contain the number of bytes returned by decodeCompressedScriptSize or it // will panic. This is acceptable since it is only an internal function. -func decompressScript(compressedPkScript []byte, - compressionVersion uint32) []byte { +func decompressScript(compressedPkScript []byte) []byte { // Empty scripts, specified by 0x00, are considered nil. if len(compressedPkScript) == 0 { return nil @@ -466,8 +463,8 @@ func decompressScript(compressedPkScript []byte, // While this is simply exchanging one uint64 for another, the resulting value // for typical amounts has a much smaller magnitude which results in fewer bytes // when encoded as variable length quantity. For example, consider the amount -// of 0.1 DCR which is 10000000 atoms. Encoding 10000000 as a VarInt would take -// 4 bytes while encoding the compressed value of 8 as a VarInt only takes 1 byte. +// of 0.1 DCR which is 10000000 atoms. Encoding 10000000 as a VLQ would take +// 4 bytes while encoding the compressed value of 8 as a VLQ only takes 1 byte. // // Essentially the compression is achieved by splitting the value into an // exponent in the range [0-9] and a digit in the range [1-9], when possible, @@ -484,15 +481,15 @@ func decompressScript(compressedPkScript []byte, // 1 + 10*(n-1) + e == 10 + 10*(n-1) // // Example encodings: -// (The numbers in parenthesis are the number of bytes when serialized as a VarInt) -// 0 (1) -> 0 (1) * 0.00000000 BTC -// 1000 (2) -> 4 (1) * 0.00001000 BTC -// 10000 (2) -> 5 (1) * 0.00010000 BTC -// 12345678 (4) -> 111111101(4) * 0.12345678 BTC -// 50000000 (4) -> 47 (1) * 0.50000000 BTC -// 100000000 (4) -> 9 (1) * 1.00000000 BTC -// 500000000 (5) -> 49 (1) * 5.00000000 BTC -// 1000000000 (5) -> 10 (1) * 10.00000000 BTC +// (The numbers in parenthesis are the number of bytes when serialized as a VLQ) +// 0 (1) -> 0 (1) * 0.00000000 DCR +// 1000 (2) -> 4 (1) * 0.00001000 DCR +// 10000 (2) -> 5 (1) * 0.00010000 DCR +// 12345678 (4) -> 111111101(4) * 0.12345678 DCR +// 50000000 (4) -> 48 (1) * 0.50000000 DCR +// 100000000 (4) -> 9 (1) * 1.00000000 DCR +// 500000000 (5) -> 49 (1) * 5.00000000 DCR +// 1000000000 (5) -> 10 (1) * 10.00000000 DCR // ----------------------------------------------------------------------------- // compressTxOutAmount compresses the passed amount according to the domain @@ -581,16 +578,16 @@ func decompressTxOutAmount(amount uint64) uint64 { // compressedTxOutSize returns the number of bytes the passed transaction output // fields would take when encoded with the format described above. func compressedTxOutSize(amount uint64, scriptVersion uint16, pkScript []byte, - compressionVersion uint32, hasAmount bool) int { + hasAmount bool) int { scriptVersionSize := serializeSizeVLQ(uint64(scriptVersion)) if !hasAmount { return scriptVersionSize + compressedScriptSize(scriptVersion, - pkScript, compressionVersion) + pkScript) } return scriptVersionSize + serializeSizeVLQ(compressTxOutAmount(amount)) + - compressedScriptSize(scriptVersion, pkScript, compressionVersion) + compressedScriptSize(scriptVersion, pkScript) } // putCompressedTxOut compresses the passed amount and script according to their @@ -599,28 +596,24 @@ func compressedTxOutSize(amount uint64, scriptVersion uint16, pkScript []byte, // slice must be at least large enough to handle the number of bytes returned by // the compressedTxOutSize function or it will panic. func putCompressedTxOut(target []byte, amount uint64, scriptVersion uint16, - pkScript []byte, compressionVersion uint32, hasAmount bool) int { + pkScript []byte, hasAmount bool) int { if !hasAmount { offset := putVLQ(target, uint64(scriptVersion)) - offset += putCompressedScript(target[offset:], scriptVersion, pkScript, - compressionVersion) + offset += putCompressedScript(target[offset:], scriptVersion, pkScript) return offset } offset := putVLQ(target, compressTxOutAmount(amount)) offset += putVLQ(target[offset:], uint64(scriptVersion)) - offset += putCompressedScript(target[offset:], scriptVersion, pkScript, - compressionVersion) + offset += putCompressedScript(target[offset:], scriptVersion, pkScript) return offset } // decodeCompressedTxOut decodes the passed compressed txout, possibly followed // by other data, into its uncompressed amount and script and returns them along // with the number of bytes they occupied prior to decompression. -func decodeCompressedTxOut(serialized []byte, compressionVersion uint32, - hasAmount bool) (int64, uint16, []byte, int, error) { - +func decodeCompressedTxOut(serialized []byte, hasAmount bool) (int64, uint16, []byte, int, error) { var amount int64 var bytesRead int var offset int @@ -648,8 +641,7 @@ func decodeCompressedTxOut(serialized []byte, compressionVersion uint32, // Decode the compressed script size and ensure there are enough bytes // left in the slice for it. - scriptSize := decodeCompressedScriptSize(serialized[offset:], - compressionVersion) + scriptSize := decodeCompressedScriptSize(serialized[offset:]) // Note: scriptSize == 0 is OK (an empty compressed script is valid) if scriptSize < 0 { return 0, 0, nil, offset, errDeserialize("negative script size") @@ -661,8 +653,7 @@ func decodeCompressedTxOut(serialized []byte, compressionVersion uint32, } // Decompress the script. - script := decompressScript(serialized[offset:offset+scriptSize], - compressionVersion) + script := decompressScript(serialized[offset : offset+scriptSize]) return amount, uint16(scriptVersion), script, offset + scriptSize, nil } diff --git a/blockchain/compress_test.go b/blockchain/compress_test.go index 81885b9ea1..bbc635527e 100644 --- a/blockchain/compress_test.go +++ b/blockchain/compress_test.go @@ -107,91 +107,78 @@ func TestScriptCompression(t *testing.T) { tests := []struct { name string - version uint32 scriptVersion uint16 uncompressed []byte compressed []byte }{ { name: "nil", - version: 1, scriptVersion: 0, uncompressed: nil, compressed: hexToBytes("40"), }, { name: "pay-to-pubkey-hash 1", - version: 1, scriptVersion: 0, uncompressed: hexToBytes("76a9141018853670f9f3b0582c5b9ee8ce93764ac32b9388ac"), compressed: hexToBytes("001018853670f9f3b0582c5b9ee8ce93764ac32b93"), }, { name: "pay-to-pubkey-hash 2", - version: 1, scriptVersion: 0, uncompressed: hexToBytes("76a914e34cce70c86373273efcc54ce7d2a491bb4a0e8488ac"), compressed: hexToBytes("00e34cce70c86373273efcc54ce7d2a491bb4a0e84"), }, { name: "pay-to-script-hash 1", - version: 1, scriptVersion: 0, uncompressed: hexToBytes("a914da1745e9b549bd0bfa1a569971c77eba30cd5a4b87"), compressed: hexToBytes("01da1745e9b549bd0bfa1a569971c77eba30cd5a4b"), }, { name: "pay-to-script-hash 2", - version: 1, scriptVersion: 0, uncompressed: hexToBytes("a914f815b036d9bbbce5e9f2a00abd1bf3dc91e9551087"), compressed: hexToBytes("01f815b036d9bbbce5e9f2a00abd1bf3dc91e95510"), }, { name: "pay-to-pubkey compressed 0x02", - version: 1, scriptVersion: 0, uncompressed: hexToBytes("2102192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b4ac"), compressed: hexToBytes("02192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b4"), }, { name: "pay-to-pubkey compressed 0x03", - version: 1, scriptVersion: 0, uncompressed: hexToBytes("2103b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e65ac"), compressed: hexToBytes("03b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e65"), }, { name: "pay-to-pubkey uncompressed 0x04 even", - version: 1, scriptVersion: 0, uncompressed: hexToBytes("4104192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b40d45264838c0bd96852662ce6a847b197376830160c6d2eb5e6a4c44d33f453eac"), compressed: hexToBytes("04192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b4"), }, { name: "pay-to-pubkey uncompressed 0x04 odd", - version: 1, scriptVersion: 0, uncompressed: hexToBytes("410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3ac"), compressed: hexToBytes("0511db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5c"), }, { name: "pay-to-pubkey invalid pubkey", - version: 1, scriptVersion: 0, uncompressed: hexToBytes("3302aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac"), compressed: hexToBytes("633302aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac"), }, { name: "null data", - version: 1, scriptVersion: 0, uncompressed: hexToBytes("6a200102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"), compressed: hexToBytes("626a200102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"), }, { name: "requires 2 size bytes - data push 200 bytes", - version: 1, scriptVersion: 0, uncompressed: append(hexToBytes("4cc8"), bytes.Repeat([]byte{0x00}, 200)...), // [0x80, 0x50] = 208 as a variable length quantity @@ -203,8 +190,7 @@ func TestScriptCompression(t *testing.T) { for _, test := range tests { // Ensure the function to calculate the serialized size without // actually serializing the value is calculated properly. - gotSize := compressedScriptSize(test.scriptVersion, test.uncompressed, - test.version) + gotSize := compressedScriptSize(test.scriptVersion, test.uncompressed) if gotSize != len(test.compressed) { t.Errorf("compressedScriptSize (%s): did not get "+ "expected size - got %d, want %d", test.name, @@ -215,7 +201,7 @@ func TestScriptCompression(t *testing.T) { // Ensure the script compresses to the expected bytes. gotCompressed := make([]byte, gotSize) gotBytesWritten := putCompressedScript(gotCompressed, test.scriptVersion, - test.uncompressed, test.version) + test.uncompressed) if !bytes.Equal(gotCompressed, test.compressed) { t.Errorf("putCompressedScript (%s): did not get "+ "expected bytes - got %x, want %x", test.name, @@ -232,8 +218,7 @@ func TestScriptCompression(t *testing.T) { // Ensure the compressed script size is properly decoded from // the compressed script. - gotDecodedSize := decodeCompressedScriptSize(test.compressed, - test.version) + gotDecodedSize := decodeCompressedScriptSize(test.compressed) if gotDecodedSize != len(test.compressed) { t.Errorf("decodeCompressedScriptSize (%s): did not get "+ "expected size - got %d, want %d", test.name, @@ -242,7 +227,7 @@ func TestScriptCompression(t *testing.T) { } // Ensure the script decompresses to the expected bytes. - gotDecompressed := decompressScript(test.compressed, test.version) + gotDecompressed := decompressScript(test.compressed) if !bytes.Equal(gotDecompressed, test.uncompressed) { t.Errorf("decompressScript (%s): did not get expected "+ "bytes - got %x, want %x", test.name, @@ -258,13 +243,13 @@ func TestScriptCompressionErrors(t *testing.T) { t.Parallel() // A nil script must result in a decoded size of 0. - if gotSize := decodeCompressedScriptSize(nil, 1); gotSize != 0 { + if gotSize := decodeCompressedScriptSize(nil); gotSize != 0 { t.Fatalf("decodeCompressedScriptSize with nil script did not "+ "return 0 - got %d", gotSize) } // A nil script must result in a nil decompressed script. - if gotScript := decompressScript(nil, 1); gotScript != nil { + if gotScript := decompressScript(nil); gotScript != nil { t.Fatalf("decompressScript with nil script did not return nil "+ "decompressed script - got %x", gotScript) } @@ -273,7 +258,7 @@ func TestScriptCompressionErrors(t *testing.T) { // in an invalid pubkey must result in a nil decompressed script. compressedScript := hexToBytes("04012d74d0cb94344c9569c2e77901573d8d" + "7903c3ebec3a957724895dca52c6b4") - if gotScript := decompressScript(compressedScript, 1); gotScript != nil { + if gotScript := decompressScript(compressedScript); gotScript != nil { t.Fatalf("decompressScript with compressed pay-to-"+ "uncompressed-pubkey that is invalid did not return "+ "nil decompressed script - got %x", gotScript) @@ -420,14 +405,15 @@ func TestCompressedTxOut(t *testing.T) { } for _, test := range tests { - targetSz := compressedTxOutSize(0, test.scriptVersion, test.pkScript, currentCompressionVersion, test.hasAmount) - 1 + targetSz := compressedTxOutSize(0, test.scriptVersion, test.pkScript, + test.hasAmount) - 1 target := make([]byte, targetSz) - putCompressedScript(target, test.scriptVersion, test.pkScript, currentCompressionVersion) + putCompressedScript(target, test.scriptVersion, test.pkScript) // Ensure the function to calculate the serialized size without // actually serializing the txout is calculated properly. gotSize := compressedTxOutSize(test.amount, test.scriptVersion, - test.pkScript, test.version, test.hasAmount) + test.pkScript, test.hasAmount) if gotSize != len(test.compressed) { t.Errorf("compressedTxOutSize (%s): did not get "+ "expected size - got %d, want %d", test.name, @@ -438,8 +424,7 @@ func TestCompressedTxOut(t *testing.T) { // Ensure the txout compresses to the expected value. gotCompressed := make([]byte, gotSize) gotBytesWritten := putCompressedTxOut(gotCompressed, - test.amount, test.scriptVersion, test.pkScript, - test.version, test.hasAmount) + test.amount, test.scriptVersion, test.pkScript, test.hasAmount) if !bytes.Equal(gotCompressed, test.compressed) { t.Errorf("compressTxOut (%s): did not get expected "+ "bytes - got %x, want %x", test.name, @@ -457,8 +442,7 @@ func TestCompressedTxOut(t *testing.T) { // Ensure the serialized bytes are decoded back to the expected // compressed values. gotAmount, gotScrVersion, gotScript, gotBytesRead, err := - decodeCompressedTxOut(test.compressed, test.version, - test.hasAmount) + decodeCompressedTxOut(test.compressed, test.hasAmount) if err != nil { t.Errorf("decodeCompressedTxOut (%s): unexpected "+ "error: %v", test.name, err) @@ -498,7 +482,7 @@ func TestTxOutCompressionErrors(t *testing.T) { // A compressed txout with a value and missing compressed script must error. compressedTxOut := hexToBytes("00") - _, _, _, _, err := decodeCompressedTxOut(compressedTxOut, 1, true) + _, _, _, _, err := decodeCompressedTxOut(compressedTxOut, true) if !isDeserializeErr(err) { t.Fatalf("decodeCompressedTxOut with value and missing "+ "compressed script did not return expected error type "+ @@ -508,7 +492,7 @@ func TestTxOutCompressionErrors(t *testing.T) { // A compressed txout without a value and with an empty compressed // script returns empty but is valid. compressedTxOut = hexToBytes("00") - _, _, _, _, err = decodeCompressedTxOut(compressedTxOut, 1, false) + _, _, _, _, err = decodeCompressedTxOut(compressedTxOut, false) if err != nil { t.Fatalf("decodeCompressedTxOut with missing compressed script "+ "did not return expected error type - got %T, want "+ @@ -517,7 +501,7 @@ func TestTxOutCompressionErrors(t *testing.T) { // A compressed txout with short compressed script must error. compressedTxOut = hexToBytes("0010") - _, _, _, _, err = decodeCompressedTxOut(compressedTxOut, 1, false) + _, _, _, _, err = decodeCompressedTxOut(compressedTxOut, false) if !isDeserializeErr(err) { t.Fatalf("decodeCompressedTxOut with short compressed script "+ "did not return expected error type - got %T, want "+ diff --git a/blockchain/upgrade.go b/blockchain/upgrade.go index 2703964535..6f0926d297 100644 --- a/blockchain/upgrade.go +++ b/blockchain/upgrade.go @@ -17,8 +17,10 @@ import ( "github.com/decred/dcrd/chaincfg/chainhash" "github.com/decred/dcrd/chaincfg/v3" "github.com/decred/dcrd/database/v2" + "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/gcs/v3" "github.com/decred/dcrd/gcs/v3/blockcf2" + "github.com/decred/dcrd/txscript/v4" "github.com/decred/dcrd/wire" ) @@ -550,6 +552,142 @@ func determineMinimalOutputsSizeV1(serialized []byte) (int, error) { return offset, nil } +// decodeCompressedScriptSizeV1 treats the passed serialized bytes as a v1 +// compressed script, possibly followed by other data, and returns the number of +// bytes it occupies taking into account the special encoding of the script size +// by the domain specific compression algorithm described above. +func decodeCompressedScriptSizeV1(serialized []byte) int { + const ( + // Hardcoded constants so updates do not affect old upgrades. + cstPayToPubKeyHash = 0 + cstPayToScriptHash = 1 + cstPayToPubKeyCompEven = 2 + cstPayToPubKeyCompOdd = 3 + cstPayToPubKeyUncompEven = 4 + cstPayToPubKeyUncompOdd = 5 + numSpecialScripts = 64 + ) + + scriptSize, bytesRead := deserializeVLQ(serialized) + if bytesRead == 0 { + return 0 + } + + switch scriptSize { + case cstPayToPubKeyHash: + return 21 + + case cstPayToScriptHash: + return 21 + + case cstPayToPubKeyCompEven, cstPayToPubKeyCompOdd, + cstPayToPubKeyUncompEven, cstPayToPubKeyUncompOdd: + return 33 + } + + scriptSize -= numSpecialScripts + scriptSize += uint64(bytesRead) + return int(scriptSize) +} + +// decompressScriptV1 returns the original script obtained by decompressing the +// passed v1 compressed script according to the domain specific compression +// algorithm described above. +// +// NOTE: The script parameter must already have been proven to be long enough +// to contain the number of bytes returned by decodeCompressedScriptSize or it +// will panic. This is acceptable since it is only an internal function. +func decompressScriptV1(compressedPkScript []byte) []byte { + const ( + // Hardcoded constants so updates do not affect old upgrades. + cstPayToPubKeyHash = 0 + cstPayToScriptHash = 1 + cstPayToPubKeyCompEven = 2 + cstPayToPubKeyCompOdd = 3 + cstPayToPubKeyUncompEven = 4 + cstPayToPubKeyUncompOdd = 5 + numSpecialScripts = 64 + ) + + // Empty scripts, specified by 0x00, are considered nil. + if len(compressedPkScript) == 0 { + return nil + } + + // Decode the script size and examine it for the special cases. + encodedScriptSize, bytesRead := deserializeVLQ(compressedPkScript) + switch encodedScriptSize { + // Pay-to-pubkey-hash script. The resulting script is: + // <20 byte hash> + case cstPayToPubKeyHash: + pkScript := make([]byte, 25) + pkScript[0] = txscript.OP_DUP + pkScript[1] = txscript.OP_HASH160 + pkScript[2] = txscript.OP_DATA_20 + copy(pkScript[3:], compressedPkScript[bytesRead:bytesRead+20]) + pkScript[23] = txscript.OP_EQUALVERIFY + pkScript[24] = txscript.OP_CHECKSIG + return pkScript + + // Pay-to-script-hash script. The resulting script is: + // <20 byte script hash> + case cstPayToScriptHash: + pkScript := make([]byte, 23) + pkScript[0] = txscript.OP_HASH160 + pkScript[1] = txscript.OP_DATA_20 + copy(pkScript[2:], compressedPkScript[bytesRead:bytesRead+20]) + pkScript[22] = txscript.OP_EQUAL + return pkScript + + // Pay-to-compressed-pubkey script. The resulting script is: + // <33 byte compressed pubkey> + case cstPayToPubKeyCompEven, cstPayToPubKeyCompOdd: + pkScript := make([]byte, 35) + pkScript[0] = txscript.OP_DATA_33 + oddness := byte(0x02) + if encodedScriptSize == cstPayToPubKeyCompOdd { + oddness = 0x03 + } + pkScript[1] = oddness + copy(pkScript[2:], compressedPkScript[bytesRead:bytesRead+32]) + pkScript[34] = txscript.OP_CHECKSIG + return pkScript + + // Pay-to-uncompressed-pubkey script. The resulting script is: + // <65 byte uncompressed pubkey> + case cstPayToPubKeyUncompEven, cstPayToPubKeyUncompOdd: + // Change the leading byte to the appropriate compressed pubkey + // identifier (0x02 or 0x03) so it can be decoded as a + // compressed pubkey. This really should never fail since the + // encoding ensures it is valid before compressing to this type. + compressedKey := make([]byte, 33) + oddness := byte(0x02) + if encodedScriptSize == cstPayToPubKeyUncompOdd { + oddness = 0x03 + } + compressedKey[0] = oddness + copy(compressedKey[1:], compressedPkScript[1:]) + key, err := secp256k1.ParsePubKey(compressedKey) + if err != nil { + return nil + } + + pkScript := make([]byte, 67) + pkScript[0] = txscript.OP_DATA_65 + copy(pkScript[1:], key.SerializeUncompressed()) + pkScript[66] = txscript.OP_CHECKSIG + return pkScript + } + + // When none of the special cases apply, the script was encoded using + // the general format, so reduce the script size by the number of + // special cases and return the unmodified script. + scriptSize := int(encodedScriptSize - numSpecialScripts) + pkScript := make([]byte, scriptSize) + copy(pkScript, compressedPkScript[bytesRead:bytesRead+scriptSize]) + return pkScript +} + // scriptSourceFromSpendJournalV1 uses the legacy v1 spend journal along with // the provided block to create a source of previous transaction scripts and // versions spent by the block. @@ -623,7 +761,6 @@ func scriptSourceFromSpendJournalV1(dbTx database.Tx, block *wire.MsgBlock) (scr v1TxTypeMask = 0x0c v1TxTypeShift = 2 v1TxTypeTicket = 1 - v1CompressionVer = 1 ) // Loop backwards through all transactions so everything is read in reverse @@ -674,8 +811,7 @@ func scriptSourceFromSpendJournalV1(dbTx database.Tx, block *wire.MsgBlock) (scr str := "unexpected end of data after script version" return nil, errDeserialize(str) } - scriptSize := decodeCompressedScriptSize(serialized[offset:], - v1CompressionVer) + scriptSize := decodeCompressedScriptSizeV1(serialized[offset:]) if scriptSize < 0 { str := "negative script size" return nil, errDeserialize(str) @@ -692,7 +828,7 @@ func scriptSourceFromSpendJournalV1(dbTx database.Tx, block *wire.MsgBlock) (scr prevOut := &txIn.PreviousOutPoint source[*prevOut] = scriptSourceEntry{ version: uint16(scriptVersion), - script: decompressScript(pkScript, v1CompressionVer), + script: decompressScriptV1(pkScript), } // Deserialize the tx version and minimal outputs for tickets as @@ -1494,8 +1630,7 @@ func migrateSpendJournalVersion1To2(ctx context.Context, db database.DB) error { str := "unexpected end of data after script version" return errDeserialize(str) } - scriptSize := decodeCompressedScriptSize(serialized[offset:], - v1CompressionVer) + scriptSize := decodeCompressedScriptSizeV1(serialized[offset:]) offset += scriptSize if fullySpent { if offset >= len(serialized) { @@ -2021,9 +2156,7 @@ func migrateUtxoSetVersion2To3(ctx context.Context, db database.DB) error { // Decode the compressed script size and ensure there are enough bytes // left in the slice for it. - const compressionVersion = 1 - scriptSize := decodeCompressedScriptSize(oldSerialized[offset:], - compressionVersion) + scriptSize := decodeCompressedScriptSizeV1(oldSerialized[offset:]) // Note: scriptSize == 0 is OK (an empty compressed script is valid) if scriptSize < 0 { return errDeserialize("negative script size") @@ -2466,9 +2599,7 @@ func migrateSpendJournalVersion2To3(ctx context.Context, b *BlockChain) error { // Decode the compressed script size and ensure there are enough bytes // left in the slice for it. - const compressionVersion = 1 - scriptSize := decodeCompressedScriptSize(v2Serialized[offset:], - compressionVersion) + scriptSize := decodeCompressedScriptSizeV1(v2Serialized[offset:]) // Note: scriptSize == 0 is OK (an empty compressed script is valid) if scriptSize < 0 { return false, errDeserialize("negative script size") @@ -2728,8 +2859,7 @@ func migrateSpendJournalVersion2To3(ctx context.Context, b *BlockChain) error { // Decode the compressed script size and ensure there are enough bytes // left in the slice for it. - scriptSize = decodeCompressedScriptSize(utxoSerialized[utxoOffset:], - compressionVersion) + scriptSize = decodeCompressedScriptSizeV1(utxoSerialized[utxoOffset:]) // Note: scriptSize == 0 is OK (an empty compressed script is valid) if scriptSize < 0 { return false, errDeserialize("negative script size")