Merge pull request #171 from tendermint/feature/38-ibc-with-light-client

Rewrite IBC to use light client verification
This commit is contained in:
Ethan Frey 2017-07-27 22:20:25 -04:00 committed by GitHub
commit 79435666ce
56 changed files with 3688 additions and 1360 deletions

View File

@ -35,7 +35,7 @@ test_cli: tests/cli/shunit2
./tests/cli/roles.sh
./tests/cli/counter.sh
./tests/cli/restart.sh
# @./tests/cli/ibc.sh
./tests/cli/ibc.sh
test_tutorial: docs/guide/shunit2
@shelldown ${TUTORIALS}

View File

@ -10,12 +10,6 @@ import (
"github.com/tendermint/basecoin"
"github.com/tendermint/basecoin/errors"
"github.com/tendermint/basecoin/modules/auth"
"github.com/tendermint/basecoin/modules/base"
"github.com/tendermint/basecoin/modules/coin"
"github.com/tendermint/basecoin/modules/fee"
"github.com/tendermint/basecoin/modules/nonce"
"github.com/tendermint/basecoin/modules/roles"
"github.com/tendermint/basecoin/stack"
sm "github.com/tendermint/basecoin/state"
"github.com/tendermint/basecoin/version"
@ -50,28 +44,6 @@ func NewBasecoin(handler basecoin.Handler, store *Store, logger log.Logger) *Bas
}
}
// DefaultHandler - placeholder to just handle sendtx
func DefaultHandler(feeDenom string) basecoin.Handler {
// use the default stack
c := coin.NewHandler()
r := roles.NewHandler()
d := stack.NewDispatcher(
stack.WrapHandler(c),
stack.WrapHandler(r),
)
return stack.New(
base.Logger{},
stack.Recovery{},
auth.Signatures{},
base.Chain{},
stack.Checkpoint{OnCheck: true},
nonce.ReplayCheck{},
roles.NewMiddleware(),
fee.NewSimpleFeeMiddleware(coin.Coin{feeDenom, 0}, fee.Bank),
stack.Checkpoint{OnDeliver: true},
).Use(d)
}
// GetChainID returns the currently stored chain
func (app *Basecoin) GetChainID() string {
return app.info.GetChainID(app.state.Committed())

View File

@ -13,13 +13,42 @@ import (
"github.com/tendermint/basecoin/modules/base"
"github.com/tendermint/basecoin/modules/coin"
"github.com/tendermint/basecoin/modules/fee"
"github.com/tendermint/basecoin/modules/ibc"
"github.com/tendermint/basecoin/modules/nonce"
"github.com/tendermint/basecoin/modules/roles"
"github.com/tendermint/basecoin/stack"
"github.com/tendermint/basecoin/state"
wire "github.com/tendermint/go-wire"
"github.com/tendermint/tmlibs/log"
)
// DefaultHandler for the tests (coin, roles, ibc)
func DefaultHandler(feeDenom string) basecoin.Handler {
// use the default stack
r := roles.NewHandler()
i := ibc.NewHandler()
return stack.New(
base.Logger{},
stack.Recovery{},
auth.Signatures{},
base.Chain{},
stack.Checkpoint{OnCheck: true},
nonce.ReplayCheck{},
).
IBC(ibc.NewMiddleware()).
Apps(
roles.NewMiddleware(),
fee.NewSimpleFeeMiddleware(coin.Coin{feeDenom, 0}, fee.Bank),
stack.Checkpoint{OnDeliver: true},
).
Dispatch(
coin.NewHandler(),
stack.WrapHandler(r),
stack.WrapHandler(i),
)
}
//--------------------------------------------------------
// test environment is a list of input and output accounts

View File

@ -32,7 +32,7 @@ func DefaultHandler(feeDenom string) basecoin.Handler {
c := coin.NewHandler()
r := roles.NewHandler()
d := stack.NewDispatcher(
stack.WrapHandler(c),
c,
stack.WrapHandler(r),
)
return stack.New(

View File

@ -79,7 +79,7 @@ func GetCertifier() (*certifiers.InquiringCertifier, error) {
return nil, err
}
cert := certifiers.NewInquiring(
viper.GetString(ChainFlag), seed.Validators, trust, source)
viper.GetString(ChainFlag), seed, trust, source)
return cert, nil
}

View File

@ -1,11 +1,15 @@
package seeds
import (
"encoding/json"
"os"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/tendermint/basecoin/client/commands"
"github.com/tendermint/light-client/certifiers"
)
var exportCmd = &cobra.Command{
@ -40,5 +44,16 @@ func exportSeed(cmd *cobra.Command, args []string) error {
}
// now get the output file and write it
return seed.Write(path)
return writeSeed(seed, path)
}
func writeSeed(seed certifiers.Seed, path string) (err error) {
f, err := os.Create(path)
if err != nil {
return errors.WithStack(err)
}
defer f.Close()
stream := json.NewEncoder(f)
err = stream.Encode(seed)
return errors.WithStack(err)
}

View File

@ -4,6 +4,7 @@ import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/tendermint/light-client/certifiers"
@ -12,12 +13,13 @@ import (
var updateCmd = &cobra.Command{
Use: "update",
Short: "Update seed to current chain state if possible",
Short: "Update seed to current height if possible",
RunE: commands.RequireInit(updateSeed),
SilenceUsage: true,
}
func init() {
updateCmd.Flags().Int(heightFlag, 0, "Update to this height, not latest")
RootCmd.AddCommand(updateCmd)
}
@ -27,14 +29,20 @@ func updateSeed(cmd *cobra.Command, args []string) error {
return err
}
// get the lastest from our source
seed, err := certifiers.LatestSeed(cert.SeedSource)
h := viper.GetInt(heightFlag)
var seed certifiers.Seed
if h <= 0 {
// get the lastest from our source
seed, err = certifiers.LatestSeed(cert.SeedSource)
} else {
seed, err = cert.SeedSource.GetByHeight(h)
}
if err != nil {
return err
}
fmt.Printf("Trying to update to height: %d...\n", seed.Height())
// let the certifier do it's magic to update....
fmt.Printf("Trying to update to height: %d...\n", seed.Height())
err = cert.Update(seed.Checkpoint, seed.Validators)
if err != nil {
return err

View File

@ -19,6 +19,7 @@ import (
basecmd "github.com/tendermint/basecoin/modules/base/commands"
coincmd "github.com/tendermint/basecoin/modules/coin/commands"
feecmd "github.com/tendermint/basecoin/modules/fee/commands"
ibccmd "github.com/tendermint/basecoin/modules/ibc/commands"
noncecmd "github.com/tendermint/basecoin/modules/nonce/commands"
rolecmd "github.com/tendermint/basecoin/modules/roles/commands"
)
@ -46,6 +47,7 @@ func main() {
coincmd.AccountQueryCmd,
noncecmd.NonceQueryCmd,
rolecmd.RoleQueryCmd,
ibccmd.IBCQueryCmd,
)
proofs.TxPresenters.Register("base", txcmd.BaseTxPresenter{})
@ -63,8 +65,13 @@ func main() {
txcmd.RootCmd.AddCommand(
// This is the default transaction, optional in your app
coincmd.SendTxCmd,
coincmd.CreditTxCmd,
// this enables creating roles
rolecmd.CreateRoleTxCmd,
// these are for handling ibc
ibccmd.RegisterChainTxCmd,
ibccmd.UpdateChainTxCmd,
ibccmd.PostPacketTxCmd,
)
// Set up the various commands to use

View File

@ -120,7 +120,10 @@ func GetGenesisJSON(chainID, addr string) string {
"amount": 9007199254740992
}
]
}]
}],
"plugin_options": [
"coin/issuer", {"app": "sigs", "addr": "%s"}
]
}
}`, chainID, addr)
}`, chainID, addr, addr)
}

View File

@ -5,15 +5,46 @@ import (
"github.com/tendermint/tmlibs/cli"
"github.com/tendermint/basecoin/app"
"github.com/tendermint/basecoin"
"github.com/tendermint/basecoin/cmd/basecoin/commands"
"github.com/tendermint/basecoin/modules/auth"
"github.com/tendermint/basecoin/modules/base"
"github.com/tendermint/basecoin/modules/coin"
"github.com/tendermint/basecoin/modules/fee"
"github.com/tendermint/basecoin/modules/ibc"
"github.com/tendermint/basecoin/modules/nonce"
"github.com/tendermint/basecoin/modules/roles"
"github.com/tendermint/basecoin/stack"
)
// BuildApp constructs the stack we want to use for this app
func BuildApp(feeDenom string) basecoin.Handler {
return stack.New(
base.Logger{},
stack.Recovery{},
auth.Signatures{},
base.Chain{},
stack.Checkpoint{OnCheck: true},
nonce.ReplayCheck{},
).
IBC(ibc.NewMiddleware()).
Apps(
roles.NewMiddleware(),
fee.NewSimpleFeeMiddleware(coin.Coin{feeDenom, 0}, fee.Bank),
stack.Checkpoint{OnDeliver: true},
).
Dispatch(
coin.NewHandler(),
stack.WrapHandler(roles.NewHandler()),
stack.WrapHandler(ibc.NewHandler()),
)
}
func main() {
rt := commands.RootCmd
// require all fees in mycoin - change this in your app!
commands.Handler = app.DefaultHandler("mycoin")
commands.Handler = BuildApp("mycoin")
rt.AddCommand(
commands.InitCmd,

View File

@ -43,6 +43,24 @@ func (a Actor) Empty() bool {
return a.ChainID == "" && a.App == "" && len(a.Address) == 0
}
// WithChain creates a copy of the actor with a different chainID
func (a Actor) WithChain(chainID string) (b Actor) {
b = a
b.ChainID = chainID
return
}
type Actors []Actor
func (a Actors) AllHaveChain(chainID string) bool {
for _, b := range a {
if b.ChainID != chainID {
return false
}
}
return true
}
// Context is an interface, so we can implement "secure" variants that
// rely on private fields to control the actions
type Context interface {

View File

@ -12,7 +12,9 @@ import (
"github.com/tendermint/basecoin/modules/base"
"github.com/tendermint/basecoin/modules/coin"
"github.com/tendermint/basecoin/modules/fee"
"github.com/tendermint/basecoin/modules/ibc"
"github.com/tendermint/basecoin/modules/nonce"
"github.com/tendermint/basecoin/modules/roles"
"github.com/tendermint/basecoin/stack"
"github.com/tendermint/basecoin/state"
)
@ -90,13 +92,6 @@ func ErrDecoding() error {
// NewHandler returns a new counter transaction processing handler
func NewHandler(feeDenom string) basecoin.Handler {
// use the default stack
ch := coin.NewHandler()
counter := Handler{}
dispatcher := stack.NewDispatcher(
stack.WrapHandler(ch),
counter,
)
return stack.New(
base.Logger{},
stack.Recovery{},
@ -104,9 +99,17 @@ func NewHandler(feeDenom string) basecoin.Handler {
base.Chain{},
stack.Checkpoint{OnCheck: true},
nonce.ReplayCheck{},
fee.NewSimpleFeeMiddleware(coin.Coin{feeDenom, 0}, fee.Bank),
stack.Checkpoint{OnDeliver: true},
).Use(dispatcher)
).
IBC(ibc.NewMiddleware()).
Apps(
roles.NewMiddleware(),
fee.NewSimpleFeeMiddleware(coin.Coin{feeDenom, 0}, fee.Bank),
stack.Checkpoint{OnDeliver: true},
).
Dispatch(
coin.NewHandler(),
Handler{},
)
}
// Handler the counter transaction processing handler

View File

@ -16,6 +16,7 @@ var (
errUnknownTxType = fmt.Errorf("Tx type unknown")
errInvalidFormat = fmt.Errorf("Invalid format")
errUnknownModule = fmt.Errorf("Unknown module")
errUnknownKey = fmt.Errorf("Unknown key")
internalErr = abci.CodeType_InternalError
encodingErr = abci.CodeType_EncodingError
@ -39,7 +40,7 @@ func unwrap(i interface{}) interface{} {
func ErrUnknownTxType(tx interface{}) TMError {
msg := fmt.Sprintf("%T", unwrap(tx))
return WithMessage(msg, errUnknownTxType, abci.CodeType_UnknownRequest)
return WithMessage(msg, errUnknownTxType, unknownRequest)
}
func IsUnknownTxTypeErr(err error) bool {
return IsSameError(errUnknownTxType, err)
@ -47,19 +48,26 @@ func IsUnknownTxTypeErr(err error) bool {
func ErrInvalidFormat(expected string, tx interface{}) TMError {
msg := fmt.Sprintf("%T not %s", unwrap(tx), expected)
return WithMessage(msg, errInvalidFormat, abci.CodeType_UnknownRequest)
return WithMessage(msg, errInvalidFormat, unknownRequest)
}
func IsInvalidFormatErr(err error) bool {
return IsSameError(errInvalidFormat, err)
}
func ErrUnknownModule(mod string) TMError {
return WithMessage(mod, errUnknownModule, abci.CodeType_UnknownRequest)
return WithMessage(mod, errUnknownModule, unknownRequest)
}
func IsUnknownModuleErr(err error) bool {
return IsSameError(errUnknownModule, err)
}
func ErrUnknownKey(mod string) TMError {
return WithMessage(mod, errUnknownKey, unknownRequest)
}
func IsUnknownKeyErr(err error) bool {
return IsSameError(errUnknownKey, err)
}
func ErrInternal(msg string) TMError {
return New(msg, internalErr)
}

9
errors/helpers.go Normal file
View File

@ -0,0 +1,9 @@
package errors
// CheckErr is the type of all the check functions here
type CheckErr func(error) bool
// NoErr is useful for test cases when you want to fulfil the CheckErr type
func NoErr(err error) bool {
return err == nil
}

10
glide.lock generated
View File

@ -1,5 +1,5 @@
hash: 45eed61138603d4d03518ea822068cf32b45d0a219bb7f3b836e52129f2a3a2b
updated: 2017-07-26T19:44:39.753066441-04:00
hash: 2848c30b31fb205f846dd7dfca14ebed8a3249cbc5aaa759066b2bab3e4bbf42
updated: 2017-07-27T16:46:31.962147949-04:00
imports:
- name: github.com/bgentry/speakeasy
version: 4aabc24848ce5fd31929f7d1e4ea74d3709c14cd
@ -57,6 +57,8 @@ imports:
- json/parser
- json/scanner
- json/token
- name: github.com/howeyc/crc16
version: 96a97a1abb579c7ff1a8ffa77f2e72d1c314b57f
- name: github.com/inconshreveable/mousetrap
version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75
- name: github.com/jmhodges/levigo
@ -117,7 +119,7 @@ imports:
- edwards25519
- extra25519
- name: github.com/tendermint/go-crypto
version: d31cfbaeaa4d930798ec327b52917975f3203c11
version: bf355d1b58b27d4e98d8fb237eb14887b93a88f7
subpackages:
- cmd
- keys
@ -133,7 +135,7 @@ imports:
- data
- data/base58
- name: github.com/tendermint/light-client
version: 1c53d04dcc65c2fd15526152ed0651af10a09982
version: fcf4e411583135a1900157b8b0274c41e20ea3a1
subpackages:
- certifiers
- certifiers/client

View File

@ -22,7 +22,7 @@ import:
subpackages:
- data
- package: github.com/tendermint/light-client
version: 1c53d04dcc65c2fd15526152ed0651af10a09982
version: unstable
subpackages:
- proofs
- certifiers

View File

@ -11,7 +11,7 @@ import (
"github.com/tendermint/basecoin/state"
)
func makeHandler() basecoin.Handler {
func makeHandler() stack.Dispatchable {
return NewHandler()
}
@ -28,7 +28,7 @@ func BenchmarkSimpleTransfer(b *testing.B) {
// set the initial account
acct := NewAccountWithKey(Coins{{"mycoin", 1234567890}})
h.SetOption(logger, store, NameCoin, "account", acct.MakeOption())
h.SetOption(logger, store, NameCoin, "account", acct.MakeOption(), nil)
sender := acct.Actor()
receiver := basecoin.Actor{App: "foo", Address: cmn.RandBytes(20)}
@ -36,7 +36,7 @@ func BenchmarkSimpleTransfer(b *testing.B) {
for i := 1; i <= b.N; i++ {
ctx := stack.MockContext("foo", 100).WithPermissions(sender)
tx := makeSimpleTx(sender, receiver, Coins{{"mycoin", 2}})
_, err := h.DeliverTx(ctx, store, tx)
_, err := h.DeliverTx(ctx, store, tx, nil)
// never should error
if err != nil {
panic(err)

View File

@ -28,6 +28,7 @@ func accountQueryCmd(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
act = coin.ChainAddr(act)
key := stack.PrefixedKey(coin.NameCoin, act.Bytes())
acc := coin.Account{}

View File

@ -17,6 +17,13 @@ var SendTxCmd = &cobra.Command{
RunE: commands.RequireInit(sendTxCmd),
}
// CreditTxCmd is CLI command to issue credit to one account
var CreditTxCmd = &cobra.Command{
Use: "credit",
Short: "issue credit to one account",
RunE: commands.RequireInit(creditTxCmd),
}
//nolint
const (
FlagTo = "to"
@ -29,9 +36,12 @@ func init() {
flags.String(FlagTo, "", "Destination address for the bits")
flags.String(FlagAmount, "", "Coins to send in the format <amt><coin>,<amt><coin>...")
flags.String(FlagFrom, "", "Address sending coins, if not first signer")
fs2 := CreditTxCmd.Flags()
fs2.String(FlagTo, "", "Destination address for the bits")
fs2.String(FlagAmount, "", "Coins to send in the format <amt><coin>,<amt><coin>...")
}
// sendTxCmd is an example of how to make a tx
func sendTxCmd(cmd *cobra.Command, args []string) error {
tx, err := readSendTxFlags()
if err != nil {
@ -62,6 +72,30 @@ func readSendTxFlags() (tx basecoin.Tx, err error) {
return
}
func creditTxCmd(cmd *cobra.Command, args []string) error {
tx, err := readCreditTxFlags()
if err != nil {
return err
}
return txcmd.DoTx(tx)
}
func readCreditTxFlags() (tx basecoin.Tx, err error) {
// parse to address
toAddr, err := commands.ParseActor(viper.GetString(FlagTo))
if err != nil {
return tx, err
}
amount, err := coin.ParseCoins(viper.GetString(FlagAmount))
if err != nil {
return tx, err
}
tx = coin.CreditTx{Debitor: toAddr, Credit: amount}.Wrap()
return
}
func readFromAddr() (basecoin.Actor, error) {
from := viper.GetString(FlagFrom)
if from == "" {

View File

@ -10,13 +10,13 @@ import (
)
var (
errNoAccount = fmt.Errorf("No such account")
errInsufficientFunds = fmt.Errorf("Insufficient funds")
errNoInputs = fmt.Errorf("No input coins")
errNoOutputs = fmt.Errorf("No output coins")
errInvalidAddress = fmt.Errorf("Invalid address")
errInvalidCoins = fmt.Errorf("Invalid coins")
errUnknownKey = fmt.Errorf("Unknown key")
errNoAccount = fmt.Errorf("No such account")
errInsufficientFunds = fmt.Errorf("Insufficient funds")
errInsufficientCredit = fmt.Errorf("Insufficient credit")
errNoInputs = fmt.Errorf("No input coins")
errNoOutputs = fmt.Errorf("No output coins")
errInvalidAddress = fmt.Errorf("Invalid address")
errInvalidCoins = fmt.Errorf("Invalid coins")
invalidInput = abci.CodeType_BaseInvalidInput
invalidOutput = abci.CodeType_BaseInvalidOutput
@ -67,6 +67,13 @@ func IsInsufficientFundsErr(err error) bool {
return errors.IsSameError(errInsufficientFunds, err)
}
func ErrInsufficientCredit() errors.TMError {
return errors.WithCode(errInsufficientCredit, invalidInput)
}
func IsInsufficientCreditErr(err error) bool {
return errors.IsSameError(errInsufficientCredit, err)
}
func ErrNoInputs() errors.TMError {
return errors.WithCode(errNoInputs, invalidInput)
}
@ -80,10 +87,3 @@ func ErrNoOutputs() errors.TMError {
func IsNoOutputsErr(err error) bool {
return errors.IsSameError(errNoOutputs, err)
}
func ErrUnknownKey(mod string) errors.TMError {
return errors.WithMessage(mod, errUnknownKey, unknownRequest)
}
func IsUnknownKeyErr(err error) bool {
return errors.IsSameError(errUnknownKey, err)
}

View File

@ -7,6 +7,8 @@ import (
"github.com/tendermint/basecoin"
"github.com/tendermint/basecoin/errors"
"github.com/tendermint/basecoin/modules/auth"
"github.com/tendermint/basecoin/modules/ibc"
"github.com/tendermint/basecoin/stack"
"github.com/tendermint/basecoin/state"
)
@ -16,7 +18,7 @@ const NameCoin = "coin"
// Handler includes an accountant
type Handler struct{}
var _ basecoin.Handler = Handler{}
var _ stack.Dispatchable = Handler{}
// NewHandler - new accountant handler for the coin module
func NewHandler() Handler {
@ -28,96 +30,207 @@ func (Handler) Name() string {
return NameCoin
}
// AssertDispatcher - to fulfill Dispatchable interface
func (Handler) AssertDispatcher() {}
// CheckTx checks if there is enough money in the account
func (h Handler) CheckTx(ctx basecoin.Context, store state.SimpleDB, tx basecoin.Tx) (res basecoin.Result, err error) {
send, err := checkTx(ctx, tx)
func (h Handler) CheckTx(ctx basecoin.Context, store state.SimpleDB,
tx basecoin.Tx, _ basecoin.Checker) (res basecoin.Result, err error) {
err = tx.ValidateBasic()
if err != nil {
return res, err
}
// now make sure there is money
for _, in := range send.Inputs {
_, err = CheckCoins(store, in.Address, in.Coins.Negative())
if err != nil {
return res, err
}
switch t := tx.Unwrap().(type) {
case SendTx:
return res, h.checkSendTx(ctx, store, t)
case CreditTx:
return h.creditTx(ctx, store, t)
}
// otherwise, we are good
return res, nil
return res, errors.ErrUnknownTxType(tx.Unwrap())
}
// DeliverTx moves the money
func (h Handler) DeliverTx(ctx basecoin.Context, store state.SimpleDB, tx basecoin.Tx) (res basecoin.Result, err error) {
send, err := checkTx(ctx, tx)
func (h Handler) DeliverTx(ctx basecoin.Context, store state.SimpleDB,
tx basecoin.Tx, cb basecoin.Deliver) (res basecoin.Result, err error) {
err = tx.ValidateBasic()
if err != nil {
return res, err
}
switch t := tx.Unwrap().(type) {
case SendTx:
return h.sendTx(ctx, store, t, cb)
case CreditTx:
return h.creditTx(ctx, store, t)
}
return res, errors.ErrUnknownTxType(tx.Unwrap())
}
// SetOption - sets the genesis account balance
func (h Handler) SetOption(l log.Logger, store state.SimpleDB,
module, key, value string, cb basecoin.SetOptioner) (log string, err error) {
if module != NameCoin {
return "", errors.ErrUnknownModule(module)
}
switch key {
case "account":
return setAccount(store, value)
case "issuer":
return setIssuer(store, value)
}
return "", errors.ErrUnknownKey(key)
}
func (h Handler) sendTx(ctx basecoin.Context, store state.SimpleDB,
send SendTx, cb basecoin.Deliver) (res basecoin.Result, err error) {
err = checkTx(ctx, send)
if err != nil {
return res, err
}
// deduct from all input accounts
senders := basecoin.Actors{}
for _, in := range send.Inputs {
_, err = ChangeCoins(store, in.Address, in.Coins.Negative())
if err != nil {
return res, err
}
senders = append(senders, in.Address)
}
// add to all output accounts
for _, out := range send.Outputs {
// TODO: cleaner way, this makes sure we don't consider
// incoming ibc packets with our chain to be remote packets
if out.Address.ChainID == ctx.ChainID() {
out.Address.ChainID = ""
}
_, err = ChangeCoins(store, out.Address, out.Coins)
if err != nil {
return res, err
}
// now send ibc packet if needed...
if out.Address.ChainID != "" {
// FIXME: if there are many outputs, we need to adjust inputs
// so the amounts in and out match. how?
inputs := make([]TxInput, len(send.Inputs))
for i := range send.Inputs {
inputs[i] = send.Inputs[i]
inputs[i].Address = inputs[i].Address.WithChain(ctx.ChainID())
}
outTx := NewSendTx(inputs, []TxOutput{out})
packet := ibc.CreatePacketTx{
DestChain: out.Address.ChainID,
Permissions: senders,
Tx: outTx,
}
ibcCtx := ctx.WithPermissions(ibc.AllowIBC(NameCoin))
_, err := cb.DeliverTx(ibcCtx, store, packet.Wrap())
if err != nil {
return res, err
}
}
}
// a-ok!
return basecoin.Result{}, nil
return res, nil
}
// SetOption - sets the genesis account balance
func (h Handler) SetOption(l log.Logger, store state.SimpleDB, module, key, value string) (log string, err error) {
if module != NameCoin {
return "", errors.ErrUnknownModule(module)
}
if key == "account" {
var acc GenesisAccount
err = data.FromJSON([]byte(value), &acc)
if err != nil {
return "", err
}
acc.Balance.Sort()
addr, err := acc.GetAddr()
if err != nil {
return "", ErrInvalidAddress()
}
// this sets the permission for a public key signature, use that app
actor := auth.SigPerm(addr)
err = storeAccount(store, actor.Bytes(), acc.ToAccount())
if err != nil {
return "", err
}
return "Success", nil
func (h Handler) creditTx(ctx basecoin.Context, store state.SimpleDB,
credit CreditTx) (res basecoin.Result, err error) {
}
return "", ErrUnknownKey(key)
}
func checkTx(ctx basecoin.Context, tx basecoin.Tx) (send SendTx, err error) {
// check if the tx is proper type and valid
send, ok := tx.Unwrap().(SendTx)
if !ok {
return send, errors.ErrInvalidFormat(TypeSend, tx)
}
err = send.ValidateBasic()
// first check permissions!!
info, err := loadHandlerInfo(store)
if err != nil {
return send, err
return res, err
}
if info.Issuer.Empty() || !ctx.HasPermission(info.Issuer) {
return res, errors.ErrUnauthorized()
}
// load up the account
addr := ChainAddr(credit.Debitor)
acct, err := GetAccount(store, addr)
if err != nil {
return res, err
}
// make and check changes
acct.Coins = acct.Coins.Plus(credit.Credit)
if !acct.Coins.IsNonnegative() {
return res, ErrInsufficientFunds()
}
acct.Credit = acct.Credit.Plus(credit.Credit)
if !acct.Credit.IsNonnegative() {
return res, ErrInsufficientCredit()
}
err = storeAccount(store, addr.Bytes(), acct)
return res, err
}
func checkTx(ctx basecoin.Context, send SendTx) error {
// check if all inputs have permission
for _, in := range send.Inputs {
if !ctx.HasPermission(in.Address) {
return send, errors.ErrUnauthorized()
return errors.ErrUnauthorized()
}
}
return send, nil
return nil
}
func (Handler) checkSendTx(ctx basecoin.Context, store state.SimpleDB, send SendTx) error {
err := checkTx(ctx, send)
if err != nil {
return err
}
// now make sure there is money
for _, in := range send.Inputs {
_, err := CheckCoins(store, in.Address, in.Coins.Negative())
if err != nil {
return err
}
}
return nil
}
func setAccount(store state.SimpleDB, value string) (log string, err error) {
var acc GenesisAccount
err = data.FromJSON([]byte(value), &acc)
if err != nil {
return "", err
}
acc.Balance.Sort()
addr, err := acc.GetAddr()
if err != nil {
return "", ErrInvalidAddress()
}
// this sets the permission for a public key signature, use that app
actor := auth.SigPerm(addr)
err = storeAccount(store, actor.Bytes(), acc.ToAccount())
if err != nil {
return "", err
}
return "Success", nil
}
// setIssuer sets a permission for some super-powerful account to
// mint money
func setIssuer(store state.SimpleDB, value string) (log string, err error) {
var issuer basecoin.Actor
err = data.FromJSON([]byte(value), &issuer)
if err != nil {
return "", err
}
err = storeIssuer(store, issuer)
if err != nil {
return "", err
}
return "Success", nil
}

View File

@ -11,6 +11,7 @@ import (
"github.com/tendermint/tmlibs/log"
"github.com/tendermint/basecoin"
"github.com/tendermint/basecoin/errors"
"github.com/tendermint/basecoin/modules/auth"
"github.com/tendermint/basecoin/stack"
"github.com/tendermint/basecoin/state"
@ -75,7 +76,7 @@ func TestHandlerValidation(t *testing.T) {
for i, tc := range cases {
ctx := stack.MockContext("base-chain", 100).WithPermissions(tc.perms...)
_, err := checkTx(ctx, tc.tx)
err := checkTx(ctx, tc.tx.Unwrap().(SendTx))
if tc.valid {
assert.Nil(err, "%d: %+v", i, err)
} else {
@ -84,7 +85,7 @@ func TestHandlerValidation(t *testing.T) {
}
}
func TestDeliverTx(t *testing.T) {
func TestDeliverSendTx(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
@ -149,7 +150,7 @@ func TestDeliverTx(t *testing.T) {
}
ctx := stack.MockContext("base-chain", 100).WithPermissions(tc.perms...)
_, err := h.DeliverTx(ctx, store, tc.tx)
_, err := h.DeliverTx(ctx, store, tc.tx, nil)
if len(tc.final) > 0 { // valid
assert.Nil(err, "%d: %+v", i, err)
// make sure the final balances are correct
@ -204,7 +205,7 @@ func TestSetOption(t *testing.T) {
for j, gen := range tc.init {
value, err := json.Marshal(gen)
require.Nil(err, "%d,%d: %+v", i, j, err)
_, err = h.SetOption(l, store, NameCoin, key, string(value))
_, err = h.SetOption(l, store, NameCoin, key, string(value), nil)
require.Nil(err)
}
@ -215,5 +216,141 @@ func TestSetOption(t *testing.T) {
assert.Equal(f.coins, acct.Coins)
}
}
}
func TestSetIssuer(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
cases := []struct {
issuer basecoin.Actor
}{
{basecoin.Actor{App: "sig", Address: []byte("gwkfgk")}},
// and set back to empty (nil is valid, but assert.Equals doesn't match)
{basecoin.Actor{Address: []byte{}}},
{basecoin.Actor{ChainID: "other", App: "role", Address: []byte("vote")}},
}
h := NewHandler()
l := log.NewNopLogger()
for i, tc := range cases {
store := state.NewMemKVStore()
key := "issuer"
value, err := json.Marshal(tc.issuer)
require.Nil(err, "%d,%d: %+v", i, err)
_, err = h.SetOption(l, store, NameCoin, key, string(value), nil)
require.Nil(err, "%+v", err)
// check state is proper
info, err := loadHandlerInfo(store)
assert.Nil(err, "%d: %+v", i, err)
assert.Equal(tc.issuer, info.Issuer)
}
}
func TestDeliverCreditTx(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
// sample coins
someCoins := Coins{{"atom", 6570}}
minusCoins := Coins{{"atom", -1234}}
lessCoins := someCoins.Plus(minusCoins)
otherCoins := Coins{{"eth", 11}}
mixedCoins := someCoins.Plus(otherCoins)
// some sample addresses
owner := basecoin.Actor{App: "foo", Address: []byte("rocks")}
addr1 := basecoin.Actor{App: "coin", Address: []byte{1, 2}}
key := NewAccountWithKey(someCoins)
addr2 := key.Actor()
addr3 := basecoin.Actor{ChainID: "other", App: "sigs", Address: []byte{3, 9}}
h := NewHandler()
store := state.NewMemKVStore()
ctx := stack.MockContext("secret", 77)
// set the owner who can issue credit
js, err := json.Marshal(owner)
require.Nil(err, "%+v", err)
_, err = h.SetOption(log.NewNopLogger(), store, "coin", "issuer", string(js), nil)
require.Nil(err, "%+v", err)
// give addr2 some coins to start
_, err = h.SetOption(log.NewNopLogger(), store, "coin", "account", key.MakeOption(), nil)
require.Nil(err, "%+v", err)
cases := []struct {
tx basecoin.Tx
perm basecoin.Actor
check errors.CheckErr
addr basecoin.Actor
expected Account
}{
// require permission
{
tx: NewCreditTx(addr1, someCoins),
check: errors.IsUnauthorizedErr,
},
// add credit
{
tx: NewCreditTx(addr1, someCoins),
perm: owner,
check: errors.NoErr,
addr: addr1,
expected: Account{Coins: someCoins, Credit: someCoins},
},
// remove some
{
tx: NewCreditTx(addr1, minusCoins),
perm: owner,
check: errors.NoErr,
addr: addr1,
expected: Account{Coins: lessCoins, Credit: lessCoins},
},
// can't remove more cash than there is
{
tx: NewCreditTx(addr1, otherCoins.Negative()),
perm: owner,
check: IsInsufficientFundsErr,
},
// cumulative with initial state
{
tx: NewCreditTx(addr2, otherCoins),
perm: owner,
check: errors.NoErr,
addr: addr2,
expected: Account{Coins: mixedCoins, Credit: otherCoins},
},
// Even if there is cash, credit can't go negative
{
tx: NewCreditTx(addr2, minusCoins),
perm: owner,
check: IsInsufficientCreditErr,
},
// make sure it works for other chains
{
tx: NewCreditTx(addr3, mixedCoins),
perm: owner,
check: errors.NoErr,
addr: ChainAddr(addr3),
expected: Account{Coins: mixedCoins, Credit: mixedCoins},
},
}
for i, tc := range cases {
myStore := store.Checkpoint()
myCtx := ctx.WithPermissions(tc.perm)
_, err = h.DeliverTx(myCtx, myStore, tc.tx, nil)
assert.True(tc.check(err), "%d: %+v", i, err)
if err == nil {
store.Commit(myStore)
acct, err := GetAccount(store, tc.addr)
require.Nil(err, "%+v", err)
assert.Equal(tc.expected, acct, "%d", i)
}
}
}

140
modules/coin/ibc_test.go Normal file
View File

@ -0,0 +1,140 @@
package coin
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tendermint/basecoin"
"github.com/tendermint/basecoin/errors"
"github.com/tendermint/basecoin/modules/auth"
"github.com/tendermint/basecoin/modules/ibc"
"github.com/tendermint/basecoin/stack"
"github.com/tendermint/basecoin/state"
wire "github.com/tendermint/go-wire"
)
// TODO: other test making sure tx is output on send, balance is updated
// This makes sure we respond properly to posttx
// TODO: set credit limit
func TestIBCPostPacket(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
otherID := "chain-2"
ourID := "dex"
start := 200
// create the app and our chain
app := stack.New().
IBC(ibc.NewMiddleware()).
Dispatch(
NewHandler(),
stack.WrapHandler(ibc.NewHandler()),
)
ourChain := ibc.NewAppChain(app, ourID)
// set up the other chain and register it with us
otherChain := ibc.NewMockChain(otherID, 7)
registerTx := otherChain.GetRegistrationTx(start).Wrap()
_, err := ourChain.DeliverTx(registerTx)
require.Nil(err, "%+v", err)
// set up a rich guy on this chain
wealth := Coins{{"btc", 300}, {"eth", 2000}, {"ltc", 5000}}
rich := NewAccountWithKey(wealth)
_, err = ourChain.SetOption("coin", "account", rich.MakeOption())
require.Nil(err, "%+v", err)
// sends money to another guy on a different chain, now other chain has credit
buddy := basecoin.Actor{ChainID: otherID, App: auth.NameSigs, Address: []byte("dude")}
outTx := NewSendOneTx(rich.Actor(), buddy, wealth)
_, err = ourChain.DeliverTx(outTx, rich.Actor())
require.Nil(err, "%+v", err)
// make sure the money moved to the other chain...
cstore := ourChain.GetStore(NameCoin)
acct, err := GetAccount(cstore, ChainAddr(buddy))
require.Nil(err, "%+v", err)
require.Equal(wealth, acct.Coins)
// make sure there is a proper packet for this....
istore := ourChain.GetStore(ibc.NameIBC)
assertPacket(t, istore, otherID, wealth)
// these are the people for testing incoming ibc from the other chain
recipient := basecoin.Actor{ChainID: ourID, App: auth.NameSigs, Address: []byte("bar")}
sender := basecoin.Actor{ChainID: otherID, App: auth.NameSigs, Address: []byte("foo")}
payment := Coins{{"eth", 100}, {"ltc", 300}}
coinTx := NewSendOneTx(sender, recipient, payment)
wrongCoin := NewSendOneTx(sender, recipient, Coins{{"missing", 20}})
p0 := ibc.NewPacket(coinTx, ourID, 0, sender)
packet0, update0 := otherChain.MakePostPacket(p0, start+5)
require.Nil(ourChain.Update(update0))
p1 := ibc.NewPacket(coinTx, ourID, 1, sender)
packet1, update1 := otherChain.MakePostPacket(p1, start+25)
require.Nil(ourChain.Update(update1))
p2 := ibc.NewPacket(wrongCoin, ourID, 2, sender)
packet2, update2 := otherChain.MakePostPacket(p2, start+50)
require.Nil(ourChain.Update(update2))
ibcPerm := basecoin.Actors{ibc.AllowIBC(NameCoin)}
cases := []struct {
packet ibc.PostPacketTx
permissions basecoin.Actors
checker errors.CheckErr
}{
// out of order -> error
{packet1, ibcPerm, ibc.IsPacketOutOfOrderErr},
// all good -> execute tx
{packet0, ibcPerm, errors.NoErr},
// all good -> execute tx (even if earlier attempt failed)
{packet1, ibcPerm, errors.NoErr},
// packet 2 attempts to spend money this chain doesn't have
{packet2, ibcPerm, IsInsufficientFundsErr},
}
for i, tc := range cases {
_, err := ourChain.DeliverTx(tc.packet.Wrap(), tc.permissions...)
assert.True(tc.checker(err), "%d: %+v", i, err)
}
// now, make sure the recipient got credited for the 2 successful sendtx
cstore = ourChain.GetStore(NameCoin)
// FIXME: we need to strip off this when it is local chain-id...
// think this throw and handle this better
local := recipient.WithChain("")
acct, err = GetAccount(cstore, local)
require.Nil(err, "%+v", err)
assert.Equal(payment.Plus(payment), acct.Coins)
}
func assertPacket(t *testing.T, istore state.SimpleDB, destID string, amount Coins) {
assert := assert.New(t)
require := require.New(t)
iq := ibc.InputQueue(istore, destID)
require.Equal(0, iq.Size())
q := ibc.OutputQueue(istore, destID)
require.Equal(1, q.Size())
d := q.Item(0)
var res ibc.Packet
err := wire.ReadBinaryBytes(d, &res)
require.Nil(err, "%+v", err)
assert.Equal(destID, res.DestChain)
assert.EqualValues(0, res.Sequence)
stx, ok := res.Tx.Unwrap().(SendTx)
if assert.True(ok) {
assert.Equal(1, len(stx.Outputs))
assert.Equal(amount, stx.Outputs[0].Coins)
}
}

View File

@ -12,6 +12,8 @@ import (
// GetAccount - Get account from store and address
func GetAccount(store state.SimpleDB, addr basecoin.Actor) (Account, error) {
// if the actor is another chain, we use one address for the chain....
addr = ChainAddr(addr)
acct, err := loadAccount(store, addr.Bytes())
// for empty accounts, don't return an error, but rather an empty account
@ -23,12 +25,18 @@ func GetAccount(store state.SimpleDB, addr basecoin.Actor) (Account, error) {
// CheckCoins makes sure there are funds, but doesn't change anything
func CheckCoins(store state.SimpleDB, addr basecoin.Actor, coins Coins) (Coins, error) {
// if the actor is another chain, we use one address for the chain....
addr = ChainAddr(addr)
acct, err := updateCoins(store, addr, coins)
return acct.Coins, err
}
// ChangeCoins changes the money, returns error if it would be negative
func ChangeCoins(store state.SimpleDB, addr basecoin.Actor, coins Coins) (Coins, error) {
// if the actor is another chain, we use one address for the chain....
addr = ChainAddr(addr)
acct, err := updateCoins(store, addr, coins)
if err != nil {
return acct.Coins, err
@ -38,6 +46,19 @@ func ChangeCoins(store state.SimpleDB, addr basecoin.Actor, coins Coins) (Coins,
return acct.Coins, err
}
// ChainAddr collapses all addresses from another chain into one, so we can
// keep an over-all balance
//
// TODO: is there a better way to do this?
func ChainAddr(addr basecoin.Actor) basecoin.Actor {
if addr.ChainID == "" {
return addr
}
addr.App = ""
addr.Address = nil
return addr
}
// updateCoins will load the account, make all checks, and return the updated account.
//
// it doesn't save anything, that is up to you to decide (Check/Change Coins)
@ -63,7 +84,11 @@ func updateCoins(store state.SimpleDB, addr basecoin.Actor, coins Coins) (acct A
// Account - coin account structure
type Account struct {
// Coins is how much is on the account
Coins Coins `json:"coins"`
// Credit is how much has been "fronted" to the account
// (this is usually 0 except for trusted chains)
Credit Coins `json:"credit"`
}
func loadAccount(store state.SimpleDB, key []byte) (acct Account, err error) {
@ -86,3 +111,35 @@ func storeAccount(store state.SimpleDB, key []byte, acct Account) error {
store.Set(key, bin)
return nil // real stores can return error...
}
// HandlerInfo - this is global info on the coin handler
type HandlerInfo struct {
Issuer basecoin.Actor `json:"issuer"`
}
// TODO: where to store these special pieces??
var handlerKey = []byte{12, 34}
func loadHandlerInfo(store state.KVStore) (info HandlerInfo, err error) {
data := store.Get(handlerKey)
if len(data) == 0 {
return info, nil
}
err = wire.ReadBinaryBytes(data, &info)
if err != nil {
msg := "Error reading handler info"
return info, errors.ErrInternal(msg)
}
return info, nil
}
func storeIssuer(store state.KVStore, issuer basecoin.Actor) error {
info, err := loadHandlerInfo(store)
if err != nil {
return err
}
info.Issuer = issuer
d := wire.BinaryBytes(info)
store.Set(handlerKey, d)
return nil // real stores can return error...
}

View File

@ -7,13 +7,17 @@ import (
)
func init() {
basecoin.TxMapper.RegisterImplementation(SendTx{}, TypeSend, ByteSend)
basecoin.TxMapper.
RegisterImplementation(SendTx{}, TypeSend, ByteSend).
RegisterImplementation(CreditTx{}, TypeCredit, ByteCredit)
}
// we reserve the 0x20-0x3f range for standard modules
const (
ByteSend = 0x20
TypeSend = NameCoin + "/send"
ByteSend = 0x20
TypeSend = NameCoin + "/send"
ByteCredit = 0x21
TypeCredit = NameCoin + "/credit"
)
//-----------------------------------------------------------------------------
@ -157,3 +161,30 @@ func (tx SendTx) String() string {
func (tx SendTx) Wrap() basecoin.Tx {
return basecoin.Tx{tx}
}
//-----------------------------------------------------------------------------
// CreditTx - this allows a special issuer to give an account credit
// Satisfies: TxInner
type CreditTx struct {
Debitor basecoin.Actor `json:"debitor"`
// Credit is the amount to change the credit...
// This may be negative to remove some over-issued credit,
// but can never bring the credit or the balance to negative
Credit Coins `json:"credit"`
}
// NewCreditTx - modify the credit granted to a given account
func NewCreditTx(debitor basecoin.Actor, credit Coins) basecoin.Tx {
return CreditTx{Debitor: debitor, Credit: credit}.Wrap()
}
// Wrap - used to satisfy TxInner
func (tx CreditTx) Wrap() basecoin.Tx {
return basecoin.Tx{tx}
}
// ValidateBasic - used to satisfy TxInner
func (tx CreditTx) ValidateBasic() error {
return nil
}

View File

@ -40,7 +40,7 @@ func TestFeeChecks(t *testing.T) {
// OKHandler will just return success to a RawTx
stack.WrapHandler(stack.OKHandler{}),
// coin is needed to handle the IPC call from Fee middleware
stack.WrapHandler(coin.NewHandler()),
coin.NewHandler(),
)
// app1 requires no fees
app1 := stack.New(fee.NewSimpleFeeMiddleware(atom(0), collector)).Use(disp)

View File

@ -7,7 +7,7 @@ import (
// nolint
const (
ByteFees = 0x21
ByteFees = 0x28
TypeFees = NameFee + "/tx"
)

View File

@ -0,0 +1,226 @@
package commands
import (
"fmt"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/tendermint/basecoin/client/commands"
proofcmd "github.com/tendermint/basecoin/client/commands/proofs"
"github.com/tendermint/basecoin/modules/ibc"
"github.com/tendermint/basecoin/stack"
"github.com/tendermint/go-wire/data"
"github.com/tendermint/light-client/proofs"
"github.com/tendermint/merkleeyes/iavl"
)
// TODO: query seeds (register/update)
// IBCQueryCmd - parent command to query ibc info
var IBCQueryCmd = &cobra.Command{
Use: "ibc",
Short: "Get information about IBC",
RunE: commands.RequireInit(ibcQueryCmd),
// HandlerInfo
}
// ChainsQueryCmd - get a list of all registered chains
var ChainsQueryCmd = &cobra.Command{
Use: "chains",
Short: "Get a list of all registered chains",
RunE: commands.RequireInit(chainsQueryCmd),
// ChainSet ([]string)
}
// ChainQueryCmd - get details on one registered chain
var ChainQueryCmd = &cobra.Command{
Use: "chain [id]",
Short: "Get details on one registered chain",
RunE: commands.RequireInit(chainQueryCmd),
// ChainInfo
}
// PacketsQueryCmd - get latest packet in a queue
var PacketsQueryCmd = &cobra.Command{
Use: "packets",
Short: "Get latest packet in a queue",
RunE: commands.RequireInit(packetsQueryCmd),
// uint64
}
// PacketQueryCmd - get the names packet (by queue and sequence)
var PacketQueryCmd = &cobra.Command{
Use: "packet",
Short: "Get packet with given sequence from the named queue",
RunE: commands.RequireInit(packetQueryCmd),
// Packet
}
//nolint
const (
FlagFromChain = "from"
FlagToChain = "to"
FlagSequence = "sequence"
)
func init() {
IBCQueryCmd.AddCommand(
ChainQueryCmd,
ChainsQueryCmd,
PacketQueryCmd,
PacketsQueryCmd,
)
fs1 := PacketsQueryCmd.Flags()
fs1.String(FlagFromChain, "", "Name of the input chain (where packets came from)")
fs1.String(FlagToChain, "", "Name of the output chain (where packets go to)")
fs2 := PacketQueryCmd.Flags()
fs2.String(FlagFromChain, "", "Name of the input chain (where packets came from)")
fs2.String(FlagToChain, "", "Name of the output chain (where packets go to)")
fs2.Int(FlagSequence, -1, "Index of the packet in the queue (starts with 0)")
}
func ibcQueryCmd(cmd *cobra.Command, args []string) error {
var res ibc.HandlerInfo
key := stack.PrefixedKey(ibc.NameIBC, ibc.HandlerKey())
proof, err := proofcmd.GetAndParseAppProof(key, &res)
if err != nil {
return err
}
return proofcmd.OutputProof(res, proof.BlockHeight())
}
func chainsQueryCmd(cmd *cobra.Command, args []string) error {
list := [][]byte{}
key := stack.PrefixedKey(ibc.NameIBC, ibc.ChainsKey())
proof, err := proofcmd.GetAndParseAppProof(key, &list)
if err != nil {
return err
}
// convert these names to strings for better output
res := make([]string, len(list))
for i := range list {
res[i] = string(list[i])
}
return proofcmd.OutputProof(res, proof.BlockHeight())
}
func chainQueryCmd(cmd *cobra.Command, args []string) error {
arg, err := commands.GetOneArg(args, "id")
if err != nil {
return err
}
var res ibc.ChainInfo
key := stack.PrefixedKey(ibc.NameIBC, ibc.ChainKey(arg))
proof, err := proofcmd.GetAndParseAppProof(key, &res)
if err != nil {
return err
}
return proofcmd.OutputProof(res, proof.BlockHeight())
}
func assertOne(from, to string) error {
if from == "" && to == "" {
return errors.Errorf("You must specify either --%s or --%s",
FlagFromChain, FlagToChain)
}
if from != "" && to != "" {
return errors.Errorf("You can only specify one of --%s or --%s",
FlagFromChain, FlagToChain)
}
return nil
}
func packetsQueryCmd(cmd *cobra.Command, args []string) error {
from := viper.GetString(FlagFromChain)
to := viper.GetString(FlagToChain)
err := assertOne(from, to)
if err != nil {
return err
}
var key []byte
if from != "" {
key = stack.PrefixedKey(ibc.NameIBC, ibc.QueueInKey(from))
} else {
key = stack.PrefixedKey(ibc.NameIBC, ibc.QueueOutKey(to))
}
var res uint64
proof, err := proofcmd.GetAndParseAppProof(key, &res)
if err != nil {
return err
}
return proofcmd.OutputProof(res, proof.BlockHeight())
}
func packetQueryCmd(cmd *cobra.Command, args []string) error {
from := viper.GetString(FlagFromChain)
to := viper.GetString(FlagToChain)
err := assertOne(from, to)
if err != nil {
return err
}
seq := viper.GetInt(FlagSequence)
if seq < 0 {
return errors.Errorf("--%s must be a non-negative number", FlagSequence)
}
var key []byte
if from != "" {
key = stack.PrefixedKey(ibc.NameIBC, ibc.QueueInPacketKey(from, uint64(seq)))
} else {
key = stack.PrefixedKey(ibc.NameIBC, ibc.QueueOutPacketKey(to, uint64(seq)))
}
// Input queue just display the results
if from != "" {
var packet ibc.Packet
proof, err := proofcmd.GetAndParseAppProof(key, &packet)
if err != nil {
return err
}
return proofcmd.OutputProof(packet, proof.BlockHeight())
}
// output queue, create a post packet
var packet ibc.Packet
proof, err := proofcmd.GetAndParseAppProof(key, &packet)
if err != nil {
return err
}
// TODO: oh so ugly. fix before merge!
// wait, i want to change go-merkle too....
appProof := proof.(proofs.AppProof)
extractedProof, err := iavl.ReadProof(appProof.Proof)
if err != nil {
return err
}
// create the post packet here.
post := ibc.PostPacketTx{
FromChainID: commands.GetChainID(),
FromChainHeight: proof.BlockHeight(),
Key: key,
Packet: packet,
Proof: extractedProof,
}
// print json direct, as we don't need to wrap with the height
res, err := data.ToJSON(post)
if err != nil {
return err
}
fmt.Println(string(res))
return nil
}

115
modules/ibc/commands/tx.go Normal file
View File

@ -0,0 +1,115 @@
package commands
import (
"encoding/json"
"os"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/tendermint/basecoin/client/commands"
txcmd "github.com/tendermint/basecoin/client/commands/txs"
"github.com/tendermint/basecoin/modules/ibc"
"github.com/tendermint/light-client/certifiers"
)
// RegisterChainTxCmd is CLI command to register a new chain for ibc
var RegisterChainTxCmd = &cobra.Command{
Use: "ibc-register",
Short: "Register a new chain",
RunE: commands.RequireInit(registerChainTxCmd),
}
// UpdateChainTxCmd is CLI command to update the header for an ibc chain
var UpdateChainTxCmd = &cobra.Command{
Use: "ibc-update",
Short: "Add new header to an existing chain",
RunE: commands.RequireInit(updateChainTxCmd),
}
// PostPacketTxCmd is CLI command to post ibc packet on the destination chain
var PostPacketTxCmd = &cobra.Command{
Use: "ibc-post",
Short: "Post an ibc packet on the destination chain",
RunE: commands.RequireInit(postPacketTxCmd),
}
// TODO: relay!
//nolint
const (
FlagSeed = "seed"
FlagPacket = "packet"
)
func init() {
fs1 := RegisterChainTxCmd.Flags()
fs1.String(FlagSeed, "", "Filename with a seed file")
fs2 := UpdateChainTxCmd.Flags()
fs2.String(FlagSeed, "", "Filename with a seed file")
fs3 := PostPacketTxCmd.Flags()
fs3.String(FlagPacket, "", "Filename with a packet to post")
}
func registerChainTxCmd(cmd *cobra.Command, args []string) error {
seed, err := readSeed()
if err != nil {
return err
}
tx := ibc.RegisterChainTx{seed}.Wrap()
return txcmd.DoTx(tx)
}
func updateChainTxCmd(cmd *cobra.Command, args []string) error {
seed, err := readSeed()
if err != nil {
return err
}
tx := ibc.UpdateChainTx{seed}.Wrap()
return txcmd.DoTx(tx)
}
func postPacketTxCmd(cmd *cobra.Command, args []string) error {
post, err := readPostPacket()
if err != nil {
return err
}
return txcmd.DoTx(post.Wrap())
}
func readSeed() (seed certifiers.Seed, err error) {
name := viper.GetString(FlagSeed)
if name == "" {
return seed, errors.New("You must specify a seed file")
}
err = readFile(name, &seed)
return
}
func readPostPacket() (post ibc.PostPacketTx, err error) {
name := viper.GetString(FlagPacket)
if name == "" {
return post, errors.New("You must specify a packet file")
}
err = readFile(name, &post)
return
}
func readFile(name string, input interface{}) (err error) {
var f *os.File
f, err = os.Open(name)
if err != nil {
return errors.WithStack(err)
}
defer f.Close()
// read the file as json into a seed
j := json.NewDecoder(f)
err = j.Decode(input)
return errors.Wrap(err, "Invalid file")
}

106
modules/ibc/errors.go Normal file
View File

@ -0,0 +1,106 @@
package ibc
import (
"fmt"
abci "github.com/tendermint/abci/types"
"github.com/tendermint/basecoin/errors"
)
// nolint
var (
errChainNotRegistered = fmt.Errorf("Chain not registered")
errChainAlreadyExists = fmt.Errorf("Chain already exists")
errWrongDestChain = fmt.Errorf("This is not the destination")
errNeedsIBCPermission = fmt.Errorf("Needs app-permission to send IBC")
errCannotSetPermission = fmt.Errorf("Requesting invalid permission on IBC")
errHeaderNotFound = fmt.Errorf("Header not found")
errPacketAlreadyExists = fmt.Errorf("Packet already handled")
errPacketOutOfOrder = fmt.Errorf("Packet out of order")
errInvalidProof = fmt.Errorf("Invalid merkle proof")
msgInvalidCommit = "Invalid header and commit"
IBCCodeChainNotRegistered = abci.CodeType(1001)
IBCCodeChainAlreadyExists = abci.CodeType(1002)
IBCCodeUnknownChain = abci.CodeType(1003)
IBCCodeInvalidPacketSequence = abci.CodeType(1004)
IBCCodeUnknownHeight = abci.CodeType(1005)
IBCCodeInvalidCommit = abci.CodeType(1006)
IBCCodeInvalidProof = abci.CodeType(1007)
IBCCodeInvalidCall = abci.CodeType(1008)
)
func ErrNotRegistered(chainID string) error {
return errors.WithMessage(chainID, errChainNotRegistered, IBCCodeChainNotRegistered)
}
func IsNotRegisteredErr(err error) bool {
return errors.IsSameError(errChainNotRegistered, err)
}
func ErrAlreadyRegistered(chainID string) error {
return errors.WithMessage(chainID, errChainAlreadyExists, IBCCodeChainAlreadyExists)
}
func IsAlreadyRegisteredErr(err error) bool {
return errors.IsSameError(errChainAlreadyExists, err)
}
func ErrWrongDestChain(chainID string) error {
return errors.WithMessage(chainID, errWrongDestChain, IBCCodeUnknownChain)
}
func IsWrongDestChainErr(err error) bool {
return errors.IsSameError(errWrongDestChain, err)
}
func ErrNeedsIBCPermission() error {
return errors.WithCode(errNeedsIBCPermission, IBCCodeInvalidCall)
}
func IsNeedsIBCPermissionErr(err error) bool {
return errors.IsSameError(errNeedsIBCPermission, err)
}
func ErrCannotSetPermission() error {
return errors.WithCode(errCannotSetPermission, IBCCodeInvalidCall)
}
func IsCannotSetPermissionErr(err error) bool {
return errors.IsSameError(errCannotSetPermission, err)
}
func ErrHeaderNotFound(h int) error {
msg := fmt.Sprintf("height %d", h)
return errors.WithMessage(msg, errHeaderNotFound, IBCCodeUnknownHeight)
}
func IsHeaderNotFoundErr(err error) bool {
return errors.IsSameError(errHeaderNotFound, err)
}
func ErrPacketAlreadyExists() error {
return errors.WithCode(errPacketAlreadyExists, IBCCodeInvalidPacketSequence)
}
func IsPacketAlreadyExistsErr(err error) bool {
return errors.IsSameError(errPacketAlreadyExists, err)
}
func ErrPacketOutOfOrder(seq uint64) error {
msg := fmt.Sprintf("expected %d", seq)
return errors.WithMessage(msg, errPacketOutOfOrder, IBCCodeInvalidPacketSequence)
}
func IsPacketOutOfOrderErr(err error) bool {
return errors.IsSameError(errPacketOutOfOrder, err)
}
func ErrInvalidProof() error {
return errors.WithCode(errInvalidProof, IBCCodeInvalidProof)
}
func IsInvalidProofErr(err error) bool {
return errors.IsSameError(errInvalidProof, err)
}
func ErrInvalidCommit(err error) error {
if err == nil {
return nil
}
return errors.WithMessage(msgInvalidCommit, err, IBCCodeInvalidCommit)
}
func IsInvalidCommitErr(err error) bool {
return errors.HasErrorCode(err, IBCCodeInvalidCommit)
}

208
modules/ibc/handler.go Normal file
View File

@ -0,0 +1,208 @@
package ibc
import (
"fmt"
"github.com/tendermint/go-wire/data"
"github.com/tendermint/tmlibs/log"
"github.com/tendermint/basecoin"
"github.com/tendermint/basecoin/errors"
"github.com/tendermint/basecoin/stack"
"github.com/tendermint/basecoin/state"
)
const (
// NameIBC is the name of this module
NameIBC = "ibc"
// OptionRegistrar is the option name to set the actor
// to handle ibc chain registration
OptionRegistrar = "registrar"
)
var (
// Semi-random bytes that shouldn't conflict with keys (20 bytes)
// or any strings (non-ascii).
// TODO: consider how to make this more collision-proof....
allowIBC = []byte{0x42, 0xbe, 0xef, 0x1}
)
// AllowIBC returns a specially crafted Actor that
// enables sending IBC packets for this app type
func AllowIBC(app string) basecoin.Actor {
return basecoin.Actor{App: app, Address: allowIBC}
}
// Handler updates the chain state or creates an ibc packet
type Handler struct{}
var _ basecoin.Handler = Handler{}
// NewHandler returns a Handler that allows all chains to connect via IBC.
// Set a Registrar via SetOption to restrict it.
func NewHandler() Handler {
return Handler{}
}
// Name returns name space
func (Handler) Name() string {
return NameIBC
}
// SetOption sets the registrar for IBC
func (h Handler) SetOption(l log.Logger, store state.SimpleDB, module, key, value string) (log string, err error) {
if module != NameIBC {
return "", errors.ErrUnknownModule(module)
}
if key == OptionRegistrar {
var act basecoin.Actor
err = data.FromJSON([]byte(value), &act)
if err != nil {
return "", err
}
// Save the data
info := HandlerInfo{act}
info.Save(store)
return "Success", nil
}
return "", errors.ErrUnknownKey(key)
}
// CheckTx verifies the packet is formated correctly, and has the proper sequence
// for a registered chain
func (h Handler) CheckTx(ctx basecoin.Context, store state.SimpleDB, tx basecoin.Tx) (res basecoin.Result, err error) {
err = tx.ValidateBasic()
if err != nil {
return res, err
}
switch t := tx.Unwrap().(type) {
case RegisterChainTx:
return h.initSeed(ctx, store, t)
case UpdateChainTx:
return h.updateSeed(ctx, store, t)
case CreatePacketTx:
return h.createPacket(ctx, store, t)
}
return res, errors.ErrUnknownTxType(tx.Unwrap())
}
// DeliverTx verifies all signatures on the tx and updates the chain state
// apropriately
func (h Handler) DeliverTx(ctx basecoin.Context, store state.SimpleDB, tx basecoin.Tx) (res basecoin.Result, err error) {
err = tx.ValidateBasic()
if err != nil {
return res, err
}
switch t := tx.Unwrap().(type) {
case RegisterChainTx:
return h.initSeed(ctx, store, t)
case UpdateChainTx:
return h.updateSeed(ctx, store, t)
case CreatePacketTx:
return h.createPacket(ctx, store, t)
}
return res, errors.ErrUnknownTxType(tx.Unwrap())
}
// initSeed imports the first seed for this chain and
// accepts it as the root of trust.
//
// only the registrar, if set, is allowed to do this
func (h Handler) initSeed(ctx basecoin.Context, store state.SimpleDB,
t RegisterChainTx) (res basecoin.Result, err error) {
info := LoadInfo(store)
if !info.Registrar.Empty() && !ctx.HasPermission(info.Registrar) {
return res, errors.ErrUnauthorized()
}
// verify that the header looks reasonable
chainID := t.ChainID()
s := NewChainSet(store)
err = s.Register(chainID, ctx.BlockHeight(), t.Seed.Height())
if err != nil {
return res, err
}
space := stack.PrefixedStore(chainID, store)
provider := newDBProvider(space)
err = provider.StoreSeed(t.Seed)
return res, err
}
// updateSeed checks the seed against the existing chain data and rejects it if it
// doesn't fit (or no chain data)
func (h Handler) updateSeed(ctx basecoin.Context, store state.SimpleDB,
t UpdateChainTx) (res basecoin.Result, err error) {
chainID := t.ChainID()
s := NewChainSet(store)
if !s.Exists([]byte(chainID)) {
return res, ErrNotRegistered(chainID)
}
// load the certifier for this chain
seed := t.Seed
space := stack.PrefixedStore(chainID, store)
cert, err := newCertifier(space, chainID, seed.Height())
if err != nil {
return res, err
}
// this will import the seed if it is valid in the current context
err = cert.Update(seed.Checkpoint, seed.Validators)
if err != nil {
return res, ErrInvalidCommit(err)
}
// update the tracked height in chain info
err = s.Update(chainID, t.Seed.Height())
return res, err
}
// createPacket makes sure all permissions are good and the destination
// chain is registed. If so, it appends it to the outgoing queue
func (h Handler) createPacket(ctx basecoin.Context, store state.SimpleDB,
t CreatePacketTx) (res basecoin.Result, err error) {
// make sure the chain is registed
dest := t.DestChain
if !NewChainSet(store).Exists([]byte(dest)) {
return res, ErrNotRegistered(dest)
}
// make sure we have the special IBC permission
mod, err := t.Tx.GetMod()
if err != nil {
return res, err
}
if !ctx.HasPermission(AllowIBC(mod)) {
return res, ErrNeedsIBCPermission()
}
// start making the packet to send
packet := Packet{
DestChain: dest,
Tx: t.Tx,
Permissions: make([]basecoin.Actor, len(t.Permissions)),
}
// make sure we have all the permissions we want to send
for i, p := range t.Permissions {
if !ctx.HasPermission(p) {
return res, ErrCannotSetPermission()
}
// add the permission with the current ChainID
packet.Permissions[i] = p.WithChain(ctx.ChainID())
}
// now add it to the output queue....
q := OutputQueue(store, dest)
packet.Sequence = q.Tail()
q.Push(packet.Bytes())
res = basecoin.Result{Log: fmt.Sprintf("Packet %s %d", dest, packet.Sequence)}
return
}

413
modules/ibc/ibc_test.go Normal file
View File

@ -0,0 +1,413 @@
package ibc
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
wire "github.com/tendermint/go-wire"
"github.com/tendermint/light-client/certifiers"
"github.com/tendermint/tmlibs/log"
"github.com/tendermint/basecoin"
"github.com/tendermint/basecoin/errors"
"github.com/tendermint/basecoin/stack"
"github.com/tendermint/basecoin/state"
)
// this tests registration without registrar permissions
func TestIBCRegister(t *testing.T) {
assert := assert.New(t)
// the validators we use to make seeds
keys := certifiers.GenValKeys(5)
keys2 := certifiers.GenValKeys(7)
appHash := []byte{0, 4, 7, 23}
appHash2 := []byte{12, 34, 56, 78}
// badSeed doesn't validate
badSeed := genEmptySeed(keys2, "chain-2", 123, appHash, len(keys2))
badSeed.Header.AppHash = appHash2
cases := []struct {
seed certifiers.Seed
checker errors.CheckErr
}{
{
genEmptySeed(keys, "chain-1", 100, appHash, len(keys)),
errors.NoErr,
},
{
genEmptySeed(keys, "chain-1", 200, appHash, len(keys)),
IsAlreadyRegisteredErr,
},
{
badSeed,
IsInvalidCommitErr,
},
{
genEmptySeed(keys2, "chain-2", 123, appHash2, 5),
errors.NoErr,
},
}
ctx := stack.MockContext("hub", 50)
store := state.NewMemKVStore()
app := stack.New().Dispatch(stack.WrapHandler(NewHandler()))
for i, tc := range cases {
tx := RegisterChainTx{tc.seed}.Wrap()
_, err := app.DeliverTx(ctx, store, tx)
assert.True(tc.checker(err), "%d: %+v", i, err)
}
}
// this tests permission controls on ibc registration
func TestIBCRegisterPermissions(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
// the validators we use to make seeds
keys := certifiers.GenValKeys(4)
appHash := []byte{0x17, 0x21, 0x5, 0x1e}
foobar := basecoin.Actor{App: "foo", Address: []byte("bar")}
baz := basecoin.Actor{App: "baz", Address: []byte("bar")}
foobaz := basecoin.Actor{App: "foo", Address: []byte("baz")}
cases := []struct {
seed certifiers.Seed
registrar basecoin.Actor
signer basecoin.Actor
checker errors.CheckErr
}{
// no sig, no registrar
{
seed: genEmptySeed(keys, "chain-1", 100, appHash, len(keys)),
checker: errors.NoErr,
},
// sig, no registrar
{
seed: genEmptySeed(keys, "chain-2", 100, appHash, len(keys)),
signer: foobaz,
checker: errors.NoErr,
},
// registrar, no sig
{
seed: genEmptySeed(keys, "chain-3", 100, appHash, len(keys)),
registrar: foobar,
checker: errors.IsUnauthorizedErr,
},
// registrar, wrong sig
{
seed: genEmptySeed(keys, "chain-4", 100, appHash, len(keys)),
signer: foobaz,
registrar: foobar,
checker: errors.IsUnauthorizedErr,
},
// registrar, wrong sig
{
seed: genEmptySeed(keys, "chain-5", 100, appHash, len(keys)),
signer: baz,
registrar: foobar,
checker: errors.IsUnauthorizedErr,
},
// registrar, proper sig
{
seed: genEmptySeed(keys, "chain-6", 100, appHash, len(keys)),
signer: foobar,
registrar: foobar,
checker: errors.NoErr,
},
}
store := state.NewMemKVStore()
app := stack.New().Dispatch(stack.WrapHandler(NewHandler()))
for i, tc := range cases {
// set option specifies the registrar
msg, err := json.Marshal(tc.registrar)
require.Nil(err, "%+v", err)
_, err = app.SetOption(log.NewNopLogger(), store,
NameIBC, OptionRegistrar, string(msg))
require.Nil(err, "%+v", err)
// add permissions to the context
ctx := stack.MockContext("hub", 50).WithPermissions(tc.signer)
tx := RegisterChainTx{tc.seed}.Wrap()
_, err = app.DeliverTx(ctx, store, tx)
assert.True(tc.checker(err), "%d: %+v", i, err)
}
}
// this verifies that we can properly update the headers on the chain
func TestIBCUpdate(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
// this is the root seed, that others are evaluated against
keys := certifiers.GenValKeys(7)
appHash := []byte{0, 4, 7, 23}
start := 100 // initial height
root := genEmptySeed(keys, "chain-1", 100, appHash, len(keys))
keys2 := keys.Extend(2)
keys3 := keys2.Extend(2)
// create the app and register the root of trust (for chain-1)
ctx := stack.MockContext("hub", 50)
store := state.NewMemKVStore()
app := stack.New().Dispatch(stack.WrapHandler(NewHandler()))
tx := RegisterChainTx{root}.Wrap()
_, err := app.DeliverTx(ctx, store, tx)
require.Nil(err, "%+v", err)
cases := []struct {
seed certifiers.Seed
checker errors.CheckErr
}{
// same validator, higher up
{
genEmptySeed(keys, "chain-1", start+50, []byte{22}, len(keys)),
errors.NoErr,
},
// same validator, between existing (not most recent)
{
genEmptySeed(keys, "chain-1", start+5, []byte{15, 43}, len(keys)),
errors.NoErr,
},
// same validators, before root of trust
{
genEmptySeed(keys, "chain-1", start-8, []byte{11, 77}, len(keys)),
IsHeaderNotFoundErr,
},
// insufficient signatures
{
genEmptySeed(keys, "chain-1", start+60, []byte{24}, len(keys)/2),
IsInvalidCommitErr,
},
// unregistered chain
{
genEmptySeed(keys, "chain-2", start+60, []byte{24}, len(keys)/2),
IsNotRegisteredErr,
},
// too much change (keys -> keys3)
{
genEmptySeed(keys3, "chain-1", start+100, []byte{22}, len(keys3)),
IsInvalidCommitErr,
},
// legit update to validator set (keys -> keys2)
{
genEmptySeed(keys2, "chain-1", start+90, []byte{33}, len(keys2)),
errors.NoErr,
},
// now impossible jump works (keys -> keys2 -> keys3)
{
genEmptySeed(keys3, "chain-1", start+100, []byte{44}, len(keys3)),
errors.NoErr,
},
}
for i, tc := range cases {
tx := UpdateChainTx{tc.seed}.Wrap()
_, err := app.DeliverTx(ctx, store, tx)
assert.True(tc.checker(err), "%d: %+v", i, err)
}
}
// try to create an ibc packet and verify the number we get back
func TestIBCCreatePacket(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
// this is the root seed, that others are evaluated against
keys := certifiers.GenValKeys(7)
appHash := []byte{1, 2, 3, 4}
start := 100 // initial height
chainID := "cosmos-hub"
root := genEmptySeed(keys, chainID, start, appHash, len(keys))
// create the app and register the root of trust (for chain-1)
ctx := stack.MockContext("hub", 50)
store := state.NewMemKVStore()
app := stack.New().Dispatch(stack.WrapHandler(NewHandler()))
tx := RegisterChainTx{root}.Wrap()
_, err := app.DeliverTx(ctx, store, tx)
require.Nil(err, "%+v", err)
// this is the tx we send, and the needed permission to send it
raw := stack.NewRawTx([]byte{0xbe, 0xef})
ibcPerm := AllowIBC(stack.NameOK)
somePerm := basecoin.Actor{App: "some", Address: []byte("perm")}
cases := []struct {
dest string
ibcPerms basecoin.Actors
ctxPerms basecoin.Actors
checker errors.CheckErr
}{
// wrong chain -> error
{
dest: "some-other-chain",
ctxPerms: basecoin.Actors{ibcPerm},
checker: IsNotRegisteredErr,
},
// no ibc permission -> error
{
dest: chainID,
checker: IsNeedsIBCPermissionErr,
},
// correct -> nice sequence
{
dest: chainID,
ctxPerms: basecoin.Actors{ibcPerm},
checker: errors.NoErr,
},
// requesting invalid permissions -> error
{
dest: chainID,
ibcPerms: basecoin.Actors{somePerm},
ctxPerms: basecoin.Actors{ibcPerm},
checker: IsCannotSetPermissionErr,
},
// requesting extra permissions when present
{
dest: chainID,
ibcPerms: basecoin.Actors{somePerm},
ctxPerms: basecoin.Actors{ibcPerm, somePerm},
checker: errors.NoErr,
},
}
for i, tc := range cases {
tx := CreatePacketTx{
DestChain: tc.dest,
Permissions: tc.ibcPerms,
Tx: raw,
}.Wrap()
myCtx := ctx.WithPermissions(tc.ctxPerms...)
_, err = app.DeliverTx(myCtx, store, tx)
assert.True(tc.checker(err), "%d: %+v", i, err)
}
// query packet state - make sure both packets are properly writen
p := stack.PrefixedStore(NameIBC, store)
q := OutputQueue(p, chainID)
if assert.Equal(2, q.Size()) {
expected := []struct {
seq uint64
perm basecoin.Actors
}{
{0, nil},
{1, basecoin.Actors{somePerm}},
}
for _, tc := range expected {
var packet Packet
err = wire.ReadBinaryBytes(q.Pop(), &packet)
require.Nil(err, "%+v", err)
assert.Equal(chainID, packet.DestChain)
assert.EqualValues(tc.seq, packet.Sequence)
assert.Equal(raw, packet.Tx)
assert.Equal(len(tc.perm), len(packet.Permissions))
}
}
}
func TestIBCPostPacket(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
otherID := "chain-1"
ourID := "hub"
start := 200
msg := "it's okay"
// create the app and our chain
app := stack.New().
IBC(NewMiddleware()).
Dispatch(
stack.WrapHandler(NewHandler()),
stack.WrapHandler(stack.OKHandler{Log: msg}),
)
ourChain := NewAppChain(app, ourID)
// set up the other chain and register it with us
otherChain := NewMockChain(otherID, 7)
registerTx := otherChain.GetRegistrationTx(start).Wrap()
_, err := ourChain.DeliverTx(registerTx)
require.Nil(err, "%+v", err)
// make a random tx that is to be passed
rawTx := stack.NewRawTx([]byte{17, 24, 3, 8})
randomChain := NewMockChain("something-else", 4)
pbad := NewPacket(rawTx, "something-else", 0)
packetBad, _ := randomChain.MakePostPacket(pbad, 123)
p0 := NewPacket(rawTx, ourID, 0)
packet0, update0 := otherChain.MakePostPacket(p0, start+5)
require.Nil(ourChain.Update(update0))
packet0badHeight := packet0
packet0badHeight.FromChainHeight -= 2
theirActor := basecoin.Actor{ChainID: otherID, App: "foo", Address: []byte{1}}
p1 := NewPacket(rawTx, ourID, 1, theirActor)
packet1, update1 := otherChain.MakePostPacket(p1, start+25)
require.Nil(ourChain.Update(update1))
packet1badProof := packet1
packet1badProof.Key = []byte("random-data")
ourActor := basecoin.Actor{ChainID: ourID, App: "bar", Address: []byte{2}}
p2 := NewPacket(rawTx, ourID, 2, ourActor)
packet2, update2 := otherChain.MakePostPacket(p2, start+50)
require.Nil(ourChain.Update(update2))
ibcPerm := basecoin.Actors{AllowIBC(stack.NameOK)}
cases := []struct {
packet PostPacketTx
permissions basecoin.Actors
checker errors.CheckErr
}{
// bad chain -> error
{packetBad, ibcPerm, IsNotRegisteredErr},
// no matching header -> error
{packet0badHeight, nil, IsHeaderNotFoundErr},
// out of order -> error
{packet1, ibcPerm, IsPacketOutOfOrderErr},
// all good -> execute tx
{packet0, ibcPerm, errors.NoErr},
// bad proof -> error
{packet1badProof, ibcPerm, IsInvalidProofErr},
// all good -> execute tx (no special permission needed)
{packet1, nil, errors.NoErr},
// repeat -> error
{packet0, nil, IsPacketAlreadyExistsErr},
// packet2 contains invalid permissions
{packet2, nil, IsCannotSetPermissionErr},
}
for i, tc := range cases {
res, err := ourChain.DeliverTx(tc.packet.Wrap(), tc.permissions...)
assert.True(tc.checker(err), "%d: %+v", i, err)
if err == nil {
assert.Equal(msg, res.Log)
}
}
}

60
modules/ibc/keys.go Normal file
View File

@ -0,0 +1,60 @@
package ibc
import (
"github.com/tendermint/basecoin/stack"
"github.com/tendermint/basecoin/state"
)
const (
// this is the prefix for the list of chains
// we otherwise use the chainid as prefix, so this must not be an
// alpha-numeric byte
prefixChains = "**"
prefixInput = "i"
prefixOutput = "o"
)
// HandlerKey is used for the global permission info
func HandlerKey() []byte {
return []byte{0x2}
}
// ChainsKey is the key to get info on all chains
func ChainsKey() []byte {
return stack.PrefixedKey(prefixChains, state.SetKey())
}
// ChainKey is the key to get info on one chain
func ChainKey(chainID string) []byte {
bkey := state.MakeBKey([]byte(chainID))
return stack.PrefixedKey(prefixChains, bkey)
}
// QueueInKey is the key to get newest of the input queue from this chain
func QueueInKey(chainID string) []byte {
return stack.PrefixedKey(chainID,
stack.PrefixedKey(prefixInput,
state.QueueTailKey()))
}
// QueueOutKey is the key to get v of the output queue from this chain
func QueueOutKey(chainID string) []byte {
return stack.PrefixedKey(chainID,
stack.PrefixedKey(prefixOutput,
state.QueueTailKey()))
}
// QueueInPacketKey is the key to get given packet from this chain's input queue
func QueueInPacketKey(chainID string, seq uint64) []byte {
return stack.PrefixedKey(chainID,
stack.PrefixedKey(prefixInput,
state.QueueItemKey(seq)))
}
// QueueOutPacketKey is the key to get given packet from this chain's output queue
func QueueOutPacketKey(chainID string, seq uint64) []byte {
return stack.PrefixedKey(chainID,
stack.PrefixedKey(prefixOutput,
state.QueueItemKey(seq)))
}

116
modules/ibc/middleware.go Normal file
View File

@ -0,0 +1,116 @@
package ibc
import (
"github.com/tendermint/basecoin"
"github.com/tendermint/basecoin/stack"
"github.com/tendermint/basecoin/state"
)
// Middleware allows us to verify the IBC proof on a packet and
// and if valid, attach this permission to the wrapped packet
type Middleware struct {
stack.PassOption
}
var _ stack.Middleware = Middleware{}
// NewMiddleware creates a role-checking middleware
func NewMiddleware() Middleware {
return Middleware{}
}
// Name - return name space
func (Middleware) Name() string {
return NameIBC
}
// CheckTx verifies the named chain and height is present, and verifies
// the merkle proof in the packet
func (m Middleware) CheckTx(ctx basecoin.Context, store state.SimpleDB, tx basecoin.Tx, next basecoin.Checker) (res basecoin.Result, err error) {
// if it is not a PostPacket, just let it go through
post, ok := tx.Unwrap().(PostPacketTx)
if !ok {
return next.CheckTx(ctx, store, tx)
}
// parse this packet and get the ibc-enhanced tx and context
ictx, itx, err := m.verifyPost(ctx, store, post)
if err != nil {
return res, err
}
return next.CheckTx(ictx, store, itx)
}
// DeliverTx verifies the named chain and height is present, and verifies
// the merkle proof in the packet
func (m Middleware) DeliverTx(ctx basecoin.Context, store state.SimpleDB, tx basecoin.Tx, next basecoin.Deliver) (res basecoin.Result, err error) {
// if it is not a PostPacket, just let it go through
post, ok := tx.Unwrap().(PostPacketTx)
if !ok {
return next.DeliverTx(ctx, store, tx)
}
// parse this packet and get the ibc-enhanced tx and context
ictx, itx, err := m.verifyPost(ctx, store, post)
if err != nil {
return res, err
}
return next.DeliverTx(ictx, store, itx)
}
// verifyPost accepts a message bound for this chain...
// TODO: think about relay
func (m Middleware) verifyPost(ctx basecoin.Context, store state.SimpleDB,
tx PostPacketTx) (ictx basecoin.Context, itx basecoin.Tx, err error) {
// make sure the chain is registered
from := tx.FromChainID
if !NewChainSet(store).Exists([]byte(from)) {
return ictx, itx, ErrNotRegistered(from)
}
// TODO: how to deal with routing/relaying???
packet := tx.Packet
if packet.DestChain != ctx.ChainID() {
return ictx, itx, ErrWrongDestChain(packet.DestChain)
}
// verify packet.Permissions all come from the other chain
if !packet.Permissions.AllHaveChain(tx.FromChainID) {
return ictx, itx, ErrCannotSetPermission()
}
// make sure this sequence number is the next in the list
q := InputQueue(store, from)
tail := q.Tail()
if packet.Sequence < tail {
return ictx, itx, ErrPacketAlreadyExists()
}
if packet.Sequence > tail {
return ictx, itx, ErrPacketOutOfOrder(tail)
}
// look up the referenced header
space := stack.PrefixedStore(from, store)
provider := newDBProvider(space)
seed, err := provider.GetExactHeight(int(tx.FromChainHeight))
if err != nil {
return ictx, itx, err
}
// verify the merkle hash....
root := seed.Header.AppHash
pBytes := packet.Bytes()
valid := tx.Proof.Verify(tx.Key, pBytes, root)
if !valid {
return ictx, itx, ErrInvalidProof()
}
// add to input queue
q.Push(pBytes)
// return the wrapped tx along with the extra permissions
ictx = ctx.WithPermissions(packet.Permissions...)
itx = packet.Tx
return
}

95
modules/ibc/provider.go Normal file
View File

@ -0,0 +1,95 @@
package ibc
import (
wire "github.com/tendermint/go-wire"
"github.com/tendermint/light-client/certifiers"
"github.com/tendermint/basecoin/stack"
"github.com/tendermint/basecoin/state"
)
const (
prefixHash = "v"
prefixHeight = "h"
prefixPacket = "p"
)
// newCertifier loads up the current state of this chain to make a proper certifier
// it will load the most recent height before block h if h is positive
// if h < 0, it will load the latest height
func newCertifier(store state.SimpleDB, chainID string, h int) (*certifiers.InquiringCertifier, error) {
// each chain has their own prefixed subspace
p := newDBProvider(store)
var seed certifiers.Seed
var err error
if h > 0 {
// this gets the most recent verified seed below the specified height
seed, err = p.GetByHeight(h)
} else {
// 0 or negative means start at latest seed
seed, err = certifiers.LatestSeed(p)
}
if err != nil {
return nil, ErrHeaderNotFound(h)
}
// we have no source for untrusted keys, but use the db to load trusted history
cert := certifiers.NewInquiring(chainID, seed, p,
certifiers.MissingProvider{})
return cert, nil
}
// dbProvider wraps our kv store so it integrates with light-client verification
type dbProvider struct {
byHash state.SimpleDB
byHeight *state.Span
}
func newDBProvider(store state.SimpleDB) *dbProvider {
return &dbProvider{
byHash: stack.PrefixedStore(prefixHash, store),
byHeight: state.NewSpan(stack.PrefixedStore(prefixHeight, store)),
}
}
var _ certifiers.Provider = &dbProvider{}
func (d *dbProvider) StoreSeed(seed certifiers.Seed) error {
// TODO: don't duplicate data....
b := wire.BinaryBytes(seed)
d.byHash.Set(seed.Hash(), b)
d.byHeight.Set(uint64(seed.Height()), b)
return nil
}
func (d *dbProvider) GetByHeight(h int) (seed certifiers.Seed, err error) {
b, _ := d.byHeight.LTE(uint64(h))
if b == nil {
return seed, certifiers.ErrSeedNotFound()
}
err = wire.ReadBinaryBytes(b, &seed)
return
}
func (d *dbProvider) GetByHash(hash []byte) (seed certifiers.Seed, err error) {
b := d.byHash.Get(hash)
if b == nil {
return seed, certifiers.ErrSeedNotFound()
}
err = wire.ReadBinaryBytes(b, &seed)
return
}
// GetExactHeight is like GetByHeight, but returns an error instead of
// closest match if there is no exact match
func (d *dbProvider) GetExactHeight(h int) (seed certifiers.Seed, err error) {
seed, err = d.GetByHeight(h)
if err != nil {
return
}
if seed.Height() != h {
err = ErrHeaderNotFound(h)
}
return
}

View File

@ -0,0 +1,136 @@
package ibc
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tendermint/basecoin/state"
"github.com/tendermint/light-client/certifiers"
)
func assertSeedEqual(t *testing.T, s, s2 certifiers.Seed) {
assert := assert.New(t)
assert.Equal(s.Height(), s2.Height())
assert.Equal(s.Hash(), s2.Hash())
// TODO: more
}
func TestProviderStore(t *testing.T) {
assert, require := assert.New(t), require.New(t)
// make a few seeds
keys := certifiers.GenValKeys(2)
seeds := makeSeeds(keys, 4, "some-chain", "demo-store")
// make a provider
store := state.NewMemKVStore()
p := newDBProvider(store)
// check it...
_, err := p.GetByHeight(20)
require.NotNil(err)
assert.True(certifiers.IsSeedNotFoundErr(err))
// add a seed
for _, s := range seeds {
err = p.StoreSeed(s)
require.Nil(err)
}
// make sure we get it...
s := seeds[0]
val, err := p.GetByHeight(s.Height())
if assert.Nil(err) {
assertSeedEqual(t, s, val)
}
// make sure we get higher
val, err = p.GetByHeight(s.Height() + 2)
if assert.Nil(err) {
assertSeedEqual(t, s, val)
}
// below is nothing
_, err = p.GetByHeight(s.Height() - 2)
assert.True(certifiers.IsSeedNotFoundErr(err))
// make sure we get highest
val, err = certifiers.LatestSeed(p)
if assert.Nil(err) {
assertSeedEqual(t, seeds[3], val)
}
// make sure by hash also (note all have same hash, so overwritten)
val, err = p.GetByHash(seeds[1].Hash())
if assert.Nil(err) {
assertSeedEqual(t, seeds[3], val)
}
}
func TestDBProvider(t *testing.T) {
store := state.NewMemKVStore()
p := newDBProvider(store)
checkProvider(t, p, "test-db", "bling")
}
func makeSeeds(keys certifiers.ValKeys, count int, chainID, app string) []certifiers.Seed {
appHash := []byte(app)
seeds := make([]certifiers.Seed, count)
for i := 0; i < count; i++ {
// two seeds for each validator, to check how we handle dups
// (10, 0), (10, 1), (10, 1), (10, 2), (10, 2), ...
vals := keys.ToValidators(10, int64(count/2))
h := 20 + 10*i
check := keys.GenCheckpoint(chainID, h, nil, vals, appHash, 0, len(keys))
seeds[i] = certifiers.Seed{check, vals}
}
return seeds
}
func checkProvider(t *testing.T, p certifiers.Provider, chainID, app string) {
assert, require := assert.New(t), require.New(t)
keys := certifiers.GenValKeys(5)
count := 10
// make a bunch of seeds...
seeds := makeSeeds(keys, count, chainID, app)
// check provider is empty
seed, err := p.GetByHeight(20)
require.NotNil(err)
assert.True(certifiers.IsSeedNotFoundErr(err))
seed, err = p.GetByHash(seeds[3].Hash())
require.NotNil(err)
assert.True(certifiers.IsSeedNotFoundErr(err))
// now add them all to the provider
for _, s := range seeds {
err = p.StoreSeed(s)
require.Nil(err)
// and make sure we can get it back
s2, err := p.GetByHash(s.Hash())
assert.Nil(err)
assertSeedEqual(t, s, s2)
// by height as well
s2, err = p.GetByHeight(s.Height())
assert.Nil(err)
assertSeedEqual(t, s, s2)
}
// make sure we get the last hash if we overstep
seed, err = p.GetByHeight(5000)
if assert.Nil(err) {
assertSeedEqual(t, seeds[count-1], seed)
}
// and middle ones as well
seed, err = p.GetByHeight(47)
if assert.Nil(err) {
// we only step by 10, so 40 must be the one below this
assert.Equal(40, seed.Height())
}
}

122
modules/ibc/store.go Normal file
View File

@ -0,0 +1,122 @@
package ibc
import (
"github.com/tendermint/basecoin"
"github.com/tendermint/basecoin/stack"
"github.com/tendermint/basecoin/state"
wire "github.com/tendermint/go-wire"
)
// HandlerInfo is the global state of the ibc.Handler
type HandlerInfo struct {
Registrar basecoin.Actor `json:"registrar"`
}
// Save the HandlerInfo to the store
func (h HandlerInfo) Save(store state.SimpleDB) {
b := wire.BinaryBytes(h)
store.Set(HandlerKey(), b)
}
// LoadInfo loads the HandlerInfo from the data store
func LoadInfo(store state.SimpleDB) (h HandlerInfo) {
b := store.Get(HandlerKey())
if len(b) > 0 {
wire.ReadBinaryBytes(b, &h)
}
return
}
// ChainInfo is the global info we store for each registered chain,
// besides the headers, proofs, and packets
type ChainInfo struct {
RegisteredAt uint64 `json:"registered_at"`
RemoteBlock int `json:"remote_block"`
}
// ChainSet is the set of all registered chains
type ChainSet struct {
*state.Set
}
// NewChainSet loads or initialized the ChainSet
func NewChainSet(store state.SimpleDB) ChainSet {
space := stack.PrefixedStore(prefixChains, store)
return ChainSet{
Set: state.NewSet(space),
}
}
// Register adds the named chain with some info
// returns error if already present
func (c ChainSet) Register(chainID string, ourHeight uint64, theirHeight int) error {
if c.Exists([]byte(chainID)) {
return ErrAlreadyRegistered(chainID)
}
info := ChainInfo{
RegisteredAt: ourHeight,
RemoteBlock: theirHeight,
}
data := wire.BinaryBytes(info)
c.Set.Set([]byte(chainID), data)
return nil
}
// Update sets the new tracked height on this chain
// returns error if not present
func (c ChainSet) Update(chainID string, theirHeight int) error {
d := c.Set.Get([]byte(chainID))
if len(d) == 0 {
return ErrNotRegistered(chainID)
}
// load the data
var info ChainInfo
err := wire.ReadBinaryBytes(d, &info)
if err != nil {
return err
}
// change the remote block and save it
info.RemoteBlock = theirHeight
d = wire.BinaryBytes(info)
c.Set.Set([]byte(chainID), d)
return nil
}
// Packet is a wrapped transaction and permission that we want to
// send off to another chain.
type Packet struct {
DestChain string `json:"dest_chain"`
Sequence uint64 `json:"sequence"`
Permissions basecoin.Actors `json:"permissions"`
Tx basecoin.Tx `json:"tx"`
}
// NewPacket creates a new outgoing packet
func NewPacket(tx basecoin.Tx, dest string, seq uint64, perm ...basecoin.Actor) Packet {
return Packet{
DestChain: dest,
Sequence: seq,
Permissions: perm,
Tx: tx,
}
}
// Bytes returns a serialization of the Packet
func (p Packet) Bytes() []byte {
return wire.BinaryBytes(p)
}
// InputQueue returns the queue of input packets from this chain
func InputQueue(store state.SimpleDB, chainID string) *state.Queue {
ch := stack.PrefixedStore(chainID, store)
space := stack.PrefixedStore(prefixInput, ch)
return state.NewQueue(space)
}
// OutputQueue returns the queue of output packets destined for this chain
func OutputQueue(store state.SimpleDB, chainID string) *state.Queue {
ch := stack.PrefixedStore(chainID, store)
space := stack.PrefixedStore(prefixOutput, ch)
return state.NewQueue(space)
}

129
modules/ibc/test_helpers.go Normal file
View File

@ -0,0 +1,129 @@
package ibc
import (
"fmt"
"github.com/tendermint/light-client/certifiers"
"github.com/tendermint/merkleeyes/iavl"
"github.com/tendermint/tmlibs/log"
"github.com/tendermint/basecoin"
"github.com/tendermint/basecoin/stack"
"github.com/tendermint/basecoin/state"
)
// MockChain is used to simulate a chain for ibc tests.
// It is able to produce ibc packets and all verification for
// them, but cannot respond to any responses.
type MockChain struct {
keys certifiers.ValKeys
chainID string
tree *iavl.IAVLTree
}
// NewMockChain initializes a teststore and test validators
func NewMockChain(chainID string, numKeys int) MockChain {
return MockChain{
keys: certifiers.GenValKeys(numKeys),
chainID: chainID,
tree: iavl.NewIAVLTree(0, nil),
}
}
// GetRegistrationTx returns a valid tx to register this chain
func (m MockChain) GetRegistrationTx(h int) RegisterChainTx {
seed := genEmptySeed(m.keys, m.chainID, h, m.tree.Hash(), len(m.keys))
return RegisterChainTx{seed}
}
// MakePostPacket commits the packet locally and returns the proof,
// in the form of two packets to update the header and prove this packet.
func (m MockChain) MakePostPacket(packet Packet, h int) (
PostPacketTx, UpdateChainTx) {
post := makePostPacket(m.tree, packet, m.chainID, h)
seed := genEmptySeed(m.keys, m.chainID, h, m.tree.Hash(), len(m.keys))
update := UpdateChainTx{seed}
return post, update
}
func genEmptySeed(keys certifiers.ValKeys, chain string, h int,
appHash []byte, count int) certifiers.Seed {
vals := keys.ToValidators(10, 0)
cp := keys.GenCheckpoint(chain, h, nil, vals, appHash, 0, count)
return certifiers.Seed{cp, vals}
}
func makePostPacket(tree *iavl.IAVLTree, packet Packet, fromID string, fromHeight int) PostPacketTx {
key := []byte(fmt.Sprintf("some-long-prefix-%06d", packet.Sequence))
tree.Set(key, packet.Bytes())
_, proof := tree.ConstructProof(key)
if proof == nil {
panic("wtf?")
}
return PostPacketTx{
FromChainID: fromID,
FromChainHeight: uint64(fromHeight),
Proof: proof,
Key: key,
Packet: packet,
}
}
// AppChain is ready to handle tx
type AppChain struct {
chainID string
app basecoin.Handler
store state.SimpleDB
height int
}
// NewAppChain returns a chain that is ready to respond to tx
func NewAppChain(app basecoin.Handler, chainID string) *AppChain {
return &AppChain{
chainID: chainID,
app: app,
store: state.NewMemKVStore(),
height: 123,
}
}
// IncrementHeight allows us to jump heights, more than the auto-step
// of 1. It returns the new height we are at.
func (a *AppChain) IncrementHeight(delta int) int {
a.height += delta
return a.height
}
// DeliverTx runs the tx and commits the new tree, incrementing height
// by one.
func (a *AppChain) DeliverTx(tx basecoin.Tx, perms ...basecoin.Actor) (basecoin.Result, error) {
ctx := stack.MockContext(a.chainID, uint64(a.height)).WithPermissions(perms...)
store := a.store.Checkpoint()
res, err := a.app.DeliverTx(ctx, store, tx)
if err == nil {
// commit data on success
a.store.Commit(store)
}
return res, err
}
// Update is a shortcut to DeliverTx with this. Also one return value
// to test inline
func (a *AppChain) Update(tx UpdateChainTx) error {
_, err := a.DeliverTx(tx.Wrap())
return err
}
// SetOption sets the option on our app
func (a *AppChain) SetOption(mod, key, value string) (string, error) {
return a.app.SetOption(log.NewNopLogger(), a.store, mod, key, value)
}
// GetStore is used to get the app-specific sub-store
func (a *AppChain) GetStore(app string) state.SimpleDB {
return stack.PrefixedStore(app, a.store)
}

132
modules/ibc/tx.go Normal file
View File

@ -0,0 +1,132 @@
package ibc
import (
"github.com/tendermint/go-wire/data"
"github.com/tendermint/light-client/certifiers"
merkle "github.com/tendermint/merkleeyes/iavl"
"github.com/tendermint/basecoin"
)
// nolint
const (
// 0x3? series for ibc
ByteRegisterChain = byte(0x30)
ByteUpdateChain = byte(0x31)
ByteCreatePacket = byte(0x32)
BytePostPacket = byte(0x33)
TypeRegisterChain = NameIBC + "/register"
TypeUpdateChain = NameIBC + "/update"
TypeCreatePacket = NameIBC + "/create"
TypePostPacket = NameIBC + "/post"
)
func init() {
basecoin.TxMapper.
RegisterImplementation(RegisterChainTx{}, TypeRegisterChain, ByteRegisterChain).
RegisterImplementation(UpdateChainTx{}, TypeUpdateChain, ByteUpdateChain).
RegisterImplementation(CreatePacketTx{}, TypeCreatePacket, ByteCreatePacket).
RegisterImplementation(PostPacketTx{}, TypePostPacket, BytePostPacket)
}
// RegisterChainTx allows you to register a new chain on this blockchain
type RegisterChainTx struct {
Seed certifiers.Seed `json:"seed"`
}
// ChainID helps get the chain this tx refers to
func (r RegisterChainTx) ChainID() string {
return r.Seed.Header.ChainID
}
// ValidateBasic makes sure this is consistent, without checking the sigs
func (r RegisterChainTx) ValidateBasic() error {
err := r.Seed.ValidateBasic(r.ChainID())
if err != nil {
err = ErrInvalidCommit(err)
}
return err
}
// Wrap - used to satisfy TxInner
func (r RegisterChainTx) Wrap() basecoin.Tx {
return basecoin.Tx{r}
}
// UpdateChainTx updates the state of this chain
type UpdateChainTx struct {
Seed certifiers.Seed `json:"seed"`
}
// ChainID helps get the chain this tx refers to
func (u UpdateChainTx) ChainID() string {
return u.Seed.Header.ChainID
}
// ValidateBasic makes sure this is consistent, without checking the sigs
func (u UpdateChainTx) ValidateBasic() error {
err := u.Seed.ValidateBasic(u.ChainID())
if err != nil {
err = ErrInvalidCommit(err)
}
return err
}
// Wrap - used to satisfy TxInner
func (u UpdateChainTx) Wrap() basecoin.Tx {
return basecoin.Tx{u}
}
// CreatePacketTx is meant to be called by IPC, another module...
//
// this is the tx that will be sent to another app and the permissions it
// comes with (which must be a subset of the permissions on the current tx)
//
// If must have the special `AllowIBC` permission from the app
// that can send this packet (so only coins can request SendTx packet)
type CreatePacketTx struct {
DestChain string `json:"dest_chain"`
Permissions basecoin.Actors `json:"permissions"`
Tx basecoin.Tx `json:"tx"`
}
// ValidateBasic makes sure this is consistent - used to satisfy TxInner
func (p CreatePacketTx) ValidateBasic() error {
if p.DestChain == "" {
return ErrWrongDestChain(p.DestChain)
}
return nil
}
// Wrap - used to satisfy TxInner
func (p CreatePacketTx) Wrap() basecoin.Tx {
return basecoin.Tx{p}
}
// PostPacketTx takes a wrapped packet from another chain and
// TODO!!!
// also think... which chains can relay packets???
// right now, enforce that these packets are only sent directly,
// not routed over the hub. add routing later.
type PostPacketTx struct {
// The immediate source of the packet, not always Packet.SrcChainID
FromChainID string `json:"src_chain"`
// The block height in which Packet was committed, to check Proof
FromChainHeight uint64 `json:"src_height"`
// this proof must match the header and the packet.Bytes()
Proof *merkle.IAVLProof `json:"proof"`
Key data.Bytes `json:"key"`
Packet Packet `json:"packet"`
}
// ValidateBasic makes sure this is consistent - used to satisfy TxInner
func (p PostPacketTx) ValidateBasic() error {
// TODO
return nil
}
// Wrap - used to satisfy TxInner
func (p PostPacketTx) Wrap() basecoin.Tx {
return basecoin.Tx{p}
}

View File

@ -1,10 +1,9 @@
package commands
import (
"github.com/pkg/errors"
"github.com/spf13/cobra"
lcmd "github.com/tendermint/basecoin/client/commands"
"github.com/tendermint/basecoin/client/commands"
proofcmd "github.com/tendermint/basecoin/client/commands/proofs"
"github.com/tendermint/basecoin/modules/roles"
"github.com/tendermint/basecoin/stack"
@ -14,17 +13,15 @@ import (
var RoleQueryCmd = &cobra.Command{
Use: "role [name]",
Short: "Get details of a role, with proof",
RunE: lcmd.RequireInit(roleQueryCmd),
RunE: commands.RequireInit(roleQueryCmd),
}
func roleQueryCmd(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return errors.New("Missing required argument [name]")
} else if len(args) > 1 {
return errors.New("Command only supports one name")
arg, err := commands.GetOneArg(args, "name")
if err != nil {
return err
}
role, err := parseRole(args[0])
role, err := parseRole(arg)
if err != nil {
return err
}

View File

@ -11,7 +11,7 @@ import (
"github.com/tendermint/basecoin/modules/roles"
)
// CreateRoleTxCmd is CLI command to send tokens between basecoin accounts
// CreateRoleTxCmd is CLI command to create a new role
var CreateRoleTxCmd = &cobra.Command{
Use: "create-role",
Short: "Create a new role",
@ -32,7 +32,7 @@ func init() {
flags.Int(FlagMinSigs, 0, "Minimum number of signatures needed to assume this role")
}
// createRoleTxCmd is an example of how to make a tx
// createRoleTxCmd creates a basic role tx and then wraps, signs, and posts it
func createRoleTxCmd(cmd *cobra.Command, args []string) error {
tx, err := readCreateRoleTxFlags()
if err != nil {

View File

@ -1,600 +0,0 @@
package ibc
// import (
// "bytes"
// "encoding/json"
// "errors"
// "fmt"
// "net/url"
// "strconv"
// "strings"
// abci "github.com/tendermint/abci/types"
// "github.com/tendermint/go-wire"
// merkle "github.com/tendermint/merkleeyes/iavl"
// cmn "github.com/tendermint/tmlibs/common"
// "github.com/tendermint/basecoin/types"
// tm "github.com/tendermint/tendermint/types"
// )
// const (
// // Key parts
// _IBC = "ibc"
// _BLOCKCHAIN = "blockchain"
// _GENESIS = "genesis"
// _STATE = "state"
// _HEADER = "header"
// _EGRESS = "egress"
// _INGRESS = "ingress"
// _CONNECTION = "connection"
// )
// type IBCPluginState struct {
// // @[:ibc, :blockchain, :genesis, ChainID] <~ BlockchainGenesis
// // @[:ibc, :blockchain, :state, ChainID] <~ BlockchainState
// // @[:ibc, :blockchain, :header, ChainID, Height] <~ tm.Header
// // @[:ibc, :egress, Src, Dst, Sequence] <~ Packet
// // @[:ibc, :ingress, Dst, Src, Sequence] <~ Packet
// // @[:ibc, :connection, Src, Dst] <~ Connection # TODO - keep connection state
// }
// type BlockchainGenesis struct {
// ChainID string
// Genesis string
// }
// type BlockchainState struct {
// ChainID string
// Validators []*tm.Validator
// LastBlockHash []byte
// LastBlockHeight uint64
// }
// type Packet struct {
// SrcChainID string
// DstChainID string
// Sequence uint64
// Type string // redundant now that Type() is a method on Payload ?
// Payload Payload
// }
// func NewPacket(src, dst string, seq uint64, payload Payload) Packet {
// return Packet{
// SrcChainID: src,
// DstChainID: dst,
// Sequence: seq,
// Type: payload.Type(),
// Payload: payload,
// }
// }
// // GetSequenceNumber gets the sequence number for packets being sent from the src chain to the dst chain.
// // The sequence number counts how many packets have been sent.
// // The next packet must include the latest sequence number.
// func GetSequenceNumber(store state.SimpleDB, src, dst string) uint64 {
// sequenceKey := toKey(_IBC, _EGRESS, src, dst)
// seqBytes := store.Get(sequenceKey)
// if seqBytes == nil {
// return 0
// }
// seq, err := strconv.ParseUint(string(seqBytes), 10, 64)
// if err != nil {
// cmn.PanicSanity(err.Error())
// }
// return seq
// }
// // SetSequenceNumber sets the sequence number for packets being sent from the src chain to the dst chain
// func SetSequenceNumber(store state.SimpleDB, src, dst string, seq uint64) {
// sequenceKey := toKey(_IBC, _EGRESS, src, dst)
// store.Set(sequenceKey, []byte(strconv.FormatUint(seq, 10)))
// }
// // SaveNewIBCPacket creates an IBC packet with the given payload from the src chain to the dst chain
// // using the correct sequence number. It also increments the sequence number by 1
// func SaveNewIBCPacket(state state.SimpleDB, src, dst string, payload Payload) {
// // fetch sequence number and increment by 1
// seq := GetSequenceNumber(state, src, dst)
// SetSequenceNumber(state, src, dst, seq+1)
// // save ibc packet
// packetKey := toKey(_IBC, _EGRESS, src, dst, cmn.Fmt("%v", seq))
// packet := NewPacket(src, dst, uint64(seq), payload)
// save(state, packetKey, packet)
// }
// func GetIBCPacket(state state.SimpleDB, src, dst string, seq uint64) (Packet, error) {
// packetKey := toKey(_IBC, _EGRESS, src, dst, cmn.Fmt("%v", seq))
// packetBytes := state.Get(packetKey)
// var packet Packet
// err := wire.ReadBinaryBytes(packetBytes, &packet)
// return packet, err
// }
// //--------------------------------------------------------------------------------
// const (
// PayloadTypeBytes = byte(0x01)
// PayloadTypeCoins = byte(0x02)
// )
// var _ = wire.RegisterInterface(
// struct{ Payload }{},
// wire.ConcreteType{DataPayload{}, PayloadTypeBytes},
// wire.ConcreteType{CoinsPayload{}, PayloadTypeCoins},
// )
// type Payload interface {
// AssertIsPayload()
// Type() string
// ValidateBasic() abci.Result
// }
// func (DataPayload) AssertIsPayload() {}
// func (CoinsPayload) AssertIsPayload() {}
// type DataPayload []byte
// func (p DataPayload) Type() string {
// return "data"
// }
// func (p DataPayload) ValidateBasic() abci.Result {
// return abci.OK
// }
// type CoinsPayload struct {
// Address []byte
// Coins coin.Coins
// }
// func (p CoinsPayload) Type() string {
// return "coin"
// }
// func (p CoinsPayload) ValidateBasic() abci.Result {
// // TODO: validate
// return abci.OK
// }
// //--------------------------------------------------------------------------------
// const (
// IBCTxTypeRegisterChain = byte(0x01)
// IBCTxTypeUpdateChain = byte(0x02)
// IBCTxTypePacketCreate = byte(0x03)
// IBCTxTypePacketPost = byte(0x04)
// IBCCodeEncodingError = abci.CodeType(1001)
// IBCCodeChainAlreadyExists = abci.CodeType(1002)
// IBCCodePacketAlreadyExists = abci.CodeType(1003)
// IBCCodeUnknownHeight = abci.CodeType(1004)
// IBCCodeInvalidCommit = abci.CodeType(1005)
// IBCCodeInvalidProof = abci.CodeType(1006)
// )
// var _ = wire.RegisterInterface(
// struct{ IBCTx }{},
// wire.ConcreteType{IBCRegisterChainTx{}, IBCTxTypeRegisterChain},
// wire.ConcreteType{IBCUpdateChainTx{}, IBCTxTypeUpdateChain},
// wire.ConcreteType{IBCPacketCreateTx{}, IBCTxTypePacketCreate},
// wire.ConcreteType{IBCPacketPostTx{}, IBCTxTypePacketPost},
// )
// type IBCTx interface {
// AssertIsIBCTx()
// ValidateBasic() abci.Result
// }
// func (IBCRegisterChainTx) AssertIsIBCTx() {}
// func (IBCUpdateChainTx) AssertIsIBCTx() {}
// func (IBCPacketCreateTx) AssertIsIBCTx() {}
// func (IBCPacketPostTx) AssertIsIBCTx() {}
// type IBCRegisterChainTx struct {
// BlockchainGenesis
// }
// func (IBCRegisterChainTx) ValidateBasic() (res abci.Result) {
// // TODO - validate
// return
// }
// type IBCUpdateChainTx struct {
// Header tm.Header
// Commit tm.Commit
// // TODO: NextValidators
// }
// func (IBCUpdateChainTx) ValidateBasic() (res abci.Result) {
// // TODO - validate
// return
// }
// type IBCPacketCreateTx struct {
// Packet
// }
// func (IBCPacketCreateTx) ValidateBasic() (res abci.Result) {
// // TODO - validate
// return
// }
// type IBCPacketPostTx struct {
// FromChainID string // The immediate source of the packet, not always Packet.SrcChainID
// FromChainHeight uint64 // The block height in which Packet was committed, to check Proof
// Packet
// Proof *merkle.IAVLProof
// }
// func (IBCPacketPostTx) ValidateBasic() (res abci.Result) {
// // TODO - validate
// return
// }
// //--------------------------------------------------------------------------------
// type IBCPlugin struct {
// }
// func (ibc *IBCPlugin) Name() string {
// return "IBC"
// }
// func (ibc *IBCPlugin) StateKey() []byte {
// return []byte("IBCPlugin.State")
// }
// func New() *IBCPlugin {
// return &IBCPlugin{}
// }
// func (ibc *IBCPlugin) SetOption(store state.SimpleDB, key string, value string) (log string) {
// return ""
// }
// func (ibc *IBCPlugin) RunTx(store state.SimpleDB, ctx types.CallContext, txBytes []byte) (res abci.Result) {
// // Decode tx
// var tx IBCTx
// err := wire.ReadBinaryBytes(txBytes, &tx)
// if err != nil {
// return abci.ErrBaseEncodingError.AppendLog("Error decoding tx: " + err.Error())
// }
// // Validate tx
// res = tx.ValidateBasic()
// if res.IsErr() {
// return res.PrependLog("ValidateBasic Failed: ")
// }
// // TODO - Check whether sufficient funds
// defer func() {
// // TODO - Refund any remaining funds left over
// // e.g. !ctx.Coins.Minus(tx.Fee).IsZero()
// // ctx.CallerAccount is synced w/ store, so just modify that and store it.
// // NOTE: We should use the CallContext to store fund/refund information.
// }()
// sm := &IBCStateMachine{store, ctx, abci.OK}
// switch tx := tx.(type) {
// case IBCRegisterChainTx:
// sm.runRegisterChainTx(tx)
// case IBCUpdateChainTx:
// sm.runUpdateChainTx(tx)
// case IBCPacketCreateTx:
// sm.runPacketCreateTx(tx)
// case IBCPacketPostTx:
// sm.runPacketPostTx(tx)
// }
// return sm.res
// }
// type IBCStateMachine struct {
// store state.SimpleDB
// ctx types.CallContext
// res abci.Result
// }
// func (sm *IBCStateMachine) runRegisterChainTx(tx IBCRegisterChainTx) {
// chainGenKey := toKey(_IBC, _BLOCKCHAIN, _GENESIS, tx.ChainID)
// chainStateKey := toKey(_IBC, _BLOCKCHAIN, _STATE, tx.ChainID)
// chainGen := tx.BlockchainGenesis
// // Parse genesis
// chainGenDoc := new(tm.GenesisDoc)
// err := json.Unmarshal([]byte(chainGen.Genesis), chainGenDoc)
// if err != nil {
// sm.res.Code = IBCCodeEncodingError
// sm.res.Log = "Genesis doc couldn't be parsed: " + err.Error()
// return
// }
// // Make sure chainGen doesn't already exist
// if exists(sm.store, chainGenKey) {
// sm.res.Code = IBCCodeChainAlreadyExists
// sm.res.Log = "Already exists"
// return
// }
// // Save new BlockchainGenesis
// save(sm.store, chainGenKey, chainGen)
// // Create new BlockchainState
// chainState := BlockchainState{
// ChainID: chainGenDoc.ChainID,
// Validators: make([]*tm.Validator, len(chainGenDoc.Validators)),
// LastBlockHash: nil,
// LastBlockHeight: 0,
// }
// // Make validators slice
// for i, val := range chainGenDoc.Validators {
// pubKey := val.PubKey
// address := pubKey.Address()
// chainState.Validators[i] = &tm.Validator{
// Address: address,
// PubKey: pubKey,
// VotingPower: val.Amount,
// }
// }
// // Save new BlockchainState
// save(sm.store, chainStateKey, chainState)
// }
// func (sm *IBCStateMachine) runUpdateChainTx(tx IBCUpdateChainTx) {
// chainID := tx.Header.ChainID
// chainStateKey := toKey(_IBC, _BLOCKCHAIN, _STATE, chainID)
// // Make sure chainState exists
// if !exists(sm.store, chainStateKey) {
// return // Chain does not exist, do nothing
// }
// // Load latest chainState
// var chainState BlockchainState
// exists, err := load(sm.store, chainStateKey, &chainState)
// if err != nil {
// sm.res = abci.ErrInternalError.AppendLog(cmn.Fmt("Loading ChainState: %v", err.Error()))
// return
// }
// if !exists {
// sm.res = abci.ErrInternalError.AppendLog(cmn.Fmt("Missing ChainState"))
// return
// }
// // Check commit against last known state & validators
// err = verifyCommit(chainState, &tx.Header, &tx.Commit)
// if err != nil {
// sm.res.Code = IBCCodeInvalidCommit
// sm.res.Log = cmn.Fmt("Invalid Commit: %v", err.Error())
// return
// }
// // Store header
// headerKey := toKey(_IBC, _BLOCKCHAIN, _HEADER, chainID, cmn.Fmt("%v", tx.Header.Height))
// save(sm.store, headerKey, tx.Header)
// // Update chainState
// chainState.LastBlockHash = tx.Header.Hash()
// chainState.LastBlockHeight = uint64(tx.Header.Height)
// // Store chainState
// save(sm.store, chainStateKey, chainState)
// }
// func (sm *IBCStateMachine) runPacketCreateTx(tx IBCPacketCreateTx) {
// packet := tx.Packet
// packetKey := toKey(_IBC, _EGRESS,
// packet.SrcChainID,
// packet.DstChainID,
// cmn.Fmt("%v", packet.Sequence),
// )
// // Make sure packet doesn't already exist
// if exists(sm.store, packetKey) {
// sm.res.Code = IBCCodePacketAlreadyExists
// // TODO: .AppendLog() does not update sm.res
// sm.res.Log = "Already exists"
// return
// }
// // Execute the payload
// switch payload := tx.Packet.Payload.(type) {
// case DataPayload:
// // do nothing
// case CoinsPayload:
// // ensure enough coins were sent in tx to cover the payload coins
// if !sm.ctx.Coins.IsGTE(payload.Coins) {
// sm.res.Code = abci.CodeType_InsufficientFunds
// sm.res.Log = fmt.Sprintf("Not enough funds sent in tx (%v) to send %v via IBC", sm.ctx.Coins, payload.Coins)
// return
// }
// // deduct coins from context
// sm.ctx.Coins = sm.ctx.Coins.Minus(payload.Coins)
// }
// // Save new Packet
// save(sm.store, packetKey, packet)
// // set the sequence number
// SetSequenceNumber(sm.store, packet.SrcChainID, packet.DstChainID, packet.Sequence)
// }
// func (sm *IBCStateMachine) runPacketPostTx(tx IBCPacketPostTx) {
// packet := tx.Packet
// packetKeyEgress := toKey(_IBC, _EGRESS,
// packet.SrcChainID,
// packet.DstChainID,
// cmn.Fmt("%v", packet.Sequence),
// )
// packetKeyIngress := toKey(_IBC, _INGRESS,
// packet.DstChainID,
// packet.SrcChainID,
// cmn.Fmt("%v", packet.Sequence),
// )
// headerKey := toKey(_IBC, _BLOCKCHAIN, _HEADER,
// tx.FromChainID,
// cmn.Fmt("%v", tx.FromChainHeight),
// )
// // Make sure packet doesn't already exist
// if exists(sm.store, packetKeyIngress) {
// sm.res.Code = IBCCodePacketAlreadyExists
// sm.res.Log = "Already exists"
// return
// }
// // Save new Packet (just for fun)
// save(sm.store, packetKeyIngress, packet)
// // Load Header and make sure it exists
// // If it exists, we already checked a valid commit for it in UpdateChainTx
// var header tm.Header
// exists, err := load(sm.store, headerKey, &header)
// if err != nil {
// sm.res = abci.ErrInternalError.AppendLog(cmn.Fmt("Loading Header: %v", err.Error()))
// return
// }
// if !exists {
// sm.res.Code = IBCCodeUnknownHeight
// sm.res.Log = cmn.Fmt("Loading Header: Unknown height")
// return
// }
// proof := tx.Proof
// if proof == nil {
// sm.res.Code = IBCCodeInvalidProof
// sm.res.Log = "Proof is nil"
// return
// }
// packetBytes := wire.BinaryBytes(packet)
// // Make sure packet's proof matches given (packet, key, blockhash)
// ok := proof.Verify(packetKeyEgress, packetBytes, header.AppHash)
// if !ok {
// sm.res.Code = IBCCodeInvalidProof
// sm.res.Log = fmt.Sprintf("Proof is invalid. key: %s; packetByes %X; header %v; proof %v", packetKeyEgress, packetBytes, header, proof)
// return
// }
// // Execute payload
// switch payload := packet.Payload.(type) {
// case DataPayload:
// // do nothing
// case CoinsPayload:
// // Add coins to destination account
// acc := types.GetAccount(sm.store, payload.Address)
// if acc == nil {
// acc = &types.Account{}
// }
// acc.Balance = acc.Balance.Plus(payload.Coins)
// types.SetAccount(sm.store, payload.Address, acc)
// }
// return
// }
// func (ibc *IBCPlugin) InitChain(store state.SimpleDB, vals []*abci.Validator) {
// }
// func (cp *IBCPlugin) BeginBlock(store state.SimpleDB, hash []byte, header *abci.Header) {
// }
// func (cp *IBCPlugin) EndBlock(store state.SimpleDB, height uint64) (res abci.ResponseEndBlock) {
// return
// }
// //--------------------------------------------------------------------------------
// // TODO: move to utils
// // Returns true if exists, false if nil.
// func exists(store state.SimpleDB, key []byte) (exists bool) {
// value := store.Get(key)
// return len(value) > 0
// }
// // Load bytes from store by reading value for key and read into ptr.
// // Returns true if exists, false if nil.
// // Returns err if decoding error.
// func load(store state.SimpleDB, key []byte, ptr interface{}) (exists bool, err error) {
// value := store.Get(key)
// if len(value) > 0 {
// err = wire.ReadBinaryBytes(value, ptr)
// if err != nil {
// return true, errors.New(
// cmn.Fmt("Error decoding key 0x%X = 0x%X: %v", key, value, err.Error()),
// )
// }
// return true, nil
// } else {
// return false, nil
// }
// }
// // Save bytes to store by writing obj's go-wire binary bytes.
// func save(store state.SimpleDB, key []byte, obj interface{}) {
// store.Set(key, wire.BinaryBytes(obj))
// }
// // Key parts are URL escaped and joined with ','
// func toKey(parts ...string) []byte {
// escParts := make([]string, len(parts))
// for i, part := range parts {
// escParts[i] = url.QueryEscape(part)
// }
// return []byte(strings.Join(escParts, ","))
// }
// // NOTE: Commit's votes include ValidatorAddress, so can be matched up
// // against chainState.Validators, even if the validator set had changed.
// // For the purpose of the demo, we assume that the validator set hadn't changed,
// // though we should check that explicitly.
// func verifyCommit(chainState BlockchainState, header *tm.Header, commit *tm.Commit) error {
// // Ensure that chainState and header ChainID match.
// if chainState.ChainID != header.ChainID {
// return errors.New(cmn.Fmt("Expected header.ChainID %v, got %v", chainState.ChainID, header.ChainID))
// }
// // Ensure things aren't empty
// if len(chainState.Validators) == 0 {
// return errors.New(cmn.Fmt("Blockchain has no validators")) // NOTE: Why would this happen?
// }
// if len(commit.Precommits) == 0 {
// return errors.New(cmn.Fmt("Commit has no signatures"))
// }
// chainID := chainState.ChainID
// vals := chainState.Validators
// valSet := tm.NewValidatorSet(vals)
// var blockID tm.BlockID
// for _, pc := range commit.Precommits {
// // XXX: incorrect. we want the one for +2/3, not just the first one
// if pc != nil {
// blockID = pc.BlockID
// }
// }
// if blockID.IsZero() {
// return errors.New("All precommits are nil!")
// }
// // NOTE: Currently this only works with the exact same validator set.
// // Not this, but perhaps "ValidatorSet.VerifyCommitAny" should expose
// // the functionality to verify commits even after validator changes.
// err := valSet.VerifyCommit(chainID, blockID, header.Height, commit)
// if err != nil {
// return err
// }
// // Ensure the committed blockID matches the header
// if !bytes.Equal(header.Hash(), blockID.Hash) {
// return errors.New(cmn.Fmt("blockID.Hash (%X) does not match header.Hash (%X)", blockID.Hash, header.Hash()))
// }
// // All ok!
// return nil
// }

View File

@ -1,512 +0,0 @@
package ibc
// import (
// "bytes"
// "encoding/json"
// "sort"
// "strings"
// "testing"
// "github.com/stretchr/testify/assert"
// "github.com/stretchr/testify/require"
// abci "github.com/tendermint/abci/types"
// crypto "github.com/tendermint/go-crypto"
// "github.com/tendermint/go-wire"
// eyes "github.com/tendermint/merkleeyes/client"
// "github.com/tendermint/merkleeyes/iavl"
// cmn "github.com/tendermint/tmlibs/common"
// "github.com/tendermint/basecoin/types"
// tm "github.com/tendermint/tendermint/types"
// )
// // NOTE: PrivAccounts are sorted by Address,
// // GenesisDoc, not necessarily.
// func genGenesisDoc(chainID string, numVals int) (*tm.GenesisDoc, []types.PrivAccount) {
// var privAccs []types.PrivAccount
// genDoc := &tm.GenesisDoc{
// ChainID: chainID,
// Validators: nil,
// }
// for i := 0; i < numVals; i++ {
// name := cmn.Fmt("%v_val_%v", chainID, i)
// privAcc := types.PrivAccountFromSecret(name)
// genDoc.Validators = append(genDoc.Validators, tm.GenesisValidator{
// PubKey: privAcc.PubKey,
// Amount: 1,
// Name: name,
// })
// privAccs = append(privAccs, privAcc)
// }
// // Sort PrivAccounts
// sort.Sort(PrivAccountsByAddress(privAccs))
// return genDoc, privAccs
// }
// //-------------------------------------
// // Implements sort for sorting PrivAccount by address.
// type PrivAccountsByAddress []types.PrivAccount
// func (pas PrivAccountsByAddress) Len() int {
// return len(pas)
// }
// func (pas PrivAccountsByAddress) Less(i, j int) bool {
// return bytes.Compare(pas[i].Account.PubKey.Address(), pas[j].Account.PubKey.Address()) == -1
// }
// func (pas PrivAccountsByAddress) Swap(i, j int) {
// it := pas[i]
// pas[i] = pas[j]
// pas[j] = it
// }
// //--------------------------------------------------------------------------------
// var testGenesisDoc = `{
// "app_hash": "",
// "chain_id": "test_chain_1",
// "genesis_time": "0001-01-01T00:00:00.000Z",
// "validators": [
// {
// "amount": 10,
// "name": "",
// "pub_key": {
// "type": "ed25519",
// "data":"D6EBB92440CF375054AA59BCF0C99D596DEEDFFB2543CAE1BA1908B72CF9676A"
// }
// }
// ],
// "app_options": {
// "accounts": [
// {
// "pub_key": {
// "type": "ed25519",
// "data": "B3588BDC92015ED3CDB6F57A86379E8C79A7111063610B7E625487C76496F4DF"
// },
// "coins": [
// {
// "denom": "mycoin",
// "amount": 9007199254740992
// }
// ]
// }
// ]
// }
// }`
// func TestIBCGenesisFromString(t *testing.T) {
// eyesClient := eyes.NewLocalClient("", 0)
// store := types.NewKVCache(eyesClient)
// store.SetLogging() // Log all activity
// ibcPlugin := New()
// ctx := types.NewCallContext(nil, nil, coin.Coins{})
// registerChain(t, ibcPlugin, store, ctx, "test_chain", testGenesisDoc)
// }
// //--------------------------------------------------------------------------------
// func TestIBCPluginRegister(t *testing.T) {
// require := require.New(t)
// eyesClient := eyes.NewLocalClient("", 0)
// store := types.NewKVCache(eyesClient)
// store.SetLogging() // Log all activity
// ibcPlugin := New()
// ctx := types.NewCallContext(nil, nil, coin.Coins{})
// chainID_1 := "test_chain"
// genDoc_1, _ := genGenesisDoc(chainID_1, 4)
// genDocJSON_1, err := json.Marshal(genDoc_1)
// require.Nil(err)
// // Register a malformed chain
// res := ibcPlugin.RunTx(store, ctx, wire.BinaryBytes(struct{ IBCTx }{IBCRegisterChainTx{
// BlockchainGenesis{
// ChainID: "test_chain",
// Genesis: "<THIS IS NOT JSON>",
// },
// }}))
// assertAndLog(t, store, res, IBCCodeEncodingError)
// // Successfully register a chain
// registerChain(t, ibcPlugin, store, ctx, "test_chain", string(genDocJSON_1))
// // Duplicate request fails
// res = ibcPlugin.RunTx(store, ctx, wire.BinaryBytes(struct{ IBCTx }{IBCRegisterChainTx{
// BlockchainGenesis{
// ChainID: "test_chain",
// Genesis: string(genDocJSON_1),
// },
// }}))
// assertAndLog(t, store, res, IBCCodeChainAlreadyExists)
// }
// func TestIBCPluginPost(t *testing.T) {
// require := require.New(t)
// eyesClient := eyes.NewLocalClient("", 0)
// store := types.NewKVCache(eyesClient)
// store.SetLogging() // Log all activity
// ibcPlugin := New()
// ctx := types.NewCallContext(nil, nil, coin.Coins{})
// chainID_1 := "test_chain"
// genDoc_1, _ := genGenesisDoc(chainID_1, 4)
// genDocJSON_1, err := json.Marshal(genDoc_1)
// require.Nil(err)
// // Register a chain
// registerChain(t, ibcPlugin, store, ctx, "test_chain", string(genDocJSON_1))
// // Create a new packet (for testing)
// packet := NewPacket("test_chain", "dst_chain", 0, DataPayload([]byte("hello world")))
// res := ibcPlugin.RunTx(store, ctx, wire.BinaryBytes(struct{ IBCTx }{IBCPacketCreateTx{
// Packet: packet,
// }}))
// assertAndLog(t, store, res, abci.CodeType_OK)
// // Post a duplicate packet
// res = ibcPlugin.RunTx(store, ctx, wire.BinaryBytes(struct{ IBCTx }{IBCPacketCreateTx{
// Packet: packet,
// }}))
// assertAndLog(t, store, res, IBCCodePacketAlreadyExists)
// }
// func TestIBCPluginPayloadBytes(t *testing.T) {
// assert := assert.New(t)
// require := require.New(t)
// eyesClient := eyes.NewLocalClient("", 0)
// store := types.NewKVCache(eyesClient)
// store.SetLogging() // Log all activity
// ibcPlugin := New()
// ctx := types.NewCallContext(nil, nil, coin.Coins{})
// chainID_1 := "test_chain"
// genDoc_1, privAccs_1 := genGenesisDoc(chainID_1, 4)
// genDocJSON_1, err := json.Marshal(genDoc_1)
// require.Nil(err)
// // Register a chain
// registerChain(t, ibcPlugin, store, ctx, "test_chain", string(genDocJSON_1))
// // Create a new packet (for testing)
// packet := NewPacket("test_chain", "dst_chain", 0, DataPayload([]byte("hello world")))
// res := ibcPlugin.RunTx(store, ctx, wire.BinaryBytes(struct{ IBCTx }{IBCPacketCreateTx{
// Packet: packet,
// }}))
// assertAndLog(t, store, res, abci.CodeType_OK)
// // Construct a Header that includes the above packet.
// store.Sync()
// resCommit := eyesClient.CommitSync()
// appHash := resCommit.Data
// header := newHeader("test_chain", 999, appHash, []byte("must_exist"))
// // Construct a Commit that signs above header
// commit := constructCommit(privAccs_1, header)
// // Update a chain
// res = ibcPlugin.RunTx(store, ctx, wire.BinaryBytes(struct{ IBCTx }{IBCUpdateChainTx{
// Header: header,
// Commit: commit,
// }}))
// assertAndLog(t, store, res, abci.CodeType_OK)
// // Get proof for the packet
// packetKey := toKey(_IBC, _EGRESS,
// packet.SrcChainID,
// packet.DstChainID,
// cmn.Fmt("%v", packet.Sequence),
// )
// resQuery, err := eyesClient.QuerySync(abci.RequestQuery{
// Path: "/store",
// Data: packetKey,
// Prove: true,
// })
// assert.Nil(err)
// var proof *iavl.IAVLProof
// err = wire.ReadBinaryBytes(resQuery.Proof, &proof)
// assert.Nil(err)
// // Post a packet
// res = ibcPlugin.RunTx(store, ctx, wire.BinaryBytes(struct{ IBCTx }{IBCPacketPostTx{
// FromChainID: "test_chain",
// FromChainHeight: 999,
// Packet: packet,
// Proof: proof,
// }}))
// assertAndLog(t, store, res, abci.CodeType_OK)
// }
// func TestIBCPluginPayloadCoins(t *testing.T) {
// assert := assert.New(t)
// require := require.New(t)
// eyesClient := eyes.NewLocalClient("", 0)
// store := types.NewKVCache(eyesClient)
// store.SetLogging() // Log all activity
// ibcPlugin := New()
// coins := coin.Coins{
// coin.Coin{
// Denom: "mycoin",
// Amount: 100,
// },
// }
// ctx := types.NewCallContext(nil, nil, coins)
// chainID_1 := "test_chain"
// genDoc_1, privAccs_1 := genGenesisDoc(chainID_1, 4)
// genDocJSON_1, err := json.Marshal(genDoc_1)
// require.Nil(err)
// // Register a chain
// registerChain(t, ibcPlugin, store, ctx, "test_chain", string(genDocJSON_1))
// // send coins to this addr on the other chain
// destinationAddr := []byte("some address")
// coinsBad := coin.Coins{coin.Coin{"mycoin", 200}}
// coinsGood := coin.Coins{coin.Coin{"mycoin", 1}}
// // Try to send too many coins
// packet := NewPacket("test_chain", "dst_chain", 0, CoinsPayload{
// Address: destinationAddr,
// Coins: coinsBad,
// })
// res := ibcPlugin.RunTx(store, ctx, wire.BinaryBytes(struct{ IBCTx }{IBCPacketCreateTx{
// Packet: packet,
// }}))
// assertAndLog(t, store, res, abci.CodeType_InsufficientFunds)
// // Send a small enough number of coins
// packet = NewPacket("test_chain", "dst_chain", 0, CoinsPayload{
// Address: destinationAddr,
// Coins: coinsGood,
// })
// res = ibcPlugin.RunTx(store, ctx, wire.BinaryBytes(struct{ IBCTx }{IBCPacketCreateTx{
// Packet: packet,
// }}))
// assertAndLog(t, store, res, abci.CodeType_OK)
// // Construct a Header that includes the above packet.
// store.Sync()
// resCommit := eyesClient.CommitSync()
// appHash := resCommit.Data
// header := newHeader("test_chain", 999, appHash, []byte("must_exist"))
// // Construct a Commit that signs above header
// commit := constructCommit(privAccs_1, header)
// // Update a chain
// res = ibcPlugin.RunTx(store, ctx, wire.BinaryBytes(struct{ IBCTx }{IBCUpdateChainTx{
// Header: header,
// Commit: commit,
// }}))
// assertAndLog(t, store, res, abci.CodeType_OK)
// // Get proof for the packet
// packetKey := toKey(_IBC, _EGRESS,
// packet.SrcChainID,
// packet.DstChainID,
// cmn.Fmt("%v", packet.Sequence),
// )
// resQuery, err := eyesClient.QuerySync(abci.RequestQuery{
// Path: "/store",
// Data: packetKey,
// Prove: true,
// })
// assert.Nil(err)
// var proof *iavl.IAVLProof
// err = wire.ReadBinaryBytes(resQuery.Proof, &proof)
// assert.Nil(err)
// // Account should be empty before the tx
// acc := types.GetAccount(store, destinationAddr)
// assert.Nil(acc)
// // Post a packet
// res = ibcPlugin.RunTx(store, ctx, wire.BinaryBytes(struct{ IBCTx }{IBCPacketPostTx{
// FromChainID: "test_chain",
// FromChainHeight: 999,
// Packet: packet,
// Proof: proof,
// }}))
// assertAndLog(t, store, res, abci.CodeType_OK)
// // Account should now have some coins
// acc = types.GetAccount(store, destinationAddr)
// assert.Equal(acc.Balance, coinsGood)
// }
// func TestIBCPluginBadCommit(t *testing.T) {
// require := require.New(t)
// eyesClient := eyes.NewLocalClient("", 0)
// store := types.NewKVCache(eyesClient)
// store.SetLogging() // Log all activity
// ibcPlugin := New()
// ctx := types.NewCallContext(nil, nil, coin.Coins{})
// chainID_1 := "test_chain"
// genDoc_1, privAccs_1 := genGenesisDoc(chainID_1, 4)
// genDocJSON_1, err := json.Marshal(genDoc_1)
// require.Nil(err)
// // Successfully register a chain
// registerChain(t, ibcPlugin, store, ctx, "test_chain", string(genDocJSON_1))
// // Construct a Header
// header := newHeader("test_chain", 999, nil, []byte("must_exist"))
// // Construct a Commit that signs above header
// commit := constructCommit(privAccs_1, header)
// // Update a chain with a broken commit
// // Modify the first byte of the first signature
// sig := commit.Precommits[0].Signature.Unwrap().(crypto.SignatureEd25519)
// sig[0] += 1
// commit.Precommits[0].Signature = sig.Wrap()
// res := ibcPlugin.RunTx(store, ctx, wire.BinaryBytes(struct{ IBCTx }{IBCUpdateChainTx{
// Header: header,
// Commit: commit,
// }}))
// assertAndLog(t, store, res, IBCCodeInvalidCommit)
// }
// func TestIBCPluginBadProof(t *testing.T) {
// assert := assert.New(t)
// require := require.New(t)
// eyesClient := eyes.NewLocalClient("", 0)
// store := types.NewKVCache(eyesClient)
// store.SetLogging() // Log all activity
// ibcPlugin := New()
// ctx := types.NewCallContext(nil, nil, coin.Coins{})
// chainID_1 := "test_chain"
// genDoc_1, privAccs_1 := genGenesisDoc(chainID_1, 4)
// genDocJSON_1, err := json.Marshal(genDoc_1)
// require.Nil(err)
// // Successfully register a chain
// registerChain(t, ibcPlugin, store, ctx, "test_chain", string(genDocJSON_1))
// // Create a new packet (for testing)
// packet := NewPacket("test_chain", "dst_chain", 0, DataPayload([]byte("hello world")))
// res := ibcPlugin.RunTx(store, ctx, wire.BinaryBytes(struct{ IBCTx }{IBCPacketCreateTx{
// Packet: packet,
// }}))
// assertAndLog(t, store, res, abci.CodeType_OK)
// // Construct a Header that includes the above packet.
// store.Sync()
// resCommit := eyesClient.CommitSync()
// appHash := resCommit.Data
// header := newHeader("test_chain", 999, appHash, []byte("must_exist"))
// // Construct a Commit that signs above header
// commit := constructCommit(privAccs_1, header)
// // Update a chain
// res = ibcPlugin.RunTx(store, ctx, wire.BinaryBytes(struct{ IBCTx }{IBCUpdateChainTx{
// Header: header,
// Commit: commit,
// }}))
// assertAndLog(t, store, res, abci.CodeType_OK)
// // Get proof for the packet
// packetKey := toKey(_IBC, _EGRESS,
// packet.SrcChainID,
// packet.DstChainID,
// cmn.Fmt("%v", packet.Sequence),
// )
// resQuery, err := eyesClient.QuerySync(abci.RequestQuery{
// Path: "/store",
// Data: packetKey,
// Prove: true,
// })
// assert.Nil(err)
// var proof *iavl.IAVLProof
// err = wire.ReadBinaryBytes(resQuery.Proof, &proof)
// assert.Nil(err)
// // Mutate the proof
// proof.InnerNodes[0].Height += 1
// // Post a packet
// res = ibcPlugin.RunTx(store, ctx, wire.BinaryBytes(struct{ IBCTx }{IBCPacketPostTx{
// FromChainID: "test_chain",
// FromChainHeight: 999,
// Packet: packet,
// Proof: proof,
// }}))
// assertAndLog(t, store, res, IBCCodeInvalidProof)
// }
// //-------------------------------------
// // utils
// func assertAndLog(t *testing.T, store *types.KVCache, res abci.Result, codeExpected abci.CodeType) {
// assert := assert.New(t)
// assert.Equal(codeExpected, res.Code, res.Log)
// t.Log(">>", strings.Join(store.GetLogLines(), "\n"))
// store.ClearLogLines()
// }
// func newHeader(chainID string, height int, appHash, valHash []byte) tm.Header {
// return tm.Header{
// ChainID: chainID,
// Height: height,
// AppHash: appHash,
// ValidatorsHash: valHash,
// }
// }
// func registerChain(t *testing.T, ibcPlugin *IBCPlugin, store *types.KVCache, ctx types.CallContext, chainID, genDoc string) {
// res := ibcPlugin.RunTx(store, ctx, wire.BinaryBytes(struct{ IBCTx }{IBCRegisterChainTx{
// BlockchainGenesis{
// ChainID: chainID,
// Genesis: genDoc,
// },
// }}))
// assertAndLog(t, store, res, abci.CodeType_OK)
// }
// func constructCommit(privAccs []types.PrivAccount, header tm.Header) tm.Commit {
// blockHash := header.Hash()
// blockID := tm.BlockID{Hash: blockHash}
// commit := tm.Commit{
// BlockID: blockID,
// Precommits: make([]*tm.Vote, len(privAccs)),
// }
// for i, privAcc := range privAccs {
// vote := &tm.Vote{
// ValidatorAddress: privAcc.Account.PubKey.Address(),
// ValidatorIndex: i,
// Height: 999,
// Round: 0,
// Type: tm.VoteTypePrecommit,
// BlockID: tm.BlockID{Hash: blockHash},
// }
// vote.Signature = privAcc.PrivKey.Sign(
// tm.SignBytes("test_chain", vote),
// )
// commit.Precommits[i] = vote
// }
// return commit
// }

View File

@ -14,14 +14,17 @@ type nonce int64
type secureContext struct {
app string
ibc bool
// this exposes the log.Logger and all other methods we don't override
naiveContext
}
// NewContext - create a new secureContext
func NewContext(chain string, height uint64, logger log.Logger) basecoin.Context {
mock := MockContext(chain, height).(naiveContext)
mock.Logger = logger
return secureContext{
naiveContext: MockContext(chain, height).(naiveContext),
naiveContext: mock,
}
}
@ -31,24 +34,35 @@ var _ basecoin.Context = secureContext{}
func (c secureContext) WithPermissions(perms ...basecoin.Actor) basecoin.Context {
// the guard makes sure you only set permissions for the app you are inside
for _, p := range perms {
// TODO: also check chainID, limit only certain middleware can set IBC?
if p.App != c.app {
err := errors.Errorf("Cannot set permission for %s from %s", c.app, p.App)
if !c.validPermisison(p) {
err := errors.Errorf("Cannot set permission for %s/%s on (app=%s, ibc=%b)",
p.ChainID, p.App, c.app, c.ibc)
panic(err)
}
}
return secureContext{
app: c.app,
ibc: c.ibc,
naiveContext: c.naiveContext.WithPermissions(perms...).(naiveContext),
}
}
func (c secureContext) validPermisison(p basecoin.Actor) bool {
// if app is set, then it must match
if c.app != "" && c.app != p.App {
return false
}
// if ibc, chain must be set, otherwise it must not
return c.ibc == (p.ChainID != "")
}
// Reset should clear out all permissions,
// but carry on knowledge that this is a child
func (c secureContext) Reset() basecoin.Context {
return secureContext{
app: c.app,
ibc: c.ibc,
naiveContext: c.naiveContext.Reset().(naiveContext),
}
}
@ -71,6 +85,20 @@ func withApp(ctx basecoin.Context, app string) basecoin.Context {
}
return secureContext{
app: app,
ibc: false,
naiveContext: sc.naiveContext,
}
}
// withIBC is a private method so we can securely allow IBC permissioning
func withIBC(ctx basecoin.Context) basecoin.Context {
sc, ok := ctx.(secureContext)
if !ok {
return ctx
}
return secureContext{
app: "",
ibc: true,
naiveContext: sc.naiveContext,
}
}

View File

@ -12,6 +12,8 @@ import (
// heavily inspired by negroni's design
type middleware struct {
middleware Middleware
space string
allowIBC bool
next basecoin.Handler
}
@ -21,13 +23,20 @@ func (m *middleware) Name() string {
return m.middleware.Name()
}
func (m *middleware) wrapCtx(ctx basecoin.Context) basecoin.Context {
if m.allowIBC {
return withIBC(ctx)
}
return withApp(ctx, m.space)
}
// CheckTx always returns an empty success tx
func (m *middleware) CheckTx(ctx basecoin.Context, store state.SimpleDB, tx basecoin.Tx) (basecoin.Result, error) {
// make sure we pass in proper context to child
next := secureCheck(m.next, ctx)
// set the permissions for this app
ctx = withApp(ctx, m.Name())
store = stateSpace(store, m.Name())
ctx = m.wrapCtx(ctx)
store = stateSpace(store, m.space)
return m.middleware.CheckTx(ctx, store, tx, next)
}
@ -37,22 +46,48 @@ func (m *middleware) DeliverTx(ctx basecoin.Context, store state.SimpleDB, tx ba
// make sure we pass in proper context to child
next := secureDeliver(m.next, ctx)
// set the permissions for this app
ctx = withApp(ctx, m.Name())
store = stateSpace(store, m.Name())
ctx = m.wrapCtx(ctx)
store = stateSpace(store, m.space)
return m.middleware.DeliverTx(ctx, store, tx, next)
}
func (m *middleware) SetOption(l log.Logger, store state.SimpleDB, module, key, value string) (string, error) {
// set the namespace for the app
store = stateSpace(store, m.Name())
store = stateSpace(store, m.space)
return m.middleware.SetOption(l, store, module, key, value, m.next)
}
// builder is used to associate info with the middleware, so we can build
// it properly
type builder struct {
middleware Middleware
stateSpace string
allowIBC bool
}
func prep(m Middleware, ibc bool) builder {
return builder{
middleware: m,
stateSpace: m.Name(),
allowIBC: ibc,
}
}
// wrap sets up the middleware with the proper options
func (b builder) wrap(next basecoin.Handler) basecoin.Handler {
return &middleware{
middleware: b.middleware,
space: b.stateSpace,
allowIBC: b.allowIBC,
next: next,
}
}
// Stack is the entire application stack
type Stack struct {
middles []Middleware
middles []builder
handler basecoin.Handler
basecoin.Handler // the compiled version, which we expose
}
@ -62,9 +97,26 @@ var _ basecoin.Handler = &Stack{}
// New prepares a middleware stack, you must `.Use()` a Handler
// before you can execute it.
func New(middlewares ...Middleware) *Stack {
return &Stack{
middles: middlewares,
stack := new(Stack)
return stack.Apps(middlewares...)
}
// Apps adds the following Middlewares as typical application
// middleware to the stack (limit permission to one app)
func (s *Stack) Apps(middlewares ...Middleware) *Stack {
// TODO: some wrapper...
for _, m := range middlewares {
s.middles = append(s.middles, prep(m, false))
}
return s
}
// IBC add the following middleware with permission to add cross-chain
// permissions
func (s *Stack) IBC(m Middleware) *Stack {
// TODO: some wrapper...
s.middles = append(s.middles, prep(m, true))
return s
}
// Use sets the final handler for the stack and prepares it for use
@ -77,10 +129,17 @@ func (s *Stack) Use(handler basecoin.Handler) *Stack {
return s
}
func build(mid []Middleware, end basecoin.Handler) basecoin.Handler {
// Dispatch is like Use, but a convenience method to construct a
// dispatcher with a set of modules to route.
func (s *Stack) Dispatch(routes ...Dispatchable) *Stack {
d := NewDispatcher(routes...)
return s.Use(d)
}
func build(mid []builder, end basecoin.Handler) basecoin.Handler {
if len(mid) == 0 {
return end
}
next := build(mid[1:], end)
return &middleware{mid[0], next}
return mid[0].wrap(next)
}

View File

@ -31,25 +31,46 @@ func TestPermissionSandbox(t *testing.T) {
// test cases to make sure permissioning is solid
grantee := basecoin.Actor{App: NameGrant, Address: []byte{1}}
grantee2 := basecoin.Actor{App: NameGrant, Address: []byte{2}}
signer := basecoin.Actor{App: nameSigner, Address: []byte{1}}
// ibc and grantee are the same, just different chains
ibc := basecoin.Actor{ChainID: "other", App: NameGrant, Address: []byte{1}}
ibc2 := basecoin.Actor{ChainID: "other", App: nameSigner, Address: []byte{21}}
signer := basecoin.Actor{App: nameSigner, Address: []byte{21}}
cases := []struct {
asIBC bool
grant basecoin.Actor
require basecoin.Actor
expectedRes data.Bytes
expected func(error) bool
}{
{grantee, grantee, rawBytes, nil},
{grantee, grantee2, nil, errors.IsUnauthorizedErr},
{grantee, signer, nil, errors.IsUnauthorizedErr},
{signer, signer, nil, errors.IsInternalErr},
// grant as normal app middleware
{false, grantee, grantee, rawBytes, nil},
{false, grantee, grantee2, nil, errors.IsUnauthorizedErr},
{false, grantee2, grantee2, rawBytes, nil},
{false, ibc, grantee, nil, errors.IsInternalErr},
{false, grantee, ibc, nil, errors.IsUnauthorizedErr},
{false, grantee, signer, nil, errors.IsUnauthorizedErr},
{false, signer, signer, nil, errors.IsInternalErr},
// grant as ibc middleware
{true, ibc, ibc, rawBytes, nil}, // ibc can set permissions
{true, ibc2, ibc2, rawBytes, nil}, // for any app
// the must match, both app and chain
{true, ibc, ibc2, nil, errors.IsUnauthorizedErr},
{true, ibc, grantee, nil, errors.IsUnauthorizedErr},
// cannot set local apps from ibc middleware
{true, grantee, grantee, nil, errors.IsInternalErr},
}
for i, tc := range cases {
app := New(
Recovery{}, // we need this so panics turn to errors
GrantMiddleware{Auth: tc.grant},
CheckMiddleware{Required: tc.require},
).Use(EchoHandler{})
app := New(Recovery{})
if tc.asIBC {
app = app.IBC(GrantMiddleware{Auth: tc.grant})
} else {
app = app.Apps(GrantMiddleware{Auth: tc.grant})
}
app = app.
Apps(CheckMiddleware{Required: tc.require}).
Use(EchoHandler{})
res, err := app.CheckTx(ctx, store, raw)
checkPerm(t, i, tc.expectedRes, tc.expected, res, err)

102
state/queue.go Normal file
View File

@ -0,0 +1,102 @@
package state
import "encoding/binary"
var (
headKey = []byte("h")
tailKey = []byte("t")
dataKey = []byte("d")
)
// QueueHeadKey gives us the key for the height at head of the queue
func QueueHeadKey() []byte {
return headKey
}
// QueueTailKey gives us the key for the height at tail of the queue
func QueueTailKey() []byte {
return tailKey
}
// QueueItemKey gives us the key to look up one item by sequence
func QueueItemKey(i uint64) []byte {
return makeKey(i)
}
// Queue allows us to fill up a range of the db, and grab from either end
type Queue struct {
store KVStore
head uint64 // if Size() > 0, the first element is here
tail uint64 // this is the first empty slot to Push() to
}
// NewQueue will load or initialize a queue in this state-space
//
// Generally, you will want to stack.PrefixStore() the space first
func NewQueue(store KVStore) *Queue {
q := &Queue{store: store}
q.head = q.getCount(headKey)
q.tail = q.getCount(tailKey)
return q
}
// Tail returns the next slot that Push() will use
func (q *Queue) Tail() uint64 {
return q.tail
}
// Size returns how many elements are in the queue
func (q *Queue) Size() int {
return int(q.tail - q.head)
}
// Push adds an element to the tail of the queue and returns it's location
func (q *Queue) Push(value []byte) uint64 {
key := makeKey(q.tail)
q.store.Set(key, value)
q.tail++
q.setCount(tailKey, q.tail)
return q.tail - 1
}
// Pop gets an element from the end of the queue
func (q *Queue) Pop() []byte {
if q.Size() <= 0 {
return nil
}
key := makeKey(q.head)
value := q.store.Get(key)
q.head++
q.setCount(headKey, q.head)
return value
}
// Item looks at any element in the queue, without modifying anything
func (q *Queue) Item(seq uint64) []byte {
if seq >= q.tail || seq < q.head {
return nil
}
return q.store.Get(makeKey(seq))
}
func (q *Queue) setCount(key []byte, val uint64) {
b := make([]byte, 8)
binary.BigEndian.PutUint64(b, val)
q.store.Set(key, b)
}
func (q *Queue) getCount(key []byte) (val uint64) {
b := q.store.Get(key)
if b != nil {
val = binary.BigEndian.Uint64(b)
}
return val
}
// makeKey returns the key for a data point
func makeKey(val uint64) []byte {
b := make([]byte, 8+len(dataKey))
copy(b, dataKey)
binary.BigEndian.PutUint64(b[len(dataKey):], val)
return b
}

67
state/queue_test.go Normal file
View File

@ -0,0 +1,67 @@
package state
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestQueue(t *testing.T) {
assert := assert.New(t)
lots := make([][]byte, 500)
for i := range lots {
lots[i] = []byte{1, 8, 7}
}
cases := []struct {
pushes [][]byte
pops [][]byte
}{
// fill it up and empty it all
{
[][]byte{{1, 2, 3}, {44}, {3, 0}},
[][]byte{{1, 2, 3}, {44}, {3, 0}},
},
// don't empty everything - size is 1 at the end
{
[][]byte{{77, 22}, {11, 9}, {121}},
[][]byte{{77, 22}, {11, 9}},
},
// empty too much, just get nil, no negative size
{
[][]byte{{1}, {2}, {4}},
[][]byte{{1}, {2}, {4}, nil, nil, nil},
},
// let's play with lots....
{lots, append(lots, nil)},
}
for i, tc := range cases {
store := NewMemKVStore()
// initialize a queue and add items
q := NewQueue(store)
for j, in := range tc.pushes {
cnt := q.Push(in)
assert.Equal(uint64(j), cnt, "%d", i)
}
assert.EqualValues(len(tc.pushes), q.Size())
// load from disk and pop them
r := NewQueue(store)
for _, out := range tc.pops {
val := r.Pop()
assert.Equal(out, val, "%d", i)
}
// it's empty in memory and on disk
expected := len(tc.pushes) - len(tc.pops)
if expected < 0 {
expected = 0
}
assert.EqualValues(expected, r.Size())
s := NewQueue(store)
assert.EqualValues(expected, s.Size())
}
}

157
state/set.go Normal file
View File

@ -0,0 +1,157 @@
package state
import (
"bytes"
"sort"
wire "github.com/tendermint/go-wire"
)
// SetKey returns the key to get all members of this set
func SetKey() []byte {
return keys
}
// Set allows us to add arbitrary k-v pairs, check existence,
// as well as iterate through the set (always in key order)
//
// If we had full access to the IAVL tree, this would be completely
// trivial and redundant
type Set struct {
store KVStore
keys KeyList
}
var _ KVStore = &Set{}
// NewSet loads or initializes a span of keys
func NewSet(store KVStore) *Set {
s := &Set{store: store}
s.loadKeys()
return s
}
// Set puts a value at a given height.
// If the value is nil, or an empty slice, remove the key from the list
func (s *Set) Set(key []byte, value []byte) {
s.store.Set(MakeBKey(key), value)
if len(value) > 0 {
s.addKey(key)
} else {
s.removeKey(key)
}
s.storeKeys()
}
// Get returns the element with a key if it exists
func (s *Set) Get(key []byte) []byte {
return s.store.Get(MakeBKey(key))
}
// Remove deletes this key from the set (same as setting value = nil)
func (s *Set) Remove(key []byte) {
s.store.Set(key, nil)
}
// Exists checks for the existence of the key in the set
func (s *Set) Exists(key []byte) bool {
return len(s.Get(key)) > 0
}
// Size returns how many elements are in the set
func (s *Set) Size() int {
return len(s.keys)
}
// List returns all keys in the set
// It makes a copy, so we don't modify this in place
func (s *Set) List() (keys KeyList) {
out := make([][]byte, len(s.keys))
for i := range s.keys {
out[i] = append([]byte(nil), s.keys[i]...)
}
return out
}
// addKey inserts this key, maintaining sorted order, no duplicates
func (s *Set) addKey(key []byte) {
for i, k := range s.keys {
cmp := bytes.Compare(k, key)
// don't add duplicates
if cmp == 0 {
return
}
// insert before the first key greater than input
if cmp > 0 {
// https://github.com/golang/go/wiki/SliceTricks
s.keys = append(s.keys, nil)
copy(s.keys[i+1:], s.keys[i:])
s.keys[i] = key
return
}
}
// if it is higher than all (or empty keys), append
s.keys = append(s.keys, key)
}
// removeKey removes this key if it is present, maintaining sorted order
func (s *Set) removeKey(key []byte) {
for i, k := range s.keys {
cmp := bytes.Compare(k, key)
// if there is a match, remove
if cmp == 0 {
s.keys = append(s.keys[:i], s.keys[i+1:]...)
return
}
// if we has the proper location, without finding it, abort
if cmp > 0 {
return
}
}
}
func (s *Set) loadKeys() {
b := s.store.Get(keys)
if b == nil {
return
}
err := wire.ReadBinaryBytes(b, &s.keys)
// hahaha... just like i love to hate :)
if err != nil {
panic(err)
}
}
func (s *Set) storeKeys() {
b := wire.BinaryBytes(s.keys)
s.store.Set(keys, b)
}
// MakeBKey prefixes the byte slice for the storage key
func MakeBKey(key []byte) []byte {
return append(dataKey, key...)
}
// KeyList is a sortable list of byte slices
type KeyList [][]byte
//nolint
func (kl KeyList) Len() int { return len(kl) }
func (kl KeyList) Less(i, j int) bool { return bytes.Compare(kl[i], kl[j]) < 0 }
func (kl KeyList) Swap(i, j int) { kl[i], kl[j] = kl[j], kl[i] }
var _ sort.Interface = KeyList{}
// Equals checks for if the two lists have the same content...
// needed as == doesn't work for slices of slices
func (kl KeyList) Equals(kl2 KeyList) bool {
if len(kl) != len(kl2) {
return false
}
for i := range kl {
if !bytes.Equal(kl[i], kl2[i]) {
return false
}
}
return true
}

77
state/set_test.go Normal file
View File

@ -0,0 +1,77 @@
package state
import (
"strconv"
"testing"
"github.com/stretchr/testify/assert"
)
type pair struct {
k []byte
v []byte
}
type setCase struct {
data []pair
// these are the tests to try out
gets []pair // for each item check the query matches
list KeyList // make sure the set returns the proper list
}
func TestSet(t *testing.T) {
a, b, c, d := []byte{0xaa}, []byte{0xbb}, []byte{0xcc}, []byte{0xdd}
cases := []setCase{
// simplest queries
{
[]pair{{a, a}, {b, b}, {c, c}},
[]pair{{c, c}, {d, nil}, {b, b}},
KeyList{a, b, c},
},
// out of order
{
[]pair{{c, a}, {a, b}, {d, c}, {b, d}},
[]pair{{a, b}, {b, d}},
KeyList{a, b, c, d},
},
// duplicate and removing
{
[]pair{{c, a}, {c, c}, {a, d}, {d, d}, {b, b}, {d, nil}, {a, nil}, {a, a}, {b, nil}},
[]pair{{a, a}, {c, c}, {b, nil}},
KeyList{a, c},
},
}
for i, tc := range cases {
store := NewMemKVStore()
// initialize a queue and add items
s := NewSet(store)
for _, x := range tc.data {
s.Set(x.k, x.v)
}
testSet(t, i, s, tc)
// reload and try the queries again
s2 := NewSet(store)
testSet(t, i+10, s2, tc)
}
}
func testSet(t *testing.T, idx int, s *Set, tc setCase) {
assert := assert.New(t)
i := strconv.Itoa(idx)
for _, g := range tc.gets {
v := s.Get(g.k)
assert.Equal(g.v, v, i)
e := s.Exists(g.k)
assert.Equal(e, (g.v != nil), i)
}
l := s.List()
assert.True(tc.list.Equals(l), "%s: %v / %v", i, tc.list, l)
}

123
state/span.go Normal file
View File

@ -0,0 +1,123 @@
package state
import wire "github.com/tendermint/go-wire"
var (
keys = []byte("keys")
// uses dataKey from queue.go to prefix data
)
// Span holds a number of different keys in a large range and allows
// use to make some basic range queries, like highest between, lowest between...
// All items are added with an index
//
// This becomes horribly inefficent as len(keys) => 1000+, but by then
// hopefully we have access to the iavl tree to do this well
//
// TODO: doesn't handle deleting....
type Span struct {
store KVStore
// keys is sorted ascending and cannot contain duplicates
keys []uint64
}
// NewSpan loads or initializes a span of keys
func NewSpan(store KVStore) *Span {
s := &Span{store: store}
s.loadKeys()
return s
}
// Set puts a value at a given height
func (s *Span) Set(h uint64, value []byte) {
key := makeKey(h)
s.store.Set(key, value)
s.addKey(h)
s.storeKeys()
}
// Get returns the element at h if it exists
func (s *Span) Get(h uint64) []byte {
key := makeKey(h)
return s.store.Get(key)
}
// Bottom returns the lowest element in the Span, along with its index
func (s *Span) Bottom() ([]byte, uint64) {
if len(s.keys) == 0 {
return nil, 0
}
h := s.keys[0]
return s.Get(h), h
}
// Top returns the highest element in the Span, along with its index
func (s *Span) Top() ([]byte, uint64) {
l := len(s.keys)
if l == 0 {
return nil, 0
}
h := s.keys[l-1]
return s.Get(h), h
}
// GTE returns the lowest element in the Span that is >= h, along with its index
func (s *Span) GTE(h uint64) ([]byte, uint64) {
for _, k := range s.keys {
if k >= h {
return s.Get(k), k
}
}
return nil, 0
}
// LTE returns the highest element in the Span that is <= h,
// along with its index
func (s *Span) LTE(h uint64) ([]byte, uint64) {
var k uint64
// start from the highest and go down for the first match
for i := len(s.keys) - 1; i >= 0; i-- {
k = s.keys[i]
if k <= h {
return s.Get(k), k
}
}
return nil, 0
}
// addKey inserts this key, maintaining sorted order, no duplicates
func (s *Span) addKey(h uint64) {
for i, k := range s.keys {
// don't add duplicates
if h == k {
return
}
// insert before this key
if h < k {
// https://github.com/golang/go/wiki/SliceTricks
s.keys = append(s.keys, 0)
copy(s.keys[i+1:], s.keys[i:])
s.keys[i] = h
return
}
}
// if it is higher than all (or empty keys), append
s.keys = append(s.keys, h)
}
func (s *Span) loadKeys() {
b := s.store.Get(keys)
if b == nil {
return
}
err := wire.ReadBinaryBytes(b, &s.keys)
// hahaha... just like i love to hate :)
if err != nil {
panic(err)
}
}
func (s *Span) storeKeys() {
b := wire.BinaryBytes(s.keys)
s.store.Set(keys, b)
}

122
state/span_test.go Normal file
View File

@ -0,0 +1,122 @@
package state
import (
"strconv"
"testing"
"github.com/stretchr/testify/assert"
)
type kv struct {
k uint64
v []byte
}
type bscase struct {
data []kv
// these are the tests to try out
top kv
bottom kv
gets []kv // for each item check the query matches
lte []kv // value for lte queires...
gte []kv // value for gte
}
func TestBasicSpan(t *testing.T) {
a, b, c := []byte{0xaa}, []byte{0xbb}, []byte{0xcc}
lots := make([]kv, 1000)
for i := range lots {
lots[i] = kv{uint64(3 * i), []byte{byte(i / 100), byte(i % 100)}}
}
cases := []bscase{
// simplest queries
{
[]kv{{1, a}, {3, b}, {5, c}},
kv{5, c},
kv{1, a},
[]kv{{1, a}, {3, b}, {5, c}},
[]kv{{2, a}, {77, c}, {3, b}, {0, nil}}, // lte
[]kv{{6, nil}, {2, b}, {1, a}}, // gte
},
// add out of order
{
[]kv{{7, a}, {2, b}, {6, c}},
kv{7, a},
kv{2, b},
[]kv{{2, b}, {6, c}, {7, a}},
[]kv{{4, b}, {7, a}, {1, nil}}, // lte
[]kv{{4, c}, {7, a}, {1, b}}, // gte
},
// add out of order and with duplicates
{
[]kv{{7, a}, {2, b}, {6, c}, {7, c}, {6, b}, {2, a}},
kv{7, c},
kv{2, a},
[]kv{{2, a}, {6, b}, {7, c}},
[]kv{{5, a}, {6, b}, {123, c}}, // lte
[]kv{{0, a}, {3, b}, {7, c}, {8, nil}}, // gte
},
// try lots...
{
lots,
lots[len(lots)-1],
lots[0],
lots,
nil,
nil,
},
}
for i, tc := range cases {
store := NewMemKVStore()
// initialize a queue and add items
s := NewSpan(store)
for _, x := range tc.data {
s.Set(x.k, x.v)
}
testSpan(t, i, s, tc)
// reload and try the queries again
s2 := NewSpan(store)
testSpan(t, i+10, s2, tc)
}
}
func testSpan(t *testing.T, idx int, s *Span, tc bscase) {
assert := assert.New(t)
i := strconv.Itoa(idx)
v, k := s.Top()
assert.Equal(tc.top.k, k, i)
assert.Equal(tc.top.v, v, i)
v, k = s.Bottom()
assert.Equal(tc.bottom.k, k, i)
assert.Equal(tc.bottom.v, v, i)
for _, g := range tc.gets {
v = s.Get(g.k)
assert.Equal(g.v, v, i)
}
for _, l := range tc.lte {
v, k = s.LTE(l.k)
assert.Equal(l.v, v, i)
if l.v != nil {
assert.True(k <= l.k, i)
}
}
for _, t := range tc.gte {
v, k = s.GTE(t.k)
assert.Equal(t.v, v, i)
if t.v != nil {
assert.True(k >= t.k, i)
}
}
}

View File

@ -83,6 +83,23 @@ test02SendTxWithFee() {
}
test03CreditTx() {
SENDER=$(getAddr $RICH)
RECV=$(getAddr $POOR)
# make sure we are controlled by permissions (only rich can issue credit)
assertFalse "line=${LINENO}, bad password" "echo qwertyuiop | ${CLIENT_EXE} tx credit --amount=1000mycoin --sequence=1 --to=$RECV --name=$POOR"
TX=$(echo qwertyuiop | ${CLIENT_EXE} tx credit --amount=1000mycoin --sequence=3 --to=$RECV --name=$RICH)
txSucceeded $? "$TX" "$RECV"
HASH=$(echo $TX | jq .hash | tr -d \")
TX_HEIGHT=$(echo $TX | jq .height)
# receiver got cash, sender didn't lose any (1000 more than last check)
checkAccount $RECV "2082"
checkAccount $SENDER "9007199254739900"
}
# Load common then run these tests with shunit2!
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" #get this files directory
. $DIR/common.sh

View File

@ -9,8 +9,8 @@ ACCOUNTS=(jae ethan bucky rigel igor)
RICH=${ACCOUNTS[0]}
POOR=${ACCOUNTS[4]}
# Uncomment the following line for full stack traces in error output
# CLIENT_EXE="basecli --trace"
# For full stack traces in error output, run
# BC_TRACE=1 ./ibc.sh
oneTimeSetUp() {
# These are passed in as args
@ -62,13 +62,13 @@ oneTimeTearDown() {
}
test00GetAccount() {
SENDER_1=$(BC_HOME=${CLIENT_1} getAddr $RICH)
RECV_1=$(BC_HOME=${CLIENT_1} getAddr $POOR)
export BC_HOME=${CLIENT_1}
SENDER_1=$(getAddr $RICH)
RECV_1=$(getAddr $POOR)
assertFalse "line=${LINENO}, requires arg" "${CLIENT_EXE} query account 2>/dev/null"
assertFalse "line=${LINENO}, has no genesis account" "${CLIENT_EXE} query account $RECV_1 2>/dev/null"
checkAccount $SENDER_1 "0" "9007199254740992"
checkAccount $SENDER_1 "9007199254740992"
export BC_HOME=${CLIENT_2}
SENDER_2=$(getAddr $RICH)
@ -76,102 +76,281 @@ test00GetAccount() {
assertFalse "line=${LINENO}, requires arg" "${CLIENT_EXE} query account 2>/dev/null"
assertFalse "line=${LINENO}, has no genesis account" "${CLIENT_EXE} query account $RECV_2 2>/dev/null"
checkAccount $SENDER_2 "0" "9007199254740992"
checkAccount $SENDER_2 "9007199254740992"
# Make sure that they have different addresses on both chains (they are random keys)
assertNotEquals "line=${LINENO}, sender keys must be different" "$SENDER_1" "$SENDER_2"
assertNotEquals "line=${LINENO}, recipient keys must be different" "$RECV_1" "$RECV_2"
}
test01SendIBCTx() {
# Trigger a cross-chain sendTx... from RICH on chain1 to POOR on chain2
# we make sure the money was reduced, but nothing arrived
SENDER=$(BC_HOME=${CLIENT_1} getAddr $RICH)
test01RegisterChains() {
# let's get the root seeds to cross-register them
ROOT_1="$BASE_DIR_1/root_seed.json"
${CLIENT_EXE} seeds export $ROOT_1 --home=${CLIENT_1}
assertTrue "line=${LINENO}, export seed failed" $?
ROOT_2="$BASE_DIR_2/root_seed.json"
${CLIENT_EXE} seeds export $ROOT_2 --home=${CLIENT_2}
assertTrue "line=${LINENO}, export seed failed" $?
# register chain2 on chain1
TX=$(echo qwertyuiop | ${CLIENT_EXE} tx ibc-register \
--sequence=1 --seed=${ROOT_2} --name=$POOR --home=${CLIENT_1})
txSucceeded $? "$TX" "register chain2 on chain 1"
# an example to quit early if there is no point in more tests
if [ $? != 0 ]; then echo "aborting!"; return 1; fi
# this is used later to check data
REG_HEIGHT=$(echo $TX | jq .height)
# register chain1 on chain2 (no money needed... yet)
TX=$(echo qwertyuiop | ${CLIENT_EXE} tx ibc-register \
--sequence=1 --seed=${ROOT_1} --name=$POOR --home=${CLIENT_2})
txSucceeded $? "$TX" "register chain1 on chain 2"
# an example to quit early if there is no point in more tests
if [ $? != 0 ]; then echo "aborting!"; return 1; fi
}
test02UpdateChains() {
# let's get the root seeds to cross-register them
UPDATE_1="$BASE_DIR_1/seed_1.json"
${CLIENT_EXE} seeds update --home=${CLIENT_1} > /dev/null
${CLIENT_EXE} seeds export $UPDATE_1 --home=${CLIENT_1}
assertTrue "line=${LINENO}, export seed failed" $?
# make sure it is newer than the other....
assertNewHeight "line=${LINENO}" $ROOT_1 $UPDATE_1
UPDATE_2="$BASE_DIR_2/seed_2.json"
${CLIENT_EXE} seeds update --home=${CLIENT_2} > /dev/null
${CLIENT_EXE} seeds export $UPDATE_2 --home=${CLIENT_2}
assertTrue "line=${LINENO}, export seed failed" $?
assertNewHeight "line=${LINENO}" $ROOT_2 $UPDATE_2
# this is used later to check query data
REGISTER_2_HEIGHT=$(cat $ROOT_2 | jq .checkpoint.header.height)
UPDATE_2_HEIGHT=$(cat $UPDATE_2 | jq .checkpoint.header.height)
# update chain2 on chain1
TX=$(echo qwertyuiop | ${CLIENT_EXE} tx ibc-update \
--sequence=2 --seed=${UPDATE_2} --name=$POOR --home=${CLIENT_1})
txSucceeded $? "$TX" "update chain2 on chain 1"
# an example to quit early if there is no point in more tests
if [ $? != 0 ]; then echo "aborting!"; return 1; fi
# update chain1 on chain2 (no money needed... yet)
TX=$(echo qwertyuiop | ${CLIENT_EXE} tx ibc-update \
--sequence=2 --seed=${UPDATE_1} --name=$POOR --home=${CLIENT_2})
txSucceeded $? "$TX" "update chain1 on chain 2"
# an example to quit early if there is no point in more tests
if [ $? != 0 ]; then echo "aborting!"; return 1; fi
}
# make sure all query commands about ibc work...
test03QueryIBC() {
# just test on one chain, as they are all symetrical
export BC_HOME=${CLIENT_1}
# make sure we can list all chains
CHAINS=$(${CLIENT_EXE} query ibc chains)
assertTrue "line=${LINENO}, cannot query chains" $?
assertEquals "1" $(echo $CHAINS | jq '.data | length')
assertEquals "line=${LINENO}" "\"$CHAIN_ID_2\"" $(echo $CHAINS | jq '.data[0]')
# error on unknown chain, data on proper chain
assertFalse "line=${LINENO}, unknown chain" "${CLIENT_EXE} query ibc chain random 2>/dev/null"
CHAIN_INFO=$(${CLIENT_EXE} query ibc chain $CHAIN_ID_2)
assertTrue "line=${LINENO}, cannot query chain $CHAIN_ID_2" $?
assertEquals "line=${LINENO}, register height" $REG_HEIGHT $(echo $CHAIN_INFO | jq .data.registered_at)
assertEquals "line=${LINENO}, tracked height" $UPDATE_2_HEIGHT $(echo $CHAIN_INFO | jq .data.remote_block)
}
# Trigger a cross-chain sendTx... from RICH on chain1 to POOR on chain2
# we make sure the money was reduced, but nothing arrived
test04SendIBCPacket() {
export BC_HOME=${CLIENT_1}
# make sure there are no packets yet
PACKETS=$(${CLIENT_EXE} query ibc packets --to=$CHAIN_ID_2 2>/dev/null)
assertFalse "line=${LINENO}, packet query" $?
SENDER=$(getAddr $RICH)
RECV=$(BC_HOME=${CLIENT_2} getAddr $POOR)
export BC_HOME=${CLIENT_1}
TX=$(echo qwertyuiop | ${CLIENT_EXE} tx send --amount=20002mycoin \
--sequence=1 --to=${CHAIN_ID_2}/${RECV} --name=$RICH)
txSucceeded $? "$TX" "${CHAIN_ID_2}/${RECV}"
# an example to quit early if there is no point in more tests
--to=${CHAIN_ID_2}::${RECV} --name=$RICH)
txSucceeded $? "$TX" "${CHAIN_ID_2}::${RECV}"
# quit early if there is no point in more tests
if [ $? != 0 ]; then echo "aborting!"; return 1; fi
HASH=$(echo $TX | jq .hash | tr -d \")
TX_HEIGHT=$(echo $TX | jq .height)
# Make sure balance went down and tx is indexed
checkAccount $SENDER "1" "9007199254720990"
checkAccount $SENDER "9007199254720990"
checkSendTx $HASH $TX_HEIGHT $SENDER "20002"
# Make sure nothing arrived - yet
waitForBlock ${PORT_1}
# look, we wrote a packet
PACKETS=$(${CLIENT_EXE} query ibc packets --to=$CHAIN_ID_2)
assertTrue "line=${LINENO}, packets query" $?
assertEquals "line=${LINENO}, packet count" 1 $(echo $PACKETS | jq .data)
# and look at the packet itself
PACKET=$(${CLIENT_EXE} query ibc packet --to=$CHAIN_ID_2 --sequence=0)
assertTrue "line=${LINENO}, packet query" $?
assertEquals "line=${LINENO}, proper src" "\"$CHAIN_ID_1\"" $(echo $PACKET | jq .src_chain)
assertEquals "line=${LINENO}, proper dest" "\"$CHAIN_ID_2\"" $(echo $PACKET | jq .packet.dest_chain)
assertEquals "line=${LINENO}, proper sequence" "0" $(echo $PACKET | jq .packet.sequence)
# nothing arrived
ARRIVED=$(${CLIENT_EXE} query ibc packets --from=$CHAIN_ID_1 --home=$CLIENT_2 2>/dev/null)
assertFalse "line=${LINENO}, packet query" $?
assertFalse "line=${LINENO}, no relay running" "BC_HOME=${CLIENT_2} ${CLIENT_EXE} query account $RECV"
# Start the relay and wait a few blocks...
# (already sent a tx on chain1, so use higher sequence)
startRelay 2 1
if [ $? != 0 ]; then echo "can't start relay"; cat ${BASE_DIR_1}/../relay.log; return 1; fi
# Give it a little time, then make sure the money arrived
echo "waiting for relay..."
sleep 1
waitForBlock ${PORT_1}
waitForBlock ${PORT_2}
# Check the new account
echo "checking ibc recipient..."
BC_HOME=${CLIENT_2} checkAccount $RECV "0" "20002"
# Stop relay
printf "stoping relay\n"
kill -9 $PID_RELAY
}
# StartRelay $seq1 $seq2
# startRelay hooks up a relay between chain1 and chain2
# it needs the proper sequence number for $RICH on chain1 and chain2 as args
startRelay() {
# Send some cash to the default key, so it can send messages
RELAY_KEY=${BASE_DIR_1}/server/key.json
RELAY_ADDR=$(cat $RELAY_KEY | jq .address | tr -d \")
echo starting relay $PID_RELAY ...
# Get paid on chain1
export BC_HOME=${CLIENT_1}
SENDER=$(getAddr $RICH)
RES=$(echo qwertyuiop | ${CLIENT_EXE} tx send --amount=100000mycoin \
--sequence=$1 --to=$RELAY_ADDR --name=$RICH)
txSucceeded $? "$RES" "$RELAY_ADDR"
if [ $? != 0 ]; then echo "can't pay chain1!"; return 1; fi
# Get paid on chain2
test05ReceiveIBCPacket() {
export BC_HOME=${CLIENT_2}
SENDER=$(getAddr $RICH)
RES=$(echo qwertyuiop | ${CLIENT_EXE} tx send --amount=100000mycoin \
--sequence=$2 --to=$RELAY_ADDR --name=$RICH)
txSucceeded $? "$RES" "$RELAY_ADDR"
if [ $? != 0 ]; then echo "can't pay chain2!"; return 1; fi
# Initialize the relay (register both chains)
${SERVER_EXE} relay init --chain1-id=$CHAIN_ID_1 --chain2-id=$CHAIN_ID_2 \
--chain1-addr=tcp://localhost:${PORT_1} --chain2-addr=tcp://localhost:${PORT_2} \
--genesis1=${BASE_DIR_1}/server/genesis.json --genesis2=${BASE_DIR_2}/server/genesis.json \
--from=$RELAY_KEY > ${BASE_DIR_1}/../relay.log
if [ $? != 0 ]; then echo "can't initialize relays"; cat ${BASE_DIR_1}/../relay.log; return 1; fi
# make some credit, so we can accept the packet
TX=$(echo qwertyuiop | ${CLIENT_EXE} tx credit --amount=60006mycoin --to=$CHAIN_ID_1:: --name=$RICH)
txSucceeded $? "$TX" "${CHAIN_ID_1}::"
checkAccount $CHAIN_ID_1:: "60006"
# Now start the relay (constantly send packets)
${SERVER_EXE} relay start --chain1-id=$CHAIN_ID_1 --chain2-id=$CHAIN_ID_2 \
--chain1-addr=tcp://localhost:${PORT_1} --chain2-addr=tcp://localhost:${PORT_2} \
--from=$RELAY_KEY >> ${BASE_DIR_1}/../relay.log &
sleep 2
PID_RELAY=$!
disown
# now, we try to post it.... (this is PACKET from last test)
# Return an error if it dies in the first two seconds to make sure it is running
ps $PID_RELAY >/dev/null
# get the seed and post it
SRC_HEIGHT=$(echo $PACKET | jq .src_height)
# FIXME: this should auto-update on proofs...
${CLIENT_EXE} seeds update --height=$SRC_HEIGHT --home=${CLIENT_1} > /dev/null
assertTrue "line=${LINENO}, update seed failed" $?
PACKET_SEED="$BASE_DIR_1/packet_seed.json"
${CLIENT_EXE} seeds export $PACKET_SEED --home=${CLIENT_1} #--height=$SRC_HEIGHT
assertTrue "line=${LINENO}, export seed failed" $?
# echo "**** SEED ****"
# cat $PACKET_SEED | jq .
TX=$(echo qwertyuiop | ${CLIENT_EXE} tx ibc-update \
--seed=${PACKET_SEED} --name=$POOR)
txSucceeded $? "$TX" "prepare packet chain1 on chain 2"
# an example to quit early if there is no point in more tests
if [ $? != 0 ]; then echo "aborting!"; return 1; fi
# write the packet to the file
POST_PACKET="$BASE_DIR_1/post_packet.json"
echo $PACKET > $POST_PACKET
# echo "**** POST ****"
# cat $POST_PACKET | jq .
# post it as a tx (cross-fingers)
TX=$(echo qwertyuiop | ${CLIENT_EXE} tx ibc-post \
--packet=${POST_PACKET} --name=$POOR)
txSucceeded $? "$TX" "post packet from chain1 on chain 2"
# TODO: more queries on stuff...
# look, we wrote a packet
PACKETS=$(${CLIENT_EXE} query ibc packets --from=$CHAIN_ID_1)
assertTrue "line=${LINENO}, packets query" $?
assertEquals "line=${LINENO}, packet count" 1 $(echo $PACKETS | jq .data)
}
# XXX Ex Usage: assertNewHeight $MSG $SEED_1 $SEED_2
# Desc: Asserts that seed2 has a higher block height than seed 1
assertNewHeight() {
H1=$(cat $2 | jq .checkpoint.header.height)
H2=$(cat $3 | jq .checkpoint.header.height)
assertTrue "$MSG" "test $H2 -gt $H1"
return $?
}
# test01SendIBCTx() {
# # Trigger a cross-chain sendTx... from RICH on chain1 to POOR on chain2
# # we make sure the money was reduced, but nothing arrived
# SENDER=$(BC_HOME=${CLIENT_1} getAddr $RICH)
# RECV=$(BC_HOME=${CLIENT_2} getAddr $POOR)
# export BC_HOME=${CLIENT_1}
# TX=$(echo qwertyuiop | ${CLIENT_EXE} tx send --amount=20002mycoin \
# --sequence=1 --to=${CHAIN_ID_2}/${RECV} --name=$RICH)
# txSucceeded $? "$TX" "${CHAIN_ID_2}/${RECV}"
# # an example to quit early if there is no point in more tests
# if [ $? != 0 ]; then echo "aborting!"; return 1; fi
# HASH=$(echo $TX | jq .hash | tr -d \")
# TX_HEIGHT=$(echo $TX | jq .height)
# # Make sure balance went down and tx is indexed
# checkAccount $SENDER "1" "9007199254720990"
# checkSendTx $HASH $TX_HEIGHT $SENDER "20002"
# # Make sure nothing arrived - yet
# waitForBlock ${PORT_1}
# assertFalse "line=${LINENO}, no relay running" "BC_HOME=${CLIENT_2} ${CLIENT_EXE} query account $RECV"
# # Start the relay and wait a few blocks...
# # (already sent a tx on chain1, so use higher sequence)
# startRelay 2 1
# if [ $? != 0 ]; then echo "can't start relay"; cat ${BASE_DIR_1}/../relay.log; return 1; fi
# # Give it a little time, then make sure the money arrived
# echo "waiting for relay..."
# sleep 1
# waitForBlock ${PORT_1}
# waitForBlock ${PORT_2}
# # Check the new account
# echo "checking ibc recipient..."
# BC_HOME=${CLIENT_2} checkAccount $RECV "0" "20002"
# # Stop relay
# printf "stoping relay\n"
# kill -9 $PID_RELAY
# }
# # StartRelay $seq1 $seq2
# # startRelay hooks up a relay between chain1 and chain2
# # it needs the proper sequence number for $RICH on chain1 and chain2 as args
# startRelay() {
# # Send some cash to the default key, so it can send messages
# RELAY_KEY=${BASE_DIR_1}/server/key.json
# RELAY_ADDR=$(cat $RELAY_KEY | jq .address | tr -d \")
# echo starting relay $PID_RELAY ...
# # Get paid on chain1
# export BC_HOME=${CLIENT_1}
# SENDER=$(getAddr $RICH)
# RES=$(echo qwertyuiop | ${CLIENT_EXE} tx send --amount=100000mycoin \
# --sequence=$1 --to=$RELAY_ADDR --name=$RICH)
# txSucceeded $? "$RES" "$RELAY_ADDR"
# if [ $? != 0 ]; then echo "can't pay chain1!"; return 1; fi
# # Get paid on chain2
# export BC_HOME=${CLIENT_2}
# SENDER=$(getAddr $RICH)
# RES=$(echo qwertyuiop | ${CLIENT_EXE} tx send --amount=100000mycoin \
# --sequence=$2 --to=$RELAY_ADDR --name=$RICH)
# txSucceeded $? "$RES" "$RELAY_ADDR"
# if [ $? != 0 ]; then echo "can't pay chain2!"; return 1; fi
# # Initialize the relay (register both chains)
# ${SERVER_EXE} relay init --chain1-id=$CHAIN_ID_1 --chain2-id=$CHAIN_ID_2 \
# --chain1-addr=tcp://localhost:${PORT_1} --chain2-addr=tcp://localhost:${PORT_2} \
# --genesis1=${BASE_DIR_1}/server/genesis.json --genesis2=${BASE_DIR_2}/server/genesis.json \
# --from=$RELAY_KEY > ${BASE_DIR_1}/../relay.log
# if [ $? != 0 ]; then echo "can't initialize relays"; cat ${BASE_DIR_1}/../relay.log; return 1; fi
# # Now start the relay (constantly send packets)
# ${SERVER_EXE} relay start --chain1-id=$CHAIN_ID_1 --chain2-id=$CHAIN_ID_2 \
# --chain1-addr=tcp://localhost:${PORT_1} --chain2-addr=tcp://localhost:${PORT_2} \
# --from=$RELAY_KEY >> ${BASE_DIR_1}/../relay.log &
# sleep 2
# PID_RELAY=$!
# disown
# # Return an error if it dies in the first two seconds to make sure it is running
# ps $PID_RELAY >/dev/null
# return $?
# }
# Load common then run these tests with shunit2!
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" #get this files directory
. $DIR/common.sh

View File

@ -98,7 +98,7 @@ test02GetSecure() {
# assertFalse "missing height" "${CLIENT_EXE} rpc headers"
HEADERS=$(${CLIENT_EXE} rpc headers --min=$CHEIGHT --max=$HEIGHT)
assertTrue "line=${LINENO}, get headers" "$?"
assertEquals "line=${LINENO}, proper height" "$HEIGHT" $(echo $HEADERS | jq '.last_height')
assertEquals "line=${LINENO}, proper height" "$HEIGHT" $(echo $HEADERS | jq '.block_metas[0].header.height')
assertEquals "line=${LINENO}, two headers" "2" $(echo $HEADERS | jq '.block_metas | length')
# should we check these headers?
CHEAD=$(echo $COMMIT | jq .header)

15
tx.go
View File

@ -1,6 +1,8 @@
package basecoin
import (
"strings"
"github.com/tendermint/go-wire/data"
"github.com/tendermint/basecoin/errors"
@ -75,3 +77,16 @@ func (t Tx) GetKind() (string, error) {
// grab the type we used in json
return text.Kind, nil
}
func (t Tx) GetMod() (string, error) {
kind, err := t.GetKind()
if err != nil {
return "", err
}
parts := strings.SplitN(kind, "/", 2)
if len(parts) != 2 {
// TODO: return "base"?
return "", errors.ErrUnknownTxType(t)
}
return parts[0], nil
}