Skip to content
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

Add Equal method for CrossSigningKey #444

Merged
merged 4 commits into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ jobs:
steps:
- uses: actions/checkout@v3
with:
repository: "matrix-org/dendrite"
repository: "element-hq/dendrite"
- name: Install libolm
run: sudo apt-get install libolm-dev libolm3
- name: Setup go
Expand Down
53 changes: 53 additions & 0 deletions fclient/crosssigning.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
package fclient

import (
"bytes"
"encoding/json"
"slices"

"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/gomatrixserverlib/spec"
Expand Down Expand Up @@ -46,6 +48,57 @@ type CrossSigningKey struct {

func (s *CrossSigningKey) isCrossSigningBody() {} // implements CrossSigningBody

func (s *CrossSigningKey) Equal(other *CrossSigningKey) bool {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can't we just use reflect.DeepEqual here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is does not allocate at all, while reflect.DeepEqual does. (not that it matters much on that endpoint)

> go test -bench=Equal -run=^# -benchmem -count=10
goos: linux
goarch: amd64
pkg: github.com/matrix-org/gomatrixserverlib/fclient
cpu: 12th Gen Intel(R) Core(TM) i5-12500H
BenchmarkReflectDeepEqual-16    	  468159	      2381 ns/op	     288 B/op	      14 allocs/op
BenchmarkReflectDeepEqual-16    	  460086	      2376 ns/op	     288 B/op	      14 allocs/op
BenchmarkReflectDeepEqual-16    	  494133	      2333 ns/op	     288 B/op	      14 allocs/op
BenchmarkReflectDeepEqual-16    	  496840	      2338 ns/op	     288 B/op	      14 allocs/op
BenchmarkReflectDeepEqual-16    	  481978	      2351 ns/op	     288 B/op	      14 allocs/op
BenchmarkReflectDeepEqual-16    	  461346	      2327 ns/op	     288 B/op	      14 allocs/op
BenchmarkReflectDeepEqual-16    	  511146	      2390 ns/op	     288 B/op	      14 allocs/op
BenchmarkReflectDeepEqual-16    	  501966	      2443 ns/op	     288 B/op	      14 allocs/op
BenchmarkReflectDeepEqual-16    	  503883	      2345 ns/op	     288 B/op	      14 allocs/op
BenchmarkReflectDeepEqual-16    	  503308	      2415 ns/op	     288 B/op	      14 allocs/op
BenchmarkEqual-16               	 5371452	       214.8 ns/op	       0 B/op	       0 allocs/op
BenchmarkEqual-16               	 5578748	       227.6 ns/op	       0 B/op	       0 allocs/op
BenchmarkEqual-16               	 5169262	       230.6 ns/op	       0 B/op	       0 allocs/op
BenchmarkEqual-16               	 5215892	       234.2 ns/op	       0 B/op	       0 allocs/op
BenchmarkEqual-16               	 5207256	       232.0 ns/op	       0 B/op	       0 allocs/op
BenchmarkEqual-16               	 5564870	       244.5 ns/op	       0 B/op	       0 allocs/op
BenchmarkEqual-16               	 4907815	       229.9 ns/op	       0 B/op	       0 allocs/op
BenchmarkEqual-16               	 5178279	       233.5 ns/op	       0 B/op	       0 allocs/op
BenchmarkEqual-16               	 4840867	       247.9 ns/op	       0 B/op	       0 allocs/op
BenchmarkEqual-16               	 5193268	       215.0 ns/op	       0 B/op	       0 allocs/op

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to document what the comparison is taking into account, i.e. there can be different equality implementations depending on the use-case. I presume here we care about the fact that a new object needs to be stored in the DB and we test for equality of all fields.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly. The Equal method here checks all fields and receives the MasterKey, SelfSigningKeys and UserSigningKeys from Dendrite, see this (which should be much like element-hq/synapse#16943

if s == nil || other == nil {
return false
}
if s.UserID != other.UserID {
return false
}
if len(s.Usage) != len(other.Usage) {
return false
}

// Make sure the slices are sorted before we compare them.
if !slices.IsSorted(s.Usage) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not necessarily needed to check, but is slightly faster doing so.

slices.Sort(s.Usage)
}
if !slices.IsSorted(other.Usage) {
slices.Sort(other.Usage)
}
for i := range s.Usage {
if s.Usage[i] != other.Usage[i] {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this slice guaranteed to be ordered? If not you might get some false negatives.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point, but does usage currently even contain more than one value?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per spec no, but you want to be forwards compatible, otherwise you might need to prevent such cross signing keys from being uploaded.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 30cbdb4

return false
}
}
if len(s.Keys) != len(other.Keys) {
return false
}
for k, v := range s.Keys {
if !bytes.Equal(other.Keys[k], v) {
return false
}
}
if len(s.Signatures) != len(other.Signatures) {
return false
}
for k, v := range s.Signatures {
otherV, ok := other.Signatures[k]
if !ok {
return false
}
if len(v) != len(otherV) {
return false
}
for k2, v2 := range v {
if !bytes.Equal(otherV[k2], v2) {
return false
}
}
}
return true
}

type CrossSigningBody interface {
isCrossSigningBody()
}
Expand Down
119 changes: 119 additions & 0 deletions fclient/crosssigning_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package fclient

import (
"testing"

"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/gomatrixserverlib/spec"
)

var tests = []struct {
name string
s *CrossSigningKey
other *CrossSigningKey
expect bool
}{
{
name: "NilReceiver_ReturnsFalse",
s: nil,
other: &CrossSigningKey{},
expect: false,
},
{
name: "NilOther_ReturnsFalse",
s: &CrossSigningKey{},
other: nil,
expect: false,
},
{
name: "DifferentUserID_ReturnsFalse",
s: &CrossSigningKey{UserID: "user1"},
other: &CrossSigningKey{UserID: "user2"},
expect: false,
},
{
name: "DifferentUsageLength_ReturnsFalse",
s: &CrossSigningKey{Usage: []CrossSigningKeyPurpose{CrossSigningKeyPurposeMaster}},
other: &CrossSigningKey{Usage: []CrossSigningKeyPurpose{CrossSigningKeyPurposeMaster, CrossSigningKeyPurposeSelfSigning}},
expect: false,
},
{
name: "UnsortedUsages_ReturnsTrue",
s: &CrossSigningKey{Usage: []CrossSigningKeyPurpose{CrossSigningKeyPurposeSelfSigning, CrossSigningKeyPurposeMaster}},
other: &CrossSigningKey{Usage: []CrossSigningKeyPurpose{CrossSigningKeyPurposeMaster, CrossSigningKeyPurposeSelfSigning}},
expect: true,
},
{
name: "UnsortedUsages_ReturnsTrue",
s: &CrossSigningKey{Usage: []CrossSigningKeyPurpose{CrossSigningKeyPurposeSelfSigning, CrossSigningKeyPurposeMaster}},
other: &CrossSigningKey{Usage: []CrossSigningKeyPurpose{CrossSigningKeyPurposeSelfSigning, CrossSigningKeyPurposeMaster}},
expect: true,
},
{
name: "DifferentUsageValues_ReturnsFalse",
s: &CrossSigningKey{Usage: []CrossSigningKeyPurpose{CrossSigningKeyPurposeMaster}},
other: &CrossSigningKey{Usage: []CrossSigningKeyPurpose{CrossSigningKeyPurposeSelfSigning}},
expect: false,
},
{
name: "DifferentKeysLength_ReturnsFalse",
s: &CrossSigningKey{Keys: map[gomatrixserverlib.KeyID]spec.Base64Bytes{"key1": {}}},
other: &CrossSigningKey{Keys: map[gomatrixserverlib.KeyID]spec.Base64Bytes{"key1": {}, "key2": {}}},
expect: false,
},
{
name: "DifferentKeysValues_ReturnsFalse",
s: &CrossSigningKey{Keys: map[gomatrixserverlib.KeyID]spec.Base64Bytes{"key1": {}}},
other: &CrossSigningKey{Keys: map[gomatrixserverlib.KeyID]spec.Base64Bytes{"key1": {1}}},
expect: false,
},
{
name: "DifferentSignaturesLength_ReturnsFalse",
s: &CrossSigningKey{Signatures: map[string]map[gomatrixserverlib.KeyID]spec.Base64Bytes{"sig1": {"key1": {}}}},
other: &CrossSigningKey{Signatures: map[string]map[gomatrixserverlib.KeyID]spec.Base64Bytes{"sig1": {"key1": {}}, "sig2": {"key2": {}}}},
expect: false,
},
{
name: "DifferentSignaturesValues_ReturnsFalse",
s: &CrossSigningKey{Signatures: map[string]map[gomatrixserverlib.KeyID]spec.Base64Bytes{"sig1": {"key1": {}}}},
other: &CrossSigningKey{Signatures: map[string]map[gomatrixserverlib.KeyID]spec.Base64Bytes{"sig1": {"key1": {1}}}},
expect: false,
},
{
name: "IdenticalKeys_ReturnsTrue",
s: &CrossSigningKey{
UserID: "user1",
Usage: []CrossSigningKeyPurpose{CrossSigningKeyPurposeMaster},
Keys: map[gomatrixserverlib.KeyID]spec.Base64Bytes{"key1": {}},
Signatures: map[string]map[gomatrixserverlib.KeyID]spec.Base64Bytes{"sig1": {"key1": {}}},
},
other: &CrossSigningKey{
UserID: "user1",
Usage: []CrossSigningKeyPurpose{CrossSigningKeyPurposeMaster},
Keys: map[gomatrixserverlib.KeyID]spec.Base64Bytes{"key1": {}},
Signatures: map[string]map[gomatrixserverlib.KeyID]spec.Base64Bytes{"sig1": {"key1": {}}},
},
expect: true,
},
}

func TestCrossSigningKeyEqual(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.s.Equal(tt.other); got != tt.expect {
t.Errorf("Equal() = %v, want %v", got, tt.expect)
}
})
}
}

func BenchmarkEqual(b *testing.B) {

for i := 0; i < b.N; i++ {
for _, tt := range tests {
if !tt.s.Equal(tt.other) && tt.expect {
b.Fatal(tt.name, tt.s)
}
}
}
}
Loading