commitreveal_test.gno
5.53 Kb · 151 lines
1package commitreveal
2
3import (
4 "strings"
5 "testing"
6
7 "gno.land/p/nt/uassert/v0"
8)
9
10const goodSalt = "0123456789abcdef" // exactly MinSaltLen
11
12func TestCommitAndVerify(t *testing.T) {
13 c, err := Commit("rock", goodSalt)
14 uassert.NoError(t, err)
15 uassert.True(t, ValidCommitment(c), "a commitment is a 32-byte hex digest")
16 uassert.Equal(t, 64, len(c), "32 bytes hex-encoded")
17
18 uassert.True(t, Verify(c, "rock", goodSalt), "the correct opening verifies")
19}
20
21func TestCommitIsDeterministic(t *testing.T) {
22 a, _ := Commit("paper", goodSalt)
23 b, _ := Commit("paper", goodSalt)
24 uassert.Equal(t, a, b, "same input, same commitment")
25}
26
27func TestDifferentValuesDiffer(t *testing.T) {
28 a, _ := Commit("rock", goodSalt)
29 b, _ := Commit("paper", goodSalt)
30 uassert.False(t, a == b)
31}
32
33// TestSameValueDifferentSaltDiffers is why the salt exists: two players
34// choosing the same move must not publish the same commitment.
35func TestSameValueDifferentSaltDiffers(t *testing.T) {
36 a, _ := Commit("rock", "salt-one-0123456789")
37 b, _ := Commit("rock", "salt-two-0123456789")
38 uassert.False(t, a == b, "the same move must not be recognisable across players")
39}
40
41func TestWrongOpeningFails(t *testing.T) {
42 c, _ := Commit("rock", goodSalt)
43
44 uassert.False(t, Verify(c, "paper", goodSalt), "wrong value")
45 uassert.False(t, Verify(c, "rock", "a-different-salt-value"), "wrong salt")
46 uassert.False(t, Verify(c, "", goodSalt), "empty value")
47}
48
49// TestShortSaltRefused is the enforced-not-advised part: a three-move game with
50// a short salt is brute-forceable, so the package will not produce one.
51func TestShortSaltRefused(t *testing.T) {
52 _, err := Commit("rock", "tooshort")
53 uassert.ErrorIs(t, err, ErrShortSalt)
54
55 _, err = Commit("rock", strings.Repeat("x", MinSaltLen-1))
56 uassert.ErrorIs(t, err, ErrShortSalt, "one byte under the minimum is still refused")
57
58 _, err = Commit("rock", strings.Repeat("x", MinSaltLen))
59 uassert.NoError(t, err, "exactly MinSaltLen is accepted")
60}
61
62func TestShortSaltNeverVerifies(t *testing.T) {
63 // Even a digest that would match cannot be opened with a short salt.
64 uassert.False(t, Verify(MustCommit("rock", goodSalt), "rock", "short"),
65 "Verify enforces the salt floor too")
66}
67
68func TestLongValueRefused(t *testing.T) {
69 _, err := Commit(strings.Repeat("x", MaxValueLen+1), goodSalt)
70 uassert.ErrorIs(t, err, ErrLongValue)
71
72 _, err = Commit(strings.Repeat("x", MaxValueLen), goodSalt)
73 uassert.NoError(t, err, "exactly MaxValueLen is accepted")
74}
75
76// TestLengthPrefixPreventsAmbiguity is the subtle one: with plain
77// concatenation, ("ab","cdefghijklmnopq") and ("abc","defghijklmnopq") produce
78// the same bytes, so one commitment could be opened two ways.
79func TestLengthPrefixPreventsAmbiguity(t *testing.T) {
80 a, err := Commit("ab", "cdefghijklmnopqr")
81 uassert.NoError(t, err)
82 b, err := Commit("abc", "defghijklmnopqr0")
83 uassert.NoError(t, err)
84
85 uassert.False(t, a == b, "a split ambiguity must not collide")
86 uassert.False(t, Verify(a, "abc", "defghijklmnopqr0"), "and must not cross-verify")
87}
88
89func TestOpenReportsReasons(t *testing.T) {
90 c := MustCommit("rock", goodSalt)
91
92 uassert.NoError(t, Open(c, "rock", goodSalt))
93 uassert.ErrorIs(t, Open(c, "paper", goodSalt), ErrMismatch)
94 uassert.ErrorIs(t, Open(c, "rock", "short"), ErrShortSalt)
95 uassert.ErrorIs(t, Open(c, strings.Repeat("x", MaxValueLen+1), goodSalt), ErrLongValue)
96 uassert.ErrorIs(t, Open("not-hex!!", "rock", goodSalt), ErrBadHexDigest)
97 uassert.ErrorIs(t, Open("abcd", "rock", goodSalt), ErrBadHexDigest, "right charset, wrong length")
98}
99
100func TestMalformedCommitments(t *testing.T) {
101 uassert.False(t, Verify("", "rock", goodSalt))
102 uassert.False(t, Verify("zz", "rock", goodSalt), "not hex")
103 uassert.False(t, Verify(strings.Repeat("ab", 16), "rock", goodSalt), "16 bytes, not 32")
104}
105
106func TestValidCommitment(t *testing.T) {
107 uassert.True(t, ValidCommitment(MustCommit("x", goodSalt)))
108 uassert.False(t, ValidCommitment(""))
109 uassert.False(t, ValidCommitment("nothex"))
110 uassert.False(t, ValidCommitment(strings.Repeat("a", 63)), "odd length")
111 uassert.False(t, ValidCommitment(strings.Repeat("a", 66)), "too long")
112 uassert.True(t, ValidCommitment(strings.Repeat("A", 64)), "uppercase hex is fine")
113}
114
115func TestMustCommitPanicsOnBadInput(t *testing.T) {
116 defer func() {
117 r := recover()
118 uassert.True(t, r != nil, "MustCommit must panic on a short salt")
119 }()
120 MustCommit("rock", "short")
121}
122
123func TestEqualConstantTime(t *testing.T) {
124 a := []byte{1, 2, 3}
125 uassert.True(t, equalConstantTime(a, []byte{1, 2, 3}))
126 uassert.False(t, equalConstantTime(a, []byte{1, 2, 4}), "differs in the last byte")
127 uassert.False(t, equalConstantTime(a, []byte{9, 2, 3}), "differs in the first byte")
128 uassert.False(t, equalConstantTime(a, []byte{1, 2}), "different lengths")
129 uassert.True(t, equalConstantTime(nil, nil))
130}
131
132// TestFullRoundTrip walks the two phases the scheme is made of.
133func TestFullRoundTrip(t *testing.T) {
134 // Phase 1: both players publish only a commitment.
135 aliceSalt := "alice-secret-salt-1"
136 bobSalt := "bob-secret-salt-0001"
137 aliceCommit := MustCommit("rock", aliceSalt)
138 bobCommit := MustCommit("paper", bobSalt)
139
140 uassert.False(t, aliceCommit == bobCommit)
141 uassert.True(t, ValidCommitment(aliceCommit))
142
143 // Phase 2: reveals are checked against the earlier commitments.
144 uassert.NoError(t, Open(aliceCommit, "rock", aliceSalt))
145 uassert.NoError(t, Open(bobCommit, "paper", bobSalt))
146
147 // Bob cannot change his move after seeing Alice's.
148 uassert.ErrorIs(t, Open(bobCommit, "scissors", bobSalt), ErrMismatch)
149 // Nor can he claim Alice's commitment as his own.
150 uassert.ErrorIs(t, Open(aliceCommit, "rock", bobSalt), ErrMismatch)
151}