package keeper import ( "encoding/binary" "encoding/json" "io/ioutil" "os" "testing" "time" "github.com/CosmWasm/wasmd/x/wasm/internal/types" stypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto/ed25519" ) const SupportedFeatures = "staking" func TestNewKeeper(t *testing.T) { tempDir, err := ioutil.TempDir("", "wasm") require.NoError(t, err) defer os.RemoveAll(tempDir) _, _, keeper := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) require.NotNil(t, keeper) } func TestCreate(t *testing.T) { tempDir, err := ioutil.TempDir("", "wasm") require.NoError(t, err) defer os.RemoveAll(tempDir) ctx, accKeeper, keeper := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) creator := createFakeFundedAccount(ctx, accKeeper, deposit) wasmCode, err := ioutil.ReadFile("./testdata/contract.wasm") require.NoError(t, err) contractID, err := keeper.Create(ctx, creator, wasmCode, "https://github.com/CosmWasm/wasmd/blob/master/x/wasm/testdata/escrow.wasm", "cosmwasm-opt:0.5.2") require.NoError(t, err) require.Equal(t, uint64(1), contractID) // and verify content storedCode, err := keeper.GetByteCode(ctx, contractID) require.NoError(t, err) require.Equal(t, wasmCode, storedCode) } func TestCreateDuplicate(t *testing.T) { tempDir, err := ioutil.TempDir("", "wasm") require.NoError(t, err) defer os.RemoveAll(tempDir) ctx, accKeeper, keeper := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) creator := createFakeFundedAccount(ctx, accKeeper, deposit) wasmCode, err := ioutil.ReadFile("./testdata/contract.wasm") require.NoError(t, err) // create one copy contractID, err := keeper.Create(ctx, creator, wasmCode, "https://github.com/CosmWasm/wasmd/blob/master/x/wasm/testdata/escrow.wasm", "cosmwasm-opt:0.5.2") require.NoError(t, err) require.Equal(t, uint64(1), contractID) // create second copy duplicateID, err := keeper.Create(ctx, creator, wasmCode, "https://github.com/CosmWasm/wasmd/blob/master/x/wasm/testdata/escrow.wasm", "cosmwasm-opt:0.5.2") require.NoError(t, err) require.Equal(t, uint64(2), duplicateID) // and verify both content is proper storedCode, err := keeper.GetByteCode(ctx, contractID) require.NoError(t, err) require.Equal(t, wasmCode, storedCode) storedCode, err = keeper.GetByteCode(ctx, duplicateID) require.NoError(t, err) require.Equal(t, wasmCode, storedCode) } func TestCreateWithSimulation(t *testing.T) { tempDir, err := ioutil.TempDir("", "wasm") require.NoError(t, err) defer os.RemoveAll(tempDir) ctx, accKeeper, keeper := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) ctx = ctx.WithBlockHeader(abci.Header{Height: 1}). WithGasMeter(stypes.NewInfiniteGasMeter()) deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) creator := createFakeFundedAccount(ctx, accKeeper, deposit) wasmCode, err := ioutil.ReadFile("./testdata/contract.wasm") require.NoError(t, err) // create this once in simulation mode contractID, err := keeper.Create(ctx, creator, wasmCode, "https://github.com/CosmWasm/wasmd/blob/master/x/wasm/testdata/escrow.wasm", "confio/cosmwasm-opt:0.57.2") require.NoError(t, err) require.Equal(t, uint64(1), contractID) // then try to create it in non-simulation mode (should not fail) ctx, accKeeper, keeper = CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) contractID, err = keeper.Create(ctx, creator, wasmCode, "https://github.com/CosmWasm/wasmd/blob/master/x/wasm/testdata/escrow.wasm", "confio/cosmwasm-opt:0.7.2") require.NoError(t, err) require.Equal(t, uint64(1), contractID) // and verify content code, err := keeper.GetByteCode(ctx, contractID) require.NoError(t, err) require.Equal(t, code, wasmCode) } func TestIsSimulationMode(t *testing.T) { specs := map[string]struct { ctx sdk.Context exp bool }{ "genesis block": { ctx: sdk.Context{}.WithBlockHeader(abci.Header{}).WithGasMeter(stypes.NewInfiniteGasMeter()), exp: false, }, "any regular block": { ctx: sdk.Context{}.WithBlockHeader(abci.Header{Height: 1}).WithGasMeter(stypes.NewGasMeter(10000000)), exp: false, }, "simulation": { ctx: sdk.Context{}.WithBlockHeader(abci.Header{Height: 1}).WithGasMeter(stypes.NewInfiniteGasMeter()), exp: true, }, } for msg, _ := range specs { t.Run(msg, func(t *testing.T) { //assert.Equal(t, spec.exp, isSimulationMode(spec.ctx)) }) } } func TestCreateWithGzippedPayload(t *testing.T) { tempDir, err := ioutil.TempDir("", "wasm") require.NoError(t, err) defer os.RemoveAll(tempDir) ctx, accKeeper, keeper := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) creator := createFakeFundedAccount(ctx, accKeeper, deposit) wasmCode, err := ioutil.ReadFile("./testdata/contract.wasm.gzip") require.NoError(t, err) contractID, err := keeper.Create(ctx, creator, wasmCode, "https://github.com/CosmWasm/wasmd/blob/master/x/wasm/testdata/escrow.wasm", "") require.NoError(t, err) require.Equal(t, uint64(1), contractID) // and verify content storedCode, err := keeper.GetByteCode(ctx, contractID) require.NoError(t, err) rawCode, err := ioutil.ReadFile("./testdata/contract.wasm") require.NoError(t, err) require.Equal(t, rawCode, storedCode) } func TestInstantiate(t *testing.T) { tempDir, err := ioutil.TempDir("", "wasm") require.NoError(t, err) defer os.RemoveAll(tempDir) ctx, accKeeper, keeper := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) creator := createFakeFundedAccount(ctx, accKeeper, deposit) wasmCode, err := ioutil.ReadFile("./testdata/contract.wasm") require.NoError(t, err) contractID, err := keeper.Create(ctx, creator, wasmCode, "https://github.com/CosmWasm/wasmd/blob/master/x/wasm/testdata/escrow.wasm", "") require.NoError(t, err) _, _, bob := keyPubAddr() _, _, fred := keyPubAddr() initMsg := InitMsg{ Verifier: fred, Beneficiary: bob, } initMsgBz, err := json.Marshal(initMsg) require.NoError(t, err) gasBefore := ctx.GasMeter().GasConsumed() // create with no balance is also legal addr, err := keeper.Instantiate(ctx, contractID, creator, initMsgBz, "demo contract 1", nil) require.NoError(t, err) require.Equal(t, "cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5", addr.String()) gasAfter := ctx.GasMeter().GasConsumed() require.Equal(t, uint64(0x61f7), gasAfter-gasBefore) // ensure it is stored properly info := keeper.GetContractInfo(ctx, addr) require.NotNil(t, info) assert.Equal(t, info.Creator, creator) assert.Equal(t, info.CodeID, contractID) assert.Equal(t, info.InitMsg, json.RawMessage(initMsgBz)) assert.Equal(t, info.Label, "demo contract 1") } func TestInstantiateWithNonExistingCodeID(t *testing.T) { tempDir, err := ioutil.TempDir("", "wasm") require.NoError(t, err) defer os.RemoveAll(tempDir) ctx, accKeeper, keeper := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) creator := createFakeFundedAccount(ctx, accKeeper, deposit) require.NoError(t, err) initMsg := InitMsg{} initMsgBz, err := json.Marshal(initMsg) require.NoError(t, err) const nonExistingCodeID = 9999 addr, err := keeper.Instantiate(ctx, nonExistingCodeID, creator, initMsgBz, "demo contract 2", nil) require.True(t, types.ErrNotFound.Is(err), err) require.Nil(t, addr) } func TestExecute(t *testing.T) { tempDir, err := ioutil.TempDir("", "wasm") require.NoError(t, err) defer os.RemoveAll(tempDir) ctx, accKeeper, keeper := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) topUp := sdk.NewCoins(sdk.NewInt64Coin("denom", 5000)) creator := createFakeFundedAccount(ctx, accKeeper, deposit.Add(deposit...)) fred := createFakeFundedAccount(ctx, accKeeper, topUp) wasmCode, err := ioutil.ReadFile("./testdata/contract.wasm") require.NoError(t, err) contractID, err := keeper.Create(ctx, creator, wasmCode, "", "") require.NoError(t, err) _, _, bob := keyPubAddr() initMsg := InitMsg{ Verifier: fred, Beneficiary: bob, } initMsgBz, err := json.Marshal(initMsg) require.NoError(t, err) addr, err := keeper.Instantiate(ctx, contractID, creator, initMsgBz, "demo contract 3", deposit) require.NoError(t, err) require.Equal(t, "cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5", addr.String()) // ensure bob doesn't exist bobAcct := accKeeper.GetAccount(ctx, bob) require.Nil(t, bobAcct) // ensure funder has reduced balance creatorAcct := accKeeper.GetAccount(ctx, creator) require.NotNil(t, creatorAcct) // we started at 2*deposit, should have spent one above assert.Equal(t, deposit, creatorAcct.GetCoins()) // ensure contract has updated balance contractAcct := accKeeper.GetAccount(ctx, addr) require.NotNil(t, contractAcct) assert.Equal(t, deposit, contractAcct.GetCoins()) // unauthorized - trialCtx so we don't change state trialCtx := ctx.WithMultiStore(ctx.MultiStore().CacheWrap().(sdk.MultiStore)) res, err := keeper.Execute(trialCtx, addr, creator, []byte(`{"release":{}}`), nil) require.Error(t, err) require.Contains(t, err.Error(), "unauthorized") // verifier can execute, and get proper gas amount start := time.Now() gasBefore := ctx.GasMeter().GasConsumed() res, err = keeper.Execute(ctx, addr, fred, []byte(`{"release":{}}`), topUp) diff := time.Now().Sub(start) require.NoError(t, err) require.NotNil(t, res) // make sure gas is properly deducted from ctx gasAfter := ctx.GasMeter().GasConsumed() require.Equal(t, uint64(0x7b20), gasAfter-gasBefore) // ensure bob now exists and got both payments released bobAcct = accKeeper.GetAccount(ctx, bob) require.NotNil(t, bobAcct) balance := bobAcct.GetCoins() assert.Equal(t, deposit.Add(topUp...), balance) // ensure contract has updated balance contractAcct = accKeeper.GetAccount(ctx, addr) require.NotNil(t, contractAcct) assert.Equal(t, sdk.Coins(nil), contractAcct.GetCoins()) t.Logf("Duration: %v (31728 gas)\n", diff) } func TestExecuteWithNonExistingAddress(t *testing.T) { tempDir, err := ioutil.TempDir("", "wasm") require.NoError(t, err) defer os.RemoveAll(tempDir) ctx, accKeeper, keeper := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) creator := createFakeFundedAccount(ctx, accKeeper, deposit.Add(deposit...)) // unauthorized - trialCtx so we don't change state nonExistingAddress := addrFromUint64(9999) _, err = keeper.Execute(ctx, nonExistingAddress, creator, []byte(`{}`), nil) require.True(t, types.ErrNotFound.Is(err), err) } func TestExecuteWithPanic(t *testing.T) { tempDir, err := ioutil.TempDir("", "wasm") require.NoError(t, err) defer os.RemoveAll(tempDir) ctx, accKeeper, keeper := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) topUp := sdk.NewCoins(sdk.NewInt64Coin("denom", 5000)) creator := createFakeFundedAccount(ctx, accKeeper, deposit.Add(deposit...)) fred := createFakeFundedAccount(ctx, accKeeper, topUp) wasmCode, err := ioutil.ReadFile("./testdata/contract.wasm") require.NoError(t, err) contractID, err := keeper.Create(ctx, creator, wasmCode, "", "") require.NoError(t, err) _, _, bob := keyPubAddr() initMsg := InitMsg{ Verifier: fred, Beneficiary: bob, } initMsgBz, err := json.Marshal(initMsg) require.NoError(t, err) addr, err := keeper.Instantiate(ctx, contractID, creator, initMsgBz, "demo contract 4", deposit) require.NoError(t, err) // let's make sure we get a reasonable error, no panic/crash _, err = keeper.Execute(ctx, addr, fred, []byte(`{"panic":{}}`), topUp) require.Error(t, err) } func TestExecuteWithCpuLoop(t *testing.T) { tempDir, err := ioutil.TempDir("", "wasm") require.NoError(t, err) defer os.RemoveAll(tempDir) ctx, accKeeper, keeper := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) topUp := sdk.NewCoins(sdk.NewInt64Coin("denom", 5000)) creator := createFakeFundedAccount(ctx, accKeeper, deposit.Add(deposit...)) fred := createFakeFundedAccount(ctx, accKeeper, topUp) wasmCode, err := ioutil.ReadFile("./testdata/contract.wasm") require.NoError(t, err) contractID, err := keeper.Create(ctx, creator, wasmCode, "", "") require.NoError(t, err) _, _, bob := keyPubAddr() initMsg := InitMsg{ Verifier: fred, Beneficiary: bob, } initMsgBz, err := json.Marshal(initMsg) require.NoError(t, err) addr, err := keeper.Instantiate(ctx, contractID, creator, initMsgBz, "demo contract 5", deposit) require.NoError(t, err) // make sure we set a limit before calling var gasLimit uint64 = 400_000 ctx = ctx.WithGasMeter(sdk.NewGasMeter(gasLimit)) require.Equal(t, uint64(0), ctx.GasMeter().GasConsumed()) // this must fail _, err = keeper.Execute(ctx, addr, fred, []byte(`{"cpu_loop":{}}`), nil) assert.Error(t, err) // make sure gas ran out // TODO: wasmer doesn't return gas used on error. we should consume it (for error on metering failure) // require.Equal(t, gasLimit, ctx.GasMeter().GasConsumed()) } func TestExecuteWithStorageLoop(t *testing.T) { tempDir, err := ioutil.TempDir("", "wasm") require.NoError(t, err) defer os.RemoveAll(tempDir) ctx, accKeeper, keeper := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) topUp := sdk.NewCoins(sdk.NewInt64Coin("denom", 5000)) creator := createFakeFundedAccount(ctx, accKeeper, deposit.Add(deposit...)) fred := createFakeFundedAccount(ctx, accKeeper, topUp) wasmCode, err := ioutil.ReadFile("./testdata/contract.wasm") require.NoError(t, err) contractID, err := keeper.Create(ctx, creator, wasmCode, "", "") require.NoError(t, err) _, _, bob := keyPubAddr() initMsg := InitMsg{ Verifier: fred, Beneficiary: bob, } initMsgBz, err := json.Marshal(initMsg) require.NoError(t, err) addr, err := keeper.Instantiate(ctx, contractID, creator, initMsgBz, "demo contract 6", deposit) require.NoError(t, err) // make sure we set a limit before calling var gasLimit uint64 = 400_000 ctx = ctx.WithGasMeter(sdk.NewGasMeter(gasLimit)) require.Equal(t, uint64(0), ctx.GasMeter().GasConsumed()) // ensure we get an out of gas panic defer func() { r := recover() require.NotNil(t, r) // TODO: ensure it is out of gas error _, ok := r.(sdk.ErrorOutOfGas) require.True(t, ok, "%v", r) }() // this should throw out of gas exception (panic) _, err = keeper.Execute(ctx, addr, fred, []byte(`{"storage_loop":{}}`), nil) require.True(t, false, "We must panic before this line") } type InitMsg struct { Verifier sdk.AccAddress `json:"verifier"` Beneficiary sdk.AccAddress `json:"beneficiary"` } func createFakeFundedAccount(ctx sdk.Context, am auth.AccountKeeper, coins sdk.Coins) sdk.AccAddress { _, _, addr := keyPubAddr() baseAcct := auth.NewBaseAccountWithAddress(addr) _ = baseAcct.SetCoins(coins) am.SetAccount(ctx, &baseAcct) return addr } var keyCounter uint64 = 0 // we need to make this deterministic (same every test run), as encoded address size and thus gas cost, // depends on the actual bytes (due to ugly CanonicalAddress encoding) func keyPubAddr() (crypto.PrivKey, crypto.PubKey, sdk.AccAddress) { keyCounter++ seed := make([]byte, 8) binary.BigEndian.PutUint64(seed, keyCounter) key := ed25519.GenPrivKeyFromSecret(seed) pub := key.PubKey() addr := sdk.AccAddress(pub.Address()) return key, pub, addr }