diff --git a/app/app.go b/app/app.go index 2107dc17e6..27a3b9c98a 100644 --- a/app/app.go +++ b/app/app.go @@ -150,7 +150,7 @@ func (app *Basecoin) Query(reqQuery abci.RequestQuery) (resQuery abci.ResponseQu // handle special path for account info if reqQuery.Path == "/account" { reqQuery.Path = "/key" - reqQuery.Data = sm.AccountKey(reqQuery.Data) + reqQuery.Data = types.AccountKey(reqQuery.Data) } resQuery, err := app.eyesCli.QuerySync(reqQuery) diff --git a/app/app_test.go b/app/app_test.go index d37a6ade8e..1f150e8c48 100644 --- a/app/app_test.go +++ b/app/app_test.go @@ -9,7 +9,6 @@ import ( "github.com/stretchr/testify/require" abci "github.com/tendermint/abci/types" - "github.com/tendermint/basecoin/state" "github.com/tendermint/basecoin/types" wire "github.com/tendermint/go-wire" eyes "github.com/tendermint/merkleeyes/client" @@ -38,7 +37,7 @@ func newAppTest(t *testing.T) *appTest { // make a tx sending 5mycoin from each accIn to accOut func (at *appTest) getTx(seq int) *types.SendTx { - tx := types.GetTx(seq, at.accOut, at.accIn) + tx := types.MakeSendTx(seq, at.accOut, at.accIn) types.SignTx(at.chainID, tx, at.accIn) return tx } @@ -123,7 +122,7 @@ func TestSetOption(t *testing.T) { res = app.SetOption("base/account", string(accsInBytes)) require.EqualValues(res, "Success") // make sure it is set correctly, with some balance - acct := state.GetAccount(app.GetState(), accIn.PubKey.Address()) + acct := types.GetAccount(app.GetState(), accIn.PubKey.Address()) require.NotNil(acct) assert.Equal(accIn.Balance, acct.Balance) @@ -149,7 +148,7 @@ func TestSetOption(t *testing.T) { }` res = app.SetOption("base/account", unsortAcc) require.EqualValues(res, "Success") - acct = state.GetAccount(app.GetState(), unsortAddr) + acct = types.GetAccount(app.GetState(), unsortAddr) require.NotNil(acct) assert.True(acct.Balance.IsValid()) assert.Equal(unsortCoins, acct.Balance) diff --git a/cmd/basecli/commands/adapters.go b/cmd/basecli/commands/adapters.go index a1899019c1..516cb2f0df 100644 --- a/cmd/basecli/commands/adapters.go +++ b/cmd/basecli/commands/adapters.go @@ -14,7 +14,6 @@ import ( "github.com/tendermint/light-client/commands" "github.com/tendermint/light-client/proofs" - "github.com/tendermint/basecoin/state" btypes "github.com/tendermint/basecoin/types" ) @@ -23,7 +22,7 @@ type AccountPresenter struct{} func (_ AccountPresenter) MakeKey(str string) ([]byte, error) { res, err := hex.DecodeString(str) if err == nil { - res = state.AccountKey(res) + res = btypes.AccountKey(res) } return res, err } diff --git a/cmd/basecoin/main.go b/cmd/basecoin/main.go index 76ba6d5e93..5bc03e0fa6 100644 --- a/cmd/basecoin/main.go +++ b/cmd/basecoin/main.go @@ -18,6 +18,7 @@ func main() { RootCmd.AddCommand( commands.InitCmd, commands.StartCmd, + commands.RelayCmd, commands.TxCmd, commands.QueryCmd, commands.KeyCmd, diff --git a/cmd/commands/ibc.go b/cmd/commands/ibc.go index fa6d4758cc..4297bba422 100644 --- a/cmd/commands/ibc.go +++ b/cmd/commands/ibc.go @@ -196,13 +196,18 @@ func ibcPacketCreateTxCmd(cmd *cobra.Command, args []string) error { return err } + var payload ibc.Payload + if err := wire.ReadBinaryBytes(payloadBytes, &payload); err != nil { + return err + } + ibcTx := ibc.IBCPacketCreateTx{ Packet: ibc.Packet{ SrcChainID: fromChain, DstChainID: toChain, Sequence: sequence, Type: packetType, - Payload: payloadBytes, + Payload: payload, }, } diff --git a/cmd/commands/query.go b/cmd/commands/query.go index 1b46a6617a..330ba1514e 100644 --- a/cmd/commands/query.go +++ b/cmd/commands/query.go @@ -10,6 +10,7 @@ import ( "github.com/tendermint/go-wire" "github.com/tendermint/merkleeyes/iavl" + "github.com/tendermint/tendermint/rpc/client" tmtypes "github.com/tendermint/tendermint/types" ) @@ -120,7 +121,8 @@ func accountCmd(cmd *cobra.Command, args []string) error { return errors.Errorf("Account address (%v) is invalid hex: %v\n", addrHex, err) } - acc, err := getAcc(nodeFlag, addr) + httpClient := client.NewHTTP(nodeFlag, "/websocket") + acc, err := getAccWithClient(httpClient, addr) if err != nil { return err } diff --git a/cmd/commands/relay.go b/cmd/commands/relay.go new file mode 100644 index 0000000000..5a2cacfc96 --- /dev/null +++ b/cmd/commands/relay.go @@ -0,0 +1,247 @@ +package commands + +import ( + "fmt" + "strconv" + "time" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + + // "github.com/spf13/viper" + // "github.com/tendermint/tmlibs/cli" + // "github.com/tendermint/tmlibs/log" + + "github.com/tendermint/go-wire" + "github.com/tendermint/merkleeyes/iavl" + cmn "github.com/tendermint/tmlibs/common" + + "github.com/tendermint/basecoin/plugins/ibc" + "github.com/tendermint/basecoin/types" + "github.com/tendermint/tendermint/rpc/client" + tmtypes "github.com/tendermint/tendermint/types" +) + +var RelayCmd = &cobra.Command{ + Use: "relay", + Short: "Start basecoin relayer to relay IBC packets between chains", + RunE: relayCmd, +} + +//flags +var ( + chain1AddrFlag string + chain2AddrFlag string + + chain1IDFlag string + chain2IDFlag string + + fromFileFlag string +) + +func init() { + + flags := []Flag2Register{ + {&chain1AddrFlag, "chain1-addr", "tcp://localhost:46657", "Node address for chain1"}, + {&chain2AddrFlag, "chain2-addr", "tcp://localhost:36657", "Node address for chain2"}, + {&chain1IDFlag, "chain1-id", "test_chain_1", "ChainID for chain1"}, + {&chain2IDFlag, "chain2-id", "test_chain_2", "ChainID for chain2"}, + {&fromFileFlag, "from", "key.json", "Path to a private key to sign the transaction"}, + } + RegisterFlags(RelayCmd, flags) +} + +func loop(addr1, addr2, id1, id2 string) { + nextSeq := 0 + + // load the priv key + privKey, err := LoadKey(fromFlag) + if err != nil { + logger.Error(err.Error()) + cmn.PanicCrisis(err.Error()) + } + + // relay from chain1 to chain2 + thisRelayer := newRelayer(privKey, id2, addr2) + + logger.Info(fmt.Sprintf("Relaying from chain %v on %v to chain %v on %v", id1, addr1, id2, addr2)) + + httpClient := client.NewHTTP(addr1, "/websocket") + +OUTER: + for { + + time.Sleep(time.Second) + + // get the latest ibc packet sequence number + key := fmt.Sprintf("ibc,egress,%v,%v", id1, id2) + query, err := queryWithClient(httpClient, []byte(key)) + if err != nil { + logger.Error("Error querying for latest sequence", "key", key, "error", err.Error()) + continue OUTER + } + if len(query.Value) == 0 { + // nothing yet + continue OUTER + } + + seq, err := strconv.ParseUint(string(query.Value), 10, 64) + if err != nil { + logger.Error("Error parsing sequence number from query", "query.Value", query.Value, "error", err.Error()) + continue OUTER + } + seq -= 1 // seq is the packet count. -1 because 0-indexed + + if nextSeq <= int(seq) { + logger.Info("Got new packets", "last-sequence", nextSeq-1, "new-sequence", seq) + } + + // get all packets since the last one we relayed + for ; nextSeq <= int(seq); nextSeq++ { + key := fmt.Sprintf("ibc,egress,%v,%v,%d", id1, id2, nextSeq) + query, err := queryWithClient(httpClient, []byte(key)) + if err != nil { + logger.Error("Error querying for packet", "seqeuence", nextSeq, "key", key, "error", err.Error()) + continue OUTER + } + + var packet ibc.Packet + err = wire.ReadBinaryBytes(query.Value, &packet) + if err != nil { + logger.Error("Error unmarshalling packet", "key", key, "query.Value", query.Value, "error", err.Error()) + continue OUTER + } + + proof := new(iavl.IAVLProof) + err = wire.ReadBinaryBytes(query.Proof, &proof) + if err != nil { + logger.Error("Error unmarshalling proof", "query.Proof", query.Proof, "error", err.Error()) + continue OUTER + } + + // query.Height is actually for the next block, + // so wait a block before we fetch the header & commit + if err := waitForBlock(httpClient); err != nil { + logger.Error("Error waiting for a block", "addr", addr1, "error", err.Error()) + continue OUTER + } + + // get the header and commit from the height the query was done at + res, err := httpClient.Commit(int(query.Height)) + if err != nil { + logger.Error("Error fetching header and commits", "height", query.Height, "error", err.Error()) + continue OUTER + } + + // update the chain state on the other chain + updateTx := ibc.IBCUpdateChainTx{ + Header: *res.Header, + Commit: *res.Commit, + } + logger.Info("Updating chain", "src-chain", id1, "height", res.Header.Height, "appHash", res.Header.AppHash) + if err := thisRelayer.appTx(updateTx); err != nil { + logger.Error("Error creating/sending IBCUpdateChainTx", "error", err.Error()) + continue OUTER + } + + // relay the packet and proof + logger.Info("Relaying packet", "src-chain", id1, "height", query.Height, "sequence", nextSeq) + postTx := ibc.IBCPacketPostTx{ + FromChainID: id1, + FromChainHeight: query.Height, + Packet: packet, + Proof: proof, + } + + if err := thisRelayer.appTx(postTx); err != nil { + logger.Error("Error creating/sending IBCPacketPostTx", "error", err.Error()) + // dont `continue OUTER` here. the error might be eg. Already exists + // TODO: catch this programmatically ? + } + } + } +} + +type relayer struct { + privKey *Key + chainID string + nodeAddr string + client *client.HTTP +} + +func newRelayer(privKey *Key, chainID, nodeAddr string) *relayer { + httpClient := client.NewHTTP(nodeAddr, "/websocket") + return &relayer{ + privKey: privKey, + chainID: chainID, + nodeAddr: nodeAddr, + client: httpClient, + } +} + +func (r *relayer) appTx(ibcTx ibc.IBCTx) error { + acc, err := getAccWithClient(r.client, r.privKey.Address[:]) + if err != nil { + return err + } + sequence := acc.Sequence + 1 + + data := []byte(wire.BinaryBytes(struct { + ibc.IBCTx `json:"unwrap"` + }{ibcTx})) + + smallCoins := types.Coin{"mycoin", 1} + + input := types.NewTxInput(r.privKey.PubKey, types.Coins{smallCoins}, sequence) + tx := &types.AppTx{ + Gas: 0, + Fee: smallCoins, + Name: "IBC", + Input: input, + Data: data, + } + + tx.Input.Signature = r.privKey.Sign(tx.SignBytes(r.chainID)) + txBytes := []byte(wire.BinaryBytes(struct { + types.Tx `json:"unwrap"` + }{tx})) + + data, log, err := broadcastTxWithClient(r.client, txBytes) + if err != nil { + return err + } + _, _ = data, log + return nil +} + +// broadcast the transaction to tendermint +func broadcastTxWithClient(httpClient *client.HTTP, tx tmtypes.Tx) ([]byte, string, error) { + res, err := httpClient.BroadcastTxCommit(tx) + if err != nil { + return nil, "", errors.Errorf("Error on broadcast tx: %v", err) + } + + if !res.CheckTx.Code.IsOK() { + r := res.CheckTx + return nil, "", errors.Errorf("BroadcastTxCommit got non-zero exit code: %v. %X; %s", r.Code, r.Data, r.Log) + } + + if !res.DeliverTx.Code.IsOK() { + r := res.DeliverTx + return nil, "", errors.Errorf("BroadcastTxCommit got non-zero exit code: %v. %X; %s", r.Code, r.Data, r.Log) + } + + return res.DeliverTx.Data, res.DeliverTx.Log, nil +} + +func relayCmd(cmd *cobra.Command, args []string) error { + + go loop(chain1AddrFlag, chain2AddrFlag, chain1IDFlag, chain2IDFlag) + go loop(chain2AddrFlag, chain1AddrFlag, chain2IDFlag, chain1IDFlag) + + cmn.TrapSignal(func() { + // TODO: Cleanup + }) + return nil + +} diff --git a/cmd/commands/tx.go b/cmd/commands/tx.go index eae7f36a6f..2cde963b99 100644 --- a/cmd/commands/tx.go +++ b/cmd/commands/tx.go @@ -3,6 +3,7 @@ package commands import ( "encoding/hex" "fmt" + "strings" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -81,12 +82,29 @@ func init() { func sendTxCmd(cmd *cobra.Command, args []string) error { + var toHex string + var chainPrefix string + spl := strings.Split(toFlag, "/") + switch len(spl) { + case 1: + toHex = spl[0] + case 2: + chainPrefix = spl[0] + toHex = spl[1] + default: + return errors.Errorf("To address has too many slashes") + } + // convert destination address to bytes - to, err := hex.DecodeString(StripHex(toFlag)) + to, err := hex.DecodeString(StripHex(toHex)) if err != nil { return errors.Errorf("To address is invalid hex: %v\n", err) } + if chainPrefix != "" { + to = []byte(chainPrefix + "/" + string(to)) + } + // load the priv key privKey, err := LoadKey(fromFlag) if err != nil { @@ -220,12 +238,12 @@ func broadcastTx(tx types.Tx) ([]byte, string, error) { // if the sequence flag is set, return it; // else, fetch the account by querying the app and return the sequence number func getSeq(address []byte) (int, error) { - if seqFlag >= 0 { return seqFlag, nil } - acc, err := getAcc(txNodeFlag, address) + httpClient := client.NewHTTP(txNodeFlag, "/websocket") + acc, err := getAccWithClient(httpClient, address) if err != nil { return 0, err } diff --git a/cmd/commands/utils.go b/cmd/commands/utils.go index ae1f49aaa4..dc0b5f1620 100644 --- a/cmd/commands/utils.go +++ b/cmd/commands/utils.go @@ -11,7 +11,6 @@ import ( abci "github.com/tendermint/abci/types" wire "github.com/tendermint/go-wire" - "github.com/tendermint/basecoin/state" "github.com/tendermint/basecoin/types" client "github.com/tendermint/tendermint/rpc/client" @@ -101,6 +100,10 @@ func StripHex(s string) string { func Query(tmAddr string, key []byte) (*abci.ResultQuery, error) { httpClient := client.NewHTTP(tmAddr, "/websocket") + return queryWithClient(httpClient, key) +} + +func queryWithClient(httpClient *client.HTTP, key []byte) (*abci.ResultQuery, error) { res, err := httpClient.ABCIQuery("/key", key, true) if err != nil { return nil, errors.Errorf("Error calling /abci_query: %v", err) @@ -112,10 +115,10 @@ func Query(tmAddr string, key []byte) (*abci.ResultQuery, error) { } // fetch the account by querying the app -func getAcc(tmAddr string, address []byte) (*types.Account, error) { +func getAccWithClient(httpClient *client.HTTP, address []byte) (*types.Account, error) { - key := state.AccountKey(address) - response, err := Query(tmAddr, key) + key := types.AccountKey(address) + response, err := queryWithClient(httpClient, key) if err != nil { return nil, err } @@ -147,3 +150,23 @@ func getHeaderAndCommit(tmAddr string, height int) (*tmtypes.Header, *tmtypes.Co return header, commit, nil } + +func waitForBlock(httpClient *client.HTTP) error { + res, err := httpClient.Status() + if err != nil { + return err + } + + lastHeight := res.LatestBlockHeight + for { + res, err := httpClient.Status() + if err != nil { + return err + } + if res.LatestBlockHeight > lastHeight { + break + } + + } + return nil +} diff --git a/demo/start.sh b/demo/start.sh index f15a5fd5bd..490806fd62 100644 --- a/demo/start.sh +++ b/demo/start.sh @@ -122,16 +122,20 @@ ifExit echo "" echo "... creating egress packet on chain1" echo "" -# create a packet on chain1 destined for chain2 -PAYLOAD="DEADBEEF" #TODO -basecoin tx ibc --amount 10mycoin $CHAIN_FLAGS1 packet create --ibc_from $CHAIN_ID1 --to $CHAIN_ID2 --type coin --payload $PAYLOAD --ibc_sequence 1 +# send coins from chain1 to an address on chain2 +# TODO: dont hardcode the address +basecoin tx send --amount 10mycoin $CHAIN_FLAGS1 --to $CHAIN_ID2/053BA0F19616AFF975C8756A2CBFF04F408B4D47 ifExit +# alternative way to create packets (for testing) +# basecoin tx ibc --amount 10mycoin $CHAIN_FLAGS1 packet create --ibc_from $CHAIN_ID1 --to $CHAIN_ID2 --type coin --payload $PAYLOAD --ibc_sequence 0 + echo "" echo "... querying for packet data" echo "" # query for the packet data and proof -QUERY_RESULT=$(basecoin query ibc,egress,$CHAIN_ID1,$CHAIN_ID2,1) +# since we only sent one packet, the sequence number is 0 +QUERY_RESULT=$(basecoin query ibc,egress,$CHAIN_ID1,$CHAIN_ID2,0) ifExit HEIGHT=$(echo $QUERY_RESULT | jq .height) PACKET=$(echo $QUERY_RESULT | jq .value) @@ -187,7 +191,7 @@ echo "" echo "... checking if the packet is present on chain2" echo "" # query for the packet on chain2 -basecoin query --node tcp://localhost:36657 ibc,ingress,test_chain_2,test_chain_1,1 +basecoin query --node tcp://localhost:36657 ibc,ingress,test_chain_2,test_chain_1,0 ifExit echo "" diff --git a/glide.lock b/glide.lock index 675b418f6d..f82aa4d990 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: 997e4cc3339141ee01aa2adf656425a49ebf117e6ca9e81ba72b8f94fee3e86e -updated: 2017-05-17T12:25:00.580569867+02:00 +hash: 9d06ae13959cbb2835f5ae400a4b65e4bc329a567c949aec4aeab318c271da39 +updated: 2017-05-21T21:31:06.796806933-04:00 imports: - name: github.com/bgentry/speakeasy version: 4aabc24848ce5fd31929f7d1e4ea74d3709c14cd @@ -101,7 +101,7 @@ imports: - leveldb/table - leveldb/util - name: github.com/tendermint/abci - version: 5dabeffb35c027d7087a12149685daa68989168b + version: 864d1f80b36b440bde030a5c18d8ac3aa8c2949d subpackages: - client - example/dummy @@ -113,7 +113,7 @@ imports: - edwards25519 - extra25519 - name: github.com/tendermint/go-crypto - version: 438b16f1f84ef002d7408ecd6fc3a3974cbc9559 + version: 7dff40942a64cdeefefa9446b2d104750b349f8a subpackages: - cmd - keys @@ -122,7 +122,7 @@ imports: - keys/server/types - keys/storage/filestorage - name: github.com/tendermint/go-wire - version: 97beaedf0f4dbc035309157c92be3b30cc6e5d74 + version: 5f88da3dbc1a72844e6dfaf274ce87f851d488eb subpackages: - data - data/base58 @@ -139,13 +139,13 @@ imports: - commands/txs - proofs - name: github.com/tendermint/merkleeyes - version: c722818b460381bc5b82e38c73ff6e22a9df624d + version: a0e73e1ac3e18e12a007520a4ea2c9822256e307 subpackages: - app - client - iavl - name: github.com/tendermint/tendermint - version: 11b5d11e9eec170e1d3dce165f0270d5c0759d69 + version: 267f134d44e76efb2adef5f0c993da8a5d5bd1b8 subpackages: - blockchain - config @@ -170,7 +170,7 @@ imports: - types - version - name: github.com/tendermint/tmlibs - version: 8af1c70a8be17543eb33e9bfbbcdd8371e3201cc + version: 306795ae1d8e4f4a10dcc8bdb32a00455843c9d5 subpackages: - autofile - cli diff --git a/glide.yaml b/glide.yaml index b050008553..abc395a69c 100644 --- a/glide.yaml +++ b/glide.yaml @@ -6,17 +6,14 @@ import: - package: github.com/spf13/pflag - package: github.com/spf13/viper - package: github.com/tendermint/abci - version: develop subpackages: - server - types - package: github.com/tendermint/go-crypto - version: develop subpackages: - cmd - keys - package: github.com/tendermint/go-wire - version: develop subpackages: - data - package: github.com/tendermint/light-client @@ -28,7 +25,6 @@ import: - commands/txs - proofs - package: github.com/tendermint/merkleeyes - version: develop subpackages: - client - iavl @@ -44,7 +40,6 @@ import: - rpc/lib/types - types - package: github.com/tendermint/tmlibs - version: develop subpackages: - cli - common diff --git a/plugins/ibc/ibc.go b/plugins/ibc/ibc.go index 7502792898..eb8a27f1b0 100644 --- a/plugins/ibc/ibc.go +++ b/plugins/ibc/ibc.go @@ -4,7 +4,9 @@ import ( "bytes" "encoding/json" "errors" + "fmt" "net/url" + "strconv" "strings" abci "github.com/tendermint/abci/types" @@ -53,8 +55,108 @@ type Packet struct { SrcChainID string DstChainID string Sequence uint64 - Type string - Payload []byte + 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 types.KVStore, 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 types.KVStore, 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 types.KVStore, 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 types.KVStore, 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 types.Coins +} + +func (p CoinsPayload) Type() string { + return "coin" +} + +func (p CoinsPayload) ValidateBasic() abci.Result { + // TODO: validate + return abci.OK } //-------------------------------------------------------------------------------- @@ -299,8 +401,28 @@ func (sm *IBCStateMachine) runPacketCreateTx(tx IBCPacketCreateTx) { 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) { @@ -327,7 +449,7 @@ func (sm *IBCStateMachine) runPacketPostTx(tx IBCPacketPostTx) { return } - // Save new Packet + // Save new Packet (just for fun) save(sm.store, packetKeyIngress, packet) // Load Header and make sure it exists @@ -356,10 +478,24 @@ func (sm *IBCStateMachine) runPacketPostTx(tx IBCPacketPostTx) { ok := proof.Verify(packetKeyEgress, packetBytes, header.AppHash) if !ok { sm.res.Code = IBCCodeInvalidProof - sm.res.Log = "Proof is invalid" + 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 } diff --git a/plugins/ibc/ibc_test.go b/plugins/ibc/ibc_test.go index 1853a3a72d..93a913dec0 100644 --- a/plugins/ibc/ibc_test.go +++ b/plugins/ibc/ibc_test.go @@ -101,34 +101,19 @@ var testGenesisDoc = `{ }` func TestIBCGenesisFromString(t *testing.T) { - assert := assert.New(t) - eyesClient := eyes.NewLocalClient("", 0) store := types.NewKVCache(eyesClient) store.SetLogging() // Log all activity ibcPlugin := New() - ctx := types.CallContext{ - CallerAddress: nil, - CallerAccount: nil, - Coins: types.Coins{}, - } + ctx := types.NewCallContext(nil, nil, types.Coins{}) - res := ibcPlugin.RunTx(store, ctx, wire.BinaryBytes(struct{ IBCTx }{IBCRegisterChainTx{ - BlockchainGenesis{ - ChainID: "test_chain", - Genesis: testGenesisDoc, - }, - }})) - assert.True(res.IsOK(), res.Log) - t.Log(">>", strings.Join(store.GetLogLines(), "\n")) - store.ClearLogLines() + registerChain(t, ibcPlugin, store, ctx, "test_chain", testGenesisDoc) } //-------------------------------------------------------------------------------- -func TestIBCPlugin(t *testing.T) { - assert := assert.New(t) +func TestIBCPluginRegister(t *testing.T) { require := require.New(t) eyesClient := eyes.NewLocalClient("", 0) @@ -136,14 +121,10 @@ func TestIBCPlugin(t *testing.T) { store.SetLogging() // Log all activity ibcPlugin := New() - ctx := types.CallContext{ - CallerAddress: nil, - CallerAccount: nil, - Coins: types.Coins{}, - } + ctx := types.NewCallContext(nil, nil, types.Coins{}) chainID_1 := "test_chain" - genDoc_1, privAccs_1 := genGenesisDoc(chainID_1, 4) + genDoc_1, _ := genGenesisDoc(chainID_1, 4) genDocJSON_1, err := json.Marshal(genDoc_1) require.Nil(err) @@ -154,20 +135,10 @@ func TestIBCPlugin(t *testing.T) { Genesis: "", }, }})) - assert.Equal(IBCCodeEncodingError, res.Code) - t.Log(">>", strings.Join(store.GetLogLines(), "\n")) - store.ClearLogLines() + assertAndLog(t, store, res, IBCCodeEncodingError) // Successfully register a chain - res = ibcPlugin.RunTx(store, ctx, wire.BinaryBytes(struct{ IBCTx }{IBCRegisterChainTx{ - BlockchainGenesis{ - ChainID: "test_chain", - Genesis: string(genDocJSON_1), - }, - }})) - assert.True(res.IsOK(), res.Log) - t.Log(">>", strings.Join(store.GetLogLines(), "\n")) - store.ClearLogLines() + registerChain(t, ibcPlugin, store, ctx, "test_chain", string(genDocJSON_1)) // Duplicate request fails res = ibcPlugin.RunTx(store, ctx, wire.BinaryBytes(struct{ IBCTx }{IBCRegisterChainTx{ @@ -176,74 +147,82 @@ func TestIBCPlugin(t *testing.T) { Genesis: string(genDocJSON_1), }, }})) - assert.Equal(IBCCodeChainAlreadyExists, res.Code, res.Log) - t.Log(">>", strings.Join(store.GetLogLines(), "\n")) - store.ClearLogLines() + 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, types.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 := Packet{ - SrcChainID: "test_chain", - DstChainID: "dst_chain", - Sequence: 0, - Type: "data", - Payload: []byte("hello world"), - } - res = ibcPlugin.RunTx(store, ctx, wire.BinaryBytes(struct{ IBCTx }{IBCPacketCreateTx{ + packet := NewPacket("test_chain", "dst_chain", 0, DataPayload([]byte("hello world"))) + res := ibcPlugin.RunTx(store, ctx, wire.BinaryBytes(struct{ IBCTx }{IBCPacketCreateTx{ Packet: packet, }})) - assert.Equal(abci.CodeType_OK, res.Code, res.Log) - t.Log(">>", strings.Join(store.GetLogLines(), "\n")) - store.ClearLogLines() + assertAndLog(t, store, res, abci.CodeType_OK) // Post a duplicate packet res = ibcPlugin.RunTx(store, ctx, wire.BinaryBytes(struct{ IBCTx }{IBCPacketCreateTx{ Packet: packet, }})) - assert.Equal(IBCCodePacketAlreadyExists, res.Code, res.Log) - t.Log(">>", strings.Join(store.GetLogLines(), "\n")) - store.ClearLogLines() + 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, types.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 := tm.Header{ - ChainID: "test_chain", - Height: 999, - AppHash: appHash, - ValidatorsHash: []byte("must_exist"), // TODO make optional - } + header := newHeader("test_chain", 999, appHash, []byte("must_exist")) // Construct a Commit that signs above header - blockHash := header.Hash() - blockID := tm.BlockID{Hash: blockHash} - commit := tm.Commit{ - BlockID: blockID, - Precommits: make([]*tm.Vote, len(privAccs_1)), - } - for i, privAcc := range privAccs_1 { - 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 - } + commit := constructCommit(privAccs_1, header) // Update a chain res = ibcPlugin.RunTx(store, ctx, wire.BinaryBytes(struct{ IBCTx }{IBCUpdateChainTx{ Header: header, Commit: commit, }})) - assert.Equal(abci.CodeType_OK, res.Code, res.Log) - t.Log(">>", strings.Join(store.GetLogLines(), "\n")) - store.ClearLogLines() + assertAndLog(t, store, res, abci.CodeType_OK) // Get proof for the packet packetKey := toKey(_IBC, _EGRESS, @@ -268,12 +247,10 @@ func TestIBCPlugin(t *testing.T) { Packet: packet, Proof: proof, }})) - assert.Equal(abci.CodeType_OK, res.Code, res.Log) - t.Log(">>", strings.Join(store.GetLogLines(), "\n")) - store.ClearLogLines() + assertAndLog(t, store, res, abci.CodeType_OK) } -func TestIBCPluginBadCommit(t *testing.T) { +func TestIBCPluginPayloadCoins(t *testing.T) { assert := assert.New(t) require := require.New(t) @@ -282,11 +259,106 @@ func TestIBCPluginBadCommit(t *testing.T) { store.SetLogging() // Log all activity ibcPlugin := New() - ctx := types.CallContext{ - CallerAddress: nil, - CallerAccount: nil, - Coins: types.Coins{}, + coins := types.Coins{ + types.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 := types.Coins{types.Coin{"mycoin", 200}} + coinsGood := types.Coins{types.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, types.Coins{}) chainID_1 := "test_chain" genDoc_1, privAccs_1 := genGenesisDoc(chainID_1, 4) @@ -294,57 +366,24 @@ func TestIBCPluginBadCommit(t *testing.T) { require.Nil(err) // Successfully register a chain - res := ibcPlugin.RunTx(store, ctx, wire.BinaryBytes(struct{ IBCTx }{IBCRegisterChainTx{ - BlockchainGenesis{ - ChainID: "test_chain", - Genesis: string(genDocJSON_1), - }, - }})) - assert.True(res.IsOK(), res.Log) - t.Log(">>", strings.Join(store.GetLogLines(), "\n")) - store.ClearLogLines() + registerChain(t, ibcPlugin, store, ctx, "test_chain", string(genDocJSON_1)) // Construct a Header - header := tm.Header{ - ChainID: "test_chain", - Height: 999, - ValidatorsHash: []byte("must_exist"), // TODO make optional - } + header := newHeader("test_chain", 999, nil, []byte("must_exist")) // Construct a Commit that signs above header - blockHash := header.Hash() - blockID := tm.BlockID{Hash: blockHash} - commit := tm.Commit{ - BlockID: blockID, - Precommits: make([]*tm.Vote, len(privAccs_1)), - } - for i, privAcc := range privAccs_1 { - 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 - } + 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{ + res := ibcPlugin.RunTx(store, ctx, wire.BinaryBytes(struct{ IBCTx }{IBCUpdateChainTx{ Header: header, Commit: commit, }})) - assert.Equal(IBCCodeInvalidCommit, res.Code, res.Log) - t.Log(">>", strings.Join(store.GetLogLines(), "\n")) - store.ClearLogLines() + assertAndLog(t, store, res, IBCCodeInvalidCommit) } @@ -357,11 +396,7 @@ func TestIBCPluginBadProof(t *testing.T) { store.SetLogging() // Log all activity ibcPlugin := New() - ctx := types.CallContext{ - CallerAddress: nil, - CallerAccount: nil, - Coins: types.Coins{}, - } + ctx := types.NewCallContext(nil, nil, types.Coins{}) chainID_1 := "test_chain" genDoc_1, privAccs_1 := genGenesisDoc(chainID_1, 4) @@ -369,72 +404,30 @@ func TestIBCPluginBadProof(t *testing.T) { require.Nil(err) // Successfully register a chain - res := ibcPlugin.RunTx(store, ctx, wire.BinaryBytes(struct{ IBCTx }{IBCRegisterChainTx{ - BlockchainGenesis{ - ChainID: "test_chain", - Genesis: string(genDocJSON_1), - }, - }})) - assert.True(res.IsOK(), res.Log) - t.Log(">>", strings.Join(store.GetLogLines(), "\n")) - store.ClearLogLines() + registerChain(t, ibcPlugin, store, ctx, "test_chain", string(genDocJSON_1)) // Create a new packet (for testing) - packet := Packet{ - SrcChainID: "test_chain", - DstChainID: "dst_chain", - Sequence: 0, - Type: "data", - Payload: []byte("hello world"), - } - res = ibcPlugin.RunTx(store, ctx, wire.BinaryBytes(struct{ IBCTx }{IBCPacketCreateTx{ + packet := NewPacket("test_chain", "dst_chain", 0, DataPayload([]byte("hello world"))) + res := ibcPlugin.RunTx(store, ctx, wire.BinaryBytes(struct{ IBCTx }{IBCPacketCreateTx{ Packet: packet, }})) - assert.Equal(abci.CodeType_OK, res.Code, res.Log) - t.Log(">>", strings.Join(store.GetLogLines(), "\n")) - store.ClearLogLines() + assertAndLog(t, store, res, abci.CodeType_OK) // Construct a Header that includes the above packet. store.Sync() resCommit := eyesClient.CommitSync() appHash := resCommit.Data - header := tm.Header{ - ChainID: "test_chain", - Height: 999, - AppHash: appHash, - ValidatorsHash: []byte("must_exist"), // TODO make optional - } + header := newHeader("test_chain", 999, appHash, []byte("must_exist")) // Construct a Commit that signs above header - blockHash := header.Hash() - blockID := tm.BlockID{Hash: blockHash} - commit := tm.Commit{ - BlockID: blockID, - Precommits: make([]*tm.Vote, len(privAccs_1)), - } - for i, privAcc := range privAccs_1 { - 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 - } + commit := constructCommit(privAccs_1, header) // Update a chain res = ibcPlugin.RunTx(store, ctx, wire.BinaryBytes(struct{ IBCTx }{IBCUpdateChainTx{ Header: header, Commit: commit, }})) - assert.Equal(abci.CodeType_OK, res.Code, res.Log) - t.Log(">>", strings.Join(store.GetLogLines(), "\n")) - store.ClearLogLines() + assertAndLog(t, store, res, abci.CodeType_OK) // Get proof for the packet packetKey := toKey(_IBC, _EGRESS, @@ -462,7 +455,58 @@ func TestIBCPluginBadProof(t *testing.T) { Packet: packet, Proof: proof, }})) - assert.Equal(IBCCodeInvalidProof, res.Code, res.Log) + 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/state/execution.go b/state/execution.go index 246246846d..307c0d463c 100644 --- a/state/execution.go +++ b/state/execution.go @@ -2,9 +2,11 @@ package state import ( abci "github.com/tendermint/abci/types" - "github.com/tendermint/basecoin/types" cmn "github.com/tendermint/tmlibs/common" "github.com/tendermint/tmlibs/events" + + "github.com/tendermint/basecoin/plugins/ibc" + "github.com/tendermint/basecoin/types" ) // If the tx is invalid, a TMSP error will be returned. @@ -189,17 +191,23 @@ func getOrMakeOutputs(state types.AccountGetter, accounts map[string]*types.Acco } for _, out := range outs { + chain, outAddress, _ := out.ChainAndAddress() // already validated + if chain != nil { + // we dont need an account for the other chain. + // we'll just create an outgoing ibc packet + continue + } // Account shouldn't be duplicated - if _, ok := accounts[string(out.Address)]; ok { + if _, ok := accounts[string(outAddress)]; ok { return nil, abci.ErrBaseDuplicateAddress } - acc := state.GetAccount(out.Address) + acc := state.GetAccount(outAddress) // output account may be nil (new) if acc == nil { // zero value is valid, empty account acc = &types.Account{} } - accounts[string(out.Address)] = acc + accounts[string(outAddress)] = acc } return accounts, abci.OK } @@ -281,15 +289,22 @@ func adjustByInputs(state types.AccountSetter, accounts map[string]*types.Accoun } } -func adjustByOutputs(state types.AccountSetter, accounts map[string]*types.Account, outs []types.TxOutput, isCheckTx bool) { +func adjustByOutputs(state *State, accounts map[string]*types.Account, outs []types.TxOutput, isCheckTx bool) { for _, out := range outs { - acc := accounts[string(out.Address)] + destChain, outAddress, _ := out.ChainAndAddress() // already validated + if destChain != nil { + payload := ibc.CoinsPayload{outAddress, out.Coins} + ibc.SaveNewIBCPacket(state, state.GetChainID(), string(destChain), payload) + continue + } + + acc := accounts[string(outAddress)] if acc == nil { cmn.PanicSanity("adjustByOutputs() expects account in accounts") } acc.Balance = acc.Balance.Plus(out.Coins) if !isCheckTx { - state.SetAccount(out.Address, acc) + state.SetAccount(outAddress, acc) } } } diff --git a/state/execution_test.go b/state/execution_test.go index b8e526ae48..b69e66c40a 100644 --- a/state/execution_test.go +++ b/state/execution_test.go @@ -6,8 +6,10 @@ import ( "github.com/stretchr/testify/assert" abci "github.com/tendermint/abci/types" - "github.com/tendermint/basecoin/types" "github.com/tendermint/tmlibs/log" + + "github.com/tendermint/basecoin/plugins/ibc" + "github.com/tendermint/basecoin/types" ) //-------------------------------------------------------- @@ -33,11 +35,6 @@ func (et *execTest) signTx(tx *types.SendTx, accsIn ...types.PrivAccount) { types.SignTx(et.chainID, tx, accsIn...) } -// make tx from accsIn to et.accOut -func (et *execTest) getTx(seq int, accOut types.PrivAccount, accsIn ...types.PrivAccount) *types.SendTx { - return types.GetTx(seq, accOut, accsIn...) -} - // returns the final balance and expected balance for input and output accounts func (et *execTest) exec(tx *types.SendTx, checkTx bool) (res abci.Result, inGot, inExp, outGot, outExp types.Coins) { initBalIn := et.state.GetAccount(et.accIn.Account.PubKey.Address()).Balance @@ -85,19 +82,19 @@ func TestGetInputs(t *testing.T) { //test getInputs for registered, non-registered account et.reset() - txs := types.Accs2TxInputs(1, et.accIn) - acc, res = getInputs(et.state, txs) + inputs := types.Accs2TxInputs(1, et.accIn) + acc, res = getInputs(et.state, inputs) assert.True(res.IsErr(), "getInputs: expected error when using getInput with non-registered Input") et.acc2State(et.accIn) - acc, res = getInputs(et.state, txs) + acc, res = getInputs(et.state, inputs) assert.True(res.IsOK(), "getInputs: expected to getInput from registered Input") //test sending duplicate accounts et.reset() et.acc2State(et.accIn, et.accIn, et.accIn) - txs = types.Accs2TxInputs(1, et.accIn, et.accIn, et.accIn) - acc, res = getInputs(et.state, txs) + inputs = types.Accs2TxInputs(1, et.accIn, et.accIn, et.accIn) + acc, res = getInputs(et.state, inputs) assert.True(res.IsErr(), "getInputs: expected error when sending duplicate accounts") } @@ -112,24 +109,24 @@ func TestGetOrMakeOutputs(t *testing.T) { //test sending duplicate accounts et.reset() - txs := types.Accs2TxOutputs(et.accIn, et.accIn, et.accIn) - _, res = getOrMakeOutputs(et.state, nil, txs) + outputs := types.Accs2TxOutputs(et.accIn, et.accIn, et.accIn) + _, res = getOrMakeOutputs(et.state, nil, outputs) assert.True(res.IsErr(), "getOrMakeOutputs: expected error when sending duplicate accounts") //test sending to existing/new account et.reset() - txs1 := types.Accs2TxOutputs(et.accIn) - txs2 := types.Accs2TxOutputs(et.accOut) + outputs1 := types.Accs2TxOutputs(et.accIn) + outputs2 := types.Accs2TxOutputs(et.accOut) et.acc2State(et.accIn) - _, res = getOrMakeOutputs(et.state, nil, txs1) + _, res = getOrMakeOutputs(et.state, nil, outputs1) assert.True(res.IsOK(), "getOrMakeOutputs: error when sending to existing account") - mapRes2, res := getOrMakeOutputs(et.state, nil, txs2) + mapRes2, res := getOrMakeOutputs(et.state, nil, outputs2) assert.True(res.IsOK(), "getOrMakeOutputs: error when sending to new account") //test the map results - _, map2ok := mapRes2[string(txs2[0].Address)] + _, map2ok := mapRes2[string(outputs2[0].Address)] assert.True(map2ok, "getOrMakeOutputs: account output does not contain new account map item") } @@ -139,12 +136,12 @@ func TestValidateInputsBasic(t *testing.T) { et := newExecTest() //validate input basic - txs := types.Accs2TxInputs(1, et.accIn) - res := validateInputsBasic(txs) + inputs := types.Accs2TxInputs(1, et.accIn) + res := validateInputsBasic(inputs) assert.True(res.IsOK(), "validateInputsBasic: expected no error on good tx input. Error: %v", res.Error()) - txs[0].Coins[0].Amount = 0 - res = validateInputsBasic(txs) + inputs[0].Coins[0].Amount = 0 + res = validateInputsBasic(inputs) assert.True(res.IsErr(), "validateInputsBasic: expected error on bad tx input") } @@ -159,28 +156,28 @@ func TestValidateInputsAdvanced(t *testing.T) { accIn3 := types.MakeAcc("fooz") //validate inputs advanced - txs := et.getTx(1, et.accOut, accIn1, accIn2, accIn3) + tx := types.MakeSendTx(1, et.accOut, accIn1, accIn2, accIn3) et.acc2State(accIn1, accIn2, accIn3, et.accOut) - accMap, res := getInputs(et.state, txs.Inputs) + accMap, res := getInputs(et.state, tx.Inputs) assert.True(res.IsOK(), "validateInputsAdvanced: error retrieving accMap. Error: %v", res.Error()) - signBytes := txs.SignBytes(et.chainID) + signBytes := tx.SignBytes(et.chainID) //test bad case, unsigned - totalCoins, res := validateInputsAdvanced(accMap, signBytes, txs.Inputs) + totalCoins, res := validateInputsAdvanced(accMap, signBytes, tx.Inputs) assert.True(res.IsErr(), "validateInputsAdvanced: expected an error on an unsigned tx input") //test good case sgined - et.signTx(txs, accIn1, accIn2, accIn3, et.accOut) - totalCoins, res = validateInputsAdvanced(accMap, signBytes, txs.Inputs) + et.signTx(tx, accIn1, accIn2, accIn3, et.accOut) + totalCoins, res = validateInputsAdvanced(accMap, signBytes, tx.Inputs) assert.True(res.IsOK(), "validateInputsAdvanced: expected no error on good tx input. Error: %v", res.Error()) - txsTotalCoins := txs.Inputs[0].Coins. - Plus(txs.Inputs[1].Coins). - Plus(txs.Inputs[2].Coins) + txTotalCoins := tx.Inputs[0].Coins. + Plus(tx.Inputs[1].Coins). + Plus(tx.Inputs[2].Coins) - assert.True(totalCoins.IsEqual(txsTotalCoins), - "ValidateInputsAdvanced: transaction total coins are not equal: got %v, expected %v", txsTotalCoins, totalCoins) + assert.True(totalCoins.IsEqual(txTotalCoins), + "ValidateInputsAdvanced: transaction total coins are not equal: got %v, expected %v", txTotalCoins, totalCoins) } func TestValidateInputAdvanced(t *testing.T) { @@ -188,31 +185,31 @@ func TestValidateInputAdvanced(t *testing.T) { et := newExecTest() //validate input advanced - txs := et.getTx(1, et.accOut, et.accIn) + tx := types.MakeSendTx(1, et.accOut, et.accIn) et.acc2State(et.accIn, et.accOut) - signBytes := txs.SignBytes(et.chainID) + signBytes := tx.SignBytes(et.chainID) //unsigned case - res := validateInputAdvanced(&et.accIn.Account, signBytes, txs.Inputs[0]) + res := validateInputAdvanced(&et.accIn.Account, signBytes, tx.Inputs[0]) assert.True(res.IsErr(), "validateInputAdvanced: expected error on tx input without signature") //good signed case - et.signTx(txs, et.accIn, et.accOut) - res = validateInputAdvanced(&et.accIn.Account, signBytes, txs.Inputs[0]) + et.signTx(tx, et.accIn, et.accOut) + res = validateInputAdvanced(&et.accIn.Account, signBytes, tx.Inputs[0]) assert.True(res.IsOK(), "validateInputAdvanced: expected no error on good tx input. Error: %v", res.Error()) //bad sequence case et.accIn.Sequence = 1 - et.signTx(txs, et.accIn, et.accOut) - res = validateInputAdvanced(&et.accIn.Account, signBytes, txs.Inputs[0]) + et.signTx(tx, et.accIn, et.accOut) + res = validateInputAdvanced(&et.accIn.Account, signBytes, tx.Inputs[0]) assert.Equal(abci.CodeType_BaseInvalidSequence, res.Code, "validateInputAdvanced: expected error on tx input with bad sequence") et.accIn.Sequence = 0 //restore sequence //bad balance case et.accIn.Balance = types.Coins{{"mycoin", 2}} - et.signTx(txs, et.accIn, et.accOut) - res = validateInputAdvanced(&et.accIn.Account, signBytes, txs.Inputs[0]) + et.signTx(tx, et.accIn, et.accOut) + res = validateInputAdvanced(&et.accIn.Account, signBytes, tx.Inputs[0]) assert.Equal(abci.CodeType_BaseInsufficientFunds, res.Code, "validateInputAdvanced: expected error on tx input with insufficient funds %v", et.accIn.Sequence) } @@ -222,12 +219,12 @@ func TestValidateOutputsAdvanced(t *testing.T) { et := newExecTest() //validateOutputsBasic - txs := types.Accs2TxOutputs(et.accIn) - res := validateOutputsBasic(txs) + tx := types.Accs2TxOutputs(et.accIn) + res := validateOutputsBasic(tx) assert.True(res.IsOK(), "validateOutputsBasic: expected no error on good tx output. Error: %v", res.Error()) - txs[0].Coins[0].Amount = 0 - res = validateOutputsBasic(txs) + tx[0].Coins[0].Amount = 0 + res = validateOutputsBasic(tx) assert.True(res.IsErr(), "validateInputBasic: expected error on bad tx output. Error: %v", res.Error()) } @@ -236,9 +233,9 @@ func TestSumOutput(t *testing.T) { et := newExecTest() //SumOutput - txs := types.Accs2TxOutputs(et.accIn, et.accOut) - total := sumOutputs(txs) - assert.True(total.IsEqual(txs[0].Coins.Plus(txs[1].Coins)), "sumOutputs: total coins are not equal") + tx := types.Accs2TxOutputs(et.accIn, et.accOut) + total := sumOutputs(tx) + assert.True(total.IsEqual(tx[0].Coins.Plus(tx[1].Coins)), "sumOutputs: total coins are not equal") } func TestAdjustBy(t *testing.T) { @@ -271,23 +268,23 @@ func TestAdjustBy(t *testing.T) { } -func TestExecTx(t *testing.T) { +func TestSendTx(t *testing.T) { assert := assert.New(t) et := newExecTest() //ExecTx - txs := et.getTx(1, et.accOut, et.accIn) + tx := types.MakeSendTx(1, et.accOut, et.accIn) et.acc2State(et.accIn) et.acc2State(et.accOut) - et.signTx(txs, et.accIn) + et.signTx(tx, et.accIn) //Bad Balance et.accIn.Balance = types.Coins{{"mycoin", 2}} et.acc2State(et.accIn) - res, _, _, _, _ := et.exec(txs, true) + res, _, _, _, _ := et.exec(tx, true) assert.True(res.IsErr(), "ExecTx/Bad CheckTx: Expected error return from ExecTx, returned: %v", res) - res, balIn, balInExp, balOut, balOutExp := et.exec(txs, false) + res, balIn, balInExp, balOut, balOutExp := et.exec(tx, false) assert.True(res.IsErr(), "ExecTx/Bad DeliverTx: Expected error return from ExecTx, returned: %v", res) assert.False(balIn.IsEqual(balInExp), "ExecTx/Bad DeliverTx: balance shouldn't be equal for accIn: got %v, expected: %v", balIn, balInExp) @@ -298,17 +295,59 @@ func TestExecTx(t *testing.T) { et.reset() et.acc2State(et.accIn) et.acc2State(et.accOut) - res, _, _, _, _ = et.exec(txs, true) + res, _, _, _, _ = et.exec(tx, true) assert.True(res.IsOK(), "ExecTx/Good CheckTx: Expected OK return from ExecTx, Error: %v", res) //Regular DeliverTx et.reset() et.acc2State(et.accIn) et.acc2State(et.accOut) - res, balIn, balInExp, balOut, balOutExp = et.exec(txs, false) + res, balIn, balInExp, balOut, balOutExp = et.exec(tx, false) assert.True(res.IsOK(), "ExecTx/Good DeliverTx: Expected OK return from ExecTx, Error: %v", res) assert.True(balIn.IsEqual(balInExp), "ExecTx/good DeliverTx: unexpected change in input balance, got: %v, expected: %v", balIn, balInExp) assert.True(balOut.IsEqual(balOutExp), "ExecTx/good DeliverTx: unexpected change in output balance, got: %v, expected: %v", balOut, balOutExp) } + +func TestSendTxIBC(t *testing.T) { + assert := assert.New(t) + et := newExecTest() + + //ExecTx + chainID2 := "otherchain" + tx := types.MakeSendTx(1, et.accOut, et.accIn) + dstAddress := tx.Outputs[0].Address + tx.Outputs[0].Address = []byte(chainID2 + "/" + string(tx.Outputs[0].Address)) + et.acc2State(et.accIn) + et.signTx(tx, et.accIn) + + //Regular DeliverTx + et.reset() + et.acc2State(et.accIn) + + initBalIn := et.state.GetAccount(et.accIn.Account.PubKey.Address()).Balance + + res := ExecTx(et.state, nil, tx, false, nil) + + balIn := et.state.GetAccount(et.accIn.Account.PubKey.Address()).Balance + decrBalInExp := tx.Outputs[0].Coins.Plus(types.Coins{tx.Fee}) //expected decrease in balance In + balInExp := initBalIn.Minus(decrBalInExp) + + assert.True(res.IsOK(), "ExecTx/Good DeliverTx: Expected OK return from ExecTx, Error: %v", res) + assert.True(balIn.IsEqual(balInExp), + "ExecTx/good DeliverTx: unexpected change in input balance, got: %v, expected: %v", balIn, balInExp) + + packet, err := ibc.GetIBCPacket(et.state, et.chainID, chainID2, 0) + assert.Nil(err) + + assert.Equal(packet.SrcChainID, et.chainID) + assert.Equal(packet.DstChainID, chainID2) + assert.Equal(packet.Sequence, uint64(0)) + assert.Equal(packet.Type, "coin") + + coins, ok := packet.Payload.(ibc.CoinsPayload) + assert.True(ok) + assert.Equal(coins.Coins, tx.Outputs[0].Coins) + assert.EqualValues(coins.Address, dstAddress) +} diff --git a/state/state.go b/state/state.go index fbec5ae8e0..e9daf73b12 100644 --- a/state/state.go +++ b/state/state.go @@ -3,9 +3,7 @@ package state import ( abci "github.com/tendermint/abci/types" "github.com/tendermint/basecoin/types" - wire "github.com/tendermint/go-wire" eyes "github.com/tendermint/merkleeyes/client" - . "github.com/tendermint/tmlibs/common" "github.com/tendermint/tmlibs/log" ) @@ -64,11 +62,11 @@ func (s *State) Set(key []byte, value []byte) { } func (s *State) GetAccount(addr []byte) *types.Account { - return GetAccount(s, addr) + return types.GetAccount(s, addr) } func (s *State) SetAccount(addr []byte, acc *types.Account) { - SetAccount(s, addr, acc) + types.SetAccount(s, addr, acc) } func (s *State) CacheWrap() *State { @@ -97,28 +95,3 @@ func (s *State) Commit() abci.Result { } } - -//---------------------------------------- - -func AccountKey(addr []byte) []byte { - return append([]byte("base/a/"), addr...) -} - -func GetAccount(store types.KVStore, addr []byte) *types.Account { - data := store.Get(AccountKey(addr)) - if len(data) == 0 { - return nil - } - var acc *types.Account - err := wire.ReadBinaryBytes(data, &acc) - if err != nil { - panic(Fmt("Error reading account %X error: %v", - data, err.Error())) - } - return acc -} - -func SetAccount(store types.KVStore, addr []byte, acc *types.Account) { - accBytes := wire.BinaryBytes(acc) - store.Set(AccountKey(addr), accBytes) -} diff --git a/types/account.go b/types/account.go index d1e62d8321..ec4154fc42 100644 --- a/types/account.go +++ b/types/account.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/tendermint/go-crypto" + "github.com/tendermint/go-wire" ) type Account struct { @@ -49,3 +50,26 @@ type AccountGetterSetter interface { GetAccount(addr []byte) *Account SetAccount(addr []byte, acc *Account) } + +func AccountKey(addr []byte) []byte { + return append([]byte("base/a/"), addr...) +} + +func GetAccount(store KVStore, addr []byte) *Account { + data := store.Get(AccountKey(addr)) + if len(data) == 0 { + return nil + } + var acc *Account + err := wire.ReadBinaryBytes(data, &acc) + if err != nil { + panic(fmt.Sprintf("Error reading account %X error: %v", + data, err.Error())) + } + return acc +} + +func SetAccount(store KVStore, addr []byte, acc *Account) { + accBytes := wire.BinaryBytes(acc) + store.Set(AccountKey(addr), accBytes) +} diff --git a/types/test_helpers.go b/types/test_helpers.go index ffd3ce931f..2b7be27a9d 100644 --- a/types/test_helpers.go +++ b/types/test_helpers.go @@ -86,15 +86,15 @@ func Accs2TxOutputs(accs ...PrivAccount) []TxOutput { return txs } -func GetTx(seq int, accOut PrivAccount, accsIn ...PrivAccount) *SendTx { - txs := &SendTx{ +func MakeSendTx(seq int, accOut PrivAccount, accsIn ...PrivAccount) *SendTx { + tx := &SendTx{ Gas: 0, Fee: Coin{"mycoin", 1}, Inputs: Accs2TxInputs(seq, accsIn...), Outputs: Accs2TxOutputs(accOut), } - return txs + return tx } func SignTx(chainID string, tx *SendTx, accs ...PrivAccount) { diff --git a/types/tx.go b/types/tx.go index ed3e2ca0c9..a54d7caf0a 100644 --- a/types/tx.go +++ b/types/tx.go @@ -116,10 +116,33 @@ type TxOutput struct { Coins Coins `json:"coins"` // } -func (txOut TxOutput) ValidateBasic() abci.Result { - if len(txOut.Address) != 20 { - return abci.ErrBaseInvalidOutput.AppendLog("Invalid address length") +// An output destined for another chain may be formatted as `chainID/address`. +// ChainAndAddress returns the chainID prefix and the address. +// If there is no chainID prefix, the first returned value is nil. +func (txOut TxOutput) ChainAndAddress() ([]byte, []byte, abci.Result) { + var chainPrefix []byte + address := txOut.Address + if len(address) > 20 { + spl := bytes.Split(address, []byte("/")) + if len(spl) < 2 { + return nil, nil, abci.ErrBaseInvalidOutput.AppendLog("Invalid address format") + } + chainPrefix = spl[0] + address = bytes.Join(spl[1:], nil) } + + if len(address) != 20 { + return nil, nil, abci.ErrBaseInvalidOutput.AppendLog("Invalid address length") + } + return chainPrefix, address, abci.OK +} + +func (txOut TxOutput) ValidateBasic() abci.Result { + _, _, r := txOut.ChainAndAddress() + if r.IsErr() { + return r + } + if !txOut.Coins.IsValid() { return abci.ErrBaseInvalidOutput.AppendLog(Fmt("Invalid coins %v", txOut.Coins)) }