diff --git a/crypto/multisig/compact_bit_array.go b/crypto/multisig/compact_bit_array.go index ff6ecd646..94e9791cd 100644 --- a/crypto/multisig/compact_bit_array.go +++ b/crypto/multisig/compact_bit_array.go @@ -71,6 +71,19 @@ func (bA *CompactBitArray) SetIndex(i int, v bool) bool { return true } +// NumTrueBitsBefore returns the number of bits set to true before the +// given index. e.g. if bA = _XX__XX, NumOfTrueBitsBefore(4) = 2, since +// there are two bits set to true before index 4. +func (bA *CompactBitArray) NumTrueBitsBefore(index int) int { + numTrueValues := 0 + for i := 0; i < index; i++ { + if bA.GetIndex(i) { + numTrueValues++ + } + } + return numTrueValues +} + // Copy returns a copy of the provided bit array. func (bA *CompactBitArray) Copy() *CompactBitArray { if bA == nil { diff --git a/crypto/multisig/compact_bit_array_test.go b/crypto/multisig/compact_bit_array_test.go index 91a82192f..8b342b7ad 100644 --- a/crypto/multisig/compact_bit_array_test.go +++ b/crypto/multisig/compact_bit_array_test.go @@ -150,6 +150,32 @@ func TestCompactMarshalUnmarshal(t *testing.T) { } } +func TestCompactBitArrayNumOfTrueBitsBefore(t *testing.T) { + testCases := []struct { + marshalledBA string + bAIndex []int + trueValueIndex []int + }{ + {`"_____"`, []int{0, 1, 2, 3, 4}, []int{0, 0, 0, 0, 0}}, + {`"x"`, []int{0}, []int{0}}, + {`"_x"`, []int{1}, []int{0}}, + {`"x___xxxx"`, []int{0, 4, 5, 6, 7}, []int{0, 1, 2, 3, 4}}, + {`"__x_xx_x__x_x___"`, []int{2, 4, 5, 7, 10, 12}, []int{0, 1, 2, 3, 4, 5}}, + {`"______________xx"`, []int{14, 15}, []int{0, 1}}, + } + for tcIndex, tc := range testCases { + t.Run(tc.marshalledBA, func(t *testing.T) { + var bA *CompactBitArray + err := json.Unmarshal([]byte(tc.marshalledBA), &bA) + require.NoError(t, err) + + for i := 0; i < len(tc.bAIndex); i++ { + require.Equal(t, tc.trueValueIndex[i], bA.NumTrueBitsBefore(tc.bAIndex[i]), "tc %d, i %d", tcIndex, i) + } + }) + } +} + func TestCompactBitArrayGetSetIndex(t *testing.T) { r := rand.New(rand.NewSource(100)) numTests := 10 diff --git a/crypto/multisig/multisignature.go b/crypto/multisig/multisignature.go new file mode 100644 index 000000000..29e8a30b9 --- /dev/null +++ b/crypto/multisig/multisignature.go @@ -0,0 +1,69 @@ +package multisig + +import ( + "errors" + + "github.com/tendermint/tendermint/crypto" +) + +// Multisignature is used to represent the signature object used in the multisigs. +// Sigs is a list of signatures, sorted by corresponding index. +type Multisignature struct { + BitArray *CompactBitArray + Sigs [][]byte +} + +// NewMultisig returns a new Multisignature of size n. +func NewMultisig(n int) *Multisignature { + // Default the signature list to have a capacity of two, since we can + // expect that most multisigs will require multiple signers. + return &Multisignature{NewCompactBitArray(n), make([][]byte, 0, 2)} +} + +// GetIndex returns the index of pk in keys. Returns -1 if not found +func getIndex(pk crypto.PubKey, keys []crypto.PubKey) int { + for i := 0; i < len(keys); i++ { + if pk.Equals(keys[i]) { + return i + } + } + return -1 +} + +// AddSignature adds a signature to the multisig, at the corresponding index. +// If the signature already exists, replace it. +func (mSig *Multisignature) AddSignature(sig []byte, index int) { + newSigIndex := mSig.BitArray.NumTrueBitsBefore(index) + // Signature already exists, just replace the value there + if mSig.BitArray.GetIndex(index) { + mSig.Sigs[newSigIndex] = sig + return + } + mSig.BitArray.SetIndex(index, true) + // Optimization if the index is the greatest index + if newSigIndex == len(mSig.Sigs) { + mSig.Sigs = append(mSig.Sigs, sig) + return + } + // Expand slice by one with a dummy element, move all elements after i + // over by one, then place the new signature in that gap. + mSig.Sigs = append(mSig.Sigs, make([]byte, 0)) + copy(mSig.Sigs[newSigIndex+1:], mSig.Sigs[newSigIndex:]) + mSig.Sigs[newSigIndex] = sig +} + +// AddSignatureFromPubkey adds a signature to the multisig, +// at the index in keys corresponding to the provided pubkey. +func (mSig *Multisignature) AddSignatureFromPubkey(sig []byte, pubkey crypto.PubKey, keys []crypto.PubKey) error { + index := getIndex(pubkey, keys) + if index == -1 { + return errors.New("provided key didn't exist in pubkeys") + } + mSig.AddSignature(sig, index) + return nil +} + +// Marshal the multisignature with amino +func (mSig *Multisignature) Marshal() []byte { + return cdc.MustMarshalBinaryBare(mSig) +} diff --git a/crypto/multisig/threshold_multisig.go b/crypto/multisig/threshold_multisig.go new file mode 100644 index 000000000..58a3ea0e3 --- /dev/null +++ b/crypto/multisig/threshold_multisig.go @@ -0,0 +1,92 @@ +package multisig + +import ( + "github.com/tendermint/tendermint/crypto" + "github.com/tendermint/tendermint/crypto/tmhash" +) + +// ThresholdMultiSignaturePubKey implements a K of N threshold multisig. +type ThresholdMultiSignaturePubKey struct { + K uint `json:"threshold"` + Pubkeys []crypto.PubKey `json:"pubkeys"` +} + +var _ crypto.PubKey = &ThresholdMultiSignaturePubKey{} + +// NewThresholdMultiSignaturePubKey returns a new ThresholdMultiSignaturePubKey. +// Panics if len(pubkeys) < k or 0 >= k. +func NewThresholdMultiSignaturePubKey(k int, pubkeys []crypto.PubKey) crypto.PubKey { + if k <= 0 { + panic("threshold k of n multisignature: k <= 0") + } + if len(pubkeys) < k { + panic("threshold k of n multisignature: len(pubkeys) < k") + } + return &ThresholdMultiSignaturePubKey{uint(k), pubkeys} +} + +// VerifyBytes expects sig to be an amino encoded version of a MultiSignature. +// Returns true iff the multisignature contains k or more signatures +// for the correct corresponding keys, +// and all signatures are valid. (Not just k of the signatures) +// The multisig uses a bitarray, so multiple signatures for the same key is not +// a concern. +func (pk *ThresholdMultiSignaturePubKey) VerifyBytes(msg []byte, marshalledSig []byte) bool { + var sig *Multisignature + err := cdc.UnmarshalBinaryBare(marshalledSig, &sig) + if err != nil { + return false + } + size := sig.BitArray.Size() + // ensure bit array is the correct size + if len(pk.Pubkeys) != size { + return false + } + // ensure size of signature list + if len(sig.Sigs) < int(pk.K) || len(sig.Sigs) > size { + return false + } + // ensure at least k signatures are set + if sig.BitArray.NumTrueBitsBefore(size) < int(pk.K) { + return false + } + // index in the list of signatures which we are concerned with. + sigIndex := 0 + for i := 0; i < size; i++ { + if sig.BitArray.GetIndex(i) { + if !pk.Pubkeys[i].VerifyBytes(msg, sig.Sigs[sigIndex]) { + return false + } + sigIndex++ + } + } + return true +} + +// Bytes returns the amino encoded version of the ThresholdMultiSignaturePubKey +func (pk *ThresholdMultiSignaturePubKey) Bytes() []byte { + return cdc.MustMarshalBinaryBare(pk) +} + +// Address returns tmhash(ThresholdMultiSignaturePubKey.Bytes()) +func (pk *ThresholdMultiSignaturePubKey) Address() crypto.Address { + return crypto.Address(tmhash.Sum(pk.Bytes())) +} + +// Equals returns true iff pk and other both have the same number of keys, and +// all constituent keys are the same, and in the same order. +func (pk *ThresholdMultiSignaturePubKey) Equals(other crypto.PubKey) bool { + otherKey, sameType := other.(*ThresholdMultiSignaturePubKey) + if !sameType { + return false + } + if pk.K != otherKey.K || len(pk.Pubkeys) != len(otherKey.Pubkeys) { + return false + } + for i := 0; i < len(pk.Pubkeys); i++ { + if !pk.Pubkeys[i].Equals(otherKey.Pubkeys[i]) { + return false + } + } + return true +} diff --git a/crypto/multisig/threshold_multisig_test.go b/crypto/multisig/threshold_multisig_test.go new file mode 100644 index 000000000..2d2fdeec8 --- /dev/null +++ b/crypto/multisig/threshold_multisig_test.go @@ -0,0 +1,112 @@ +package multisig + +import ( + "math/rand" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/tendermint/tendermint/crypto" + "github.com/tendermint/tendermint/crypto/ed25519" + "github.com/tendermint/tendermint/crypto/secp256k1" +) + +// This tests multisig functionality, but it expects the first k signatures to be valid +// TODO: Adapt it to give more flexibility about first k signatures being valid +func TestThresholdMultisigValidCases(t *testing.T) { + pkSet1, sigSet1 := generatePubKeysAndSignatures(5, []byte{1, 2, 3, 4}) + cases := []struct { + msg []byte + k int + pubkeys []crypto.PubKey + signingIndices []int + // signatures should be the same size as signingIndices. + signatures [][]byte + passAfterKSignatures []bool + }{ + { + msg: []byte{1, 2, 3, 4}, + k: 2, + pubkeys: pkSet1, + signingIndices: []int{0, 3, 1}, + signatures: sigSet1, + passAfterKSignatures: []bool{false}, + }, + } + for tcIndex, tc := range cases { + multisigKey := NewThresholdMultiSignaturePubKey(tc.k, tc.pubkeys) + multisignature := NewMultisig(len(tc.pubkeys)) + for i := 0; i < tc.k-1; i++ { + signingIndex := tc.signingIndices[i] + multisignature.AddSignatureFromPubkey(tc.signatures[signingIndex], tc.pubkeys[signingIndex], tc.pubkeys) + require.False(t, multisigKey.VerifyBytes(tc.msg, multisignature.Marshal()), + "multisig passed when i < k, tc %d, i %d", tcIndex, i) + multisignature.AddSignatureFromPubkey(tc.signatures[signingIndex], tc.pubkeys[signingIndex], tc.pubkeys) + require.Equal(t, i+1, len(multisignature.Sigs), + "adding a signature for the same pubkey twice increased signature count by 2, tc %d", tcIndex) + } + require.False(t, multisigKey.VerifyBytes(tc.msg, multisignature.Marshal()), + "multisig passed with k - 1 sigs, tc %d", tcIndex) + multisignature.AddSignatureFromPubkey(tc.signatures[tc.signingIndices[tc.k]], tc.pubkeys[tc.signingIndices[tc.k]], tc.pubkeys) + require.True(t, multisigKey.VerifyBytes(tc.msg, multisignature.Marshal()), + "multisig failed after k good signatures, tc %d", tcIndex) + for i := tc.k + 1; i < len(tc.signingIndices); i++ { + signingIndex := tc.signingIndices[i] + multisignature.AddSignatureFromPubkey(tc.signatures[signingIndex], tc.pubkeys[signingIndex], tc.pubkeys) + require.Equal(t, tc.passAfterKSignatures[i-tc.k-1], + multisigKey.VerifyBytes(tc.msg, multisignature.Marshal()), + "multisig didn't verify as expected after k sigs, tc %d, i %d", tcIndex, i) + + multisignature.AddSignatureFromPubkey(tc.signatures[signingIndex], tc.pubkeys[signingIndex], tc.pubkeys) + require.Equal(t, i+1, len(multisignature.Sigs), + "adding a signature for the same pubkey twice increased signature count by 2, tc %d", tcIndex) + } + } +} + +// TODO: Fully replace this test with table driven tests +func TestThresholdMultisigDuplicateSignatures(t *testing.T) { + msg := []byte{1, 2, 3, 4, 5} + pubkeys, sigs := generatePubKeysAndSignatures(5, msg) + multisigKey := NewThresholdMultiSignaturePubKey(2, pubkeys) + multisignature := NewMultisig(5) + require.False(t, multisigKey.VerifyBytes(msg, multisignature.Marshal())) + multisignature.AddSignatureFromPubkey(sigs[0], pubkeys[0], pubkeys) + // Add second signature manually + multisignature.Sigs = append(multisignature.Sigs, sigs[0]) + require.False(t, multisigKey.VerifyBytes(msg, multisignature.Marshal())) +} + +// TODO: Fully replace this test with table driven tests +func TestMultiSigPubkeyEquality(t *testing.T) { + msg := []byte{1, 2, 3, 4} + pubkeys, _ := generatePubKeysAndSignatures(5, msg) + multisigKey := NewThresholdMultiSignaturePubKey(2, pubkeys) + var unmarshalledMultisig *ThresholdMultiSignaturePubKey + cdc.MustUnmarshalBinaryBare(multisigKey.Bytes(), &unmarshalledMultisig) + require.True(t, multisigKey.Equals(unmarshalledMultisig)) + + // Ensure that reordering pubkeys is treated as a different pubkey + pubkeysCpy := make([]crypto.PubKey, 5) + copy(pubkeysCpy, pubkeys) + pubkeysCpy[4] = pubkeys[3] + pubkeysCpy[3] = pubkeys[4] + multisigKey2 := NewThresholdMultiSignaturePubKey(2, pubkeysCpy) + require.False(t, multisigKey.Equals(multisigKey2)) +} + +func generatePubKeysAndSignatures(n int, msg []byte) (pubkeys []crypto.PubKey, signatures [][]byte) { + pubkeys = make([]crypto.PubKey, n) + signatures = make([][]byte, n) + for i := 0; i < n; i++ { + var privkey crypto.PrivKey + if rand.Int63()%2 == 0 { + privkey = ed25519.GenPrivKey() + } else { + privkey = secp256k1.GenPrivKey() + } + pubkeys[i] = privkey.PubKey() + signatures[i], _ = privkey.Sign(msg) + } + return +} diff --git a/crypto/multisig/wire.go b/crypto/multisig/wire.go new file mode 100644 index 000000000..a6cda34a9 --- /dev/null +++ b/crypto/multisig/wire.go @@ -0,0 +1,26 @@ +package multisig + +import ( + amino "github.com/tendermint/go-amino" + "github.com/tendermint/tendermint/crypto" + "github.com/tendermint/tendermint/crypto/ed25519" + "github.com/tendermint/tendermint/crypto/secp256k1" +) + +// TODO: Figure out API for others to either add their own pubkey types, or +// to make verify / marshal accept a cdc. +const ( + ThresholdPubkeyAminoRoute = "tendermint/PubkeyThresholdMultisig" +) + +var cdc = amino.NewCodec() + +func init() { + cdc.RegisterInterface((*crypto.PubKey)(nil), nil) + cdc.RegisterConcrete(ThresholdMultiSignaturePubKey{}, + ThresholdPubkeyAminoRoute, nil) + cdc.RegisterConcrete(ed25519.PubKeyEd25519{}, + ed25519.Ed25519PubKeyAminoRoute, nil) + cdc.RegisterConcrete(secp256k1.PubKeySecp256k1{}, + secp256k1.Secp256k1PubKeyAminoRoute, nil) +}