mirror of
https://github.com/fluencelabs/tendermint
synced 2025-06-30 13:11:38 +00:00
Move tx from light-client and add tests
This commit is contained in:
10
tx/docs.go
Normal file
10
tx/docs.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/*
|
||||||
|
package tx contains generic Signable implementations that can be used
|
||||||
|
by your application or tests to handle authentication needs.
|
||||||
|
|
||||||
|
It currently supports transaction data as opaque bytes and either single
|
||||||
|
or multiple private key signatures using straightforward algorithms.
|
||||||
|
It currently does not support N-of-M key share signing of other more
|
||||||
|
complex algorithms (although it would be great to add them)
|
||||||
|
*/
|
||||||
|
package tx
|
67
tx/multi.go
Normal file
67
tx/multi.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package tx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
crypto "github.com/tendermint/go-crypto"
|
||||||
|
data "github.com/tendermint/go-data"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MultiSig lets us wrap arbitrary data with a go-crypto signature
|
||||||
|
//
|
||||||
|
// TODO: rethink how we want to integrate this with KeyStore so it makes
|
||||||
|
// more sense (particularly the verify method)
|
||||||
|
type MultiSig struct {
|
||||||
|
Data data.Bytes
|
||||||
|
Sigs []Signed
|
||||||
|
}
|
||||||
|
|
||||||
|
type Signed struct {
|
||||||
|
Sig crypto.SignatureS
|
||||||
|
Pubkey crypto.PubKeyS
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ SigInner = &MultiSig{}
|
||||||
|
|
||||||
|
func NewMulti(data []byte) Sig {
|
||||||
|
return Sig{&MultiSig{Data: data}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignBytes returns the original data passed into `NewSig`
|
||||||
|
func (s *MultiSig) SignBytes() []byte {
|
||||||
|
return s.Data
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign will add a signature and pubkey.
|
||||||
|
//
|
||||||
|
// Depending on the Signable, one may be able to call this multiple times for multisig
|
||||||
|
// Returns error if called with invalid data or too many times
|
||||||
|
func (s *MultiSig) Sign(pubkey crypto.PubKey, sig crypto.Signature) error {
|
||||||
|
if pubkey == nil || sig == nil {
|
||||||
|
return errors.New("Signature or Key missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the value once we are happy
|
||||||
|
x := Signed{crypto.SignatureS{sig}, crypto.PubKeyS{pubkey}}
|
||||||
|
s.Sigs = append(s.Sigs, x)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signers will return the public key(s) that signed if the signature
|
||||||
|
// is valid, or an error if there is any issue with the signature,
|
||||||
|
// including if there are no signatures
|
||||||
|
func (s *MultiSig) Signers() ([]crypto.PubKey, error) {
|
||||||
|
if len(s.Sigs) == 0 {
|
||||||
|
return nil, errors.New("Never signed")
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := make([]crypto.PubKey, len(s.Sigs))
|
||||||
|
for i := range s.Sigs {
|
||||||
|
ms := s.Sigs[i]
|
||||||
|
if !ms.Pubkey.VerifyBytes(s.Data, ms.Sig) {
|
||||||
|
return nil, errors.Errorf("Signature %d doesn't match (key: %X)", i, ms.Pubkey.Bytes())
|
||||||
|
}
|
||||||
|
keys[i] = ms.Pubkey
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys, nil
|
||||||
|
}
|
77
tx/multi_test.go
Normal file
77
tx/multi_test.go
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
package tx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
crypto "github.com/tendermint/go-crypto"
|
||||||
|
keys "github.com/tendermint/go-keys"
|
||||||
|
"github.com/tendermint/go-keys/cryptostore"
|
||||||
|
"github.com/tendermint/go-keys/storage/memstorage"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMultiSig(t *testing.T) {
|
||||||
|
assert, require := assert.New(t), require.New(t)
|
||||||
|
|
||||||
|
algo := crypto.NameEd25519
|
||||||
|
cstore := cryptostore.New(
|
||||||
|
cryptostore.SecretBox,
|
||||||
|
memstorage.New(),
|
||||||
|
)
|
||||||
|
n, p := "foo", "bar"
|
||||||
|
n2, p2 := "other", "thing"
|
||||||
|
|
||||||
|
acct, err := cstore.Create(n, p, algo)
|
||||||
|
require.Nil(err, "%+v", err)
|
||||||
|
acct2, err := cstore.Create(n2, p2, algo)
|
||||||
|
require.Nil(err, "%+v", err)
|
||||||
|
|
||||||
|
type signer struct {
|
||||||
|
key keys.Info
|
||||||
|
name, pass string
|
||||||
|
}
|
||||||
|
cases := []struct {
|
||||||
|
data string
|
||||||
|
signers []signer
|
||||||
|
}{
|
||||||
|
{"one", []signer{{acct, n, p}}},
|
||||||
|
{"two", []signer{{acct2, n2, p2}}},
|
||||||
|
{"both", []signer{{acct, n, p}, {acct2, n2, p2}}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
tx := NewMulti([]byte(tc.data))
|
||||||
|
// unsigned version
|
||||||
|
_, err = tx.Signers()
|
||||||
|
assert.NotNil(err)
|
||||||
|
orig, err := tx.TxBytes()
|
||||||
|
require.Nil(err, "%+v", err)
|
||||||
|
data := tx.SignBytes()
|
||||||
|
assert.Equal(tc.data, string(data))
|
||||||
|
|
||||||
|
// sign it
|
||||||
|
for _, s := range tc.signers {
|
||||||
|
err = cstore.Sign(s.name, s.pass, tx)
|
||||||
|
require.Nil(err, "%+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure it is proper now
|
||||||
|
sigs, err := tx.Signers()
|
||||||
|
require.Nil(err, "%+v", err)
|
||||||
|
if assert.Equal(len(tc.signers), len(sigs)) {
|
||||||
|
for i := range sigs {
|
||||||
|
// This must be refactored...
|
||||||
|
assert.Equal(tc.signers[i].key.PubKey, sigs[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// the tx bytes should change after this
|
||||||
|
after, err := tx.TxBytes()
|
||||||
|
require.Nil(err, "%+v", err)
|
||||||
|
assert.NotEqual(orig, after, "%X != %X", orig, after)
|
||||||
|
|
||||||
|
// sign bytes are the same
|
||||||
|
data = tx.SignBytes()
|
||||||
|
assert.Equal(tc.data, string(data))
|
||||||
|
}
|
||||||
|
}
|
58
tx/one.go
Normal file
58
tx/one.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package tx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
crypto "github.com/tendermint/go-crypto"
|
||||||
|
data "github.com/tendermint/go-data"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OneSig lets us wrap arbitrary data with a go-crypto signature
|
||||||
|
//
|
||||||
|
// TODO: rethink how we want to integrate this with KeyStore so it makes
|
||||||
|
// more sense (particularly the verify method)
|
||||||
|
type OneSig struct {
|
||||||
|
Data data.Bytes
|
||||||
|
Signed
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ SigInner = &OneSig{}
|
||||||
|
|
||||||
|
func New(data []byte) Sig {
|
||||||
|
return WrapSig(&OneSig{Data: data})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignBytes returns the original data passed into `NewSig`
|
||||||
|
func (s *OneSig) SignBytes() []byte {
|
||||||
|
return s.Data
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign will add a signature and pubkey.
|
||||||
|
//
|
||||||
|
// Depending on the Signable, one may be able to call this multiple times for multisig
|
||||||
|
// Returns error if called with invalid data or too many times
|
||||||
|
func (s *OneSig) Sign(pubkey crypto.PubKey, sig crypto.Signature) error {
|
||||||
|
if pubkey == nil || sig == nil {
|
||||||
|
return errors.New("Signature or Key missing")
|
||||||
|
}
|
||||||
|
if !s.Sig.Empty() {
|
||||||
|
return errors.New("Transaction can only be signed once")
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the value once we are happy
|
||||||
|
s.Pubkey = crypto.PubKeyS{pubkey}
|
||||||
|
s.Sig = crypto.SignatureS{sig}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signers will return the public key(s) that signed if the signature
|
||||||
|
// is valid, or an error if there is any issue with the signature,
|
||||||
|
// including if there are no signatures
|
||||||
|
func (s *OneSig) Signers() ([]crypto.PubKey, error) {
|
||||||
|
if s.Pubkey.Empty() || s.Sig.Empty() {
|
||||||
|
return nil, errors.New("Never signed")
|
||||||
|
}
|
||||||
|
if !s.Pubkey.VerifyBytes(s.Data, s.Sig) {
|
||||||
|
return nil, errors.New("Signature doesn't match")
|
||||||
|
}
|
||||||
|
return []crypto.PubKey{s.Pubkey}, nil
|
||||||
|
}
|
73
tx/one_test.go
Normal file
73
tx/one_test.go
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
package tx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
crypto "github.com/tendermint/go-crypto"
|
||||||
|
keys "github.com/tendermint/go-keys"
|
||||||
|
"github.com/tendermint/go-keys/cryptostore"
|
||||||
|
"github.com/tendermint/go-keys/storage/memstorage"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOneSig(t *testing.T) {
|
||||||
|
assert, require := assert.New(t), require.New(t)
|
||||||
|
|
||||||
|
algo := crypto.NameEd25519
|
||||||
|
cstore := cryptostore.New(
|
||||||
|
cryptostore.SecretBox,
|
||||||
|
memstorage.New(),
|
||||||
|
)
|
||||||
|
n, p := "foo", "bar"
|
||||||
|
n2, p2 := "other", "thing"
|
||||||
|
|
||||||
|
acct, err := cstore.Create(n, p, algo)
|
||||||
|
require.Nil(err, "%+v", err)
|
||||||
|
acct2, err := cstore.Create(n2, p2, algo)
|
||||||
|
require.Nil(err, "%+v", err)
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
data string
|
||||||
|
key keys.Info
|
||||||
|
name, pass string
|
||||||
|
}{
|
||||||
|
{"first", acct, n, p},
|
||||||
|
{"kehfkhefy8y", acct, n, p},
|
||||||
|
{"second", acct2, n2, p2},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
tx := New([]byte(tc.data))
|
||||||
|
// unsigned version
|
||||||
|
_, err = tx.Signers()
|
||||||
|
assert.NotNil(err)
|
||||||
|
orig, err := tx.TxBytes()
|
||||||
|
require.Nil(err, "%+v", err)
|
||||||
|
data := tx.SignBytes()
|
||||||
|
assert.Equal(tc.data, string(data))
|
||||||
|
|
||||||
|
// sign it
|
||||||
|
err = cstore.Sign(tc.name, tc.pass, tx)
|
||||||
|
require.Nil(err, "%+v", err)
|
||||||
|
// but not twice
|
||||||
|
err = cstore.Sign(tc.name, tc.pass, tx)
|
||||||
|
require.NotNil(err)
|
||||||
|
|
||||||
|
// make sure it is proper now
|
||||||
|
sigs, err := tx.Signers()
|
||||||
|
require.Nil(err, "%+v", err)
|
||||||
|
if assert.Equal(1, len(sigs)) {
|
||||||
|
// This must be refactored...
|
||||||
|
assert.Equal(tc.key.PubKey, sigs[0])
|
||||||
|
}
|
||||||
|
// the tx bytes should change after this
|
||||||
|
after, err := tx.TxBytes()
|
||||||
|
require.Nil(err, "%+v", err)
|
||||||
|
assert.NotEqual(orig, after, "%X != %X", orig, after)
|
||||||
|
|
||||||
|
// sign bytes are the same
|
||||||
|
data = tx.SignBytes()
|
||||||
|
assert.Equal(tc.data, string(data))
|
||||||
|
}
|
||||||
|
}
|
76
tx/reader.go
Normal file
76
tx/reader.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package tx
|
||||||
|
|
||||||
|
import (
|
||||||
|
crypto "github.com/tendermint/go-crypto"
|
||||||
|
data "github.com/tendermint/go-data"
|
||||||
|
keys "github.com/tendermint/go-keys"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
typeOneSig = byte(0x01)
|
||||||
|
typeMultiSig = byte(0x02)
|
||||||
|
nameOneSig = "sig"
|
||||||
|
nameMultiSig = "multi"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ keys.Signable = Sig{}
|
||||||
|
var TxMapper data.Mapper
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
TxMapper = data.NewMapper(Sig{}).
|
||||||
|
RegisterInterface(&OneSig{}, nameOneSig, typeOneSig).
|
||||||
|
RegisterInterface(&MultiSig{}, nameMultiSig, typeMultiSig)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
DO NOT USE this interface.
|
||||||
|
|
||||||
|
It is public by necessity but should never be used directly
|
||||||
|
outside of this package.
|
||||||
|
|
||||||
|
Only use Sig, never SigInner
|
||||||
|
*/
|
||||||
|
type SigInner interface {
|
||||||
|
SignBytes() []byte
|
||||||
|
Sign(pubkey crypto.PubKey, sig crypto.Signature) error
|
||||||
|
Signers() ([]crypto.PubKey, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sig is what is exported, and handles serialization
|
||||||
|
type Sig struct {
|
||||||
|
SigInner
|
||||||
|
}
|
||||||
|
|
||||||
|
// TxBytes
|
||||||
|
func (s Sig) TxBytes() ([]byte, error) {
|
||||||
|
return data.ToWire(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapSig goes from concrete implementation to "interface" struct
|
||||||
|
func WrapSig(pk SigInner) Sig {
|
||||||
|
if wrap, ok := pk.(Sig); ok {
|
||||||
|
pk = wrap.Unwrap()
|
||||||
|
}
|
||||||
|
return Sig{pk}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap recovers the concrete interface safely (regardless of levels of embeds)
|
||||||
|
func (p Sig) Unwrap() SigInner {
|
||||||
|
pk := p.SigInner
|
||||||
|
for wrap, ok := pk.(Sig); ok; wrap, ok = pk.(Sig) {
|
||||||
|
pk = wrap.SigInner
|
||||||
|
}
|
||||||
|
return pk
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Sig) MarshalJSON() ([]byte, error) {
|
||||||
|
return TxMapper.ToJSON(p.Unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Sig) UnmarshalJSON(data []byte) (err error) {
|
||||||
|
parsed, err := TxMapper.FromJSON(data)
|
||||||
|
if err == nil && parsed != nil {
|
||||||
|
p.SigInner = parsed.(SigInner)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
70
tx/reader_test.go
Normal file
70
tx/reader_test.go
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
package tx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
crypto "github.com/tendermint/go-crypto"
|
||||||
|
data "github.com/tendermint/go-data"
|
||||||
|
"github.com/tendermint/go-keys/cryptostore"
|
||||||
|
"github.com/tendermint/go-keys/storage/memstorage"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestReader(t *testing.T) {
|
||||||
|
assert, require := assert.New(t), require.New(t)
|
||||||
|
|
||||||
|
algo := crypto.NameEd25519
|
||||||
|
cstore := cryptostore.New(
|
||||||
|
cryptostore.SecretBox,
|
||||||
|
memstorage.New(),
|
||||||
|
)
|
||||||
|
type sigs struct{ name, pass string }
|
||||||
|
u := sigs{"alice", "1234"}
|
||||||
|
u2 := sigs{"bob", "foobar"}
|
||||||
|
|
||||||
|
_, err := cstore.Create(u.name, u.pass, algo)
|
||||||
|
require.Nil(err, "%+v", err)
|
||||||
|
_, err = cstore.Create(u2.name, u2.pass, algo)
|
||||||
|
require.Nil(err, "%+v", err)
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
tx Sig
|
||||||
|
sigs []sigs
|
||||||
|
}{
|
||||||
|
{New([]byte("first")), nil},
|
||||||
|
{New([]byte("second")), []sigs{u}},
|
||||||
|
{New([]byte("other")), []sigs{u2}},
|
||||||
|
{NewMulti([]byte("m-first")), nil},
|
||||||
|
{NewMulti([]byte("m-second")), []sigs{u}},
|
||||||
|
{NewMulti([]byte("m-other")), []sigs{u, u2}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
tx := tc.tx
|
||||||
|
|
||||||
|
// make sure json serialization and loading works w/o sigs
|
||||||
|
var pre Sig
|
||||||
|
pjs, err := data.ToJSON(tx)
|
||||||
|
require.Nil(err, "%+v", err)
|
||||||
|
err = data.FromJSON(pjs, &pre)
|
||||||
|
require.Nil(err, "%+v", err)
|
||||||
|
assert.Equal(tx, pre)
|
||||||
|
|
||||||
|
for _, s := range tc.sigs {
|
||||||
|
err = cstore.Sign(s.name, s.pass, tx)
|
||||||
|
require.Nil(err, "%+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var post Sig
|
||||||
|
sjs, err := data.ToJSON(tx)
|
||||||
|
require.Nil(err, "%+v", err)
|
||||||
|
err = data.FromJSON(sjs, &post)
|
||||||
|
require.Nil(err, "%+v\n%s", err, string(sjs))
|
||||||
|
assert.Equal(tx, post)
|
||||||
|
|
||||||
|
if len(tc.sigs) > 0 {
|
||||||
|
assert.NotEqual(pjs, sjs, "%s\n ------ %s", string(pjs), string(sjs))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user