diff --git a/crypto/multisig/compact_bit_array.go b/crypto/multisig/compact_bit_array.go index d14dd8e6..f7f508ce 100644 --- a/crypto/multisig/compact_bit_array.go +++ b/crypto/multisig/compact_bit_array.go @@ -69,6 +69,21 @@ func (bA *CompactBitArray) SetIndex(i int, v bool) bool { return true } +// trueIndex 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, trueIndex(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) trueIndex(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 91a82192..dd3bed76 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 TestCompactBitArrayTrueIndex(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.trueIndex(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 00000000..60300ef5 --- /dev/null +++ b/crypto/multisig/multisignature.go @@ -0,0 +1,60 @@ +package multisig + +import "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. +func (mSig *Multisignature) AddSignature(sig []byte, index int) { + i := mSig.BitArray.trueIndex(index) + // Signature already exists, just replace the value there + if mSig.BitArray.GetIndex(index) { + mSig.Sigs[i] = sig + return + } + mSig.BitArray.SetIndex(index, true) + // Optimization if the index is the greatest index + if i > 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[i+1:], mSig.Sigs[i:]) + mSig.Sigs[i] = 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) { + index := GetIndex(pubkey, keys) + mSig.AddSignature(sig, index) +} + +// Marshal the multisignature with amino +func (mSig *Multisignature) Marshal() []byte { + return cdc.MustMarshalBinary(mSig) +} diff --git a/crypto/multisig/threshold_multisig.go b/crypto/multisig/threshold_multisig.go new file mode 100644 index 00000000..79438f89 --- /dev/null +++ b/crypto/multisig/threshold_multisig.go @@ -0,0 +1,78 @@ +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. +func NewThresholdMultiSignaturePubKey(k int, pubkeys []crypto.PubKey) crypto.PubKey { + 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.UnmarshalBinary(marshalledSig, &sig) + if err != nil { + return false + } + size := sig.BitArray.Size() + if len(sig.Sigs) < int(pk.K) || len(pk.Pubkeys) != size { + 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.MustMarshalBinary(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 { + if otherKey, ok := other.(*ThresholdMultiSignaturePubKey); ok { + if pk.K != otherKey.K { + return false + } + for i := uint(0); i < pk.K; i++ { + if !pk.Pubkeys[i].Equals(otherKey.Pubkeys[i]) { + return false + } + } + return true + } + return false +} diff --git a/crypto/multisig/threshold_multisig_test.go b/crypto/multisig/threshold_multisig_test.go new file mode 100644 index 00000000..31da1319 --- /dev/null +++ b/crypto/multisig/threshold_multisig_test.go @@ -0,0 +1,73 @@ +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" +) + +func TestThresholdMultisig(t *testing.T) { + msg := []byte{1, 2, 3, 4} + 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) + require.False(t, multisigKey.VerifyBytes(msg, multisignature.Marshal())) + // Make sure adding the same signature twice doesn't make the signature pass + multisignature.AddSignatureFromPubkey(sigs[0], pubkeys[0], pubkeys) + require.False(t, multisigKey.VerifyBytes(msg, multisignature.Marshal())) + + // Adding two signatures should make it pass, as k = 2 + multisignature.AddSignatureFromPubkey(sigs[3], pubkeys[3], pubkeys) + require.True(t, multisigKey.VerifyBytes(msg, multisignature.Marshal())) + + // Adding a third invalid signature should make verification fail. + multisignature.AddSignatureFromPubkey(sigs[0], pubkeys[4], pubkeys) + require.False(t, multisigKey.VerifyBytes(msg, multisignature.Marshal())) + + // try adding the invalid signature one signature before + // first reset the multisig + multisignature.BitArray.SetIndex(4, false) + multisignature.Sigs = multisignature.Sigs[:2] + multisignature.AddSignatureFromPubkey(sigs[0], pubkeys[2], pubkeys) + require.False(t, multisigKey.VerifyBytes(msg, multisignature.Marshal())) +} + +func TestMultiSigPubkeyEquality(t *testing.T) { + msg := []byte{1, 2, 3, 4} + pubkeys, _ := generatePubKeysAndSignatures(5, msg) + multisigKey := NewThresholdMultiSignaturePubKey(2, pubkeys) + var unmarshalledMultisig *ThresholdMultiSignaturePubKey + cdc.MustUnmarshalBinary(multisigKey.Bytes(), &unmarshalledMultisig) + require.Equal(t, multisigKey, 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.NotEqual(t, multisigKey, 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 00000000..e0cb2a59 --- /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/ThresholdMultisigPubkey" +) + +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) +}