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  }