Skip to content
Draft
Show file tree
Hide file tree
Changes from 11 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.

102 changes: 87 additions & 15 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,36 +59,86 @@ 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--
}

// FinaliseBalanceUpdates keeps committed update presence without retaining
// the number of updates from journals that can no longer be reverted.
func (acct *StateAccount) FinaliseBalanceUpdates() {
Comment thread
ping-ke marked this conversation as resolved.
Outdated
if acct.balanceUpdateCount > 0 {
acct.balanceUpdateCount = 1
}
}

// 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.
// 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)
Expand All @@ -95,7 +154,20 @@ func FullAccount(data []byte) (*StateAccount, error) {
return nil, err
}
var account StateAccount
account.Nonce, account.Balance = slim.Nonce, slim.Balance
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 {
// SlimAccount uses 00c0 to preserve a zero-balance update. Restore the
// update marker instead of creating an empty MNT balance set.
account.AddBalanceUpdate()
} else {
account.MntBalances = tb
}
}

// Interpret the storage root and code hash in slim format.
if len(slim.Root) == 0 {
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