Skip to content

Commit 0156e75

Browse files
authored
[CON-126] Do not return error string on precompile error (#2563)
## Describe your changes and provide context ### Context Different precompile error messages lead to app hash. Reproduction suite: https://github.com/sei-protocol/sei-chain/pull/2545/files ### Mechanism `resultsHash` which is part of consensus is derived by hashing marshalled transaction results, only deterministic fields should be included during the marshalling [operation](https://github.com/sei-protocol/sei-chain/blob/fe95a1ff76a108ff37347a16ee6139509322e058/sei-tendermint/abci/types/types.go#L199-L214) - [error code](https://github.com/sei-protocol/sei-chain/blob/fe95a1ff76a108ff37347a16ee6139509322e058/sei-tendermint/proto/tendermint/abci/types.proto#L429), [data](https://github.com/sei-protocol/sei-chain/blob/fe95a1ff76a108ff37347a16ee6139509322e058/sei-tendermint/proto/tendermint/abci/types.proto#L430), [gas_wanted](https://github.com/sei-protocol/sei-chain/blob/fe95a1ff76a108ff37347a16ee6139509322e058/sei-tendermint/proto/tendermint/abci/types.proto#L433), [gas_used](https://github.com/sei-protocol/sei-chain/blob/fe95a1ff76a108ff37347a16ee6139509322e058/sei-tendermint/proto/tendermint/abci/types.proto#L434). Initially I thought that the error code is being [decoded indeterministically](https://github.com/sei-protocol/sei-chain/blob/06a4e242bf80fff303be607734e121bd2f0f6916/sei-cosmos/types/errors/abci.go#L39) which turned out [untrue](https://github.com/sei-protocol/sei-chain/blob/fe95a1ff76a108ff37347a16ee6139509322e058/sei-cosmos/baseapp/abci.go#L283-L289). It turned out that the data field is indeterministic. Return data gets [populated with a stringified error ](https://github.com/sei-protocol/sei-chain/blob/07441d7bfcd7f9fc69119cf3002be7d6912b3a87/precompiles/common/precompiles.go#L159-L166)if the precompile errors out, this bubbles up as aforementioned [data](https://github.com/sei-protocol/sei-chain/blob/fe95a1ff76a108ff37347a16ee6139509322e058/sei-tendermint/proto/tendermint/abci/types.proto#L430), stringified error can among other things contain the path to executable by way of including the call stack. PR that introduced the issue - #1757. ### Potential Solutions One of the reasons ABCI decodes errors and excludes [log](https://github.com/sei-protocol/sei-chain/blob/fe95a1ff76a108ff37347a16ee6139509322e058/sei-tendermint/proto/tendermint/abci/types.proto#L293), [info](https://github.com/sei-protocol/sei-chain/blob/fe95a1ff76a108ff37347a16ee6139509322e058/sei-tendermint/proto/tendermint/abci/types.proto#L294), [events](https://github.com/sei-protocol/sei-chain/blob/fe95a1ff76a108ff37347a16ee6139509322e058/sei-tendermint/proto/tendermint/abci/types.proto#L297) fields from consensus is to guard from indeterminism. Ethereum on the other side discerns between two types of errors during transaction execution - vmerrors and consensus/client errors - vmerrors are not part of consensus in any way and errors coming from precompile runs fall into this category. Broadly there are a couple of ways to solve this: 1. Remove all inderterminism from errors - do not wrap errors, etc. - short term this will work for the specific scenario we encountered, long term we will have exactly the same issue because we will forget that changing an error message is an app hash break 2. Populate return data deterministically if a precompile errors out - this approach would be similar to what ABCI does by reducing errors to codes/codespaces by decoding them 3. Do not populate return data if a precompile errors out - precompiles that error out should never have side effects (to be confirmed) which makes this approach viable and it is the most right/Ethereum equivalent way of doing things ### Solution [1.] is a hotfix - which we already have - but not a long running solution as every precompile error message diff would lead to app hash break. [2.] does work but we've got no additional benefits from reducing precompile errors to error codes/spaces. Therefore picked [3.] What we lose by this solution is visibility into the specific error that happened - we can retain that by concatenate the error string to the execution reverted error. Comment on security concerns (proof by negation): This won't affect security - the only way in which including a precompile error message into consensus can boost security is by making sure the precompile run of every actor ended at exactly the same point - this can easily be bypassed (both with or without this PR) by a malicious actor executing the actual precompile and additional code besides that so guardrails should be elsewhere. ## Testing performed to validate your change Ran a local chain against the testing suite, also CI/CD.
1 parent cc5bd64 commit 0156e75

File tree

9 files changed

+20
-22
lines changed

9 files changed

+20
-22
lines changed

precompiles/addr/addr_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ func TestAssociatePubKey(t *testing.T) {
167167
}
168168
if err != nil {
169169
require.Equal(t, vm.ErrExecutionReverted, err)
170-
require.Equal(t, tt.wantErrMsg, string(ret))
170+
require.Nil(t, ret)
171171
} else if tt.wrongRet {
172172
// tt.wrongRet is set if we expect a return value that's different from the happy path. This means that the wrong addresses were associated.
173173
require.NotEqual(t, tt.wantRet, ret)
@@ -358,7 +358,7 @@ func TestAssociate(t *testing.T) {
358358
}
359359
if err != nil {
360360
require.Equal(t, vm.ErrExecutionReverted, err)
361-
require.Equal(t, tt.wantErrMsg, string(ret))
361+
require.Nil(t, ret)
362362
} else if tt.wrongRet {
363363
// tt.wrongRet is set if we expect a return value that's different from the happy path. This means that the wrong addresses were associated.
364364
require.NotEqual(t, tt.wantRet, ret)

precompiles/common/precompiles.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ func (p Precompile) Run(evm *vm.EVM, caller common.Address, callingContract comm
6666
defer func() {
6767
HandlePrecompileError(err, evm, operation)
6868
if err != nil {
69-
bz = []byte(err.Error())
7069
err = vm.ErrExecutionReverted
7170
}
7271
}()
@@ -159,7 +158,6 @@ func (d DynamicGasPrecompile) RunAndCalculateGas(evm *vm.EVM, caller common.Addr
159158
defer func() {
160159
HandlePrecompileError(err, evm, operation)
161160
if err != nil {
162-
ret = []byte(err.Error())
163161
fmt.Printf("precompile %s encountered error: %v\n", d.name, err)
164162
err = vm.ErrExecutionReverted
165163
}

precompiles/common/precompiles_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ func TestPrecompileRun(t *testing.T) {
8686
stateDB.WithCtx(ctx.WithEventManager(sdk.NewEventManager()))
8787
precompile = common.NewPrecompile(newAbi, &MockPrecompileExecutor{throw: true}, ethcommon.Address{}, "test")
8888
res, err = precompile.Run(&vm.EVM{StateDB: stateDB}, ethcommon.Address{}, ethcommon.Address{}, input, big.NewInt(0), false, false, nil)
89-
require.NotNil(t, res)
89+
require.Nil(t, res)
9090
require.NotNil(t, err)
9191
// should not emit any event
9292
require.Empty(t, stateDB.Ctx().EventManager().Events())
@@ -127,7 +127,7 @@ func TestDynamicGasPrecompileRun(t *testing.T) {
127127
stateDB.WithCtx(ctx.WithEventManager(sdk.NewEventManager()))
128128
precompile = common.NewDynamicGasPrecompile(newAbi, &MockDynamicGasPrecompileExecutor{throw: true, evmKeeper: k}, ethcommon.Address{}, "test")
129129
res, _, err = precompile.RunAndCalculateGas(&vm.EVM{StateDB: stateDB}, ethcommon.Address{}, ethcommon.Address{}, input, 0, big.NewInt(0), nil, false, false)
130-
require.NotNil(t, res)
130+
require.Nil(t, res)
131131
require.NotNil(t, err)
132132
// should not emit any event
133133
require.Empty(t, stateDB.Ctx().EventManager().Events())

precompiles/distribution/distribution_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,7 @@ func TestPrecompile_RunAndCalculateGas_WithdrawDelegationRewards(t *testing.T) {
502502
}
503503
if err != nil {
504504
require.Equal(t, vm.ErrExecutionReverted, err)
505-
require.Equal(t, tt.wantErrMsg, string(gotRet))
505+
require.Nil(t, gotRet)
506506
} else if !reflect.DeepEqual(gotRet, tt.wantRet) {
507507
t.Errorf("RunAndCalculateGas() gotRet = %v, want %v", gotRet, tt.wantRet)
508508
}
@@ -663,7 +663,7 @@ func TestPrecompile_RunAndCalculateGas_WithdrawMultipleDelegationRewards(t *test
663663
}
664664
if err != nil {
665665
require.Equal(t, vm.ErrExecutionReverted, err)
666-
require.Equal(t, tt.wantErrMsg, string(gotRet))
666+
require.Nil(t, gotRet)
667667
} else if !reflect.DeepEqual(gotRet, tt.wantRet) {
668668
t.Errorf("RunAndCalculateGas() gotRet = %v, want %v", gotRet, tt.wantRet)
669669
}
@@ -839,7 +839,7 @@ func TestPrecompile_RunAndCalculateGas_SetWithdrawAddress(t *testing.T) {
839839
}
840840
if err != nil {
841841
require.Equal(t, vm.ErrExecutionReverted, err)
842-
require.Equal(t, tt.wantErrMsg, string(gotRet))
842+
require.Nil(t, gotRet)
843843
} else if !reflect.DeepEqual(gotRet, tt.wantRet) {
844844
t.Errorf("RunAndCalculateGas() gotRet = %v, want %v", gotRet, tt.wantRet)
845845
}
@@ -1136,7 +1136,7 @@ func TestPrecompile_RunAndCalculateGas_Rewards(t *testing.T) {
11361136
}
11371137
if err != nil {
11381138
require.Equal(t, vm.ErrExecutionReverted, err)
1139-
require.Equal(t, tt.wantErrMsg, string(gotRet))
1139+
require.Nil(t, gotRet)
11401140
} else if !reflect.DeepEqual(gotRet, tt.wantRet) {
11411141
t.Errorf("RunAndCalculateGas() gotRet = %v, want %v", gotRet, tt.wantRet)
11421142
}
@@ -1235,7 +1235,7 @@ func TestWithdrawValidatorCommission_noCommissionToWithdrawRightAfterDelegation(
12351235
ante.Preprocess(ctx, req, k.ChainID(ctx), false)
12361236
res, err = msgServer.EVMTransaction(sdk.WrapSDKContext(ctx), req)
12371237
require.Nil(t, err)
1238-
require.Equal(t, "no validator commission to withdraw", string(res.ReturnData))
1238+
require.Nil(t, res.ReturnData)
12391239

12401240
}
12411241

@@ -1376,7 +1376,7 @@ func TestWithdrawValidatorCommission_InputValidation(t *testing.T) {
13761376

13771377
if tc.wantError {
13781378
require.NotNil(t, err, "Expected error for test case: %s", tc.name)
1379-
require.Equal(t, tc.wantErrMsg, string(ret))
1379+
require.Nil(t, ret)
13801380
} else {
13811381
require.Nil(t, err, "Expected no error for test case: %s", tc.name)
13821382
require.Greater(t, remainingGas, uint64(0), "Should have remaining gas")

precompiles/gov/gov_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ func TestGovPrecompile(t *testing.T) {
312312
res, err := msgServer.EVMTransaction(sdk.WrapSDKContext(ctx), req)
313313
if tt.wantErr {
314314
require.NotEmpty(t, res.VmError)
315-
require.Contains(t, string(res.ReturnData), tt.wantErrMsgToContain)
315+
require.Nil(t, res.ReturnData)
316316
} else {
317317
require.Nil(t, err)
318318
require.Empty(t, res.VmError)
@@ -751,7 +751,7 @@ func TestPrecompileExecutor_submitProposal(t *testing.T) {
751751

752752
if tt.wantErr {
753753
require.NotEmpty(t, gotRet.VmError)
754-
require.Equal(t, string(gotRet.ReturnData), tt.wantErrMsg)
754+
require.Nil(t, gotRet.ReturnData)
755755
} else {
756756
require.Empty(t, gotRet.VmError)
757757
require.Nil(t, err)

precompiles/ibc/ibc_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ func TestPrecompile_Run(t *testing.T) {
337337
}
338338
if err != nil {
339339
require.Equal(t, vm.ErrExecutionReverted, err)
340-
require.Equal(t, tt.wantErrMsg, string(gotBz))
340+
require.Nil(t, gotBz)
341341
} else if !reflect.DeepEqual(gotBz, tt.wantBz) {
342342
t.Errorf("Run() gotRet = %v, want %v", gotBz, tt.wantBz)
343343
}
@@ -538,7 +538,7 @@ func TestTransferWithDefaultTimeoutPrecompile_Run(t *testing.T) {
538538
}
539539
if err != nil {
540540
require.Equal(t, vm.ErrExecutionReverted, err)
541-
require.Equal(t, tt.wantErrMsg, string(gotBz))
541+
require.Nil(t, gotBz)
542542
} else if !reflect.DeepEqual(gotBz, tt.wantBz) {
543543
t.Errorf("Run() gotRet = %v, want %v", gotBz, tt.wantBz)
544544
}

precompiles/json/json_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ func TestPrecompileExecutor_extractAsBytesFromArray(t *testing.T) {
281281

282282
if tt.wantErr {
283283
require.Error(t, err)
284-
require.Equal(t, tt.wantErrMsg, string(res))
284+
require.Nil(t, res)
285285
return
286286
} else {
287287
output, err := method.Outputs.Unpack(res)

precompiles/staking/staking_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,7 @@ func TestPrecompile_Run_Delegation(t *testing.T) {
473473
}
474474
if err != nil {
475475
require.Equal(t, vm.ErrExecutionReverted, err)
476-
require.Equal(t, tt.wantErrMsg, string(gotRet))
476+
require.Nil(t, gotRet)
477477
} else if !reflect.DeepEqual(gotRet, tt.wantRet) {
478478
t.Errorf("Run() gotRet = %v, want %v", gotRet, tt.wantRet)
479479
}
@@ -691,7 +691,7 @@ func TestCreateValidator(t *testing.T) {
691691

692692
if tt.wantErr {
693693
require.NotEmpty(t, res.VmError, "Expected error but transaction succeeded")
694-
require.Equal(t, tt.wantErrMsg, string(res.ReturnData), "Expected error: %s", res.VmError)
694+
require.Nil(t, res.ReturnData)
695695
} else {
696696
require.Empty(t, res.VmError, "Unexpected error: %s", res.VmError)
697697
// Additional validation for successful cases
@@ -752,7 +752,7 @@ func TestCreateValidator_UnassociatedAddress(t *testing.T) {
752752
res, err := setup.msgServer.EVMTransaction(sdk.WrapSDKContext(setup.ctx), req)
753753
require.NoError(t, err)
754754
require.NotEmpty(t, res.VmError, "Should fail with unassociated address")
755-
require.Equal(t, "address "+unassociatedEvmAddr.String()+" is not linked", string(res.ReturnData), "Should fail with unassociated address")
755+
require.Nil(t, res.ReturnData)
756756
}
757757

758758
func TestEditValidator_ErorrIfDoesNotExist(t *testing.T) {
@@ -809,7 +809,7 @@ func TestEditValidator_ErorrIfDoesNotExist(t *testing.T) {
809809
require.NoError(t, err)
810810
// Should fail because validator doesn't exist
811811
require.NotEmpty(t, res.VmError, "Should fail because validator doesn't exist")
812-
require.Contains(t, string(res.ReturnData), "validator does not exist")
812+
require.Nil(t, res.ReturnData)
813813
}
814814

815815
func TestEditValidator(t *testing.T) {

precompiles/wasmd/wasmd_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ func TestExecute(t *testing.T) {
156156
statedb.WithCtx(statedb.Ctx().WithIsEVM(false))
157157
testApp.EvmKeeper.SetCode(statedb.Ctx(), mockEVMAddr, []byte{1, 2, 3})
158158
res, _, err := p.RunAndCalculateGas(&evm, mockEVMAddr, mockEVMAddr, append(p.GetExecutor().(*wasmd.PrecompileExecutor).ExecuteID, args...), suppliedGas, big.NewInt(1000_000_000_000_000), nil, false, false)
159-
require.Equal(t, "sei does not support CW->EVM->CW call pattern", string(res))
159+
require.Nil(t, res)
160160
require.Equal(t, vm.ErrExecutionReverted, err)
161161
statedb.WithCtx(statedb.Ctx().WithIsEVM(true))
162162
res, g, err := p.RunAndCalculateGas(&evm, mockEVMAddr, mockEVMAddr, append(p.GetExecutor().(*wasmd.PrecompileExecutor).ExecuteID, args...), suppliedGas, big.NewInt(1000_000_000_000_000), nil, false, false)

0 commit comments

Comments
 (0)