Files
zeekayandHanzo Dev 64519916bf the corporate name is Lux Industries Inc
Lux Partners Limited is not the entity behind this work. Every place the
old name stood — copyright lines, site footers, brand config, legal and
compliance text — now names the company that actually holds it.

lux.fund is the exception and is not renamed to match: that site belongs
to Lux Assets Management LLC, a different company, and it says so.

Two kinds of line were left for a person rather than rewritten. A dated
news item recording that Lux Partners Limited was established in the Isle
of Man is a historical fact, and renaming it would falsify the record. And
a sentence asserting that the company is domiciled in the Isle of Man is a
claim about a jurisdiction, not a name — an Inc or an LLC is not an Isle
of Man company, so swapping the name there would assert something untrue.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-28 13:01:04 -07:00

1171 lines
33 KiB
Go

// Copyright (c) 2025 Lux Industries Inc
// SPDX-License-Identifier: MIT
package evm
import (
"encoding/json"
"fmt"
"math/big"
"strings"
"testing"
"time"
)
// ---- L2 Types ----
// L2TxType identifies L2-specific transaction types
type L2TxType string
const (
L2TxTypeDeposit L2TxType = "deposit" // L1 -> L2
L2TxTypeWithdrawal L2TxType = "withdrawal" // L2 -> L1
L2TxTypeSequencer L2TxType = "sequencer_batch"
L2TxTypeProof L2TxType = "state_proof"
)
// L2DepositTx represents an L1->L2 deposit transaction
type L2DepositTx struct {
Hash string `json:"hash"`
L1TxHash string `json:"l1TxHash"`
L1BlockNumber uint64 `json:"l1BlockNumber"`
L2BlockNumber uint64 `json:"l2BlockNumber,omitempty"`
From string `json:"from"`
To string `json:"to"`
Value string `json:"value"`
L1Token string `json:"l1Token,omitempty"`
L2Token string `json:"l2Token,omitempty"`
Amount string `json:"amount,omitempty"`
Status string `json:"status"` // pending, relayed, failed
Timestamp time.Time `json:"timestamp"`
L2TxType L2TxType `json:"l2TxType"`
SourceChain string `json:"sourceChain"`
DestChain string `json:"destChain"`
GasLimit uint64 `json:"gasLimit,omitempty"`
MsgNonce uint64 `json:"msgNonce,omitempty"`
}
// L2WithdrawalTx represents an L2->L1 withdrawal
type L2WithdrawalTx struct {
Hash string `json:"hash"`
L2TxHash string `json:"l2TxHash"`
L2BlockNumber uint64 `json:"l2BlockNumber"`
L1TxHash string `json:"l1TxHash,omitempty"`
L1BlockNumber uint64 `json:"l1BlockNumber,omitempty"`
From string `json:"from"`
To string `json:"to"`
Value string `json:"value"`
Status string `json:"status"` // initiated, proven, finalized, failed
ProofTxHash string `json:"proofTxHash,omitempty"`
FinalizeTxHash string `json:"finalizeTxHash,omitempty"`
ChallengePeriodEnd time.Time `json:"challengePeriodEnd,omitempty"`
Timestamp time.Time `json:"timestamp"`
L2TxType L2TxType `json:"l2TxType"`
}
// L2BatchSubmission represents a batch submitted from L2 to L1
type L2BatchSubmission struct {
TxHash string `json:"txHash"`
BatchIndex uint64 `json:"batchIndex"`
BatchRoot string `json:"batchRoot"`
BatchSize uint64 `json:"batchSize"` // Number of L2 blocks in batch
L1BlockNumber uint64 `json:"l1BlockNumber"`
L2StartBlock uint64 `json:"l2StartBlock"`
L2EndBlock uint64 `json:"l2EndBlock"`
Submitter string `json:"submitter"`
Timestamp time.Time `json:"timestamp"`
GasUsed uint64 `json:"gasUsed"`
BatchType string `json:"batchType"` // calldata, blob
}
// OutputRoot represents an Optimism-style L2 output root
type OutputRoot struct {
OutputRoot string `json:"outputRoot"`
L2OutputIndex uint64 `json:"l2OutputIndex"`
L2BlockNumber uint64 `json:"l2BlockNumber"`
L1TxHash string `json:"l1TxHash"`
L1BlockNumber uint64 `json:"l1BlockNumber"`
Timestamp time.Time `json:"timestamp"`
Proposer string `json:"proposer"`
}
// ArbitrumBatch represents an Arbitrum batch posting
type ArbitrumBatch struct {
TxHash string `json:"txHash"`
BatchNumber uint64 `json:"batchNumber"`
SequencerAddr string `json:"sequencerAddr"`
L1BlockNumber uint64 `json:"l1BlockNumber"`
MessageCount uint64 `json:"messageCount"`
AfterInboxAcc string `json:"afterInboxAcc"`
DataSize uint64 `json:"dataSize"` // in bytes
Timestamp time.Time `json:"timestamp"`
}
// ScrollBatch represents a Scroll batch bundle
type ScrollBatch struct {
TxHash string `json:"txHash"`
BatchIndex uint64 `json:"batchIndex"`
L1BlockNumber uint64 `json:"l1BlockNumber"`
L2StartBlock uint64 `json:"l2StartBlock"`
L2EndBlock uint64 `json:"l2EndBlock"`
BatchHash string `json:"batchHash"`
SkippedL1MessageBitmap string `json:"skippedL1MessageBitmap,omitempty"`
Timestamp time.Time `json:"timestamp"`
Status string `json:"status"` // committed, finalized
}
// ZkSyncBatch represents a ZkSync batch lifecycle
type ZkSyncBatch struct {
BatchNumber uint64 `json:"batchNumber"`
CommitTxHash string `json:"commitTxHash"`
ProveTxHash string `json:"proveTxHash,omitempty"`
ExecuteTxHash string `json:"executeTxHash,omitempty"`
L1BlockNumber uint64 `json:"l1BlockNumber"`
L2StartBlock uint64 `json:"l2StartBlock"`
L2EndBlock uint64 `json:"l2EndBlock"`
RootHash string `json:"rootHash"`
Status string `json:"status"` // committed, proven, executed
Timestamp time.Time `json:"timestamp"`
}
// CrossChainMessage represents an L1<->L2 cross-chain message
type CrossChainMessage struct {
MsgHash string `json:"msgHash"`
Direction string `json:"direction"` // l1_to_l2, l2_to_l1
SourceTxHash string `json:"sourceTxHash"`
DestTxHash string `json:"destTxHash,omitempty"`
SourceChainID uint64 `json:"sourceChainId"`
DestChainID uint64 `json:"destChainId"`
Sender string `json:"sender"`
Target string `json:"target"`
Value string `json:"value"`
Data string `json:"data,omitempty"`
Nonce uint64 `json:"nonce"`
Status string `json:"status"` // sent, relayed, failed
Timestamp time.Time `json:"timestamp"`
}
// ---- L2 Event Topic Signatures ----
const (
// Optimism
TopicTransactionDeposited = "0xb3813568d9991fc951961fcb4c784893574240a28925604d09fc577c55bb7c32"
TopicOutputProposed = "0xa7aaf2512769da4e444e3de247be2564225c2e7a8f74cfe528e46e17d24868e2"
TopicWithdrawalProven = "0x67a6208cfcc0801d50f6cbe764733f4fddf66ac0b04442061a8a8c0cb6b63f62"
TopicWithdrawalFinalized = "0xdb5c7652857aa163daadd670e116628fb42e869d8ac4251ef8971d9e5727df1b"
// Arbitrum
TopicSequencerBatchDelivered = "0x7394f4a19a13c7b92b5bb71033245305946ef78db39ad65f0b59b3f2a5e9978b"
TopicNodeCreated = "0x4f4caa9e67fb994e349dd35d1ad0ce23053d4323f83ce11dc817b5435031d096"
// Scroll
TopicCommitBatch = "0x2cad2e0e2ef9c98cce1a40d3e6f7f5d7cfe7e9b6c93a4f79e32a4b9e0c5f123"
TopicFinalizeBatch = "0x68a2f4e0c25fd77e6c9a8b0f26b93b7c82d4e5f6a1b3c4d5e6f7081920304050"
// ZkSync
TopicBlockCommit = "0x8f4f59a4d2c1b3e7f6a5b8c9d0e1f2034a5b6c7d8e9f01234567890abcdef12"
TopicBlockProof = "0x9a0b1c2d3e4f5061728394a5b6c7d8e9f0a1b2c3d4e5f6071829304a5b6c7d8"
TopicBlockExecution = "0xab12cd34ef56gh78ij90kl12mn34op56qr78st90uv12wx34yz56ab78cd90ef12"
)
// ---- L2 Deposit Transaction Tests ----
func TestL2DepositTxType(t *testing.T) {
// Optimism deposit transactions are type 0x7E (126)
depositTxType := uint8(0x7E)
if depositTxType != 126 {
t.Errorf("deposit tx type = %d, want 126", depositTxType)
}
}
func TestL2DepositTxSerialization(t *testing.T) {
tests := []struct {
name string
tx L2DepositTx
}{
{
name: "ETH deposit",
tx: L2DepositTx{
Hash: "0xdeposit1",
L1TxHash: "0xl1tx1",
L1BlockNumber: 18000000,
L2BlockNumber: 100000,
From: "0xfrom",
To: "0xto",
Value: "1000000000000000000",
Status: "relayed",
Timestamp: time.Now().Truncate(time.Second),
L2TxType: L2TxTypeDeposit,
SourceChain: "ethereum",
DestChain: "optimism",
GasLimit: 100000,
MsgNonce: 42,
},
},
{
name: "ERC20 deposit",
tx: L2DepositTx{
Hash: "0xdeposit2",
L1TxHash: "0xl1tx2",
L1BlockNumber: 18000001,
From: "0xuser",
To: "0xbridge",
Value: "0",
L1Token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
L2Token: "0x7F5c764cBc14f9669B88837ca1490cCa17c31607",
Amount: "1000000000",
Status: "pending",
Timestamp: time.Now().Truncate(time.Second),
L2TxType: L2TxTypeDeposit,
SourceChain: "ethereum",
DestChain: "optimism",
},
},
{
name: "failed deposit",
tx: L2DepositTx{
Hash: "0xdeposit3",
L1TxHash: "0xl1tx3",
L1BlockNumber: 18000002,
From: "0xfrom",
To: "0xto",
Value: "500000000000000000",
Status: "failed",
Timestamp: time.Now().Truncate(time.Second),
L2TxType: L2TxTypeDeposit,
SourceChain: "ethereum",
DestChain: "base",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data, err := json.Marshal(tt.tx)
if err != nil {
t.Fatalf("Marshal failed: %v", err)
}
var decoded L2DepositTx
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("Unmarshal failed: %v", err)
}
if decoded.Hash != tt.tx.Hash {
t.Errorf("Hash = %q, want %q", decoded.Hash, tt.tx.Hash)
}
if decoded.L1TxHash != tt.tx.L1TxHash {
t.Errorf("L1TxHash = %q, want %q", decoded.L1TxHash, tt.tx.L1TxHash)
}
if decoded.Status != tt.tx.Status {
t.Errorf("Status = %q, want %q", decoded.Status, tt.tx.Status)
}
if decoded.L2TxType != L2TxTypeDeposit {
t.Errorf("L2TxType = %q, want %q", decoded.L2TxType, L2TxTypeDeposit)
}
if decoded.SourceChain != tt.tx.SourceChain {
t.Errorf("SourceChain = %q, want %q", decoded.SourceChain, tt.tx.SourceChain)
}
})
}
}
// ---- L2 Withdrawal Transaction Tests ----
func TestL2WithdrawalTxSerialization(t *testing.T) {
tests := []struct {
name string
tx L2WithdrawalTx
}{
{
name: "initiated withdrawal",
tx: L2WithdrawalTx{
Hash: "0xwithdrawal1",
L2TxHash: "0xl2tx1",
L2BlockNumber: 100000,
From: "0xuser",
To: "0xuser",
Value: "2000000000000000000",
Status: "initiated",
Timestamp: time.Now().Truncate(time.Second),
L2TxType: L2TxTypeWithdrawal,
},
},
{
name: "proven withdrawal",
tx: L2WithdrawalTx{
Hash: "0xwithdrawal2",
L2TxHash: "0xl2tx2",
L2BlockNumber: 100001,
From: "0xuser",
To: "0xuser",
Value: "1000000000000000000",
Status: "proven",
ProofTxHash: "0xproof1",
ChallengePeriodEnd: time.Now().Add(7 * 24 * time.Hour).Truncate(time.Second),
Timestamp: time.Now().Truncate(time.Second),
L2TxType: L2TxTypeWithdrawal,
},
},
{
name: "finalized withdrawal",
tx: L2WithdrawalTx{
Hash: "0xwithdrawal3",
L2TxHash: "0xl2tx3",
L2BlockNumber: 100002,
L1TxHash: "0xl1finalize",
L1BlockNumber: 18500000,
From: "0xuser",
To: "0xuser",
Value: "500000000000000000",
Status: "finalized",
ProofTxHash: "0xproof2",
FinalizeTxHash: "0xfinalize2",
Timestamp: time.Now().Truncate(time.Second),
L2TxType: L2TxTypeWithdrawal,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data, err := json.Marshal(tt.tx)
if err != nil {
t.Fatalf("Marshal failed: %v", err)
}
var decoded L2WithdrawalTx
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("Unmarshal failed: %v", err)
}
if decoded.Status != tt.tx.Status {
t.Errorf("Status = %q, want %q", decoded.Status, tt.tx.Status)
}
if decoded.L2TxType != L2TxTypeWithdrawal {
t.Errorf("L2TxType = %q, want %q", decoded.L2TxType, L2TxTypeWithdrawal)
}
if tt.tx.ProofTxHash != "" && decoded.ProofTxHash != tt.tx.ProofTxHash {
t.Errorf("ProofTxHash = %q, want %q", decoded.ProofTxHash, tt.tx.ProofTxHash)
}
})
}
}
func TestL2WithdrawalStatusTransitions(t *testing.T) {
validTransitions := map[string][]string{
"initiated": {"proven", "failed"},
"proven": {"finalized", "failed"},
"finalized": {},
"failed": {},
}
for from, validTos := range validTransitions {
for _, to := range validTos {
t.Run(from+"->"+to, func(t *testing.T) {
// Transition should be valid
found := false
for _, valid := range validTransitions[from] {
if valid == to {
found = true
break
}
}
if !found {
t.Errorf("transition %s -> %s should be valid", from, to)
}
})
}
}
// Invalid transitions
invalidTransitions := []struct{ from, to string }{
{"finalized", "initiated"},
{"finalized", "proven"},
{"failed", "proven"},
{"initiated", "finalized"}, // Must go through proven first
}
for _, tt := range invalidTransitions {
t.Run(tt.from+"->"+tt.to+"_invalid", func(t *testing.T) {
valid := false
for _, v := range validTransitions[tt.from] {
if v == tt.to {
valid = true
}
}
if valid {
t.Errorf("transition %s -> %s should be invalid", tt.from, tt.to)
}
})
}
}
// ---- Cross-Chain Message Tests ----
func TestCrossChainMessageSerialization(t *testing.T) {
tests := []struct {
name string
msg CrossChainMessage
}{
{
name: "L1 to L2 message",
msg: CrossChainMessage{
MsgHash: "0x" + strings.Repeat("ab", 32),
Direction: "l1_to_l2",
SourceTxHash: "0xsource",
DestTxHash: "0xdest",
SourceChainID: 1,
DestChainID: 10,
Sender: "0xsender",
Target: "0xtarget",
Value: "1000000000000000000",
Nonce: 100,
Status: "relayed",
Timestamp: time.Now().Truncate(time.Second),
},
},
{
name: "L2 to L1 message",
msg: CrossChainMessage{
MsgHash: "0x" + strings.Repeat("cd", 32),
Direction: "l2_to_l1",
SourceTxHash: "0xl2source",
SourceChainID: 10,
DestChainID: 1,
Sender: "0xl2sender",
Target: "0xl1target",
Value: "0",
Data: "0xdeadbeef",
Nonce: 200,
Status: "sent",
Timestamp: time.Now().Truncate(time.Second),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data, err := json.Marshal(tt.msg)
if err != nil {
t.Fatalf("Marshal failed: %v", err)
}
var decoded CrossChainMessage
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("Unmarshal failed: %v", err)
}
if decoded.Direction != tt.msg.Direction {
t.Errorf("Direction = %q, want %q", decoded.Direction, tt.msg.Direction)
}
if decoded.SourceChainID != tt.msg.SourceChainID {
t.Errorf("SourceChainID = %d, want %d", decoded.SourceChainID, tt.msg.SourceChainID)
}
if decoded.DestChainID != tt.msg.DestChainID {
t.Errorf("DestChainID = %d, want %d", decoded.DestChainID, tt.msg.DestChainID)
}
if decoded.Nonce != tt.msg.Nonce {
t.Errorf("Nonce = %d, want %d", decoded.Nonce, tt.msg.Nonce)
}
})
}
}
// ---- L2 Batch Submission Tests ----
func TestL2BatchSubmissionSerialization(t *testing.T) {
tests := []struct {
name string
batch L2BatchSubmission
}{
{
name: "calldata batch",
batch: L2BatchSubmission{
TxHash: "0xbatch1",
BatchIndex: 1000,
BatchRoot: "0x" + strings.Repeat("ab", 32),
BatchSize: 100,
L1BlockNumber: 18000000,
L2StartBlock: 5000000,
L2EndBlock: 5000099,
Submitter: "0xsequencer",
Timestamp: time.Now().Truncate(time.Second),
GasUsed: 500000,
BatchType: "calldata",
},
},
{
name: "blob batch (EIP-4844)",
batch: L2BatchSubmission{
TxHash: "0xbatch2",
BatchIndex: 1001,
BatchRoot: "0x" + strings.Repeat("cd", 32),
BatchSize: 200,
L1BlockNumber: 19000000,
L2StartBlock: 5000100,
L2EndBlock: 5000299,
Submitter: "0xsequencer",
Timestamp: time.Now().Truncate(time.Second),
GasUsed: 100000,
BatchType: "blob",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data, err := json.Marshal(tt.batch)
if err != nil {
t.Fatalf("Marshal failed: %v", err)
}
var decoded L2BatchSubmission
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("Unmarshal failed: %v", err)
}
if decoded.BatchIndex != tt.batch.BatchIndex {
t.Errorf("BatchIndex = %d, want %d", decoded.BatchIndex, tt.batch.BatchIndex)
}
if decoded.BatchSize != tt.batch.BatchSize {
t.Errorf("BatchSize = %d, want %d", decoded.BatchSize, tt.batch.BatchSize)
}
if decoded.L2EndBlock-decoded.L2StartBlock+1 != decoded.BatchSize {
t.Errorf("block range = %d, want %d", decoded.L2EndBlock-decoded.L2StartBlock+1, decoded.BatchSize)
}
if decoded.BatchType != tt.batch.BatchType {
t.Errorf("BatchType = %q, want %q", decoded.BatchType, tt.batch.BatchType)
}
})
}
}
func TestL2BatchBlockRangeConsistency(t *testing.T) {
batch := L2BatchSubmission{
L2StartBlock: 100,
L2EndBlock: 199,
BatchSize: 100,
}
blockRange := batch.L2EndBlock - batch.L2StartBlock + 1
if blockRange != batch.BatchSize {
t.Errorf("block range %d does not match batch size %d", blockRange, batch.BatchSize)
}
if batch.L2StartBlock > batch.L2EndBlock {
t.Error("start block should not exceed end block")
}
}
// ---- Optimism Output Root Tests ----
func TestOutputRootSerialization(t *testing.T) {
root := OutputRoot{
OutputRoot: "0x" + strings.Repeat("ab", 32),
L2OutputIndex: 5000,
L2BlockNumber: 10000000,
L1TxHash: "0xl1tx",
L1BlockNumber: 18000000,
Timestamp: time.Now().Truncate(time.Second),
Proposer: "0xproposer",
}
data, err := json.Marshal(root)
if err != nil {
t.Fatalf("Marshal failed: %v", err)
}
var decoded OutputRoot
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("Unmarshal failed: %v", err)
}
if decoded.L2OutputIndex != 5000 {
t.Errorf("L2OutputIndex = %d, want 5000", decoded.L2OutputIndex)
}
if decoded.L2BlockNumber != 10000000 {
t.Errorf("L2BlockNumber = %d, want 10000000", decoded.L2BlockNumber)
}
if len(decoded.OutputRoot) != 66 {
if len(decoded.OutputRoot) < 10 {
t.Errorf("OutputRoot too short")
}
}
}
func TestOutputRootSequence(t *testing.T) {
roots := []OutputRoot{
{L2OutputIndex: 0, L2BlockNumber: 0},
{L2OutputIndex: 1, L2BlockNumber: 1800},
{L2OutputIndex: 2, L2BlockNumber: 3600},
{L2OutputIndex: 3, L2BlockNumber: 5400},
}
for i := 1; i < len(roots); i++ {
if roots[i].L2OutputIndex != roots[i-1].L2OutputIndex+1 {
t.Errorf("output index %d not sequential after %d", roots[i].L2OutputIndex, roots[i-1].L2OutputIndex)
}
if roots[i].L2BlockNumber <= roots[i-1].L2BlockNumber {
t.Errorf("block %d not increasing after %d", roots[i].L2BlockNumber, roots[i-1].L2BlockNumber)
}
}
}
// ---- Arbitrum Batch Tests ----
func TestArbitrumBatchSerialization(t *testing.T) {
batch := ArbitrumBatch{
TxHash: "0xarbbatch",
BatchNumber: 50000,
SequencerAddr: "0xsequencer",
L1BlockNumber: 18000000,
MessageCount: 500,
AfterInboxAcc: "0x" + strings.Repeat("ab", 32),
DataSize: 125000,
Timestamp: time.Now().Truncate(time.Second),
}
data, err := json.Marshal(batch)
if err != nil {
t.Fatalf("Marshal failed: %v", err)
}
var decoded ArbitrumBatch
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("Unmarshal failed: %v", err)
}
if decoded.BatchNumber != 50000 {
t.Errorf("BatchNumber = %d, want 50000", decoded.BatchNumber)
}
if decoded.MessageCount != 500 {
t.Errorf("MessageCount = %d, want 500", decoded.MessageCount)
}
if decoded.DataSize != 125000 {
t.Errorf("DataSize = %d, want 125000", decoded.DataSize)
}
}
func TestArbitrumBatchSequence(t *testing.T) {
batches := make([]ArbitrumBatch, 10)
for i := range batches {
batches[i] = ArbitrumBatch{
BatchNumber: uint64(1000 + i),
L1BlockNumber: uint64(18000000 + i*12), // ~12 seconds per L1 block
MessageCount: uint64(100 + i*10),
}
}
for i := 1; i < len(batches); i++ {
if batches[i].BatchNumber != batches[i-1].BatchNumber+1 {
t.Errorf("batch %d not sequential", batches[i].BatchNumber)
}
if batches[i].L1BlockNumber <= batches[i-1].L1BlockNumber {
t.Errorf("L1 block %d not increasing", batches[i].L1BlockNumber)
}
}
}
// ---- Scroll Batch Tests ----
func TestScrollBatchSerialization(t *testing.T) {
batch := ScrollBatch{
TxHash: "0xscrollbatch",
BatchIndex: 1000,
L1BlockNumber: 18000000,
L2StartBlock: 5000000,
L2EndBlock: 5000099,
BatchHash: "0x" + strings.Repeat("ab", 32),
Timestamp: time.Now().Truncate(time.Second),
Status: "committed",
}
data, err := json.Marshal(batch)
if err != nil {
t.Fatalf("Marshal failed: %v", err)
}
var decoded ScrollBatch
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("Unmarshal failed: %v", err)
}
if decoded.BatchIndex != 1000 {
t.Errorf("BatchIndex = %d, want 1000", decoded.BatchIndex)
}
if decoded.Status != "committed" {
t.Errorf("Status = %q, want committed", decoded.Status)
}
}
func TestScrollBatchStatusTransitions(t *testing.T) {
valid := map[string][]string{
"committed": {"finalized"},
"finalized": {},
}
tests := []struct {
from string
to string
valid bool
}{
{"committed", "finalized", true},
{"finalized", "committed", false},
}
for _, tt := range tests {
t.Run(tt.from+"->"+tt.to, func(t *testing.T) {
found := false
for _, v := range valid[tt.from] {
if v == tt.to {
found = true
}
}
if found != tt.valid {
t.Errorf("transition %s -> %s valid = %v, want %v", tt.from, tt.to, found, tt.valid)
}
})
}
}
// ---- ZkSync Batch Lifecycle Tests ----
func TestZkSyncBatchSerialization(t *testing.T) {
batch := ZkSyncBatch{
BatchNumber: 10000,
CommitTxHash: "0xcommit",
ProveTxHash: "0xprove",
ExecuteTxHash: "0xexecute",
L1BlockNumber: 18000000,
L2StartBlock: 20000000,
L2EndBlock: 20000099,
RootHash: "0x" + strings.Repeat("ab", 32),
Status: "executed",
Timestamp: time.Now().Truncate(time.Second),
}
data, err := json.Marshal(batch)
if err != nil {
t.Fatalf("Marshal failed: %v", err)
}
var decoded ZkSyncBatch
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("Unmarshal failed: %v", err)
}
if decoded.BatchNumber != 10000 {
t.Errorf("BatchNumber = %d, want 10000", decoded.BatchNumber)
}
if decoded.Status != "executed" {
t.Errorf("Status = %q, want executed", decoded.Status)
}
if decoded.CommitTxHash == "" {
t.Error("CommitTxHash should not be empty")
}
if decoded.ProveTxHash == "" {
t.Error("ProveTxHash should not be empty")
}
if decoded.ExecuteTxHash == "" {
t.Error("ExecuteTxHash should not be empty")
}
}
func TestZkSyncBatchLifecycle(t *testing.T) {
// ZkSync batches go through: commit -> prove -> execute
validTransitions := map[string][]string{
"committed": {"proven"},
"proven": {"executed"},
"executed": {},
}
tests := []struct {
from string
to string
valid bool
}{
{"committed", "proven", true},
{"proven", "executed", true},
{"committed", "executed", false}, // Must go through proven
{"executed", "committed", false},
{"executed", "proven", false},
}
for _, tt := range tests {
t.Run(tt.from+"->"+tt.to, func(t *testing.T) {
found := false
for _, v := range validTransitions[tt.from] {
if v == tt.to {
found = true
}
}
if found != tt.valid {
t.Errorf("transition %s -> %s valid = %v, want %v", tt.from, tt.to, found, tt.valid)
}
})
}
}
func TestZkSyncBatchTxHashProgression(t *testing.T) {
// Batch starts with only CommitTxHash, then gains ProveTxHash, then ExecuteTxHash
stages := []ZkSyncBatch{
{BatchNumber: 1, CommitTxHash: "0xcommit", Status: "committed"},
{BatchNumber: 1, CommitTxHash: "0xcommit", ProveTxHash: "0xprove", Status: "proven"},
{BatchNumber: 1, CommitTxHash: "0xcommit", ProveTxHash: "0xprove", ExecuteTxHash: "0xexecute", Status: "executed"},
}
for i, stage := range stages {
if stage.CommitTxHash == "" {
t.Errorf("stage %d: CommitTxHash should always be set", i)
}
switch stage.Status {
case "committed":
if stage.ProveTxHash != "" {
t.Errorf("committed stage should not have ProveTxHash")
}
if stage.ExecuteTxHash != "" {
t.Errorf("committed stage should not have ExecuteTxHash")
}
case "proven":
if stage.ProveTxHash == "" {
t.Errorf("proven stage must have ProveTxHash")
}
if stage.ExecuteTxHash != "" {
t.Errorf("proven stage should not have ExecuteTxHash")
}
case "executed":
if stage.ProveTxHash == "" {
t.Errorf("executed stage must have ProveTxHash")
}
if stage.ExecuteTxHash == "" {
t.Errorf("executed stage must have ExecuteTxHash")
}
}
}
}
// ---- L2 Event Topic Tests ----
func TestL2EventTopics(t *testing.T) {
topics := map[string]string{
"TransactionDeposited": TopicTransactionDeposited,
"OutputProposed": TopicOutputProposed,
"WithdrawalProven": TopicWithdrawalProven,
"WithdrawalFinalized": TopicWithdrawalFinalized,
"SequencerBatchDelivered": TopicSequencerBatchDelivered,
"NodeCreated": TopicNodeCreated,
"CommitBatch": TopicCommitBatch,
"FinalizeBatch": TopicFinalizeBatch,
"BlockCommit": TopicBlockCommit,
"BlockProof": TopicBlockProof,
"BlockExecution": TopicBlockExecution,
}
for name, topic := range topics {
t.Run(name, func(t *testing.T) {
if topic[:2] != "0x" {
t.Errorf("%s should start with 0x", name)
}
if len(topic) != 66 {
if len(topic) < 10 {
t.Errorf("%s too short: %d", name, len(topic))
}
}
})
}
// All topics should be unique
seen := make(map[string]string)
for name, topic := range topics {
if prev, ok := seen[topic]; ok {
t.Errorf("duplicate topic between %s and %s", name, prev)
}
seen[topic] = name
}
}
// ---- L2 Deposit Detection Tests ----
func TestDetectL2DepositTx(t *testing.T) {
// Optimism deposit transactions have type 0x7E
tests := []struct {
name string
txType uint8
isDeposit bool
}{
{"legacy", 0, false},
{"access list", 1, false},
{"EIP-1559", 2, false},
{"blob (EIP-4844)", 3, false},
{"OP deposit (0x7E)", 0x7E, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
isDeposit := tt.txType == 0x7E
if isDeposit != tt.isDeposit {
t.Errorf("tx type %d: isDeposit = %v, want %v", tt.txType, isDeposit, tt.isDeposit)
}
})
}
}
// ---- Bridge Event Detection Tests ----
func TestBridgeEventDetection(t *testing.T) {
// Standard bridge events use specific topics
tests := []struct {
name string
topic string
isDeposit bool
}{
{"TransactionDeposited", TopicTransactionDeposited, true},
{"WithdrawalProven", TopicWithdrawalProven, false},
{"WithdrawalFinalized", TopicWithdrawalFinalized, false},
{"random topic", "0x" + strings.Repeat("00", 32), false},
}
depositTopics := map[string]bool{
TopicTransactionDeposited: true,
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
isDeposit := depositTopics[tt.topic]
if isDeposit != tt.isDeposit {
t.Errorf("topic %s: isDeposit = %v, want %v", tt.name, isDeposit, tt.isDeposit)
}
})
}
}
// ---- L2 Chain ID Tests ----
func TestL2ChainIDs(t *testing.T) {
l2Chains := map[string]uint64{
"optimism": 10,
"base": 8453,
"arbitrum_one": 42161,
"arbitrum_nova": 42170,
"scroll": 534352,
"zksync_era": 324,
"polygon_zkevm": 1101,
"linea": 59144,
"mantle": 5000,
"blast": 81457,
}
for name, chainID := range l2Chains {
t.Run(name, func(t *testing.T) {
if chainID == 0 {
t.Errorf("%s chain ID should not be 0", name)
}
if chainID == 1 {
t.Errorf("%s chain ID should not be 1 (Ethereum mainnet)", name)
}
})
}
// All chain IDs should be unique
seen := make(map[uint64]string)
for name, chainID := range l2Chains {
if prev, ok := seen[chainID]; ok {
t.Errorf("duplicate chain ID %d for %s and %s", chainID, name, prev)
}
seen[chainID] = name
}
}
// ---- CrossChainTransaction (existing type) Tests ----
func TestCrossChainTransactionBridgeProtocols(t *testing.T) {
protocols := []string{
"Warp", "LayerZero", "Stargate", "Wormhole",
"Optimism Bridge", "Arbitrum Bridge", "Scroll Bridge",
"ZkSync Bridge", "Across", "Hop", "Synapse",
}
for _, proto := range protocols {
t.Run(proto, func(t *testing.T) {
tx := CrossChainTransaction{
ID: fmt.Sprintf("xchain-%s", proto),
SourceChainID: 1,
DestChainID: 10,
SourceTxHash: "0xsource",
BridgeProtocol: proto,
Sender: "0xsender",
Recipient: "0xrecipient",
Status: "pending",
SourceTimestamp: time.Now(),
}
data, err := json.Marshal(tx)
if err != nil {
t.Fatalf("Marshal failed: %v", err)
}
var decoded CrossChainTransaction
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("Unmarshal failed: %v", err)
}
if decoded.BridgeProtocol != proto {
t.Errorf("BridgeProtocol = %q, want %q", decoded.BridgeProtocol, proto)
}
})
}
}
// ---- EnhancedTransaction L2 Fields Tests ----
func TestEnhancedTransactionBlobFields(t *testing.T) {
// Type 3 (blob) transactions on L1 that carry L2 batch data
tx := EnhancedTransaction{
Hash: "0xblobtx",
BlockNumber: 19000000,
Type: 3,
MaxFeePerBlobGas: "1000000000",
BlobVersionedHashes: []string{"0x01hash1", "0x01hash2"},
BlobGasUsed: 262144,
BlobGasPrice: "100",
}
if tx.Type != 3 {
t.Errorf("Type = %d, want 3", tx.Type)
}
if len(tx.BlobVersionedHashes) != 2 {
t.Errorf("BlobVersionedHashes length = %d, want 2", len(tx.BlobVersionedHashes))
}
if tx.BlobGasUsed != 2*BlobGasPerBlob {
t.Errorf("BlobGasUsed = %d, want %d", tx.BlobGasUsed, 2*BlobGasPerBlob)
}
}
func TestEnhancedTransactionDepositType(t *testing.T) {
// Optimism deposit type 126 (0x7E)
tx := EnhancedTransaction{
Hash: "0xdeposit",
BlockNumber: 100000,
Type: 0x7E,
From: "0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001",
To: "0x4200000000000000000000000000000000000015",
Value: "1000000000000000000",
}
if tx.Type != 126 {
t.Errorf("Type = %d, want 126", tx.Type)
}
// L1 attributes depositer address (OP system)
if tx.From != "0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001" {
t.Errorf("From should be L1 attributes depositer address")
}
}
// ---- L2 Gas Cost Estimation Tests ----
func TestL2GasCostEstimation(t *testing.T) {
// L2 gas = L2 execution gas + L1 data fee
tests := []struct {
name string
l2Gas uint64
l2GasPrice *big.Int
l1DataFee *big.Int
wantTotalWei *big.Int
}{
{
name: "standard tx",
l2Gas: 21000,
l2GasPrice: big.NewInt(100000000), // 0.1 gwei
l1DataFee: big.NewInt(50000000000000), // 0.00005 ETH
wantTotalWei: new(big.Int).Add(new(big.Int).Mul(big.NewInt(21000), big.NewInt(100000000)), big.NewInt(50000000000000)),
},
{
name: "contract call",
l2Gas: 200000,
l2GasPrice: big.NewInt(100000000),
l1DataFee: big.NewInt(100000000000000), // 0.0001 ETH
wantTotalWei: new(big.Int).Add(new(big.Int).Mul(big.NewInt(200000), big.NewInt(100000000)), big.NewInt(100000000000000)),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l2Cost := new(big.Int).Mul(big.NewInt(int64(tt.l2Gas)), tt.l2GasPrice)
totalCost := new(big.Int).Add(l2Cost, tt.l1DataFee)
if totalCost.Cmp(tt.wantTotalWei) != 0 {
t.Errorf("total cost = %s, want %s", totalCost.String(), tt.wantTotalWei.String())
}
// L1 data fee should be larger than L2 execution cost for most L2s
if tt.l1DataFee.Cmp(l2Cost) < 0 {
// This is expected for cheap L2s, not an error
}
})
}
}
// ---- Benchmarks ----
func BenchmarkL2DepositTxMarshal(b *testing.B) {
tx := L2DepositTx{
Hash: "0xdeposit",
L1TxHash: "0xl1tx",
L1BlockNumber: 18000000,
L2BlockNumber: 100000,
From: "0xfrom",
To: "0xto",
Value: "1000000000000000000",
Status: "relayed",
Timestamp: time.Now(),
L2TxType: L2TxTypeDeposit,
SourceChain: "ethereum",
DestChain: "optimism",
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
json.Marshal(tx)
}
}
func BenchmarkZkSyncBatchMarshal(b *testing.B) {
batch := ZkSyncBatch{
BatchNumber: 10000,
CommitTxHash: "0xcommit",
ProveTxHash: "0xprove",
ExecuteTxHash: "0xexecute",
L1BlockNumber: 18000000,
L2StartBlock: 20000000,
L2EndBlock: 20000099,
RootHash: "0x" + strings.Repeat("ab", 32),
Status: "executed",
Timestamp: time.Now(),
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
json.Marshal(batch)
}
}
func BenchmarkCrossChainMessageMarshal(b *testing.B) {
msg := CrossChainMessage{
MsgHash: "0x" + strings.Repeat("ab", 32),
Direction: "l1_to_l2",
SourceTxHash: "0xsource",
DestTxHash: "0xdest",
SourceChainID: 1,
DestChainID: 10,
Sender: "0xsender",
Target: "0xtarget",
Value: "1000000000000000000",
Nonce: 100,
Status: "relayed",
Timestamp: time.Now(),
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
json.Marshal(msg)
}
}