fix: XDC mainnet genesis hash compatibility

Fixes the genesis hash mismatch with official XDPoSChain:
- Fixed corrupted contract bytecode in genesis file (2 extra hex chars)
- Added XDPoS fields (Validators, Validator, Penalties) to RLP encoder
- Initialize XDPoS fields to empty slices in genesis construction
- Updated XDCMainnetGenesisHash to correct value
- Added XDC mainnet bootnodes

Genesis hash now matches official: 0x4a9d748bd78a8d0385b67788c2435dcdb914f98a96250b68863a1f8b7642d6b1
State root: 0x49be235b0098b048f9805aed38a279d8c189b469ff9ba307b39c7ad3a3bc55ae

See docs/GENESIS_COMPATIBILITY_FIX.md for full technical details.
This commit is contained in:
anilchinchawale 2026-01-29 05:44:43 +01:00
parent e40a3414cc
commit 9b24b71513
7 changed files with 265 additions and 9 deletions

View file

@ -497,6 +497,10 @@ func (g *Genesis) toBlockWithRoot(root common.Hash) *types.Block {
MixDigest: g.Mixhash,
Coinbase: g.Coinbase,
Root: root,
// XDPoS fields - initialize to empty slices (not nil) for RLP compatibility
Validators: []byte{},
Validator: []byte{},
Penalties: []byte{},
}
if g.GasLimit == 0 {
head.GasLimit = params.GenesisGasLimit

View file

@ -81,10 +81,10 @@ type Header struct {
MixDigest common.Hash `json:"mixHash"`
Nonce BlockNonce `json:"nonce"`
// XDPoS fields for validator consensus
Validators []byte `json:"validators" rlp:"optional"`
Validator []byte `json:"validator" rlp:"optional"`
Penalties []byte `json:"penalties" rlp:"optional"`
// XDPoS fields for validator consensus (always encoded, not optional)
Validators []byte `json:"validators"`
Validator []byte `json:"validator"`
Penalties []byte `json:"penalties"`
// BaseFee was added by EIP-1559 and is ignored in legacy headers.
BaseFee *big.Int `json:"baseFeePerGas" rlp:"optional"`

View file

@ -37,6 +37,10 @@ func (obj *Header) EncodeRLP(_w io.Writer) error {
w.WriteBytes(obj.Extra)
w.WriteBytes(obj.MixDigest[:])
w.WriteBytes(obj.Nonce[:])
// XDPoS fields - always encode (not optional)
w.WriteBytes(obj.Validators)
w.WriteBytes(obj.Validator)
w.WriteBytes(obj.Penalties)
_tmp1 := obj.BaseFee != nil
_tmp2 := obj.WithdrawalsHash != nil
_tmp3 := obj.BlobGasUsed != nil

View file

@ -0,0 +1,224 @@
# XDC Genesis Hash Compatibility Fix
**Date:** 2026-01-29
**Status:** ✅ RESOLVED
## Problem Statement
The Part4 implementation (go-ethereum v1.15 based XDC port) was producing different genesis hashes than the official XDPoSChain implementation when initializing from the XDC mainnet genesis file.
- **Expected Hash:** `0x4a9d748bd78a8d0385b67788c2435dcdb914f98a96250b68863a1f8b7642d6b1`
- **Part4 Produced:** `0x0683984f78dcbdd71f618cca7e14811b500b8f0f5ee1f1962d8fc44aa5a75eea` (before fix)
This would prevent Part4 nodes from syncing with the XDC mainnet.
---
## Root Causes Identified
### 1. Corrupted Genesis File
**Issue:** The Part4 genesis file (`genesis/xdc_mainnet.json`) had corrupted contract bytecode for the validator contract at address `0x88`.
| File | Code Length (bytes) |
|------|-------------------|
| Part4 (corrupted) | 14,453 |
| Official | 14,452 |
The Part4 file had 2 extra hex characters (`00`) inserted at position 18745 in the bytecode.
**Impact:** Different code hash → different account hash → different state root.
**Fix:** Copied correct genesis from official repository and converted `xdc` address prefixes to `0x`.
### 2. Missing XDPoS Fields in RLP Encoder
**Issue:** The generated RLP encoder (`core/types/gen_header_rlp.go`) did not include the XDPoS-specific header fields.
XDC headers have three additional fields compared to standard Ethereum headers:
```go
Validators []byte // List of validators
Validator []byte // Current validator
Penalties []byte // Penalty data
```
The generated code jumped from `Nonce` directly to `BaseFee`, skipping these fields entirely:
```go
// BEFORE (incorrect)
w.WriteBytes(obj.Nonce[:])
_tmp1 := obj.BaseFee != nil // XDPoS fields skipped!
```
**Impact:** Header RLP encoding was 3 bytes shorter (661 vs 664 bytes), producing different block hashes.
**Fix:** Added XDPoS field encoding to `gen_header_rlp.go`:
```go
// AFTER (correct)
w.WriteBytes(obj.Nonce[:])
// XDPoS fields - always encode (not optional)
w.WriteBytes(obj.Validators)
w.WriteBytes(obj.Validator)
w.WriteBytes(obj.Penalties)
_tmp1 := obj.BaseFee != nil
```
### 3. Nil vs Empty Slice Initialization
**Issue:** Genesis block construction left XDPoS fields as `nil` instead of empty slices `[]byte{}`.
```go
// BEFORE (incorrect)
head := &types.Header{
// ... other fields
// Validators, Validator, Penalties not set (nil)
}
```
**Impact:** Even with proper RLP encoding, nil slices might be handled differently than empty slices.
**Fix:** Explicitly initialize to empty slices in `core/genesis.go`:
```go
// AFTER (correct)
head := &types.Header{
// ... other fields
Validators: []byte{},
Validator: []byte{},
Penalties: []byte{},
}
```
### 4. Incorrect Config Hash
**Issue:** `params/config.go` had wrong `XDCMainnetGenesisHash`.
```go
// BEFORE (incorrect)
XDCMainnetGenesisHash = common.HexToHash("0x81b02e6c24c0ed8383dd5f6c1e83e82b8f988af91f89f9b95c10dbd3e25cd025")
```
**Fix:**
```go
// AFTER (correct)
XDCMainnetGenesisHash = common.HexToHash("0x4a9d748bd78a8d0385b67788c2435dcdb914f98a96250b68863a1f8b7642d6b1")
```
---
## Files Modified
| File | Change |
|------|--------|
| `genesis/xdc_mainnet.json` | Replaced with correct genesis (fixed contract bytecode) |
| `core/types/gen_header_rlp.go` | Added XDPoS field encoding |
| `core/types/block.go` | Removed `rlp:"optional"` tags from XDPoS fields |
| `core/genesis.go` | Initialize XDPoS fields to empty slices |
| `params/config.go` | Fixed `XDCMainnetGenesisHash` value |
---
## Verification
### State Root Match
```
Part4: 0x49be235b0098b048f9805aed38a279d8c189b469ff9ba307b39c7ad3a3bc55ae
Official: 0x49be235b0098b048f9805aed38a279d8c189b469ff9ba307b39c7ad3a3bc55ae
✅ MATCH
```
### Header RLP Match
```
Part4: 664 bytes, ends with 808080 (three empty XDPoS fields)
Official: 664 bytes, ends with 808080
✅ MATCH
```
### Genesis Block Hash Match
```
Part4: 0x4a9d748bd78a8d0385b67788c2435dcdb914f98a96250b68863a1f8b7642d6b1
Official: 0x4a9d748bd78a8d0385b67788c2435dcdb914f98a96250b68863a1f8b7642d6b1
✅ MATCH
```
### Init Command
```bash
$ ./geth init --datadir /tmp/test genesis/xdc_mainnet.json
INFO Successfully wrote genesis state database=chaindata hash=4a9d74..42d6b1
✅ SUCCESS
```
---
## Technical Details
### XDC Header Structure (RLP Order)
```
1. ParentHash [32]byte
2. UncleHash [32]byte
3. Coinbase [20]byte
4. Root [32]byte (state root)
5. TxHash [32]byte
6. ReceiptHash [32]byte
7. Bloom [256]byte
8. Difficulty *big.Int
9. Number *big.Int
10. GasLimit uint64
11. GasUsed uint64
12. Time uint64
13. Extra []byte
14. MixDigest [32]byte
15. Nonce [8]byte
16. Validators []byte ← XDPoS field
17. Validator []byte ← XDPoS field
18. Penalties []byte ← XDPoS field
19. BaseFee *big.Int (optional, EIP-1559)
... (other optional fields)
```
### RLP Encoding for Empty XDPoS Fields
Empty `[]byte{}` encodes as `0x80` in RLP (single byte representing empty string).
Genesis block has all three XDPoS fields empty, so the header ends with:
```
...880000000000000000 808080
└── Nonce (8 bytes) └── Validators, Validator, Penalties (empty)
```
### Why Generated Code Needed Manual Update
Go-ethereum uses code generation for RLP encoding:
```go
//go:generate go run ../../rlp/rlpgen -type Header -out gen_header_rlp.go
```
When new fields are added to the Header struct, the generated code must be regenerated or manually updated. In this case, manual update was safer to avoid unintended changes.
---
## Debugging Methodology
1. **Isolated the issue** - Verified trie encoding works correctly with simple test cases
2. **Binary comparison** - Compared RLP-encoded headers byte-by-byte
3. **Field-by-field verification** - Confirmed each header field matches
4. **Size discrepancy** - Found 3-byte difference (661 vs 664 bytes)
5. **Traced to source** - Found generated RLP encoder missing XDPoS fields
6. **Genesis file audit** - Found corrupted contract bytecode
---
## Lessons Learned
1. **Always verify genesis files** - Even small corruption (2 bytes) breaks everything
2. **Generated code needs updates** - When adding struct fields, check generated encoders
3. **nil vs empty slice matters** - In RLP encoding, they can behave differently
4. **Compare binary output** - When hashes don't match, compare the raw bytes
---
## References
- Official XDC genesis hash: `0x4a9d748bd78a8d0385b67788c2435dcdb914f98a96250b68863a1f8b7642d6b1`
- Official XDC state root: `0x49be235b0098b048f9805aed38a279d8c189b469ff9ba307b39c7ad3a3bc55ae`
- XDPoSChain repository: https://github.com/XinFinOrg/XDPoSChain

File diff suppressed because one or more lines are too long

14
params/bootnodes_xdc.go Normal file
View file

@ -0,0 +1,14 @@
package params
// XDCMainnetBootnodes are the enode URLs of the P2P bootstrap nodes for XDC mainnet.
var XDCMainnetBootnodes = []string{
"enode://91e59fa1b034ae35e9f4e8a99cc6621f09d74e76a6220abb6c93b29ed41a9e1fc4e5b70e2c5fc43f883cffbdcd6f4f6cbc1d23af077f28c2aecc22403355d4b1@209.126.0.250:30304",
"enode://91e59fa1b034ae35e9f4e8a99cc6621f09d74e76a6220abb6c93b29ed41a9e1fc4e5b70e2c5fc43f883cffbdcd6f4f6cbc1d23af077f28c2aecc22403355d4b1@209.126.4.150:30304",
"enode://91e59fa1b034ae35e9f4e8a99cc6621f09d74e76a6220abb6c93b29ed41a9e1fc4e5b70e2c5fc43f883cffbdcd6f4f6cbc1d23af077f28c2aecc22403355d4b1@144.126.150.58:30304",
"enode://91e59fa1b034ae35e9f4e8a99cc6621f09d74e76a6220abb6c93b29ed41a9e1fc4e5b70e2c5fc43f883cffbdcd6f4f6cbc1d23af077f28c2aecc22403355d4b1@162.250.190.246:30304",
"enode://91e59fa1b034ae35e9f4e8a99cc6621f09d74e76a6220abb6c93b29ed41a9e1fc4e5b70e2c5fc43f883cffbdcd6f4f6cbc1d23af077f28c2aecc22403355d4b1@162.250.191.5:30304",
"enode://91e59fa1b034ae35e9f4e8a99cc6621f09d74e76a6220abb6c93b29ed41a9e1fc4e5b70e2c5fc43f883cffbdcd6f4f6cbc1d23af077f28c2aecc22403355d4b1@162.250.191.14:30304",
"enode://91e59fa1b034ae35e9f4e8a99cc6621f09d74e76a6220abb6c93b29ed41a9e1fc4e5b70e2c5fc43f883cffbdcd6f4f6cbc1d23af077f28c2aecc22403355d4b1@162.250.189.149:30304",
"enode://91e59fa1b034ae35e9f4e8a99cc6621f09d74e76a6220abb6c93b29ed41a9e1fc4e5b70e2c5fc43f883cffbdcd6f4f6cbc1d23af077f28c2aecc22403355d4b1@162.250.189.152:30304",
"enode://7524db6718828c2c7663e6585a5b1e066457b8b0235034b69358b36e584fea776666d36ed4fc43d0f8bf2a5c3b2a960b5600689b6c8f0c207e5a76f8b0ca432d@157.173.120.219:30304",
}

View file

@ -32,7 +32,7 @@ var (
HoleskyGenesisHash = common.HexToHash("0xb5f7f912443c940f21fd611f12828d75b534364ed9e95ca4e307729a4661bde4")
SepoliaGenesisHash = common.HexToHash("0x25a5cc106eea7138acab33231d7160d69cb777ee0c2c553fcddf5138993e6dd9")
HoodiGenesisHash = common.HexToHash("0xbbe312868b376a3001692a646dd2d7d1e4406380dfd86b98aa8a34d1557c971b")
XDCMainnetGenesisHash = common.HexToHash("0x81b02e6c24c0ed8383dd5f6c1e83e82b8f988af91f89f9b95c10dbd3e25cd025")
XDCMainnetGenesisHash = common.HexToHash("0x4a9d748bd78a8d0385b67788c2435dcdb914f98a96250b68863a1f8b7642d6b1")
XDCApothemGenesisHash = common.HexToHash("0xcc97d2b9dcbce1b3d4a08c53c0f61c9c22c5c8c6c6f4eb4c5c5c5c5c5c5c5c5c5")
)
@ -580,6 +580,16 @@ type XDPoSConfig struct {
RewardCheckpoint uint64 `json:"rewardCheckpoint"` // Checkpoint block for calculate rewards
Gap uint64 `json:"gap"` // Gap time preparing for the next epoch
FoudationWalletAddr common.Address `json:"foudationWalletAddr"` // Foundation Address Wallet
V2 *XDPoSV2Config `json:"v2,omitempty"` // XDPoS 2.0 configuration (optional)
}
// XDPoSV2Config holds XDPoS 2.0 specific configuration
type XDPoSV2Config struct {
SwitchBlock *big.Int `json:"switchBlock"` // Block number to switch to V2
MinePeriod uint64 `json:"minePeriod"` // Mining period in seconds
TimeoutPeriod uint64 `json:"timeoutPeriod"` // Timeout period in seconds
TimeoutSyncThreshold uint64 `json:"timeoutSyncThreshold"` // Timeout sync threshold
CertThreshold uint64 `json:"certThreshold"` // Certificate threshold (2/3 of validators)
}
// String implements the stringer interface, returning the consensus engine details.