github.com/fibonacci-chain/fbc@v0.0.0-20231124064014-c7636198c1e9/x/wasm/keeper/recurse_test.go (about) 1 package keeper 2 3 import ( 4 "encoding/json" 5 "testing" 6 7 "github.com/fibonacci-chain/fbc/x/wasm/types" 8 9 wasmvmtypes "github.com/CosmWasm/wasmvm/types" 10 "github.com/stretchr/testify/assert" 11 "github.com/stretchr/testify/require" 12 13 sdk "github.com/fibonacci-chain/fbc/libs/cosmos-sdk/types" 14 abci "github.com/fibonacci-chain/fbc/libs/tendermint/abci/types" 15 ) 16 17 type Recurse struct { 18 Depth uint32 `json:"depth"` 19 Work uint32 `json:"work"` 20 Contract sdk.AccAddress `json:"contract"` 21 } 22 23 type recurseWrapper struct { 24 Recurse Recurse `json:"recurse"` 25 } 26 27 func buildRecurseQuery(t *testing.T, msg Recurse) []byte { 28 wrapper := recurseWrapper{Recurse: msg} 29 bz, err := json.Marshal(wrapper) 30 require.NoError(t, err) 31 return bz 32 } 33 34 type recurseResponse struct { 35 Hashed []byte `json:"hashed"` 36 } 37 38 // number os wasm queries called from a contract 39 var totalWasmQueryCounter int 40 41 func initRecurseContract(t *testing.T) (contract sdk.AccAddress, creator sdk.AccAddress, ctx sdk.Context, keeper *Keeper) { 42 countingQuerierDec := func(realWasmQuerier WasmVMQueryHandler) WasmVMQueryHandler { 43 return WasmVMQueryHandlerFn(func(ctx sdk.Context, caller sdk.AccAddress, request wasmvmtypes.QueryRequest) ([]byte, error) { 44 totalWasmQueryCounter++ 45 return realWasmQuerier.HandleQuery(ctx, caller, request) 46 }) 47 } 48 ctx, keepers := CreateTestInput(t, false, SupportedFeatures, WithQueryHandlerDecorator(countingQuerierDec)) 49 keeper = keepers.WasmKeeper 50 exampleContract := InstantiateHackatomExampleContract(t, ctx, keepers) 51 return exampleContract.Contract, exampleContract.CreatorAddr, ctx, keeper 52 } 53 54 func TestGasCostOnQuery(t *testing.T) { 55 const ( 56 GasNoWork uint64 = 63_832 57 // Note: about 100 SDK gas (10k wasmer gas) for each round of sha256 58 GasWork50 uint64 = 64_275 // this is a little shy of 50k gas - to keep an eye on the limit 59 60 GasReturnUnhashed uint64 = 33 61 GasReturnHashed uint64 = 25 62 ) 63 64 cases := map[string]struct { 65 gasLimit uint64 66 msg Recurse 67 expectedGas uint64 68 }{ 69 "no recursion, no work": { 70 gasLimit: 400_000, 71 msg: Recurse{}, 72 expectedGas: GasNoWork, 73 }, 74 "no recursion, some work": { 75 gasLimit: 400_000, 76 msg: Recurse{ 77 Work: 50, // 50 rounds of sha256 inside the contract 78 }, 79 expectedGas: GasWork50, 80 }, 81 "recursion 1, no work": { 82 gasLimit: 400_000, 83 msg: Recurse{ 84 Depth: 1, 85 }, 86 expectedGas: 2*GasNoWork + GasReturnUnhashed, 87 }, 88 "recursion 1, some work": { 89 gasLimit: 400_000, 90 msg: Recurse{ 91 Depth: 1, 92 Work: 50, 93 }, 94 expectedGas: 2*GasWork50 + GasReturnHashed, 95 }, 96 "recursion 4, some work": { 97 gasLimit: 400_000, 98 msg: Recurse{ 99 Depth: 4, 100 Work: 50, 101 }, 102 expectedGas: 5*GasWork50 + 4*GasReturnHashed, 103 }, 104 } 105 106 contractAddr, _, ctx, keeper := initRecurseContract(t) 107 108 for name, tc := range cases { 109 t.Run(name, func(t *testing.T) { 110 // external limit has no effect (we get a panic if this is enforced) 111 keeper.queryGasLimit = 1000 112 113 // make sure we set a limit before calling 114 ctx.SetGasMeter(sdk.NewGasMeter(tc.gasLimit)) 115 require.Equal(t, uint64(0), ctx.GasMeter().GasConsumed()) 116 117 // do the query 118 recurse := tc.msg 119 recurse.Contract = contractAddr 120 msg := buildRecurseQuery(t, recurse) 121 data, err := keeper.QuerySmart(ctx, contractAddr, msg) 122 require.NoError(t, err) 123 124 // check the gas is what we expected 125 if types.EnableGasVerification { 126 assert.Equal(t, tc.expectedGas, ctx.GasMeter().GasConsumed()) 127 } 128 // assert result is 32 byte sha256 hash (if hashed), or contractAddr if not 129 var resp recurseResponse 130 err = json.Unmarshal(data, &resp) 131 require.NoError(t, err) 132 if recurse.Work == 0 { 133 assert.Equal(t, len(contractAddr.String()), len(resp.Hashed)) 134 } else { 135 assert.Equal(t, 32, len(resp.Hashed)) 136 } 137 }) 138 } 139 } 140 141 func TestGasOnExternalQuery(t *testing.T) { 142 const ( 143 GasWork50 uint64 = DefaultInstanceCost + 8_464 144 ) 145 146 cases := map[string]struct { 147 gasLimit uint64 148 msg Recurse 149 expectPanic bool 150 }{ 151 "no recursion, plenty gas": { 152 gasLimit: 400_000, 153 msg: Recurse{ 154 Work: 50, // 50 rounds of sha256 inside the contract 155 }, 156 }, 157 "recursion 4, plenty gas": { 158 // this uses 244708 gas 159 gasLimit: 400_000, 160 msg: Recurse{ 161 Depth: 4, 162 Work: 50, 163 }, 164 }, 165 "no recursion, external gas limit": { 166 gasLimit: 5000, // this is not enough 167 msg: Recurse{ 168 Work: 50, 169 }, 170 expectPanic: true, 171 }, 172 "recursion 4, external gas limit": { 173 // this uses 244708 gas but give less 174 gasLimit: 4 * GasWork50, 175 msg: Recurse{ 176 Depth: 4, 177 Work: 50, 178 }, 179 expectPanic: true, 180 }, 181 } 182 183 contractAddr, _, ctx, keeper := initRecurseContract(t) 184 185 for name, tc := range cases { 186 t.Run(name, func(t *testing.T) { 187 recurse := tc.msg 188 recurse.Contract = contractAddr 189 msg := buildRecurseQuery(t, recurse) 190 191 // do the query 192 path := []string{QueryGetContractState, contractAddr.String(), QueryMethodContractStateSmart} 193 req := abci.RequestQuery{Data: msg} 194 if tc.expectPanic { 195 require.Panics(t, func() { 196 // this should run out of gas 197 _, err := NewLegacyQuerier(keeper, tc.gasLimit)(ctx, path, req) 198 t.Logf("%v", err) 199 }) 200 } else { 201 // otherwise, make sure we get a good success 202 _, err := NewLegacyQuerier(keeper, tc.gasLimit)(ctx, path, req) 203 require.NoError(t, err) 204 } 205 }) 206 } 207 } 208 209 func TestLimitRecursiveQueryGas(t *testing.T) { 210 // The point of this test from https://github.com/CosmWasm/cosmwasm/issues/456 211 // Basically, if I burn 90% of gas in CPU loop, then query out (to my self) 212 // the sub-query will have all the original gas (minus the 40k instance charge) 213 // and can burn 90% and call a sub-contract again... 214 // This attack would allow us to use far more than the provided gas before 215 // eventually hitting an OutOfGas panic. 216 217 const ( 218 // Note: about 100 SDK gas (10k wasmer gas) for each round of sha256 219 GasWork2k uint64 = 84_110 // = NewContractInstanceCosts + x // we have 6x gas used in cpu than in the instance 220 // This is overhead for calling into a sub-contract 221 GasReturnHashed uint64 = 26 222 ) 223 224 cases := map[string]struct { 225 gasLimit uint64 226 msg Recurse 227 expectQueriesFromContract int 228 expectedGas uint64 229 expectOutOfGas bool 230 expectError string 231 }{ 232 "no recursion, lots of work": { 233 gasLimit: 4_000_000, 234 msg: Recurse{ 235 Depth: 0, 236 Work: 2000, 237 }, 238 expectQueriesFromContract: 0, 239 expectedGas: GasWork2k, 240 }, 241 "recursion 5, lots of work": { 242 gasLimit: 4_000_000, 243 msg: Recurse{ 244 Depth: 5, 245 Work: 2000, 246 }, 247 expectQueriesFromContract: 5, 248 // FIXME: why -1 ... confused a bit by calculations, seems like rounding issues 249 expectedGas: GasWork2k + 5*(GasWork2k+GasReturnHashed) - 1, 250 }, 251 // this is where we expect an error... 252 // it has enough gas to run 4 times and die on the 5th (4th time dispatching to sub-contract) 253 // however, if we don't charge the cpu gas before sub-dispatching, we can recurse over 20 times 254 "deep recursion, should die on 5th level": { 255 gasLimit: 400_000, 256 msg: Recurse{ 257 Depth: 50, 258 Work: 2000, 259 }, 260 expectQueriesFromContract: 4, 261 expectOutOfGas: true, 262 }, 263 "very deep recursion, hits recursion limit": { 264 gasLimit: 10_000_000, 265 msg: Recurse{ 266 Depth: 100, 267 Work: 2000, 268 }, 269 expectQueriesFromContract: 10, 270 expectOutOfGas: false, 271 expectError: "query wasm contract failed", // Error we get from the contract instance doing the failing query, not wasmd 272 expectedGas: 10*(GasWork2k+GasReturnHashed) - 264, 273 }, 274 } 275 276 contractAddr, _, ctx, keeper := initRecurseContract(t) 277 278 for name, tc := range cases { 279 t.Run(name, func(t *testing.T) { 280 // reset the counter before test 281 totalWasmQueryCounter = 0 282 283 // make sure we set a limit before calling 284 ctx.SetGasMeter(sdk.NewGasMeter(tc.gasLimit)) 285 require.Equal(t, uint64(0), ctx.GasMeter().GasConsumed()) 286 287 // prepare the query 288 recurse := tc.msg 289 recurse.Contract = contractAddr 290 msg := buildRecurseQuery(t, recurse) 291 292 // if we expect out of gas, make sure this panics 293 if tc.expectOutOfGas { 294 require.Panics(t, func() { 295 _, err := keeper.QuerySmart(ctx, contractAddr, msg) 296 t.Logf("Got error not panic: %#v", err) 297 }) 298 assert.Equal(t, tc.expectQueriesFromContract, totalWasmQueryCounter) 299 return 300 } 301 302 // otherwise, we expect a successful call 303 _, err := keeper.QuerySmart(ctx, contractAddr, msg) 304 if tc.expectError != "" { 305 require.ErrorContains(t, err, tc.expectError) 306 } else { 307 require.NoError(t, err) 308 } 309 if types.EnableGasVerification { 310 assert.Equal(t, tc.expectedGas, ctx.GasMeter().GasConsumed()) 311 } 312 assert.Equal(t, tc.expectQueriesFromContract, totalWasmQueryCounter) 313 }) 314 } 315 }