Skip to content
Draft
Show file tree
Hide file tree
Changes from 13 commits
Commits
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
21 changes: 0 additions & 21 deletions core/types/gen_account_rlp.go

This file was deleted.

121 changes: 102 additions & 19 deletions core/types/state_account.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,28 @@ import (
"bytes"

"github.com/ethereum/go-ethereum/common"
qkccommon "github.com/ethereum/go-ethereum/qkc/common"
"github.com/ethereum/go-ethereum/rlp"
"github.com/holiman/uint256"
)

//go:generate go run ../../rlp/rlpgen -type StateAccount -out gen_account_rlp.go
// NOTE: StateAccount uses a hand-written QuarkChain codec (EncodeRLP/DecodeRLP in
// state_account_qkc.go) for the 6-element MNT account format. The rlpgen go:generate
// directive was intentionally removed: regenerating gen_account_rlp.go would create a
// conflicting standard 4-field codec and silently drop MntBalances / FullShardKey.

// StateAccount is the Ethereum consensus representation of accounts.
// These objects are stored in the main account trie.
type StateAccount struct {
Nonce uint64
Balance *uint256.Int
Root common.Hash // merkle root of the storage trie
CodeHash []byte
Nonce uint64
Balance *uint256.Int
Root common.Hash // merkle root of the storage trie
CodeHash []byte
MntBalances *qkccommon.TokenBalances // Non-QKC balances.
FullShardKey uint32 // QuarkChain shard key; set on first tx, preserved thereafter
// balanceUpdateCount keeps a changed zero QKC balance encoded as 00c0. Using
// a counter ensures that reverting one update does not clear earlier updates.
balanceUpdateCount uint64
}

// NewEmptyStateAccount constructs an empty state account.
Expand All @@ -50,54 +59,128 @@ func (acct *StateAccount) Copy() *StateAccount {
if acct.Balance != nil {
balance = new(uint256.Int).Set(acct.Balance)
}
var mnt *qkccommon.TokenBalances
if acct.MntBalances != nil {
mnt = acct.MntBalances.Copy()
}
return &StateAccount{
Nonce: acct.Nonce,
Balance: balance,
Root: acct.Root,
CodeHash: common.CopyBytes(acct.CodeHash),
Nonce: acct.Nonce,
Balance: balance,
Root: acct.Root,
CodeHash: common.CopyBytes(acct.CodeHash),
MntBalances: mnt,
FullShardKey: acct.FullShardKey,
balanceUpdateCount: acct.balanceUpdateCount,
}
}

// IsBalanceUpdated reports whether the QKC balance has been explicitly updated.
func (acct *StateAccount) IsBalanceUpdated() bool {
return acct.balanceUpdateCount > 0
}

// AddBalanceUpdate records a QKC balance update.
func (acct *StateAccount) AddBalanceUpdate() {
acct.balanceUpdateCount++
}

// RevertBalanceUpdate removes a reverted QKC balance update.
func (acct *StateAccount) RevertBalanceUpdate() {
Comment thread
ping-ke marked this conversation as resolved.
Outdated
if acct.balanceUpdateCount == 0 {
panic("reverting untracked QKC balance update")
}
acct.balanceUpdateCount--
}

// SlimAccount is a modified version of an Account, where the root is replaced
// with a byte slice. This format can be used to represent full-consensus format
// or slim format which replaces the empty root and code hash as nil byte slice.
// FinaliseBalanceUpdates is called at the transaction boundary before the
// journal is cleared. It collapses committed balance updates to a single
// presence marker: their individual counts are no longer needed for reverts,
// but the marker must remain so an explicitly updated zero balance continues
// to encode as 00c0 in snapshots and consensus state. The compaction is
// required to keep the counter bounded; without it, successful transactions
// would accumulate counts indefinitely and could eventually overflow.
func (acct *StateAccount) FinaliseBalanceUpdates() {
Comment thread
ping-ke marked this conversation as resolved.
Outdated
if acct.balanceUpdateCount > 0 {
acct.balanceUpdateCount = 1
}
}

// SlimAccount is the compact RLP account format used by state snapshots,
// pathdb readers, and account iterators. To support snapshots in goshard, the
// standard format must be extended with FullShardKey and MntBal so snapshots
// preserve QuarkChain-specific account state. The added fields are optional
// trailing RLP fields, keeping old snapshots readable and leaving room for
// future extensions without changing the existing account format.
type SlimAccount struct {
Nonce uint64
Balance *uint256.Int
Root []byte // Nil if root equals to types.EmptyRootHash
CodeHash []byte // Nil if hash equals to types.EmptyCodeHash
// QKC-specific fields; both optional so old snapshots remain readable.
Comment thread
ping-ke marked this conversation as resolved.
FullShardKey uint32 `rlp:"optional"` // QuarkChain shard key
MntBal []byte `rlp:"optional"` // Non-QKC TokenBalances.SerializeToBytes output
}

// SlimAccountRLP encodes the state account in 'slim RLP' format.
func SlimAccountRLP(account StateAccount) []byte {
slim := SlimAccount{
Nonce: account.Nonce,
Balance: account.Balance,
Nonce: account.Nonce,
Balance: account.Balance,
FullShardKey: account.FullShardKey,
}
if account.Root != EmptyRootHash {
slim.Root = account.Root[:]
}
if !bytes.Equal(account.CodeHash, EmptyCodeHash[:]) {
slim.CodeHash = account.CodeHash
}
if account.MntBalances != nil {
mntBal, err := account.MntBalances.SerializeToBytes()
if err != nil {
panic(err)
Comment thread
ping-ke marked this conversation as resolved.
}
slim.MntBal = mntBal
}
if len(slim.MntBal) == 0 && account.IsBalanceUpdated() {
slim.MntBal = []byte{0x00, 0xc0}
}
data, err := rlp.EncodeToBytes(slim)
if err != nil {
panic(err)
}
return data
}

// FullAccount decodes the data on the 'slim RLP' format and returns
// the consensus format account.
// FullAccount decodes snapshot data from slim RLP into a StateAccount.
//
// This conversion intentionally follows the semantics of StateAccount's
// EncodeRLP and DecodeRLP methods instead of preserving the original account
// bytes. An explicitly updated zero balance can initially encode as 00c0, but
// decoding loses the zero entry because the serialized token list is empty.
// Re-encoding the decoded account therefore produces an empty TokenBal. Doing
// the same normalization here ensures that snapshot and trie reads return the
// same StateAccount.
//
// This behavior supports snapshot reads, but it cannot preserve trie leaves
// byte-for-byte as required by snap sync. If snap sync support is needed, the
// raw []byte account encodings must be transferred to the remote node and
// stored directly without decoding and re-encoding them through StateAccount.
func FullAccount(data []byte) (*StateAccount, error) {
var slim SlimAccount
if err := rlp.DecodeBytes(data, &slim); err != nil {
return nil, err
}
var account StateAccount
account.Nonce, account.Balance = slim.Nonce, slim.Balance

// Interpret the storage root and code hash in slim format.
account.Nonce, account.Balance, account.FullShardKey = slim.Nonce, slim.Balance, slim.FullShardKey
if len(slim.MntBal) > 0 {
tb, err := qkccommon.NewTokenBalances(slim.MntBal)
if err != nil {
return nil, err
}
if tb.Len() != 0 {
account.MntBalances = tb
}
}
if len(slim.Root) == 0 {
account.Root = EmptyRootHash
} else {
Expand Down
99 changes: 99 additions & 0 deletions core/types/state_account_qkc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Copyright 2026-2027, QuarkChain.

package types

import (
"errors"
"io"

"github.com/ethereum/go-ethereum/common"
qkccommon "github.com/ethereum/go-ethereum/qkc/common"
"github.com/ethereum/go-ethereum/rlp"
"github.com/holiman/uint256"
)

// qkcAccountRLP is the wire struct for QuarkChain's 6-element account format:
// [Nonce, TokenBal(bytes), Root, CodeHash, FullShardKey(4B fixed), Optional].
// TokenBal is stored as raw serialized bytes (matching pyquarkchain's `binary` type),
// so nil encodes as 0x80 (empty string), not 0xC0 (empty list).
type qkcAccountRLP struct {
Nonce uint64
TokenBal []byte // SerializeToBytes output; nil = no balances
Root common.Hash
CodeHash []byte
FullShardKey qkccommon.Uint32
Optional []byte
}

// tokenBalancesForEncoding combines the split QKC and MNT balances into the
// unified token table used by the wire format.
func (acct *StateAccount) tokenBalancesForEncoding() *qkccommon.TokenBalances {
merged := qkccommon.NewEmptyTokenBalances()
qkcIsZero := acct.Balance == nil || acct.Balance.IsZero()
if !qkcIsZero || acct.IsBalanceUpdated() {
merged.SetValue(acct.Balance, qkccommon.DefaultTokenID)
}
if acct.MntBalances != nil && acct.MntBalances.Len() > 0 {
for id, bal := range acct.MntBalances.GetBalanceMap() {
merged.SetValue(bal, id)
}
}
return merged
}

// EncodeRLP implements rlp.Encoder for StateAccount using QuarkChain's
// 6-element format. Root is always written as 32 bytes (no nil optimization).
func (acct *StateAccount) EncodeRLP(w io.Writer) error {
tokenBal, err := acct.tokenBalancesForEncoding().SerializeToBytes()
if err != nil {
return err
}
qkc := &qkcAccountRLP{
Nonce: acct.Nonce,
Root: acct.Root,
CodeHash: acct.CodeHash,
TokenBal: tokenBal,
FullShardKey: qkccommon.Uint32(acct.FullShardKey),
Optional: nil,
}
return rlp.Encode(w, qkc)
}

// DecodeRLP implements rlp.Decoder for StateAccount using QuarkChain's
// 6-element format.
func (acct *StateAccount) DecodeRLP(s *rlp.Stream) error {
raw, err := s.Raw()
if err != nil {
return err
}
var qkc qkcAccountRLP
if err := rlp.DecodeBytes(raw, &qkc); err != nil {
return err
}
acct.Nonce = qkc.Nonce
acct.CodeHash = qkc.CodeHash
acct.Root = qkc.Root
acct.FullShardKey = uint32(qkc.FullShardKey)
if len(qkc.Optional) != 0 {
return errors.New("unsupported non-empty QuarkChain account optional field")
}
acct.Balance = new(uint256.Int)
acct.MntBalances = nil
acct.balanceUpdateCount = 0
if len(qkc.TokenBal) > 0 {
tb, err := qkccommon.NewTokenBalances(qkc.TokenBal)
if err != nil {
return err
}
balMap := tb.GetBalanceMap()
qkcBal, hasQKC := balMap[qkccommon.DefaultTokenID]
if hasQKC {
acct.Balance.Set(qkcBal)
delete(balMap, qkccommon.DefaultTokenID)
}
if len(balMap) != 0 {
acct.MntBalances = qkccommon.NewTokenBalancesWithMap(balMap)
}
}
return nil
}
Loading