github.com/decred/dcrd/blockchain@v1.2.1/chainio_test.go (about) 1 // Copyright (c) 2015-2016 The btcsuite developers 2 // Copyright (c) 2015-2018 The Decred developers 3 // Use of this source code is governed by an ISC 4 // license that can be found in the LICENSE file. 5 6 package blockchain 7 8 import ( 9 "bytes" 10 "encoding/hex" 11 "errors" 12 "math/big" 13 "reflect" 14 "testing" 15 "time" 16 17 "github.com/decred/dcrd/blockchain/stake" 18 "github.com/decred/dcrd/blockchain/standalone" 19 "github.com/decred/dcrd/chaincfg/chainhash" 20 "github.com/decred/dcrd/database" 21 "github.com/decred/dcrd/wire" 22 ) 23 24 // newHashFromStr converts the passed big-endian hex string into a 25 // chainhash.Hash. It only differs from the one available in chainhash in that 26 // it ignores the error since it will only (and must only) be called with 27 // hard-coded, and therefore known good, hashes. 28 func newHashFromStr(hexStr string) *chainhash.Hash { 29 hash, _ := chainhash.NewHashFromStr(hexStr) 30 return hash 31 } 32 33 // hexToFinalState converts the passed hex string into an array of 6 bytes and 34 // will panic if there is an error. This is only provided for the hard-coded 35 // constants so errors in the source code can be detected. It will only (and 36 // must only) be called with hard-coded values. 37 func hexToFinalState(s string) [6]byte { 38 b, err := hex.DecodeString(s) 39 if err != nil { 40 panic("invalid hex in source file: " + s) 41 } 42 43 var finalState [6]byte 44 if len(b) != len(finalState) { 45 panic("invalid hex in source file: " + s) 46 } 47 copy(finalState[:], b) 48 return finalState 49 } 50 51 // hexToExtraData converts the passed hex string into an array of 32 bytes and 52 // will panic if there is an error. This is only provided for the hard-coded 53 // constants so errors in the source code can be detected. It will only (and 54 // must only) be called with hard-coded values. 55 func hexToExtraData(s string) [32]byte { 56 b, err := hex.DecodeString(s) 57 if err != nil { 58 panic("invalid hex in source file: " + s) 59 } 60 61 var extraData [32]byte 62 if len(b) != len(extraData) { 63 panic("invalid hex in source file: " + s) 64 } 65 copy(extraData[:], b) 66 return extraData 67 } 68 69 // isNotInMainChainErr returns whether or not the passed error is an 70 // errNotInMainChain error. 71 func isNotInMainChainErr(err error) bool { 72 _, ok := err.(errNotInMainChain) 73 return ok 74 } 75 76 // TestErrNotInMainChain ensures the functions related to errNotInMainChain work 77 // as expected. 78 func TestErrNotInMainChain(t *testing.T) { 79 errStr := "no block at height 1 exists" 80 err := error(errNotInMainChain(errStr)) 81 82 // Ensure the stringized output for the error is as expected. 83 if err.Error() != errStr { 84 t.Fatalf("errNotInMainChain retuned unexpected error string - "+ 85 "got %q, want %q", err.Error(), errStr) 86 } 87 88 // Ensure error is detected as the correct type. 89 if !isNotInMainChainErr(err) { 90 t.Fatalf("isNotInMainChainErr did not detect as expected type") 91 } 92 err = errors.New("something else") 93 if isNotInMainChainErr(err) { 94 t.Fatalf("isNotInMainChainErr detected incorrect type") 95 } 96 } 97 98 // TestBlockIndexSerialization ensures serializing and deserializing block index 99 // entries works as expected. 100 func TestBlockIndexSerialization(t *testing.T) { 101 t.Parallel() 102 103 // base data is based on block 150287 on mainnet and serves as a template 104 // for the various tests below. 105 baseHeader := wire.BlockHeader{ 106 Version: 4, 107 PrevBlock: *newHashFromStr("000000000000016916671ae225343a5ee131c999d5cadb6348805db25737731f"), 108 MerkleRoot: *newHashFromStr("5ef2bb79795d7503c0ccc5cb6e0d4731992fc8c8c5b332c1c0e2c687d864c666"), 109 StakeRoot: *newHashFromStr("022965059b7527dc2bc18daaa533f806eda1f96fd0b04bbda2381f5552d7c2de"), 110 VoteBits: 0x0001, 111 FinalState: hexToFinalState("313e16e64c0b"), 112 Voters: 4, 113 FreshStake: 3, 114 Revocations: 2, 115 PoolSize: 41332, 116 Bits: 0x1a016f98, 117 SBits: 7473162478, 118 Height: 150287, 119 Size: 11295, 120 Timestamp: time.Unix(1499907127, 0), 121 Nonce: 4116576260, 122 ExtraData: hexToExtraData("8f01ed92645e0a6b11ee3b3c0000000000000000000000000000000000000000"), 123 StakeVersion: 4, 124 } 125 baseTicketsVoted := []chainhash.Hash{ 126 *newHashFromStr("8b62a877544753ea80a822142a48ec066170e9381d21a9e8a84bc7373f0f9b2e"), 127 *newHashFromStr("4427a003a7aceb1404ffd9072e9aff1e128a24333a543332030e91668a389db7"), 128 *newHashFromStr("4415b88ac74881d7b6b15d41df465257cd1cc92d55e95f1b648434aef3a2110b"), 129 *newHashFromStr("9d2621b57352088809d3a069b04b76c832f30a76da14e56aece72208b3e5b87a"), 130 } 131 baseTicketsRevoked := []chainhash.Hash{ 132 *newHashFromStr("8146f01b8ffca8008ebc80293d2978d63b1dffa5c456a73e7b39a9b1e695e8eb"), 133 *newHashFromStr("2292ff2461e725c58cc6e2051eac2a10e6ee6d1f62327ed676b7a196fb94be0c"), 134 } 135 baseVoteInfo := []stake.VoteVersionTuple{ 136 {Version: 4, Bits: 0x0001}, 137 {Version: 4, Bits: 0x0015}, 138 {Version: 4, Bits: 0x0015}, 139 {Version: 4, Bits: 0x0001}, 140 } 141 142 tests := []struct { 143 name string 144 entry blockIndexEntry 145 serialized []byte 146 }{ 147 { 148 name: "no votes, no revokes", 149 entry: blockIndexEntry{ 150 header: baseHeader, 151 status: statusDataStored | statusValid, 152 voteInfo: nil, 153 ticketsVoted: nil, 154 ticketsRevoked: nil, 155 }, 156 serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f833a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f011aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0a6b11ee3b3c000000000000000000000000000000000000000004000000030000"), 157 }, 158 { 159 name: "1 vote, no revokes", 160 entry: blockIndexEntry{ 161 header: baseHeader, 162 status: statusDataStored | statusValid, 163 voteInfo: baseVoteInfo[:1], 164 ticketsVoted: baseTicketsVoted[:1], 165 ticketsRevoked: nil, 166 }, 167 serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f833a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f011aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0a6b11ee3b3c00000000000000000000000000000000000000000400000003012e9b0f3f37c74ba8e8a9211d38e9706106ec482a1422a880ea53475477a8628b040100"), 168 }, 169 { 170 name: "no votes, 1 revoke", 171 entry: blockIndexEntry{ 172 header: baseHeader, 173 status: statusDataStored | statusValid, 174 voteInfo: nil, 175 ticketsVoted: nil, 176 ticketsRevoked: baseTicketsRevoked[:1], 177 }, 178 serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f833a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f011aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0a6b11ee3b3c000000000000000000000000000000000000000004000000030001ebe895e6b1a9397b3ea756c4a5ff1d3bd678293d2980bc8e00a8fc8f1bf04681"), 179 }, 180 { 181 name: "4 votes, same vote versions, different vote bits, 2 revokes", 182 entry: blockIndexEntry{ 183 header: baseHeader, 184 status: statusDataStored | statusValid, 185 voteInfo: baseVoteInfo, 186 ticketsVoted: baseTicketsVoted, 187 ticketsRevoked: baseTicketsRevoked, 188 }, 189 serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f833a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f011aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0a6b11ee3b3c00000000000000000000000000000000000000000400000003042e9b0f3f37c74ba8e8a9211d38e9706106ec482a1422a880ea53475477a8628b0401b79d388a66910e033233543a33248a121eff9a2e07d9ff0414ebaca703a0274404150b11a2f3ae3484641b5fe9552dc91ccd575246df415db1b6d78148c78ab8154404157ab8e5b30822e7ec6ae514da760af332c8764bb069a0d30988085273b521269d040102ebe895e6b1a9397b3ea756c4a5ff1d3bd678293d2980bc8e00a8fc8f1bf046810cbe94fb96a1b776d67e32621f6deee6102aac1e05e2c68cc525e76124ff9222"), 190 }, 191 } 192 193 for _, test := range tests { 194 // Ensure the function to calculate the serialized size without 195 // actually serializing it is calculated properly. 196 gotSize := blockIndexEntrySerializeSize(&test.entry) 197 if gotSize != len(test.serialized) { 198 t.Errorf("blockIndexEntrySerializeSize (%s): did not "+ 199 "get expected size - got %d, want %d", test.name, 200 gotSize, len(test.serialized)) 201 } 202 203 // Ensure the block index entry serializes to the expected value. 204 gotSerialized, err := serializeBlockIndexEntry(&test.entry) 205 if err != nil { 206 t.Errorf("serializeBlockIndexEntry (%s): unexpected "+ 207 "error: %v", test.name, err) 208 continue 209 } 210 if !bytes.Equal(gotSerialized, test.serialized) { 211 t.Errorf("serializeBlockIndexEntry (%s): did not get "+ 212 "expected bytes - got %x, want %x", test.name, 213 gotSerialized, test.serialized) 214 continue 215 } 216 217 // Ensure the block index entry serializes to the expected value 218 // and produces the expected number of bytes written via a 219 // direct put. 220 gotSerialized2 := make([]byte, gotSize) 221 gotBytesWritten, err := putBlockIndexEntry(gotSerialized2, 222 &test.entry) 223 if err != nil { 224 t.Errorf("putBlockIndexEntry (%s): unexpected error: %v", 225 test.name, err) 226 continue 227 } 228 if !bytes.Equal(gotSerialized2, test.serialized) { 229 t.Errorf("putBlockIndexEntry (%s): did not get "+ 230 "expected bytes - got %x, want %x", test.name, 231 gotSerialized2, test.serialized) 232 continue 233 } 234 if gotBytesWritten != len(test.serialized) { 235 t.Errorf("putBlockIndexEntry (%s): did not get "+ 236 "expected number of bytes written - got %d, "+ 237 "want %d", test.name, gotBytesWritten, 238 len(test.serialized)) 239 continue 240 } 241 242 // Ensure the serialized bytes are decoded back to the expected 243 // block index entry. 244 gotEntry, err := deserializeBlockIndexEntry(test.serialized) 245 if err != nil { 246 t.Errorf("deserializeBlockIndexEntry (%s): unexpected "+ 247 "error: %v", test.name, err) 248 continue 249 } 250 if !reflect.DeepEqual(*gotEntry, test.entry) { 251 t.Errorf("deserializeBlockIndexEntry (%s): mismatched "+ 252 "entries\ngot %+v\nwant %+v", test.name, 253 gotEntry, test.entry) 254 continue 255 } 256 257 // Ensure the serialized bytes are decoded back to the expected 258 // block index entry. 259 var gotEntry2 blockIndexEntry 260 bytesRead, err := decodeBlockIndexEntry(test.serialized, &gotEntry2) 261 if err != nil { 262 t.Errorf("decodeBlockIndexEntry (%s): unexpected "+ 263 "error: %v", test.name, err) 264 continue 265 } 266 if !reflect.DeepEqual(gotEntry2, test.entry) { 267 t.Errorf("decodeBlockIndexEntry (%s): mismatched "+ 268 "entries\ngot %+v\nwant %+v", test.name, 269 gotEntry2, test.entry) 270 continue 271 } 272 if bytesRead != len(test.serialized) { 273 t.Errorf("decodeBlockIndexEntry (%s): did not get "+ 274 "expected number of bytes read - got %d, "+ 275 "want %d", test.name, bytesRead, 276 len(test.serialized)) 277 continue 278 } 279 } 280 } 281 282 // TestBlockIndexDecodeErrors performs negative tests against decoding block 283 // index entries to ensure error paths work as expected. 284 func TestBlockIndexDecodeErrors(t *testing.T) { 285 t.Parallel() 286 tests := []struct { 287 name string 288 entry blockIndexEntry 289 serialized []byte 290 bytesRead int // Expected number of bytes read. 291 errType error 292 }{ 293 { 294 name: "nothing serialized", 295 entry: blockIndexEntry{}, 296 serialized: hexToBytes(""), 297 errType: errDeserialize(""), 298 bytesRead: 0, 299 }, 300 { 301 name: "no data after block header", 302 entry: blockIndexEntry{}, 303 serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f833a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f011aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0a6b11ee3b3c000000000000000000000000000000000000000004000000"), 304 errType: errDeserialize(""), 305 bytesRead: 180, 306 }, 307 { 308 name: "no data after status", 309 entry: blockIndexEntry{}, 310 serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f833a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f011aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0a6b11ee3b3c00000000000000000000000000000000000000000400000003"), 311 errType: errDeserialize(""), 312 bytesRead: 181, 313 }, 314 { 315 name: "no data after num votes with no votes", 316 entry: blockIndexEntry{}, 317 serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f833a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f011aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0a6b11ee3b3c0000000000000000000000000000000000000000040000000300"), 318 errType: errDeserialize(""), 319 bytesRead: 182, 320 }, 321 { 322 name: "no data after num votes with votes", 323 entry: blockIndexEntry{}, 324 serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f833a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f011aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0a6b11ee3b3c0000000000000000000000000000000000000000040000000301"), 325 errType: errDeserialize(""), 326 bytesRead: 182, 327 }, 328 { 329 name: "short data in vote ticket hash", 330 entry: blockIndexEntry{}, 331 serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f833a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f011aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0a6b11ee3b3c00000000000000000000000000000000000000000400000003012e9b0f3f37c74ba8e8a9211d38e9706106ec482a1422a880ea53475477a862"), 332 errType: errDeserialize(""), 333 bytesRead: 182, 334 }, 335 { 336 name: "no data after vote ticket hash", 337 entry: blockIndexEntry{}, 338 serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f833a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f011aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0a6b11ee3b3c00000000000000000000000000000000000000000400000003012e9b0f3f37c74ba8e8a9211d38e9706106ec482a1422a880ea53475477a8628b"), 339 errType: errDeserialize(""), 340 bytesRead: 214, 341 }, 342 { 343 name: "no data after vote version", 344 entry: blockIndexEntry{}, 345 serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f833a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f011aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0a6b11ee3b3c00000000000000000000000000000000000000000400000003012e9b0f3f37c74ba8e8a9211d38e9706106ec482a1422a880ea53475477a8628b04"), 346 errType: errDeserialize(""), 347 bytesRead: 215, 348 }, 349 { 350 name: "no data after votes", 351 entry: blockIndexEntry{}, 352 serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f833a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f011aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0a6b11ee3b3c00000000000000000000000000000000000000000400000003012e9b0f3f37c74ba8e8a9211d38e9706106ec482a1422a880ea53475477a8628b0401"), 353 errType: errDeserialize(""), 354 bytesRead: 216, 355 }, 356 { 357 name: "no data after num revokes with revokes", 358 entry: blockIndexEntry{}, 359 serialized: hexToBytes("040000001f733757b25d804863dbcad599c931e15e3a3425e21a6716690100000000000066c664d887c6e2c0c132b3c5c8c82f9931470d6ecbc5ccc003755d7979bbf25edec2d752551f38a2bd4bb0d06ff9a1ed06f833a5aa8dc12bdc27759b056529020100313e16e64c0b0400030274a10000986f011aee686fbd010000000f4b02001f2c000037c4665904f85df58f01ed92645e0a6b11ee3b3c000000000000000000000000000000000000000004000000030001"), 360 errType: errDeserialize(""), 361 bytesRead: 183, 362 }, 363 } 364 365 for _, test := range tests { 366 // Ensure the expected error type is returned. 367 gotBytesRead, err := decodeBlockIndexEntry(test.serialized, 368 &test.entry) 369 if reflect.TypeOf(err) != reflect.TypeOf(test.errType) { 370 t.Errorf("decodeBlockIndexEntry (%s): expected error "+ 371 "type does not match - got %T, want %T", 372 test.name, err, test.errType) 373 continue 374 } 375 376 // Ensure the expected number of bytes read is returned. 377 if gotBytesRead != test.bytesRead { 378 t.Errorf("decodeBlockIndexEntry (%s): unexpected "+ 379 "number of bytes read - got %d, want %d", 380 test.name, gotBytesRead, test.bytesRead) 381 continue 382 } 383 } 384 } 385 386 // TestStxoSerialization ensures serializing and deserializing spent transaction 387 // output entries works as expected. 388 func TestStxoSerialization(t *testing.T) { 389 t.Parallel() 390 391 tests := []struct { 392 name string 393 stxo spentTxOut 394 txVersion int32 // When the txout is not fully spent. 395 serialized []byte 396 }{ 397 { 398 name: "Spends last output of coinbase", 399 stxo: spentTxOut{ 400 amount: 9999, 401 scriptVersion: 0, 402 pkScript: hexToBytes("76a9146edbc6c4d31bae9f1ccc38538a114bf42de65e8688ac"), 403 height: 12345, 404 index: 54321, 405 isCoinBase: true, 406 hasExpiry: false, 407 txFullySpent: true, 408 txType: 0, 409 txVersion: 1, 410 }, 411 serialized: hexToBytes("1100006edbc6c4d31bae9f1ccc38538a114bf42de65e8601"), 412 }, 413 { 414 name: "Spends last output of non coinbase and is a ticket", 415 stxo: spentTxOut{ 416 amount: 9999, 417 scriptVersion: 0, 418 pkScript: hexToBytes("76a9146edbc6c4d31bae9f1ccc38538a114bf42de65e8688ac"), 419 height: 12345, 420 index: 54321, 421 isCoinBase: false, 422 hasExpiry: false, 423 txFullySpent: true, 424 txType: 1, 425 txVersion: 1, 426 stakeExtra: []byte{0x00}, 427 }, 428 serialized: hexToBytes("1400006edbc6c4d31bae9f1ccc38538a114bf42de65e860100"), 429 }, 430 { 431 name: "Does not spend last output", 432 stxo: spentTxOut{ 433 amount: 34405000000, 434 pkScript: hexToBytes("76a9146edbc6c4d31bae9f1ccc38538a114bf42de65e8688ac"), 435 scriptVersion: 0, 436 }, 437 txVersion: 1, 438 serialized: hexToBytes("0000006edbc6c4d31bae9f1ccc38538a114bf42de65e86"), 439 }, 440 } 441 442 for _, test := range tests { 443 // Ensure the function to calculate the serialized size without 444 // actually serializing it is calculated properly. 445 gotSize := spentTxOutSerializeSize(&test.stxo) 446 if gotSize != len(test.serialized) { 447 t.Errorf("spentTxOutSerializeSize (%s): did not get "+ 448 "expected size - got %d, want %d", test.name, 449 gotSize, len(test.serialized)) 450 } 451 452 // Ensure the stxo serializes to the expected value. 453 gotSerialized := make([]byte, gotSize) 454 gotBytesWritten := putSpentTxOut(gotSerialized, &test.stxo) 455 if !bytes.Equal(gotSerialized, test.serialized) { 456 t.Errorf("putSpentTxOut (%s): did not get expected "+ 457 "bytes - got %x, want %x", test.name, 458 gotSerialized, test.serialized) 459 continue 460 } 461 if gotBytesWritten != len(test.serialized) { 462 t.Errorf("putSpentTxOut (%s): did not get expected "+ 463 "number of bytes written - got %d, want %d", 464 test.name, gotBytesWritten, 465 len(test.serialized)) 466 continue 467 } 468 469 // Ensure the serialized bytes are decoded back to the expected 470 // stxo. 471 var gotStxo spentTxOut 472 offset, err := decodeSpentTxOut(test.serialized, &gotStxo, 473 test.stxo.amount, test.stxo.height, test.stxo.index) 474 if err != nil { 475 t.Errorf("decodeSpentTxOut (%s): unexpected error: %v", 476 test.name, err) 477 continue 478 } 479 if offset != len(test.serialized) { 480 t.Errorf("decodeSpentTxOut (%s): did not get expected "+ 481 "number of bytes read - got %d, want %d", 482 test.name, offset, len(test.serialized)) 483 continue 484 } 485 } 486 } 487 488 // TestStxoDecodeErrors performs negative tests against decoding spent 489 // transaction outputs to ensure error paths work as expected. 490 func TestStxoDecodeErrors(t *testing.T) { 491 t.Parallel() 492 493 tests := []struct { 494 name string 495 stxo spentTxOut 496 txVersion int32 // When the txout is not fully spent. 497 serialized []byte 498 bytesRead int // Expected number of bytes read. 499 errType error 500 }{ 501 { 502 name: "nothing serialized", 503 stxo: spentTxOut{}, 504 serialized: hexToBytes(""), 505 errType: errDeserialize(""), 506 bytesRead: 0, 507 }, 508 { 509 name: "no data after flags w/o version", 510 stxo: spentTxOut{}, 511 serialized: hexToBytes("00"), 512 errType: errDeserialize(""), 513 bytesRead: 1, 514 }, 515 { 516 name: "no data after flags code", 517 stxo: spentTxOut{}, 518 serialized: hexToBytes("14"), 519 errType: errDeserialize(""), 520 bytesRead: 1, 521 }, 522 { 523 name: "no tx version data after script", 524 stxo: spentTxOut{}, 525 serialized: hexToBytes("1400016edbc6c4d31bae9f1ccc38538a114bf42de65e86"), 526 errType: errDeserialize(""), 527 bytesRead: 23, 528 }, 529 { 530 name: "no stakeextra data after script for ticket", 531 stxo: spentTxOut{}, 532 serialized: hexToBytes("1400016edbc6c4d31bae9f1ccc38538a114bf42de65e8601"), 533 errType: errDeserialize(""), 534 bytesRead: 24, 535 }, 536 { 537 name: "incomplete compressed txout", 538 stxo: spentTxOut{}, 539 txVersion: 1, 540 serialized: hexToBytes("1432"), 541 errType: errDeserialize(""), 542 bytesRead: 2, 543 }, 544 } 545 546 for _, test := range tests { 547 // Ensure the expected error type is returned. 548 gotBytesRead, err := decodeSpentTxOut(test.serialized, 549 &test.stxo, test.stxo.amount, test.stxo.height, test.stxo.index) 550 if reflect.TypeOf(err) != reflect.TypeOf(test.errType) { 551 t.Errorf("decodeSpentTxOut (%s): expected error type "+ 552 "does not match - got %T, want %T", test.name, 553 err, test.errType) 554 continue 555 } 556 557 // Ensure the expected number of bytes read is returned. 558 if gotBytesRead != test.bytesRead { 559 t.Errorf("decodeSpentTxOut (%s): unexpected number of "+ 560 "bytes read - got %d, want %d", test.name, 561 gotBytesRead, test.bytesRead) 562 continue 563 } 564 } 565 } 566 567 // TestSpendJournalSerialization ensures serializing and deserializing spend 568 // journal entries works as expected. 569 func TestSpendJournalSerialization(t *testing.T) { 570 t.Parallel() 571 572 tests := []struct { 573 name string 574 entry []spentTxOut 575 blockTxns []*wire.MsgTx 576 utxoView *UtxoViewpoint 577 serialized []byte 578 }{ 579 { 580 name: "No spends", 581 entry: nil, 582 blockTxns: nil, 583 utxoView: NewUtxoViewpoint(), 584 serialized: nil, 585 }, 586 { 587 name: "One tx with one input spends last output of coinbase", 588 entry: []spentTxOut{{ 589 amount: 5000000000, 590 scriptVersion: 0, 591 pkScript: hexToBytes("0511db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5c"), 592 compressed: true, 593 height: 9, 594 index: 0, 595 isCoinBase: true, 596 hasExpiry: false, 597 txFullySpent: true, 598 txType: 0, 599 txVersion: 1, 600 stakeExtra: nil, 601 }}, 602 blockTxns: []*wire.MsgTx{{ // Coinbase omitted. 603 SerType: wire.TxSerializeFull, 604 Version: 1, 605 TxIn: []*wire.TxIn{{ 606 PreviousOutPoint: wire.OutPoint{ 607 Hash: *newHashFromStr("0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9"), 608 Index: 0, 609 Tree: 0, 610 }, 611 SignatureScript: hexToBytes("47304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4acdd12909d831cc56cbbac4622082221a8768d1d0901"), 612 Sequence: 0xffffffff, 613 BlockHeight: 9, 614 BlockIndex: 0, 615 ValueIn: 5000000000, 616 }}, 617 TxOut: []*wire.TxOut{{ 618 Value: 1000000000, 619 Version: 0, 620 PkScript: hexToBytes("4104ae1a62fe09c5f51b13905f07f06b99a2f7159b2225f374cd378d71302fa28414e7aab37397f554a7df5f142c21c1b7303b8a0626f1baded5c72a704f7e6cd84cac"), 621 }, { 622 Value: 4000000000, 623 Version: 0, 624 PkScript: hexToBytes("410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3ac"), 625 }}, 626 LockTime: 0, 627 Expiry: 0, 628 }}, 629 utxoView: NewUtxoViewpoint(), 630 serialized: hexToBytes("11000511db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5c01"), 631 }, 632 { 633 name: "Two txns when one spends last output, one doesn't", 634 entry: []spentTxOut{{ 635 amount: 34405000000, 636 height: 321, 637 index: 123, 638 scriptVersion: 0, 639 pkScript: hexToBytes("016edbc6c4d31bae9f1ccc38538a114bf42de65e86"), 640 compressed: true, 641 }, { 642 amount: 13761000000, 643 scriptVersion: 0, 644 pkScript: hexToBytes("01b2fb57eadf61e106a100a7445a8c3f67898841ec"), 645 compressed: true, 646 height: 3214, 647 index: 1234, 648 isCoinBase: false, 649 hasExpiry: false, 650 txFullySpent: true, 651 txType: 0, 652 txVersion: 1, 653 stakeExtra: nil, 654 }}, 655 blockTxns: []*wire.MsgTx{{ // Coinbase omitted. 656 SerType: wire.TxSerializeFull, 657 Version: 1, 658 TxIn: []*wire.TxIn{{ 659 PreviousOutPoint: wire.OutPoint{ 660 Hash: *newHashFromStr("c0ed017828e59ad5ed3cf70ee7c6fb0f426433047462477dc7a5d470f987a537"), 661 Index: 1, 662 Tree: 0, 663 }, 664 SignatureScript: hexToBytes("493046022100c167eead9840da4a033c9a56470d7794a9bb1605b377ebe5688499b39f94be59022100fb6345cab4324f9ea0b9ee9169337534834638d818129778370f7d378ee4a325014104d962cac5390f12ddb7539507065d0def320d68c040f2e73337c3a1aaaab7195cb5c4d02e0959624d534f3c10c3cf3d73ca5065ebd62ae986b04c6d090d32627c"), 665 Sequence: 0xffffffff, 666 BlockHeight: 321, 667 BlockIndex: 123, 668 ValueIn: 34405000000, 669 }}, 670 TxOut: []*wire.TxOut{{ 671 Value: 5000000, 672 Version: 0, 673 PkScript: hexToBytes("76a914f419b8db4ba65f3b6fcc233acb762ca6f51c23d488ac"), 674 }, { 675 Value: 34400000000, 676 Version: 0, 677 PkScript: hexToBytes("76a914cadf4fc336ab3c6a4610b75f31ba0676b7f663d288ac"), 678 }}, 679 LockTime: 0, 680 Expiry: 0, 681 }, { 682 SerType: wire.TxSerializeFull, 683 Version: 1, 684 TxIn: []*wire.TxIn{{ 685 PreviousOutPoint: wire.OutPoint{ 686 Hash: *newHashFromStr("92fbe1d4be82f765dfabc9559d4620864b05cc897c4db0e29adac92d294e52b7"), 687 Index: 0, 688 Tree: 0, 689 }, 690 SignatureScript: hexToBytes("483045022100e256743154c097465cf13e89955e1c9ff2e55c46051b627751dee0144183157e02201d8d4f02cde8496aae66768f94d35ce54465bd4ae8836004992d3216a93a13f00141049d23ce8686fe9b802a7a938e8952174d35dd2c2089d4112001ed8089023ab4f93a3c9fcd5bfeaa9727858bf640dc1b1c05ec3b434bb59837f8640e8810e87742"), 691 Sequence: 0xffffffff, 692 BlockHeight: 3214, 693 BlockIndex: 1234, 694 ValueIn: 13761000000, 695 }}, 696 TxOut: []*wire.TxOut{{ 697 Value: 5000000, 698 Version: 0, 699 PkScript: hexToBytes("76a914a983ad7c92c38fc0e2025212e9f972204c6e687088ac"), 700 }, { 701 Value: 13756000000, 702 Version: 0, 703 PkScript: hexToBytes("76a914a6ebd69952ab486a7a300bfffdcb395dc7d47c2388ac"), 704 }}, 705 LockTime: 0, 706 Expiry: 0, 707 }}, 708 utxoView: &UtxoViewpoint{entries: map[chainhash.Hash]*UtxoEntry{ 709 *newHashFromStr("c0ed017828e59ad5ed3cf70ee7c6fb0f426433047462477dc7a5d470f987a537"): { 710 txVersion: 1, 711 isCoinBase: false, 712 hasExpiry: false, 713 height: 321, 714 index: 123, 715 sparseOutputs: map[uint32]*utxoOutput{ 716 1: { 717 amount: 34405000000, 718 scriptVersion: 0, 719 pkScript: hexToBytes("76a9142084541c3931677527a7eafe56fd90207c344eb088ac"), 720 compressed: false, 721 }, 722 }, 723 }, 724 }}, 725 serialized: hexToBytes("100001b2fb57eadf61e106a100a7445a8c3f67898841ec010000016edbc6c4d31bae9f1ccc38538a114bf42de65e86"), 726 }, 727 { 728 name: "One tx, two inputs from same tx, neither spend last output", 729 entry: []spentTxOut{{ 730 amount: 159747816, 731 height: 1111, 732 index: 2222, 733 scriptVersion: 0, 734 pkScript: hexToBytes("4151"), 735 compressed: true, 736 }, { 737 amount: 159747816, 738 height: 3333, 739 index: 4444, 740 scriptVersion: 0, 741 pkScript: hexToBytes("4151"), 742 compressed: true, 743 }}, 744 blockTxns: []*wire.MsgTx{{ // Coinbase omitted. 745 SerType: wire.TxSerializeFull, 746 Version: 1, 747 TxIn: []*wire.TxIn{{ 748 PreviousOutPoint: wire.OutPoint{ 749 Hash: *newHashFromStr("c0ed017828e59ad5ed3cf70ee7c6fb0f426433047462477dc7a5d470f987a537"), 750 Index: 1, 751 }, 752 SignatureScript: hexToBytes(""), 753 Sequence: 0xffffffff, 754 BlockHeight: 1111, 755 BlockIndex: 2222, 756 ValueIn: 159747816, 757 }, { 758 PreviousOutPoint: wire.OutPoint{ 759 Hash: *newHashFromStr("c0ed017828e59ad5ed3cf70ee7c6fb0f426433047462477dc7a5d470f987a537"), 760 Index: 2, 761 }, 762 SignatureScript: hexToBytes(""), 763 Sequence: 0xffffffff, 764 BlockHeight: 3333, 765 BlockIndex: 4444, 766 ValueIn: 159747816, 767 }}, 768 TxOut: []*wire.TxOut{{ 769 Value: 165125632, 770 Version: 0, 771 PkScript: hexToBytes("51"), 772 }, { 773 Value: 154370000, 774 Version: 0, 775 PkScript: hexToBytes("51"), 776 }}, 777 LockTime: 0, 778 Expiry: 0, 779 }}, 780 utxoView: &UtxoViewpoint{entries: map[chainhash.Hash]*UtxoEntry{ 781 *newHashFromStr("c0ed017828e59ad5ed3cf70ee7c6fb0f426433047462477dc7a5d470f987a537"): { 782 txVersion: 1, 783 isCoinBase: false, 784 hasExpiry: false, 785 height: 100000, 786 sparseOutputs: map[uint32]*utxoOutput{ 787 0: { 788 amount: 165712179, 789 pkScript: hexToBytes("51"), 790 }, 791 }, 792 }, 793 }}, 794 serialized: hexToBytes("0000415100004151"), 795 }, 796 } 797 798 for i, test := range tests { 799 // Ensure the journal entry serializes to the expected value. 800 gotBytes, err := serializeSpendJournalEntry(test.entry) 801 if err != nil { 802 t.Errorf("serializeSpendJournalEntry #%d (%s) "+ 803 "unexpected error: %v", i, test.name, err) 804 continue 805 } 806 if !bytes.Equal(gotBytes, test.serialized) { 807 t.Errorf("serializeSpendJournalEntry #%d (%s): "+ 808 "mismatched bytes - got %x, want %x", i, 809 test.name, gotBytes, test.serialized) 810 continue 811 } 812 813 // Deserialize to a spend journal entry. 814 gotEntry, err := deserializeSpendJournalEntry(test.serialized, 815 test.blockTxns) 816 if err != nil { 817 t.Errorf("deserializeSpendJournalEntry #%d (%s) "+ 818 "unexpected error: %v", i, test.name, err) 819 continue 820 } 821 822 // Ensure that the deserialized spend journal entry has the 823 // correct properties. 824 for j := range gotEntry { 825 if !reflect.DeepEqual(gotEntry[j], test.entry[j]) { 826 t.Errorf("deserializeSpendJournalEntry #%d (%s) "+ 827 "mismatched entries in idx %v - got %v, want %v", 828 i, test.name, j, gotEntry[j], test.entry[j]) 829 continue 830 } 831 } 832 } 833 } 834 835 // TestSpendJournalErrors performs negative tests against deserializing spend 836 // journal entries to ensure error paths work as expected. 837 func TestSpendJournalErrors(t *testing.T) { 838 t.Parallel() 839 840 tests := []struct { 841 name string 842 blockTxns []*wire.MsgTx 843 serialized []byte 844 errType error 845 }{ 846 // Adapted from block 170 in main blockchain. 847 { 848 name: "Force assertion due to missing stxos", 849 blockTxns: []*wire.MsgTx{{ // Coinbase omitted. 850 SerType: wire.TxSerializeFull, 851 Version: 1, 852 TxIn: []*wire.TxIn{{ 853 PreviousOutPoint: wire.OutPoint{ 854 Hash: *newHashFromStr("0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9"), 855 Index: 0, 856 }, 857 SignatureScript: hexToBytes("47304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4acdd12909d831cc56cbbac4622082221a8768d1d0901"), 858 Sequence: 0xffffffff, 859 }}, 860 LockTime: 0, 861 }}, 862 serialized: hexToBytes(""), 863 errType: AssertError(""), 864 }, 865 { 866 name: "Force deserialization error in stxos", 867 blockTxns: []*wire.MsgTx{{ // Coinbase omitted. 868 SerType: wire.TxSerializeFull, 869 Version: 1, 870 TxIn: []*wire.TxIn{{ 871 PreviousOutPoint: wire.OutPoint{ 872 Hash: *newHashFromStr("0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9"), 873 Index: 0, 874 }, 875 SignatureScript: hexToBytes("47304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4acdd12909d831cc56cbbac4622082221a8768d1d0901"), 876 Sequence: 0xffffffff, 877 }}, 878 LockTime: 0, 879 }}, 880 serialized: hexToBytes("1301320511db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a"), 881 errType: errDeserialize(""), 882 }, 883 } 884 885 for _, test := range tests { 886 // Ensure the expected error type is returned and the returned 887 // slice is nil. 888 stxos, err := deserializeSpendJournalEntry(test.serialized, 889 test.blockTxns) 890 if reflect.TypeOf(err) != reflect.TypeOf(test.errType) { 891 t.Errorf("deserializeSpendJournalEntry (%s): expected "+ 892 "error type does not match - got %T, want %T", 893 test.name, err, test.errType) 894 continue 895 } 896 if stxos != nil { 897 t.Errorf("deserializeSpendJournalEntry (%s): returned "+ 898 "slice of spent transaction outputs is not nil", 899 test.name) 900 continue 901 } 902 } 903 } 904 905 // TestUtxoSerialization ensures serializing and deserializing unspent 906 // trasaction output entries works as expected. 907 func TestUtxoSerialization(t *testing.T) { 908 t.Parallel() 909 910 tests := []struct { 911 name string 912 entry *UtxoEntry 913 serialized []byte 914 }{ 915 { 916 name: "Only output 0, coinbase, even uncomp pubkey", 917 entry: &UtxoEntry{ 918 txVersion: 1, 919 isCoinBase: true, 920 hasExpiry: false, 921 txType: 0, 922 height: 12345, 923 index: 54321, 924 stakeExtra: nil, 925 sparseOutputs: map[uint32]*utxoOutput{ 926 0: { 927 amount: 5000000000, 928 scriptVersion: 0, 929 pkScript: hexToBytes("410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac"), 930 compressed: false, 931 }, 932 }, 933 }, 934 serialized: hexToBytes("01df3982a731010132000496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52"), 935 }, 936 { 937 name: "Only output 0, coinbase, odd uncomp pubkey", 938 entry: &UtxoEntry{ 939 txVersion: 1, 940 isCoinBase: true, 941 hasExpiry: false, 942 txType: 0, 943 height: 12345, 944 index: 54321, 945 stakeExtra: nil, 946 sparseOutputs: map[uint32]*utxoOutput{ 947 0: { 948 amount: 5000000000, 949 scriptVersion: 0, 950 pkScript: hexToBytes("410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52258a76c86aea2b1f59fb07ebe87e19dd6b8dee99409de18c57d340dbbd37a341ac"), 951 compressed: false, 952 }, 953 }, 954 }, 955 serialized: hexToBytes("01df3982a731010132000596b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52"), 956 }, 957 { 958 name: "Only output 1, not coinbase", 959 entry: &UtxoEntry{ 960 txVersion: 1, 961 isCoinBase: false, 962 hasExpiry: false, 963 txType: 0, 964 height: 55555, 965 index: 1, 966 stakeExtra: nil, 967 sparseOutputs: map[uint32]*utxoOutput{ 968 1: { 969 amount: 1000000, 970 scriptVersion: 0, 971 pkScript: hexToBytes("76a914ee8bd501094a7d5ca318da2506de35e1cb025ddc88ac"), 972 compressed: false, 973 }, 974 }, 975 }, 976 serialized: hexToBytes("0182b103010002070000ee8bd501094a7d5ca318da2506de35e1cb025ddc"), 977 }, 978 { 979 name: "Ticket with one output", 980 entry: &UtxoEntry{ 981 txVersion: 1, 982 isCoinBase: false, 983 hasExpiry: false, 984 txType: 1, 985 height: 55555, 986 index: 1, 987 stakeExtra: hexToBytes("030f001aba76a9140cdf9941c0c221243cb8672cd1ad2c4c0933850588ac0000206a1e1a221182c26bbae681e4d96d452794e1951e70a208520000000000000054b5f466001abd76a9146c4f8b15918566534d134be7d7004b7f481bf36988ac"), 988 sparseOutputs: map[uint32]*utxoOutput{ 989 1: { 990 amount: 1000000, 991 scriptVersion: 0, 992 pkScript: hexToBytes("76a914ee8bd501094a7d5ca318da2506de35e1cb025ddc88ac"), 993 compressed: false, 994 }, 995 }, 996 }, 997 serialized: hexToBytes("0182b103010402070000ee8bd501094a7d5ca318da2506de35e1cb025ddc030f001aba76a9140cdf9941c0c221243cb8672cd1ad2c4c0933850588ac0000206a1e1a221182c26bbae681e4d96d452794e1951e70a208520000000000000054b5f466001abd76a9146c4f8b15918566534d134be7d7004b7f481bf36988ac"), 998 }, 999 { 1000 name: "Only output 2, coinbase, non-zero script version", 1001 entry: &UtxoEntry{ 1002 txVersion: 1, 1003 isCoinBase: true, 1004 hasExpiry: false, 1005 txType: 0, 1006 height: 12345, 1007 index: 1, 1008 sparseOutputs: map[uint32]*utxoOutput{ 1009 2: { 1010 amount: 100937281, 1011 scriptVersion: 0xffff, 1012 pkScript: hexToBytes("76a914da33f77cee27c2a975ed5124d7e4f7f97513510188ac"), 1013 compressed: false, 1014 }, 1015 }, 1016 }, 1017 serialized: hexToBytes("01df390101000182b095bf4182fe7f00da33f77cee27c2a975ed5124d7e4f7f975135101"), 1018 }, 1019 { 1020 name: "outputs 0 and 2 not coinbase", 1021 entry: &UtxoEntry{ 1022 txVersion: 1, 1023 isCoinBase: false, 1024 hasExpiry: false, 1025 txType: 0, 1026 height: 99999, 1027 index: 3, 1028 sparseOutputs: map[uint32]*utxoOutput{ 1029 0: { 1030 amount: 20000000, 1031 scriptVersion: 0, 1032 pkScript: hexToBytes("76a914e2ccd6ec7c6e2e581349c77e067385fa8236bf8a88ac"), 1033 compressed: false, 1034 }, 1035 2: { 1036 amount: 15000000, 1037 scriptVersion: 0, 1038 pkScript: hexToBytes("76a914b8025be1b3efc63b0ad48e7f9f10e87544528d5888ac"), 1039 compressed: false, 1040 }, 1041 }, 1042 }, 1043 serialized: hexToBytes("01858c1f03000501120000e2ccd6ec7c6e2e581349c77e067385fa8236bf8a80090000b8025be1b3efc63b0ad48e7f9f10e87544528d58"), 1044 }, 1045 { 1046 name: "outputs 0 and 2 not coinbase, has expiry", 1047 entry: &UtxoEntry{ 1048 txVersion: 1, 1049 isCoinBase: false, 1050 hasExpiry: true, 1051 txType: 0, 1052 height: 99999, 1053 index: 3, 1054 sparseOutputs: map[uint32]*utxoOutput{ 1055 0: { 1056 amount: 20000000, 1057 scriptVersion: 0, 1058 pkScript: hexToBytes("76a914e2ccd6ec7c6e2e581349c77e067385fa8236bf8a88ac"), 1059 compressed: false, 1060 }, 1061 2: { 1062 amount: 15000000, 1063 scriptVersion: 0, 1064 pkScript: hexToBytes("76a914b8025be1b3efc63b0ad48e7f9f10e87544528d5888ac"), 1065 compressed: false, 1066 }, 1067 }, 1068 }, 1069 serialized: hexToBytes("01858c1f03020501120000e2ccd6ec7c6e2e581349c77e067385fa8236bf8a80090000b8025be1b3efc63b0ad48e7f9f10e87544528d58"), 1070 }, 1071 { 1072 name: "outputs 0 and 2, not coinbase, 1 marked spent", 1073 entry: &UtxoEntry{ 1074 txVersion: 1, 1075 isCoinBase: false, 1076 hasExpiry: false, 1077 txType: 0, 1078 height: 12345, 1079 index: 1, 1080 sparseOutputs: map[uint32]*utxoOutput{ 1081 0: { 1082 amount: 20000000, 1083 scriptVersion: 0, 1084 pkScript: hexToBytes("76a914e2ccd6ec7c6e2e581349c77e067385fa8236bf8a88ac"), 1085 compressed: false, 1086 }, 1087 1: { // This won't be serialized. 1088 spent: true, 1089 amount: 1000000, 1090 scriptVersion: 0, 1091 pkScript: hexToBytes("76a914e43031c3e46f20bf1ccee9553ce815de5a48467588ac"), 1092 compressed: false, 1093 }, 1094 2: { 1095 amount: 15000000, 1096 scriptVersion: 0, 1097 pkScript: hexToBytes("76a914b8025be1b3efc63b0ad48e7f9f10e87544528d5888ac"), 1098 compressed: false, 1099 }, 1100 }, 1101 }, 1102 serialized: hexToBytes("01df3901000501120000e2ccd6ec7c6e2e581349c77e067385fa8236bf8a80090000b8025be1b3efc63b0ad48e7f9f10e87544528d58"), 1103 }, 1104 { 1105 name: "outputs 0 and 2, not coinbase, output 2 compressed", 1106 entry: &UtxoEntry{ 1107 txVersion: 1, 1108 isCoinBase: false, 1109 hasExpiry: false, 1110 txType: 0, 1111 height: 12345, 1112 index: 1, 1113 sparseOutputs: map[uint32]*utxoOutput{ 1114 0: { 1115 amount: 20000000, 1116 scriptVersion: 0, 1117 pkScript: hexToBytes("76a914e2ccd6ec7c6e2e581349c77e067385fa8236bf8a88ac"), 1118 compressed: false, 1119 }, 1120 2: { 1121 // Uncompressed Amount: 15000000 1122 // Uncompressed PkScript: 76a914b8025be1b3efc63b0ad48e7f9f10e87544528d5888ac 1123 amount: 137, 1124 1125 pkScript: hexToBytes("00b8025be1b3efc63b0ad48e7f9f10e87544528d58"), 1126 compressed: true, 1127 }, 1128 }, 1129 }, 1130 serialized: hexToBytes("01df3901000501120000e2ccd6ec7c6e2e581349c77e067385fa8236bf8a884f0000b8025be1b3efc63b0ad48e7f9f10e87544528d58"), 1131 }, 1132 { 1133 name: "outputs 0 and 2, not coinbase, output 2 compressed, packed indexes reversed", 1134 entry: &UtxoEntry{ 1135 txVersion: 1, 1136 isCoinBase: false, 1137 hasExpiry: false, 1138 txType: 0, 1139 height: 33333, 1140 index: 21, 1141 sparseOutputs: map[uint32]*utxoOutput{ 1142 0: { 1143 amount: 20000000, 1144 scriptVersion: 0, 1145 pkScript: hexToBytes("76a914e2ccd6ec7c6e2e581349c77e067385fa8236bf8a88ac"), 1146 compressed: false, 1147 }, 1148 2: { 1149 // Uncompressed Amount: 15000000 1150 // Uncompressed PkScript: 76a914b8025be1b3efc63b0ad48e7f9f10e87544528d5888ac 1151 amount: 137, 1152 scriptVersion: 0, 1153 pkScript: hexToBytes("00b8025be1b3efc63b0ad48e7f9f10e87544528d58"), 1154 compressed: true, 1155 }, 1156 }, 1157 }, 1158 serialized: hexToBytes("0181833515000501120000e2ccd6ec7c6e2e581349c77e067385fa8236bf8a884f0000b8025be1b3efc63b0ad48e7f9f10e87544528d58"), 1159 }, 1160 { 1161 name: "Only output 0, coinbase, fully spent", 1162 entry: &UtxoEntry{ 1163 txVersion: 1, 1164 isCoinBase: false, 1165 hasExpiry: false, 1166 txType: 0, 1167 height: 33333, 1168 index: 231, 1169 sparseOutputs: map[uint32]*utxoOutput{ 1170 0: { 1171 spent: true, 1172 amount: 5000000000, 1173 scriptVersion: 0, 1174 pkScript: hexToBytes("410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac"), 1175 compressed: false, 1176 }, 1177 }, 1178 }, 1179 serialized: nil, 1180 }, 1181 { 1182 name: "Only output 22, not coinbase", 1183 entry: &UtxoEntry{ 1184 txVersion: 1, 1185 isCoinBase: false, 1186 hasExpiry: false, 1187 txType: 0, 1188 height: 3221, 1189 index: 211, 1190 sparseOutputs: map[uint32]*utxoOutput{ 1191 22: { 1192 spent: false, 1193 amount: 366875659, 1194 scriptVersion: 0, 1195 pkScript: hexToBytes("a9141dd46a006572d820e448e12d2bbb38640bc718e687"), 1196 compressed: false, 1197 }, 1198 }, 1199 }, 1200 serialized: hexToBytes("019815805300080000108ba5b9e76300011dd46a006572d820e448e12d2bbb38640bc718e6"), 1201 }, 1202 } 1203 1204 for i, test := range tests { 1205 // Ensure the utxo entry serializes to the expected value. 1206 gotBytes, err := serializeUtxoEntry(test.entry) 1207 if err != nil { 1208 t.Errorf("serializeUtxoEntry #%d (%s) unexpected "+ 1209 "error: %v", i, test.name, err) 1210 continue 1211 } 1212 if !bytes.Equal(gotBytes, test.serialized) { 1213 t.Errorf("serializeUtxoEntry #%d (%s): mismatched "+ 1214 "bytes - got %x, want %x", i, test.name, 1215 gotBytes, test.serialized) 1216 continue 1217 } 1218 1219 // Don't try to deserialize if the test entry was fully spent 1220 // since it will have a nil serialization. 1221 if test.entry.IsFullySpent() { 1222 continue 1223 } 1224 1225 // Deserialize to a utxo entry. 1226 utxoEntry, err := deserializeUtxoEntry(test.serialized) 1227 if err != nil { 1228 t.Errorf("deserializeUtxoEntry #%d (%s) unexpected "+ 1229 "error: %v", i, test.name, err) 1230 continue 1231 } 1232 1233 // Ensure that the deserialized utxo entry has the same 1234 // properties for the containing transaction and block height. 1235 if utxoEntry.TxVersion() != test.entry.TxVersion() { 1236 t.Errorf("deserializeUtxoEntry #%d (%s) mismatched "+ 1237 " txVersion: got %d, want %d", i, test.name, 1238 utxoEntry.TxVersion(), test.entry.TxVersion()) 1239 continue 1240 } 1241 if utxoEntry.IsCoinBase() != test.entry.IsCoinBase() { 1242 t.Errorf("deserializeUtxoEntry #%d (%s) mismatched "+ 1243 "coinbase flag: got %v, want %v", i, test.name, 1244 utxoEntry.IsCoinBase(), test.entry.IsCoinBase()) 1245 continue 1246 } 1247 if utxoEntry.BlockHeight() != test.entry.BlockHeight() { 1248 t.Errorf("deserializeUtxoEntry #%d (%s) mismatched "+ 1249 "block height: got %d, want %d", i, test.name, 1250 utxoEntry.BlockHeight(), 1251 test.entry.BlockHeight()) 1252 continue 1253 } 1254 if utxoEntry.IsFullySpent() != test.entry.IsFullySpent() { 1255 t.Errorf("deserializeUtxoEntry #%d (%s) mismatched "+ 1256 "fully spent: got %v, want %v", i, test.name, 1257 utxoEntry.IsFullySpent(), 1258 test.entry.IsFullySpent()) 1259 continue 1260 } 1261 1262 // Ensure all of the outputs in the test entry match the 1263 // spentness of the output in the deserialized entry and the 1264 // deserialized entry does not contain any additional utxos. 1265 var numUnspent int 1266 for outputIndex := range test.entry.sparseOutputs { 1267 gotSpent := utxoEntry.IsOutputSpent(outputIndex) 1268 wantSpent := test.entry.IsOutputSpent(outputIndex) 1269 if !wantSpent { 1270 numUnspent++ 1271 } 1272 if gotSpent != wantSpent { 1273 t.Errorf("deserializeUtxoEntry #%d (%s) output "+ 1274 "#%d: mismatched spent: got %v, want "+ 1275 "%v", i, test.name, outputIndex, 1276 gotSpent, wantSpent) 1277 continue 1278 1279 } 1280 } 1281 if len(utxoEntry.sparseOutputs) != numUnspent { 1282 t.Errorf("deserializeUtxoEntry #%d (%s): mismatched "+ 1283 "number of unspent outputs: got %d, want %d", i, 1284 test.name, len(utxoEntry.sparseOutputs), 1285 numUnspent) 1286 continue 1287 } 1288 1289 // Ensure all of the amounts and scripts of the utxos in the 1290 // deserialized entry match the ones in the test entry. 1291 for outputIndex := range utxoEntry.sparseOutputs { 1292 gotAmount := utxoEntry.AmountByIndex(outputIndex) 1293 wantAmount := test.entry.AmountByIndex(outputIndex) 1294 if gotAmount != wantAmount { 1295 t.Errorf("deserializeUtxoEntry #%d (%s) "+ 1296 "output #%d: mismatched amounts: got "+ 1297 "%d, want %d", i, test.name, 1298 outputIndex, gotAmount, wantAmount) 1299 continue 1300 } 1301 1302 gotPkScript := utxoEntry.PkScriptByIndex(outputIndex) 1303 wantPkScript := test.entry.PkScriptByIndex(outputIndex) 1304 if !bytes.Equal(gotPkScript, wantPkScript) { 1305 t.Errorf("deserializeUtxoEntry #%d (%s) "+ 1306 "output #%d mismatched scripts: got "+ 1307 "%x, want %x", i, test.name, 1308 outputIndex, gotPkScript, wantPkScript) 1309 continue 1310 } 1311 } 1312 } 1313 } 1314 1315 // TestUtxoEntryDeserializeErrors performs negative tests against deserializing 1316 // unspent transaction outputs to ensure error paths work as expected. 1317 func TestUtxoEntryDeserializeErrors(t *testing.T) { 1318 t.Parallel() 1319 1320 tests := []struct { 1321 name string 1322 serialized []byte 1323 errType error 1324 }{ 1325 { 1326 name: "no data after version", 1327 serialized: hexToBytes("01"), 1328 errType: errDeserialize(""), 1329 }, 1330 { 1331 name: "no data after block height", 1332 serialized: hexToBytes("0101"), 1333 errType: errDeserialize(""), 1334 }, 1335 { 1336 name: "no data after header code", 1337 serialized: hexToBytes("010102"), 1338 errType: errDeserialize(""), 1339 }, 1340 { 1341 name: "not enough bytes for unspentness bitmap", 1342 serialized: hexToBytes("01017800"), 1343 errType: errDeserialize(""), 1344 }, 1345 { 1346 name: "incomplete compressed txout", 1347 serialized: hexToBytes("01010232"), 1348 errType: errDeserialize(""), 1349 }, 1350 } 1351 1352 for _, test := range tests { 1353 // Ensure the expected error type is returned and the returned 1354 // entry is nil. 1355 entry, err := deserializeUtxoEntry(test.serialized) 1356 if reflect.TypeOf(err) != reflect.TypeOf(test.errType) { 1357 t.Errorf("deserializeUtxoEntry (%s): expected error "+ 1358 "type does not match - got %T, want %T", 1359 test.name, err, test.errType) 1360 continue 1361 } 1362 if entry != nil { 1363 t.Errorf("deserializeUtxoEntry (%s): returned entry "+ 1364 "is not nil", test.name) 1365 continue 1366 } 1367 } 1368 } 1369 1370 // TestBestChainStateSerialization ensures serializing and deserializing the 1371 // best chain state works as expected. 1372 func TestBestChainStateSerialization(t *testing.T) { 1373 t.Parallel() 1374 1375 workSum := new(big.Int) 1376 tests := []struct { 1377 name string 1378 state bestChainState 1379 serialized []byte 1380 }{ 1381 { 1382 name: "genesis", 1383 state: bestChainState{ 1384 hash: *newHashFromStr("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"), 1385 height: 0, 1386 totalTxns: 1, 1387 totalSubsidy: 0, 1388 workSum: func() *big.Int { 1389 workSum.Add(workSum, standalone.CalcWork(486604799)) 1390 return new(big.Int).Set(workSum) 1391 }(), // 0x0100010001 1392 }, 1393 serialized: hexToBytes("6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d61900000000000000000001000000000000000000000000000000050000000100010001"), 1394 }, 1395 { 1396 name: "block 1", 1397 state: bestChainState{ 1398 hash: *newHashFromStr("00000000839a8e6886ab5951d76f411475428afc90947ee320161bbf18eb6048"), 1399 height: 1, 1400 totalTxns: 2, 1401 totalSubsidy: 123456789, 1402 workSum: func() *big.Int { 1403 workSum.Add(workSum, standalone.CalcWork(486604799)) 1404 return new(big.Int).Set(workSum) 1405 }(), // 0x0200020002, 1406 }, 1407 serialized: hexToBytes("4860eb18bf1b1620e37e9490fc8a427514416fd75159ab86688e9a830000000001000000020000000000000015cd5b0700000000050000000200020002"), 1408 }, 1409 } 1410 1411 for i, test := range tests { 1412 // Ensure the state serializes to the expected value. 1413 gotBytes := serializeBestChainState(test.state) 1414 if !bytes.Equal(gotBytes, test.serialized) { 1415 t.Errorf("serializeBestChainState #%d (%s): mismatched "+ 1416 "bytes - got %x, want %x", i, test.name, 1417 gotBytes, test.serialized) 1418 continue 1419 } 1420 1421 // Ensure the serialized bytes are decoded back to the expected 1422 // state. 1423 state, err := deserializeBestChainState(test.serialized) 1424 if err != nil { 1425 t.Errorf("deserializeBestChainState #%d (%s) "+ 1426 "unexpected error: %v", i, test.name, err) 1427 continue 1428 } 1429 if !reflect.DeepEqual(state, test.state) { 1430 t.Errorf("deserializeBestChainState #%d (%s) "+ 1431 "mismatched state - got %v, want %v", i, 1432 test.name, state, test.state) 1433 continue 1434 1435 } 1436 } 1437 } 1438 1439 // TestBestChainStateDeserializeErrors performs negative tests against 1440 // deserializing the chain state to ensure error paths work as expected. 1441 func TestBestChainStateDeserializeErrors(t *testing.T) { 1442 t.Parallel() 1443 1444 tests := []struct { 1445 name string 1446 serialized []byte 1447 errType error 1448 }{ 1449 { 1450 name: "nothing serialized", 1451 serialized: hexToBytes(""), 1452 errType: database.Error{ErrorCode: database.ErrCorruption}, 1453 }, 1454 { 1455 name: "short data in hash", 1456 serialized: hexToBytes("0000"), 1457 errType: database.Error{ErrorCode: database.ErrCorruption}, 1458 }, 1459 { 1460 name: "short data in work sum", 1461 serialized: hexToBytes("6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d61900000000000000000001000000000000000500000001000100"), 1462 errType: database.Error{ErrorCode: database.ErrCorruption}, 1463 }, 1464 } 1465 1466 for _, test := range tests { 1467 // Ensure the expected error type and code is returned. 1468 _, err := deserializeBestChainState(test.serialized) 1469 if reflect.TypeOf(err) != reflect.TypeOf(test.errType) { 1470 t.Errorf("deserializeBestChainState (%s): expected "+ 1471 "error type does not match - got %T, want %T", 1472 test.name, err, test.errType) 1473 continue 1474 } 1475 if derr, ok := err.(database.Error); ok { 1476 tderr := test.errType.(database.Error) 1477 if derr.ErrorCode != tderr.ErrorCode { 1478 t.Errorf("deserializeBestChainState (%s): "+ 1479 "wrong error code got: %v, want: %v", 1480 test.name, derr.ErrorCode, 1481 tderr.ErrorCode) 1482 continue 1483 } 1484 } 1485 } 1486 }