Merge pull request #168 from tendermint/feature/160-newseq
Middleware replay protection
This commit is contained in:
commit
33c9f099dd
6
Makefile
6
Makefile
|
@ -27,9 +27,9 @@ test_unit:
|
||||||
|
|
||||||
test_cli: tests/cli/shunit2
|
test_cli: tests/cli/shunit2
|
||||||
# sudo apt-get install jq
|
# sudo apt-get install jq
|
||||||
@./tests/cli/basictx.sh
|
./tests/cli/basictx.sh
|
||||||
@./tests/cli/counter.sh
|
./tests/cli/counter.sh
|
||||||
@./tests/cli/restart.sh
|
./tests/cli/restart.sh
|
||||||
# @./tests/cli/ibc.sh
|
# @./tests/cli/ibc.sh
|
||||||
|
|
||||||
test_tutorial: docs/guide/shunit2
|
test_tutorial: docs/guide/shunit2
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"github.com/tendermint/basecoin/modules/base"
|
"github.com/tendermint/basecoin/modules/base"
|
||||||
"github.com/tendermint/basecoin/modules/coin"
|
"github.com/tendermint/basecoin/modules/coin"
|
||||||
"github.com/tendermint/basecoin/modules/fee"
|
"github.com/tendermint/basecoin/modules/fee"
|
||||||
|
"github.com/tendermint/basecoin/modules/nonce"
|
||||||
"github.com/tendermint/basecoin/stack"
|
"github.com/tendermint/basecoin/stack"
|
||||||
sm "github.com/tendermint/basecoin/state"
|
sm "github.com/tendermint/basecoin/state"
|
||||||
"github.com/tendermint/basecoin/version"
|
"github.com/tendermint/basecoin/version"
|
||||||
|
@ -62,6 +63,7 @@ func DefaultHandler(feeDenom string) basecoin.Handler {
|
||||||
stack.Recovery{},
|
stack.Recovery{},
|
||||||
auth.Signatures{},
|
auth.Signatures{},
|
||||||
base.Chain{},
|
base.Chain{},
|
||||||
|
nonce.ReplayCheck{},
|
||||||
fee.NewSimpleFeeMiddleware(coin.Coin{feeDenom, 0}, fee.Bank),
|
fee.NewSimpleFeeMiddleware(coin.Coin{feeDenom, 0}, fee.Bank),
|
||||||
).Use(d)
|
).Use(d)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"github.com/tendermint/basecoin/modules/base"
|
"github.com/tendermint/basecoin/modules/base"
|
||||||
"github.com/tendermint/basecoin/modules/coin"
|
"github.com/tendermint/basecoin/modules/coin"
|
||||||
"github.com/tendermint/basecoin/modules/fee"
|
"github.com/tendermint/basecoin/modules/fee"
|
||||||
|
"github.com/tendermint/basecoin/modules/nonce"
|
||||||
"github.com/tendermint/basecoin/stack"
|
"github.com/tendermint/basecoin/stack"
|
||||||
"github.com/tendermint/basecoin/state"
|
"github.com/tendermint/basecoin/state"
|
||||||
wire "github.com/tendermint/go-wire"
|
wire "github.com/tendermint/go-wire"
|
||||||
|
@ -55,15 +56,17 @@ func (at *appTest) signTx(tx basecoin.Tx) basecoin.Tx {
|
||||||
return stx.Wrap()
|
return stx.Wrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (at *appTest) getTx(coins coin.Coins) basecoin.Tx {
|
func (at *appTest) getTx(coins coin.Coins, sequence uint32) basecoin.Tx {
|
||||||
tx := at.baseTx(coins)
|
tx := at.baseTx(coins)
|
||||||
|
tx = nonce.NewTx(sequence, []basecoin.Actor{at.acctIn.Actor()}, tx)
|
||||||
tx = base.NewChainTx(at.chainID, 0, tx)
|
tx = base.NewChainTx(at.chainID, 0, tx)
|
||||||
return at.signTx(tx)
|
return at.signTx(tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (at *appTest) feeTx(coins coin.Coins, toll coin.Coin) basecoin.Tx {
|
func (at *appTest) feeTx(coins coin.Coins, toll coin.Coin, sequence uint32) basecoin.Tx {
|
||||||
tx := at.baseTx(coins)
|
tx := at.baseTx(coins)
|
||||||
tx = fee.NewFee(tx, toll, at.acctIn.Actor())
|
tx = fee.NewFee(tx, toll, at.acctIn.Actor())
|
||||||
|
tx = nonce.NewTx(sequence, []basecoin.Actor{at.acctIn.Actor()}, tx)
|
||||||
tx = base.NewChainTx(at.chainID, 0, tx)
|
tx = base.NewChainTx(at.chainID, 0, tx)
|
||||||
return at.signTx(tx)
|
return at.signTx(tx)
|
||||||
}
|
}
|
||||||
|
@ -210,22 +213,22 @@ func TestTx(t *testing.T) {
|
||||||
//Bad Balance
|
//Bad Balance
|
||||||
at.acctIn.Coins = coin.Coins{{"mycoin", 2}}
|
at.acctIn.Coins = coin.Coins{{"mycoin", 2}}
|
||||||
at.initAccount(at.acctIn)
|
at.initAccount(at.acctIn)
|
||||||
res, _, _ := at.exec(t, at.getTx(coin.Coins{{"mycoin", 5}}), true)
|
res, _, _ := at.exec(t, at.getTx(coin.Coins{{"mycoin", 5}}, 1), true)
|
||||||
assert.True(res.IsErr(), "ExecTx/Bad CheckTx: Expected error return from ExecTx, returned: %v", res)
|
assert.True(res.IsErr(), "ExecTx/Bad CheckTx: Expected error return from ExecTx, returned: %v", res)
|
||||||
res, diffIn, diffOut := at.exec(t, at.getTx(coin.Coins{{"mycoin", 5}}), false)
|
res, diffIn, diffOut := at.exec(t, at.getTx(coin.Coins{{"mycoin", 5}}, 1), false)
|
||||||
assert.True(res.IsErr(), "ExecTx/Bad DeliverTx: Expected error return from ExecTx, returned: %v", res)
|
assert.True(res.IsErr(), "ExecTx/Bad DeliverTx: Expected error return from ExecTx, returned: %v", res)
|
||||||
assert.True(diffIn.IsZero())
|
assert.True(diffIn.IsZero())
|
||||||
assert.True(diffOut.IsZero())
|
assert.True(diffOut.IsZero())
|
||||||
|
|
||||||
//Regular CheckTx
|
//Regular CheckTx
|
||||||
at.reset()
|
at.reset()
|
||||||
res, _, _ = at.exec(t, at.getTx(coin.Coins{{"mycoin", 5}}), true)
|
res, _, _ = at.exec(t, at.getTx(coin.Coins{{"mycoin", 5}}, 1), true)
|
||||||
assert.True(res.IsOK(), "ExecTx/Good CheckTx: Expected OK return from ExecTx, Error: %v", res)
|
assert.True(res.IsOK(), "ExecTx/Good CheckTx: Expected OK return from ExecTx, Error: %v", res)
|
||||||
|
|
||||||
//Regular DeliverTx
|
//Regular DeliverTx
|
||||||
at.reset()
|
at.reset()
|
||||||
amt := coin.Coins{{"mycoin", 3}}
|
amt := coin.Coins{{"mycoin", 3}}
|
||||||
res, diffIn, diffOut = at.exec(t, at.getTx(amt), false)
|
res, diffIn, diffOut = at.exec(t, at.getTx(amt, 1), false)
|
||||||
assert.True(res.IsOK(), "ExecTx/Good DeliverTx: Expected OK return from ExecTx, Error: %v", res)
|
assert.True(res.IsOK(), "ExecTx/Good DeliverTx: Expected OK return from ExecTx, Error: %v", res)
|
||||||
assert.Equal(amt.Negative(), diffIn)
|
assert.Equal(amt.Negative(), diffIn)
|
||||||
assert.Equal(amt, diffOut)
|
assert.Equal(amt, diffOut)
|
||||||
|
@ -234,7 +237,7 @@ func TestTx(t *testing.T) {
|
||||||
at.reset()
|
at.reset()
|
||||||
amt = coin.Coins{{"mycoin", 4}}
|
amt = coin.Coins{{"mycoin", 4}}
|
||||||
toll := coin.Coin{"mycoin", 1}
|
toll := coin.Coin{"mycoin", 1}
|
||||||
res, diffIn, diffOut = at.exec(t, at.feeTx(amt, toll), false)
|
res, diffIn, diffOut = at.exec(t, at.feeTx(amt, toll, 1), false)
|
||||||
assert.True(res.IsOK(), "ExecTx/Good DeliverTx: Expected OK return from ExecTx, Error: %v", res)
|
assert.True(res.IsOK(), "ExecTx/Good DeliverTx: Expected OK return from ExecTx, Error: %v", res)
|
||||||
payment := amt.Plus(coin.Coins{toll}).Negative()
|
payment := amt.Plus(coin.Coins{toll}).Negative()
|
||||||
assert.Equal(payment, diffIn)
|
assert.Equal(payment, diffIn)
|
||||||
|
@ -246,7 +249,7 @@ func TestQuery(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
at := newAppTest(t)
|
at := newAppTest(t)
|
||||||
|
|
||||||
res, _, _ := at.exec(t, at.getTx(coin.Coins{{"mycoin", 5}}), false)
|
res, _, _ := at.exec(t, at.getTx(coin.Coins{{"mycoin", 5}}, 1), false)
|
||||||
assert.True(res.IsOK(), "Commit, DeliverTx: Expected OK return from DeliverTx, Error: %v", res)
|
assert.True(res.IsOK(), "Commit, DeliverTx: Expected OK return from DeliverTx, Error: %v", res)
|
||||||
|
|
||||||
resQueryPreCommit := at.app.Query(abci.RequestQuery{
|
resQueryPreCommit := at.app.Query(abci.RequestQuery{
|
||||||
|
|
|
@ -2,21 +2,25 @@ package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
"github.com/tendermint/basecoin"
|
|
||||||
"github.com/tendermint/light-client/commands"
|
"github.com/tendermint/light-client/commands"
|
||||||
txcmd "github.com/tendermint/light-client/commands/txs"
|
txcmd "github.com/tendermint/light-client/commands/txs"
|
||||||
cmn "github.com/tendermint/tmlibs/common"
|
cmn "github.com/tendermint/tmlibs/common"
|
||||||
|
|
||||||
|
ctypes "github.com/tendermint/tendermint/rpc/core/types"
|
||||||
|
|
||||||
|
"github.com/tendermint/basecoin"
|
||||||
"github.com/tendermint/basecoin/modules/auth"
|
"github.com/tendermint/basecoin/modules/auth"
|
||||||
"github.com/tendermint/basecoin/modules/base"
|
"github.com/tendermint/basecoin/modules/base"
|
||||||
"github.com/tendermint/basecoin/modules/coin"
|
"github.com/tendermint/basecoin/modules/coin"
|
||||||
"github.com/tendermint/basecoin/modules/fee"
|
"github.com/tendermint/basecoin/modules/fee"
|
||||||
|
"github.com/tendermint/basecoin/modules/nonce"
|
||||||
)
|
)
|
||||||
|
|
||||||
//-------------------------
|
//-------------------------
|
||||||
|
@ -49,7 +53,7 @@ func init() {
|
||||||
flags.Int(FlagSequence, -1, "Sequence number for this transaction")
|
flags.Int(FlagSequence, -1, "Sequence number for this transaction")
|
||||||
}
|
}
|
||||||
|
|
||||||
// runDemo is an example of how to make a tx
|
// doSendTx is an example of how to make a tx
|
||||||
func doSendTx(cmd *cobra.Command, args []string) error {
|
func doSendTx(cmd *cobra.Command, args []string) error {
|
||||||
// load data from json or flags
|
// load data from json or flags
|
||||||
var tx basecoin.Tx
|
var tx basecoin.Tx
|
||||||
|
@ -69,6 +73,10 @@ func doSendTx(cmd *cobra.Command, args []string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
tx, err = WrapNonceTx(tx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
tx, err = WrapChainTx(tx)
|
tx, err = WrapChainTx(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -82,11 +90,40 @@ func doSendTx(cmd *cobra.Command, args []string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err = ValidateResult(bres); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Output result
|
// Output result
|
||||||
return txcmd.OutputTx(bres)
|
return txcmd.OutputTx(bres)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidateResult returns an appropriate error if the server rejected the
|
||||||
|
// tx in CheckTx or DeliverTx
|
||||||
|
func ValidateResult(res *ctypes.ResultBroadcastTxCommit) error {
|
||||||
|
if res.CheckTx.IsErr() {
|
||||||
|
return fmt.Errorf("CheckTx: (%d): %s", res.CheckTx.Code, res.CheckTx.Log)
|
||||||
|
}
|
||||||
|
if res.DeliverTx.IsErr() {
|
||||||
|
return fmt.Errorf("DeliverTx: (%d): %s", res.DeliverTx.Code, res.DeliverTx.Log)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapNonceTx grabs the sequence number from the flag and wraps
|
||||||
|
// the tx with this nonce. Grabs the permission from the signer,
|
||||||
|
// as we still only support single sig on the cli
|
||||||
|
func WrapNonceTx(tx basecoin.Tx) (res basecoin.Tx, err error) {
|
||||||
|
//add the nonce tx layer to the tx
|
||||||
|
seq := viper.GetInt(FlagSequence)
|
||||||
|
if seq < 0 {
|
||||||
|
return res, fmt.Errorf("sequence must be greater than 0")
|
||||||
|
}
|
||||||
|
signers := []basecoin.Actor{GetSignerAct()}
|
||||||
|
res = nonce.NewTx(uint32(seq), signers, tx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// WrapFeeTx checks for FlagFee and if present wraps the tx with a
|
// WrapFeeTx checks for FlagFee and if present wraps the tx with a
|
||||||
// FeeTx of the given amount, paid by the signer
|
// FeeTx of the given amount, paid by the signer
|
||||||
func WrapFeeTx(tx basecoin.Tx) (res basecoin.Tx, err error) {
|
func WrapFeeTx(tx basecoin.Tx) (res basecoin.Tx, err error) {
|
||||||
|
@ -99,7 +136,8 @@ func WrapFeeTx(tx basecoin.Tx) (res basecoin.Tx, err error) {
|
||||||
if toll.IsZero() {
|
if toll.IsZero() {
|
||||||
return tx, nil
|
return tx, nil
|
||||||
}
|
}
|
||||||
return fee.NewFee(tx, toll, getSignerAddr()), nil
|
res = fee.NewFee(tx, toll, GetSignerAct())
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// WrapChainTx will wrap the tx with a ChainTx from the standard flags
|
// WrapChainTx will wrap the tx with a ChainTx from the standard flags
|
||||||
|
@ -110,10 +148,12 @@ func WrapChainTx(tx basecoin.Tx) (res basecoin.Tx, err error) {
|
||||||
return res, errors.New("No chain-id provided")
|
return res, errors.New("No chain-id provided")
|
||||||
}
|
}
|
||||||
res = base.NewChainTx(chain, uint64(expires), tx)
|
res = base.NewChainTx(chain, uint64(expires), tx)
|
||||||
return res, nil
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSignerAddr() (res basecoin.Actor) {
|
// GetSignerAct returns the address of the signer of the tx
|
||||||
|
// (as we still only support single sig)
|
||||||
|
func GetSignerAct() (res basecoin.Actor) {
|
||||||
// this could be much cooler with multisig...
|
// this could be much cooler with multisig...
|
||||||
signer := txcmd.GetSigner()
|
signer := txcmd.GetSigner()
|
||||||
if !signer.Empty() {
|
if !signer.Empty() {
|
||||||
|
@ -138,7 +178,7 @@ func readSendTxFlags() (tx basecoin.Tx, err error) {
|
||||||
|
|
||||||
// craft the inputs and outputs
|
// craft the inputs and outputs
|
||||||
ins := []coin.TxInput{{
|
ins := []coin.TxInput{{
|
||||||
Address: getSignerAddr(),
|
Address: GetSignerAct(),
|
||||||
Coins: amountCoins,
|
Coins: amountCoins,
|
||||||
}}
|
}}
|
||||||
outs := []coin.TxOutput{{
|
outs := []coin.TxOutput{{
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
|
|
||||||
"github.com/tendermint/basecoin/modules/auth"
|
"github.com/tendermint/basecoin/modules/auth"
|
||||||
"github.com/tendermint/basecoin/modules/coin"
|
"github.com/tendermint/basecoin/modules/coin"
|
||||||
|
"github.com/tendermint/basecoin/modules/nonce"
|
||||||
"github.com/tendermint/basecoin/stack"
|
"github.com/tendermint/basecoin/stack"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -41,6 +42,37 @@ func doAccountQuery(cmd *cobra.Command, args []string) error {
|
||||||
return proofcmd.OutputProof(acc, proof.BlockHeight())
|
return proofcmd.OutputProof(acc, proof.BlockHeight())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NonceQueryCmd - command to query an nonce account
|
||||||
|
var NonceQueryCmd = &cobra.Command{
|
||||||
|
Use: "nonce [address]",
|
||||||
|
Short: "Get details of a nonce sequence number, with proof",
|
||||||
|
RunE: lcmd.RequireInit(doNonceQuery),
|
||||||
|
}
|
||||||
|
|
||||||
|
func doNonceQuery(cmd *cobra.Command, args []string) error {
|
||||||
|
addr, err := proofcmd.ParseHexKey(args, "address")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
act := []basecoin.Actor{basecoin.NewActor(
|
||||||
|
auth.NameSigs,
|
||||||
|
addr,
|
||||||
|
)}
|
||||||
|
|
||||||
|
key := stack.PrefixedKey(nonce.NameNonce, nonce.GetSeqKey(act))
|
||||||
|
|
||||||
|
var seq uint32
|
||||||
|
proof, err := proofcmd.GetAndParseAppProof(key, &seq)
|
||||||
|
if lc.IsNoDataErr(err) {
|
||||||
|
return errors.Errorf("Sequence is empty for address %X ", addr)
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return proofcmd.OutputProof(seq, proof.BlockHeight())
|
||||||
|
}
|
||||||
|
|
||||||
// BaseTxPresenter this decodes all basecoin tx
|
// BaseTxPresenter this decodes all basecoin tx
|
||||||
type BaseTxPresenter struct {
|
type BaseTxPresenter struct {
|
||||||
proofs.RawPresenter // this handles MakeKey as hex bytes
|
proofs.RawPresenter // this handles MakeKey as hex bytes
|
||||||
|
|
|
@ -39,6 +39,7 @@ func main() {
|
||||||
proofs.TxCmd,
|
proofs.TxCmd,
|
||||||
proofs.KeyCmd,
|
proofs.KeyCmd,
|
||||||
bcmd.AccountQueryCmd,
|
bcmd.AccountQueryCmd,
|
||||||
|
bcmd.NonceQueryCmd,
|
||||||
)
|
)
|
||||||
|
|
||||||
// you will always want this for the base send command
|
// you will always want this for the base send command
|
||||||
|
|
41
context.go
41
context.go
|
@ -2,6 +2,8 @@ package basecoin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
|
||||||
wire "github.com/tendermint/go-wire"
|
wire "github.com/tendermint/go-wire"
|
||||||
"github.com/tendermint/go-wire/data"
|
"github.com/tendermint/go-wire/data"
|
||||||
|
@ -19,6 +21,7 @@ type Actor struct {
|
||||||
Address data.Bytes `json:"addr"` // arbitrary app-specific unique id
|
Address data.Bytes `json:"addr"` // arbitrary app-specific unique id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewActor - create a new actor
|
||||||
func NewActor(app string, addr []byte) Actor {
|
func NewActor(app string, addr []byte) Actor {
|
||||||
return Actor{App: app, Address: addr}
|
return Actor{App: app, Address: addr}
|
||||||
}
|
}
|
||||||
|
@ -35,6 +38,11 @@ func (a Actor) Equals(b Actor) bool {
|
||||||
bytes.Equal(a.Address, b.Address)
|
bytes.Equal(a.Address, b.Address)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Empty checks if the actor is not initialized
|
||||||
|
func (a Actor) Empty() bool {
|
||||||
|
return a.ChainID == "" && a.App == "" && len(a.Address) == 0
|
||||||
|
}
|
||||||
|
|
||||||
// Context is an interface, so we can implement "secure" variants that
|
// Context is an interface, so we can implement "secure" variants that
|
||||||
// rely on private fields to control the actions
|
// rely on private fields to control the actions
|
||||||
type Context interface {
|
type Context interface {
|
||||||
|
@ -48,3 +56,36 @@ type Context interface {
|
||||||
ChainID() string
|
ChainID() string
|
||||||
BlockHeight() uint64
|
BlockHeight() uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//////////////////////////////// Sort Interface
|
||||||
|
// USAGE sort.Sort(ByAll(<actor instance>))
|
||||||
|
|
||||||
|
func (a Actor) String() string {
|
||||||
|
return fmt.Sprintf("%x", a.Address)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByAll implements sort.Interface for []Actor.
|
||||||
|
// It sorts be the ChainID, followed by the App, followed by the Address
|
||||||
|
type ByAll []Actor
|
||||||
|
|
||||||
|
// Verify the sort interface at compile time
|
||||||
|
var _ sort.Interface = ByAll{}
|
||||||
|
|
||||||
|
func (a ByAll) Len() int { return len(a) }
|
||||||
|
func (a ByAll) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||||
|
func (a ByAll) Less(i, j int) bool {
|
||||||
|
|
||||||
|
if a[i].ChainID < a[j].ChainID {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if a[i].ChainID > a[j].ChainID {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if a[i].App < a[j].App {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if a[i].App > a[j].App {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return bytes.Compare(a[i].Address, a[j].Address) == -1
|
||||||
|
}
|
||||||
|
|
|
@ -60,6 +60,10 @@ func counterTx(cmd *cobra.Command, args []string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
tx, err = bcmd.WrapNonceTx(tx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
tx, err = bcmd.WrapChainTx(tx)
|
tx, err = bcmd.WrapChainTx(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -72,6 +76,9 @@ func counterTx(cmd *cobra.Command, args []string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err = bcmd.ValidateResult(bres); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Output result
|
// Output result
|
||||||
return txcmd.OutputTx(bres)
|
return txcmd.OutputTx(bres)
|
||||||
|
@ -83,6 +90,6 @@ func readCounterTxFlags() (tx basecoin.Tx, err error) {
|
||||||
return tx, err
|
return tx, err
|
||||||
}
|
}
|
||||||
|
|
||||||
tx = counter.NewTx(viper.GetBool(FlagValid), feeCoins, viper.GetInt(bcmd.FlagSequence))
|
tx = counter.NewTx(viper.GetBool(FlagValid), feeCoins)
|
||||||
return tx, nil
|
return tx, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"github.com/tendermint/basecoin/modules/base"
|
"github.com/tendermint/basecoin/modules/base"
|
||||||
"github.com/tendermint/basecoin/modules/coin"
|
"github.com/tendermint/basecoin/modules/coin"
|
||||||
"github.com/tendermint/basecoin/modules/fee"
|
"github.com/tendermint/basecoin/modules/fee"
|
||||||
|
"github.com/tendermint/basecoin/modules/nonce"
|
||||||
"github.com/tendermint/basecoin/stack"
|
"github.com/tendermint/basecoin/stack"
|
||||||
"github.com/tendermint/basecoin/state"
|
"github.com/tendermint/basecoin/state"
|
||||||
)
|
)
|
||||||
|
@ -34,17 +35,15 @@ func init() {
|
||||||
|
|
||||||
// Tx - struct for all counter transactions
|
// Tx - struct for all counter transactions
|
||||||
type Tx struct {
|
type Tx struct {
|
||||||
Valid bool `json:"valid"`
|
Valid bool `json:"valid"`
|
||||||
Fee coin.Coins `json:"fee"`
|
Fee coin.Coins `json:"fee"`
|
||||||
Sequence int `json:"sequence"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTx - return a new counter transaction struct wrapped as a basecoin transaction
|
// NewTx - return a new counter transaction struct wrapped as a basecoin transaction
|
||||||
func NewTx(valid bool, fee coin.Coins, sequence int) basecoin.Tx {
|
func NewTx(valid bool, fee coin.Coins) basecoin.Tx {
|
||||||
return Tx{
|
return Tx{
|
||||||
Valid: valid,
|
Valid: valid,
|
||||||
Fee: fee,
|
Fee: fee,
|
||||||
Sequence: sequence,
|
|
||||||
}.Wrap()
|
}.Wrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,6 +102,7 @@ func NewHandler(feeDenom string) basecoin.Handler {
|
||||||
stack.Recovery{},
|
stack.Recovery{},
|
||||||
auth.Signatures{},
|
auth.Signatures{},
|
||||||
base.Chain{},
|
base.Chain{},
|
||||||
|
nonce.ReplayCheck{},
|
||||||
fee.NewSimpleFeeMiddleware(coin.Coin{feeDenom, 0}, fee.Bank),
|
fee.NewSimpleFeeMiddleware(coin.Coin{feeDenom, 0}, fee.Bank),
|
||||||
).Use(dispatcher)
|
).Use(dispatcher)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,10 +7,12 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
abci "github.com/tendermint/abci/types"
|
abci "github.com/tendermint/abci/types"
|
||||||
|
"github.com/tendermint/basecoin"
|
||||||
"github.com/tendermint/basecoin/app"
|
"github.com/tendermint/basecoin/app"
|
||||||
"github.com/tendermint/basecoin/modules/auth"
|
"github.com/tendermint/basecoin/modules/auth"
|
||||||
"github.com/tendermint/basecoin/modules/base"
|
"github.com/tendermint/basecoin/modules/base"
|
||||||
"github.com/tendermint/basecoin/modules/coin"
|
"github.com/tendermint/basecoin/modules/coin"
|
||||||
|
"github.com/tendermint/basecoin/modules/nonce"
|
||||||
"github.com/tendermint/go-wire"
|
"github.com/tendermint/go-wire"
|
||||||
eyescli "github.com/tendermint/merkleeyes/client"
|
eyescli "github.com/tendermint/merkleeyes/client"
|
||||||
"github.com/tendermint/tmlibs/log"
|
"github.com/tendermint/tmlibs/log"
|
||||||
|
@ -40,8 +42,9 @@ func TestCounterPlugin(t *testing.T) {
|
||||||
require.Equal(t, "Success", log)
|
require.Equal(t, "Success", log)
|
||||||
|
|
||||||
// Deliver a CounterTx
|
// Deliver a CounterTx
|
||||||
DeliverCounterTx := func(valid bool, counterFee coin.Coins, inputSequence int) abci.Result {
|
DeliverCounterTx := func(valid bool, counterFee coin.Coins, sequence uint32) abci.Result {
|
||||||
tx := NewTx(valid, counterFee, inputSequence)
|
tx := NewTx(valid, counterFee)
|
||||||
|
tx = nonce.NewTx(sequence, []basecoin.Actor{acct.Actor()}, tx)
|
||||||
tx = base.NewChainTx(chainID, 0, tx)
|
tx = base.NewChainTx(chainID, 0, tx)
|
||||||
stx := auth.NewSig(tx)
|
stx := auth.NewSig(tx)
|
||||||
auth.Sign(stx, acct.Key)
|
auth.Sign(stx, acct.Key)
|
||||||
|
@ -49,19 +52,19 @@ func TestCounterPlugin(t *testing.T) {
|
||||||
return bcApp.DeliverTx(txBytes)
|
return bcApp.DeliverTx(txBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test a basic send, no fee (doesn't update sequence as no money spent)
|
// Test a basic send, no fee
|
||||||
res := DeliverCounterTx(true, nil, 1)
|
res := DeliverCounterTx(true, nil, 1)
|
||||||
assert.True(res.IsOK(), res.String())
|
assert.True(res.IsOK(), res.String())
|
||||||
|
|
||||||
// Test an invalid send, no fee
|
// Test an invalid send, no fee
|
||||||
res = DeliverCounterTx(false, nil, 1)
|
res = DeliverCounterTx(false, nil, 2)
|
||||||
assert.True(res.IsErr(), res.String())
|
assert.True(res.IsErr(), res.String())
|
||||||
|
|
||||||
// Test the fee (increments sequence)
|
// Test an invalid send, with supported fee
|
||||||
res = DeliverCounterTx(true, coin.Coins{{"gold", 100}}, 1)
|
res = DeliverCounterTx(true, coin.Coins{{"gold", 100}}, 2)
|
||||||
assert.True(res.IsOK(), res.String())
|
assert.True(res.IsOK(), res.String())
|
||||||
|
|
||||||
// Test unsupported fee
|
// Test unsupported fee
|
||||||
res = DeliverCounterTx(true, coin.Coins{{"silver", 100}}, 2)
|
res = DeliverCounterTx(true, coin.Coins{{"silver", 100}}, 3)
|
||||||
assert.True(res.IsErr(), res.String())
|
assert.True(res.IsErr(), res.String())
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,15 +15,23 @@ var (
|
||||||
errUnauthorized = fmt.Errorf("Unauthorized")
|
errUnauthorized = fmt.Errorf("Unauthorized")
|
||||||
errInvalidSignature = fmt.Errorf("Invalid Signature")
|
errInvalidSignature = fmt.Errorf("Invalid Signature")
|
||||||
errTooLarge = fmt.Errorf("Input size too large")
|
errTooLarge = fmt.Errorf("Input size too large")
|
||||||
|
errNoSigners = fmt.Errorf("There are no signers")
|
||||||
errMissingSignature = fmt.Errorf("Signature missing")
|
errMissingSignature = fmt.Errorf("Signature missing")
|
||||||
errTooManySignatures = fmt.Errorf("Too many signatures")
|
errTooManySignatures = fmt.Errorf("Too many signatures")
|
||||||
errNoChain = fmt.Errorf("No chain id provided")
|
errNoChain = fmt.Errorf("No chain id provided")
|
||||||
|
errTxEmpty = fmt.Errorf("The provided Tx is empty")
|
||||||
errWrongChain = fmt.Errorf("Wrong chain for tx")
|
errWrongChain = fmt.Errorf("Wrong chain for tx")
|
||||||
errUnknownTxType = fmt.Errorf("Tx type unknown")
|
errUnknownTxType = fmt.Errorf("Tx type unknown")
|
||||||
errInvalidFormat = fmt.Errorf("Invalid format")
|
errInvalidFormat = fmt.Errorf("Invalid format")
|
||||||
errUnknownModule = fmt.Errorf("Unknown module")
|
errUnknownModule = fmt.Errorf("Unknown module")
|
||||||
errExpired = fmt.Errorf("Tx expired")
|
errExpired = fmt.Errorf("Tx expired")
|
||||||
errUnknownKey = fmt.Errorf("Unknown key")
|
errUnknownKey = fmt.Errorf("Unknown key")
|
||||||
|
|
||||||
|
internalErr = abci.CodeType_InternalError
|
||||||
|
encodingErr = abci.CodeType_EncodingError
|
||||||
|
unauthorized = abci.CodeType_Unauthorized
|
||||||
|
unknownRequest = abci.CodeType_UnknownRequest
|
||||||
|
unknownAddress = abci.CodeType_BaseUnknownAddress
|
||||||
)
|
)
|
||||||
|
|
||||||
// some crazy reflection to unwrap any generated struct.
|
// some crazy reflection to unwrap any generated struct.
|
||||||
|
@ -71,76 +79,84 @@ func IsUnknownKeyErr(err error) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func ErrInternal(msg string) TMError {
|
func ErrInternal(msg string) TMError {
|
||||||
return New(msg, abci.CodeType_InternalError)
|
return New(msg, internalErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsInternalErr matches any error that is not classified
|
// IsInternalErr matches any error that is not classified
|
||||||
func IsInternalErr(err error) bool {
|
func IsInternalErr(err error) bool {
|
||||||
return HasErrorCode(err, abci.CodeType_InternalError)
|
return HasErrorCode(err, internalErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ErrDecoding() TMError {
|
func ErrDecoding() TMError {
|
||||||
return WithCode(errDecoding, abci.CodeType_EncodingError)
|
return WithCode(errDecoding, encodingErr)
|
||||||
}
|
}
|
||||||
func IsDecodingErr(err error) bool {
|
func IsDecodingErr(err error) bool {
|
||||||
return IsSameError(errDecoding, err)
|
return IsSameError(errDecoding, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ErrUnauthorized() TMError {
|
func ErrUnauthorized() TMError {
|
||||||
return WithCode(errUnauthorized, abci.CodeType_Unauthorized)
|
return WithCode(errUnauthorized, unauthorized)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsUnauthorizedErr is generic helper for any unauthorized errors,
|
// IsUnauthorizedErr is generic helper for any unauthorized errors,
|
||||||
// also specific sub-types
|
// also specific sub-types
|
||||||
func IsUnauthorizedErr(err error) bool {
|
func IsUnauthorizedErr(err error) bool {
|
||||||
return HasErrorCode(err, abci.CodeType_Unauthorized)
|
return HasErrorCode(err, unauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ErrNoSigners() TMError {
|
||||||
|
return WithCode(errNoSigners, unauthorized)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ErrMissingSignature() TMError {
|
func ErrMissingSignature() TMError {
|
||||||
return WithCode(errMissingSignature, abci.CodeType_Unauthorized)
|
return WithCode(errMissingSignature, unauthorized)
|
||||||
}
|
}
|
||||||
func IsMissingSignatureErr(err error) bool {
|
func IsMissingSignatureErr(err error) bool {
|
||||||
return IsSameError(errMissingSignature, err)
|
return IsSameError(errMissingSignature, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ErrTooManySignatures() TMError {
|
func ErrTooManySignatures() TMError {
|
||||||
return WithCode(errTooManySignatures, abci.CodeType_Unauthorized)
|
return WithCode(errTooManySignatures, unauthorized)
|
||||||
}
|
}
|
||||||
func IsTooManySignaturesErr(err error) bool {
|
func IsTooManySignaturesErr(err error) bool {
|
||||||
return IsSameError(errTooManySignatures, err)
|
return IsSameError(errTooManySignatures, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ErrInvalidSignature() TMError {
|
func ErrInvalidSignature() TMError {
|
||||||
return WithCode(errInvalidSignature, abci.CodeType_Unauthorized)
|
return WithCode(errInvalidSignature, unauthorized)
|
||||||
}
|
}
|
||||||
func IsInvalidSignatureErr(err error) bool {
|
func IsInvalidSignatureErr(err error) bool {
|
||||||
return IsSameError(errInvalidSignature, err)
|
return IsSameError(errInvalidSignature, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ErrNoChain() TMError {
|
func ErrNoChain() TMError {
|
||||||
return WithCode(errNoChain, abci.CodeType_Unauthorized)
|
return WithCode(errNoChain, unauthorized)
|
||||||
}
|
}
|
||||||
func IsNoChainErr(err error) bool {
|
func IsNoChainErr(err error) bool {
|
||||||
return IsSameError(errNoChain, err)
|
return IsSameError(errNoChain, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ErrTxEmpty() TMError {
|
||||||
|
return WithCode(errTxEmpty, unauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
func ErrWrongChain(chain string) TMError {
|
func ErrWrongChain(chain string) TMError {
|
||||||
msg := errors.Wrap(errWrongChain, chain)
|
msg := errors.Wrap(errWrongChain, chain)
|
||||||
return WithCode(msg, abci.CodeType_Unauthorized)
|
return WithCode(msg, unauthorized)
|
||||||
}
|
}
|
||||||
func IsWrongChainErr(err error) bool {
|
func IsWrongChainErr(err error) bool {
|
||||||
return IsSameError(errWrongChain, err)
|
return IsSameError(errWrongChain, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ErrTooLarge() TMError {
|
func ErrTooLarge() TMError {
|
||||||
return WithCode(errTooLarge, abci.CodeType_EncodingError)
|
return WithCode(errTooLarge, encodingErr)
|
||||||
}
|
}
|
||||||
func IsTooLargeErr(err error) bool {
|
func IsTooLargeErr(err error) bool {
|
||||||
return IsSameError(errTooLarge, err)
|
return IsSameError(errTooLarge, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ErrExpired() TMError {
|
func ErrExpired() TMError {
|
||||||
return WithCode(errExpired, abci.CodeType_Unauthorized)
|
return WithCode(errExpired, unauthorized)
|
||||||
}
|
}
|
||||||
func IsExpiredErr(err error) bool {
|
func IsExpiredErr(err error) bool {
|
||||||
return IsSameError(errExpired, err)
|
return IsSameError(errExpired, err)
|
||||||
|
|
|
@ -60,13 +60,6 @@ func IsInvalidCoinsErr(err error) bool {
|
||||||
return errors.IsSameError(errInvalidCoins, err)
|
return errors.IsSameError(errInvalidCoins, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ErrInvalidSequence() errors.TMError {
|
|
||||||
return errors.WithCode(errInvalidSequence, invalidInput)
|
|
||||||
}
|
|
||||||
func IsInvalidSequenceErr(err error) bool {
|
|
||||||
return errors.IsSameError(errInvalidSequence, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ErrInsufficientFunds() errors.TMError {
|
func ErrInsufficientFunds() errors.TMError {
|
||||||
return errors.WithCode(errInsufficientFunds, invalidInput)
|
return errors.WithCode(errInsufficientFunds, invalidInput)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
//nolint
|
||||||
|
package nonce
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
abci "github.com/tendermint/abci/types"
|
||||||
|
|
||||||
|
"github.com/tendermint/basecoin/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errNoNonce = fmt.Errorf("Tx doesn't contain nonce")
|
||||||
|
errNotMember = fmt.Errorf("nonce contains non-permissioned member")
|
||||||
|
errZeroSequence = fmt.Errorf("Sequence number cannot be zero")
|
||||||
|
|
||||||
|
unauthorized = abci.CodeType_Unauthorized
|
||||||
|
)
|
||||||
|
|
||||||
|
func ErrBadNonce(got, expected uint32) errors.TMError {
|
||||||
|
return errors.WithCode(fmt.Errorf("Bad nonce sequence, got %d, expected %d", got, expected), unauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ErrNoNonce() errors.TMError {
|
||||||
|
return errors.WithCode(errNoNonce, unauthorized)
|
||||||
|
}
|
||||||
|
func ErrNotMember() errors.TMError {
|
||||||
|
return errors.WithCode(errNotMember, unauthorized)
|
||||||
|
}
|
||||||
|
func ErrZeroSequence() errors.TMError {
|
||||||
|
return errors.WithCode(errZeroSequence, unauthorized)
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
package nonce
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/tendermint/basecoin"
|
||||||
|
"github.com/tendermint/basecoin/stack"
|
||||||
|
"github.com/tendermint/basecoin/state"
|
||||||
|
)
|
||||||
|
|
||||||
|
//nolint
|
||||||
|
const (
|
||||||
|
NameNonce = "nonce"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReplayCheck uses the sequence to check for replay attacks
|
||||||
|
type ReplayCheck struct {
|
||||||
|
stack.PassOption
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name of the module - fulfills Middleware interface
|
||||||
|
func (ReplayCheck) Name() string {
|
||||||
|
return NameNonce
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ stack.Middleware = ReplayCheck{}
|
||||||
|
|
||||||
|
// CheckTx verifies tx is not being replayed - fulfills Middlware interface
|
||||||
|
func (r ReplayCheck) CheckTx(ctx basecoin.Context, store state.KVStore,
|
||||||
|
tx basecoin.Tx, next basecoin.Checker) (res basecoin.Result, err error) {
|
||||||
|
|
||||||
|
stx, err := r.checkIncrementNonceTx(ctx, store, tx)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return next.CheckTx(ctx, store, stx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeliverTx verifies tx is not being replayed - fulfills Middlware interface
|
||||||
|
// NOTE It is okay to modify the sequence before running the wrapped TX because if the
|
||||||
|
// wrapped Tx fails, the state changes are not applied
|
||||||
|
func (r ReplayCheck) DeliverTx(ctx basecoin.Context, store state.KVStore,
|
||||||
|
tx basecoin.Tx, next basecoin.Deliver) (res basecoin.Result, err error) {
|
||||||
|
|
||||||
|
stx, err := r.checkIncrementNonceTx(ctx, store, tx)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return next.DeliverTx(ctx, store, stx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkNonceTx varifies the nonce sequence, an increment sequence number
|
||||||
|
func (r ReplayCheck) checkIncrementNonceTx(ctx basecoin.Context, store state.KVStore,
|
||||||
|
tx basecoin.Tx) (basecoin.Tx, error) {
|
||||||
|
|
||||||
|
// make sure it is a the nonce Tx (Tx from this package)
|
||||||
|
nonceTx, ok := tx.Unwrap().(Tx)
|
||||||
|
if !ok {
|
||||||
|
return tx, ErrNoNonce()
|
||||||
|
}
|
||||||
|
|
||||||
|
err := nonceTx.ValidateBasic()
|
||||||
|
if err != nil {
|
||||||
|
return tx, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// check the nonce sequence number
|
||||||
|
err = nonceTx.CheckIncrementSeq(ctx, store)
|
||||||
|
if err != nil {
|
||||||
|
return tx, err
|
||||||
|
}
|
||||||
|
return nonceTx.Tx, nil
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
package nonce
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
wire "github.com/tendermint/go-wire"
|
||||||
|
|
||||||
|
"github.com/tendermint/basecoin/errors"
|
||||||
|
"github.com/tendermint/basecoin/state"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getSeq(store state.KVStore, key []byte) (seq uint32, err error) {
|
||||||
|
data := store.Get(key)
|
||||||
|
if len(data) == 0 {
|
||||||
|
//if the key is not stored, its a new key with a sequence of zero!
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
err = wire.ReadBinaryBytes(data, &seq)
|
||||||
|
if err != nil {
|
||||||
|
msg := fmt.Sprintf("Error reading sequence for %X", key)
|
||||||
|
return seq, errors.ErrInternal(msg)
|
||||||
|
}
|
||||||
|
return seq, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setSeq(store state.KVStore, key []byte, seq uint32) error {
|
||||||
|
bin := wire.BinaryBytes(seq)
|
||||||
|
store.Set(key, bin)
|
||||||
|
return nil // real stores can return error...
|
||||||
|
}
|
|
@ -0,0 +1,112 @@
|
||||||
|
/*
|
||||||
|
Package nonce - This module allows replay protection to be added to process stack.
|
||||||
|
This is achieved through the use of a sequence number for each unique set of signers.
|
||||||
|
Note that the sequence number for the single signing account "foo" will be unique
|
||||||
|
from the sequence number for a multi-sig account {"foo", "bar"} which would also be
|
||||||
|
unique from a different multi-sig account {"foo", "soup"}
|
||||||
|
*/
|
||||||
|
package nonce
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/tendermint/basecoin"
|
||||||
|
"github.com/tendermint/basecoin/errors"
|
||||||
|
"github.com/tendermint/basecoin/state"
|
||||||
|
)
|
||||||
|
|
||||||
|
// nolint
|
||||||
|
const (
|
||||||
|
ByteNonce = 0x69 //TODO overhaul byte assign system don't make no sense!
|
||||||
|
TypeNonce = "nonce"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
basecoin.TxMapper.RegisterImplementation(Tx{}, TypeNonce, ByteNonce)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tx - Nonce transaction structure, contains list of signers and current sequence number
|
||||||
|
type Tx struct {
|
||||||
|
Sequence uint32 `json:"sequence"`
|
||||||
|
Signers []basecoin.Actor `json:"signers"`
|
||||||
|
Tx basecoin.Tx `json:"tx"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ basecoin.TxInner = &Tx{}
|
||||||
|
|
||||||
|
// NewTx wraps the tx with a signable nonce
|
||||||
|
func NewTx(sequence uint32, signers []basecoin.Actor, tx basecoin.Tx) basecoin.Tx {
|
||||||
|
return (Tx{
|
||||||
|
Sequence: sequence,
|
||||||
|
Signers: signers,
|
||||||
|
Tx: tx,
|
||||||
|
}).Wrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint
|
||||||
|
func (n Tx) Wrap() basecoin.Tx {
|
||||||
|
return basecoin.Tx{n}
|
||||||
|
}
|
||||||
|
func (n Tx) ValidateBasic() error {
|
||||||
|
switch {
|
||||||
|
case n.Tx.Empty():
|
||||||
|
return errors.ErrTxEmpty()
|
||||||
|
case n.Sequence == 0:
|
||||||
|
return ErrZeroSequence()
|
||||||
|
case len(n.Signers) == 0:
|
||||||
|
return errors.ErrNoSigners()
|
||||||
|
}
|
||||||
|
return n.Tx.ValidateBasic()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckIncrementSeq - Check that the sequence number is one more than the state sequence number
|
||||||
|
// and further increment the sequence number
|
||||||
|
// NOTE It is okay to modify the sequence before running the wrapped TX because if the
|
||||||
|
// wrapped Tx fails, the state changes are not applied
|
||||||
|
func (n Tx) CheckIncrementSeq(ctx basecoin.Context, store state.KVStore) error {
|
||||||
|
|
||||||
|
seqKey := n.getSeqKey()
|
||||||
|
|
||||||
|
// check the current state
|
||||||
|
cur, err := getSeq(store, seqKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if n.Sequence != cur+1 {
|
||||||
|
return ErrBadNonce(n.Sequence, cur+1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure they all signed
|
||||||
|
for _, s := range n.Signers {
|
||||||
|
if !ctx.HasPermission(s) {
|
||||||
|
return ErrNotMember()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// increment the sequence by 1
|
||||||
|
err = setSeq(store, seqKey, cur+1)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n Tx) getSeqKey() (seqKey []byte) {
|
||||||
|
return GetSeqKey(n.Signers)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSeqKey - Generate the sequence key as the concatenated list of signers, sorted by address.
|
||||||
|
func GetSeqKey(signers []basecoin.Actor) (seqKey []byte) {
|
||||||
|
|
||||||
|
// First copy the list of signers to sort as sort is done in place
|
||||||
|
signers2sort := make([]basecoin.Actor, len(signers))
|
||||||
|
copy(signers2sort, signers)
|
||||||
|
sort.Sort(basecoin.ByAll(signers))
|
||||||
|
|
||||||
|
for _, signer := range signers {
|
||||||
|
seqKey = append(seqKey, signer.Bytes()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,119 @@
|
||||||
|
package nonce
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/tendermint/basecoin"
|
||||||
|
"github.com/tendermint/basecoin/stack"
|
||||||
|
"github.com/tendermint/basecoin/state"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNonce(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
// generic args here...
|
||||||
|
chainID := "my-chain"
|
||||||
|
chain2ID := "woohoo"
|
||||||
|
|
||||||
|
height := uint64(100)
|
||||||
|
ctx := stack.MockContext(chainID, height)
|
||||||
|
store := state.NewMemKVStore()
|
||||||
|
|
||||||
|
appName1 := "fooz"
|
||||||
|
appName2 := "foot"
|
||||||
|
|
||||||
|
//root actors for the tests
|
||||||
|
act1 := basecoin.Actor{ChainID: chainID, App: appName1, Address: []byte{1, 2, 3, 4}}
|
||||||
|
act2 := basecoin.Actor{ChainID: chainID, App: appName1, Address: []byte{1, 1, 1, 1}}
|
||||||
|
act3 := basecoin.Actor{ChainID: chainID, App: appName1, Address: []byte{3, 3, 3, 3}}
|
||||||
|
act1DiffChain := basecoin.Actor{ChainID: chain2ID, App: appName1, Address: []byte{1, 2, 3, 4}}
|
||||||
|
act2DiffChain := basecoin.Actor{ChainID: chain2ID, App: appName1, Address: []byte{1, 1, 1, 1}}
|
||||||
|
act3DiffChain := basecoin.Actor{ChainID: chain2ID, App: appName1, Address: []byte{3, 3, 3, 3}}
|
||||||
|
act1DiffApp := basecoin.Actor{ChainID: chainID, App: appName2, Address: []byte{1, 2, 3, 4}}
|
||||||
|
act2DiffApp := basecoin.Actor{ChainID: chainID, App: appName2, Address: []byte{1, 1, 1, 1}}
|
||||||
|
act3DiffApp := basecoin.Actor{ChainID: chainID, App: appName2, Address: []byte{3, 3, 3, 3}}
|
||||||
|
|
||||||
|
// let's construct some tests to make the table a bit less verbose
|
||||||
|
set0 := []basecoin.Actor{}
|
||||||
|
set1 := []basecoin.Actor{act1}
|
||||||
|
set2 := []basecoin.Actor{act2}
|
||||||
|
set12 := []basecoin.Actor{act1, act2}
|
||||||
|
set21 := []basecoin.Actor{act2, act1}
|
||||||
|
set123 := []basecoin.Actor{act1, act2, act3}
|
||||||
|
set321 := []basecoin.Actor{act3, act2, act1}
|
||||||
|
|
||||||
|
//some more test cases for different chains and apps for each actor
|
||||||
|
set123Chain2 := []basecoin.Actor{act1DiffChain, act2DiffChain, act3DiffChain}
|
||||||
|
set123App2 := []basecoin.Actor{act1DiffApp, act2DiffApp, act3DiffApp}
|
||||||
|
set123MixedChains := []basecoin.Actor{act1, act2DiffChain, act3}
|
||||||
|
set123MixedApps := []basecoin.Actor{act1, act2DiffApp, act3}
|
||||||
|
|
||||||
|
testList := []struct {
|
||||||
|
valid bool
|
||||||
|
seq uint32
|
||||||
|
actors []basecoin.Actor
|
||||||
|
signers []basecoin.Actor
|
||||||
|
}{
|
||||||
|
// one signer
|
||||||
|
{false, 0, set1, set1}, // seq 0 is no good
|
||||||
|
{false, 1, set1, set0}, // sig is required
|
||||||
|
{true, 1, set1, set1}, // sig and seq are good
|
||||||
|
{true, 2, set1, set1}, // increments each time
|
||||||
|
{false, 777, set1, set1}, // seq is too high
|
||||||
|
|
||||||
|
// independent from second signer
|
||||||
|
{false, 1, set2, set1}, // sig must match
|
||||||
|
{true, 1, set2, set2}, // seq of set2 independent from set1
|
||||||
|
{true, 2, set2, set321}, // extra sigs don't change the situation
|
||||||
|
|
||||||
|
// multisig has same requirements
|
||||||
|
{false, 0, set12, set12}, // need valid sequence number
|
||||||
|
{false, 1, set12, set2}, // they all must sign
|
||||||
|
{true, 1, set12, set12}, // this is proper, independent of act1 and act2
|
||||||
|
{true, 2, set21, set21}, // order of actors doesn't matter
|
||||||
|
{false, 2, set12, set12}, // but can't repeat sequence
|
||||||
|
{true, 3, set12, set321}, // no effect from extra sigs
|
||||||
|
|
||||||
|
// triple sigs also work
|
||||||
|
{false, 2, set123, set123}, // must start with seq=1
|
||||||
|
{false, 1, set123, set12}, // all must sign
|
||||||
|
{true, 1, set123, set321}, // this works
|
||||||
|
{true, 2, set321, set321}, // other order is the same
|
||||||
|
{false, 2, set321, set321}, // no repetition
|
||||||
|
|
||||||
|
// signers with different chain-IDs and apps from actors
|
||||||
|
{false, 3, set123, set123Chain2}, // sign with different chain actors
|
||||||
|
{false, 3, set123, set123App2}, // sign with different app actors
|
||||||
|
{false, 3, set123, set123MixedChains}, // sign with mixed chain actor
|
||||||
|
{false, 3, set123, set123MixedApps}, // sign with mixed app actors
|
||||||
|
|
||||||
|
// signers from different chain-IDs and apps, working
|
||||||
|
{true, 1, set123Chain2, set123Chain2},
|
||||||
|
{true, 1, set123App2, set123App2},
|
||||||
|
{true, 1, set123MixedChains, set123MixedChains},
|
||||||
|
{true, 1, set123MixedApps, set123MixedApps},
|
||||||
|
}
|
||||||
|
|
||||||
|
raw := stack.NewRawTx([]byte{42})
|
||||||
|
for i, test := range testList {
|
||||||
|
|
||||||
|
// set the permissions
|
||||||
|
myCtx := ctx.WithPermissions(test.signers...)
|
||||||
|
|
||||||
|
tx := NewTx(test.seq, test.actors, raw)
|
||||||
|
nonceTx, ok := tx.Unwrap().(Tx)
|
||||||
|
require.True(ok)
|
||||||
|
|
||||||
|
err := nonceTx.CheckIncrementSeq(myCtx, store)
|
||||||
|
if test.valid {
|
||||||
|
assert.Nil(err, "%d: %+v", i, err)
|
||||||
|
} else {
|
||||||
|
assert.NotNil(err, "%d", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -64,6 +64,21 @@ test02SendTxWithFee() {
|
||||||
|
|
||||||
# Make sure tx is indexed
|
# Make sure tx is indexed
|
||||||
checkSendFeeTx $HASH $TX_HEIGHT $SENDER "90" "10"
|
checkSendFeeTx $HASH $TX_HEIGHT $SENDER "90" "10"
|
||||||
|
|
||||||
|
# assert replay protection
|
||||||
|
TX=$(echo qwertyuiop | ${CLIENT_EXE} tx send --amount=90mycoin --fee=10mycoin --sequence=2 --to=$RECV --name=$RICH 2>/dev/null)
|
||||||
|
assertFalse "replay: $TX" $?
|
||||||
|
checkAccount $SENDER "9007199254739900"
|
||||||
|
checkAccount $RECV "1082"
|
||||||
|
|
||||||
|
# make sure we can query the proper nonce
|
||||||
|
NONCE=$(${CLIENT_EXE} query nonce $SENDER)
|
||||||
|
if [ -n "$DEBUG" ]; then echo $NONCE; echo; fi
|
||||||
|
# TODO: note that cobra returns error code 0 on parse failure,
|
||||||
|
# so currently this check passes even if there is no nonce query command
|
||||||
|
if assertTrue "no nonce query" $?; then
|
||||||
|
assertEquals "line=${LINENO}, proper nonce" "2" $(echo $NONCE | jq .data)
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,7 @@ quickTearDown() {
|
||||||
prepareClient() {
|
prepareClient() {
|
||||||
echo "Preparing client keys..."
|
echo "Preparing client keys..."
|
||||||
${CLIENT_EXE} reset_all
|
${CLIENT_EXE} reset_all
|
||||||
assertTrue $?
|
assertTrue "line=${LINENO}, prepare client" $?
|
||||||
|
|
||||||
for i in "${!ACCOUNTS[@]}"; do
|
for i in "${!ACCOUNTS[@]}"; do
|
||||||
newKey ${ACCOUNTS[$i]}
|
newKey ${ACCOUNTS[$i]}
|
||||||
|
@ -60,7 +60,7 @@ prepareClient() {
|
||||||
initServer() {
|
initServer() {
|
||||||
echo "Setting up genesis..."
|
echo "Setting up genesis..."
|
||||||
SERVE_DIR=$1/server
|
SERVE_DIR=$1/server
|
||||||
assertNotNull "no chain" $2
|
assertNotNull "line=${LINENO}, no chain" $2
|
||||||
CHAIN=$2
|
CHAIN=$2
|
||||||
SERVER_LOG=$1/${SERVER_EXE}.log
|
SERVER_LOG=$1/${SERVER_EXE}.log
|
||||||
|
|
||||||
|
@ -100,26 +100,26 @@ initClient() {
|
||||||
PORT=${2:-46657}
|
PORT=${2:-46657}
|
||||||
# hard-code the expected validator hash
|
# hard-code the expected validator hash
|
||||||
${CLIENT_EXE} init --chain-id=$1 --node=tcp://localhost:${PORT} --valhash=EB168E17E45BAEB194D4C79067FFECF345C64DE6
|
${CLIENT_EXE} init --chain-id=$1 --node=tcp://localhost:${PORT} --valhash=EB168E17E45BAEB194D4C79067FFECF345C64DE6
|
||||||
assertTrue "initialized light-client" $?
|
assertTrue "line=${LINENO}, initialized light-client" $?
|
||||||
}
|
}
|
||||||
|
|
||||||
# XXX Ex Usage1: newKey $NAME
|
# XXX Ex Usage1: newKey $NAME
|
||||||
# XXX Ex Usage2: newKey $NAME $PASSWORD
|
# XXX Ex Usage2: newKey $NAME $PASSWORD
|
||||||
# Desc: Generates key for given username and password
|
# Desc: Generates key for given username and password
|
||||||
newKey(){
|
newKey(){
|
||||||
assertNotNull "keyname required" "$1"
|
assertNotNull "line=${LINENO}, keyname required" "$1"
|
||||||
KEYPASS=${2:-qwertyuiop}
|
KEYPASS=${2:-qwertyuiop}
|
||||||
(echo $KEYPASS; echo $KEYPASS) | ${CLIENT_EXE} keys new $1 >/dev/null 2>/dev/null
|
(echo $KEYPASS; echo $KEYPASS) | ${CLIENT_EXE} keys new $1 >/dev/null 2>/dev/null
|
||||||
assertTrue "created $1" $?
|
assertTrue "line=${LINENO}, created $1" $?
|
||||||
assertTrue "$1 doesn't exist" "${CLIENT_EXE} keys get $1"
|
assertTrue "line=${LINENO}, $1 doesn't exist" "${CLIENT_EXE} keys get $1"
|
||||||
}
|
}
|
||||||
|
|
||||||
# XXX Ex Usage: getAddr $NAME
|
# XXX Ex Usage: getAddr $NAME
|
||||||
# Desc: Gets the address for a key name
|
# Desc: Gets the address for a key name
|
||||||
getAddr() {
|
getAddr() {
|
||||||
assertNotNull "keyname required" "$1"
|
assertNotNull "line=${LINENO}, keyname required" "$1"
|
||||||
RAW=$(${CLIENT_EXE} keys get $1)
|
RAW=$(${CLIENT_EXE} keys get $1)
|
||||||
assertTrue "no key for $1" $?
|
assertTrue "line=${LINENO}, no key for $1" $?
|
||||||
# print the addr
|
# print the addr
|
||||||
echo $RAW | cut -d' ' -f2
|
echo $RAW | cut -d' ' -f2
|
||||||
}
|
}
|
||||||
|
@ -129,12 +129,12 @@ getAddr() {
|
||||||
checkAccount() {
|
checkAccount() {
|
||||||
# make sure sender goes down
|
# make sure sender goes down
|
||||||
ACCT=$(${CLIENT_EXE} query account $1)
|
ACCT=$(${CLIENT_EXE} query account $1)
|
||||||
if ! assertTrue "account must exist" $?; then
|
if ! assertTrue "line=${LINENO}, account must exist" $?; then
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -n "$DEBUG" ]; then echo $ACCT; echo; fi
|
if [ -n "$DEBUG" ]; then echo $ACCT; echo; fi
|
||||||
assertEquals "proper money" "$2" $(echo $ACCT | jq .data.coins[0].amount)
|
assertEquals "line=${LINENO}, proper money" "$2" $(echo $ACCT | jq .data.coins[0].amount)
|
||||||
return $?
|
return $?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,8 +143,8 @@ checkAccount() {
|
||||||
txSucceeded() {
|
txSucceeded() {
|
||||||
if (assertTrue "sent tx ($3): $2" $1); then
|
if (assertTrue "sent tx ($3): $2" $1); then
|
||||||
TX=$2
|
TX=$2
|
||||||
assertEquals "good check ($3): $TX" "0" $(echo $TX | jq .check_tx.code)
|
assertEquals "line=${LINENO}, good check ($3): $TX" "0" $(echo $TX | jq .check_tx.code)
|
||||||
assertEquals "good deliver ($3): $TX" "0" $(echo $TX | jq .deliver_tx.code)
|
assertEquals "line=${LINENO}, good deliver ($3): $TX" "0" $(echo $TX | jq .deliver_tx.code)
|
||||||
else
|
else
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
@ -155,17 +155,19 @@ txSucceeded() {
|
||||||
# and that the first input was from this sender for this amount
|
# and that the first input was from this sender for this amount
|
||||||
checkSendTx() {
|
checkSendTx() {
|
||||||
TX=$(${CLIENT_EXE} query tx $1)
|
TX=$(${CLIENT_EXE} query tx $1)
|
||||||
assertTrue "found tx" $?
|
assertTrue "line=${LINENO}, found tx" $?
|
||||||
if [ -n "$DEBUG" ]; then echo $TX; echo; fi
|
if [ -n "$DEBUG" ]; then echo $TX; echo; fi
|
||||||
|
|
||||||
assertEquals "proper height" $2 $(echo $TX | jq .height)
|
assertEquals "line=${LINENO}, proper height" $2 $(echo $TX | jq .height)
|
||||||
assertEquals "type=sigs/one" '"sigs/one"' $(echo $TX | jq .data.type)
|
assertEquals "line=${LINENO}, type=sigs/one" '"sigs/one"' $(echo $TX | jq .data.type)
|
||||||
CTX=$(echo $TX | jq .data.data.tx)
|
CTX=$(echo $TX | jq .data.data.tx)
|
||||||
assertEquals "type=chain/tx" '"chain/tx"' $(echo $CTX | jq .type)
|
assertEquals "line=${LINENO}, type=chain/tx" '"chain/tx"' $(echo $CTX | jq .type)
|
||||||
STX=$(echo $CTX | jq .data.tx)
|
NTX=$(echo $CTX | jq .data.tx)
|
||||||
assertEquals "type=coin/send" '"coin/send"' $(echo $STX | jq .type)
|
assertEquals "line=${LINENO}, type=nonce" '"nonce"' $(echo $NTX | jq .type)
|
||||||
assertEquals "proper sender" "\"$3\"" $(echo $STX | jq .data.inputs[0].address.addr)
|
STX=$(echo $NTX | jq .data.tx)
|
||||||
assertEquals "proper out amount" "$4" $(echo $STX | jq .data.outputs[0].coins[0].amount)
|
assertEquals "line=${LINENO}, type=coin/send" '"coin/send"' $(echo $STX | jq .type)
|
||||||
|
assertEquals "line=${LINENO}, proper sender" "\"$3\"" $(echo $STX | jq .data.inputs[0].address.addr)
|
||||||
|
assertEquals "line=${LINENO}, proper out amount" "$4" $(echo $STX | jq .data.outputs[0].coins[0].amount)
|
||||||
return $?
|
return $?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,17 +180,19 @@ checkSendFeeTx() {
|
||||||
assertTrue "found tx" $?
|
assertTrue "found tx" $?
|
||||||
if [ -n "$DEBUG" ]; then echo $TX; echo; fi
|
if [ -n "$DEBUG" ]; then echo $TX; echo; fi
|
||||||
|
|
||||||
assertEquals "proper height" $2 $(echo $TX | jq .height)
|
assertEquals "line=${LINENO}, proper height" $2 $(echo $TX | jq .height)
|
||||||
assertEquals "type=sigs/one" '"sigs/one"' $(echo $TX | jq .data.type)
|
assertEquals "line=${LINENO}, type=sigs/one" '"sigs/one"' $(echo $TX | jq .data.type)
|
||||||
CTX=$(echo $TX | jq .data.data.tx)
|
CTX=$(echo $TX | jq .data.data.tx)
|
||||||
assertEquals "type=chain/tx" '"chain/tx"' $(echo $CTX | jq .type)
|
assertEquals "line=${LINENO}, type=chain/tx" '"chain/tx"' $(echo $CTX | jq .type)
|
||||||
FTX=$(echo $CTX | jq .data.tx)
|
NTX=$(echo $CTX | jq .data.tx)
|
||||||
assertEquals "type=fee/tx" '"fee/tx"' $(echo $FTX | jq .type)
|
assertEquals "line=${LINENO}, type=nonce" '"nonce"' $(echo $NTX | jq .type)
|
||||||
assertEquals "proper fee" "$5" $(echo $FTX | jq .data.fee.amount)
|
FTX=$(echo $NTX | jq .data.tx)
|
||||||
|
assertEquals "line=${LINENO}, type=fee/tx" '"fee/tx"' $(echo $FTX | jq .type)
|
||||||
|
assertEquals "line=${LINENO}, proper fee" "$5" $(echo $FTX | jq .data.fee.amount)
|
||||||
STX=$(echo $FTX | jq .data.tx)
|
STX=$(echo $FTX | jq .data.tx)
|
||||||
assertEquals "type=coin/send" '"coin/send"' $(echo $STX | jq .type)
|
assertEquals "line=${LINENO}, type=coin/send" '"coin/send"' $(echo $STX | jq .type)
|
||||||
assertEquals "proper sender" "\"$3\"" $(echo $STX | jq .data.inputs[0].address.addr)
|
assertEquals "line=${LINENO}, proper sender" "\"$3\"" $(echo $STX | jq .data.inputs[0].address.addr)
|
||||||
assertEquals "proper out amount" "$4" $(echo $STX | jq .data.outputs[0].coins[0].amount)
|
assertEquals "line=${LINENO}, proper out amount" "$4" $(echo $STX | jq .data.outputs[0].coins[0].amount)
|
||||||
return $?
|
return $?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,21 +21,21 @@ test00GetAccount() {
|
||||||
SENDER=$(getAddr $RICH)
|
SENDER=$(getAddr $RICH)
|
||||||
RECV=$(getAddr $POOR)
|
RECV=$(getAddr $POOR)
|
||||||
|
|
||||||
assertFalse "requires arg" "${CLIENT_EXE} query account"
|
assertFalse "Line=${LINENO}, requires arg" "${CLIENT_EXE} query account"
|
||||||
|
|
||||||
checkAccount $SENDER "9007199254740992"
|
checkAccount $SENDER "9007199254740992"
|
||||||
|
|
||||||
ACCT2=$(${CLIENT_EXE} query account $RECV 2>/dev/null)
|
ACCT2=$(${CLIENT_EXE} query account $RECV 2>/dev/null)
|
||||||
assertFalse "has no genesis account" $?
|
assertFalse "Line=${LINENO}, has no genesis account" $?
|
||||||
}
|
}
|
||||||
|
|
||||||
test01SendTx() {
|
test01SendTx() {
|
||||||
SENDER=$(getAddr $RICH)
|
SENDER=$(getAddr $RICH)
|
||||||
RECV=$(getAddr $POOR)
|
RECV=$(getAddr $POOR)
|
||||||
|
|
||||||
assertFalse "missing dest" "${CLIENT_EXE} tx send --amount=992mycoin --sequence=1 2>/dev/null"
|
assertFalse "Line=${LINENO}, missing dest" "${CLIENT_EXE} tx send --amount=992mycoin --sequence=1 2>/dev/null"
|
||||||
assertFalse "bad password" "echo foo | ${CLIENT_EXE} tx send --amount=992mycoin --sequence=1 --to=$RECV --name=$RICH 2>/dev/null"
|
assertFalse "Line=${LINENO}, bad password" "echo foo | ${CLIENT_EXE} tx send --amount=992mycoin --sequence=1 --to=$RECV --name=$RICH 2>/dev/null"
|
||||||
TX=$(echo qwertyuiop | ${CLIENT_EXE} tx send --amount=992mycoin --sequence=1 --to=$RECV --name=$RICH 2>/dev/null)
|
TX=$(echo qwertyuiop | ${CLIENT_EXE} tx send --amount=992mycoin --sequence=1 --to=$RECV --name=$RICH)
|
||||||
txSucceeded $? "$TX" "$RECV"
|
txSucceeded $? "$TX" "$RECV"
|
||||||
HASH=$(echo $TX | jq .hash | tr -d \")
|
HASH=$(echo $TX | jq .hash | tr -d \")
|
||||||
TX_HEIGHT=$(echo $TX | jq .height)
|
TX_HEIGHT=$(echo $TX | jq .height)
|
||||||
|
@ -49,7 +49,7 @@ test01SendTx() {
|
||||||
|
|
||||||
test02GetCounter() {
|
test02GetCounter() {
|
||||||
COUNT=$(${CLIENT_EXE} query counter 2>/dev/null)
|
COUNT=$(${CLIENT_EXE} query counter 2>/dev/null)
|
||||||
assertFalse "no default count" $?
|
assertFalse "Line=${LINENO}, no default count" $?
|
||||||
}
|
}
|
||||||
|
|
||||||
# checkCounter $COUNT $BALANCE
|
# checkCounter $COUNT $BALANCE
|
||||||
|
@ -57,15 +57,15 @@ test02GetCounter() {
|
||||||
checkCounter() {
|
checkCounter() {
|
||||||
# make sure sender goes down
|
# make sure sender goes down
|
||||||
ACCT=$(${CLIENT_EXE} query counter)
|
ACCT=$(${CLIENT_EXE} query counter)
|
||||||
if assertTrue "count is set" $?; then
|
if assertTrue "Line=${LINENO}, count is set" $?; then
|
||||||
assertEquals "proper count" "$1" $(echo $ACCT | jq .data.counter)
|
assertEquals "Line=${LINENO}, proper count" "$1" $(echo $ACCT | jq .data.counter)
|
||||||
assertEquals "proper money" "$2" $(echo $ACCT | jq .data.total_fees[0].amount)
|
assertEquals "Line=${LINENO}, proper money" "$2" $(echo $ACCT | jq .data.total_fees[0].amount)
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
test03AddCount() {
|
test03AddCount() {
|
||||||
SENDER=$(getAddr $RICH)
|
SENDER=$(getAddr $RICH)
|
||||||
assertFalse "bad password" "echo hi | ${CLIENT_EXE} tx counter --countfee=100mycoin --sequence=2 --name=${RICH} 2>/dev/null"
|
assertFalse "Line=${LINENO}, bad password" "echo hi | ${CLIENT_EXE} tx counter --countfee=100mycoin --sequence=2 --name=${RICH} 2>/dev/null"
|
||||||
|
|
||||||
TX=$(echo qwertyuiop | ${CLIENT_EXE} tx counter --countfee=10mycoin --sequence=2 --name=${RICH} --valid)
|
TX=$(echo qwertyuiop | ${CLIENT_EXE} tx counter --countfee=10mycoin --sequence=2 --name=${RICH} --valid)
|
||||||
txSucceeded $? "$TX" "counter"
|
txSucceeded $? "$TX" "counter"
|
||||||
|
@ -80,14 +80,16 @@ test03AddCount() {
|
||||||
|
|
||||||
# make sure tx is indexed
|
# make sure tx is indexed
|
||||||
TX=$(${CLIENT_EXE} query tx $HASH --trace)
|
TX=$(${CLIENT_EXE} query tx $HASH --trace)
|
||||||
if assertTrue "found tx" $?; then
|
if assertTrue "Line=${LINENO}, found tx" $?; then
|
||||||
assertEquals "proper height" $TX_HEIGHT $(echo $TX | jq .height)
|
assertEquals "Line=${LINENO}, proper height" $TX_HEIGHT $(echo $TX | jq .height)
|
||||||
assertEquals "type=sigs/one" '"sigs/one"' $(echo $TX | jq .data.type)
|
assertEquals "Line=${LINENO}, type=sigs/one" '"sigs/one"' $(echo $TX | jq .data.type)
|
||||||
CTX=$(echo $TX | jq .data.data.tx)
|
CTX=$(echo $TX | jq .data.data.tx)
|
||||||
assertEquals "type=chain/tx" '"chain/tx"' $(echo $CTX | jq .type)
|
assertEquals "Line=${LINENO}, type=chain/tx" '"chain/tx"' $(echo $CTX | jq .type)
|
||||||
CNTX=$(echo $CTX | jq .data.tx)
|
NTX=$(echo $CTX | jq .data.tx)
|
||||||
assertEquals "type=cntr/count" '"cntr/count"' $(echo $CNTX | jq .type)
|
assertEquals "line=${LINENO}, type=nonce" '"nonce"' $(echo $NTX | jq .type)
|
||||||
assertEquals "proper fee" "10" $(echo $CNTX | jq .data.fee[0].amount)
|
CNTX=$(echo $NTX | jq .data.tx)
|
||||||
|
assertEquals "Line=${LINENO}, type=cntr/count" '"cntr/count"' $(echo $CNTX | jq .type)
|
||||||
|
assertEquals "Line=${LINENO}, proper fee" "10" $(echo $CNTX | jq .data.fee[0].amount)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# test again with fees...
|
# test again with fees...
|
||||||
|
@ -99,6 +101,12 @@ test03AddCount() {
|
||||||
|
|
||||||
# make sure the account was debited 11
|
# make sure the account was debited 11
|
||||||
checkAccount $SENDER "9007199254739979"
|
checkAccount $SENDER "9007199254739979"
|
||||||
|
|
||||||
|
# make sure we cannot replay the counter, no state change
|
||||||
|
TX=$(echo qwertyuiop | ${CLIENT_EXE} tx counter --countfee=10mycoin --sequence=2 --name=${RICH} --valid 2>/dev/null)
|
||||||
|
assertFalse "replay: $TX" $?
|
||||||
|
checkCounter "2" "17"
|
||||||
|
checkAccount $SENDER "9007199254739979"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Load common then run these tests with shunit2!
|
# Load common then run these tests with shunit2!
|
||||||
|
|
Loading…
Reference in New Issue