mukan-ibc/modules/apps/transfer/keeper/mbt_relay_test.go
Mukan Erkin Törük 88dd97a9f8
Some checks failed
CodeQL / Analyze (push) Waiting to run
golangci-lint / lint (push) Waiting to run
Tests / Code Coverage / build (amd64) (push) Waiting to run
Tests / Code Coverage / build (arm64) (push) Waiting to run
Tests / Code Coverage / unit-tests (map[additional-args:-tags="test_e2e" name:e2e path:./e2e]) (push) Waiting to run
Tests / Code Coverage / unit-tests (map[name:08-wasm path:./modules/light-clients/08-wasm]) (push) Waiting to run
Tests / Code Coverage / unit-tests (map[name:ibc-go path:.]) (push) Waiting to run
Docker Build & Push Simapp (main) / docker-build (push) Has been cancelled
refactor: replace all github.com upstream refs with git.cw.tr/mukan-network
2026-05-11 03:36:22 +03:00

403 lines
12 KiB
Go

package keeper_test
// This file is a test driver for model-based tests generated from the TLA+ model of token transfer
// Written by Andrey Kuprianov within the scope of IBC Audit performed by Informal Systems.
// In case of any questions please don't hesitate to contact andrey@informal.systems.
import (
"encoding/json"
"errors"
"fmt"
"os"
"strconv"
"strings"
errorsmod "cosmossdk.io/errors"
sdkmath "cosmossdk.io/math"
sdk "git.cw.tr/mukan-network/mukan-sdk/types"
"git.cw.tr/mukan-network/mukan-consensus/crypto"
"git.cw.tr/mukan-network/mukan-ibc/modules/apps/transfer/types"
clienttypes "git.cw.tr/mukan-network/mukan-ibc/modules/core/02-client/types"
channeltypes "git.cw.tr/mukan-network/mukan-ibc/modules/core/04-channel/types"
ibcerrors "git.cw.tr/mukan-network/mukan-ibc/modules/core/errors"
ibctesting "git.cw.tr/mukan-network/mukan-ibc/testing"
)
type TlaBalance struct {
Address []string `json:"address"`
Denom []string `json:"denom"`
Amount int64 `json:"amount"`
}
type TlaFungibleTokenPacketData struct {
Sender string `json:"sender"`
Receiver string `json:"receiver"`
Amount string `json:"amount"`
Denom []string `json:"denom"`
}
type TlaFungibleTokenPacket struct {
SourceChannel string `json:"sourceChannel"`
SourcePort string `json:"sourcePort"`
DestChannel string `json:"destChannel"`
DestPort string `json:"destPort"`
Data TlaFungibleTokenPacketData `json:"data"`
}
type TlaOnRecvPacketTestCase = struct {
// The required subset of bank balances
BankBefore []TlaBalance `json:"bankBefore"`
// The packet to process
Packet TlaFungibleTokenPacket `json:"packet"`
// The handler to call
Handler string `json:"handler"`
// The expected changes in the bank
BankAfter []TlaBalance `json:"bankAfter"`
// Whether OnRecvPacket should fail or not
Error bool `json:"error"`
}
type FungibleTokenPacket struct {
SourceChannel string
SourcePort string
DestChannel string
DestPort string
Data types.InternalTransferRepresentation
}
type OnRecvPacketTestCase = struct {
description string
// The required subset of bank balances
bankBefore []Balance
// The packet to process
packet FungibleTokenPacket
// The handler to call
handler string
// The expected bank state after processing (wrt. bankBefore)
bankAfter []Balance
// Whether OnRecvPacket should pass or fail
pass bool
}
type OwnedCoin struct {
Address string
Denom string
}
type Balance struct {
ID string
Address string
Denom string
Amount sdkmath.Int
}
func AddressFromString(address string) string {
return sdk.AccAddress(crypto.AddressHash([]byte(address))).String()
}
func AddressFromTla(addr []string) string {
if len(addr) != 3 {
panic(errors.New("failed to convert from TLA+ address: wrong number of address components"))
}
s := ""
if len(addr[0]) == 0 && len(addr[1]) == 0 { //nolint:gocritic
// simple address: id
s = addr[2]
} else if len(addr[2]) == 0 {
// escrow address: ics20-1\x00port/channel
s = fmt.Sprintf("%s\x00%s/%s", types.V1, addr[0], addr[1])
} else {
panic(errors.New("failed to convert from TLA+ address: neither simple nor escrow address"))
}
return s
}
func DenomFromTla(denom []string) string {
var i int
for i = 0; i+1 < len(denom); i += 2 {
if len(denom[i]) != 0 || len(denom[i+1]) != 0 {
break
}
}
return strings.Join(denom[i:], "/")
}
func BalanceFromTla(balance TlaBalance) Balance {
return Balance{
ID: AddressFromTla(balance.Address),
Address: AddressFromString(AddressFromTla(balance.Address)),
Denom: DenomFromTla(balance.Denom),
Amount: sdkmath.NewInt(balance.Amount),
}
}
func BalancesFromTla(tla []TlaBalance) []Balance {
balances := make([]Balance, 0)
for _, b := range tla {
balances = append(balances, BalanceFromTla(b))
}
return balances
}
func FungibleTokenPacketFromTla(packet TlaFungibleTokenPacket) FungibleTokenPacket {
denom := types.ExtractDenomFromPath(DenomFromTla(packet.Data.Denom))
return FungibleTokenPacket{
SourceChannel: packet.SourceChannel,
SourcePort: packet.SourcePort,
DestChannel: packet.DestChannel,
DestPort: packet.DestPort,
Data: types.NewInternalTransferRepresentation(
types.Token{
Denom: denom,
Amount: packet.Data.Amount,
},
AddressFromString(packet.Data.Sender),
AddressFromString(packet.Data.Receiver),
"",
),
}
}
func OnRecvPacketTestCaseFromTla(tc TlaOnRecvPacketTestCase) OnRecvPacketTestCase {
return OnRecvPacketTestCase{
description: "auto-generated",
bankBefore: BalancesFromTla(tc.BankBefore),
packet: FungibleTokenPacketFromTla(tc.Packet),
handler: tc.Handler,
bankAfter: BalancesFromTla(tc.BankAfter), // TODO different semantics
pass: !tc.Error,
}
}
var addressMap = make(map[string]string)
type Bank struct {
balances map[OwnedCoin]sdkmath.Int
}
// Make an empty bank
func MakeBank() Bank {
return Bank{balances: make(map[OwnedCoin]sdkmath.Int)}
}
// Subtract other bank from this bank
func (bank *Bank) Sub(other *Bank) Bank {
diff := MakeBank()
for coin, amount := range bank.balances {
otherAmount, exists := other.balances[coin]
if exists {
diff.balances[coin] = amount.Sub(otherAmount)
} else {
diff.balances[coin] = amount
}
}
for coin, amount := range other.balances {
if _, exists := bank.balances[coin]; !exists {
diff.balances[coin] = amount.Neg()
}
}
return diff
}
// Set specific bank balance
func (bank *Bank) SetBalance(address string, denom string, amount sdkmath.Int) {
bank.balances[OwnedCoin{address, denom}] = amount
}
// Set several balances at once
func (bank *Bank) SetBalances(balances []Balance) {
for _, balance := range balances {
bank.balances[OwnedCoin{balance.Address, balance.Denom}] = balance.Amount
addressMap[balance.Address] = balance.ID
}
}
func NullCoin() OwnedCoin {
return OwnedCoin{
Address: AddressFromString(""),
Denom: "",
}
}
// Set several balances at once
func BankFromBalances(balances []Balance) Bank {
bank := MakeBank()
for _, balance := range balances {
coin := OwnedCoin{balance.Address, balance.Denom}
if coin != NullCoin() { // ignore null coin
bank.balances[coin] = balance.Amount
addressMap[balance.Address] = balance.ID
}
}
return bank
}
// String representation of all bank balances
func (bank *Bank) String() string {
str := ""
for coin, amount := range bank.balances {
str += coin.Address
if addressMap[coin.Address] != "" {
str += "(" + addressMap[coin.Address] + ")"
}
str += " : " + coin.Denom + " = " + amount.String() + "\n"
}
return str
}
// String representation of non-zero bank balances
func (bank *Bank) NonZeroString() string {
str := ""
for coin, amount := range bank.balances {
if !amount.IsZero() {
str += coin.Address + " : " + coin.Denom + " = " + amount.String() + "\n"
}
}
return str
}
// Construct a bank out of the chain bank
func BankOfChain(chain *ibctesting.TestChain) Bank {
bank := MakeBank()
chain.GetSimApp().BankKeeper.IterateAllBalances(chain.GetContext(), func(address sdk.AccAddress, coin sdk.Coin) (stop bool) {
token, err := chain.GetSimApp().TransferKeeper.TokenFromCoin(chain.GetContext(), coin)
if err != nil {
panic(fmt.Errorf("Failed to construct token from coin: %w", err))
}
bank.SetBalance(address.String(), token.Denom.Path(), coin.Amount)
return false
})
return bank
}
// Check that the state of the bank is the bankBefore + expectedBankChange
func (*KeeperTestSuite) CheckBankBalances(chain *ibctesting.TestChain, bankBefore *Bank, expectedBankChange *Bank) error {
bankAfter := BankOfChain(chain)
bankChange := bankAfter.Sub(bankBefore)
diff := bankChange.Sub(expectedBankChange)
nonZeroString := diff.NonZeroString()
if len(nonZeroString) != 0 {
return errorsmod.Wrap(ibcerrors.ErrInvalidCoins, "Unexpected changes in the bank: \n"+nonZeroString)
}
return nil
}
func (suite *KeeperTestSuite) TestModelBasedRelay() {
dirname := "model_based_tests/"
files, err := os.ReadDir(dirname)
if err != nil {
panic(fmt.Errorf("Failed to read model-based test files: %w", err))
}
for _, fileInfo := range files {
tlaTestCases := []TlaOnRecvPacketTestCase{}
if !strings.HasSuffix(fileInfo.Name(), ".json") {
continue
}
jsonBlob, err := os.ReadFile(dirname + fileInfo.Name())
if err != nil {
panic(fmt.Errorf("Failed to read JSON test fixture: %w", err))
}
err = json.Unmarshal(jsonBlob, &tlaTestCases)
if err != nil {
panic(fmt.Errorf("Failed to parse JSON test fixture: %w", err))
}
suite.SetupTest()
pathAtoB := ibctesting.NewTransferPath(suite.chainA, suite.chainB).DisableUniqueChannelIDs()
pathBtoC := ibctesting.NewTransferPath(suite.chainB, suite.chainC).DisableUniqueChannelIDs()
pathAtoB.Setup()
pathBtoC.Setup()
for i, tlaTc := range tlaTestCases {
tc := OnRecvPacketTestCaseFromTla(tlaTc)
registerDenomFn := func() {
if !suite.chainB.GetSimApp().TransferKeeper.HasDenom(suite.chainB.GetContext(), tc.packet.Data.Token.Denom.Hash()) {
suite.chainB.GetSimApp().TransferKeeper.SetDenom(suite.chainB.GetContext(), tc.packet.Data.Token.Denom)
}
}
description := fileInfo.Name() + " # " + strconv.Itoa(i+1)
suite.Run(fmt.Sprintf("Case %s", description), func() {
seq := uint64(1)
packet := channeltypes.NewPacket([]byte("mockdata"), seq, tc.packet.SourcePort, tc.packet.SourceChannel, tc.packet.DestPort, tc.packet.DestChannel, clienttypes.NewHeight(1, 100), 0)
bankBefore := BankFromBalances(tc.bankBefore)
realBankBefore := BankOfChain(suite.chainB)
// First validate the packet itself (mimics what happens when the packet is being sent and/or received)
err := packet.ValidateBasic()
if err != nil {
suite.Require().False(tc.pass, err.Error())
return
}
switch tc.handler {
case "SendTransfer":
var sender sdk.AccAddress
sender, err = sdk.AccAddressFromBech32(tc.packet.Data.Sender)
if err != nil {
panic(errors.New("MBT failed to convert sender address"))
}
registerDenomFn()
denom := tc.packet.Data.Token.Denom.IBCDenom()
err = sdk.ValidateDenom(denom)
if err == nil {
amount, ok := sdkmath.NewIntFromString(tc.packet.Data.Token.Amount)
if !ok {
panic(errors.New("MBT failed to parse amount from string"))
}
msg := types.NewMsgTransfer(
tc.packet.SourcePort,
tc.packet.SourceChannel,
sdk.NewCoin(denom, amount),
sender.String(),
tc.packet.Data.Receiver,
suite.chainA.GetTimeoutHeight(), 0, // only use timeout height
"",
)
_, err = suite.chainB.GetSimApp().TransferKeeper.Transfer(suite.chainB.GetContext(), msg)
}
case "OnRecvPacket":
err = suite.chainB.GetSimApp().TransferKeeper.OnRecvPacket(
suite.chainB.GetContext(),
tc.packet.Data,
packet.SourcePort,
packet.SourceChannel,
packet.DestinationPort,
packet.DestinationChannel,
)
case "OnTimeoutPacket":
registerDenomFn()
err = suite.chainB.GetSimApp().TransferKeeper.OnTimeoutPacket(suite.chainB.GetContext(), packet.SourcePort, packet.SourceChannel, tc.packet.Data)
case "OnRecvAcknowledgementResult":
err = suite.chainB.GetSimApp().TransferKeeper.OnAcknowledgementPacket(
suite.chainB.GetContext(), packet.SourcePort, packet.SourceChannel, tc.packet.Data,
channeltypes.NewResultAcknowledgement(nil))
case "OnRecvAcknowledgementError":
registerDenomFn()
err = suite.chainB.GetSimApp().TransferKeeper.OnAcknowledgementPacket(
suite.chainB.GetContext(), packet.SourcePort, packet.SourceChannel, tc.packet.Data,
channeltypes.NewErrorAcknowledgement(errors.New("MBT Error Acknowledgement")))
default:
err = fmt.Errorf("Unknown handler: %s", tc.handler)
}
if err != nil {
suite.Require().False(tc.pass, err.Error())
return
}
bankAfter := BankFromBalances(tc.bankAfter)
expectedBankChange := bankAfter.Sub(&bankBefore)
if err := suite.CheckBankBalances(suite.chainB, &realBankBefore, &expectedBankChange); err != nil {
suite.Require().False(tc.pass, err.Error())
return
}
suite.Require().True(tc.pass)
})
}
}
}