Conversation
This is in a separate PR for ease of review.
This is in a separate PR for ease of review.
Codecov Report
@@ Coverage Diff @@
## develop #2164 +/- ##
===========================================
+ Coverage 62.23% 62.35% +0.12%
===========================================
Files 212 215 +3
Lines 17567 17632 +65
===========================================
+ Hits 10932 10994 +62
Misses 5725 5725
- Partials 910 913 +3
|
e532602 to
67b6d51
Compare
| pubkeysCpy[3] = pubkeys[4] | ||
| multisigKey2 := NewThresholdMultiSignaturePubKey(2, pubkeysCpy) | ||
| require.NotEqual(t, multisigKey, multisigKey2) | ||
| require.False(t, multisigKey.Equals(multisigKey2)) |
There was a problem hiding this comment.
Interesting, I would also have expected this to call the underlying Equal instead
|
Looks like there's some commit chaos here - can we get the relevant commits rebased on develop? |
|
Absolutely! Let's merge the compact bit array pr first, that should simplify rebasing |
melekes
left a comment
There was a problem hiding this comment.
Needs to be rebased against latest develop.
ebuchman
left a comment
There was a problem hiding this comment.
Awesome work @ValarDragon. Lots of minor comments/questions about comments, code structure, and some additional checks we could do.
We should definitely up the tests and use tables, and figure out how we want to address the recursion.
Haven't reviewed the bit array yet - will do that next.
Happy for this to be merged if we open an issue to address this review - but we should address everything before it makes it to a release.
| // Sigs is a list of signatures, sorted by corresponding index. | ||
| type Multisignature struct { | ||
| BitArray *CompactBitArray | ||
| Sigs [][]byte |
There was a problem hiding this comment.
What do you think of introducing a type Signature []byte instead of using []byte directly everywhere?
There was a problem hiding this comment.
If you think that improves clarity, then sure. I didn't really see it as a problem.
There was a problem hiding this comment.
Leave for now. Maybe we'll open an issue for it later
| return -1 | ||
| } | ||
|
|
||
| // AddSignature adds a signature to the multisig, at the corresponding index. |
There was a problem hiding this comment.
// If the signature already exists, replace it.
|
|
||
| var _ crypto.PubKey = &ThresholdMultiSignaturePubKey{} | ||
|
|
||
| // NewThresholdMultiSignaturePubKey returns a new ThresholdMultiSignaturePubKey. |
There was a problem hiding this comment.
// Panics if len(pubkeys) < k
There was a problem hiding this comment.
I saw your comment about Go not having these panics. I think that's bad on their part. We should be as clear as possible to users about when we panic.
|
|
||
| // NewThresholdMultiSignaturePubKey returns a new ThresholdMultiSignaturePubKey. | ||
| func NewThresholdMultiSignaturePubKey(k int, pubkeys []crypto.PubKey) crypto.PubKey { | ||
| if len(pubkeys) < k { |
There was a problem hiding this comment.
lets also check that k > 0 since the construction is meaningless otherwise and the uint will make k > len(pubkeys ) :P
| } | ||
|
|
||
| // NewMultisig returns a new Multisignature of size n. | ||
| func NewMultisig(n int) *Multisignature { |
There was a problem hiding this comment.
Should we consider not using a pointer here? The methods could return new Multisignature, and the object becomes immutable
There was a problem hiding this comment.
The object shouldn't be immutable? You want to call AddSignature to it repeatedly.
There was a problem hiding this comment.
Was suggesting AddSignature to return a new Multisignature, but yeh it's silly
| return false | ||
| } | ||
| size := sig.BitArray.Size() | ||
| if len(sig.Sigs) < int(pk.K) || len(pk.Pubkeys) != size || sig.BitArray.NumOfTrueBitsBefore(size) < int(pk.K) { |
There was a problem hiding this comment.
Should we also do a consistency check that len(sig.Sigs) < size?
Maybe it would be better if we split this up and evaluate the concerns separately.
Like first we check the size of the bitarray against the number of sigs and the number of pubkeys.
Then we handle the K stuff.
There was a problem hiding this comment.
Theres no point in checking len(sig.Sigs) < size. k <= size. If k was > size, the sig would be impossible to verify. (I guess we can ban that case in marshalling / unmarshalling the multisig)
There was a problem hiding this comment.
but we allow len(sigs.Sigs) > k, so we should set an upper bound.
There was a problem hiding this comment.
Oh woops, your totally right, great catch!
| "github.com/tendermint/tendermint/crypto/secp256k1" | ||
| ) | ||
|
|
||
| func TestThresholdMultisig(t *testing.T) { |
There was a problem hiding this comment.
I think this test is ripe for table-driven testing.
We should be testing multiple values of N, and make sure we get expected behaviour when N<=0 and when N=1.
We should be testing multiple values of K, including that we get expected behaviour in the edges, eg. when K<=0, K=1, K=N-1, K=N, K>N
There was a problem hiding this comment.
Same with the other tests really ...
There was a problem hiding this comment.
We should also test what happens with recursive multisig pubkeys. Or if we want to restrict it, then test it fails.
There was a problem hiding this comment.
We should also test unmarshalling
There was a problem hiding this comment.
Table driven sounds good. I do think we should enable recursive multisigs.
Not really sure why we would test unmarshalling. We assume that amino works, and all variables are public?
| ) | ||
|
|
||
| // TODO: Figure out API for others to either add their own pubkey types, or | ||
| // to make verify / marshal accept a cdc. |
There was a problem hiding this comment.
Huh. We don't really expose this as an API at all, which perhaps suggests that PubKey really could be a oneof
There was a problem hiding this comment.
I'm confused. Don't we have to register the pubkey type in order to decode it? I'm not really sure what your suggesting. (Perhaps due to my unfamiliarity with oneof)
There was a problem hiding this comment.
Yes I wasn't suggesting you do anything. But it goes back to the larger conversation of "do we really need pubkey to be an any-one-can-implement-it-without-permission interface" ie. the kind of thing Amino was built for, or would pure protobuf (using protobufs union type, called oneof) do just fine, since there isn't really a nice way to add new pubkey types right now without changing our crypto lib
Note this is unlike Msg in the SDK which really does want to be an interface.
There was a problem hiding this comment.
Oh, I think pure protobuf is better here. We have to register it in all the codecs, so its not really like anyone can just implement it.
| cdc.RegisterConcrete(ThresholdMultiSignaturePubKey{}, | ||
| ThresholdPubkeyAminoRoute, nil) | ||
| cdc.RegisterConcrete(ed25519.PubKeyEd25519{}, | ||
| ed25519.Ed25519PubKeyAminoRoute, nil) |
There was a problem hiding this comment.
oh, should these just becomePubKeyAminoRoute since it's an anti-pattern to have the double prefix (pkg and var name)?
There was a problem hiding this comment.
You're right, they should
There was a problem hiding this comment.
I'll address this in a separate PR
crypto/multisig/wire.go
Outdated
| // TODO: Figure out API for others to either add their own pubkey types, or | ||
| // to make verify / marshal accept a cdc. | ||
| const ( | ||
| ThresholdPubkeyAminoRoute = "tendermint/ThresholdMultisigPubkey" |
There was a problem hiding this comment.
We use eg. tendermint/PubKeyEd25519 so this should be tendermint/PubKeyMultisigThreshold
|
Some review of the compact bit array PR: #2140 (review) |
|
I think we should merge this. I can add more test cases and documentation for the recursive case in a separate PR. (I'll make those other tests table driven there as well. I made the main test table driven for what its aimed for, though I need to populate that table more as well) The remaining things should be discussed in seperate issues or require seperate PR's. |
31a0567 to
8d28344
Compare
|
Follow up comment made in an open issue! |
crypto/multisig/compact_bit_array.go
Outdated
| // e.g. if bA = _XX_X_X, NumOfTrueBitsBefore(4) = 2, since | ||
| // the value at index 4 of the bit array is the third | ||
| // value that is true. (And it is 0-indexed) | ||
| func (bA *CompactBitArray) NumOfTrueBitsBefore(index int) int { |
There was a problem hiding this comment.
minor point, I'd just call this NumTrueBitsBefore(index int). I think "of" is implied.
crypto/multisig/compact_bit_array.go
Outdated
| return true | ||
| } | ||
|
|
||
| // NumOfTrueBitsBefore returns the location of the given index, among the |
There was a problem hiding this comment.
"returns the location of the given index" is not the right wording since it doesn't return a location, but number of bits true.
crypto/multisig/compact_bit_array.go
Outdated
|
|
||
| // NumOfTrueBitsBefore returns the location of the given index, among the | ||
| // values in the bit array that are set to true. | ||
| // e.g. if bA = _XX_X_X, NumOfTrueBitsBefore(4) = 2, since |
There was a problem hiding this comment.
"since ... value at index 4 .... is true"
"since" seems like a strange modifier and the following statement a bit orthogonal.
There was a problem hiding this comment.
Maybe it make sense to let the user specify 0 to count the total number of true bits?
And then we can comment that, which makes it clear that you don't want to enter NumTrueBitsBefore(0) when you really mean NumTrueBitsBefore(1).
There was a problem hiding this comment.
I feel like making edge cases like letting 0 counting the number of true bits makes it more confusing. I do definitely agree with your point about the comments here!
|
|
Sorry I forgot to squash everything. Follow up is in #2163 (comment) |
This adds an implementation of the threshold multisig. I also merged develop in, for the change to the crypto.Signature interface. This is the only commit with new code changes: e7dd76c.
This may be easier to review if we merged the compact bitmap PR first, and then I aimed this PR at develop / remade this PR.