support SendTx to other chains via IBC

This commit is contained in:
Ethan Buchman 2017-05-21 15:36:46 -04:00
parent 38716d4815
commit 412c2b5bb7
6 changed files with 135 additions and 59 deletions

View File

@ -6,6 +6,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/url" "net/url"
"strconv"
"strings" "strings"
abci "github.com/tendermint/abci/types" abci "github.com/tendermint/abci/types"
@ -13,7 +14,6 @@ import (
merkle "github.com/tendermint/merkleeyes/iavl" merkle "github.com/tendermint/merkleeyes/iavl"
cmn "github.com/tendermint/tmlibs/common" cmn "github.com/tendermint/tmlibs/common"
bcsm "github.com/tendermint/basecoin/state"
"github.com/tendermint/basecoin/types" "github.com/tendermint/basecoin/types"
tm "github.com/tendermint/tendermint/types" tm "github.com/tendermint/tendermint/types"
) )
@ -55,20 +55,53 @@ type Packet struct {
SrcChainID string SrcChainID string
DstChainID string DstChainID string
Sequence uint64 Sequence uint64
Type string Type string // redundant now that Type() is a method on Payload ?
Payload 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{ return Packet{
SrcChainID: src, SrcChainID: src,
DstChainID: dst, DstChainID: dst,
Sequence: seq, Sequence: seq,
Type: ty, Type: payload.Type(),
Payload: payload, 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 ( const (
@ -78,21 +111,26 @@ const (
var _ = wire.RegisterInterface( var _ = wire.RegisterInterface(
struct{ Payload }{}, struct{ Payload }{},
wire.ConcreteType{BytesPayload{}, PayloadTypeBytes}, wire.ConcreteType{DataPayload{}, PayloadTypeBytes},
wire.ConcreteType{CoinsPayload{}, PayloadTypeCoins}, wire.ConcreteType{CoinsPayload{}, PayloadTypeCoins},
) )
type Payload interface { type Payload interface {
AssertIsPayload() AssertIsPayload()
Type() string
ValidateBasic() abci.Result ValidateBasic() abci.Result
} }
func (BytesPayload) AssertIsPayload() {} func (DataPayload) AssertIsPayload() {}
func (CoinsPayload) 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 return abci.OK
} }
@ -101,6 +139,10 @@ type CoinsPayload struct {
Coins types.Coins Coins types.Coins
} }
func (p CoinsPayload) Type() string {
return "coin"
}
func (p CoinsPayload) ValidateBasic() abci.Result { func (p CoinsPayload) ValidateBasic() abci.Result {
// TODO: validate // TODO: validate
return abci.OK return abci.OK
@ -351,7 +393,7 @@ func (sm *IBCStateMachine) runPacketCreateTx(tx IBCPacketCreateTx) {
// Execute the payload // Execute the payload
switch payload := tx.Packet.Payload.(type) { switch payload := tx.Packet.Payload.(type) {
case BytesPayload: case DataPayload:
// do nothing // do nothing
case CoinsPayload: case CoinsPayload:
// ensure enough coins were sent in tx to cover the payload coins // ensure enough coins were sent in tx to cover the payload coins
@ -428,16 +470,16 @@ func (sm *IBCStateMachine) runPacketPostTx(tx IBCPacketPostTx) {
// Execute payload // Execute payload
switch payload := packet.Payload.(type) { switch payload := packet.Payload.(type) {
case BytesPayload: case DataPayload:
// do nothing // do nothing
case CoinsPayload: case CoinsPayload:
// Add coins to destination account // Add coins to destination account
acc := bcsm.GetAccount(sm.store, payload.Address) acc := types.GetAccount(sm.store, payload.Address)
if acc == nil { if acc == nil {
acc = &types.Account{} acc = &types.Account{}
} }
acc.Balance = acc.Balance.Plus(payload.Coins) acc.Balance = acc.Balance.Plus(payload.Coins)
bcsm.SetAccount(sm.store, payload.Address, acc) types.SetAccount(sm.store, payload.Address, acc)
} }
return return

View File

@ -17,7 +17,6 @@ import (
"github.com/tendermint/merkleeyes/iavl" "github.com/tendermint/merkleeyes/iavl"
cmn "github.com/tendermint/tmlibs/common" cmn "github.com/tendermint/tmlibs/common"
sm "github.com/tendermint/basecoin/state"
"github.com/tendermint/basecoin/types" "github.com/tendermint/basecoin/types"
tm "github.com/tendermint/tendermint/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)) registerChain(t, ibcPlugin, store, ctx, "test_chain", string(genDocJSON_1))
// Create a new packet (for testing) // 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{ res := ibcPlugin.RunTx(store, ctx, wire.BinaryBytes(struct{ IBCTx }{IBCPacketCreateTx{
Packet: packet, Packet: packet,
}})) }}))
@ -203,7 +202,7 @@ func TestIBCPluginPayloadBytes(t *testing.T) {
registerChain(t, ibcPlugin, store, ctx, "test_chain", string(genDocJSON_1)) registerChain(t, ibcPlugin, store, ctx, "test_chain", string(genDocJSON_1))
// Create a new packet (for testing) // 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{ res := ibcPlugin.RunTx(store, ctx, wire.BinaryBytes(struct{ IBCTx }{IBCPacketCreateTx{
Packet: packet, Packet: packet,
}})) }}))
@ -282,7 +281,7 @@ func TestIBCPluginPayloadCoins(t *testing.T) {
coinsGood := types.Coins{types.Coin{"mycoin", 1}} coinsGood := types.Coins{types.Coin{"mycoin", 1}}
// Try to send too many coins // 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, Address: destinationAddr,
Coins: coinsBad, Coins: coinsBad,
}) })
@ -292,7 +291,7 @@ func TestIBCPluginPayloadCoins(t *testing.T) {
assertAndLog(t, store, res, abci.CodeType_InsufficientFunds) assertAndLog(t, store, res, abci.CodeType_InsufficientFunds)
// Send a small enough number of coins // 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, Address: destinationAddr,
Coins: coinsGood, Coins: coinsGood,
}) })
@ -334,7 +333,7 @@ func TestIBCPluginPayloadCoins(t *testing.T) {
assert.Nil(err) assert.Nil(err)
// Account should be empty before the tx // Account should be empty before the tx
acc := sm.GetAccount(store, destinationAddr) acc := types.GetAccount(store, destinationAddr)
assert.Nil(acc) assert.Nil(acc)
// Post a packet // Post a packet
@ -347,7 +346,7 @@ func TestIBCPluginPayloadCoins(t *testing.T) {
assertAndLog(t, store, res, abci.CodeType_OK) assertAndLog(t, store, res, abci.CodeType_OK)
// Account should now have some coins // Account should now have some coins
acc = sm.GetAccount(store, destinationAddr) acc = types.GetAccount(store, destinationAddr)
assert.Equal(acc.Balance, coinsGood) assert.Equal(acc.Balance, coinsGood)
} }
@ -408,7 +407,7 @@ func TestIBCPluginBadProof(t *testing.T) {
registerChain(t, ibcPlugin, store, ctx, "test_chain", string(genDocJSON_1)) registerChain(t, ibcPlugin, store, ctx, "test_chain", string(genDocJSON_1))
// Create a new packet (for testing) // 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{ res := ibcPlugin.RunTx(store, ctx, wire.BinaryBytes(struct{ IBCTx }{IBCPacketCreateTx{
Packet: packet, Packet: packet,
}})) }}))

View File

@ -2,9 +2,11 @@ package state
import ( import (
abci "github.com/tendermint/abci/types" abci "github.com/tendermint/abci/types"
"github.com/tendermint/basecoin/types"
cmn "github.com/tendermint/tmlibs/common" cmn "github.com/tendermint/tmlibs/common"
"github.com/tendermint/tmlibs/events" "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. // 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 { 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 // Account shouldn't be duplicated
if _, ok := accounts[string(out.Address)]; ok { if _, ok := accounts[string(outAddress)]; ok {
return nil, abci.ErrBaseDuplicateAddress return nil, abci.ErrBaseDuplicateAddress
} }
acc := state.GetAccount(out.Address) acc := state.GetAccount(outAddress)
// output account may be nil (new) // output account may be nil (new)
if acc == nil { if acc == nil {
// zero value is valid, empty account // zero value is valid, empty account
acc = &types.Account{} acc = &types.Account{}
} }
accounts[string(out.Address)] = acc accounts[string(outAddress)] = acc
} }
return accounts, abci.OK 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 { 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 { if acc == nil {
cmn.PanicSanity("adjustByOutputs() expects account in accounts") cmn.PanicSanity("adjustByOutputs() expects account in accounts")
} }
acc.Balance = acc.Balance.Plus(out.Coins) acc.Balance = acc.Balance.Plus(out.Coins)
if !isCheckTx { if !isCheckTx {
state.SetAccount(out.Address, acc) state.SetAccount(outAddress, acc)
} }
} }
} }

View File

@ -3,9 +3,7 @@ package state
import ( import (
abci "github.com/tendermint/abci/types" abci "github.com/tendermint/abci/types"
"github.com/tendermint/basecoin/types" "github.com/tendermint/basecoin/types"
wire "github.com/tendermint/go-wire"
eyes "github.com/tendermint/merkleeyes/client" eyes "github.com/tendermint/merkleeyes/client"
. "github.com/tendermint/tmlibs/common"
"github.com/tendermint/tmlibs/log" "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 { 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) { func (s *State) SetAccount(addr []byte, acc *types.Account) {
SetAccount(s, addr, acc) types.SetAccount(s, addr, acc)
} }
func (s *State) CacheWrap() *State { 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)
}

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"github.com/tendermint/go-crypto" "github.com/tendermint/go-crypto"
"github.com/tendermint/go-wire"
) )
type Account struct { type Account struct {
@ -49,3 +50,26 @@ type AccountGetterSetter interface {
GetAccount(addr []byte) *Account GetAccount(addr []byte) *Account
SetAccount(addr []byte, acc *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)
}

View File

@ -116,10 +116,33 @@ type TxOutput struct {
Coins Coins `json:"coins"` // Coins Coins `json:"coins"` //
} }
func (txOut TxOutput) ValidateBasic() abci.Result { // An output destined for another chain may be formatted as `chainID/address`.
if len(txOut.Address) != 20 { // ChainAndAddress returns the chainID prefix and the address.
return abci.ErrBaseInvalidOutput.AppendLog("Invalid address length") // 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() { if !txOut.Coins.IsValid() {
return abci.ErrBaseInvalidOutput.AppendLog(Fmt("Invalid coins %v", txOut.Coins)) return abci.ErrBaseInvalidOutput.AppendLog(Fmt("Invalid coins %v", txOut.Coins))
} }