Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions stateupgrade/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import (
type StateDB interface {
SetState(common.Address, common.Hash, common.Hash, ...stateconf.StateDBStateOption)
SetCode(common.Address, []byte)

GetBalance(common.Address) *uint256.Int
AddBalance(common.Address, *uint256.Int)
SubBalance(common.Address, *uint256.Int)

GetNonce(common.Address) uint64
SetNonce(common.Address, uint64)
Expand Down
22 changes: 20 additions & 2 deletions stateupgrade/state_upgrade.go
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's ugly to have this in this package and then have the definition in config/extras package. I'm working on a refactor to move them to here https://github.com/ava-labs/subnet-evm/pull/1811/files (this is not a blocker for this one)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it! If you'd like it to be a blocker, this PR can certainly wait until after that one (and perhaps that makes more sense).

Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package stateupgrade

import (
"fmt"
"math/big"

"github.com/ava-labs/libevm/common"
Expand All @@ -30,9 +31,26 @@ func upgradeAccount(account common.Address, upgrade extras.StateUpgradeAccount,
state.CreateAccount(account)
}

// Balance change detected - update the balance of the account
if upgrade.BalanceChange != nil {
balanceChange, _ := uint256.FromBig((*big.Int)(upgrade.BalanceChange))
state.AddBalance(account, balanceChange)
// BalanceChange is a HexOrDecimal256, which is just a typed big.Int
bigChange := (*big.Int)(upgrade.BalanceChange)
switch bigChange.Sign() {
case 1: // Positive
// ParseBig256 already enforced 256-bit limit during JSON parsing, so no overflow check needed
balanceChange, _ := uint256.FromBig(bigChange)
state.AddBalance(account, balanceChange)
case -1: // Negative
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not allow negative values. Can we check this in the verification and return an error if it's negative?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why shouldn't we allow negative values? I assume there are users who would like to subtract from some account's balance.

absChange := new(big.Int).Abs(bigChange)
balanceChange, _ := uint256.FromBig(absChange)
currentBalance := state.GetBalance(account)
if currentBalance.Cmp(balanceChange) < 0 {
return fmt.Errorf("insufficient balance for subtraction: account %s has %s but trying to subtract %s",
account.Hex(), currentBalance.ToBig().String(), balanceChange.ToBig().String())
}
state.SubBalance(account, balanceChange)
}
// If zero (Sign() == 0), do nothing
}
if len(upgrade.Code) != 0 {
// if the nonce is 0, set the nonce to 1 as we would when deploying a contract at
Expand Down
216 changes: 216 additions & 0 deletions stateupgrade/state_upgrade_test.go
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for adding tests!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(we should probably add these tests to verification)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you mean by we should add these tests to verification?

Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package stateupgrade

import (
"math/big"
"testing"

"github.com/ava-labs/libevm/common"
"github.com/ava-labs/libevm/common/math"
"github.com/ava-labs/libevm/core/rawdb"
"github.com/ava-labs/libevm/core/state"
"github.com/ava-labs/libevm/core/types"
"github.com/holiman/uint256"
"github.com/stretchr/testify/require"

"github.com/ava-labs/subnet-evm/core/extstate"
"github.com/ava-labs/subnet-evm/params/extras"
)

func TestUpgradeAccount_BalanceChanges(t *testing.T) {
testAddr := common.HexToAddress("0x1234567890123456789012345678901234567890")

tests := []struct {
name string
initialBalance *uint256.Int
balanceChange *math.HexOrDecimal256
accountExists bool
wantBalance *uint256.Int
wantError string
}{
{
name: "positive balance change on existing account",
initialBalance: uint256.NewInt(100),
balanceChange: hexOrDecimal256FromInt64(50),
accountExists: true,
wantBalance: uint256.NewInt(150),
},
{
name: "negative balance change with sufficient funds",
initialBalance: uint256.NewInt(100),
balanceChange: hexOrDecimal256FromInt64(-30),
accountExists: true,
wantBalance: uint256.NewInt(70),
},
{
name: "negative balance change with insufficient funds",
initialBalance: uint256.NewInt(50),
balanceChange: hexOrDecimal256FromInt64(-100),
accountExists: true,
wantBalance: uint256.NewInt(50), // unchanged
wantError: "insufficient balance for subtraction",
},
{
name: "zero balance change",
initialBalance: uint256.NewInt(100),
balanceChange: hexOrDecimal256FromInt64(0),
accountExists: true,
wantBalance: uint256.NewInt(100),
},
{
name: "nil balance change",
initialBalance: uint256.NewInt(100),
balanceChange: nil,
accountExists: true,
wantBalance: uint256.NewInt(100),
},
{
name: "new account with positive balance",
initialBalance: uint256.NewInt(0),
balanceChange: hexOrDecimal256FromInt64(1000),
accountExists: false,
wantBalance: uint256.NewInt(1000),
},
{
name: "new account with negative balance",
initialBalance: uint256.NewInt(0),
balanceChange: hexOrDecimal256FromInt64(-100),
accountExists: false,
wantBalance: uint256.NewInt(0), // unchanged
wantError: "insufficient balance for subtraction",
},
{
name: "exact balance subtraction",
initialBalance: uint256.NewInt(100),
balanceChange: hexOrDecimal256FromInt64(-100),
accountExists: true,
wantBalance: uint256.NewInt(0),
},
{
name: "off-by-one underflow",
initialBalance: uint256.NewInt(100),
balanceChange: hexOrDecimal256FromInt64(-101),
accountExists: true,
wantBalance: uint256.NewInt(100), // unchanged
wantError: "insufficient balance for subtraction",
},
{
name: "large positive balance change",
initialBalance: uint256.MustFromBig(new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 255), big.NewInt(1000))),
balanceChange: hexOrDecimal256FromInt64(500),
accountExists: true,
wantBalance: uint256.MustFromBig(new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 255), big.NewInt(500))),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create a fresh state database for each test
statedb := createTestStateDB(t)

// Set up initial account state
if tt.accountExists {
setAccountBalance(t, statedb, testAddr, tt.initialBalance)
}

upgrade := extras.StateUpgradeAccount{
BalanceChange: tt.balanceChange,
}
err := upgradeAccount(testAddr, upgrade, statedb, false)

// Check error expectations
if tt.wantError != "" {
require.Error(t, err)
require.Contains(t, err.Error(), tt.wantError)
require.Contains(t, err.Error(), testAddr.Hex())
} else {
require.NoError(t, err)
}

// Check final balance
actualBalance := statedb.GetBalance(testAddr)
require.Equal(t, tt.wantBalance, actualBalance, "final balance mismatch")

// Verify account was created if it didn't exist
require.True(t, statedb.Exist(testAddr), "account should exist after upgrade")
})
}
}

func TestUpgradeAccount_ErrorMessageFormat(t *testing.T) {
statedb := createTestStateDB(t)
addr := common.HexToAddress("0xabcdef1234567890abcdef1234567890abcdef12")

// Set initial balance to 50
setAccountBalance(t, statedb, addr, uint256.NewInt(50))

upgrade := extras.StateUpgradeAccount{
BalanceChange: hexOrDecimal256FromInt64(-75),
}

err := upgradeAccount(addr, upgrade, statedb, false)
require.Error(t, err)

// Check specific error message components
errorMsg := err.Error()
require.Contains(t, errorMsg, "insufficient balance for subtraction")
require.Contains(t, errorMsg, addr.Hex()) // EIP-55 checksum format
require.Contains(t, errorMsg, "has 50")
require.Contains(t, errorMsg, "trying to subtract 75")
}

func TestUpgradeAccount_CompleteUpgrade(t *testing.T) {
statedb := createTestStateDB(t)
addr := common.HexToAddress("0x1234567890123456789012345678901234567890")

// Create a complete state upgrade with balance change, code, and storage
code := []byte{0xde, 0xad, 0xbe, 0xef}
storageKey := common.HexToHash("0x1234")
storageValue := common.HexToHash("0x5678")

upgrade := extras.StateUpgradeAccount{
BalanceChange: hexOrDecimal256FromInt64(1000),
Code: code,
Storage: map[common.Hash]common.Hash{storageKey: storageValue},
}
require.NoError(t, upgradeAccount(addr, upgrade, statedb, true)) // Test with EIP158 = true

// Verify all changes were applied
require.Equal(t, uint256.NewInt(1000), statedb.GetBalance(addr))
require.True(t, statedb.Exist(addr))
require.Equal(t, uint64(1), statedb.GetNonce(addr)) // Should be set to 1 due to EIP158 and code deployment

// Cast to access code and storage (these aren't in our StateDB interface)
extStateDB := statedb.(*extstate.StateDB)
require.Equal(t, code, extStateDB.GetCode(addr))
require.Equal(t, storageValue, extStateDB.GetState(addr, storageKey))
}

// createTestStateDB creates a real StateDB instance for testing
func createTestStateDB(t *testing.T) StateDB {
t.Helper()

db := rawdb.NewMemoryDatabase()
statedb, err := state.New(types.EmptyRootHash, state.NewDatabase(db), nil)
require.NoError(t, err)

// Wrap it with extstate for predicate support (matching the interface)
return extstate.New(statedb)
}

// setAccountBalance is a helper to set an account's initial balance for testing
func setAccountBalance(t *testing.T, statedb StateDB, addr common.Address, balance *uint256.Int) {
t.Helper()

// Cast to extstate.StateDB to access the underlying state
extStateDB := statedb.(*extstate.StateDB)
extStateDB.CreateAccount(addr)
extStateDB.SetBalance(addr, balance)
}

// Helper function to create HexOrDecimal256 from int64
func hexOrDecimal256FromInt64(i int64) *math.HexOrDecimal256 {
return (*math.HexOrDecimal256)(big.NewInt(i))
}