Browse Source

Merge pull request #2164 from tendermint/dev/multisig

Threshold Multisignature implementation
pull/2205/head
Ethan Buchman 6 years ago
committed by GitHub
parent
commit
db53dc5fd4
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 338 additions and 0 deletions
  1. +13
    -0
      crypto/multisig/compact_bit_array.go
  2. +26
    -0
      crypto/multisig/compact_bit_array_test.go
  3. +69
    -0
      crypto/multisig/multisignature.go
  4. +92
    -0
      crypto/multisig/threshold_multisig.go
  5. +112
    -0
      crypto/multisig/threshold_multisig_test.go
  6. +26
    -0
      crypto/multisig/wire.go

+ 13
- 0
crypto/multisig/compact_bit_array.go View File

@ -71,6 +71,19 @@ func (bA *CompactBitArray) SetIndex(i int, v bool) bool {
return true 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. // Copy returns a copy of the provided bit array.
func (bA *CompactBitArray) Copy() *CompactBitArray { func (bA *CompactBitArray) Copy() *CompactBitArray {
if bA == nil { if bA == nil {


+ 26
- 0
crypto/multisig/compact_bit_array_test.go View File

@ -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) { func TestCompactBitArrayGetSetIndex(t *testing.T) {
r := rand.New(rand.NewSource(100)) r := rand.New(rand.NewSource(100))
numTests := 10 numTests := 10


+ 69
- 0
crypto/multisig/multisignature.go View File

@ -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)
}

+ 92
- 0
crypto/multisig/threshold_multisig.go View File

@ -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
}

+ 112
- 0
crypto/multisig/threshold_multisig_test.go View File

@ -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
}

+ 26
- 0
crypto/multisig/wire.go View File

@ -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)
}

Loading…
Cancel
Save