-
Notifications
You must be signed in to change notification settings - Fork 2.5k
chaincfg/chainhash: add strict parsing #2520
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ import ( | |
| "bytes" | ||
| "encoding/hex" | ||
| "encoding/json" | ||
| "strings" | ||
| "testing" | ||
| ) | ||
|
|
||
|
|
@@ -110,7 +111,8 @@ func TestHashString(t *testing.T) { | |
| } | ||
| } | ||
|
|
||
| // TestNewHashFromStr executes tests against the NewHashFromStr function. | ||
| // TestNewHashFromStr executes compatibility tests against the lenient | ||
| // NewHashFromStr function. | ||
| func TestNewHashFromStr(t *testing.T) { | ||
| tests := []struct { | ||
| in string | ||
|
|
@@ -196,6 +198,155 @@ func TestNewHashFromStr(t *testing.T) { | |
| } | ||
| } | ||
|
|
||
| // TestNewHashFromStrStrict executes tests against the NewHashFromStrStrict | ||
| // function. | ||
| func TestNewHashFromStrStrict(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| in string | ||
| want Hash | ||
| err error | ||
| }{ | ||
| { | ||
| name: "genesis hash", | ||
| in: "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f", | ||
| want: mainNetGenesisHash, | ||
| err: nil, | ||
| }, | ||
| { | ||
| name: "stripped leading zeros", | ||
| in: "19d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f", | ||
| want: Hash{}, | ||
| err: ErrHashStrSizeMismatch, | ||
| }, | ||
| { | ||
| name: "odd length hash", | ||
| in: "1", | ||
| want: Hash{}, | ||
| err: ErrHashStrSizeMismatch, | ||
| }, | ||
| { | ||
| name: "empty string", | ||
| in: "", | ||
| want: Hash{}, | ||
| err: ErrHashStrSizeMismatch, | ||
| }, | ||
| { | ||
| name: "hash string that is too long", | ||
| in: "01234567890123456789012345678901234567890123456789012345678912345", | ||
| want: Hash{}, | ||
| err: ErrHashStrSizeMismatch, | ||
| }, | ||
| { | ||
| name: "hash string that contains non-hex chars", | ||
| in: "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26g", | ||
| want: Hash{}, | ||
| err: hex.InvalidByteError('g'), | ||
| }, | ||
| } | ||
|
|
||
| unexpectedErrStr := "NewHashFromStrStrict #%d failed to detect expected error - got: %v want: %v" | ||
| unexpectedResultStr := "NewHashFromStrStrict #%d got: %v want: %v" | ||
| t.Logf("Running %d tests", len(tests)) | ||
| for i, test := range tests { | ||
| test := test | ||
|
|
||
| t.Run(test.name, func(t *testing.T) { | ||
| result, err := NewHashFromStrStrict(test.in) | ||
| if err != test.err { | ||
| t.Errorf(unexpectedErrStr, i, err, test.err) | ||
| return | ||
| } else if err != nil { | ||
| return | ||
| } | ||
| if !test.want.IsEqual(result) { | ||
| t.Errorf(unexpectedResultStr, i, result, &test.want) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| // TestDecodeStrict executes tests against the DecodeStrict function. | ||
| func TestDecodeStrict(t *testing.T) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added |
||
| tests := []struct { | ||
| name string | ||
| in string | ||
| want Hash | ||
| err error | ||
| }{ | ||
| { | ||
| name: "genesis hash", | ||
| in: "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f", | ||
| want: mainNetGenesisHash, | ||
| err: nil, | ||
| }, | ||
| { | ||
| name: "odd length hash", | ||
| in: "1", | ||
| want: Hash{}, | ||
| err: ErrHashStrSizeMismatch, | ||
| }, | ||
| { | ||
| name: "even length hash that is too short, 32 chars", | ||
| in: "deadbeef" + strings.Repeat("0", 24), | ||
| want: Hash{}, | ||
| err: ErrHashStrSizeMismatch, | ||
| }, | ||
| { | ||
| name: "hash string that contains non-hex chars", | ||
| in: "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26g", | ||
| want: Hash{}, | ||
| err: hex.InvalidByteError('g'), | ||
| }, | ||
| { | ||
| name: "hash string that is too long", | ||
| in: "01234567890123456789012345678901234567890123456789012345678912345", | ||
| want: Hash{}, | ||
| err: ErrHashStrSizeMismatch, | ||
| }, | ||
| } | ||
|
|
||
| unexpectedErrStr := "DecodeStrict #%d failed to detect expected error - got: %v want: %v" | ||
| unexpectedResultStr := "DecodeStrict #%d got: %v want: %v" | ||
| t.Logf("Running %d tests", len(tests)) | ||
| for i, test := range tests { | ||
| test := test | ||
|
|
||
| t.Run(test.name, func(t *testing.T) { | ||
| var result Hash | ||
| err := DecodeStrict(&result, test.in) | ||
| if err != test.err { | ||
| t.Errorf(unexpectedErrStr, i, err, test.err) | ||
| return | ||
| } else if err != nil { | ||
| return | ||
| } | ||
| if !test.want.IsEqual(&result) { | ||
| t.Errorf(unexpectedResultStr, i, &result, &test.want) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| // BenchmarkDecodeStrict benchmarks DecodeStrict on a valid canonical hash. | ||
| func BenchmarkDecodeStrict(b *testing.B) { | ||
| // Use a canonical 64-character hash to exercise the successful strict path. | ||
| hashStr := "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f" | ||
| var result Hash | ||
|
|
||
| b.ReportAllocs() | ||
| for i := 0; i < b.N; i++ { | ||
| if err := DecodeStrict(&result, hashStr); err != nil { | ||
| b.Fatalf("unexpected decode error: %v", err) | ||
| } | ||
| } | ||
|
|
||
| if !mainNetGenesisHash.IsEqual(&result) { | ||
| b.Fatalf("unexpected decode result: got %v want %v", | ||
| &result, &mainNetGenesisHash) | ||
| } | ||
| } | ||
|
|
||
| // TestHashJsonMarshal tests json marshal and unmarshal. | ||
| func TestHashJsonMarshal(t *testing.T) { | ||
| hashStr := "000000000003ba27aa200b1cecaad478d2b00432346c3f1f3986da1afd33e506" | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
decodeHashhelper accepts[]byteso thatDecodecan pass its already-allocatedsrcBytesslice without an extra copy. However,DecodeStrictcalls it with[]byte(src), which allocates on every call. SinceDecodeStrictonly accepts exactly 64-char strings, you could declare a[MaxHashStringSize]byteon the stack and copy into it (or keep a localvar b [MaxHashStringSize]byte; copy(b[:], src); return decodeHash(dst, b[:])) to avoid the heap allocation on the hot path.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Go already optimizes such cases - no need to use a stack array for this.
I added a benchmark and ensured there is already no memory allocation here: