@@ -2,15 +2,16 @@ package keeper
22
33import (
44 "encoding/json"
5- abci "github.com/tendermint/tendermint/abci/types"
65 "io/ioutil"
76 "os"
87 "testing"
98
9+ wasmTypes "github.com/CosmWasm/go-cosmwasm/types"
1010 "github.com/stretchr/testify/assert"
1111 "github.com/stretchr/testify/require"
1212
1313 sdk "github.com/cosmos/cosmos-sdk/types"
14+ abci "github.com/tendermint/tendermint/abci/types"
1415)
1516
1617type Recurse struct {
@@ -34,6 +35,50 @@ type recurseResponse struct {
3435 Hashed []byte `json:"hashed"`
3536}
3637
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 , cleanup func ()) {
42+ // we do one basic setup before all test cases (which are read-only and don't change state)
43+ tempDir , err := ioutil .TempDir ("" , "wasm" )
44+ require .NoError (t , err )
45+ cleanup = func () { os .RemoveAll (tempDir ) }
46+
47+ var realWasmQuerier func (ctx sdk.Context , request * wasmTypes.WasmQuery ) ([]byte , error )
48+ countingQuerier := & QueryPlugins {
49+ Wasm : func (ctx sdk.Context , request * wasmTypes.WasmQuery ) ([]byte , error ) {
50+ totalWasmQueryCounter ++
51+ return realWasmQuerier (ctx , request )
52+ },
53+ }
54+ ctx , keepers := CreateTestInput (t , false , tempDir , SupportedFeatures , nil , countingQuerier )
55+ accKeeper , keeper := keepers .AccountKeeper , keepers .WasmKeeper
56+ realWasmQuerier = WasmQuerier (& keeper )
57+
58+ deposit := sdk .NewCoins (sdk .NewInt64Coin ("denom" , 100000 ))
59+ creator = createFakeFundedAccount (ctx , accKeeper , deposit .Add (deposit ... ))
60+
61+ // store the code
62+ wasmCode , err := ioutil .ReadFile ("./testdata/contract.wasm" )
63+ require .NoError (t , err )
64+ codeID , err := keeper .Create (ctx , creator , wasmCode , "" , "" , nil )
65+ require .NoError (t , err )
66+
67+ // instantiate the contract
68+ _ , _ , bob := keyPubAddr ()
69+ _ , _ , fred := keyPubAddr ()
70+ initMsg := InitMsg {
71+ Verifier : fred ,
72+ Beneficiary : bob ,
73+ }
74+ initMsgBz , err := json .Marshal (initMsg )
75+ require .NoError (t , err )
76+ contractAddr , err := keeper .Instantiate (ctx , codeID , creator , nil , initMsgBz , "recursive contract" , deposit )
77+ require .NoError (t , err )
78+
79+ return contractAddr , creator , ctx , keeper , cleanup
80+ }
81+
3782func TestGasCostOnQuery (t * testing.T ) {
3883 const (
3984 GasNoWork uint64 = InstanceCost + 2_756
@@ -87,33 +132,8 @@ func TestGasCostOnQuery(t *testing.T) {
87132 },
88133 }
89134
90- // we do one basic setup before all test cases (which are read-only and don't change state)
91- tempDir , err := ioutil .TempDir ("" , "wasm" )
92- require .NoError (t , err )
93- defer os .RemoveAll (tempDir )
94-
95- ctx , keepers := CreateTestInput (t , false , tempDir , SupportedFeatures , nil , nil )
96- accKeeper , keeper := keepers .AccountKeeper , keepers .WasmKeeper
97- deposit := sdk .NewCoins (sdk .NewInt64Coin ("denom" , 100000 ))
98- creator := createFakeFundedAccount (ctx , accKeeper , deposit .Add (deposit ... ))
99-
100- // store the code
101- wasmCode , err := ioutil .ReadFile ("./testdata/contract.wasm" )
102- require .NoError (t , err )
103- codeID , err := keeper .Create (ctx , creator , wasmCode , "" , "" , nil )
104- require .NoError (t , err )
105-
106- // instantiate the contract
107- _ , _ , bob := keyPubAddr ()
108- _ , _ , fred := keyPubAddr ()
109- initMsg := InitMsg {
110- Verifier : fred ,
111- Beneficiary : bob ,
112- }
113- initMsgBz , err := json .Marshal (initMsg )
114- require .NoError (t , err )
115- contractAddr , err := keeper .Instantiate (ctx , codeID , creator , nil , initMsgBz , "recursive contract" , deposit )
116- require .NoError (t , err )
135+ contractAddr , creator , ctx , keeper , cleanup := initRecurseContract (t )
136+ defer cleanup ()
117137
118138 for name , tc := range cases {
119139 t .Run (name , func (t * testing.T ) {
@@ -149,8 +169,7 @@ func TestGasCostOnQuery(t *testing.T) {
149169
150170func TestGasOnExternalQuery (t * testing.T ) {
151171 const (
152- GasWork50 uint64 = InstanceCost + 8_464
153- GasReturnHashed uint64 = 597
172+ GasWork50 uint64 = InstanceCost + 8_464
154173 )
155174
156175 cases := map [string ]struct {
@@ -190,33 +209,8 @@ func TestGasOnExternalQuery(t *testing.T) {
190209 },
191210 }
192211
193- // we do one basic setup before all test cases (which are read-only and don't change state)
194- tempDir , err := ioutil .TempDir ("" , "wasm" )
195- require .NoError (t , err )
196- defer os .RemoveAll (tempDir )
197-
198- ctx , keepers := CreateTestInput (t , false , tempDir , SupportedFeatures , nil , nil )
199- accKeeper , keeper := keepers .AccountKeeper , keepers .WasmKeeper
200- deposit := sdk .NewCoins (sdk .NewInt64Coin ("denom" , 100000 ))
201- creator := createFakeFundedAccount (ctx , accKeeper , deposit .Add (deposit ... ))
202-
203- // store the code
204- wasmCode , err := ioutil .ReadFile ("./testdata/contract.wasm" )
205- require .NoError (t , err )
206- codeID , err := keeper .Create (ctx , creator , wasmCode , "" , "" , nil )
207- require .NoError (t , err )
208-
209- // instantiate the contract
210- _ , _ , bob := keyPubAddr ()
211- _ , _ , fred := keyPubAddr ()
212- initMsg := InitMsg {
213- Verifier : fred ,
214- Beneficiary : bob ,
215- }
216- initMsgBz , err := json .Marshal (initMsg )
217- require .NoError (t , err )
218- contractAddr , err := keeper .Instantiate (ctx , codeID , creator , nil , initMsgBz , "recursive contract" , deposit )
219- require .NoError (t , err )
212+ contractAddr , _ , ctx , keeper , cleanup := initRecurseContract (t )
213+ defer cleanup ()
220214
221215 for name , tc := range cases {
222216 t .Run (name , func (t * testing.T ) {
@@ -233,7 +227,8 @@ func TestGasOnExternalQuery(t *testing.T) {
233227 if tc .expectPanic {
234228 require .Panics (t , func () {
235229 // this should run out of gas
236- _ , _ = NewQuerier (keeper )(ctx , path , req )
230+ _ , err := NewQuerier (keeper )(ctx , path , req )
231+ t .Logf ("%v" , err )
237232 })
238233 } else {
239234 // otherwise, make sure we get a good success
@@ -243,3 +238,95 @@ func TestGasOnExternalQuery(t *testing.T) {
243238 })
244239 }
245240}
241+
242+ func TestLimitRecursiveQueryGas (t * testing.T ) {
243+ // The point of this test from https://github.com/CosmWasm/cosmwasm/issues/456
244+ // Basically, if I burn 90% of gas in CPU loop, then query out (to my self)
245+ // the sub-query will have all the original gas (minus the 40k instance charge)
246+ // and can burn 90% and call a sub-contract again...
247+ // This attack would allow us to use far more than the provided gas before
248+ // eventually hitting an OutOfGas panic.
249+
250+ const (
251+ // Note: about 100 SDK gas (10k wasmer gas) for each round of sha256
252+ GasWork2k uint64 = InstanceCost + 233_379 // we have 6x gas used in cpu than in the instance
253+ // This is overhead for calling into a sub-contract
254+ GasReturnHashed uint64 = 603
255+ )
256+
257+ cases := map [string ]struct {
258+ gasLimit uint64
259+ msg Recurse
260+ expectQueriesFromContract int
261+ expectedGas uint64
262+ expectOutOfGas bool
263+ }{
264+ "no recursion, lots of work" : {
265+ gasLimit : 4_000_000 ,
266+ msg : Recurse {
267+ Depth : 0 ,
268+ Work : 2000 ,
269+ },
270+ expectQueriesFromContract : 0 ,
271+ expectedGas : GasWork2k ,
272+ },
273+ "recursion 5, lots of work" : {
274+ gasLimit : 4_000_000 ,
275+ msg : Recurse {
276+ Depth : 5 ,
277+ Work : 2000 ,
278+ },
279+ expectQueriesFromContract : 5 ,
280+ expectedGas : GasWork2k + 5 * (GasWork2k + GasReturnHashed ),
281+ },
282+ // this is where we expect an error...
283+ // it has enough gas to run 4 times and die on the 5th (4th time dispatching to sub-contract)
284+ // however, if we don't charge the cpu gas before sub-dispatching, we can recurse over 20 times
285+ // TODO: figure out how to asset how deep it went
286+ "deep recursion, should die on 5th level" : {
287+ gasLimit : 1_200_000 ,
288+ msg : Recurse {
289+ Depth : 50 ,
290+ Work : 2000 ,
291+ },
292+ expectQueriesFromContract : 4 ,
293+ expectOutOfGas : true ,
294+ },
295+ }
296+
297+ contractAddr , _ , ctx , keeper , cleanup := initRecurseContract (t )
298+ defer cleanup ()
299+
300+ for name , tc := range cases {
301+ t .Run (name , func (t * testing.T ) {
302+ // reset the counter before test
303+ totalWasmQueryCounter = 0
304+
305+ // make sure we set a limit before calling
306+ ctx = ctx .WithGasMeter (sdk .NewGasMeter (tc .gasLimit ))
307+ require .Equal (t , uint64 (0 ), ctx .GasMeter ().GasConsumed ())
308+
309+ // prepare the query
310+ recurse := tc .msg
311+ recurse .Contract = contractAddr
312+ msg := buildQuery (t , recurse )
313+
314+ // if we expect out of gas, make sure this panics
315+ if tc .expectOutOfGas {
316+ require .Panics (t , func () {
317+ _ , err := keeper .QuerySmart (ctx , contractAddr , msg )
318+ t .Logf ("Got error not panic: %#v" , err )
319+ })
320+ assert .Equal (t , tc .expectQueriesFromContract , totalWasmQueryCounter )
321+ return
322+ }
323+
324+ // otherwise, we expect a successful call
325+ _ , err := keeper .QuerySmart (ctx , contractAddr , msg )
326+ require .NoError (t , err )
327+ assert .Equal (t , tc .expectedGas , ctx .GasMeter ().GasConsumed ())
328+
329+ assert .Equal (t , tc .expectQueriesFromContract , totalWasmQueryCounter )
330+ })
331+ }
332+ }
0 commit comments