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 750b4d2f14..0f3edf3fb0 100644 --- a/blockchain/chainio.go +++ b/blockchain/chainio.go @@ -550,7 +550,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) @@ -570,7 +570,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) @@ -598,7 +598,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 "+ @@ -932,7 +932,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) @@ -944,7 +944,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) @@ -981,8 +981,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 b8c6698eae..b6114a901e 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 61c4544f75..dd6fc0521e 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") @@ -2473,9 +2606,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") @@ -2732,8 +2863,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(v3UtxoSerialized[v3UtxoOffset:], - compressionVersion) + scriptSize = decodeCompressedScriptSizeV1(v3UtxoSerialized[v3UtxoOffset:]) // Note: scriptSize == 0 is OK (an empty compressed script is valid) if scriptSize < 0 { return false, errDeserialize("negative script size")