Skip to content

Commit d3bb8a5

Browse files
JonathanOppenheimerceyonurARR4NStephenButtolph
authored
sync: coreth PR #1149: add precompile reversion logic for Granite activation (#1761)
Signed-off-by: Jonathan Oppenheimer <[email protected]> Signed-off-by: Jonathan Oppenheimer <[email protected]> Co-authored-by: Ceyhun Onur <[email protected]> Co-authored-by: Arran Schlosberg <[email protected]> Co-authored-by: Stephen Buttolph <[email protected]>
1 parent 34f9192 commit d3bb8a5

File tree

2 files changed

+196
-5
lines changed

2 files changed

+196
-5
lines changed

params/hooks_libevm.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import (
2929

3030
// invalidateDelegateTime is the Unix timestamp for August 2nd, 2025, midnight Eastern Time
3131
// (August 2nd, 2025, 04:00 UTC)
32-
const invalidateDelegateUnix = 1754107200
32+
const InvalidateDelegateUnix = 1754107200
3333

3434
// P256VerifyAddress is the address of the p256 signature verification precompile
3535
var P256VerifyAddress = common.BytesToAddress([]byte{0x1, 0x00})
@@ -115,10 +115,15 @@ func makePrecompile(contract contract.StatefulPrecompiledContract) libevm.Precom
115115
},
116116
}
117117

118-
callType := env.IncomingCallType()
119-
isDisallowedCallType := callType == vm.DelegateCall || callType == vm.CallCode
120-
if env.BlockTime() >= invalidateDelegateUnix && isDisallowedCallType {
121-
env.InvalidateExecution(fmt.Errorf("precompile cannot be called with %s", callType))
118+
rules := GetRulesExtra(env.Rules()).AvalancheRules
119+
switch call := env.IncomingCallType(); {
120+
case call != vm.DelegateCall && call != vm.CallCode: // Others always allowed
121+
case rules.IsGranite:
122+
return nil, 0, vm.ErrExecutionReverted
123+
case env.BlockTime() >= InvalidateDelegateUnix:
124+
env.InvalidateExecution(fmt.Errorf("precompile cannot be called with %s", call))
125+
default:
126+
// Otherwise, we allow the precompile to be called
122127
}
123128

124129
return contract.Run(accessibleState, env.Addresses().EVMSemantic.Caller, env.Addresses().EVMSemantic.Self, input, suppliedGas, env.ReadOnly())

plugin/evm/vm_test.go

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package evm
55

66
import (
77
"context"
8+
"crypto/ecdsa"
89
"encoding/json"
910
"errors"
1011
"fmt"
@@ -31,12 +32,14 @@ import (
3132
"github.com/ava-labs/avalanchego/utils/crypto/secp256k1"
3233
"github.com/ava-labs/avalanchego/utils/set"
3334
"github.com/ava-labs/avalanchego/vms/components/chain"
35+
"github.com/ava-labs/avalanchego/vms/evm/acp176"
3436
"github.com/ava-labs/avalanchego/vms/evm/predicate"
3537
"github.com/ava-labs/avalanchego/vms/platformvm/warp/payload"
3638
"github.com/ava-labs/libevm/common"
3739
"github.com/ava-labs/libevm/common/math"
3840
"github.com/ava-labs/libevm/core/rawdb"
3941
"github.com/ava-labs/libevm/core/types"
42+
"github.com/ava-labs/libevm/crypto"
4043
"github.com/ava-labs/libevm/log"
4144
"github.com/ava-labs/libevm/trie"
4245
"github.com/stretchr/testify/assert"
@@ -71,6 +74,8 @@ import (
7174
warpcontract "github.com/ava-labs/subnet-evm/precompile/contracts/warp"
7275
)
7376

77+
const delegateCallPrecompileCode = "6080604052348015600e575f5ffd5b506106608061001c5f395ff3fe608060405234801561000f575f5ffd5b506004361061003f575f3560e01c80638b336b5e14610043578063b771b3bc14610061578063e4246eec1461007f575b5f5ffd5b61004b61009d565b604051610058919061029e565b60405180910390f35b610069610256565b6040516100769190610331565b60405180910390f35b61008761026e565b604051610094919061036a565b60405180910390f35b5f5f6040516020016100ae906103dd565b60405160208183030381529060405290505f63ee5b48eb60e01b826040516024016100d9919061046b565b604051602081830303815290604052907bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff838183161783525050505090505f5f73020000000000000000000000000000000000000573ffffffffffffffffffffffffffffffffffffffff168360405161017391906104c5565b5f60405180830381855af49150503d805f81146101ab576040519150601f19603f3d011682016040523d82523d5f602084013e6101b0565b606091505b5091509150816101f5576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101ec9061054b565b60405180910390fd5b808060200190518101906102099190610597565b94505f5f1b850361024f576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016102469061060c565b60405180910390fd5b5050505090565b73020000000000000000000000000000000000000581565b73020000000000000000000000000000000000000581565b5f819050919050565b61029881610286565b82525050565b5f6020820190506102b15f83018461028f565b92915050565b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f819050919050565b5f6102f96102f46102ef846102b7565b6102d6565b6102b7565b9050919050565b5f61030a826102df565b9050919050565b5f61031b82610300565b9050919050565b61032b81610311565b82525050565b5f6020820190506103445f830184610322565b92915050565b5f610354826102b7565b9050919050565b6103648161034a565b82525050565b5f60208201905061037d5f83018461035b565b92915050565b5f82825260208201905092915050565b7f68656c6c6f0000000000000000000000000000000000000000000000000000005f82015250565b5f6103c7600583610383565b91506103d282610393565b602082019050919050565b5f6020820190508181035f8301526103f4816103bb565b9050919050565b5f81519050919050565b5f82825260208201905092915050565b8281835e5f83830152505050565b5f601f19601f8301169050919050565b5f61043d826103fb565b6104478185610405565b9350610457818560208601610415565b61046081610423565b840191505092915050565b5f6020820190508181035f8301526104838184610433565b905092915050565b5f81905092915050565b5f61049f826103fb565b6104a9818561048b565b93506104b9818560208601610415565b80840191505092915050565b5f6104d08284610495565b915081905092915050565b7f44656c65676174652063616c6c20746f2073656e64576172704d6573736167655f8201527f206661696c656400000000000000000000000000000000000000000000000000602082015250565b5f610535602783610383565b9150610540826104db565b604082019050919050565b5f6020820190508181035f83015261056281610529565b9050919050565b5f5ffd5b61057681610286565b8114610580575f5ffd5b50565b5f815190506105918161056d565b92915050565b5f602082840312156105ac576105ab610569565b5b5f6105b984828501610583565b91505092915050565b7f4661696c656420746f2073656e642077617270206d65737361676500000000005f82015250565b5f6105f6601b83610383565b9150610601826105c2565b602082019050919050565b5f6020820190508181035f830152610623816105ea565b905091905056fea2646970667358221220192acba01cff6d70ce187c63c7ccac116d811f6c35e316fde721f14929ced12564736f6c634300081e0033"
78+
7479
func TestMain(m *testing.M) {
7580
RegisterAllLibEVMExtras()
7681
os.Exit(m.Run())
@@ -3972,3 +3977,184 @@ func TestBlockGasValidation(t *testing.T) {
39723977
})
39733978
}
39743979
}
3980+
3981+
// newSignedLegacyTx builds a legacy transaction and signs it using the
3982+
// LatestSigner derived from the provided chain config.
3983+
func newSignedLegacyTx(
3984+
t *testing.T,
3985+
cfg *params.ChainConfig,
3986+
key *ecdsa.PrivateKey,
3987+
nonce uint64,
3988+
to *common.Address,
3989+
value *big.Int,
3990+
gas uint64,
3991+
gasPrice *big.Int,
3992+
data []byte,
3993+
) *types.Transaction {
3994+
t.Helper()
3995+
3996+
tx := types.NewTx(&types.LegacyTx{
3997+
Nonce: nonce,
3998+
To: to,
3999+
Value: value,
4000+
Gas: gas,
4001+
GasPrice: gasPrice,
4002+
Data: data,
4003+
})
4004+
signedTx, err := types.SignTx(tx, types.LatestSigner(cfg), key)
4005+
require.NoError(t, err)
4006+
return signedTx
4007+
}
4008+
4009+
// deployContract deploys the provided EVM bytecode using a prefunded test account
4010+
// and returns the created contract address. It is reusable for any contract code.
4011+
func deployContract(ctx context.Context, t *testing.T, vm *VM, gasPrice *big.Int, code []byte) common.Address {
4012+
callerAddr := testEthAddrs[0]
4013+
callerKey := testKeys[0]
4014+
4015+
nonce := vm.txPool.Nonce(callerAddr)
4016+
signedTx := newSignedLegacyTx(t, vm.chainConfig, callerKey.ToECDSA(), nonce, nil, big.NewInt(0), 1000000, gasPrice, code)
4017+
4018+
for _, err := range vm.txPool.AddRemotesSync([]*types.Transaction{signedTx}) {
4019+
require.NoError(t, err)
4020+
}
4021+
4022+
blk, err := vm.BuildBlock(ctx)
4023+
require.NoError(t, err)
4024+
require.NoError(t, blk.Verify(ctx))
4025+
require.NoError(t, vm.SetPreference(ctx, blk.ID()))
4026+
require.NoError(t, blk.Accept(ctx))
4027+
4028+
ethBlock := blk.(*chain.BlockWrapper).Block.(*wrappedBlock).ethBlock
4029+
receipts := vm.blockChain.GetReceiptsByHash(ethBlock.Hash())
4030+
require.Len(t, receipts, len(ethBlock.Transactions()))
4031+
4032+
found := false
4033+
for i, btx := range ethBlock.Transactions() {
4034+
if btx.Hash() == signedTx.Hash() {
4035+
found = true
4036+
require.Equal(t, types.ReceiptStatusSuccessful, receipts[i].Status)
4037+
break
4038+
}
4039+
}
4040+
require.True(t, found, "deployContract: expected deploy tx %s to be included in block %s (caller=%s, nonce=%d)",
4041+
signedTx.Hash().Hex(),
4042+
ethBlock.Hash().Hex(),
4043+
callerAddr.Hex(),
4044+
nonce,
4045+
)
4046+
4047+
return crypto.CreateAddress(callerAddr, nonce)
4048+
}
4049+
4050+
func TestDelegatePrecompile_BehaviorAcrossUpgrades(t *testing.T) {
4051+
ctx := context.Background()
4052+
tests := []struct {
4053+
name string
4054+
fork upgradetest.Fork
4055+
deployGasPrice *big.Int
4056+
txGasPrice *big.Int
4057+
preDeployTime int64
4058+
setTime int64
4059+
refillCapacityFortuna bool
4060+
wantIncluded bool
4061+
wantReceiptStatus uint64
4062+
}{
4063+
{
4064+
name: "granite_should_revert",
4065+
fork: upgradetest.Granite,
4066+
deployGasPrice: big.NewInt(testMinGasPrice),
4067+
txGasPrice: big.NewInt(testMinGasPrice),
4068+
// Time is irrelevant as only the fork dictates the logic
4069+
refillCapacityFortuna: false,
4070+
wantIncluded: true,
4071+
wantReceiptStatus: types.ReceiptStatusFailed,
4072+
},
4073+
{
4074+
name: "fortuna_post_cutoff_should_invalidate",
4075+
fork: upgradetest.Fortuna,
4076+
deployGasPrice: big.NewInt(testMinGasPrice),
4077+
txGasPrice: big.NewInt(testMinGasPrice),
4078+
setTime: params.InvalidateDelegateUnix + 1,
4079+
refillCapacityFortuna: true,
4080+
wantIncluded: false,
4081+
},
4082+
{
4083+
name: "fortuna_pre_cutoff_should_succeed",
4084+
fork: upgradetest.Fortuna,
4085+
deployGasPrice: big.NewInt(testMinGasPrice),
4086+
txGasPrice: big.NewInt(testMinGasPrice),
4087+
preDeployTime: params.InvalidateDelegateUnix - acp176.TimeToFillCapacity - 1,
4088+
refillCapacityFortuna: true,
4089+
wantIncluded: true,
4090+
wantReceiptStatus: types.ReceiptStatusSuccessful,
4091+
},
4092+
}
4093+
4094+
for _, tt := range tests {
4095+
t.Run(tt.name, func(t *testing.T) {
4096+
genesis := &core.Genesis{}
4097+
require.NoError(t, genesis.UnmarshalJSON([]byte(toGenesisJSON(paramstest.ForkToChainConfig[tt.fork]))))
4098+
params.GetExtra(genesis.Config).GenesisPrecompiles = extras.Precompiles{
4099+
warpcontract.ConfigKey: warpcontract.NewDefaultConfig(utils.TimeToNewUint64(upgrade.InitiallyActiveTime)),
4100+
}
4101+
genesisJSON, err := genesis.MarshalJSON()
4102+
require.NoError(t, err)
4103+
4104+
vm := newVM(t, testVMConfig{
4105+
genesisJSON: string(genesisJSON),
4106+
fork: &tt.fork,
4107+
}).vm
4108+
defer vm.Shutdown(ctx)
4109+
4110+
if tt.preDeployTime != 0 {
4111+
vm.clock.Set(time.Unix(tt.preDeployTime, 0))
4112+
}
4113+
4114+
contractAddr := deployContract(ctx, t, vm, tt.deployGasPrice, common.FromHex(delegateCallPrecompileCode))
4115+
4116+
if tt.setTime != 0 {
4117+
vm.clock.Set(time.Unix(tt.setTime, 0))
4118+
}
4119+
4120+
if tt.refillCapacityFortuna {
4121+
// Ensure gas capacity is refilled relative to the parent block's timestamp
4122+
parent := vm.blockChain.CurrentBlock()
4123+
parentTime := time.Unix(int64(parent.Time), 0)
4124+
minRefillTime := parentTime.Add(acp176.TimeToFillCapacity * time.Second)
4125+
if vm.clock.Time().Before(minRefillTime) {
4126+
vm.clock.Set(minRefillTime)
4127+
}
4128+
}
4129+
4130+
data := crypto.Keccak256([]byte("delegateSendHello()"))[:4]
4131+
nonce := vm.txPool.Nonce(testEthAddrs[0])
4132+
signedTx := newSignedLegacyTx(t, vm.chainConfig, testKeys[0].ToECDSA(), nonce, &contractAddr, big.NewInt(0), 100000, tt.txGasPrice, data)
4133+
for _, err := range vm.txPool.AddRemotesSync([]*types.Transaction{signedTx}) {
4134+
require.NoError(t, err)
4135+
}
4136+
4137+
blk, err := vm.BuildBlock(ctx)
4138+
4139+
if !tt.wantIncluded {
4140+
// On subnet-evm, InvalidateExecution causes the transaction to be excluded from the block.
4141+
// BuildBlock will create a block but it will fail verification because it's empty
4142+
// and subnet-evm doesn't allow empty blocks.
4143+
require.Error(t, err, "BuildBlock should fail because it would create an empty block")
4144+
require.ErrorContains(t, err, "empty block", "Should fail with empty block error")
4145+
return
4146+
}
4147+
4148+
require.NoError(t, err)
4149+
require.NoError(t, blk.Verify(ctx))
4150+
require.NoError(t, vm.SetPreference(ctx, blk.ID()))
4151+
require.NoError(t, blk.Accept(ctx))
4152+
4153+
ethBlock := blk.(*chain.BlockWrapper).Block.(*wrappedBlock).ethBlock
4154+
require.Len(t, ethBlock.Transactions(), 1)
4155+
receipts := vm.blockChain.GetReceiptsByHash(ethBlock.Hash())
4156+
require.Len(t, receipts, 1)
4157+
require.Equal(t, tt.wantReceiptStatus, receipts[0].Status)
4158+
})
4159+
}
4160+
}

0 commit comments

Comments
 (0)