diff --git a/Makefile b/Makefile index 0d3eac4ce..089ee47c4 100644 --- a/Makefile +++ b/Makefile @@ -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} diff --git a/app/app.go b/app/app.go index d27ee26e3..ca62ea80f 100644 --- a/app/app.go +++ b/app/app.go @@ -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()) diff --git a/app/app_test.go b/app/app_test.go index d09a69d23..4bef8443c 100644 --- a/app/app_test.go +++ b/app/app_test.go @@ -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 diff --git a/benchmarks/app_test.go b/benchmarks/app_test.go index 4d0c228f8..96487afe0 100644 --- a/benchmarks/app_test.go +++ b/benchmarks/app_test.go @@ -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( diff --git a/client/commands/common.go b/client/commands/common.go index a7ea8da7d..4bb565b6f 100644 --- a/client/commands/common.go +++ b/client/commands/common.go @@ -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 } diff --git a/client/commands/seeds/export.go b/client/commands/seeds/export.go index 1ac3ac42a..b37c11bfb 100644 --- a/client/commands/seeds/export.go +++ b/client/commands/seeds/export.go @@ -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) } diff --git a/client/commands/seeds/update.go b/client/commands/seeds/update.go index 153f090f8..a3be67ace 100644 --- a/client/commands/seeds/update.go +++ b/client/commands/seeds/update.go @@ -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 diff --git a/cmd/basecli/main.go b/cmd/basecli/main.go index 02335e390..6813dd916 100644 --- a/cmd/basecli/main.go +++ b/cmd/basecli/main.go @@ -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 diff --git a/cmd/basecoin/commands/init.go b/cmd/basecoin/commands/init.go index ec0d62435..ce9bcff40 100644 --- a/cmd/basecoin/commands/init.go +++ b/cmd/basecoin/commands/init.go @@ -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) } diff --git a/cmd/basecoin/main.go b/cmd/basecoin/main.go index 33ba8c1d4..b99482e1d 100644 --- a/cmd/basecoin/main.go +++ b/cmd/basecoin/main.go @@ -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, diff --git a/context.go b/context.go index 4da626ed3..d22102e94 100644 --- a/context.go +++ b/context.go @@ -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 { diff --git a/docs/guide/counter/plugins/counter/counter.go b/docs/guide/counter/plugins/counter/counter.go index 16a10650c..e9c2b503d 100644 --- a/docs/guide/counter/plugins/counter/counter.go +++ b/docs/guide/counter/plugins/counter/counter.go @@ -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 diff --git a/errors/common.go b/errors/common.go index f4fd66deb..64d45c082 100644 --- a/errors/common.go +++ b/errors/common.go @@ -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) } diff --git a/errors/helpers.go b/errors/helpers.go new file mode 100644 index 000000000..c5286575e --- /dev/null +++ b/errors/helpers.go @@ -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 +} diff --git a/glide.lock b/glide.lock index f68a89d0c..5071a9ab7 100644 --- a/glide.lock +++ b/glide.lock @@ -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 diff --git a/glide.yaml b/glide.yaml index be5c66c3d..e10ba05f9 100644 --- a/glide.yaml +++ b/glide.yaml @@ -22,7 +22,7 @@ import: subpackages: - data - package: github.com/tendermint/light-client - version: 1c53d04dcc65c2fd15526152ed0651af10a09982 + version: unstable subpackages: - proofs - certifiers diff --git a/modules/coin/bench_test.go b/modules/coin/bench_test.go index 34bf39bd1..bc83ea90c 100644 --- a/modules/coin/bench_test.go +++ b/modules/coin/bench_test.go @@ -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) diff --git a/modules/coin/commands/query.go b/modules/coin/commands/query.go index e77614bea..1faabb621 100644 --- a/modules/coin/commands/query.go +++ b/modules/coin/commands/query.go @@ -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{} diff --git a/modules/coin/commands/tx.go b/modules/coin/commands/tx.go index 4f130ed8b..aa23fab82 100644 --- a/modules/coin/commands/tx.go +++ b/modules/coin/commands/tx.go @@ -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 ,...") 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 ,...") } -// 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 == "" { diff --git a/modules/coin/errors.go b/modules/coin/errors.go index 49513f433..39c334e8b 100644 --- a/modules/coin/errors.go +++ b/modules/coin/errors.go @@ -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) -} diff --git a/modules/coin/handler.go b/modules/coin/handler.go index 08fc2e539..e22dacd26 100644 --- a/modules/coin/handler.go +++ b/modules/coin/handler.go @@ -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 } diff --git a/modules/coin/handler_test.go b/modules/coin/handler_test.go index 11959eccf..b0f20c42f 100644 --- a/modules/coin/handler_test.go +++ b/modules/coin/handler_test.go @@ -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) + } + } } diff --git a/modules/coin/ibc_test.go b/modules/coin/ibc_test.go new file mode 100644 index 000000000..7b4014a32 --- /dev/null +++ b/modules/coin/ibc_test.go @@ -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) + } +} diff --git a/modules/coin/store.go b/modules/coin/store.go index 4d1d4c109..58e031961 100644 --- a/modules/coin/store.go +++ b/modules/coin/store.go @@ -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... +} diff --git a/modules/coin/tx.go b/modules/coin/tx.go index b3598970e..bc5f77e44 100644 --- a/modules/coin/tx.go +++ b/modules/coin/tx.go @@ -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 +} diff --git a/modules/fee/handler_test.go b/modules/fee/handler_test.go index b775c2d56..000d99f0b 100644 --- a/modules/fee/handler_test.go +++ b/modules/fee/handler_test.go @@ -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) diff --git a/modules/fee/tx.go b/modules/fee/tx.go index d2f61f732..20ce861fd 100644 --- a/modules/fee/tx.go +++ b/modules/fee/tx.go @@ -7,7 +7,7 @@ import ( // nolint const ( - ByteFees = 0x21 + ByteFees = 0x28 TypeFees = NameFee + "/tx" ) diff --git a/modules/ibc/commands/query.go b/modules/ibc/commands/query.go new file mode 100644 index 000000000..38c3d01e1 --- /dev/null +++ b/modules/ibc/commands/query.go @@ -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 +} diff --git a/modules/ibc/commands/tx.go b/modules/ibc/commands/tx.go new file mode 100644 index 000000000..475d35809 --- /dev/null +++ b/modules/ibc/commands/tx.go @@ -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") +} diff --git a/modules/ibc/errors.go b/modules/ibc/errors.go new file mode 100644 index 000000000..8fafe7a73 --- /dev/null +++ b/modules/ibc/errors.go @@ -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) +} diff --git a/modules/ibc/handler.go b/modules/ibc/handler.go new file mode 100644 index 000000000..4665f5f32 --- /dev/null +++ b/modules/ibc/handler.go @@ -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 +} diff --git a/modules/ibc/ibc_test.go b/modules/ibc/ibc_test.go new file mode 100644 index 000000000..c31aeae53 --- /dev/null +++ b/modules/ibc/ibc_test.go @@ -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) + } + } +} diff --git a/modules/ibc/keys.go b/modules/ibc/keys.go new file mode 100644 index 000000000..e520d1888 --- /dev/null +++ b/modules/ibc/keys.go @@ -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))) +} diff --git a/modules/ibc/middleware.go b/modules/ibc/middleware.go new file mode 100644 index 000000000..fe6176964 --- /dev/null +++ b/modules/ibc/middleware.go @@ -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 +} diff --git a/modules/ibc/provider.go b/modules/ibc/provider.go new file mode 100644 index 000000000..d5fb877da --- /dev/null +++ b/modules/ibc/provider.go @@ -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 +} diff --git a/modules/ibc/provider_test.go b/modules/ibc/provider_test.go new file mode 100644 index 000000000..6028570ea --- /dev/null +++ b/modules/ibc/provider_test.go @@ -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()) + } + +} diff --git a/modules/ibc/store.go b/modules/ibc/store.go new file mode 100644 index 000000000..e675455d7 --- /dev/null +++ b/modules/ibc/store.go @@ -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) +} diff --git a/modules/ibc/test_helpers.go b/modules/ibc/test_helpers.go new file mode 100644 index 000000000..9d118141e --- /dev/null +++ b/modules/ibc/test_helpers.go @@ -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) +} diff --git a/modules/ibc/tx.go b/modules/ibc/tx.go new file mode 100644 index 000000000..8ee4d934c --- /dev/null +++ b/modules/ibc/tx.go @@ -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} +} diff --git a/modules/roles/commands/query.go b/modules/roles/commands/query.go index d7c97a8e2..6a333f211 100644 --- a/modules/roles/commands/query.go +++ b/modules/roles/commands/query.go @@ -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 } diff --git a/modules/roles/commands/tx.go b/modules/roles/commands/tx.go index 710413142..d0492445c 100644 --- a/modules/roles/commands/tx.go +++ b/modules/roles/commands/tx.go @@ -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 { diff --git a/plugins/ibc/ibc.go b/plugins/ibc/ibc.go deleted file mode 100644 index 0117a101e..000000000 --- a/plugins/ibc/ibc.go +++ /dev/null @@ -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 -// } diff --git a/plugins/ibc/ibc_test.go b/plugins/ibc/ibc_test.go deleted file mode 100644 index 2950633c5..000000000 --- a/plugins/ibc/ibc_test.go +++ /dev/null @@ -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: "", -// }, -// }})) -// 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 -// } diff --git a/stack/context.go b/stack/context.go index e688a5a08..7359adbe1 100644 --- a/stack/context.go +++ b/stack/context.go @@ -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, } } diff --git a/stack/middleware.go b/stack/middleware.go index e3400ff58..c5f6a9b71 100644 --- a/stack/middleware.go +++ b/stack/middleware.go @@ -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) } diff --git a/stack/middleware_test.go b/stack/middleware_test.go index 1b4ed8df3..bb33a9a62 100644 --- a/stack/middleware_test.go +++ b/stack/middleware_test.go @@ -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) diff --git a/state/queue.go b/state/queue.go new file mode 100644 index 000000000..756f90bb6 --- /dev/null +++ b/state/queue.go @@ -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 +} diff --git a/state/queue_test.go b/state/queue_test.go new file mode 100644 index 000000000..d6212be90 --- /dev/null +++ b/state/queue_test.go @@ -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()) + } +} diff --git a/state/set.go b/state/set.go new file mode 100644 index 000000000..ea3a0ca24 --- /dev/null +++ b/state/set.go @@ -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 +} diff --git a/state/set_test.go b/state/set_test.go new file mode 100644 index 000000000..2c2f3a5d2 --- /dev/null +++ b/state/set_test.go @@ -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) +} diff --git a/state/span.go b/state/span.go new file mode 100644 index 000000000..5b9708417 --- /dev/null +++ b/state/span.go @@ -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) +} diff --git a/state/span_test.go b/state/span_test.go new file mode 100644 index 000000000..8362f3d90 --- /dev/null +++ b/state/span_test.go @@ -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) + } + } + +} diff --git a/tests/cli/basictx.sh b/tests/cli/basictx.sh index d6ab7ea9e..dadc833c7 100755 --- a/tests/cli/basictx.sh +++ b/tests/cli/basictx.sh @@ -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 diff --git a/tests/cli/ibc.sh b/tests/cli/ibc.sh index 36f380ded..849516cf2 100755 --- a/tests/cli/ibc.sh +++ b/tests/cli/ibc.sh @@ -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 diff --git a/tests/cli/rpc.sh b/tests/cli/rpc.sh index 1026ee890..0a1575d10 100755 --- a/tests/cli/rpc.sh +++ b/tests/cli/rpc.sh @@ -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) diff --git a/tx.go b/tx.go index cc143ffc4..3a5626a7f 100644 --- a/tx.go +++ b/tx.go @@ -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 +}