diff --git a/core/types/gen_account_rlp.go b/core/types/gen_account_rlp.go deleted file mode 100644 index 8b424493af..0000000000 --- a/core/types/gen_account_rlp.go +++ /dev/null @@ -1,21 +0,0 @@ -// Code generated by rlpgen. DO NOT EDIT. - -package types - -import "github.com/ethereum/go-ethereum/rlp" -import "io" - -func (obj *StateAccount) EncodeRLP(_w io.Writer) error { - w := rlp.NewEncoderBuffer(_w) - _tmp0 := w.List() - w.WriteUint64(obj.Nonce) - if obj.Balance == nil { - w.Write(rlp.EmptyString) - } else { - w.WriteUint256(obj.Balance) - } - w.WriteBytes(obj.Root[:]) - w.WriteBytes(obj.CodeHash) - w.ListEnd(_tmp0) - return w.Flush() -} diff --git a/core/types/state_account.go b/core/types/state_account.go index 52ef843b35..be647cce73 100644 --- a/core/types/state_account.go +++ b/core/types/state_account.go @@ -29,10 +29,13 @@ import ( // 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 *TokenBalances `rlp:"optional"` // non-QKC MNT balances; nil = no MNT tokens + FullShardKey uint32 // QuarkChain shard key; set on first tx, preserved thereafter + } // NewEmptyStateAccount constructs an empty state account. @@ -50,11 +53,17 @@ func (acct *StateAccount) Copy() *StateAccount { if acct.Balance != nil { balance = new(uint256.Int).Set(acct.Balance) } + var mnt *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, } } diff --git a/core/types/state_account_qkc.go b/core/types/state_account_qkc.go new file mode 100644 index 0000000000..1b8747e822 --- /dev/null +++ b/core/types/state_account_qkc.go @@ -0,0 +1,127 @@ +// 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 types + +import ( + "io" + + "github.com/ethereum/go-ethereum/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 Uint32 + Optional []byte +} + +// mergeQKCTokenBalances combines the QKC native balance (tokenID=35760) and MNT +// balances into a single TokenBalances for wire encoding. Returns nil if both empty, +// which causes EncodeRLP to write 0x80 (RLP nil/empty) matching goquarkchain behavior. +func mergeQKCTokenBalances(balance *uint256.Int, mnt *TokenBalances) *TokenBalances { + if (balance == nil || balance.IsZero()) && (mnt == nil || mnt.IsBlank()) { + return nil + } + merged := NewEmptyTokenBalances() + if mnt != nil { + for id, bal := range mnt.GetBalanceMap() { + merged.SetValue(bal, id) + } + } + if balance != nil && !balance.IsZero() { + merged.SetValue(balance, DefaultTokenID) + } + 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 { + var tokenBal []byte + switch { + case acct.MntBalances == nil && (acct.Balance == nil || acct.Balance.IsZero()): + // No QKC balance, no MNT tokens — encode TokenBal as nil → 0x80. + // Covers: new accounts and accounts that never entered the TokenBalances map. + tokenBal = nil + + case acct.MntBalances != nil && acct.MntBalances.IsBlank() && acct.Balance != nil && acct.Balance.IsZero(): + // QKC balance zero, MNT map explicitly empty → re-serialize as + // list-format with zero pairs → 0x8200c0. This preserves the + // "account touched TokenBalances map then emptied it" history. + tokenBal, _ = acct.MntBalances.SerializeToBytes() + + default: + // Normal case: has QKC balance and/or non-empty MNT tokens. + tb := mergeQKCTokenBalances(acct.Balance, acct.MntBalances) + var err error + tokenBal, err = tb.SerializeToBytes() + if err != nil { + return err + } + } + qkc := &qkcAccountRLP{ + Nonce: acct.Nonce, + Root: acct.Root, + CodeHash: acct.CodeHash, + TokenBal: tokenBal, + FullShardKey: 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) + acct.Balance = new(uint256.Int) + if len(qkc.TokenBal) > 0 { + tb, err := NewTokenBalancesFromBytes(qkc.TokenBal) + if err != nil { + return err + } + balMap := tb.GetBalanceMap() + if qkcBal, ok := balMap[DefaultTokenID]; ok { + acct.Balance.Set(qkcBal) + delete(balMap, DefaultTokenID) + } + // Always set MntBalances when TokenBal has content — even if empty + // after stripping QKC, this lets EncodeRLP produce 0x8200c0 instead + // of 0x80, preserving byte-identical round-trip. + acct.MntBalances = &TokenBalances{balances: balMap} + } + return nil +} diff --git a/core/types/state_account_qkc_test.go b/core/types/state_account_qkc_test.go new file mode 100644 index 0000000000..b836aeb566 --- /dev/null +++ b/core/types/state_account_qkc_test.go @@ -0,0 +1,285 @@ +// 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 types + +import ( + "bytes" + "encoding/hex" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rlp" + "github.com/holiman/uint256" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// pyquarkchain test vectors generated by: +// +// from quarkchain.evm.state import _Account, TokenBalancePair +// import rlp +// from rlp.sedes import BigEndianInt, binary, CountableList, big_endian_int +// +// See pyquarkchain/quarkchain/evm/state.py _Account for the canonical format. +// +// All vectors use: +// - storage = EmptyRootHash (keccak256 of empty) +// - code_hash = EmptyCodeHash (keccak256 of empty string) +// - full_shard_key = 1 (BigEndianInt(4) → always 4 bytes on the wire) +// - optional = b"" +var ( + pyqkcVecNonce1QKC1000 = mustHex("f853018900c7c6828bb08203e8a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470840000000180") + pyqkcVecNonce5QKC2000MNT500 = mustHex("f858058e00ccc4648201f4c6828bb08207d0a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470840000000180") + pyqkcVecZeroAccount = mustHex("f84a8080a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470840000000180") +) + +func mustHex(s string) []byte { + b, err := hex.DecodeString(s) + if err != nil { + panic(err) + } + return b +} + +// TestStateAccountEncodeDecodeRoundtrip verifies that EncodeRLP→DecodeRLP is +// a lossless roundtrip for all fields including FullShardKey. +func TestStateAccountEncodeDecodeRoundtrip(t *testing.T) { + cases := []struct { + name string + acct StateAccount + }{ + { + name: "empty", + acct: *NewEmptyStateAccount(), + }, + { + name: "nonce1 QKC=1000 shard=1", + acct: StateAccount{ + Nonce: 1, + Balance: uint256.NewInt(1000), + Root: EmptyRootHash, + CodeHash: EmptyCodeHash[:], + FullShardKey: 1, + }, + }, + { + name: "nonce5 QKC=2000 MNT[100]=500 shard=0x2f3e", + acct: StateAccount{ + Nonce: 5, + Balance: uint256.NewInt(2000), + Root: EmptyRootHash, + CodeHash: EmptyCodeHash[:], + MntBalances: NewTokenBalancesWithMap(map[uint64]*uint256.Int{ + 100: uint256.NewInt(500), + }), + FullShardKey: 0x2f3e, + }, + }, + { + name: "non-empty root and codehash shard=0x72ea", + acct: StateAccount{ + Nonce: 3, + Balance: uint256.NewInt(1e18), + Root: common.HexToHash("0xdeadbeef"), + CodeHash: common.Hex2Bytes("abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"), + FullShardKey: 0x72ea, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + encoded, err := rlp.EncodeToBytes(&tc.acct) + require.NoError(t, err) + + var decoded StateAccount + require.NoError(t, rlp.DecodeBytes(encoded, &decoded)) + + assert.Equal(t, tc.acct.Nonce, decoded.Nonce) + assert.Equal(t, tc.acct.Root, decoded.Root) + assert.Equal(t, tc.acct.CodeHash, decoded.CodeHash) + assert.Equal(t, tc.acct.FullShardKey, decoded.FullShardKey) + require.NotNil(t, decoded.Balance) + assert.Equal(t, tc.acct.Balance, decoded.Balance) + if tc.acct.MntBalances == nil || tc.acct.MntBalances.IsBlank() { + assert.True(t, decoded.MntBalances == nil || decoded.MntBalances.IsBlank()) + } else { + require.NotNil(t, decoded.MntBalances) + assert.Equal(t, tc.acct.MntBalances.GetBalanceMap(), decoded.MntBalances.GetBalanceMap()) + } + }) + } +} + +// TestStateAccountPyquarkchainDecodeCompatibility verifies that goshard can +// decode blobs produced by pyquarkchain. These are the authoritative wire +// vectors for the QKC 6-element RLP format. All vectors use full_shard_key=1. +func TestStateAccountPyquarkchainDecodeCompatibility(t *testing.T) { + cases := []struct { + name string + blob []byte + wantNonce uint64 + wantQKC *uint256.Int + wantMNT map[uint64]*uint256.Int // nil = no MNT tokens expected + wantFullShardKey uint32 + }{ + { + name: "nonce=1 QKC=1000", + blob: pyqkcVecNonce1QKC1000, + wantNonce: 1, + wantQKC: uint256.NewInt(1000), + wantFullShardKey: 1, + }, + { + name: "nonce=5 QKC=2000 MNT[100]=500", + blob: pyqkcVecNonce5QKC2000MNT500, + wantNonce: 5, + wantQKC: uint256.NewInt(2000), + wantMNT: map[uint64]*uint256.Int{100: uint256.NewInt(500)}, + wantFullShardKey: 1, + }, + { + name: "zero account", + blob: pyqkcVecZeroAccount, + wantNonce: 0, + wantQKC: uint256.NewInt(0), + wantFullShardKey: 1, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + var acct StateAccount + require.NoError(t, rlp.DecodeBytes(tc.blob, &acct)) + + assert.Equal(t, tc.wantNonce, acct.Nonce) + assert.Equal(t, EmptyRootHash, acct.Root) + assert.Equal(t, EmptyCodeHash[:], acct.CodeHash) + assert.Equal(t, tc.wantFullShardKey, acct.FullShardKey) + require.NotNil(t, acct.Balance) + assert.Equal(t, tc.wantQKC, acct.Balance) + + if tc.wantMNT == nil { + assert.True(t, acct.MntBalances == nil || acct.MntBalances.IsBlank()) + } else { + require.NotNil(t, acct.MntBalances) + assert.Equal(t, tc.wantMNT, acct.MntBalances.GetBalanceMap()) + } + }) + } +} + +// TestStateAccountPyquarkchainEncodeCompatibility verifies that goshard +// produces byte-for-byte identical output to pyquarkchain for the same account. +// This is the strongest compatibility check: same input → same wire bytes. +func TestStateAccountPyquarkchainEncodeCompatibility(t *testing.T) { + cases := []struct { + name string + acct StateAccount + want []byte + }{ + { + name: "nonce=1 QKC=1000", + acct: StateAccount{ + Nonce: 1, + Balance: uint256.NewInt(1000), + Root: EmptyRootHash, + CodeHash: EmptyCodeHash[:], + FullShardKey: 1, + }, + want: pyqkcVecNonce1QKC1000, + }, + { + name: "nonce=5 QKC=2000 MNT[100]=500", + acct: StateAccount{ + Nonce: 5, + Balance: uint256.NewInt(2000), + Root: EmptyRootHash, + CodeHash: EmptyCodeHash[:], + MntBalances: NewTokenBalancesWithMap(map[uint64]*uint256.Int{ + 100: uint256.NewInt(500), + }), + FullShardKey: 1, + }, + want: pyqkcVecNonce5QKC2000MNT500, + }, + { + name: "zero account", + acct: StateAccount{ + Nonce: 0, + Balance: uint256.NewInt(0), + Root: EmptyRootHash, + CodeHash: EmptyCodeHash[:], + FullShardKey: 1, + }, + want: pyqkcVecZeroAccount, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got, err := rlp.EncodeToBytes(&tc.acct) + require.NoError(t, err) + assert.Equal(t, hex.EncodeToString(tc.want), hex.EncodeToString(got), + "goshard encoding must match pyquarkchain byte-for-byte") + }) + } +} + +// TestSlimRLPRoundTripEquivalence verifies that for non-MNT accounts (no MntBalances), +// the round-trip slim-RLP → decode → QKC-encode produces the same bytes as direct +// QKC-encode. This is the invariant that allows execute.go to use slim-RLP as the +// accountOrigin format for history reversal without affecting trie root correctness. +// +// Note: SlimAccount does not carry FullShardKey. Both paths therefore encode with +// FullShardKey=0, so the test holds — but the slim-RLP path should only be used +// for accounts where FullShardKey is known to be zero (e.g. freshly created accounts +// before any shard key is set). +func TestSlimRLPRoundTripEquivalence(t *testing.T) { + cases := []struct { + name string + acc StateAccount + }{ + {"zero balance, empty root", StateAccount{Nonce: 0, Balance: new(uint256.Int), Root: EmptyRootHash, CodeHash: EmptyCodeHash.Bytes()}}, + {"nonzero balance, empty root", StateAccount{Nonce: 5, Balance: uint256.NewInt(1_000_000), Root: EmptyRootHash, CodeHash: EmptyCodeHash.Bytes()}}, + {"with storage root", StateAccount{Nonce: 1, Balance: uint256.NewInt(42), Root: common.HexToHash("0xabcdef"), CodeHash: EmptyCodeHash.Bytes()}}, + {"with code hash", StateAccount{Nonce: 99, Balance: uint256.NewInt(999), Root: EmptyRootHash, CodeHash: common.FromHex("0xdeadbeef" + strings.Repeat("00", 28))}}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + // Path A: direct QKC encode + directQKC, err := rlp.EncodeToBytes(&tc.acc) + if err != nil { + t.Fatalf("direct QKC encode: %v", err) + } + // Path B: slim-RLP → FullAccount decode → QKC encode (execute.go path) + slim := SlimAccountRLP(tc.acc) + decoded, err := FullAccount(slim) + if err != nil { + t.Fatalf("FullAccount decode: %v", err) + } + viaSlim, err := rlp.EncodeToBytes(decoded) + if err != nil { + t.Fatalf("QKC encode after slim round-trip: %v", err) + } + if !bytes.Equal(directQKC, viaSlim) { + t.Errorf("mismatch:\n direct QKC: %x\n via slim: %x", directQKC, viaSlim) + } + }) + } +} diff --git a/core/types/token.go b/core/types/token.go new file mode 100644 index 0000000000..b6d1bdf260 --- /dev/null +++ b/core/types/token.go @@ -0,0 +1,200 @@ +// 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 types + +import ( + "fmt" + "sort" + + "github.com/ethereum/go-ethereum/rlp" + "github.com/holiman/uint256" +) + +// TokenTrieThreshold is the maximum number of non-zero token balances supported +// in list format. Accounts exceeding this limit are not supported. +// +// NOTE: goquarkchain supports a trie format (0x01 prefix + 32-byte SecureTrie root) +// for accounts with > 16 MNT tokens. That format is NOT implemented here because: +// 1. The trie format requires a SecureTrie backed by a database, which would +// introduce an import cycle (core/types → trie → core/rawdb → core/types). +// 2. No real mainnet accounts with > 16 MNT tokens have been observed in practice. +// +// If trie-format support becomes necessary, a standalone MPT implementation must +// be added (without importing the trie package) and verified byte-for-byte against +// goquarkchain's SecureTrie output. +const TokenTrieThreshold = 16 + +// listFormatPrefix is the prefix byte for the list serialization format (≤ 16 tokens). +const listFormatPrefix = byte(0x00) + +// trieFormatPrefix is the goquarkchain trie format prefix (> 16 tokens). +// Deserialization of this format returns an error; serialization is unsupported. +const trieFormatPrefix = byte(0x01) + +// DefaultTokenID is the QKC token ID (= TokenIDEncode("QKC") = 35760). +const DefaultTokenID = uint64(35760) + +// TokenBalancePair is a (TokenID, Balance) pair used in list-format encoding. +type TokenBalancePair struct { + TokenID uint64 + Balance *uint256.Int +} + +// TokenBalances holds multi-token balances for an account. +// Serialization uses list format (0x00 prefix) for ≤ TokenTrieThreshold non-zero +// balances. Trie format (> 16 tokens) is not supported; SerializeToBytes returns +// an error if more than TokenTrieThreshold non-zero balances are present. +type TokenBalances struct { + balances map[uint64]*uint256.Int +} + +// NewEmptyTokenBalances creates an empty TokenBalances. +func NewEmptyTokenBalances() *TokenBalances { + return &TokenBalances{ + balances: make(map[uint64]*uint256.Int), + } +} + +// NewTokenBalancesWithMap creates a TokenBalances from a map. +// The provided values are deep-copied. +func NewTokenBalancesWithMap(data map[uint64]*uint256.Int) *TokenBalances { + tb := NewEmptyTokenBalances() + for id, bal := range data { + if bal != nil && !bal.IsZero() { + tb.balances[id] = new(uint256.Int).Set(bal) + } + } + return tb +} + +// NewTokenBalancesFromBytes deserializes a TokenBalances from its serialized form. +// Prefix 0x00 → list format (RLP-decoded, ≤ 16 tokens). +// Prefix 0x01 → trie format (> 16 tokens); NOT SUPPORTED — returns an error. +func NewTokenBalancesFromBytes(data []byte) (*TokenBalances, error) { + if len(data) == 0 { + return NewEmptyTokenBalances(), nil + } + switch data[0] { + case listFormatPrefix: + var pairs []TokenBalancePair + if err := rlp.DecodeBytes(data[1:], &pairs); err != nil { + return nil, fmt.Errorf("token_balances: list decode error: %w", err) + } + tb := NewEmptyTokenBalances() + for _, p := range pairs { + if p.Balance != nil && !p.Balance.IsZero() { + tb.balances[p.TokenID] = new(uint256.Int).Set(p.Balance) + } + } + return tb, nil + + case trieFormatPrefix: + // Trie format (> 16 MNT tokens) is not implemented. + // See TokenTrieThreshold comment for details. + return nil, fmt.Errorf("token_balances: trie format (0x01, >%d tokens) is not supported", TokenTrieThreshold) + + default: + return nil, fmt.Errorf("token_balances: unknown format prefix 0x%02x", data[0]) + } +} + +// SetValue sets the balance for the given tokenID. +// A nil or zero amount removes the entry. +func (t *TokenBalances) SetValue(amount *uint256.Int, tokenID uint64) { + if amount == nil || amount.IsZero() { + delete(t.balances, tokenID) + return + } + t.balances[tokenID] = new(uint256.Int).Set(amount) +} + +// GetTokenBalance returns the balance for the given tokenID. +// Returns a new zero uint256 if not present. +func (t *TokenBalances) GetTokenBalance(tokenID uint64) *uint256.Int { + if bal, ok := t.balances[tokenID]; ok { + return new(uint256.Int).Set(bal) + } + return new(uint256.Int) +} + +// GetBalanceMap returns a copy of the internal balance map. +func (t *TokenBalances) GetBalanceMap() map[uint64]*uint256.Int { + out := make(map[uint64]*uint256.Int, len(t.balances)) + for id, bal := range t.balances { + out[id] = new(uint256.Int).Set(bal) + } + return out +} + +// IsBlank reports whether there are no non-zero balances. +func (t *TokenBalances) IsBlank() bool { + return len(t.balances) == 0 +} + +// Len returns the number of non-zero token balances. +func (t *TokenBalances) Len() int { + return len(t.balances) +} + +// Copy returns a deep copy of the TokenBalances. +func (t *TokenBalances) Copy() *TokenBalances { + cp := NewEmptyTokenBalances() + for id, bal := range t.balances { + cp.balances[id] = new(uint256.Int).Set(bal) + } + return cp +} + +// SerializeToBytes encodes TokenBalances to bytes. +// +// List format (≤ TokenTrieThreshold non-zero balances): +// - Output: 0x00 + RLP([]TokenBalancePair sorted by TokenID ascending) +// +// Trie format (> TokenTrieThreshold) is NOT supported and returns an error. +// See TokenTrieThreshold comment for details. +func (t *TokenBalances) SerializeToBytes() ([]byte, error) { + if t.Len() > TokenTrieThreshold { + // Trie format (0x01) is not implemented. + // See TokenTrieThreshold comment for details. + return nil, fmt.Errorf("token_balances: %d tokens exceeds the supported maximum of %d; trie format (0x01) is not implemented", t.Len(), TokenTrieThreshold) + } + return t.serializeListFormat() +} + +// serializeListFormat encodes in list format: 0x00 + RLP(sorted pairs). +func (t *TokenBalances) serializeListFormat() ([]byte, error) { + pairs := make([]TokenBalancePair, 0, len(t.balances)) + for id, bal := range t.balances { + pairs = append(pairs, TokenBalancePair{ + TokenID: id, + Balance: bal, + }) + } + sort.Slice(pairs, func(i, j int) bool { + return pairs[i].TokenID < pairs[j].TokenID + }) + + encoded, err := rlp.EncodeToBytes(pairs) + if err != nil { + return nil, fmt.Errorf("token_balances: list encode error: %w", err) + } + + out := make([]byte, 1+len(encoded)) + out[0] = listFormatPrefix + copy(out[1:], encoded) + return out, nil +} diff --git a/core/types/token_test.go b/core/types/token_test.go new file mode 100644 index 0000000000..3b6fdfdd19 --- /dev/null +++ b/core/types/token_test.go @@ -0,0 +1,80 @@ +// 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 types + +import ( + "testing" + + "github.com/holiman/uint256" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTokenBalancesListFormat(t *testing.T) { + tb := NewEmptyTokenBalances() + tb.SetValue(uint256.NewInt(1000), DefaultTokenID) + tb.SetValue(uint256.NewInt(500), 100) + + data, err := tb.SerializeToBytes() + require.NoError(t, err) + assert.Equal(t, byte(0x00), data[0], "list format prefix") + + tb2, err := NewTokenBalancesFromBytes(data) + require.NoError(t, err) + assert.Equal(t, uint256.NewInt(1000), tb2.GetTokenBalance(DefaultTokenID)) + assert.Equal(t, uint256.NewInt(500), tb2.GetTokenBalance(100)) +} + +func TestTokenBalancesIsBlank(t *testing.T) { + tb := NewEmptyTokenBalances() + assert.True(t, tb.IsBlank()) + tb.SetValue(uint256.NewInt(0), DefaultTokenID) + assert.True(t, tb.IsBlank(), "zero balance is blank") + tb.SetValue(uint256.NewInt(1), DefaultTokenID) + assert.False(t, tb.IsBlank()) +} + +// TestTokenBalancesTrieFormatUnsupported verifies that serializing more than +// TokenTrieThreshold (16) non-zero token balances returns an error, since the +// trie format (0x01) is not implemented. +func TestTokenBalancesTrieFormatUnsupported(t *testing.T) { + tb := NewEmptyTokenBalances() + for i := uint64(1); i <= 17; i++ { + tb.SetValue(uint256.NewInt(i*100), i) + } + _, err := tb.SerializeToBytes() + require.Error(t, err, "expected error for >16 token balances (trie format unsupported)") +} + +// TestTokenBalancesFromBytesTrieFormatUnsupported verifies that deserializing +// a 0x01-prefixed (trie format) byte slice returns an error. +func TestTokenBalancesFromBytesTrieFormatUnsupported(t *testing.T) { + data := make([]byte, 33) + data[0] = 0x01 // trie format prefix + _, err := NewTokenBalancesFromBytes(data) + require.Error(t, err, "expected error for trie format (0x01) deserialization") +} + +func TestTokenBalancesCopy(t *testing.T) { + tb := NewTokenBalancesWithMap(map[uint64]*uint256.Int{ + DefaultTokenID: uint256.NewInt(1e18), + 100: uint256.NewInt(500), + }) + cp := tb.Copy() + cp.SetValue(uint256.NewInt(0), DefaultTokenID) + assert.Equal(t, uint256.NewInt(1e18), tb.GetTokenBalance(DefaultTokenID), "original unaffected") +} diff --git a/core/types/uint32_rlp.go b/core/types/uint32_rlp.go new file mode 100644 index 0000000000..27a3ad75ec --- /dev/null +++ b/core/types/uint32_rlp.go @@ -0,0 +1,40 @@ +package types + +import ( + "encoding/binary" + "fmt" + "io" + + "github.com/ethereum/go-ethereum/rlp" +) + +type Uint32 uint32 + +const ( + rlpUint32Prefix = byte(0x84) // RLP "string of length 4" + rlpUint32Len = 5 +) + +func (u *Uint32) GetValue() uint32 { return uint32(*u) } + +func (u *Uint32) EncodeRLP(w io.Writer) error { + b := [rlpUint32Len]byte{rlpUint32Prefix} + binary.BigEndian.PutUint32(b[1:], uint32(*u)) + _, err := w.Write(b[:]) + return err +} + +func (u *Uint32) DecodeRLP(s *rlp.Stream) error { + data, err := s.Raw() + if err != nil { + return err + } + if len(data) != rlpUint32Len { + return fmt.Errorf("Uint32 RLP: expected %d bytes, got %d", rlpUint32Len, len(data)) + } + if data[0] != rlpUint32Prefix { + return fmt.Errorf("Uint32 RLP: expected prefix 0x%02x, got 0x%02x", rlpUint32Prefix, data[0]) + } + *u = Uint32(binary.BigEndian.Uint32(data[1:])) + return nil +} diff --git a/core/types/uint32_rlp_test.go b/core/types/uint32_rlp_test.go new file mode 100644 index 0000000000..a4023f7173 --- /dev/null +++ b/core/types/uint32_rlp_test.go @@ -0,0 +1,28 @@ +package types + +import ( + "bytes" + "testing" + + "github.com/ethereum/go-ethereum/rlp" +) + +func TestUint32RLP(t *testing.T) { + v := Uint32(0x12345678) + encoded, err := rlp.EncodeToBytes(&v) + if err != nil { + t.Fatal(err) + } + // Expected: 0x84 + big-endian uint32 + want := []byte{0x84, 0x12, 0x34, 0x56, 0x78} + if !bytes.Equal(encoded, want) { + t.Fatalf("encode: want %x, got %x", want, encoded) + } + var v2 Uint32 + if err := rlp.DecodeBytes(encoded, &v2); err != nil { + t.Fatal(err) + } + if v2 != v { + t.Fatalf("decode: want %d, got %d", v, v2) + } +} diff --git a/params/config.go b/params/config.go index 17508cbf27..b82e7bd73f 100644 --- a/params/config.go +++ b/params/config.go @@ -27,11 +27,14 @@ import ( ) // Genesis hashes to enforce below configs on. +// NOTE: These reflect the QKC (QuarkChain) 6-element account encoding which differs +// from standard Ethereum encoding. The state root and therefore block hash changes +// when the account RLP format changes. var ( - MainnetGenesisHash = common.HexToHash("0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3") - HoleskyGenesisHash = common.HexToHash("0xb5f7f912443c940f21fd611f12828d75b534364ed9e95ca4e307729a4661bde4") - SepoliaGenesisHash = common.HexToHash("0x25a5cc106eea7138acab33231d7160d69cb777ee0c2c553fcddf5138993e6dd9") - HoodiGenesisHash = common.HexToHash("0xbbe312868b376a3001692a646dd2d7d1e4406380dfd86b98aa8a34d1557c971b") + MainnetGenesisHash = common.HexToHash("0x7a86192e8901d313f5ca3af78e13a660aae94173959c69a3f33e0ae5f2749d7c") + HoleskyGenesisHash = common.HexToHash("0x0706dbd8f023baa1808450dd3dfce54335339247b2116c5613e178037b2ccc8f") + SepoliaGenesisHash = common.HexToHash("0xc61eb78385a53483f9de8a5710f91d0921bb4ff968ed482c8955b3a9c6d3ccaf") + HoodiGenesisHash = common.HexToHash("0x21a5ed4cc25beb5aa3e1fe4552e2ff7bcf785aeb6cf0472e1b529bd3346c3ba3") ) func newUint64(val uint64) *uint64 { return &val } @@ -1410,3 +1413,4 @@ func (c *ChainConfig) Rules(num *big.Int, isMerge bool, timestamp uint64) Rules IsEIP4762: isUBT, } } +