diff --git a/core/state/database_history.go b/core/state/database_history.go
index fbf4ab5f9c70..b4a9de429884 100644
--- a/core/state/database_history.go
+++ b/core/state/database_history.go
@@ -55,19 +55,7 @@ func (r *historicStateReader) Account(addr common.Address) (*types.StateAccount,
if account == nil {
return nil, nil
}
- acct := &types.StateAccount{
- Nonce: account.Nonce,
- Balance: account.Balance,
- CodeHash: account.CodeHash,
- Root: common.BytesToHash(account.Root),
- }
- if len(acct.CodeHash) == 0 {
- acct.CodeHash = types.EmptyCodeHash.Bytes()
- }
- if acct.Root == (common.Hash{}) {
- acct.Root = types.EmptyRootHash
- }
- return acct, nil
+ return slimAccountToStateAccount(account)
}
// Storage implements StateReader, retrieving the storage slot specified by the
diff --git a/core/state/journal.go b/core/state/journal.go
index a79bd7331a06..e350da5bafb6 100644
--- a/core/state/journal.go
+++ b/core/state/journal.go
@@ -24,6 +24,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
+ qkccommon "github.com/ethereum/go-ethereum/qkc/common"
"github.com/holiman/uint256"
)
@@ -229,6 +230,14 @@ func (j *journal) accessListAddSlot(addr common.Address, slot common.Hash) {
})
}
+func (j *journal) mntBalanceChange(addr common.Address, prev *qkccommon.TokenBalances) {
+ var snap *qkccommon.TokenBalances
+ if prev != nil {
+ snap = prev.Copy()
+ }
+ j.append(mntBalanceChange{addr: addr, prev: snap})
+}
+
type (
// Changes to the account trie.
createObjectChange struct {
@@ -289,6 +298,12 @@ type (
account common.Address
key, prevalue common.Hash
}
+
+ // mntBalanceChange records a snapshot of MNT balances for revert support.
+ mntBalanceChange struct {
+ addr common.Address
+ prev *qkccommon.TokenBalances
+ }
)
func (ch createObjectChange) revert(s *StateDB) {
@@ -352,6 +367,8 @@ func (ch touchChange) copy() journalEntry {
}
func (ch balanceChange) revert(s *StateDB) {
+ // pyquarkchain restores the previous value by writing it back into the
+ // balance map. Even when the previous value is zero, the token entry remains.
s.getStateObject(ch.account).setBalance(ch.prev)
}
@@ -500,3 +517,29 @@ func (ch accessListAddSlotChange) copy() journalEntry {
slot: ch.slot,
}
}
+
+func (ch mntBalanceChange) revert(s *StateDB) {
+ obj := s.getStateObject(ch.addr)
+ if obj != nil {
+ if ch.prev == nil {
+ obj.data.MntBalances = nil
+ } else {
+ obj.data.MntBalances = ch.prev.Copy()
+ }
+ }
+}
+
+func (ch mntBalanceChange) dirtied() (common.Address, bool) {
+ return ch.addr, true
+}
+
+func (ch mntBalanceChange) copy() journalEntry {
+ var prev *qkccommon.TokenBalances
+ if ch.prev != nil {
+ prev = ch.prev.Copy()
+ }
+ return mntBalanceChange{
+ addr: ch.addr,
+ prev: prev,
+ }
+}
diff --git a/core/state/mnt_test.go b/core/state/mnt_test.go
new file mode 100644
index 000000000000..23a03232121c
--- /dev/null
+++ b/core/state/mnt_test.go
@@ -0,0 +1,306 @@
+// Copyright 2024 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package state
+
+import (
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/rawdb"
+ "github.com/ethereum/go-ethereum/core/tracing"
+ "github.com/ethereum/go-ethereum/core/types"
+ qkccommon "github.com/ethereum/go-ethereum/qkc/common"
+ "github.com/ethereum/go-ethereum/triedb"
+ "github.com/holiman/uint256"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func newMntTestStateDB(t *testing.T) *StateDB {
+ t.Helper()
+ db := triedb.NewDatabase(rawdb.NewMemoryDatabase(), nil)
+ s, err := New(common.Hash{}, NewDatabase(db, nil))
+ require.NoError(t, err)
+ return s
+}
+
+func TestMntBalanceBasic(t *testing.T) {
+ s := newMntTestStateDB(t)
+ addr := common.HexToAddress("0x1234")
+ s.CreateAccount(addr)
+
+ const tokenID = uint64(100)
+ s.AddMntBalance(addr, uint256.NewInt(500), tokenID)
+ assert.Equal(t, uint256.NewInt(500), s.GetMntBalance(addr, tokenID))
+
+ s.SubMntBalance(addr, uint256.NewInt(200), tokenID)
+ assert.Equal(t, uint256.NewInt(300), s.GetMntBalance(addr, tokenID))
+}
+
+func TestSubMntBalanceRejectsUnderflow(t *testing.T) {
+ s := newMntTestStateDB(t)
+ addr := common.HexToAddress("0x1235")
+
+ s.SubMntBalance(addr, uint256.NewInt(1), 100)
+
+ assert.False(t, s.Exist(addr), "underflow must not create an account")
+ assert.True(t, s.GetMntBalance(addr, 100).IsZero())
+}
+
+func TestAddMntBalanceRejectsOverflow(t *testing.T) {
+ s := newMntTestStateDB(t)
+ addr := common.HexToAddress("0x1236")
+ max := new(uint256.Int).Not(new(uint256.Int))
+ s.SetMntBalance(addr, max, 100)
+ dirtyCount := s.journal.dirties[addr]
+
+ s.AddMntBalance(addr, uint256.NewInt(1), 100)
+
+ assert.Equal(t, max, s.GetMntBalance(addr, 100))
+ assert.Equal(t, dirtyCount, s.journal.dirties[addr], "overflow must not add a dirty journal entry")
+}
+
+func TestMntRejectsQKCTokenID(t *testing.T) {
+ s := newMntTestStateDB(t)
+ addr := common.HexToAddress("0x5678")
+
+ // SetMntBalance with QKC tokenID (35760) must be a no-op
+ s.SetMntBalance(addr, uint256.NewInt(999), qkccommon.DefaultTokenID)
+ assert.True(t, s.GetMntBalance(addr, qkccommon.DefaultTokenID).IsZero())
+ assert.True(t, s.GetBalance(addr).IsZero()) // QKC balance unchanged
+ assert.False(t, s.Exist(addr), "rejected update must not create an account")
+}
+
+func TestMntRejectsTokenAboveListLimit(t *testing.T) {
+ s := newMntTestStateDB(t)
+ addr := common.HexToAddress("0x5679")
+ s.CreateAccount(addr)
+
+ for tokenID := uint64(1); tokenID <= qkccommon.TokenTrieThreshold; tokenID++ {
+ s.SetMntBalance(addr, uint256.NewInt(tokenID), tokenID)
+ }
+ dirtyCount := s.journal.dirties[addr]
+ s.SetMntBalance(addr, uint256.NewInt(17), qkccommon.TokenTrieThreshold+1)
+
+ assert.True(t, s.GetMntBalance(addr, qkccommon.TokenTrieThreshold+1).IsZero())
+ assert.Equal(t, dirtyCount, s.journal.dirties[addr], "rejected update must not add a dirty journal entry")
+ _, err := s.Commit(0, false, false)
+ require.NoError(t, err)
+}
+
+func TestMntTokenLimitIncludesQKC(t *testing.T) {
+ s := newMntTestStateDB(t)
+ addr := common.HexToAddress("0x5680")
+ s.CreateAccount(addr)
+ s.SetBalance(addr, uint256.NewInt(1), tracing.BalanceChangeUnspecified)
+
+ for tokenID := uint64(1); tokenID < qkccommon.TokenTrieThreshold; tokenID++ {
+ s.SetMntBalance(addr, uint256.NewInt(tokenID), tokenID)
+ }
+ s.SetMntBalance(addr, uint256.NewInt(16), qkccommon.TokenTrieThreshold)
+
+ assert.True(t, s.GetMntBalance(addr, qkccommon.TokenTrieThreshold).IsZero())
+ _, err := s.Commit(0, false, false)
+ require.NoError(t, err)
+}
+
+func TestQKCBalanceRejectsTokenAboveListLimit(t *testing.T) {
+ s := newMntTestStateDB(t)
+ addr := common.HexToAddress("0x5683")
+ s.CreateAccount(addr)
+
+ for tokenID := uint64(1); tokenID <= qkccommon.TokenTrieThreshold; tokenID++ {
+ s.SetMntBalance(addr, uint256.NewInt(tokenID), tokenID)
+ }
+ dirtyCount := s.journal.dirties[addr]
+ s.SetBalance(addr, uint256.NewInt(1), tracing.BalanceChangeUnspecified)
+
+ assert.True(t, s.GetBalance(addr).IsZero())
+ assert.Equal(t, dirtyCount, s.journal.dirties[addr], "rejected update must not add a dirty journal entry")
+ _, err := s.Commit(0, false, false)
+ require.NoError(t, err)
+}
+
+func TestMntTokenLimitIgnoresZeroEntries(t *testing.T) {
+ s := newMntTestStateDB(t)
+ addr := common.HexToAddress("0x5681")
+ s.CreateAccount(addr)
+
+ for tokenID := uint64(1); tokenID <= qkccommon.TokenTrieThreshold; tokenID++ {
+ s.SetMntBalance(addr, uint256.NewInt(tokenID), tokenID)
+ }
+ s.SetMntBalance(addr, new(uint256.Int), 1)
+ s.SetMntBalance(addr, uint256.NewInt(17), qkccommon.TokenTrieThreshold+1)
+
+ assert.Equal(t, uint256.NewInt(17), s.GetMntBalance(addr, qkccommon.TokenTrieThreshold+1))
+ _, err := s.Commit(0, false, false)
+ require.NoError(t, err)
+}
+
+func TestMntTokenLimitAllowsExistingTokenUpdates(t *testing.T) {
+ s := newMntTestStateDB(t)
+ addr := common.HexToAddress("0x5682")
+ s.CreateAccount(addr)
+
+ for tokenID := uint64(1); tokenID <= qkccommon.TokenTrieThreshold; tokenID++ {
+ s.SetMntBalance(addr, uint256.NewInt(tokenID), tokenID)
+ }
+ s.SetMntBalance(addr, uint256.NewInt(1000), 1)
+ s.SetMntBalance(addr, new(uint256.Int), 2)
+
+ assert.Equal(t, uint256.NewInt(1000), s.GetMntBalance(addr, 1))
+ assert.True(t, s.GetMntBalance(addr, 2).IsZero())
+ _, err := s.Commit(0, false, false)
+ require.NoError(t, err)
+}
+
+func TestMntJournalRevert(t *testing.T) {
+ s := newMntTestStateDB(t)
+ addr := common.HexToAddress("0xABCD")
+ s.CreateAccount(addr)
+
+ const tokenID = uint64(200)
+ snap := s.Snapshot()
+ s.AddMntBalance(addr, uint256.NewInt(1000), tokenID)
+ assert.Equal(t, uint256.NewInt(1000), s.GetMntBalance(addr, tokenID))
+
+ s.RevertToSnapshot(snap)
+ assert.True(t, s.GetMntBalance(addr, tokenID).IsZero(), "revert should clear MNT balance")
+}
+
+func TestEncodeDecodeRoundTrip(t *testing.T) {
+ s := newMntTestStateDB(t)
+ addr := common.HexToAddress("0x2222")
+ s.CreateAccount(addr)
+ s.AddBalance(addr, uint256.NewInt(1e18), tracing.BalanceChangeUnspecified) // QKC balance
+ s.AddMntBalance(addr, uint256.NewInt(500), uint64(100)) // MNT token
+
+ root, err := s.Commit(0, false, false)
+ require.NoError(t, err)
+
+ // Re-open state at the committed root and verify balances survive
+ s2, err := New(root, s.Database())
+ require.NoError(t, err)
+
+ assert.Equal(t, uint256.NewInt(1e18), s2.GetBalance(addr), "QKC balance")
+ assert.Equal(t, uint256.NewInt(500), s2.GetMntBalance(addr, 100), "MNT balance")
+}
+
+func TestSlimAccountToStateAccountPreservesQKCFields(t *testing.T) {
+ mnt := qkccommon.NewTokenBalancesWithMap(map[uint64]*uint256.Int{
+ 100: uint256.NewInt(500),
+ })
+ mntBal, err := mnt.SerializeToBytes()
+ require.NoError(t, err)
+
+ acct, err := slimAccountToStateAccount(&types.SlimAccount{
+ Balance: uint256.NewInt(1000),
+ FullShardKey: 0x12345678,
+ MntBal: mntBal,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, uint256.NewInt(1000), acct.Balance)
+ assert.Equal(t, uint32(0x12345678), acct.FullShardKey)
+ require.NotNil(t, acct.MntBalances)
+ assert.Equal(t, uint256.NewInt(500), acct.MntBalances.GetTokenBalance(100))
+ assert.Equal(t, types.EmptyRootHash, acct.Root)
+ assert.Equal(t, types.EmptyCodeHash.Bytes(), acct.CodeHash)
+}
+
+// TestEmptyAccountWithMntNotPruned guards the EIP-158 divergence: an account
+// with nonce==0 / QKC==0 / MNT!=0 / no code must NOT be treated as empty, since
+// pyquarkchain's is_blank spans all tokens and keeps it. Pruning it here would
+// diverge the state root. Removing the MNT balance must flip it back to empty.
+func TestEmptyAccountWithMntNotPruned(t *testing.T) {
+ s := newMntTestStateDB(t)
+ addr := common.HexToAddress("0xBEEF")
+ s.CreateAccount(addr)
+
+ const tokenID = uint64(100)
+ s.AddMntBalance(addr, uint256.NewInt(500), tokenID)
+
+ // nonce==0, QKC==0, MNT!=0, no code → not empty.
+ require.True(t, s.GetBalance(addr).IsZero(), "QKC balance must be zero for this case")
+ require.Zero(t, s.GetNonce(addr), "nonce must be zero for this case")
+ assert.False(t, s.Empty(addr), "account with non-zero MNT balance must not be empty")
+
+ // Finalise with deleteEmptyObjects=true must keep the account.
+ s.Finalise(true)
+ assert.True(t, s.Exist(addr), "MNT-only account must survive empty-object pruning")
+ assert.Equal(t, uint256.NewInt(500), s.GetMntBalance(addr, tokenID), "MNT balance must survive")
+
+ // Draining the last MNT balance makes the account empty/prunable again.
+ s.SubMntBalance(addr, uint256.NewInt(500), tokenID)
+ assert.True(t, s.Empty(addr), "account with no QKC, no MNT, no code, nonce 0 must be empty")
+}
+
+// TestCopyDoesNotAliasMntBalances guards the deepCopy aliasing bug: StateDB.Copy()
+// must deep-copy the MntBalances map, or a mutation on the copy corrupts the
+// original (and vice versa), diverging the state root.
+func TestCopyDoesNotAliasMntBalances(t *testing.T) {
+ s := newMntTestStateDB(t)
+ addr := common.HexToAddress("0xA11A5")
+ s.CreateAccount(addr)
+ const tokenID = uint64(777)
+ s.AddMntBalance(addr, uint256.NewInt(1000), tokenID)
+
+ cp := s.Copy()
+ // Mutate the copy; the original must be unaffected.
+ cp.AddMntBalance(addr, uint256.NewInt(500), tokenID)
+
+ assert.Equal(t, uint256.NewInt(1000), s.GetMntBalance(addr, tokenID), "original must not see copy's MNT mutation")
+ assert.Equal(t, uint256.NewInt(1500), cp.GetMntBalance(addr, tokenID), "copy must reflect its own mutation")
+
+ // And the reverse direction.
+ s.AddMntBalance(addr, uint256.NewInt(1), tokenID)
+ assert.Equal(t, uint256.NewInt(1001), s.GetMntBalance(addr, tokenID), "original reflects its own mutation")
+ assert.Equal(t, uint256.NewInt(1500), cp.GetMntBalance(addr, tokenID), "copy must not see original's later mutation")
+}
+
+// TestLoadedObjectDoesNotAliasOriginMnt verifies that when a state object is
+// loaded from an existing account (newObject with a non-nil origin holding MNT
+// balances), mutating the MNT balance does not corrupt s.origin. Without the
+// deep-copy in newObject, SetValue mutates the map shared by data and origin,
+// so commit() would record the post-mutation balance as the rollback baseline.
+func TestLoadedObjectDoesNotAliasOriginMnt(t *testing.T) {
+ db := triedb.NewDatabase(rawdb.NewMemoryDatabase(), nil)
+ sdb := NewDatabase(db, nil)
+ s, err := New(common.Hash{}, sdb)
+ require.NoError(t, err)
+
+ addr := common.HexToAddress("0xB0B")
+ const tokenID = uint64(888)
+ s.CreateAccount(addr)
+ s.AddMntBalance(addr, uint256.NewInt(1000), tokenID)
+ root, err := s.Commit(0, false, false)
+ require.NoError(t, err)
+
+ // Reload from the committed state so the object is built via newObject with
+ // a non-nil origin carrying MntBalances.
+ s2, err := New(root, sdb)
+ require.NoError(t, err)
+
+ obj := s2.getStateObject(addr)
+ require.NotNil(t, obj)
+ require.NotNil(t, obj.origin)
+
+ s2.AddMntBalance(addr, uint256.NewInt(500), tokenID)
+
+ // The mutation must land on data, not on origin.
+ assert.Equal(t, uint256.NewInt(1500), obj.GetMntBalance(tokenID), "live data reflects the mutation")
+ assert.Equal(t, uint256.NewInt(1000), obj.origin.MntBalances.GetTokenBalance(tokenID), "origin must retain the pre-mutation balance")
+}
diff --git a/core/state/reader.go b/core/state/reader.go
index be07cec0f97f..5b313b4a29fc 100644
--- a/core/state/reader.go
+++ b/core/state/reader.go
@@ -25,6 +25,7 @@ import (
"github.com/ethereum/go-ethereum/core/overlay"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
+ qkccommon "github.com/ethereum/go-ethereum/qkc/common"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/trie"
"github.com/ethereum/go-ethereum/trie/bintrie"
@@ -105,11 +106,23 @@ func (r *flatReader) Account(addr common.Address) (*types.StateAccount, error) {
if account == nil {
return nil, nil
}
+ return slimAccountToStateAccount(account)
+}
+
+func slimAccountToStateAccount(account *types.SlimAccount) (*types.StateAccount, error) {
acct := &types.StateAccount{
- Nonce: account.Nonce,
- Balance: account.Balance,
- CodeHash: account.CodeHash,
- Root: common.BytesToHash(account.Root),
+ Nonce: account.Nonce,
+ Balance: account.Balance,
+ CodeHash: account.CodeHash,
+ Root: common.BytesToHash(account.Root),
+ FullShardKey: account.FullShardKey,
+ }
+ if len(account.MntBal) > 0 {
+ mnt, err := qkccommon.NewTokenBalances(account.MntBal)
+ if err != nil {
+ return nil, err
+ }
+ acct.MntBalances = mnt
}
if len(acct.CodeHash) == 0 {
acct.CodeHash = types.EmptyCodeHash.Bytes()
diff --git a/core/state/state_object.go b/core/state/state_object.go
index 8e72486825e2..fd6043ff3fa2 100644
--- a/core/state/state_object.go
+++ b/core/state/state_object.go
@@ -27,6 +27,7 @@ import (
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log"
+ qkccommon "github.com/ethereum/go-ethereum/qkc/common"
"github.com/ethereum/go-ethereum/trie"
"github.com/ethereum/go-ethereum/trie/bintrie"
"github.com/ethereum/go-ethereum/trie/transitiontrie"
@@ -88,8 +89,14 @@ type stateObject struct {
}
// empty returns whether the account is considered empty.
+//
+// The QKC fork also requires the account to hold no MNT (non-QKC) token
+// balances (IsBlankMnt). This mirrors pyquarkchain's _Account.is_blank, whose
+// token_balances.is_blank() spans every token; checking only the QKC balance
+// would prune nonce0/QKC0/MNT-nonzero/no-code accounts that pyquarkchain keeps,
+// diverging the state root. See state_object_qkc.go.
func (s *stateObject) empty() bool {
- return s.data.Nonce == 0 && s.data.Balance.IsZero() && bytes.Equal(s.data.CodeHash, types.EmptyCodeHash.Bytes())
+ return s.data.Nonce == 0 && s.data.Balance.IsZero() && s.IsBlankMnt() && bytes.Equal(s.data.CodeHash, types.EmptyCodeHash.Bytes())
}
// newObject creates a state object.
@@ -98,7 +105,7 @@ func newObject(db *StateDB, address common.Address, acct *types.StateAccount) *s
if acct == nil {
acct = types.NewEmptyStateAccount()
}
- return &stateObject{
+ obj := &stateObject{
db: db,
address: address,
origin: origin,
@@ -108,6 +115,16 @@ func newObject(db *StateDB, address common.Address, acct *types.StateAccount) *s
pendingStorage: make(Storage),
uncommittedStorage: make(Storage),
}
+ // data is a shallow value copy of *acct, so data.MntBalances aliases the
+ // same map as origin.MntBalances. SetMntBalance mutates that map in place
+ // (TokenBalances.SetValue), so without this copy an MNT mutation would also
+ // rewrite s.origin, and commit() would record the post-mutation balance as
+ // the "origin" — corrupting the pathdb rollback baseline. Deep-copy so data
+ // and origin own independent maps. Mirrors the deepCopy() guard below.
+ if origin != nil && origin.MntBalances != nil {
+ obj.data.MntBalances = origin.MntBalances.Copy()
+ }
+ return obj
}
func (s *stateObject) addrHash() common.Hash {
@@ -490,7 +507,15 @@ func (s *stateObject) AddBalance(amount *uint256.Int) uint256.Int {
// SetBalance sets the balance for the object, and returns the previous balance.
func (s *stateObject) SetBalance(amount *uint256.Int) uint256.Int {
prev := *s.data.Balance
+ if s.data.Balance.IsZero() && amount != nil && !amount.IsZero() && s.nonZeroMntBalanceCount() >= qkccommon.TokenTrieThreshold {
+ log.Error("SetBalance exceeds supported token limit", "addr", s.address, "limit", qkccommon.TokenTrieThreshold)
+ return prev
+ }
+ if s.data.Balance.Eq(amount) {
+ return prev
+ }
s.db.journal.balanceChange(s.address, s.data.Balance)
+ s.data.MarkBalanceUpdated()
s.setBalance(amount)
return prev
}
@@ -515,6 +540,14 @@ func (s *stateObject) deepCopy(db *StateDB) *stateObject {
selfDestructed: s.selfDestructed,
newContract: s.newContract,
}
+ // data above is a shallow value copy of StateAccount; MntBalances is a
+ // pointer whose underlying map is mutated in place by SetMntBalance, so it
+ // must be deep-copied. Otherwise the copy and the original alias the same
+ // balances map and an MNT mutation on one StateDB silently corrupts the
+ // other (e.g. after StateDB.Copy()), diverging the state root.
+ if s.data.MntBalances != nil {
+ obj.data.MntBalances = s.data.MntBalances.Copy()
+ }
switch s.trie.(type) {
case *bintrie.BinaryTrie:
diff --git a/core/state/state_object_qkc.go b/core/state/state_object_qkc.go
new file mode 100644
index 000000000000..6888aa3c5745
--- /dev/null
+++ b/core/state/state_object_qkc.go
@@ -0,0 +1,109 @@
+// Copyright 2026-2027, QuarkChain.
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package state
+
+import (
+ "github.com/ethereum/go-ethereum/log"
+ qkccommon "github.com/ethereum/go-ethereum/qkc/common"
+ "github.com/holiman/uint256"
+)
+
+func (s *stateObject) SetMntBalance(amount *uint256.Int, tokenID uint64) {
+ if !s.canSetMntBalance(amount, tokenID) {
+ return
+ }
+ s.setMntBalance(amount, tokenID)
+}
+
+func (s *stateObject) canSetMntBalance(amount *uint256.Int, tokenID uint64) bool {
+ if tokenID == qkccommon.DefaultTokenID {
+ log.Error("SetMntBalance called with QKC tokenID; use SetBalance", "addr", s.address)
+ return false
+ }
+ if amount != nil && !amount.IsZero() && s.GetMntBalance(tokenID).IsZero() {
+ cnt := s.nonZeroMntBalanceCount()
+ if !s.Balance().IsZero() {
+ cnt++
+ }
+ if cnt >= qkccommon.TokenTrieThreshold {
+ log.Error("SetMntBalance exceeds supported token limit", "addr", s.address, "limit", qkccommon.TokenTrieThreshold)
+ return false
+ }
+ }
+ return true
+}
+
+func (s *stateObject) nonZeroMntBalanceCount() int {
+ if s.data.MntBalances == nil {
+ return 0
+ }
+ cnt := 0
+ for _, balance := range s.data.MntBalances.GetBalanceMap() {
+ if !balance.IsZero() {
+ cnt++
+ }
+ }
+ return cnt
+}
+
+func (s *stateObject) setMntBalance(amount *uint256.Int, tokenID uint64) {
+ if s.data.MntBalances == nil {
+ s.data.MntBalances = qkccommon.NewEmptyTokenBalances()
+ }
+ s.data.MntBalances.SetValue(amount, tokenID)
+}
+
+func (s *stateObject) AddMntBalance(amount *uint256.Int, tokenID uint64) {
+ if amount.IsZero() {
+ return
+ }
+ if tokenID == qkccommon.DefaultTokenID {
+ log.Error("AddMntBalance called with QKC tokenID; use AddBalance", "addr", s.address)
+ return
+ }
+ cur := s.GetMntBalance(tokenID)
+ s.SetMntBalance(new(uint256.Int).Add(cur, amount), tokenID)
+}
+
+func (s *stateObject) SubMntBalance(amount *uint256.Int, tokenID uint64) {
+ if amount.IsZero() {
+ return
+ }
+ if tokenID == qkccommon.DefaultTokenID {
+ log.Error("SubMntBalance called with QKC tokenID; use SubBalance", "addr", s.address)
+ return
+ }
+ cur := s.GetMntBalance(tokenID)
+ s.SetMntBalance(new(uint256.Int).Sub(cur, amount), tokenID)
+}
+
+func (s *stateObject) GetMntBalance(tokenID uint64) *uint256.Int {
+ if s.data.MntBalances == nil {
+ return new(uint256.Int)
+ }
+ return s.data.MntBalances.GetTokenBalance(tokenID)
+}
+
+// IsBlankMnt reports whether the account holds no non-QKC (MNT) token balances.
+// It is consumed by empty() so that the EIP-158 empty-account check spans every
+// token, matching pyquarkchain's _Account.is_blank (which evaluates
+// token_balances.is_blank() across all tokens). Without this, an account with
+// nonce==0 / QKC==0 / MNT!=0 / no code would be pruned here but kept by
+// pyquarkchain, producing a divergent state root.
+func (s *stateObject) IsBlankMnt() bool {
+ return s.data.MntBalances == nil || s.data.MntBalances.IsBlank()
+}
diff --git a/core/state/state_test.go b/core/state/state_test.go
index eeeb7fa2df87..2b36c64d1e2c 100644
--- a/core/state/state_test.go
+++ b/core/state/state_test.go
@@ -25,6 +25,7 @@ import (
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/triedb"
"github.com/holiman/uint256"
)
@@ -60,7 +61,7 @@ func TestDump(t *testing.T) {
s.state, _ = New(root, tdb)
got := string(s.state.Dump(nil))
want := `{
- "root": "71edff0130dd2385947095001c73d9e28d862fc286fca2b922ca6f6f3cddfdd2",
+ "root": "c35a032efd1d99e0348bc4e29ff402133c643fb3c4550e377ab351ca57de387a",
"accounts": {
"0x0000000000000000000000000000000000000001": {
"balance": "22",
@@ -119,7 +120,7 @@ func TestIterativeDump(t *testing.T) {
s.state.IterativeDump(nil, json.NewEncoder(b))
// check that DumpToCollector contains the state objects that are in trie
got := b.String()
- want := `{"root":"0xd5710ea8166b7b04bc2bfb129d7db12931cee82f75ca8e2d075b4884322bf3de"}
+ want := `{"root":"0x7b190b66d76c5b2d5bd1bf02f1918437d910a2e825a16465cf57d1bd1c2433fe"}
{"balance":"22","nonce":0,"root":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","codeHash":"0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470","address":"0x0000000000000000000000000000000000000001","key":"0x1468288056310c82aa4c01a7e12a10f8111a0560e72b700555479031b86c357d"}
{"balance":"1337","nonce":0,"root":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","codeHash":"0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470","address":"0x0000000000000000000000000000000000000000","key":"0x5380c7b7ae81a58eb98d9c78de4a1fd7fd9535fc953ed2be602daaa41767312a"}
{"balance":"0","nonce":0,"root":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","codeHash":"0x87874902497a5bb968da31a2998d8f22e949d1ef6214bcdedd8bae24cca4b9e3","code":"0x03030303030303","address":"0x0000000000000000000000000000000000000102","key":"0xa17eacbc25cda025e81db9c5c62868822c73ce097cee2a63e33a2e41268358a1"}
@@ -188,6 +189,84 @@ func TestSnapshotEmpty(t *testing.T) {
s.state.RevertToSnapshot(s.state.Snapshot())
}
+func TestBalanceChangeTracksQKCPresence(t *testing.T) {
+ state, _ := New(types.EmptyRootHash, NewDatabaseForTesting())
+ addr := common.BytesToAddress([]byte("qkc-presence"))
+ obj := state.getOrNewStateObject(addr)
+
+ obj.SetBalance(new(uint256.Int))
+ if obj.data.IsBalanceUpdated() {
+ t.Fatal("unchanged zero balance created a QKC presence marker")
+ }
+ assertAccountTokenBalance(t, &obj.data, nil)
+
+ snapshot := state.Snapshot()
+ obj.SetBalance(uint256.NewInt(1000))
+ if !obj.data.IsBalanceUpdated() {
+ t.Fatal("balance change did not create a QKC presence marker")
+ }
+ obj.SetBalance(new(uint256.Int))
+ if !obj.data.IsBalanceUpdated() {
+ t.Fatal("draining QKC balance removed its presence marker")
+ }
+ assertAccountTokenBalance(t, &obj.data, []byte{0x00, 0xc0})
+ nestedSnapshot := state.Snapshot()
+ obj.SetBalance(uint256.NewInt(10))
+ state.RevertToSnapshot(nestedSnapshot)
+ if !obj.data.IsBalanceUpdated() {
+ t.Fatal("reverting a later balance change removed earlier update history")
+ }
+ assertAccountTokenBalance(t, &obj.data, []byte{0x00, 0xc0})
+
+ state.RevertToSnapshot(snapshot)
+ obj = state.getStateObject(addr)
+ if !obj.data.IsBalanceUpdated() {
+ t.Fatal("snapshot revert removed the QKC presence marker")
+ }
+ if !obj.Balance().IsZero() {
+ t.Fatal("snapshot revert did not restore the zero QKC balance")
+ }
+ assertAccountTokenBalance(t, &obj.data, []byte{0x00, 0xc0})
+}
+
+func TestBalanceChangeRevertKeepsExistingQKCPresence(t *testing.T) {
+ state, _ := New(types.EmptyRootHash, NewDatabaseForTesting())
+ addr := common.BytesToAddress([]byte("qkc-existing"))
+ obj := state.getOrNewStateObject(addr)
+ obj.SetBalance(uint256.NewInt(100))
+ obj.SetBalance(new(uint256.Int))
+ state.Finalise(false)
+ if !obj.data.IsBalanceUpdated() {
+ t.Fatal("finalising state removed an existing QKC balance update")
+ }
+
+ snapshot := state.Snapshot()
+ obj.SetBalance(uint256.NewInt(1000))
+ state.RevertToSnapshot(snapshot)
+ if !state.getStateObject(addr).data.IsBalanceUpdated() {
+ t.Fatal("snapshot revert removed an existing QKC presence marker")
+ }
+}
+
+func assertAccountTokenBalance(t *testing.T, account *types.StateAccount, want []byte) {
+ t.Helper()
+ encoded, err := rlp.EncodeToBytes(account)
+ if err != nil {
+ t.Fatalf("failed to encode account: %v", err)
+ }
+ var fields []rlp.RawValue
+ if err := rlp.DecodeBytes(encoded, &fields); err != nil {
+ t.Fatalf("failed to decode account fields: %v", err)
+ }
+ var tokenBalance []byte
+ if err := rlp.DecodeBytes(fields[1], &tokenBalance); err != nil {
+ t.Fatalf("failed to decode token balance: %v", err)
+ }
+ if !bytes.Equal(tokenBalance, want) {
+ t.Fatalf("unexpected token balance encoding: have %x, want %x", tokenBalance, want)
+ }
+}
+
func TestCreateObjectRevert(t *testing.T) {
state, _ := New(types.EmptyRootHash, NewDatabaseForTesting())
addr := common.BytesToAddress([]byte("so0"))
diff --git a/core/state/statedb.go b/core/state/statedb.go
index e6d8b5bffc94..0addc5618c82 100644
--- a/core/state/statedb.go
+++ b/core/state/statedb.go
@@ -137,6 +137,11 @@ type StateDB struct {
// Snapshot and RevertToSnapshot.
journal *journal
+ // fullShardKey is the QuarkChain shard key of the current transaction's
+ // destination. It is set once per transaction via SetFullShardKey and
+ // assigned to any newly created accounts that have no prior state.
+ fullShardKey uint32
+
// State witness if cross validation is needed
witness *stateless.Witness
@@ -292,6 +297,13 @@ func (s *StateDB) Preimages() map[common.Hash][]byte {
return s.preimages
}
+// SetFullShardKey sets the QuarkChain shard key for the current transaction
+// context. It is called once per transaction before account operations, and
+// any newly created accounts (with no prior state) inherit this value.
+func (s *StateDB) SetFullShardKey(fullShardKey uint32) {
+ s.fullShardKey = fullShardKey
+}
+
// AddRefund adds gas to the refund counter
func (s *StateDB) AddRefund(gas uint64) {
s.journal.refundChange(s.refund)
@@ -638,10 +650,23 @@ func (s *StateDB) getOrNewStateObject(addr common.Address) *stateObject {
return obj
}
-// createObject creates a new state object. The assumption is held there is no
-// existing account with the given address, otherwise it will be silently overwritten.
+// createObject creates a new state object, replacing any object currently live
+// at the address. The prior object's storage/balance/nonce are dropped (the
+// caller is responsible for having deleted them where required), but its
+// QuarkChain FullShardKey is preserved: the shard key is assigned on first
+// creation and must remain stable across a resurrection, so it is carried over
+// from the existing account rather than reset to the current transaction's key.
func (s *StateDB) createObject(addr common.Address) *stateObject {
+ // Check for an existing account so we can preserve its FullShardKey.
+ prev := s.getStateObject(addr)
+
obj := newObject(s, addr, nil)
+ // New accounts inherit the current transaction's destination shard key.
+ obj.data.FullShardKey = s.fullShardKey
+ // If the account previously existed, keep its original shard key unchanged.
+ if prev != nil {
+ obj.data.FullShardKey = prev.data.FullShardKey
+ }
s.journal.createObject(addr)
s.setStateObject(obj)
return obj
@@ -696,6 +721,7 @@ func (s *StateDB) Copy() *StateDB {
logs: make(map[common.Hash][]*types.Log, len(s.logs)),
logSize: s.logSize,
preimages: maps.Clone(s.preimages),
+ fullShardKey: s.fullShardKey,
// Do we need to copy the access list and transient storage?
// In practice: No. At the start of a transaction, these two lists are empty.
diff --git a/core/state/statedb_fuzz_test.go b/core/state/statedb_fuzz_test.go
index c796b416a32b..91b741f6a6b1 100644
--- a/core/state/statedb_fuzz_test.go
+++ b/core/state/statedb_fuzz_test.go
@@ -276,19 +276,21 @@ func (test *stateTest) verifyAccountCreation(next common.Hash, db *triedb.Databa
if len(nBlob) == 0 {
return fmt.Errorf("missing account in new trie, %x", addrHash)
}
- full, err := types.FullAccountRLP(account)
+ // Decode the expected account from slim-RLP and the trie account from QKC format,
+ // then compare logical fields (not raw bytes, since QKC adds extra fields).
+ wantAcct, err := types.FullAccount(account)
if err != nil {
return err
}
- if !bytes.Equal(nBlob, full) {
- return fmt.Errorf("unexpected account data, want: %v, got: %v", full, nBlob)
+ nAcct := new(types.StateAccount)
+ if err := rlp.DecodeBytes(nBlob, nAcct); err != nil {
+ return fmt.Errorf("unexpected account data, want: %v, got: %v", wantAcct, nBlob)
}
-
- // Verify storage changes
- var nAcct types.StateAccount
- if err := rlp.DecodeBytes(nBlob, &nAcct); err != nil {
- return err
+ if wantAcct.Nonce != nAcct.Nonce || wantAcct.Balance.Cmp(nAcct.Balance) != 0 ||
+ wantAcct.Root != nAcct.Root || !bytes.Equal(wantAcct.CodeHash, nAcct.CodeHash) {
+ return fmt.Errorf("unexpected account data, want: %v, got: %v", wantAcct, nAcct)
}
+
// Account has no slot, empty slot set is expected
if nAcct.Root == types.EmptyRootHash {
if len(storagesOrigin) != 0 {
@@ -347,37 +349,39 @@ func (test *stateTest) verifyAccountUpdate(next common.Hash, db *triedb.Database
if len(oBlob) == 0 {
return fmt.Errorf("missing account in old trie, %x", addrHash)
}
- full, err := types.FullAccountRLP(accountOrigin)
+ // Decode old and new trie blobs using QKC-aware decoder.
+ // Compare logical fields against the slim-RLP account from the diff,
+ // not raw bytes (QKC format adds extra fields).
+ wantOriginAcct, err := types.FullAccount(accountOrigin)
if err != nil {
return err
}
- if !bytes.Equal(full, oBlob) {
+ oAcct := new(types.StateAccount)
+ if err := rlp.DecodeBytes(oBlob, oAcct); err != nil {
+ return fmt.Errorf("failed to decode old trie account %x: %v", addrHash, err)
+ }
+ if wantOriginAcct.Nonce != oAcct.Nonce || wantOriginAcct.Balance.Cmp(oAcct.Balance) != 0 ||
+ wantOriginAcct.Root != oAcct.Root || !bytes.Equal(wantOriginAcct.CodeHash, oAcct.CodeHash) {
return fmt.Errorf("account value is not matched, %x", addrHash)
}
+ var nRoot common.Hash
if len(nBlob) == 0 {
if len(account) != 0 {
return errors.New("unexpected account data")
}
- } else {
- full, _ = types.FullAccountRLP(account)
- if !bytes.Equal(full, nBlob) {
- return fmt.Errorf("unexpected account data, %x, want %v, got: %v", addrHash, full, nBlob)
- }
- }
- // Decode accounts
- var (
- oAcct types.StateAccount
- nAcct types.StateAccount
- nRoot common.Hash
- )
- if err := rlp.DecodeBytes(oBlob, &oAcct); err != nil {
- return err
- }
- if len(nBlob) == 0 {
nRoot = types.EmptyRootHash
} else {
- if err := rlp.DecodeBytes(nBlob, &nAcct); err != nil {
- return err
+ wantAcct, wantErr := types.FullAccount(account)
+ if wantErr != nil {
+ return wantErr
+ }
+ nAcct := new(types.StateAccount)
+ if decErr := rlp.DecodeBytes(nBlob, nAcct); decErr != nil {
+ return fmt.Errorf("failed to decode new trie account %x: %v", addrHash, decErr)
+ }
+ if wantAcct.Nonce != nAcct.Nonce || wantAcct.Balance.Cmp(nAcct.Balance) != 0 ||
+ wantAcct.Root != nAcct.Root || !bytes.Equal(wantAcct.CodeHash, nAcct.CodeHash) {
+ return fmt.Errorf("unexpected account data, %x, want %v, got: %v", addrHash, wantAcct, nAcct)
}
nRoot = nAcct.Root
}
diff --git a/core/state/statedb_hooked.go b/core/state/statedb_hooked.go
index c5faa7c98eb8..f0e0e277441c 100644
--- a/core/state/statedb_hooked.go
+++ b/core/state/statedb_hooked.go
@@ -63,6 +63,18 @@ func (s *hookedStateDB) GetBalance(addr common.Address) *uint256.Int {
return s.inner.GetBalance(addr)
}
+func (s *hookedStateDB) GetMntBalance(addr common.Address, tokenID uint64) *uint256.Int {
+ return s.inner.GetMntBalance(addr, tokenID)
+}
+
+func (s *hookedStateDB) AddMntBalance(addr common.Address, amount *uint256.Int, tokenID uint64) {
+ s.inner.AddMntBalance(addr, amount, tokenID)
+}
+
+func (s *hookedStateDB) SubMntBalance(addr common.Address, amount *uint256.Int, tokenID uint64) {
+ s.inner.SubMntBalance(addr, amount, tokenID)
+}
+
func (s *hookedStateDB) GetNonce(addr common.Address) uint64 {
return s.inner.GetNonce(addr)
}
diff --git a/core/state/statedb_qkc.go b/core/state/statedb_qkc.go
new file mode 100644
index 000000000000..ad3b0041db3c
--- /dev/null
+++ b/core/state/statedb_qkc.go
@@ -0,0 +1,85 @@
+// Copyright 2026-2027, QuarkChain.
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package state
+
+import (
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/tracing"
+ qkccommon "github.com/ethereum/go-ethereum/qkc/common"
+ "github.com/holiman/uint256"
+)
+
+// ===== StateDB MNT methods =====
+
+func (s *StateDB) SetMntBalance(addr common.Address, amount *uint256.Int, tokenID uint64) {
+ if tokenID == qkccommon.DefaultTokenID {
+ return
+ }
+ obj := s.getOrNewStateObject(addr)
+ if obj == nil || !obj.canSetMntBalance(amount, tokenID) {
+ return
+ }
+ s.journal.mntBalanceChange(addr, obj.data.MntBalances)
+ obj.setMntBalance(amount, tokenID)
+}
+
+func (s *StateDB) AddMntBalance(addr common.Address, amount *uint256.Int, tokenID uint64) {
+ if amount.IsZero() {
+ return
+ }
+ updated, overflow := new(uint256.Int).AddOverflow(s.GetMntBalance(addr, tokenID), amount)
+ if overflow {
+ return
+ }
+ s.SetMntBalance(addr, updated, tokenID)
+}
+
+func (s *StateDB) SubMntBalance(addr common.Address, amount *uint256.Int, tokenID uint64) {
+ if amount.IsZero() {
+ return
+ }
+ updated, underflow := new(uint256.Int).SubOverflow(s.GetMntBalance(addr, tokenID), amount)
+ if underflow {
+ return
+ }
+ s.SetMntBalance(addr, updated, tokenID)
+}
+
+func (s *StateDB) GetMntBalance(addr common.Address, tokenID uint64) *uint256.Int {
+ obj := s.getStateObject(addr)
+ if obj == nil {
+ return new(uint256.Int)
+ }
+ return obj.GetMntBalance(tokenID)
+}
+
+// SubBalanceByTokenID subtracts QKC or MNT balance based on tokenID.
+func (s *StateDB) SubBalanceByTokenID(addr common.Address, amount *uint256.Int, tokenID uint64, reason tracing.BalanceChangeReason) {
+ if tokenID == qkccommon.DefaultTokenID {
+ s.SubBalance(addr, amount, reason)
+ } else {
+ s.SubMntBalance(addr, amount, tokenID)
+ }
+}
+
+// GetBalanceByTokenID returns QKC or MNT balance for the given tokenID.
+func (s *StateDB) GetBalanceByTokenID(addr common.Address, tokenID uint64) *uint256.Int {
+ if tokenID == qkccommon.DefaultTokenID {
+ return s.GetBalance(addr)
+ }
+ return s.GetMntBalance(addr, tokenID)
+}
diff --git a/triedb/pathdb/execute.go b/triedb/pathdb/execute.go
index 4c1cafec12aa..6f1b7947fb7d 100644
--- a/triedb/pathdb/execute.go
+++ b/triedb/pathdb/execute.go
@@ -90,8 +90,7 @@ func apply(db database.NodeDatabase, prevRoot common.Hash, postRoot common.Hash,
// existent in post-state. Apply the reverse diff and verify if the storage
// root matches the one in prev-state account.
func updateAccount(ctx *context, db database.NodeDatabase, addr common.Address) error {
- // The account was present in prev-state, decode it from the
- // 'slim-rlp' format bytes.
+ // The account was present in prev-state, decode it from the slim-RLP format.
addrHash := crypto.Keccak256Hash(addr.Bytes())
prev, err := types.FullAccount(ctx.accounts[addr])
if err != nil {