-
Notifications
You must be signed in to change notification settings - Fork 715
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
Make bls.Signer
api fallible
#3696
base: master
Are you sure you want to change the base?
Conversation
7509338
to
7de1094
Compare
2653584
to
9fcc414
Compare
4819785
to
70389bd
Compare
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.
please help with naming!?
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.
Shouldn't this just be server.go
? I don't think this is a test file - this is the server implementation
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.
I thought I already responded to this (maybe I forgot to submit the comment). My issue here is that I actually don't want people to use this. There's no benefit to running this on the same server vs running a local-signer and there's no auth mechanism implementation for running it on a separate server.
Maybe the mistake is testing the client at all?
Maybe we can just have a set of tests that implementors can run to make sure this works as expected? Curious to hear your thoughts
a7c3a22
to
42e450d
Compare
network/peer/peer_test.go
Outdated
BLSSignature: (func() *bls.Signature { | ||
sig, err := blsKey.SignProofOfPossession([]byte("wrong message")) | ||
require.NoError(t, err) | ||
return sig | ||
})(), |
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.
BLSSignature: (func() *bls.Signature { | |
sig, err := blsKey.SignProofOfPossession([]byte("wrong message")) | |
require.NoError(t, err) | |
return sig | |
})(), | |
BLSSignature: func() *bls.Signature { | |
sig, err := blsKey.SignProofOfPossession([]byte("wrong message")) | |
require.NoError(t, err) | |
return sig | |
}(), |
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.
We wrap this with ()
in a few spots in this PR, but I don't think it's necessary?
node/node.go
Outdated
if err != nil { | ||
return err | ||
} |
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.
I don't have a strong preference for wrapping this error or not, but I think we should be consistent w/ whatever we end up doing for this and the above error earlier in this diff
proto/signer/signer.proto
Outdated
service Signer { | ||
rpc PublicKey(PublicKeyRequest) returns (PublicKeyResponse) {} | ||
rpc Sign(SignatureRequest) returns (SignatureResponse) {} | ||
rpc ProofOfPossession(ProofOfPossessionSignatureRequest) returns (ProofOfPossessionSignatureResponse) {} |
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.
Maybe prefix this + the corresponding request/response types with Sign*
to align this more closely with the existing Sign
rpc, as they are similar apis with differing ciphertexts? i.e
rpc SignProofOfPossession(SignProofOfPossessionRequest) returns (SignProofOfPossessionResponse) {}
?
func collectN[T any](n int, fn func() T) []T { | ||
result := make([]T, n) | ||
for i := 0; i < n; i++ { | ||
result[i] = fn() | ||
} | ||
return result | ||
} | ||
|
||
func mapWithError[T any, U any](arr []T, fn func(T) (U, error)) ([]U, error) { | ||
result := make([]U, len(arr)) | ||
for i, val := range arr { | ||
newVal, err := fn(val) | ||
if err != nil { | ||
return nil, err | ||
} | ||
result[i] = newVal | ||
} | ||
return result, nil | ||
} |
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.
I would probably not use these functions... the way we're using collectN
I think is over-engineering/not improving the readability of the tests. mapWithError
is more useful than collectN
so if you want to keep it in I'm fine with it, but I don't think writing out:
sig1, err := sk1.Sign(msg)
require.NoError(err)
sig2, err := sk2.Sign(msg)
require.NoError(err)
sig3, err := sk3.Sign(msg)
require.NoError(err)
sigs := []*bls.SIgnature{sig1, sig2, sig3}
is actually that bad IMO.
wrongSig, err := signers[0].Sign(differentMsg) | ||
require.NoError(err) | ||
|
||
signatures[0] = wrongSig |
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.
This line is an example where I feel like the mapWithError
util looks worse, instead of just writing out:
wrongSig, err := sk1.Sign(differentMsg)
require.NoError(err)
sig2, err := sk2.Sign(msg)
require.NoError(err)
sig3, err := sk3.Sign(msg)
require.NoError(err)
sigs := []*bls.SIgnature{wrongSig, sig2, sig3}
isValid := bls.Verify(signer.PublicKey(), sig, msg) | ||
require.True(isValid) |
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.
- This can be done in a one-liner
- Generally Go uses the
ok
var name for bool return values
isValid := bls.Verify(signer.PublicKey(), sig, msg) | |
require.True(isValid) | |
require.True(bls.Verify(signer.PublicKey(), sig, msg)) |
msg := []byte("TestVerifyWrongMessageSignature local signer") | ||
wrongMsg := []byte("TestVerifyWrongMessageSignature local signer with wrong message") |
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.
Nit: The payloads of these messages in this test file don't need to be related to the test name + why make them longer when the contents don't matter? Ex. my IDE will pick up on this + we have to keep the test names/strings consistent
msg := []byte("TestVerifyWrongMessageSignature local signer") | |
wrongMsg := []byte("TestVerifyWrongMessageSignature local signer with wrong message") | |
msg := []byte("foo") | |
wrongMsg := []byte("bar") |
require.True(isValid) | ||
} | ||
|
||
func TestVerifyWrongMessageSignature(t *testing.T) { |
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.
This can be refactored w/ TestVerifyValidSignature
into a test-table if we the expected message a parameter
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.
Is it really worth combining two tests into a table test? The logic is different for each test. In the first test, we have a message stored at variable msg
. We sign the message and pass that same message and signature for verification. In the second test we have two separate inputs. We sign msg
again, but we pass wrongMsg
in for signature verification. Therefore the logic for each of the two tests are subtly different.
I could make a table test (with only two tests) that has a msg
field as well as a verificationMsg
field, but now I have to either copy/paste the same thing into one of the test-structs or define a variable outside. I'm kind of shoehorning things to save a few lines of code.
func TestValidAggregation(t *testing.T) { | ||
require := require.New(t) | ||
keyPairs := collectN(2, func() bls.Signer { | ||
return NewLocalSigner(require) | ||
}) | ||
|
||
keyPairs = append(keyPairs, NewLocalSigner(require)) | ||
|
||
msg := []byte("TestValidAggregation local signer") | ||
|
||
sigs, err := mapWithError(keyPairs, func(s bls.Signer) (*bls.Signature, error) { | ||
return s.Sign(msg) | ||
}) | ||
require.NoError(err) | ||
|
||
pks, _ := mapWithError(keyPairs, func(s bls.Signer) (*bls.PublicKey, error) { | ||
return s.PublicKey(), nil | ||
}) | ||
|
||
isValid, err := AggregateAndVerify(pks, sigs, msg) | ||
require.NoError(err) | ||
require.True(isValid) | ||
} | ||
|
||
func TestSingleKeyAggregation(t *testing.T) { | ||
require := require.New(t) | ||
signer := NewRpcSigner(require) | ||
|
||
pks := []*bls.PublicKey{signer.PublicKey()} | ||
|
||
msg := []byte("TestSingleKeyAggregation local signer") | ||
|
||
sig, err := signer.Sign(msg) | ||
require.NoError(err) | ||
|
||
isValid, err := AggregateAndVerify(pks, []*bls.Signature{sig}, msg) | ||
require.NoError(err) | ||
require.True(isValid) | ||
} |
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.
Do we need tests on aggregation working? I feel like we already know aggregation will work because we are just implementing a type that produces bls.Signature
and bls.Publickey
, and those types + aggregation are already tested in the upstream package bls
. I feel like coverage for rpcsigner
only needs to cover that the Sign*
functions produce signatures that are verifiable off of the PublicKey
.
require.True(isValid) | ||
} | ||
|
||
func TestVerifyValidProofOfPossession(t *testing.T) { |
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.
Similar comment regarding test tables w/ the following test
d57fb5b
to
69ae4aa
Compare
69ae4aa
to
1ccd0ce
Compare
f14afa0
to
e86406c
Compare
proto/signer/signer.proto
Outdated
message SignatureRequest { | ||
bytes message = 1; | ||
} | ||
message SignatureResponse { |
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.
nit: SignRequest/SignResponse
proto/signer/signer.proto
Outdated
message ProofOfPossessionSignatureRequest { | ||
bytes message = 1; | ||
} | ||
message ProofOfPossessionSignatureResponse { |
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.
nit: SignProofOfPossessionSignatureRequest/Response
return getSignatureFromResponse(resp) | ||
} | ||
|
||
func (c *Client) PublicKey() *bls.PublicKey { |
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.
nit: put this before Sign
to be consistent w/ ordering on the interface
} | ||
|
||
func (c *Client) Sign(message []byte) (*bls.Signature, error) { | ||
resp, err := c.client.Sign(context.Background(), &pb.SignatureRequest{Message: message}) |
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.
Use TODO
instead of Background
d1e9fde
to
6e26bcd
Compare
239e57f
to
3a9faa6
Compare
|
||
pubkeyResponse, err := client.PublicKey(context.TODO(), &pb.PublicKeyRequest{}) | ||
if err != nil { | ||
conn.Close() |
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.
I feel weird about this code owning the lifecycle of the connection since it's provided by the caller. Some of these errors don't look persistent + you can still re-use the connection since you can retry rpcs on different streams on the same http/2 connection even if this errored out.
You could probably make a strong case for some of these errors being persistent (i.e the empty public key case, the invalid pk case), but I would still think that you could handle all of these errors at the call-site through a connection close and have all the connection lifecycle managed there instead of having mixed ownership
func (c *Client) Close() error { | ||
return c.conn.Close() | ||
} |
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.
Related to my last comment but I think this can be removed as well
type signatureResponse interface { | ||
GetSignature() []byte | ||
} | ||
|
||
func getSignatureFromResponse[T signatureResponse](resp T) (*bls.Signature, error) { | ||
signature := resp.GetSignature() | ||
if len(signature) == 0 { | ||
return nil, ErrorEmptySignature | ||
} | ||
|
||
return bls.SignatureFromBytes(signature) | ||
} |
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.
Can't tell if this is hacky or clever but it works
|
||
var tests = []test{ | ||
{ | ||
name: "Uncompressed", |
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.
nit: lowercase all of the test names (not sure if this is a go standard or not, but it is consistent w/ the rest of our tests)
func TestValidSignature(t *testing.T) { | ||
sig, err := client.Sign(validSignatureMsg) | ||
require.NoError(t, err) | ||
bls.Verify(client.PublicKey(), sig, validSignatureMsg) |
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.
I think you probably meant to wrap a require.True(...)
around this
t.Run(test.name, func(t *testing.T) { | ||
sig, err := client.Sign(test.msg) | ||
require.Nil(t, sig) | ||
require.EqualError(t, err, test.err.Error()) |
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.
same comment regarding ErrorIs
func TestValidPOPSignature(t *testing.T) { | ||
sig, err := client.SignProofOfPossession(validSignatureMsg) | ||
require.NoError(t, err) | ||
bls.VerifyProofOfPossession(client.PublicKey(), sig, validSignatureMsg) |
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.
Same comment regarding the assertion
t.Run(test.name, func(t *testing.T) { | ||
sig, err := client.SignProofOfPossession(test.msg) | ||
require.Nil(t, sig) | ||
require.EqualError(t, err, test.err.Error()) |
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.
Can't these tests use require.ErrorIs
which also handles error wrapping?
localSigner, _ = localsigner.New() | ||
|
||
client = &Client{ | ||
client: &testClient{}, | ||
pk: localSigner.PublicKey(), | ||
} | ||
|
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.
Why do we re-use this instance across tests? Even if there isn't mutable state I generally try to avoid sharing types across tests to avoid introducing it accidentally
} | ||
|
||
func (*testClient) PublicKey(_ context.Context, _ *signer.PublicKeyRequest, _ ...grpc.CallOption) (*signer.PublicKeyResponse, error) { | ||
return nil, nil |
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.
I think this is supposed to return the pk instead of nil - my guess is that this isn't caught because of the missing assertions
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.
This isn't tested, I'll add a comment
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.
Comment added
bytes := sig.Serialize() | ||
|
||
return &signer.SignResponse{ | ||
Signature: bytes[:bls.SignatureLen], |
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.
Why do we reslice here? Is this this sig otherwise not 96 bytes?
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.
It's a failure case, I'll add a comment
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.
comments added
Why this should be merged
If we want to add any networking in between the keys, we'll need a fallible API.
I've implemented the
rpcsigner
here as a grpc client with a server implemented in a_test.go
file. The idea here is that anyone who implements the interface.Open Question:
There's one little gotchya here for anyone implementing the interface. It's that we expect the keys and signatures to be compressed when sent over the wire. I'm open to using the more general serialization OR adding the
compressed
prefix to the proto-message field names.TODO:
Further, we should probably add a minimal test suite that an external implementor can run, however, I think could probably be implemented in a follow up PR as it is not a priority at the moment.
How this works
How this was tested
Need to be documented in RELEASES.md?