diff --git a/qkc/genesis.go b/qkc/genesis.go index 2b3ff8d21b4e..9ee835211362 100644 --- a/qkc/genesis.go +++ b/qkc/genesis.go @@ -171,8 +171,8 @@ func CreateMinorBlock(qkc *config.QuarkChainConfig, fullShardID uint32, root *ty CrossShardGasUsed: &serialize.Uint256{Value: new(big.Int)}, // The cross-shard cursor starts at (root_height, 0, 0), matching // pyquarkchain (quarkchain/genesis.py:92). - XShardTxCursor: types.XShardTxCursorInfo{RootBlockHeight: uint64(root.Number)}, - XShardGasLimit: &serialize.Uint256{Value: big.NewInt(defaultXShardGasLimit)}, + XShardTxCursorInfo: &types.XShardTxCursorInfo{RootBlockHeight: uint64(root.Number)}, + XShardGasLimit: &serialize.Uint256{Value: big.NewInt(defaultXShardGasLimit)}, } header := &types.MinorBlockHeader{ Version: g.Version, @@ -188,7 +188,10 @@ func CreateMinorBlock(qkc *config.QuarkChainConfig, fullShardID uint32, root *ty Difficulty: new(big.Int).SetUint64(g.Difficulty), Extra: bytes.Clone(g.ExtraData), } - return types.NewMinorBlock(header, meta, nil, nil), nil + // NewMinorBlockWithHeader, not NewMinorBlock: the latter derives meta.TxHash + // from the tx list, which would overwrite the HASH_MERKLE_ROOT the genesis + // config pins (and that header.MetaHash above already commits to). + return types.NewMinorBlockWithHeader(header, meta), nil } // ShardChainConfig returns the EVM rule set one shard runs: Petersburg-only, diff --git a/qkc/genesis_test.go b/qkc/genesis_test.go index 7288e3b8d872..4b580c4544cb 100644 --- a/qkc/genesis_test.go +++ b/qkc/genesis_test.go @@ -193,7 +193,7 @@ func TestMinorGenesisGolden(t *testing.T) { if err != nil { t.Fatalf("CreateMinorBlock: %v", err) } - h, m := block.Header, block.Meta + h, m := block.Header(), block.Meta() if h.PrevRootBlockHash != common.HexToHash(want.RootGenesisHash) { t.Errorf("PrevRootBlockHash = %s, want %s", h.PrevRootBlockHash, want.RootGenesisHash) @@ -220,19 +220,19 @@ func TestMinorGenesisGolden(t *testing.T) { MinorBlockIndex: want.XShardCursor[1], XShardDepositIndex: want.XShardCursor[2], } - if m.XShardTxCursor != wantCursor { - t.Errorf("XShardTxCursor = %+v, want %+v", m.XShardTxCursor, wantCursor) + if *m.XShardTxCursorInfo != wantCursor { + t.Errorf("XShardTxCursor = %+v, want %+v", *m.XShardTxCursorInfo, wantCursor) } // The whole point: materializing ALLOC and assembling the block must // reproduce pyquarkchain's genesis block, byte for byte. - if got := block.Meta.Root; got != common.HexToHash(want.StateRoot) { + if got := block.Meta().Root; got != common.HexToHash(want.StateRoot) { t.Errorf("genesis state root\n got %s\nwant %s", got.Hex(), want.StateRoot) } - if got := block.Meta.Hash(); got != common.HexToHash(want.MetaHash) { + if got := block.Meta().Hash(); got != common.HexToHash(want.MetaHash) { t.Errorf("genesis meta hash\n got %s\nwant %s", got.Hex(), want.MetaHash) } - if got := block.Header.MetaHash; got != common.HexToHash(want.MetaHash) { + if got := block.Header().MetaHash; got != common.HexToHash(want.MetaHash) { t.Errorf("header's meta hash\n got %s\nwant %s", got.Hex(), want.MetaHash) } if got := block.Hash(); got != common.HexToHash(want.HeaderHash) { @@ -254,7 +254,7 @@ func TestCreateMinorBlock(t *testing.T) { if err != nil { t.Fatalf("CreateMinorBlock: %v", err) } - h, m, sg := block.Header, block.Meta, shardCfg.Genesis + h, m, sg := block.Header(), block.Meta(), shardCfg.Genesis if h.Branch.GetFullShardID() != firstShardID { t.Errorf("branch = 0x%08x, want 0x%08x", h.Branch.GetFullShardID(), firstShardID) @@ -280,8 +280,8 @@ func TestCreateMinorBlock(t *testing.T) { if h.PrevRootBlockHash != root.Hash() { t.Errorf("PrevRootBlockHash = %s, want the root genesis %s", h.PrevRootBlockHash, root.Hash()) } - if want := (types.XShardTxCursorInfo{RootBlockHeight: uint64(root.Number)}); m.XShardTxCursor != want { - t.Errorf("XShardTxCursor = %+v, want %+v", m.XShardTxCursor, want) + if want := (types.XShardTxCursorInfo{RootBlockHeight: uint64(root.Number)}); *m.XShardTxCursorInfo != want { + t.Errorf("XShardTxCursor = %+v, want %+v", *m.XShardTxCursorInfo, want) } // Meta: the config's merkle root, a materialized state root, and an @@ -320,8 +320,8 @@ func TestCreateMinorBlock(t *testing.T) { } // The genesis body is empty. - if len(block.Transactions) != 0 || len(block.TrackingData) != 0 { - t.Errorf("body = %d txs / %d tracking bytes, want empty", len(block.Transactions), len(block.TrackingData)) + if len(block.Transactions()) != 0 || len(block.TrackingData()) != 0 { + t.Errorf("body = %d txs / %d tracking bytes, want empty", len(block.Transactions()), len(block.TrackingData())) } }) } @@ -346,8 +346,8 @@ func TestCommitGenesisState(t *testing.T) { if err != nil { t.Fatalf("CommitGenesisState: %v", err) } - if stateRoot != block.Meta.Root { - t.Errorf("committed state root = %s, want the derived meta root %s", stateRoot, block.Meta.Root) + if stateRoot != block.Meta().Root { + t.Errorf("committed state root = %s, want the derived meta root %s", stateRoot, block.Meta().Root) } if !rawdb.HasLegacyTrieNode(db, stateRoot) { t.Errorf("state root %s was not written to the database", stateRoot) diff --git a/qkc/shard/rawdb.go b/qkc/shard/rawdb.go index efd0d783c35b..b0dd4b02e3e3 100644 --- a/qkc/shard/rawdb.go +++ b/qkc/shard/rawdb.go @@ -96,7 +96,7 @@ func WriteGenesisBlock(db ethdb.KeyValueWriter, block *types.MinorBlock) error { // another shard's chaindb, a changed config, or a header whose meta and body no // longer match it. func ReconcileGenesisBlock(db ethdb.KeyValueStore, expected *types.MinorBlock, dbPath string) (existed bool, err error) { - fullShardID := expected.Header.Branch.GetFullShardID() + fullShardID := expected.Header().Branch.GetFullShardID() storedData, err := readGenesisBlockBytes(db) if err != nil { return false, fmt.Errorf("shard 0x%08x: read genesis block (db %s): %w", fullShardID, dbPath, err) @@ -118,7 +118,7 @@ func ReconcileGenesisBlock(db ethdb.KeyValueStore, expected *types.MinorBlock, d } // A chaindb holding another shard's genesis is a misplaced directory, not a // config change — name the right cause. - if storedID := stored.Header.Branch.GetFullShardID(); storedID != fullShardID { + if storedID := stored.Header().Branch.GetFullShardID(); storedID != fullShardID { return true, fmt.Errorf("shard 0x%08x: stored genesis belongs to shard 0x%08x (db %s) — misplaced chaindb", fullShardID, storedID, dbPath) } @@ -163,7 +163,7 @@ func ReconcileGenesisBlock(db ethdb.KeyValueStore, expected *types.MinorBlock, d // CheckCompatible's time argument is inert, and the ShardChain seam exposes no // head timestamp to pass. A timestamp-scheduled fork would need both. func ReconcileChainConfig(db ethdb.Database, genesis *types.MinorBlock, cfg *params.ChainConfig, head uint64, existed bool, dbPath string) error { - fullShardID := genesis.Header.Branch.GetFullShardID() + fullShardID := genesis.Header().Branch.GetFullShardID() if cfg == nil { return fmt.Errorf("shard 0x%08x: shard has no chain config (db %s)", fullShardID, dbPath) } diff --git a/qkc/shard/rawdb_test.go b/qkc/shard/rawdb_test.go index e30c3d3b49fd..1ef518dcae29 100644 --- a/qkc/shard/rawdb_test.go +++ b/qkc/shard/rawdb_test.go @@ -63,17 +63,17 @@ func TestGenesisBlockRoundTrip(t *testing.T) { if got.Hash() != want.Hash() { t.Errorf("round-trip hash mismatch: got %s, want %s", got.Hash(), want.Hash()) } - if got.Meta.Hash() != want.Meta.Hash() { - t.Errorf("round-trip meta mismatch: got %s, want %s", got.Meta.Hash(), want.Meta.Hash()) + if got.Meta().Hash() != want.Meta().Hash() { + t.Errorf("round-trip meta mismatch: got %s, want %s", got.Meta().Hash(), want.Meta().Hash()) } - if got.Meta.Root != want.Meta.Root { - t.Errorf("round-trip state root = %s, want %s", got.Meta.Root, want.Meta.Root) + if got.Root() != want.Root() { + t.Errorf("round-trip state root = %s, want %s", got.Root(), want.Root()) } - if got.Header.Branch.GetFullShardID() != firstShardID { - t.Errorf("round-trip branch = 0x%08x, want 0x%08x", got.Header.Branch.GetFullShardID(), firstShardID) + if got.Header().Branch.GetFullShardID() != firstShardID { + t.Errorf("round-trip branch = 0x%08x, want 0x%08x", got.Header().Branch.GetFullShardID(), firstShardID) } - if len(got.Transactions) != 0 || len(got.TrackingData) != 0 { - t.Errorf("round-trip body = %d txs / %d tracking bytes, want empty", len(got.Transactions), len(got.TrackingData)) + if len(got.Transactions()) != 0 || len(got.TrackingData()) != 0 { + t.Errorf("round-trip body = %d txs / %d tracking bytes, want empty", len(got.Transactions()), len(got.TrackingData())) } } @@ -135,9 +135,9 @@ func TestReconcileGenesisBlockTamperedMeta(t *testing.T) { db := rawdb.NewMemoryDatabase() expected := testGenesisBlock(t, fixtureMainnet) - meta := *expected.Meta + meta := *expected.Meta() meta.Root = common.HexToHash("0xdead") - tampered := types.NewMinorBlock(expected.Header, &meta, nil, nil) + tampered := types.NewMinorBlockWithHeader(expected.Header(), &meta) if tampered.Hash() != expected.Hash() { t.Fatal("tampering with the meta moved the block hash: this test no longer covers what it claims") } diff --git a/qkc/shard/shard.go b/qkc/shard/shard.go index 9bde38346e1c..4f2a3e6db5d7 100644 --- a/qkc/shard/shard.go +++ b/qkc/shard/shard.go @@ -128,8 +128,8 @@ func New(ctx *config.SlaveContext, branch account.Branch, rootGenesis *types.Roo // The block was derived from a hash of the same allocation. If the // flushed root disagrees, the chain would open on a genesis whose state // is not the one below it. - if stateRoot != genesis.Meta.Root { - return fmt.Errorf("committed genesis state %s does not match the derived genesis root %s", stateRoot, genesis.Meta.Root) + if stateRoot != genesis.Root() { + return fmt.Errorf("committed genesis state %s does not match the derived genesis root %s", stateRoot, genesis.Root()) } return nil }, @@ -177,7 +177,7 @@ type genesisSetup struct { // the genesis block only once the chain is standing. It stops a constructed chain // on failure; the caller retains ownership of the db. func initializeChain(db ethdb.Database, dbPath string, g genesisSetup, service ChainService) (ShardChain, bool, error) { - fullShardID := g.block.Header.Branch.GetFullShardID() + fullShardID := g.block.Header().Branch.GetFullShardID() existed, err := ReconcileGenesisBlock(db, g.block, dbPath) if err != nil { return nil, false, err @@ -188,7 +188,7 @@ func initializeChain(db ethdb.Database, dbPath string, g genesisSetup, service C if err := g.commit(db); err != nil { return nil, false, fmt.Errorf("shard 0x%08x: commit genesis state (db %s): %w", fullShardID, dbPath, err) } - } else if err := qkc.CheckGenesisState(db, g.block.Meta.Root); err != nil { + } else if err := qkc.CheckGenesisState(db, g.block.Root()); err != nil { // On reopen the state is already there — unless the datadir lost it. The // stored genesis is an identity and says nothing about the trie under it, so // the two are checked separately. Re-materializing here would repair a diff --git a/qkc/shard/shard_test.go b/qkc/shard/shard_test.go index 4f35844c94bb..2be17fdca752 100644 --- a/qkc/shard/shard_test.go +++ b/qkc/shard/shard_test.go @@ -94,8 +94,8 @@ func TestShardNewAndReopen(t *testing.T) { // Deriving the block persists nothing, so the state is here only // because the fresh path flushed it into the shard's own database. - if !rawdb.HasLegacyTrieNode(s.DB(), genesis.Meta.Root) { - t.Errorf("genesis state root %s is missing from the shard db", genesis.Meta.Root) + if !rawdb.HasLegacyTrieNode(s.DB(), genesis.Root()) { + t.Errorf("genesis state root %s is missing from the shard db", genesis.Root()) } // The genesis block is stored, and carries the shard's root linkage @@ -107,10 +107,11 @@ func TestShardNewAndReopen(t *testing.T) { if stored.Hash() != genesis.Hash() { t.Errorf("stored genesis %s, want %s", stored.Hash(), genesis.Hash()) } - if stored.Header.Branch.GetFullShardID() != firstShardID || - stored.Header.PrevRootBlockHash != root.Hash() || - stored.Meta.XShardTxCursor != (types.XShardTxCursorInfo{RootBlockHeight: uint64(root.Number)}) { - t.Errorf("stored genesis %+v inconsistent with config derivation", stored.Header) + cursor := stored.Meta().XShardTxCursorInfo + if stored.Header().Branch.GetFullShardID() != firstShardID || + stored.PrevRootBlockHash() != root.Hash() || cursor == nil || + *cursor != (types.XShardTxCursorInfo{RootBlockHeight: uint64(root.Number)}) { + t.Errorf("stored genesis %+v inconsistent with config derivation", stored.Header()) } if err := s.Stop(); err != nil { @@ -230,7 +231,7 @@ func TestShardReopenMissingGenesisState(t *testing.T) { // Drop the genesis state while leaving the stored genesis block intact. withDB(t, datadir, func(db ethdb.Database) { - rawdb.DeleteLegacyTrieNode(db, genesis.Meta.Root) + rawdb.DeleteLegacyTrieNode(db, genesis.Root()) }) _, err = New(ctx, branch, root, datadir, Options{}) @@ -242,7 +243,7 @@ func TestShardReopenMissingGenesisState(t *testing.T) { // The failed boot did not quietly re-materialize what was lost: a corrupt // datadir stays corrupt until an operator looks at it. withDB(t, datadir, func(db ethdb.Database) { - if rawdb.HasLegacyTrieNode(db, genesis.Meta.Root) { + if rawdb.HasLegacyTrieNode(db, genesis.Root()) { t.Error("the failed reopen rewrote the genesis state instead of reporting corruption") } }) diff --git a/qkc/types/interface.go b/qkc/types/interface.go new file mode 100644 index 000000000000..930d701d7ca6 --- /dev/null +++ b/qkc/types/interface.go @@ -0,0 +1,52 @@ +// Copyright 2026-2027, QuarkChain. + +// QKC block interfaces follow pyquarkchain-compatible type boundaries. + +package types + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/qkc/account" + qkcCommon "github.com/ethereum/go-ethereum/qkc/common" +) + +type IHeader interface { + Hash() common.Hash + SealHash() common.Hash + NumberU64() uint64 + GetVersion() uint32 + GetParentHash() common.Hash + GetCoinbase() account.Address + GetTime() uint64 + GetCoinbaseAmount() *qkcCommon.TokenBalances + GetDifficulty() *big.Int + GetNonce() uint64 + GetExtra() []byte + GetMixDigest() common.Hash +} + +type IBlock interface { + Hash() common.Hash + NumberU64() uint64 + IHeader() IHeader + Content() []IHashable + GetTrackingData() []byte + GetSize() common.StorageSize + ParentHash() common.Hash + Coinbase() account.Address + Time() uint64 + Difficulty() *big.Int +} + +type IHashable interface { + Hash() common.Hash +} + +var ( + _ IHeader = (*MinorBlockHeader)(nil) + _ IHeader = (*RootBlockHeader)(nil) + _ IBlock = (*MinorBlock)(nil) + _ IBlock = (*RootBlock)(nil) +) diff --git a/qkc/types/minorblock.go b/qkc/types/minorblock.go index ae0e1aa4782f..76403f9f769d 100644 --- a/qkc/types/minorblock.go +++ b/qkc/types/minorblock.go @@ -1,116 +1,535 @@ // Copyright 2026-2027, QuarkChain. -// MinorBlockMeta, MinorBlockHeader and MinorBlock mirror goquarkchain's -// core/types, kept to what the shard genesis needs: the fields, their wire order, -// and the hashes. Mining, RLP, copy and accessor helpers are omitted. -// -// Field order and ser tags reproduce pyquarkchain's FIELDS exactly, so -// qkc/serialize encodes them byte-identically: -// -// MinorBlockMeta (quarkchain/core.py:637) -// hash_merkle_root hash256 -// hash_evm_state_root hash256 -// hash_evm_receipt_root hash256 -// evm_gas_used uint256 -// evm_cross_shard_receive_gas_used uint256 -// xshard_tx_cursor_info XshardTxCursorInfo -// evm_xshard_gas_limit uint256 -// -// MinorBlockHeader (quarkchain/core.py:673) -// version uint32 -// branch Branch (uint32) -// height uint64 -// coinbase_address Address (20-byte recipient + uint32 full_shard_key) -// coinbase_amount_map TokenBalanceMap -// hash_prev_minor_block hash256 -// hash_prev_root_block hash256 -// evm_gas_limit uint256 -// hash_meta hash256 -// create_time uint64 -// difficulty biguint -// nonce uint64 -// bloom uint2048 (256 raw bytes) -// extra_data PrependedSizeBytes(2) -// mixhash hash256 -// -// MinorBlock (quarkchain/core.py:745) -// header, meta, tx_list PrependedSizeList(4), tracking_data PrependedSizeBytes(2) - +// Minor blocks follow pyquarkchain-compatible QKC wire encoding. +// Modified from go-ethereum under GNU Lesser General Public License package types import ( "math/big" + "sync/atomic" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/qkc/account" qkcCommon "github.com/ethereum/go-ethereum/qkc/common" + "github.com/ethereum/go-ethereum/qkc/params" "github.com/ethereum/go-ethereum/qkc/serialize" ) -// MinorBlockMeta holds the minor block fields the root chain does not carry. +// MinorBlockHeaderList represents a minor block header in the QuarkChain. +type MinorBlockHeader struct { + Version uint32 `json:"version" gencodec:"required"` + Branch account.Branch `json:"branch" gencodec:"required"` + Number uint64 `json:"number" gencodec:"required"` + Coinbase account.Address `json:"miner" gencodec:"required"` + CoinbaseAmount *qkcCommon.TokenBalances `json:"coinbaseAmount" gencodec:"required"` + ParentHash common.Hash `json:"parentHash" gencodec:"required"` + PrevRootBlockHash common.Hash `json:"prevRootBlockHash" gencodec:"required"` + GasLimit *serialize.Uint256 `json:"gasLimit" gencodec:"required"` + MetaHash common.Hash `json:"metaHash" gencodec:"required"` + Time uint64 `json:"timestamp" gencodec:"required"` + Difficulty *big.Int `json:"difficulty" gencodec:"required"` + Nonce uint64 `json:"nonce"` + Bloom Bloom `json:"logsBloom" gencodec:"required"` + Extra []byte `json:"extraData" gencodec:"required" bytesizeofslicelen:"2"` + MixDigest common.Hash `json:"mixHash"` +} + type MinorBlockMeta struct { - TxHash common.Hash // = pyquarkchain hash_merkle_root - Root common.Hash // EVM state root - ReceiptHash common.Hash - GasUsed *serialize.Uint256 - CrossShardGasUsed *serialize.Uint256 - XShardTxCursor XShardTxCursorInfo - XShardGasLimit *serialize.Uint256 + TxHash common.Hash `json:"transactionsRoot" gencodec:"required"` + Root common.Hash `json:"stateRoot" gencodec:"required"` + ReceiptHash common.Hash `json:"receiptsRoot" gencodec:"required"` + GasUsed *serialize.Uint256 `json:"gasUsed" gencodec:"required"` + CrossShardGasUsed *serialize.Uint256 `json:"crossShardGasUsed" gencodec:"required"` + XShardTxCursorInfo *XShardTxCursorInfo `json:"xShardTxCursorInfo" gencodec:"required"` + XShardGasLimit *serialize.Uint256 `json:"xShardGasLimit" gencodec:"required"` } -// Hash returns keccak256 of the serialized meta — the value the header commits to -// as hash_meta (quarkchain/genesis.py:111). func (m *MinorBlockMeta) Hash() common.Hash { return serHash(*m, nil) } -// MinorBlockHeader is the QuarkChain minor (shard) block header. -type MinorBlockHeader struct { - Version uint32 - Branch account.Branch - Number uint64 // = pyquarkchain height - Coinbase account.Address - CoinbaseAmount *qkcCommon.TokenBalances - ParentHash common.Hash // = pyquarkchain hash_prev_minor_block - PrevRootBlockHash common.Hash - GasLimit *serialize.Uint256 - MetaHash common.Hash - Time uint64 - Difficulty *big.Int - Nonce uint64 - Bloom Bloom - Extra []byte `bytesizeofslicelen:"2"` - MixDigest common.Hash -} - -// Hash returns keccak256 of the full serialized header — the value pyquarkchain -// records as the block hash (MinorBlockHeader.get_hash, quarkchain/core.py:733). +// Hash returns the block hash of the header, which is simply the keccak256 hash of its +// Serialize encoding. func (h *MinorBlockHeader) Hash() common.Hash { return serHash(*h, nil) } -// SealHash returns keccak256 of the header serialized without Nonce and -// MixDigest — pyquarkchain's get_hash_for_mining (the proof-of-work input). +// SealHash returns the block hash of the header, which is keccak256 hash of its +// Serialize encoding for Seal. func (h *MinorBlockHeader) SealHash() common.Hash { - return serHash(*h, map[string]bool{"Nonce": true, "MixDigest": true}) + excludeList := map[string]bool{"MixDigest": true, "Nonce": true} + return serHash(*h, excludeList) +} + +func (h *MinorBlockHeader) GetParentHash() common.Hash { return h.ParentHash } +func (h *MinorBlockHeader) GetPrevRootBlockHash() common.Hash { return h.PrevRootBlockHash } +func (h *MinorBlockHeader) GetCoinbase() account.Address { return h.Coinbase } +func (h *MinorBlockHeader) GetTime() uint64 { return h.Time } +func (h *MinorBlockHeader) GetDifficulty() *big.Int { return new(big.Int).Set(h.Difficulty) } +func (h *MinorBlockHeader) GetNonce() uint64 { return h.Nonce } +func (h *MinorBlockHeader) GetGasLimit() *big.Int { return new(big.Int).Set(h.GasLimit.Value) } +func (h *MinorBlockHeader) GetBranch() account.Branch { return h.Branch } +func (h *MinorBlockHeader) GetMetaHash() common.Hash { return h.MetaHash } +func (h *MinorBlockHeader) GetBloom() Bloom { return h.Bloom } +func (h *MinorBlockHeader) GetMixDigest() common.Hash { return h.MixDigest } +func (h *MinorBlockHeader) NumberU64() uint64 { return h.Number } +func (h *MinorBlockHeader) GetVersion() uint32 { return h.Version } + +func (h *MinorBlockHeader) GetExtra() []byte { + if h.Extra != nil { + return common.CopyBytes(h.Extra) + } + return nil +} + +func (h *MinorBlockHeader) GetCoinbaseAmount() *qkcCommon.TokenBalances { + if h.CoinbaseAmount != nil { + return h.CoinbaseAmount.Copy() + } + return qkcCommon.NewEmptyTokenBalances() +} + +func (h *MinorBlockHeader) SetExtra(data []byte) { h.Extra = common.CopyBytes(data) } +func (h *MinorBlockHeader) SetNonce(nonce uint64) { h.Nonce = nonce } +func (h *MinorBlockHeader) SetCoinbase(addr account.Address) { h.Coinbase = addr } + +// MinorBlockHeaders is a MinorBlockHeaderList slice type for basic sorting. +type MinorBlockHeaders []*MinorBlockHeader + +// Len returns the length of s. +func (s MinorBlockHeaders) Len() int { return len(s) } + +// Swap swaps the i'th and the j'th element in s. +func (s MinorBlockHeaders) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +// Bytes implements DerivableList and returns the i'th element of s in serialize. +func (s MinorBlockHeaders) Bytes(i int) []byte { + enc, err := serialize.SerializeToBytes(s[i]) + if err != nil { + panic(err) + } + return enc +} + +// MinorHeaderDifference returns a new set which is the difference between a and b. +func MinorHeaderDifference(a, b MinorBlockHeaders) MinorBlockHeaders { + keep := make(MinorBlockHeaders, 0, len(a)) + + remove := make(map[common.Hash]struct{}) + for _, header := range b { + remove[header.Hash()] = struct{}{} + } + + for _, header := range a { + if _, ok := remove[header.Hash()]; !ok { + keep = append(keep, header) + } + } + + return keep } -// MinorBlock is a minor block: its header, its meta, and its body. +// MinorBlock represents an entire block in the Ethereum blockchain. type MinorBlock struct { + header *MinorBlockHeader + meta *MinorBlockMeta + transactions Transactions + trackingdata []byte + + // caches + hash atomic.Pointer[common.Hash] + size atomic.Pointer[common.StorageSize] +} + +// "external" block encoding. used for qkc protocol, etc. +type extminorblock struct { Header *MinorBlockHeader Meta *MinorBlockMeta - Transactions []*Transaction `bytesizeofslicelen:"4"` - TrackingData []byte `bytesizeofslicelen:"2"` + Txs Transactions `bytesizeofslicelen:"4"` + Trackingdata []byte `bytesizeofslicelen:"2"` +} + +// NewMinorBlock creates a new block. The header, meta, transaction slice, and +// tracking data are copied. Transactions are shared and must not be mutated +// after being added to the block. +// +// TxHash and ReceiptHash in meta, and Bloom and MetaHash in header, +// are replaced with values derived from the transactions and receipts. +func NewMinorBlock(header *MinorBlockHeader, meta *MinorBlockMeta, txs []*Transaction, receipts []*Receipt, trackingdata []byte) *MinorBlock { + // Every local transaction produces a receipt, while incoming cross-shard + // deposits may add receipts that have no corresponding local transaction. + if len(receipts) < len(txs) { + panic("receipts count is less than txs count") + } + b := &MinorBlock{header: CopyMinorBlockHeader(header), meta: CopyMinorBlockMeta(meta)} + if len(txs) > 0 { + b.transactions = make(Transactions, len(txs)) + copy(b.transactions, txs) + } + b.meta.TxHash = CalculateMerkleRoot(b.transactions) + + b.meta.ReceiptHash = DeriveSha(Receipts(receipts)) + b.header.Bloom = CreateBloom(receipts) + b.header.MetaHash = b.meta.Hash() + + if len(trackingdata) > 0 { + b.trackingdata = make([]byte, len(trackingdata)) + copy(b.trackingdata, trackingdata) + } + + return b +} + +// NewBlockWithHeader creates a block with the given header data. The +// header data is copied, changes to header and to the field values +// will not affect the block. +func NewMinorBlockWithHeader(header *MinorBlockHeader, meta *MinorBlockMeta) *MinorBlock { + return &MinorBlock{header: CopyMinorBlockHeader(header), meta: CopyMinorBlockMeta(meta)} +} + +// CopyHeader creates a deep copy of a block header to prevent side effects from +// modifying a header variable. +func CopyMinorBlockHeader(h *MinorBlockHeader) *MinorBlockHeader { + cpy := *h + if cpy.Difficulty = new(big.Int); h.Difficulty != nil { + cpy.Difficulty.Set(h.Difficulty) + } + if h.CoinbaseAmount != nil { + cpy.CoinbaseAmount = h.CoinbaseAmount.Copy() + } + if cpy.GasLimit = new(serialize.Uint256); h.GasLimit != nil && h.GasLimit.Value != nil { + cpy.GasLimit.Value = new(big.Int).Set(h.GasLimit.Value) + } + if len(h.Extra) > 0 { + cpy.Extra = make([]byte, len(h.Extra)) + copy(cpy.Extra, h.Extra) + } + + return &cpy //todo verify the copy for struct +} + +func CopyMinorBlockMeta(m *MinorBlockMeta) *MinorBlockMeta { + cpy := *m + if cpy.GasUsed = new(serialize.Uint256); m.GasUsed != nil && m.GasUsed.Value != nil { + cpy.GasUsed.Value = new(big.Int).Set(m.GasUsed.Value) + } + if cpy.CrossShardGasUsed = new(serialize.Uint256); m.CrossShardGasUsed != nil && m.CrossShardGasUsed.Value != nil { + cpy.CrossShardGasUsed.Value = new(big.Int).Set(m.CrossShardGasUsed.Value) + } + if cpy.XShardGasLimit = new(serialize.Uint256); m.XShardGasLimit != nil && m.XShardGasLimit.Value != nil { + cpy.XShardGasLimit.Value = new(big.Int).Set(m.XShardGasLimit.Value) + } + if m.XShardTxCursorInfo != nil { + cursor := *m.XShardTxCursorInfo + cpy.XShardTxCursorInfo = &cursor + } + return &cpy +} + +// Deserialize deserialize the QKC minor block +func (b *MinorBlock) Deserialize(bb *serialize.ByteBuffer) error { + var eb extminorblock + startIndex := bb.GetOffset() + if err := serialize.Deserialize(bb, &eb); err != nil { + return err + } + b.header, b.meta, b.transactions, b.trackingdata = eb.Header, eb.Meta, eb.Txs, eb.Trackingdata + b.hash.Store(nil) + size := common.StorageSize(bb.GetOffset() - startIndex) + b.size.Store(&size) + return nil } -// NewMinorBlock assembles a minor block. The block's identity is its header's -// hash; the body is committed to through the meta's merkle root. -func NewMinorBlock(header *MinorBlockHeader, meta *MinorBlockMeta, txs []*Transaction, trackingData []byte) *MinorBlock { - return &MinorBlock{Header: header, Meta: meta, Transactions: txs, TrackingData: trackingData} +// Serialize serialize the QKC minor block. +func (b *MinorBlock) Serialize(w *[]byte) error { + offset := len(*w) + if err := serialize.Serialize(w, extminorblock{ + Header: b.header, + Meta: b.meta, + Txs: b.transactions, + Trackingdata: b.trackingdata, + }); err != nil { + return err + } + + size := common.StorageSize(len(*w) - offset) + b.size.Store(&size) + return nil +} + +// Transactions returns the block's transaction slice without copying because +// transactions are treated as immutable once shared with a block. +func (b *MinorBlock) Transactions() Transactions { return b.transactions } + +func (b *MinorBlock) Transaction(hash common.Hash) *Transaction { + for _, transaction := range b.transactions { + if transaction.Hash() == hash { + return transaction + } + } + return nil } -// Hash returns the block hash: the header's hash. -func (b *MinorBlock) Hash() common.Hash { return b.Header.Hash() } +func (b *MinorBlock) TrackingData() []byte { return common.CopyBytes(b.trackingdata) } + +// header properties +func (b *MinorBlock) GetXShardGasLimit() *big.Int { + return new(big.Int).Set(b.meta.XShardGasLimit.Value) +} +func (b *MinorBlock) Version() uint32 { return b.header.Version } +func (b *MinorBlock) Branch() account.Branch { return b.header.Branch } +func (b *MinorBlock) Number() uint64 { return b.header.Number } +func (b *MinorBlock) Coinbase() account.Address { return b.header.Coinbase } +func (b *MinorBlock) ParentHash() common.Hash { return b.header.ParentHash } +func (b *MinorBlock) PrevRootBlockHash() common.Hash { return b.header.PrevRootBlockHash } +func (b *MinorBlock) GasLimit() *big.Int { return new(big.Int).Set(b.header.GasLimit.Value) } +func (b *MinorBlock) MetaHash() common.Hash { return b.header.MetaHash } +func (b *MinorBlock) Time() uint64 { return b.header.Time } +func (b *MinorBlock) Difficulty() *big.Int { return new(big.Int).Set(b.header.Difficulty) } +func (b *MinorBlock) Nonce() uint64 { return b.header.Nonce } +func (b *MinorBlock) Extra() []byte { return common.CopyBytes(b.header.Extra) } +func (b *MinorBlock) Bloom() Bloom { return b.header.Bloom } +func (b *MinorBlock) MixDigest() common.Hash { return b.header.MixDigest } +func (b *MinorBlock) CoinbaseAmount() *qkcCommon.TokenBalances { return b.header.GetCoinbaseAmount() } -// NumberU64 returns the block height. -func (b *MinorBlock) NumberU64() uint64 { return b.Header.Number } +// meta properties +func (b *MinorBlock) Root() common.Hash { return b.meta.Root } +func (b *MinorBlock) TxHash() common.Hash { return b.meta.TxHash } +func (b *MinorBlock) ReceiptHash() common.Hash { return b.meta.ReceiptHash } +func (b *MinorBlock) GasUsed() *big.Int { return new(big.Int).Set(b.meta.GasUsed.Value) } +func (b *MinorBlock) CrossShardGasUsed() *big.Int { + return new(big.Int).Set(b.meta.CrossShardGasUsed.Value) +} + +func (b *MinorBlock) Header() *MinorBlockHeader { return CopyMinorBlockHeader(b.header) } +func (b *MinorBlock) Meta() *MinorBlockMeta { return CopyMinorBlockMeta(b.meta) } + +// Size returns the true RLP encoded storage size of the block, either by encoding +// and returning it, or returning a previsouly cached value. +func (b *MinorBlock) Size() common.StorageSize { + if size := b.size.Load(); size != nil { + return *size + } + + bytes, err := serialize.SerializeToBytes(b) + if err != nil { + panic(err) + } + size := common.StorageSize(len(bytes)) + b.size.Store(&size) + return size +} + +// WithSeal returns a new block with the data from b but the header replaced with +// the sealed one. +func (b *MinorBlock) WithSeal(header *MinorBlockHeader) *MinorBlock { + return &MinorBlock{ + header: CopyMinorBlockHeader(header), + meta: CopyMinorBlockMeta(b.meta), + transactions: b.transactions, + trackingdata: common.CopyBytes(b.trackingdata), + } +} + +// WithBody returns a new block with the given transactions and tracking data. +func (b *MinorBlock) WithBody(transactions []*Transaction, trackingData []byte) *MinorBlock { + block := &MinorBlock{ + header: CopyMinorBlockHeader(b.header), + meta: CopyMinorBlockMeta(b.meta), + transactions: make(Transactions, len(transactions)), + trackingdata: make([]byte, len(trackingData)), + } + copy(block.transactions, transactions) + copy(block.trackingdata, trackingData) + return block +} + +// Hash returns the keccak256 hash of b's header. +// The hash is computed on the first call and cached thereafter. +func (b *MinorBlock) Hash() common.Hash { + if hash := b.hash.Load(); hash != nil { + return *hash + } + v := b.header.Hash() + b.hash.Store(&v) + return v +} + +func (b *MinorBlock) NumberU64() uint64 { + return b.header.Number +} + +func (b *MinorBlock) IHeader() IHeader { + return CopyMinorBlockHeader(b.header) +} + +// WithMiningResult returns a new block with the data from b and update nonce and mixDigest. +// +// signature is ignored because minor block headers carry no signature field. +func (b *MinorBlock) WithMiningResult(nonce uint64, mixDigest common.Hash, signature *[65]byte) *MinorBlock { + cpy := CopyMinorBlockHeader(b.header) + cpy.Nonce = nonce + cpy.MixDigest = mixDigest + + return b.WithSeal(cpy) +} + +func (b *MinorBlock) Content() []IHashable { + items := make([]IHashable, len(b.transactions)) + for i, item := range b.transactions { + items[i] = item + } + return items +} + +func (b *MinorBlock) GetMetaData() *MinorBlockMeta { + return b.Meta() +} + +func (b *MinorBlock) GetTrackingData() []byte { + return b.TrackingData() +} + +func (b *MinorBlock) GetSize() common.StorageSize { + return b.Size() +} + +func (m *MinorBlock) Finalize(receipts Receipts, rootHash common.Hash, gasUsed *big.Int, xShardReceiveGasUsed *big.Int, coinbaseAmount *qkcCommon.TokenBalances, xShardTxCursorInfo *XShardTxCursorInfo) { + if len(receipts) < len(m.transactions) { + panic("receipts count is less than txs count") + } + if gasUsed == nil { + gasUsed = new(big.Int) + } + if xShardReceiveGasUsed == nil { + xShardReceiveGasUsed = new(big.Int) + } + + if xShardTxCursorInfo == nil { + m.meta.XShardTxCursorInfo = &XShardTxCursorInfo{} + } else { + cursor := *xShardTxCursorInfo + m.meta.XShardTxCursorInfo = &cursor + } + m.meta.Root = rootHash + m.meta.GasUsed = &serialize.Uint256{Value: new(big.Int).Set(gasUsed)} + m.meta.CrossShardGasUsed = &serialize.Uint256{Value: new(big.Int).Set(xShardReceiveGasUsed)} + if coinbaseAmount == nil { + m.header.CoinbaseAmount = nil + } else { + m.header.CoinbaseAmount = coinbaseAmount.Copy() + } + m.meta.TxHash = CalculateMerkleRoot(m.transactions) + m.meta.ReceiptHash = DeriveSha(receipts) + m.header.MetaHash = m.meta.Hash() + m.header.Bloom = CreateBloom(receipts) + hash := m.header.Hash() + m.hash.Store(&hash) + m.size.Store(nil) +} + +func (h *MinorBlock) CreateBlockToAppend(createTime *uint64, difficulty *big.Int, address *account.Address, nonce *uint64, gasLimit *big.Int, xShardGasLimit *big.Int, extraData []byte, coinbaseAmount *qkcCommon.TokenBalances, prevRootHash *common.Hash) *MinorBlock { + if createTime == nil { + preTime := h.Time() + 1 + createTime = &preTime + } + + if difficulty == nil { + difficulty = h.Difficulty() + } + + if address == nil { + emptyAddress := account.CreatEmptyAddress(h.header.Coinbase.FullShardKey) + address = &emptyAddress + } + + if nonce == nil { + zeroNonce := uint64(0) + nonce = &zeroNonce + } + + if gasLimit == nil { + gasLimit = h.GasLimit() + } + + if xShardGasLimit == nil { + xShardGasLimit = new(big.Int).Div(gasLimit, new(big.Int).SetUint64(2)) + } + + if extraData == nil { + extraData = make([]byte, 0) + } + + if coinbaseAmount == nil { + coinbaseAmount = qkcCommon.NewEmptyTokenBalances() + } + + if prevRootHash == nil { + preHash := h.PrevRootBlockHash() + prevRootHash = &preHash + } + header := &MinorBlockHeader{ + Version: h.Version(), + Number: h.Number() + 1, + Branch: h.Branch(), + Coinbase: *address, + CoinbaseAmount: coinbaseAmount.Copy(), + ParentHash: h.Hash(), + PrevRootBlockHash: *prevRootHash, + GasLimit: &serialize.Uint256{Value: new(big.Int).Set(gasLimit)}, + Time: *createTime, + Difficulty: new(big.Int).Set(difficulty), + Nonce: *nonce, + Extra: common.CopyBytes(extraData), + } + var cursor *XShardTxCursorInfo + if h.meta.XShardTxCursorInfo != nil { + cpy := *h.meta.XShardTxCursorInfo + cursor = &cpy + } + meta := MinorBlockMeta{ + GasUsed: &serialize.Uint256{Value: new(big.Int)}, + CrossShardGasUsed: &serialize.Uint256{Value: new(big.Int)}, + XShardTxCursorInfo: cursor, + XShardGasLimit: &serialize.Uint256{Value: new(big.Int).Set(xShardGasLimit)}, + } + return &MinorBlock{ + header: header, + meta: &meta, + trackingdata: []byte{}, + } +} + +// AddTx appends to the body without touching meta or header, so it leaves +// meta.TxHash, header.MetaHash, and any Hash() already cached stale. Callers +// must Finalize before relying on Hash(); Finalize recomputes both hashes and +// refreshes the cache. +func (h *MinorBlock) AddTx(tx *Transaction) { + h.transactions = append(h.transactions, tx) + h.hash.Store(nil) + h.size.Store(nil) +} + +func GetEmptyMinorBlock() *MinorBlock { + return NewMinorBlockWithHeader(getDefaultMinorBlockHeader(), getDefaultMinorBlockMeta()) +} + +func getDefaultMinorBlockHeader() *MinorBlockHeader { + return &MinorBlockHeader{ + CoinbaseAmount: qkcCommon.NewEmptyTokenBalances(), + Branch: account.Branch{Value: 1}, + GasLimit: &serialize.Uint256{Value: params.DefaultBlockGasLimit}, + Difficulty: new(big.Int).SetUint64(0), + } +} + +func getDefaultMinorBlockMeta() *MinorBlockMeta { + return &MinorBlockMeta{ + GasUsed: &serialize.Uint256{Value: new(big.Int)}, + CrossShardGasUsed: &serialize.Uint256{Value: new(big.Int)}, + XShardTxCursorInfo: &XShardTxCursorInfo{}, + XShardGasLimit: &serialize.Uint256{ + Value: new(big.Int).Div(params.DefaultBlockGasLimit, big.NewInt(2)), + }, + } +} diff --git a/qkc/types/minorblock_test.go b/qkc/types/minorblock_test.go index ed7b0ddc0630..fa4be37b9306 100644 --- a/qkc/types/minorblock_test.go +++ b/qkc/types/minorblock_test.go @@ -1,21 +1,316 @@ // Copyright 2026-2027, QuarkChain. -// Minor block body tests pin the tx_list wire segment against pyquarkchain. +// Minor block tests exercise pyquarkchain-compatible QKC wire bytes. package types import ( "bytes" + "encoding/hex" "math/big" + "reflect" "testing" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/qkc/account" qkcCommon "github.com/ethereum/go-ethereum/qkc/common" "github.com/ethereum/go-ethereum/qkc/serialize" "github.com/holiman/uint256" ) +// testU256 mirrors the helper in qkc/common's tests: TokenBalances balances are +// *uint256.Int, so comparing a decoded balance needs one built the same way. +func testU256(v uint64) *uint256.Int { + return uint256.NewInt(v) +} + +var ( + // reciept, _ = account.BytesToIdentityRecipient(common.Hex2Bytes("b94f5374fce5edbc8e2a8697c15331677e6ebf0b")) + tx1 = NewEvmTransaction( + 0, + reciept, + big.NewInt(0), 0, big.NewInt(0), + 0, 0, 1, 0, nil, 0, 0, + ) + //nonce , to , amount , gasLimit , gasPrice, fromFullShardKey , toFullShardKey , networkId , version , data + tx2 = NewEvmTransaction( + 3, + reciept, + big.NewInt(10), + 2000, + big.NewInt(1), + 0, + 0, + 1, + 0, + nil, 0, 0, + ) +) + +// from bcValidBlockTest.json, "SimpleTx" +func TestMinorBlockHeaderSerializing(t *testing.T) { + blocHeaderEnc := common.FromHex("00000001000000010000000000000002d3f86deb4a2bbf85048b3e790460c40dbab1f621000003ff00000002010101010102010200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000030000000000000005010600000000000000070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100030102030000000000000000000000000000000000000000000000000000000000000004") + var blockHeader MinorBlockHeader + bb := serialize.NewByteBuffer(blocHeaderEnc) + if err := serialize.Deserialize(bb, &blockHeader); err != nil { + t.Fatal("Deserialize error: ", err) + } + + bytes, err := serialize.SerializeToBytes(&blockHeader) + if err != nil { + t.Fatal("Serialize error: ", err) + } + + check := func(f string, got, want interface{}) { + if !reflect.DeepEqual(got, want) { + t.Errorf("%s mismatch: got %v, want %v", f, got, want) + } + } + + check("Version", blockHeader.Version, uint32(1)) + check("Height", blockHeader.Number, uint64(2)) + check("Branch", blockHeader.Branch.Value, uint32(1)) + check("coinbase_Recipient", blockHeader.Coinbase.Recipient[:], common.FromHex("d3f86deb4a2bbf85048b3e790460c40dbab1f621")) + check("coinbase_FullShardKey", uint32(blockHeader.Coinbase.FullShardKey), uint32(0x000003ff)) + check("CoinbaseAmount", blockHeader.CoinbaseAmount.GetBalanceMap()[1], testU256(1)) + check("CoinbaseAmount", blockHeader.CoinbaseAmount.GetBalanceMap()[2], testU256(2)) + check("CoinbaseAmount", len(blockHeader.CoinbaseAmount.GetBalanceMap()), 2) + check("ParentHash", blockHeader.ParentHash, common.HexToHash("0000000000000000000000000000000000000000000000000000000000000001")) + check("PrevRootBlockHash", blockHeader.PrevRootBlockHash, common.HexToHash("0000000000000000000000000000000000000000000000000000000000000002")) + check("GasLimit", blockHeader.GasLimit.Value.Uint64(), uint64(4)) + check("MetaHash", blockHeader.MetaHash, common.HexToHash("0000000000000000000000000000000000000000000000000000000000000003")) + check("Time", blockHeader.Time, uint64(5)) + check("Difficulty", blockHeader.Difficulty, big.NewInt(6)) + check("Nonce", blockHeader.Nonce, uint64(7)) + check("Bloom", common.Bytes2Hex(blockHeader.Bloom[:]), "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001") + check("Extra", common.Bytes2Hex(blockHeader.Extra), "010203") + check("MixDigest", common.Bytes2Hex(blockHeader.MixDigest.Bytes()), "0000000000000000000000000000000000000000000000000000000000000004") + check("Hash", common.Bytes2Hex(blockHeader.Hash().Bytes()), "b0b7dfab9a8f485ea97a4642cdd380182ede101a64ecb3e73eb211496153d869") + check("serialize", hex.EncodeToString(bytes), hex.EncodeToString(blocHeaderEnc)) + + blocMetaEnc := common.FromHex("a40920ae6f758f88c61b405f9fc39fdd6274666462b14e3887522166e6537a97297d6ae9803346cdb059a671dea7e37b684dcabfa767f2d872026ad0a3aba495df227f34313c2bc4a4a986817ea46437f049873f2fca8e2b89b1ecd0f9e67a280000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000012c0000000000000001000000000000000200000000000000030000000000000000000000000000000000000000000000000000000000000190") + var blockMeta MinorBlockMeta + bb = serialize.NewByteBuffer(blocMetaEnc) + if err := serialize.Deserialize(bb, &blockMeta); err != nil { + t.Fatal("Deserialize error: ", err) + } + + bytes, err = serialize.SerializeToBytes(blockMeta) + if err != nil { + t.Fatal("Serialize error: ", err) + } + + check("TxHash", blockMeta.TxHash[:], common.FromHex("a40920ae6f758f88c61b405f9fc39fdd6274666462b14e3887522166e6537a97")) + check("Root", blockMeta.Root[:], common.FromHex("297d6ae9803346cdb059a671dea7e37b684dcabfa767f2d872026ad0a3aba495")) + check("ReceiptHash", blockMeta.ReceiptHash[:], common.FromHex("df227f34313c2bc4a4a986817ea46437f049873f2fca8e2b89b1ecd0f9e67a28")) + check("GasUsed", *blockMeta.GasUsed, serialize.Uint256{Value: big.NewInt(100)}) + check("CrossShardGasUsed", *blockMeta.CrossShardGasUsed, serialize.Uint256{Value: big.NewInt(300)}) + check("xshard_tx_cursor_info", blockMeta.XShardTxCursorInfo.RootBlockHeight, uint64(1)) + check("xshard_tx_cursor_info", blockMeta.XShardTxCursorInfo.MinorBlockIndex, uint64(2)) + check("xshard_tx_cursor_info", blockMeta.XShardTxCursorInfo.XShardDepositIndex, uint64(3)) + check("evm_xshard_gas_limit", blockMeta.XShardGasLimit.Value.Uint64(), uint64(400)) + check("bmserialize", bytes, blocMetaEnc) + + signer := NewQKCSigner(1, 1) + key, _ := crypto.HexToECDSA("45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8") + transactionsEnc := common.FromHex("00000002000000006df86b80808094b94f5374fce5edbc8e2a8697c15331677e6ebf0b808001840000000084000000008080801ba0d7265f92d763da5e2ea5016b837bf56f5bf42d22aead9ad5e7be2ddf01efcc68a07159634972d77349a76108c6db0634ea7b65768881b152c656deca190df6e427000000006ff86d03018207d094b94f5374fce5edbc8e2a8697c15331677e6ebf0b0a8001840000000084000000008080801ba01e681d99a80f28640faa7e224823dd133ffbd59731e3c7009f4375134a4bd58ea0089addb6d4ca918d12471682a9e5f9d03f0738358a72e493a075519cb07cf34f") + var trans Transactions + bb = serialize.NewByteBuffer(transactionsEnc) + if err := serialize.DeserializeWithTags(bb, &trans, serialize.Tags{ByteSizeOfSliceLen: 4}); err != nil { + t.Fatal("Deserialize error: ", err) + } + + bytes = nil + err = serialize.SerializeWithTags(&bytes, trans, serialize.Tags{ByteSizeOfSliceLen: 4}) + if err != nil { + t.Fatal("Serialize error: ", err) + } + + tx1, _ = SignTx(tx1, signer, key) + tx2, _ = SignTx(tx2, signer, key) + check("len(Transactions)", len(trans), 2) + check("Transactions[0].Hash", common.Bytes2Hex(trans[0].Hash().Bytes()), common.Bytes2Hex(tx1.Hash().Bytes())) + check("Transactions[1]", common.Bytes2Hex(trans[1].Hash().Bytes()), common.Bytes2Hex(tx2.Hash().Bytes())) + check("txserialize", common.Bytes2Hex(bytes), common.Bytes2Hex(transactionsEnc)) + + blockEnc := append(blocHeaderEnc, append(blocMetaEnc, append(transactionsEnc, common.Hex2Bytes("00020102")...)...)...) + var block MinorBlock + bb = serialize.NewByteBuffer(blockEnc) + if err := serialize.Deserialize(bb, &block); err != nil { + t.Fatal("Deserialize error: ", err) + } + + bytes, err = serialize.SerializeToBytes(&block) + if err != nil { + t.Fatal("Serialize error: ", err) + } + + check("header", block.header, &blockHeader) + check("meta", block.meta, &blockMeta) + check("transactions", block.transactions.Len(), trans.Len()) + check("transactions[0]", block.transactions[0].Hash(), trans[0].Hash()) + check("transactions[1]", block.transactions[1].Hash(), trans[1].Hash()) + check("trackingdata", common.Bytes2Hex(block.trackingdata), "0102") + check("blockhash", common.Bytes2Hex(block.Hash().Bytes()), "b0b7dfab9a8f485ea97a4642cdd380182ede101a64ecb3e73eb211496153d869") + check("serialize", common.Bytes2Hex(bytes), common.Bytes2Hex(blockEnc)) + +} + +func TestMinorBlockMetaCopyDoesNotAliasCursor(t *testing.T) { + cursor := &XShardTxCursorInfo{RootBlockHeight: 1, MinorBlockIndex: 2, XShardDepositIndex: 3} + meta := getDefaultMinorBlockMeta() + meta.XShardTxCursorInfo = cursor + copy := CopyMinorBlockMeta(meta) + copy.XShardTxCursorInfo.RootBlockHeight = 9 + if cursor.RootBlockHeight != 1 { + t.Fatalf("copy mutated source cursor: got %d, want 1", cursor.RootBlockHeight) + } +} + +func TestMinorBlockHeaderGetGasLimitReturnsCopy(t *testing.T) { + header, _ := testMinorBlockHeader() + gasLimit := header.GetGasLimit() + gasLimit.SetUint64(1) + if got, want := header.GasLimit.Value.Uint64(), uint64(12_000_000); got != want { + t.Fatalf("GetGasLimit exposed the header value: got %d, want %d", got, want) + } +} + +func TestCreateBlockToAppendCopiesInputs(t *testing.T) { + header, meta := testMinorBlockHeader() + parent := NewMinorBlockWithHeader(header, meta) + difficulty := big.NewInt(2) + gasLimit := big.NewInt(3) + extraData := []byte{4} + coinbaseAmount := qkcCommon.NewEmptyTokenBalances() + coinbaseAmount.SetValue(uint256.NewInt(5), 1) + child := parent.CreateBlockToAppend(nil, difficulty, nil, nil, gasLimit, nil, extraData, coinbaseAmount, nil) + wantHash := child.Hash() + + difficulty.SetInt64(6) + gasLimit.SetInt64(7) + extraData[0] = 8 + coinbaseAmount.SetValue(uint256.NewInt(9), 1) + if child.Difficulty().Cmp(big.NewInt(2)) != 0 || child.GasLimit().Cmp(big.NewInt(3)) != 0 { + t.Fatal("child block retained caller-owned big integers") + } + if got, want := child.GetXShardGasLimit(), big.NewInt(1); got.Cmp(want) != 0 { + t.Fatalf("child block cross-shard gas limit = %v, want %v", got, want) + } + if got := child.Extra()[0]; got != 4 { + t.Fatalf("child block retained caller-owned extra data: got %d, want 4", got) + } + if got := child.CoinbaseAmount().GetBalanceMap()[1]; got.Cmp(uint256.NewInt(5)) != 0 { + t.Fatalf("child block retained caller-owned coinbase amount: got %v, want 5", got) + } + if child.Hash() != wantHash || child.Header().Hash() != wantHash { + t.Fatal("caller input mutation changed the child header") + } + + child.meta.XShardTxCursorInfo.RootBlockHeight = 9 + if got, want := parent.meta.XShardTxCursorInfo.RootBlockHeight, uint64(1); got != want { + t.Fatalf("child block cursor mutated parent: got height %d, want %d", got, want) + } +} + +func TestMinorBlockReceiptCount(t *testing.T) { + header, meta := testMinorBlockHeader() + tx := goldenTxs()[0] + receipt := NewReceipt(false, 0) + + // Incoming cross-shard deposits may add receipts beyond the local tx count. + NewMinorBlock(header, meta, []*Transaction{tx}, []*Receipt{receipt, receipt}, nil) + + tests := []struct { + name string + call func() + }{ + { + name: "NewMinorBlock", + call: func() { NewMinorBlock(header, meta, []*Transaction{tx}, nil, nil) }, + }, + { + name: "Finalize", + call: func() { + block := NewMinorBlockWithHeader(header, meta).WithBody([]*Transaction{tx}, nil) + block.Finalize(nil, common.Hash{}, nil, nil, nil, nil) + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatalf("%s accepted fewer receipts than transactions", test.name) + } + }() + test.call() + }) + } +} + +func TestEmptyMinorBlockDefaults(t *testing.T) { + block := GetEmptyMinorBlock() + if block.GasUsed().Sign() != 0 || block.CrossShardGasUsed().Sign() != 0 { + t.Fatalf("empty block gas used = %v/%v, want 0/0", block.GasUsed(), block.CrossShardGasUsed()) + } + if cursor := block.Meta().XShardTxCursorInfo; cursor == nil || *cursor != (XShardTxCursorInfo{}) { + t.Fatalf("empty block cursor = %#v, want zero cursor", cursor) + } + if got, want := block.GetXShardGasLimit(), big.NewInt(6_000_000); got.Cmp(want) != 0 { + t.Fatalf("empty block cross-shard gas limit = %v, want %v", got, want) + } + if block.TxHash() != (common.Hash{}) || block.ReceiptHash() != (common.Hash{}) || block.MetaHash() != (common.Hash{}) { + t.Fatalf("empty block derived non-default hashes: tx=%s receipt=%s meta=%s", block.TxHash(), block.ReceiptHash(), block.MetaHash()) + } +} + +func TestCalculateMerkleRoot(t *testing.T) { + encList := [][]byte{ + common.FromHex("00000001000000010000000000000002d3f86deb4a2bbf85048b3e790460c40dbab1f621000003ff00000002010101010102010200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000030000000000000005010600000000000000070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100030102030000000000000000000000000000000000000000000000000000000000000004"), + common.FromHex("0000000100000001000000000000006fd3f86deb4a2bbf85048b3e790460c40dbab1f621000003ff00000002010101010102010200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000030000000000000005010600000000000000070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100030102030000000000000000000000000000000000000000000000000000000000000004"), + } + list := make([]*MinorBlockHeader, 0) + for _, bytes := range encList { + var blockHeader MinorBlockHeader + bb := serialize.NewByteBuffer(bytes) + if err := serialize.Deserialize(bb, &blockHeader); err != nil { + t.Fatal("Deserialize error: ", err) + } + list = append(list, &blockHeader) + } + check := func(f string, got, want interface{}) { + if !reflect.DeepEqual(got, want) { + t.Errorf("%s mismatch: got %v, want %v", f, got, want) + } + } + + check("header", list[0].Hash().Hex(), "0xb0b7dfab9a8f485ea97a4642cdd380182ede101a64ecb3e73eb211496153d869") + check("header", list[1].Hash().Hex(), "0xc1eaf394ed0b62b881e163c5399ad6342e753e72a6f585cc75a18b06dd45a59c") + check("merkleRootHash", CalculateMerkleRoot(list).Hex(), "0xf175a1f35419972b352b2e2a7bbba6a6ade1c5a59da57114b23438bd3dbf82f2") +} + +func TestNewMinorBlockEmptyDerivedFields(t *testing.T) { + header, meta := testMinorBlockHeader() + header.Bloom[0] = 1 + block := NewMinorBlock(header, meta, nil, nil, nil) + wantTxRoot := common.HexToHash("0xdaa77426c30c02a43d9fba4e841a6556c524d47030762eb14dc4af897e605d9b") + if got := block.TxHash(); got != wantTxRoot { + t.Fatalf("empty transaction root mismatch: got %s, want %s", got, wantTxRoot) + } + wantReceiptRoot := common.HexToHash("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421") + if got := block.ReceiptHash(); got != wantReceiptRoot { + t.Fatalf("empty receipt root mismatch: got %s, want %s", got, wantReceiptRoot) + } + if got := block.Bloom(); got != (Bloom{}) { + t.Fatalf("empty receipt bloom mismatch: got %x", got) + } + if got, want := block.MetaHash(), block.Meta().Hash(); got != want { + t.Fatalf("meta hash mismatch: got %s, want %s", got, want) + } +} + // Golden bytes and hashes below come from pyquarkchain, over the two // transactions goldenTxs builds: // @@ -62,13 +357,13 @@ func goldenTxs() []*Transaction { // on zero-value substitutes. func testMinorBlockHeader() (*MinorBlockHeader, *MinorBlockMeta) { meta := &MinorBlockMeta{ - TxHash: common.HexToHash("0x01"), - Root: common.HexToHash("0x02"), - ReceiptHash: common.HexToHash("0x03"), - GasUsed: &serialize.Uint256{Value: big.NewInt(21000)}, - CrossShardGasUsed: &serialize.Uint256{Value: new(big.Int)}, - XShardTxCursor: XShardTxCursorInfo{RootBlockHeight: 1}, - XShardGasLimit: &serialize.Uint256{Value: big.NewInt(6000000)}, + TxHash: common.HexToHash("0x01"), + Root: common.HexToHash("0x02"), + ReceiptHash: common.HexToHash("0x03"), + GasUsed: &serialize.Uint256{Value: big.NewInt(21000)}, + CrossShardGasUsed: &serialize.Uint256{Value: new(big.Int)}, + XShardTxCursorInfo: &XShardTxCursorInfo{RootBlockHeight: 1}, + XShardGasLimit: &serialize.Uint256{Value: big.NewInt(6000000)}, } header := &MinorBlockHeader{ Version: 0, @@ -117,7 +412,7 @@ func TestMinorBlockTxListEncoding(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - block := NewMinorBlock(header, meta, test.txs, nil) + block := NewMinorBlockWithHeader(header, meta).WithBody(test.txs, nil) got, err := serialize.SerializeToBytes(block) if err != nil { t.Fatalf("serialize block: %v", err) @@ -137,7 +432,7 @@ func TestMinorBlockTxListRoundTrip(t *testing.T) { header, meta := testMinorBlockHeader() want := goldenTxs() - enc, err := serialize.SerializeToBytes(NewMinorBlock(header, meta, want, []byte("tracking"))) + enc, err := serialize.SerializeToBytes(NewMinorBlockWithHeader(header, meta).WithBody(want, []byte("tracking"))) if err != nil { t.Fatalf("serialize block: %v", err) } @@ -149,13 +444,13 @@ func TestMinorBlockTxListRoundTrip(t *testing.T) { if got.Hash() != header.Hash() { t.Errorf("block hash = %s, want %s", got.Hash(), header.Hash()) } - if string(got.TrackingData) != "tracking" { - t.Errorf("TrackingData = %q, want %q", got.TrackingData, "tracking") + if string(got.TrackingData()) != "tracking" { + t.Errorf("TrackingData = %q, want %q", got.TrackingData(), "tracking") } - if len(got.Transactions) != len(want) { - t.Fatalf("decoded %d transactions, want %d", len(got.Transactions), len(want)) + if len(got.Transactions()) != len(want) { + t.Fatalf("decoded %d transactions, want %d", len(got.Transactions()), len(want)) } - for i, tx := range got.Transactions { + for i, tx := range got.Transactions() { if tx.Type() != EvmTxType { t.Errorf("tx %d: type = %d, want %d", i, tx.Type(), EvmTxType) } @@ -182,3 +477,88 @@ func TestTypedTransactionHash(t *testing.T) { } } } + +func TestMinorBlockMutationInvalidatesCaches(t *testing.T) { + header, meta := testMinorBlockHeader() + block := NewMinorBlockWithHeader(header, meta) + originalHash := block.Hash() + block.Size() + + block.Header().SetNonce(1) + if block.Hash() != originalHash { + t.Fatal("Header exposed the block's internal header") + } + sealedHeader := block.Header() + sealed := block.WithSeal(sealedHeader) + sealedHeader.Difficulty.SetInt64(2) + if sealed.Difficulty().Cmp(big.NewInt(1_000_000)) != 0 { + t.Fatal("WithSeal retained mutable header fields") + } + + block.AddTx(goldenTxs()[0]) + receipts := Receipts{NewReceipt(false, 0)} + if block.hash.Load() != nil { + t.Fatal("AddTx did not clear the hash cache") + } + if block.size.Load() != nil { + t.Fatal("AddTx did not clear the size cache") + } + block.Size() + block.Finalize(receipts, common.Hash{}, nil, nil, nil, nil) + if block.size.Load() != nil { + t.Fatal("Finalize did not clear the size cache") + } + if cursor := block.Meta().XShardTxCursorInfo; cursor == nil || *cursor != (XShardTxCursorInfo{}) { + t.Fatalf("Finalize nil cursor = %#v, want zero cursor", cursor) + } + + cursor := &XShardTxCursorInfo{RootBlockHeight: 1, MinorBlockIndex: 2, XShardDepositIndex: 3} + block.Finalize(receipts, common.Hash{}, nil, nil, nil, cursor) + wantMetaHash := block.MetaHash() + wantHash := block.Hash() + cursor.RootBlockHeight = 9 + if got := block.Meta().XShardTxCursorInfo.RootBlockHeight; got != 1 { + t.Fatalf("Finalize retained caller's cursor: got height %d, want 1", got) + } + if block.MetaHash() != wantMetaHash || block.Hash() != wantHash { + t.Fatal("caller cursor mutation changed finalized block hashes") + } + + gasUsed := big.NewInt(11) + xShardGasUsed := big.NewInt(12) + coinbaseAmount := qkcCommon.NewEmptyTokenBalances() + coinbaseAmount.SetValue(uint256.NewInt(13), 1) + block.Finalize(receipts, common.Hash{}, gasUsed, xShardGasUsed, coinbaseAmount, nil) + wantMetaHash = block.MetaHash() + wantHash = block.Hash() + gasUsed.SetInt64(21) + xShardGasUsed.SetInt64(22) + coinbaseAmount.SetValue(uint256.NewInt(23), 1) + if block.GasUsed().Cmp(big.NewInt(11)) != 0 || block.CrossShardGasUsed().Cmp(big.NewInt(12)) != 0 { + t.Fatal("Finalize retained caller-owned gas values") + } + if got := block.CoinbaseAmount().GetBalanceMap()[1]; got.Cmp(uint256.NewInt(13)) != 0 { + t.Fatalf("Finalize retained caller-owned coinbase amount: got %v, want 13", got) + } + if block.MetaHash() != wantMetaHash || block.Hash() != wantHash { + t.Fatal("caller finalization input mutation changed finalized block hashes") + } +} + +func TestMinorBlockDeserializeClearsHash(t *testing.T) { + header, meta := testMinorBlockHeader() + block := NewMinorBlockWithHeader(header, meta) + oldHash := block.Hash() + + header.Nonce++ + encoded, err := serialize.SerializeToBytes(NewMinorBlockWithHeader(header, meta)) + if err != nil { + t.Fatal(err) + } + if err := serialize.DeserializeFromBytes(encoded, block); err != nil { + t.Fatal(err) + } + if block.Hash() == oldHash || block.Hash() != header.Hash() { + t.Fatal("Deserialize retained the previous hash cache") + } +} diff --git a/qkc/types/rootblock.go b/qkc/types/rootblock.go index 8b445fb6fcf2..e1af5baea170 100644 --- a/qkc/types/rootblock.go +++ b/qkc/types/rootblock.go @@ -1,65 +1,255 @@ // Copyright 2026-2027, QuarkChain. -// RootBlockHeader mirrors goquarkchain's core/types.RootBlockHeader, kept minimal: -// only the fields and the Hash/SealHash needed to derive and pin the root genesis -// block. Mining, signing, and RLP helpers are omitted. -// TODO: more content need to be added later. +// Root blocks follow pyquarkchain-compatible QKC wire encoding. +// Modified from go-ethereum under GNU Lesser General Public License // -// Field order and ser tags reproduce pyquarkchain's RootBlockHeader.FIELDS -// (quarkchain/core.py:888) exactly, so qkc/serialize encodes it byte-identically: -// -// version uint32 -// height uint32 -// hash_prev_block hash256 (32 raw bytes) -// hash_merkle_root hash256 -// hash_evm_state_root hash256 -// coinbase_address Address (20-byte recipient + uint32 full_shard_key) -// coinbase_amount_map TokenBalanceMap -// create_time uint64 -// difficulty biguint (1-byte length prefix + big-endian bytes) -// total_difficulty biguint -// nonce uint64 -// extra_data PrependedSizeBytes(2) -// mixhash hash256 -// signature FixedSizeBytes(65) +// NOTE: ported as a passive dependency of the shardchain code only — the root +// chain itself (RootBlockChain, root consensus/mining) is NOT implemented here. package types import ( "math/big" + "sync/atomic" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/qkc/account" qkcCommon "github.com/ethereum/go-ethereum/qkc/common" + "github.com/ethereum/go-ethereum/qkc/serialize" ) -// RootBlockHeader is the QuarkChain root block header. Only the subset of behavior -// needed to derive, hash, and seal-hash the root genesis is implemented. +// RootBlockHeader represents a root block header in the QuarkChain. type RootBlockHeader struct { - Version uint32 - Number uint32 - ParentHash common.Hash - MinorHeaderHash common.Hash - Root common.Hash - Coinbase account.Address - CoinbaseAmount *qkcCommon.TokenBalances - Time uint64 - Difficulty *big.Int - TotalDifficulty *big.Int - Nonce uint64 - Extra []byte `bytesizeofslicelen:"2"` - MixDigest common.Hash - Signature [65]byte -} - -// Hash returns keccak256 of the full serialized header — the value pyquarkchain -// records as the block hash (RootBlockHeader.get_hash, quarkchain/core.py:938). + Version uint32 `json:"version" gencodec:"required"` + Number uint32 `json:"number" gencodec:"required"` + ParentHash common.Hash `json:"parentHash" gencodec:"required"` + MinorHeaderHash common.Hash `json:"transactionsRoot" gencodec:"required"` + Root common.Hash `json:"root" gencodec:"required"` + Coinbase account.Address `json:"miner" gencodec:"required"` + CoinbaseAmount *qkcCommon.TokenBalances `json:"coinbaseAmount" gencodec:"required"` + Time uint64 `json:"timestamp" gencodec:"required"` + Difficulty *big.Int `json:"difficulty" gencodec:"required"` + TotalDifficulty *big.Int `json:"total_difficulty" gencodec:"required"` + Nonce uint64 `json:"nonce"` + Extra []byte `json:"extraData" gencodec:"required" bytesizeofslicelen:"2"` + MixDigest common.Hash `json:"mixHash"` + Signature [65]byte `json:"signature" gencodec:"required"` +} + +// Hash returns the block hash of the header, which is simply the keccak256 hash of its +// Serialize encoding. func (h *RootBlockHeader) Hash() common.Hash { return serHash(*h, nil) } -// SealHash returns keccak256 of the header serialized without Nonce, MixDigest, -// and Signature — pyquarkchain's get_hash_for_mining (the proof-of-work input). +// SealHash returns the block hash of the header, which is keccak256 hash of its +// Serialize encoding for Seal. func (h *RootBlockHeader) SealHash() common.Hash { - return serHash(*h, map[string]bool{"Nonce": true, "MixDigest": true, "Signature": true}) + return serHash(*h, map[string]bool{"Signature": true, "MixDigest": true, "Nonce": true}) +} + +func (h *RootBlockHeader) GetParentHash() common.Hash { return h.ParentHash } +func (h *RootBlockHeader) GetCoinbase() account.Address { return h.Coinbase } + +func (h *RootBlockHeader) GetTime() uint64 { return h.Time } +func (h *RootBlockHeader) GetDifficulty() *big.Int { return new(big.Int).Set(h.Difficulty) } +func (h *RootBlockHeader) GetTotalDifficulty() *big.Int { return new(big.Int).Set(h.TotalDifficulty) } +func (h *RootBlockHeader) GetNonce() uint64 { return h.Nonce } +func (h *RootBlockHeader) GetExtra() []byte { + if h.Extra != nil { + return common.CopyBytes(h.Extra) + } + return nil +} + +func (b *RootBlockHeader) GetCoinbaseAmount() *qkcCommon.TokenBalances { + if b.CoinbaseAmount != nil && b.CoinbaseAmount.GetBalanceMap() != nil { + return qkcCommon.NewTokenBalancesWithMap(b.CoinbaseAmount.GetBalanceMap()) + } + return qkcCommon.NewEmptyTokenBalances() +} + +func (h *RootBlockHeader) GetMixDigest() common.Hash { return h.MixDigest } + +func (h *RootBlockHeader) NumberU64() uint64 { return uint64(h.Number) } + +func (h *RootBlockHeader) GetVersion() uint32 { return h.Version } + +// Block represents an entire block in the QuarkChain. +type RootBlock struct { + header *RootBlockHeader + minorBlockHeaders MinorBlockHeaders + trackingdata []byte + + // caches + hash atomic.Pointer[common.Hash] + size atomic.Pointer[common.StorageSize] +} + +func (b *RootBlock) IHeader() IHeader { + return CopyRootBlockHeader(b.header) +} + +// "external" block encoding. used for eth protocol, etc. +type extrootblock struct { + Header *RootBlockHeader + MinorBlockHeaders MinorBlockHeaders `bytesizeofslicelen:"4"` + Trackingdata []byte `bytesizeofslicelen:"2"` +} + +// NewRootBlock creates a root block and copies all inputs. MinorHeaderHash in +// the copied header is replaced with the commitment derived from mbHeaders. +func NewRootBlock(header *RootBlockHeader, mbHeaders MinorBlockHeaders, trackingdata []byte) *RootBlock { + b := &RootBlock{header: CopyRootBlockHeader(header)} + + b.header.MinorHeaderHash = CalculateMerkleRoot(MinorBlockHeaders(mbHeaders)) + if len(mbHeaders) > 0 { + b.minorBlockHeaders = make(MinorBlockHeaders, len(mbHeaders)) + for i, header := range mbHeaders { + b.minorBlockHeaders[i] = CopyMinorBlockHeader(header) + } + } + if trackingdata != nil && len(trackingdata) > 0 { + b.trackingdata = make([]byte, len(trackingdata)) + copy(b.trackingdata, trackingdata) + } + + return b +} + +// NewBlockWithHeader creates a block with the given header data. The +// header data is copied, changes to header and to the field values +// will not affect the block. +func NewRootBlockWithHeader(header *RootBlockHeader) *RootBlock { + return &RootBlock{header: CopyRootBlockHeader(header)} +} + +// CopyRootHeader creates a deep copy of a block header to prevent side effects from +// modifying a header variable. +func CopyRootBlockHeader(h *RootBlockHeader) *RootBlockHeader { + cpy := *h + if h.CoinbaseAmount != nil && h.CoinbaseAmount.GetBalanceMap() != nil { + cpy.CoinbaseAmount = h.CoinbaseAmount.Copy() + } + if cpy.Difficulty = new(big.Int); h.Difficulty != nil { + cpy.Difficulty.Set(h.Difficulty) + } + if cpy.TotalDifficulty = new(big.Int); h.TotalDifficulty != nil { + cpy.TotalDifficulty.Set(h.TotalDifficulty) + } + if len(h.Extra) > 0 { + cpy.Extra = make([]byte, len(h.Extra)) + copy(cpy.Extra, h.Extra) + } + cpy.Signature = [65]byte{} + copy(cpy.Signature[:], h.Signature[:]) + + return &cpy +} + +// Deserialize deserialize the QKC root block +func (b *RootBlock) Deserialize(bb *serialize.ByteBuffer) error { + var eb extrootblock + startIndex := bb.GetOffset() + if err := serialize.Deserialize(bb, &eb); err != nil { + return err + } + b.header, b.minorBlockHeaders, b.trackingdata = eb.Header, eb.MinorBlockHeaders, eb.Trackingdata + b.hash.Store(nil) + size := common.StorageSize(bb.GetOffset() - startIndex) + b.size.Store(&size) + return nil +} + +// Serialize serialize the QKC root block. +func (b *RootBlock) Serialize(w *[]byte) error { + offset := len(*w) + if err := serialize.Serialize(w, extrootblock{ + Header: b.header, + MinorBlockHeaders: b.minorBlockHeaders, + Trackingdata: b.trackingdata, + }); err != nil { + return err + } + + size := common.StorageSize(len(*w) - offset) + b.size.Store(&size) + return nil +} + +func (b *RootBlock) MinorBlockHeaders() MinorBlockHeaders { + headers := make(MinorBlockHeaders, len(b.minorBlockHeaders)) + for i, header := range b.minorBlockHeaders { + headers[i] = CopyMinorBlockHeader(header) + } + return headers +} + +func (b *RootBlock) MinorBlockHeader(hash common.Hash) *MinorBlockHeader { + for _, minorBlockHeader := range b.minorBlockHeaders { + if minorBlockHeader.Hash() == hash { + return CopyMinorBlockHeader(minorBlockHeader) + } + } + + return nil +} + +func (b *RootBlock) TrackingData() []byte { return common.CopyBytes(b.trackingdata) } + +func (b *RootBlock) Version() uint32 { return b.header.Version } +func (b *RootBlock) Number() uint32 { return b.header.Number } +func (b *RootBlock) NumberU64() uint64 { return uint64(b.header.Number) } +func (b *RootBlock) ParentHash() common.Hash { return b.header.ParentHash } +func (b *RootBlock) MinorHeaderHash() common.Hash { return b.header.MinorHeaderHash } +func (b *RootBlock) Coinbase() account.Address { return b.header.Coinbase } +func (b *RootBlock) CoinbaseAmount() *qkcCommon.TokenBalances { return b.header.GetCoinbaseAmount() } +func (b *RootBlock) Time() uint64 { return b.header.Time } +func (b *RootBlock) Difficulty() *big.Int { return new(big.Int).Set(b.header.Difficulty) } +func (b *RootBlock) TotalDifficulty() *big.Int { return new(big.Int).Set(b.header.TotalDifficulty) } +func (b *RootBlock) Nonce() uint64 { return b.header.Nonce } +func (b *RootBlock) Extra() []byte { return common.CopyBytes(b.header.Extra) } +func (b *RootBlock) MixDigest() common.Hash { return b.header.MixDigest } +func (b *RootBlock) Signature() [65]byte { return b.header.Signature } + +func (b *RootBlock) Header() *RootBlockHeader { return CopyRootBlockHeader(b.header) } +func (b *RootBlock) Content() []IHashable { + items := make([]IHashable, len(b.minorBlockHeaders), len(b.minorBlockHeaders)) + for i, item := range b.minorBlockHeaders { + items[i] = CopyMinorBlockHeader(item) + } + return items +} + +func (b *RootBlock) Size() common.StorageSize { + if size := b.size.Load(); size != nil { + return *size + } + + bytes, err := serialize.SerializeToBytes(b) + if err != nil { + panic(err) + } + size := common.StorageSize(len(bytes)) + b.size.Store(&size) + return size +} + +// Hash returns the keccak256 hash of b's header. +// The hash is computed on the first call and cached thereafter. +func (b *RootBlock) Hash() common.Hash { + if hash := b.hash.Load(); hash != nil { + return *hash + } + v := b.header.Hash() + b.hash.Store(&v) + return v +} + +func (b *RootBlock) GetTrackingData() []byte { + return common.CopyBytes(b.trackingdata) +} + +func (b *RootBlock) GetSize() common.StorageSize { + return b.Size() } diff --git a/qkc/types/rootblock_test.go b/qkc/types/rootblock_test.go index 1eb70e961557..cc986423f685 100644 --- a/qkc/types/rootblock_test.go +++ b/qkc/types/rootblock_test.go @@ -1,10 +1,13 @@ // Copyright 2026-2027, QuarkChain. +// Root block tests exercise pyquarkchain-compatible QKC wire bytes. + package types import ( "encoding/hex" "math/big" + "reflect" "testing" "github.com/ethereum/go-ethereum/common" @@ -14,6 +17,196 @@ import ( "github.com/holiman/uint256" ) +func TestRootBlockEncoding(t *testing.T) { + rootBlockHeaderEnc := common.FromHex("0000000100000002a40920ae6f758f88c61b405f9fc39fdd6274666462b14e3887522166e6537a97297d6ae9803346cdb059a671dea7e37b684dcabfa767f2d872026ad0a3aba4950000000000000000000000000000000000000000000000000000000000000000d3f86deb4a2bbf85048b3e790460c40dbab1f621000003ff00000002010101010102010200000000009896800227100227100000000000000064000401020304df227f34313c2bc4a4a986817ea46437f049873f2fca8e2b89b1ecd0f9e67a28c758a15769202219b1fce50049eeac1af1dddb28bc282c1fb79a2208fa24f763308b1b191d656a5123ac979067a6c941867f3000d978a5d34810fe6c194dc38101") + var blockHeader RootBlockHeader + bb := serialize.NewByteBuffer(rootBlockHeaderEnc) + if err := serialize.Deserialize(bb, &blockHeader); err != nil { + t.Fatal("Deserialize error: ", err) + } + + bytes, err := serialize.SerializeToBytes(&blockHeader) + + if err != nil { + t.Fatal("Serialize error: ", err) + } + + check := func(f string, got, want interface{}) { + if !reflect.DeepEqual(got, want) { + t.Errorf("%s mismatch: got %v, want %v", f, got, want) + } + } + check("Version", blockHeader.Version, uint32(1)) + check("Number", blockHeader.Number, uint32(2)) + check("ParentHash", common.Bytes2Hex(blockHeader.ParentHash.Bytes()), "a40920ae6f758f88c61b405f9fc39fdd6274666462b14e3887522166e6537a97") + check("MinorHeaderHash", common.Bytes2Hex(blockHeader.MinorHeaderHash.Bytes()), "297d6ae9803346cdb059a671dea7e37b684dcabfa767f2d872026ad0a3aba495") + check("coinbase_Recipient", common.Bytes2Hex(blockHeader.Coinbase.Recipient[:]), "d3f86deb4a2bbf85048b3e790460c40dbab1f621") + check("coinbase_FullShardKey", uint32(blockHeader.Coinbase.FullShardKey), uint32(1023)) + check("CoinbaseAmount", blockHeader.CoinbaseAmount.GetBalanceMap()[1], testU256(1)) + check("CoinbaseAmount", blockHeader.CoinbaseAmount.GetBalanceMap()[2], testU256(2)) + check("Time", blockHeader.Time, uint64(10000000)) + check("Difficulty", blockHeader.Difficulty, big.NewInt(10000)) + check("TotalDifficulty", blockHeader.TotalDifficulty, big.NewInt(10000)) + check("Nonce", blockHeader.Nonce, uint64(100)) + check("Extra", common.Bytes2Hex(blockHeader.Extra), "01020304") + check("MixDigest", common.Bytes2Hex(blockHeader.MixDigest.Bytes()), "df227f34313c2bc4a4a986817ea46437f049873f2fca8e2b89b1ecd0f9e67a28") + check("Hash", common.Bytes2Hex(blockHeader.Hash().Bytes()), "725576c58f70f22166767d41d50fd1e22d2913524f967bf1a7fc020cb0e19b10") + check("Hash", common.Bytes2Hex(blockHeader.Hash().Bytes()), "725576c58f70f22166767d41d50fd1e22d2913524f967bf1a7fc020cb0e19b10") + check("serialize", common.Bytes2Hex(bytes), common.Bytes2Hex(rootBlockHeaderEnc)) + + minorBlockHeadersEnc := common.FromHex("0000000200000457000000010000000000002b67d3f86deb4a2bbf85048b3e790460c40dbab1f621000003ff0000000201010101010201020000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000003000000000000000501060000000000000007000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010003010203000000000000000000000000000000000000000000000000000000000000000400000457000000010000000000a98ac7d3f86deb4a2bbf85048b3e790460c40dbab1f621000003ff00000002010101010102010200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000030000000000000005010600000000000000070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100030102030000000000000000000000000000000000000000000000000000000000000004") + var headers MinorBlockHeaders + bb = serialize.NewByteBuffer(minorBlockHeadersEnc) + if err := serialize.DeserializeWithTags(bb, &headers, serialize.Tags{ByteSizeOfSliceLen: 4}); err != nil { + t.Fatal("Deserialize error: ", err) + } + + bytes = nil + err = serialize.SerializeWithTags(&bytes, headers, serialize.Tags{ByteSizeOfSliceLen: 4}) + if err != nil { + t.Fatal("Serialize error: ", err) + } + + check("len(headers)", len(headers), 2) + check("headers[0].Hash", common.Bytes2Hex(headers[0].Hash().Bytes()), "cfe6b217b566f12e7568d46c47de85d13193902eafb8f39d9d56ae725cf11f7f") + check("headers[1].Hash", common.Bytes2Hex(headers[1].Hash().Bytes()), "1245f631e4ce43188fd9412d1fcab34db8c62f5728d0d54550d1a0dc67617f01") + check("serialize", common.Bytes2Hex(bytes), common.Bytes2Hex(minorBlockHeadersEnc)) + + blockEnc := append(rootBlockHeaderEnc, append(minorBlockHeadersEnc, common.Hex2Bytes("00020102")...)...) + var block RootBlock + bb = serialize.NewByteBuffer(blockEnc) + if err := serialize.Deserialize(bb, &block); err != nil { + t.Fatal("Deserialize error: ", err) + } + + bytes, err = serialize.SerializeToBytes(&block) + if err != nil { + t.Fatal("Serialize error: ", err) + } + + check("header", block.header, &blockHeader) + check("headers", block.minorBlockHeaders.Len(), headers.Len()) + check("headers[0]", block.minorBlockHeaders[0].Hash(), headers[0].Hash()) + check("headers[1]", block.minorBlockHeaders[1].Hash(), headers[1].Hash()) + check("trackingdata", common.Bytes2Hex(block.trackingdata), "0102") + check("Signature", common.Bytes2Hex(blockHeader.Signature[:]), "c758a15769202219b1fce50049eeac1af1dddb28bc282c1fb79a2208fa24f763308b1b191d656a5123ac979067a6c941867f3000d978a5d34810fe6c194dc38101") + check("blockhash", common.Bytes2Hex(block.Hash().Bytes()), "725576c58f70f22166767d41d50fd1e22d2913524f967bf1a7fc020cb0e19b10") + check("serialize", common.Bytes2Hex(bytes), common.Bytes2Hex(blockEnc)) + +} + +func TestNewRootBlockEmptyMinorHeaderRoot(t *testing.T) { + block := NewRootBlock(&RootBlockHeader{}, nil, nil) + want := common.HexToHash("0xdaa77426c30c02a43d9fba4e841a6556c524d47030762eb14dc4af897e605d9b") + if got := block.MinorHeaderHash(); got != want { + t.Fatalf("empty minor header root mismatch: got %s, want %s", got, want) + } +} + +func TestRootBlockCopiesBody(t *testing.T) { + minorHeader, _ := testMinorBlockHeader() + trackingData := []byte{1, 2, 3} + block := NewRootBlock(&RootBlockHeader{}, MinorBlockHeaders{minorHeader}, trackingData) + wantHash := block.MinorBlockHeaders()[0].Hash() + + minorHeader.Time++ + trackingData[0] = 9 + if got := block.MinorBlockHeaders()[0].Hash(); got != wantHash { + t.Fatal("NewRootBlock retained the caller's minor header") + } + if got := block.TrackingData(); got[0] != 1 { + t.Fatal("NewRootBlock retained the caller's tracking data") + } + + headers := block.MinorBlockHeaders() + headers[0].Time++ + returnedTrackingData := block.TrackingData() + returnedTrackingData[0] = 9 + if got := block.MinorBlockHeaders()[0].Hash(); got != wantHash { + t.Fatal("MinorBlockHeaders exposed the block's internal header") + } + if got := block.TrackingData(); got[0] != 1 { + t.Fatal("TrackingData exposed the block's internal data") + } +} + +func TestRootBlockDeserializeClearsHash(t *testing.T) { + header := &RootBlockHeader{ + CoinbaseAmount: qkcCommon.NewEmptyTokenBalances(), + Difficulty: big.NewInt(1), + TotalDifficulty: big.NewInt(1), + } + block := NewRootBlockWithHeader(header) + oldHash := block.Hash() + + header.Nonce++ + encoded, err := serialize.SerializeToBytes(NewRootBlockWithHeader(header)) + if err != nil { + t.Fatal(err) + } + if err := serialize.DeserializeFromBytes(encoded, block); err != nil { + t.Fatal(err) + } + if block.Hash() == oldHash || block.Hash() != header.Hash() { + t.Fatal("Deserialize retained the previous hash cache") + } +} + +func TestDataSize(t *testing.T) { + check := func(f string, got, want interface{}) { + if !reflect.DeepEqual(got, want) { + t.Errorf("%s mismatch: got %v, want %v", f, got, want) + } + } + var rootBlockHeader RootBlockHeader + rootBlockHeaderBytes, err := serialize.SerializeToBytes(&rootBlockHeader) + + if err != nil { + t.Fatal("Serialize error: ", err) + } + var minorBlockHeader MinorBlockHeader + minorBlockHeaderBytes, err := serialize.SerializeToBytes(&minorBlockHeader) + if err != nil { + t.Fatal("Serialize error: ", err) + } + var minorBlockMeta MinorBlockMeta + minorBlockMetaBytes, err := serialize.SerializeToBytes(&minorBlockMeta) + if err != nil { + t.Fatal("Serialize error: ", err) + } + + check("RootBlockHeader", len(rootBlockHeaderBytes), 249) + check("MinorBlockHeader", len(minorBlockHeaderBytes), 479) + check("MinorBlockMeta", len(minorBlockMetaBytes), 216) +} + +/* +Py code to generate data: + + header=RootBlockHeader() + header.version=1 + header.height=2 + header.hash_prev_block=bytes.fromhex("a40920ae6f758f88c61b405f9fc39fdd6274666462b14e3887522166e6537a97") + header.hash_merkle_root=bytes.fromhex("297d6ae9803346cdb059a671dea7e37b684dcabfa767f2d872026ad0a3aba495") + header.coinbase_address=Address.create_from(bytes.fromhex("d3f86deb4a2bbf85048b3e790460c40dbab1f621000003ff")) + header.coinbase_amount=1000 + header.create_time=10000000 + header.difficulty=10000 + header.total_difficulty=10000 + header.nonce=100 + header.extra_data=bytes.fromhex("01020304") + header.mixhash=bytes.fromhex("df227f34313c2bc4a4a986817ea46437f049873f2fca8e2b89b1ecd0f9e67a28") + privkey = KeyAPI.PrivateKey( + private_key_bytes=bytes.fromhex("c987d4506fb6824639f9a9e3b8834584f5165e94680501d1b0044071cd36c3b3") + ) + + header.sign_with_private_key(privkey) + data=header.serialize() + print("data",len(data),data.hex()) + print("hash",header.get_hash().hex()) + print("sigb",header.signature.hex()) +*/ + // Golden values produced by pyquarkchain (the compatibility source of truth). See // qkc/config/singularity/README.md for the regeneration one-liner; the synthetic // case is built directly in quarkchain.core.RootBlockHeader with every field @@ -117,13 +310,3 @@ func TestRootBlockHeaderSerializeAndHash(t *testing.T) { }) } } - -func TestEmptyTokenBalancesSerialize(t *testing.T) { - var w []byte - if err := qkcCommon.NewEmptyTokenBalances().Serialize(&w); err != nil { - t.Fatalf("serialize: %v", err) - } - if h := hex.EncodeToString(w); h != "00000000" { - t.Fatalf("empty map serialize = %s, want 00000000", h) - } -}