From 412c2b5bb751edcc568ac63245114c4dec891044 Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Sun, 21 May 2017 15:36:46 -0400 Subject: [PATCH] support SendTx to other chains via IBC --- plugins/ibc/ibc.go | 66 +++++++++++++++++++++++++++++++++-------- plugins/ibc/ibc_test.go | 15 +++++----- state/execution.go | 29 +++++++++++++----- state/state.go | 31 ++----------------- types/account.go | 24 +++++++++++++++ types/tx.go | 29 ++++++++++++++++-- 6 files changed, 135 insertions(+), 59 deletions(-) diff --git a/plugins/ibc/ibc.go b/plugins/ibc/ibc.go index bcf224d6b..f20442222 100644 --- a/plugins/ibc/ibc.go +++ b/plugins/ibc/ibc.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "net/url" + "strconv" "strings" abci "github.com/tendermint/abci/types" @@ -13,7 +14,6 @@ import ( merkle "github.com/tendermint/merkleeyes/iavl" cmn "github.com/tendermint/tmlibs/common" - bcsm "github.com/tendermint/basecoin/state" "github.com/tendermint/basecoin/types" tm "github.com/tendermint/tendermint/types" ) @@ -55,20 +55,53 @@ type Packet struct { SrcChainID string DstChainID string Sequence uint64 - Type string + Type string // redundant now that Type() is a method on Payload ? Payload Payload } -func NewPacket(src, dst string, seq uint64, ty string, payload Payload) Packet { +func NewPacket(src, dst string, seq uint64, payload Payload) Packet { return Packet{ SrcChainID: src, DstChainID: dst, Sequence: seq, - Type: ty, + Type: payload.Type(), Payload: payload, } } +// GetSequenceNumber gets the sequence number for packets being sent from the src chain to the dst chain +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) +} + //-------------------------------------------------------------------------------- const ( @@ -78,21 +111,26 @@ const ( var _ = wire.RegisterInterface( struct{ Payload }{}, - wire.ConcreteType{BytesPayload{}, PayloadTypeBytes}, + wire.ConcreteType{DataPayload{}, PayloadTypeBytes}, wire.ConcreteType{CoinsPayload{}, PayloadTypeCoins}, ) type Payload interface { AssertIsPayload() + Type() string ValidateBasic() abci.Result } -func (BytesPayload) AssertIsPayload() {} +func (DataPayload) AssertIsPayload() {} func (CoinsPayload) AssertIsPayload() {} -type BytesPayload []byte +type DataPayload []byte -func (p BytesPayload) ValidateBasic() abci.Result { +func (p DataPayload) Type() string { + return "data" +} + +func (p DataPayload) ValidateBasic() abci.Result { return abci.OK } @@ -101,6 +139,10 @@ type CoinsPayload struct { Coins types.Coins } +func (p CoinsPayload) Type() string { + return "coin" +} + func (p CoinsPayload) ValidateBasic() abci.Result { // TODO: validate return abci.OK @@ -351,7 +393,7 @@ func (sm *IBCStateMachine) runPacketCreateTx(tx IBCPacketCreateTx) { // Execute the payload switch payload := tx.Packet.Payload.(type) { - case BytesPayload: + case DataPayload: // do nothing case CoinsPayload: // ensure enough coins were sent in tx to cover the payload coins @@ -428,16 +470,16 @@ func (sm *IBCStateMachine) runPacketPostTx(tx IBCPacketPostTx) { // Execute payload switch payload := packet.Payload.(type) { - case BytesPayload: + case DataPayload: // do nothing case CoinsPayload: // Add coins to destination account - acc := bcsm.GetAccount(sm.store, payload.Address) + acc := types.GetAccount(sm.store, payload.Address) if acc == nil { acc = &types.Account{} } acc.Balance = acc.Balance.Plus(payload.Coins) - bcsm.SetAccount(sm.store, payload.Address, acc) + types.SetAccount(sm.store, payload.Address, acc) } return diff --git a/plugins/ibc/ibc_test.go b/plugins/ibc/ibc_test.go index 40d49a823..93a913dec 100644 --- a/plugins/ibc/ibc_test.go +++ b/plugins/ibc/ibc_test.go @@ -17,7 +17,6 @@ import ( "github.com/tendermint/merkleeyes/iavl" cmn "github.com/tendermint/tmlibs/common" - sm "github.com/tendermint/basecoin/state" "github.com/tendermint/basecoin/types" tm "github.com/tendermint/tendermint/types" ) @@ -170,7 +169,7 @@ func TestIBCPluginPost(t *testing.T) { registerChain(t, ibcPlugin, store, ctx, "test_chain", string(genDocJSON_1)) // Create a new packet (for testing) - packet := NewPacket("test_chain", "dst_chain", 0, "data", BytesPayload([]byte("hello world"))) + packet := NewPacket("test_chain", "dst_chain", 0, DataPayload([]byte("hello world"))) res := ibcPlugin.RunTx(store, ctx, wire.BinaryBytes(struct{ IBCTx }{IBCPacketCreateTx{ Packet: packet, }})) @@ -203,7 +202,7 @@ func TestIBCPluginPayloadBytes(t *testing.T) { registerChain(t, ibcPlugin, store, ctx, "test_chain", string(genDocJSON_1)) // Create a new packet (for testing) - packet := NewPacket("test_chain", "dst_chain", 0, "data", BytesPayload([]byte("hello world"))) + packet := NewPacket("test_chain", "dst_chain", 0, DataPayload([]byte("hello world"))) res := ibcPlugin.RunTx(store, ctx, wire.BinaryBytes(struct{ IBCTx }{IBCPacketCreateTx{ Packet: packet, }})) @@ -282,7 +281,7 @@ func TestIBCPluginPayloadCoins(t *testing.T) { coinsGood := types.Coins{types.Coin{"mycoin", 1}} // Try to send too many coins - packet := NewPacket("test_chain", "dst_chain", 0, "data", CoinsPayload{ + packet := NewPacket("test_chain", "dst_chain", 0, CoinsPayload{ Address: destinationAddr, Coins: coinsBad, }) @@ -292,7 +291,7 @@ func TestIBCPluginPayloadCoins(t *testing.T) { assertAndLog(t, store, res, abci.CodeType_InsufficientFunds) // Send a small enough number of coins - packet = NewPacket("test_chain", "dst_chain", 0, "data", CoinsPayload{ + packet = NewPacket("test_chain", "dst_chain", 0, CoinsPayload{ Address: destinationAddr, Coins: coinsGood, }) @@ -334,7 +333,7 @@ func TestIBCPluginPayloadCoins(t *testing.T) { assert.Nil(err) // Account should be empty before the tx - acc := sm.GetAccount(store, destinationAddr) + acc := types.GetAccount(store, destinationAddr) assert.Nil(acc) // Post a packet @@ -347,7 +346,7 @@ func TestIBCPluginPayloadCoins(t *testing.T) { assertAndLog(t, store, res, abci.CodeType_OK) // Account should now have some coins - acc = sm.GetAccount(store, destinationAddr) + acc = types.GetAccount(store, destinationAddr) assert.Equal(acc.Balance, coinsGood) } @@ -408,7 +407,7 @@ func TestIBCPluginBadProof(t *testing.T) { registerChain(t, ibcPlugin, store, ctx, "test_chain", string(genDocJSON_1)) // Create a new packet (for testing) - packet := NewPacket("test_chain", "dst_chain", 0, "data", BytesPayload([]byte("hello world"))) + packet := NewPacket("test_chain", "dst_chain", 0, DataPayload([]byte("hello world"))) res := ibcPlugin.RunTx(store, ctx, wire.BinaryBytes(struct{ IBCTx }{IBCPacketCreateTx{ Packet: packet, }})) diff --git a/state/execution.go b/state/execution.go index 246246846..307c0d463 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/state.go b/state/state.go index fbec5ae8e..e9daf73b1 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 d1e62d832..ec4154fc4 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/tx.go b/types/tx.go index ed3e2ca0c..a54d7caf0 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)) }