Skip to content

Commit 7b83233

Browse files
feat: Use deterministic marshal for digest hashing - GRPC node auth. (#1621)
1 parent 9a6afcd commit 7b83233

2 files changed

Lines changed: 162 additions & 2 deletions

File tree

pkg/nodeauth/utils/utils.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ import (
1515
func CalculateRequestDigest(req any) string {
1616
var data []byte
1717
if m, ok := req.(proto.Message); ok {
18-
// Use protobuf canonical serialization
19-
serialized, err := proto.Marshal(m)
18+
// Use deterministic protobuf serialization for consistent hashing
19+
serialized, err := proto.MarshalOptions{Deterministic: true}.Marshal(m)
2020
if err == nil {
2121
data = serialized
2222
} else {

pkg/nodeauth/utils/utils_test.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"github.com/stretchr/testify/assert"
1010
"github.com/stretchr/testify/require"
1111

12+
"github.com/smartcontractkit/chainlink-common/pkg/beholder/pb"
13+
dontimepb "github.com/smartcontractkit/chainlink-common/pkg/workflows/dontime/pb"
1214
p2ptypes "github.com/smartcontractkit/libocr/ragep2p/types"
1315
)
1416

@@ -91,3 +93,161 @@ func TestDecodeP2PId_InvalidSize(t *testing.T) {
9193
require.Error(t, err)
9294
assert.Contains(t, err.Error(), "failed to unmarshal PeerID")
9395
}
96+
97+
// TestCalculateRequestDigest_DeterministicWithSimpleMap tests that hashing is deterministic
98+
// for protobuf messages with simple string maps, regardless of insertion order
99+
func TestCalculateRequestDigest_DeterministicWithSimpleMap(t *testing.T) {
100+
// Create two messages with the same content but different insertion order
101+
msg1 := &pb.BaseMessage{
102+
Msg: "test message",
103+
Timestamp: "2024-01-01T00:00:00Z",
104+
Labels: map[string]string{
105+
"key1": "value1",
106+
"key2": "value2",
107+
"key3": "value3",
108+
},
109+
}
110+
111+
msg2 := &pb.BaseMessage{
112+
Msg: "test message",
113+
Timestamp: "2024-01-01T00:00:00Z",
114+
Labels: map[string]string{
115+
"key3": "value3", // Different insertion order
116+
"key1": "value1",
117+
"key2": "value2",
118+
},
119+
}
120+
121+
// Calculate digests
122+
digest1 := CalculateRequestDigest(msg1)
123+
digest2 := CalculateRequestDigest(msg2)
124+
125+
// Assert: Same content should produce the same digest
126+
require.Equal(t, digest1, digest2, "Digests should be identical for same content with different map insertion order")
127+
128+
// Verify digest format (should be 64 character hex string for SHA256)
129+
assert.Len(t, digest1, 64)
130+
assert.Regexp(t, "^[0-9a-f]{64}$", digest1)
131+
}
132+
133+
// TestCalculateRequestDigest_DeterministicWithComplexNestedMap tests that hashing is deterministic
134+
// for protobuf messages with complex nested maps
135+
func TestCalculateRequestDigest_DeterministicWithComplexNestedMap(t *testing.T) {
136+
// Create two messages with the same content but different insertion order
137+
msg1 := &dontimepb.Outcome{
138+
Timestamp: 1234567890,
139+
ObservedDonTimes: map[string]*dontimepb.ObservedDonTimes{
140+
"workflow1": {
141+
Timestamps: []int64{100, 200, 300},
142+
},
143+
"workflow2": {
144+
Timestamps: []int64{400, 500, 600},
145+
},
146+
"workflow3": {
147+
Timestamps: []int64{700, 800, 900},
148+
},
149+
},
150+
}
151+
152+
msg2 := &dontimepb.Outcome{
153+
Timestamp: 1234567890,
154+
ObservedDonTimes: map[string]*dontimepb.ObservedDonTimes{
155+
"workflow3": { // Different insertion order
156+
Timestamps: []int64{700, 800, 900},
157+
},
158+
"workflow1": {
159+
Timestamps: []int64{100, 200, 300},
160+
},
161+
"workflow2": {
162+
Timestamps: []int64{400, 500, 600},
163+
},
164+
},
165+
}
166+
167+
// Calculate digests
168+
digest1 := CalculateRequestDigest(msg1)
169+
digest2 := CalculateRequestDigest(msg2)
170+
171+
// Assert: Same content should produce the same digest
172+
require.Equal(t, digest1, digest2, "Digests should be identical for same content with different nested map insertion order")
173+
174+
// Verify digest format
175+
assert.Len(t, digest1, 64)
176+
assert.Regexp(t, "^[0-9a-f]{64}$", digest1)
177+
}
178+
179+
// TestCalculateRequestDigest_ConsistentMultipleCalls verifies that calling the function
180+
// multiple times on the same message produces consistent results
181+
func TestCalculateRequestDigest_ConsistentMultipleCalls(t *testing.T) {
182+
msg := &pb.BaseMessage{
183+
Msg: "test consistency",
184+
Timestamp: "2024-01-01T00:00:00Z",
185+
Labels: map[string]string{
186+
"env": "production",
187+
"service": "node-auth",
188+
"version": "1.0.0",
189+
},
190+
}
191+
192+
// Call digest function multiple times
193+
digest1 := CalculateRequestDigest(msg)
194+
digest2 := CalculateRequestDigest(msg)
195+
digest3 := CalculateRequestDigest(msg)
196+
197+
// All digests should be identical
198+
assert.Equal(t, digest1, digest2)
199+
assert.Equal(t, digest2, digest3)
200+
}
201+
202+
// TestCalculateRequestDigest_DifferentContentDifferentDigest ensures that different
203+
// content produces different digests
204+
func TestCalculateRequestDigest_DifferentContentDifferentDigest(t *testing.T) {
205+
msg1 := &pb.BaseMessage{
206+
Msg: "message1",
207+
Labels: map[string]string{
208+
"key": "value1",
209+
},
210+
}
211+
212+
msg2 := &pb.BaseMessage{
213+
Msg: "message2",
214+
Labels: map[string]string{
215+
"key": "value1",
216+
},
217+
}
218+
219+
msg3 := &pb.BaseMessage{
220+
Msg: "message1",
221+
Labels: map[string]string{
222+
"key": "value2",
223+
},
224+
}
225+
226+
digest1 := CalculateRequestDigest(msg1)
227+
digest2 := CalculateRequestDigest(msg2)
228+
digest3 := CalculateRequestDigest(msg3)
229+
230+
// All digests should be different
231+
assert.NotEqual(t, digest1, digest2, "Different message content should produce different digests")
232+
assert.NotEqual(t, digest1, digest3, "Different label values should produce different digests")
233+
assert.NotEqual(t, digest2, digest3, "Different messages should produce different digests")
234+
}
235+
236+
// TestCalculateRequestDigest_EmptyMap tests that empty maps are handled correctly
237+
func TestCalculateRequestDigest_EmptyMap(t *testing.T) {
238+
msg1 := &pb.BaseMessage{
239+
Msg: "test",
240+
Labels: map[string]string{},
241+
}
242+
243+
msg2 := &pb.BaseMessage{
244+
Msg: "test",
245+
Labels: nil,
246+
}
247+
248+
digest1 := CalculateRequestDigest(msg1)
249+
digest2 := CalculateRequestDigest(msg2)
250+
251+
// Empty map and nil map should produce the same digest
252+
assert.Equal(t, digest1, digest2, "Empty and nil maps should produce the same digest")
253+
}

0 commit comments

Comments
 (0)