Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add IBC Ping Pong contract demo #259

Merged
merged 5 commits into from
Aug 25, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions x/wasm/genesis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func TestInitGenesis(t *testing.T) {
h := data.module.Route().Handler()
q := data.module.LegacyQuerierHandler(nil)

t.Log("fail with invalid source url")
t.Log("fail with invalid Our url")
alpe marked this conversation as resolved.
Show resolved Hide resolved
msg := MsgStoreCode{
Sender: creator,
WASMByteCode: testContract,
Expand All @@ -37,7 +37,7 @@ func TestInitGenesis(t *testing.T) {
_, err = h(data.ctx, &msg)
require.Error(t, err)

t.Log("fail with relative source url")
t.Log("fail with relative Our url")
msg = MsgStoreCode{
Sender: creator,
WASMByteCode: testContract,
Expand Down Expand Up @@ -65,7 +65,7 @@ func TestInitGenesis(t *testing.T) {
_, err = h(data.ctx, &msg)
require.Error(t, err)

t.Log("no error with valid source and build tag")
t.Log("no error with valid Our and build tag")
msg = MsgStoreCode{
Sender: creator,
WASMByteCode: testContract,
Expand Down
2 changes: 1 addition & 1 deletion x/wasm/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func NewHandler(k Keeper) sdk.Handler {
}

// filterMessageEvents returns the same events with all of type == EventTypeMessage removed.
// this is so only our top-level message event comes through
// this is so only Our top-level message event comes through
alpe marked this conversation as resolved.
Show resolved Hide resolved
func filteredMessageEvents(manager *sdk.EventManager) []abci.Event {
events := manager.ABCIEvents()
res := make([]abci.Event, 0, len(events))
Expand Down
6 changes: 4 additions & 2 deletions x/wasm/internal/keeper/cosmwasm/msg.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ type CosmosMsg struct {
type IBCMsg struct {
SendPacket *IBCSendMsg `json:"execute,omitempty"`
CloseChannel *IBCCloseChannelMsg `json:"instantiate,omitempty"`
Transfer *IBCTransferMsg `json:"instantiate,omitempty"`
// Transfer starts an ics-20 transfer from a contract using the sdk ibc-transfer module.
Transfer *IBCTransferMsg `json:"instantiate,omitempty"`
}

type IBCSendMsg struct {
Expand All @@ -34,7 +35,8 @@ type IBCSendMsg struct {
type IBCCloseChannelMsg struct {
ChannelID string
}
type IBCTransferMsg struct {

type IBCTransferMsg struct { // TODO: impl
}

//------- Results / Msgs -------------
Expand Down
92 changes: 75 additions & 17 deletions x/wasm/relay_pingpong_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@ func TestPinPong(t *testing.T) {
var err error

const startValue uint64 = 100
const rounds = 3
s := startGame{
alpe marked this conversation as resolved.
Show resolved Hide resolved
Source: cosmwasmv2.IBCEndpoint{channelA.ID, channelA.PortID},
CounterParty: cosmwasmv2.IBCEndpoint{channelB.ID, channelB.PortID},
Value: startValue,
ChannelID: channelA.ID,
alpe marked this conversation as resolved.
Show resolved Hide resolved
Value: startValue,
}
startMsg := &wasm.MsgExecuteContract{
Sender: chainA.SenderAccount.GetAddress(),
Expand All @@ -74,7 +74,6 @@ func TestPinPong(t *testing.T) {

t.Log("Duplicate messages are due to check/deliver tx calls")

const rounds = 3
var (
activePlayer = ping
pingBallValue = startValue
Expand Down Expand Up @@ -145,6 +144,10 @@ func (h hit) BuildAck() hitAcknowledgement {
return hitAcknowledgement{Success: &h}
}

func (h hit) BuildError(errMsg string) hitAcknowledgement {
return hitAcknowledgement{Error: errMsg}
}

// hitAcknowledgement is ibc acknowledgment payload
type hitAcknowledgement struct {
Error string `json:"error,omitempty"`
Expand All @@ -161,8 +164,10 @@ func (a hitAcknowledgement) GetBytes() []byte {

// startGame is an execute message payload
type startGame struct {
Source, CounterParty cosmwasmv2.IBCEndpoint
Value uint64
ChannelID string
Value uint64
// limit above the game is aborted
MaxValue uint64 `json:"max_value,omitempty"`
}

func (g startGame) GetBytes() json.RawMessage {
Expand All @@ -178,10 +183,11 @@ type player struct {
t *testing.T
chain *ibc_testing.TestChain
contractAddr sdk.AccAddress
actor string
execCalls int
actor string // either ping or pong
execCalls int // number of calls to Execute method (checkTx + deliverTx)
}

// Execute starts the ping pong game
func (p *player) Execute(hash []byte, params wasmTypes.Env, data []byte, store prefix.Store, api cosmwasm.GoAPI, querier wasmkeeper.QueryHandler, meter sdk.GasMeter, gas uint64) (*cosmwasmv2.HandleResponse, uint64, error) {
p.execCalls++
if p.execCalls%2 == 1 { // skip checkTx step because of no rollback with `chain.GetContext()`
Expand All @@ -193,37 +199,81 @@ func (p *player) Execute(hash []byte, params wasmTypes.Env, data []byte, store p
return nil, 0, err
}

if start.MaxValue != 0 {
store.Set(maxValueKey, sdk.Uint64ToBigEndian(start.MaxValue))
}
endpoints := p.loadEndpoints(store, start.ChannelID)
ctx := p.chain.GetContext()
channelCap, ok := p.chain.App.WasmKeeper.ScopedKeeper.GetCapability(ctx, host.ChannelCapabilityPath(start.Source.Port, start.Source.Channel))
channelCap, ok := p.chain.App.WasmKeeper.ScopedKeeper.GetCapability(ctx, host.ChannelCapabilityPath(endpoints.Our.Port, endpoints.Our.Channel))
if !ok {
return nil, 0, sdkerrors.Wrap(channeltypes.ErrChannelCapabilityNotFound, "module does not own channel capability")
}

service := NewHit(p.actor, start.Value)
p.t.Logf("[%s] starting game with: %d: %v\n", p.actor, start.Value, service)

var seq uint64 = 1
packet := channeltypes.NewPacket(service.GetBytes(), seq, start.Source.Port, start.Source.Channel, start.CounterParty.Port, start.CounterParty.Channel, doNotTimeout, 0)
packet := channeltypes.NewPacket(service.GetBytes(), seq, endpoints.Our.Port, endpoints.Our.Channel, endpoints.Their.Port, endpoints.Their.Channel, doNotTimeout, 0)
alpe marked this conversation as resolved.
Show resolved Hide resolved
err := p.chain.App.WasmKeeper.ChannelKeeper.SendPacket(ctx, channelCap, packet)
if err != nil {
return nil, 0, err
}

p.IncrementCounter(sentBallsCountKey, store)
p.incrementCounter(sentBallsCountKey, store)
store.Set(lastBallSentKey, sdk.Uint64ToBigEndian(start.Value))
return &cosmwasmv2.HandleResponse{}, 0, nil
}

// OnIBCChannelOpen ensures to accept only configured version
func (p player) OnIBCChannelOpen(hash []byte, params cosmwasmv2.Env, channel cosmwasmv2.IBCChannel, store prefix.Store, api cosmwasm.GoAPI, querier wasmkeeper.QueryHandler, meter sdk.GasMeter, gas uint64) (*cosmwasmv2.IBCChannelOpenResponse, uint64, error) {
if channel.Version != p.actor {
return &cosmwasmv2.IBCChannelOpenResponse{Success: false, Reason: fmt.Sprintf("expected %q but got %q", p.actor, channel.Version)}, 0, nil
}
return &cosmwasmv2.IBCChannelOpenResponse{Success: true}, 0, nil
}

// OnIBCChannelConnect persists connection endpoints
func (p player) OnIBCChannelConnect(hash []byte, params cosmwasmv2.Env, channel cosmwasmv2.IBCChannel, store prefix.Store, api cosmwasm.GoAPI, querier wasmkeeper.QueryHandler, meter sdk.GasMeter, gas uint64) (*cosmwasmv2.IBCChannelConnectResponse, uint64, error) {
p.storeEndpoint(store, channel)
return &cosmwasmv2.IBCChannelConnectResponse{}, 0, nil
}

// connectedChannelsModel is a simple persistence model to store endpoint addresses within the contract's store
type connectedChannelsModel struct {
Our cosmwasmv2.IBCEndpoint
Their cosmwasmv2.IBCEndpoint
}

var ( // store keys
ibcEndpointsKey = []byte("ibc-endpoints")
maxValueKey = []byte("max-value")
)

func (p player) loadEndpoints(store prefix.Store, channelID string) *connectedChannelsModel {
var counterparties []connectedChannelsModel
if bz := store.Get(ibcEndpointsKey); bz != nil {
require.NoError(p.t, json.Unmarshal(bz, &counterparties))
}
for _, v := range counterparties {
if v.Our.Channel == channelID {
return &v
}
}
p.t.Fatalf("no counterparty found for channel %q", channelID)
return nil
}

func (p player) storeEndpoint(store prefix.Store, channel cosmwasmv2.IBCChannel) {
var counterparties []connectedChannelsModel
if b := store.Get(ibcEndpointsKey); b != nil {
require.NoError(p.t, json.Unmarshal(b, &counterparties))
}
counterparties = append(counterparties, connectedChannelsModel{Our: channel.Endpoint, Their: channel.CounterpartyEndpoint})
bz, err := json.Marshal(&counterparties)
alpe marked this conversation as resolved.
Show resolved Hide resolved
require.NoError(p.t, err)
store.Set(ibcEndpointsKey, bz)
}

func (p player) OnIBCChannelClose(ctx sdk.Context, hash []byte, params cosmwasmv2.Env, channel cosmwasmv2.IBCChannel, meter sdk.GasMeter, gas uint64) (*cosmwasmv2.IBCChannelCloseResponse, uint64, error) {
panic("implement me")
}
Expand All @@ -236,6 +286,7 @@ var ( // store keys
confirmedBallsCountKey = []byte("confBalls")
)

// OnIBCPacketReceive receives the hit and serves a response hit via `cosmwasmv2.IBCMsg`
func (p player) OnIBCPacketReceive(hash []byte, params cosmwasmv2.Env, packet cosmwasmv2.IBCPacket, store prefix.Store, api cosmwasm.GoAPI, querier wasmkeeper.QueryHandler, meter sdk.GasMeter, gas uint64) (*cosmwasmv2.IBCPacketReceiveResponse, uint64, error) {
// parse received data and store
var receivedBall hit
Expand All @@ -245,20 +296,26 @@ func (p player) OnIBCPacketReceive(hash []byte, params cosmwasmv2.Env, packet co
// no hit msg, we stop the game
}, 0, nil
}
p.incrementCounter(receivedBallsCountKey, store)

otherCount := receivedBall[counterParty(p.actor)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add a max hits (3 or 4) and if this is higher, then we return IBCPacketReceiveResponse with a "failed" response. In this case, we do not initiate another round. It will allow us to test error handling in acknowledge.

store.Set(lastBallReceivedKey, sdk.Uint64ToBigEndian(otherCount))

nextValue := p.IncrementCounter(lastBallSentKey, store)
if maxVal := store.Get(maxValueKey); maxVal != nil && otherCount > sdk.BigEndianToUint64(maxVal) {
errMsg := fmt.Sprintf("max value exceeded: %d got %d", sdk.BigEndianToUint64(maxVal), otherCount)
return &cosmwasmv2.IBCPacketReceiveResponse{
Acknowledgement: receivedBall.BuildError(errMsg).GetBytes(),
}, 0, nil
}

nextValue := p.incrementCounter(lastBallSentKey, store)
newHit := NewHit(p.actor, nextValue)
respHit := &cosmwasmv2.IBCMsg{SendPacket: &cosmwasmv2.IBCSendMsg{
ChannelID: packet.Source.Channel,
Data: newHit.GetBytes(),
TimeoutHeight: doNotTimeout,
}}

p.IncrementCounter(receivedBallsCountKey, store)
p.IncrementCounter(sentBallsCountKey, store)
p.incrementCounter(sentBallsCountKey, store)
p.t.Logf("[%s] received %d, returning %d: %v\n", p.actor, otherCount, nextValue, newHit)

return &cosmwasmv2.IBCPacketReceiveResponse{
Expand All @@ -267,6 +324,7 @@ func (p player) OnIBCPacketReceive(hash []byte, params cosmwasmv2.Env, packet co
}, 0, nil
}

// OnIBCPacketAcknowledgement handles the packet acknowledgment frame. Stops the game on an any error
func (p player) OnIBCPacketAcknowledgement(hash []byte, params cosmwasmv2.Env, packetAck cosmwasmv2.IBCAcknowledgement, store prefix.Store, api cosmwasm.GoAPI, querier wasmkeeper.QueryHandler, meter sdk.GasMeter, gas uint64) (*cosmwasmv2.IBCPacketAcknowledgementResponse, uint64, error) {
// parse received data and store
var sentBall hit
Expand All @@ -286,15 +344,15 @@ func (p player) OnIBCPacketAcknowledgement(hash []byte, params cosmwasmv2.Env, p

}

p.IncrementCounter(confirmedBallsCountKey, store)
p.incrementCounter(confirmedBallsCountKey, store)
return &cosmwasmv2.IBCPacketAcknowledgementResponse{}, 0, nil
}

func (p player) OnIBCPacketTimeout(hash []byte, params cosmwasmv2.Env, packet cosmwasmv2.IBCPacket, store prefix.Store, api cosmwasm.GoAPI, querier wasmkeeper.QueryHandler, meter sdk.GasMeter, gas uint64) (*cosmwasmv2.IBCPacketTimeoutResponse, uint64, error) {
panic("implement me")
}

func (p player) IncrementCounter(key []byte, store prefix.Store) uint64 {
func (p player) incrementCounter(key []byte, store prefix.Store) uint64 {
var count uint64
bz := store.Get(key)
if bz != nil {
Expand Down
6 changes: 3 additions & 3 deletions x/wasm/relay_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ func (c *receiverContract) OnIBCPacketReceive(hash []byte, params cosmwasmv2.Env
ctx := c.chain.GetContext() // HACK: please note that this is not reverted after checkTX
err := c.chain.App.TransferKeeper.OnRecvPacket(ctx, ibcPacket, src)
if err != nil {
return nil, 0, sdkerrors.Wrap(err, "within our smart contract")
return nil, 0, sdkerrors.Wrap(err, "within Our smart contract")
alpe marked this conversation as resolved.
Show resolved Hide resolved
}

log := []cosmwasmv1.LogAttribute{} // note: all events are under `wasm` event type
Expand All @@ -221,7 +221,7 @@ func (c *receiverContract) OnIBCPacketAcknowledgement(hash []byte, params cosmwa
ibcPacket := toIBCPacket(packetAck.OriginalPacket)
err := c.chain.App.TransferKeeper.OnAcknowledgementPacket(ctx, ibcPacket, src, ack)
if err != nil {
return nil, 0, sdkerrors.Wrap(err, "within our smart contract")
return nil, 0, sdkerrors.Wrap(err, "within Our smart contract")
}

return &cosmwasmv2.IBCPacketAcknowledgementResponse{}, 0, nil
Expand All @@ -239,7 +239,7 @@ func (c *receiverContract) OnIBCPacketTimeout(hash []byte, params cosmwasmv2.Env
ctx := c.chain.GetContext() // HACK: please note that this is not reverted after checkTX
err := c.chain.App.TransferKeeper.OnTimeoutPacket(ctx, ibcPacket, src)
if err != nil {
return nil, 0, sdkerrors.Wrap(err, "within our smart contract")
return nil, 0, sdkerrors.Wrap(err, "within Our smart contract")
}

return &cosmwasmv2.IBCPacketTimeoutResponse{}, 0, nil
Expand Down