diff --git a/op-acceptance-tests/tests/altda/altda_test.go b/op-acceptance-tests/tests/altda/altda_test.go new file mode 100644 index 0000000000..17199aa4c1 --- /dev/null +++ b/op-acceptance-tests/tests/altda/altda_test.go @@ -0,0 +1,107 @@ +package altda + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/dsl" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum-optimism/optimism/op-devstack/stack/match" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" +) + +// TestAltDA_SafeHeadProgresses verifies the full AltDA pipeline: +// 1. Batcher posts batch data to the DA server +// 2. Batcher posts only the commitment to L1 +// 3. Op-node reads commitment from L1 +// 4. Op-node fetches batch data from DA server using the commitment +// 5. Op-node derives the batch and advances the safe head +// +// If the safe head advances after an L2 transaction, the entire AltDA +// derivation pipeline is proven to work end-to-end. +func TestAltDA_SafeHeadProgresses(gt *testing.T) { + t := devtest.SerialT(gt) + sys := presets.NewMinimal(t) + l := t.Logger() + + // Record initial safe head + initialSafe := sys.L2CL.HeadBlockRef(types.LocalSafe) + l.Info("Initial safe head", "number", initialSafe.Number, "hash", initialSafe.Hash) + + // Send an L2 transaction + alice := sys.FunderL2.NewFundedEOA(eth.OneTenthEther) + bob := sys.Wallet.NewEOA(sys.L2EL) + alice.Transfer(bob.Address(), eth.OneHundredthEther) + + // Wait for safe head to advance — this is the critical assertion. + // Safe head can only advance if the batcher submitted a commitment, + // and op-node resolved it via the DA server. + dsl.CheckAll(t, sys.L2CL.AdvancedFn(types.LocalSafe, 1, 30)) + + // Log final sync status as evidence + status := sys.L2CL.SyncStatus() + l.Info("AltDA chain progressed successfully", + "unsafeL2", status.UnsafeL2.Number, + "safeL2", status.SafeL2.Number, + "localSafeL2", status.LocalSafeL2.Number, + ) + + // Verify L1 batch transactions contain commitments (start with 0x01), + // not raw batch data (which starts with 0x00). + verifyL1CommitmentData(t, sys) +} + +// verifyL1CommitmentData scans recent L1 blocks for batcher transactions +// and verifies they contain AltDA commitment data (version byte 0x01). +func verifyL1CommitmentData(t devtest.T, sys *presets.Minimal) { + l := t.Logger() + rollupCfg := sys.L2Chain.Escape().RollupConfig() + batchInbox := rollupCfg.BatchInboxAddress + + l1EC := sys.L1Network.Escape().L1ELNode(match.FirstL1EL).EthClient() + + ctx, cancel := context.WithTimeout(t.Ctx(), dsl.DefaultTimeout) + defer cancel() + + head, err := l1EC.BlockRefByLabel(ctx, eth.Unsafe) + require.NoError(t, err) + + // Scan backwards through recent L1 blocks looking for batcher txs + scanFloor := uint64(0) + if head.Number > 20 { + scanFloor = head.Number - 20 + } + foundCommitment := false + for blockNum := head.Number; blockNum > scanFloor; blockNum-- { + _, txs, err := l1EC.InfoAndTxsByNumber(ctx, blockNum) + require.NoError(t, err) + + for _, tx := range txs { + if tx.To() == nil || *tx.To() != batchInbox { + continue + } + data := tx.Data() + if len(data) == 0 { + continue + } + // In AltDA mode, batcher prefixes commitment data with DerivationVersion1 (0x01). + // Regular calldata batches use DerivationVersion0 (0x00). + version := data[0] + l.Info("Found batcher tx on L1", + "block", blockNum, + "txHash", tx.Hash(), + "dataLen", len(data), + "versionByte", version, + ) + require.Equal(t, byte(0x01), version, + "batcher tx should contain AltDA commitment (version 0x01), got version 0x%02x", version) + foundCommitment = true + } + } + require.True(t, foundCommitment, "should find at least one batcher commitment tx on L1") + l.Info("L1 commitment data verified — batcher posted commitments, not raw batch data") +} diff --git a/op-acceptance-tests/tests/altda/init_test.go b/op-acceptance-tests/tests/altda/init_test.go new file mode 100644 index 0000000000..7fbf670429 --- /dev/null +++ b/op-acceptance-tests/tests/altda/init_test.go @@ -0,0 +1,60 @@ +package altda + +import ( + "testing" + + "github.com/ethereum-optimism/optimism/op-chain-ops/devkeys" + "github.com/ethereum-optimism/optimism/op-devstack/presets" + "github.com/ethereum-optimism/optimism/op-devstack/stack" + "github.com/ethereum-optimism/optimism/op-devstack/sysgo" +) + +// minimalSystemNoChallenger mirrors DefaultMinimalSystem but omits the L2 challenger +// (which requires the cannon binary) since AltDA tests don't need fault proofs. +func minimalSystemNoChallenger() stack.Option[*sysgo.Orchestrator] { + ids := sysgo.NewDefaultMinimalSystemIDs(sysgo.DefaultL1ID, sysgo.DefaultL2AID) + dest := &sysgo.DefaultMinimalSystemIDs{} + + opt := stack.Combine[*sysgo.Orchestrator]() + + opt.Add(stack.BeforeDeploy(func(o *sysgo.Orchestrator) { + o.P().Logger().Info("Setting up (AltDA, no challenger)") + })) + + opt.Add(sysgo.WithMnemonicKeys(devkeys.TestMnemonic)) + + opt.Add(sysgo.WithDeployer(), + sysgo.WithDeployerOptions( + sysgo.WithLocalContractSources(), + sysgo.WithCommons(ids.L1.ChainID()), + sysgo.WithPrefundedL2(ids.L1.ChainID(), ids.L2.ChainID()), + ), + ) + + opt.Add(sysgo.WithL1Nodes(ids.L1EL, ids.L1CL)) + + opt.Add(sysgo.WithL2ELNode(ids.L2EL)) + opt.Add(sysgo.WithL2CLNode(ids.L2CL, ids.L1CL, ids.L1EL, ids.L2EL, sysgo.L2CLSequencer())) + + opt.Add(sysgo.WithBatcher(ids.L2Batcher, ids.L1EL, ids.L2CL, ids.L2EL)) + opt.Add(sysgo.WithProposer(ids.L2Proposer, ids.L1EL, &ids.L2CL, nil)) + + opt.Add(sysgo.WithFaucets([]stack.L1ELNodeID{ids.L1EL}, []stack.L2ELNodeID{ids.L2EL})) + + opt.Add(sysgo.WithTestSequencer(ids.TestSequencer, ids.L1CL, ids.L2CL, ids.L1EL, ids.L2EL)) + + // Deliberately omit WithL2Challenger — AltDA tests don't need fault proofs. + + opt.Add(stack.Finally(func(orch *sysgo.Orchestrator) { + *dest = ids + })) + + return opt +} + +func TestMain(m *testing.M) { + presets.DoMain(m, + stack.MakeCommon(sysgo.WithAltDA(sysgo.DefaultL2AID)), + stack.MakeCommon[*sysgo.Orchestrator](minimalSystemNoChallenger()), + ) +} diff --git a/op-devstack/sysgo/da_server.go b/op-devstack/sysgo/da_server.go new file mode 100644 index 0000000000..f9a44428fe --- /dev/null +++ b/op-devstack/sysgo/da_server.go @@ -0,0 +1,77 @@ +package sysgo + +import ( + "os" + + "github.com/ethereum/go-ethereum/common" + + altda "github.com/ethereum-optimism/optimism/op-alt-da" + bss "github.com/ethereum-optimism/optimism/op-batcher/batcher" + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/stack" + "github.com/ethereum-optimism/optimism/op-node/rollup" + "github.com/ethereum-optimism/optimism/op-service/eth" +) + +// WithAltDA starts a DA server and configures the batcher and op-node to use AltDA mode +// with Keccak256 commitments. The DA server computes keccak256(data) as the commitment, +// which the Rust AltDA data source can verify inside the zkVM proof. +// +// The DA server is started and batcher/L2CL options are accumulated during the Deploy phase. +// The rollup config is overridden via L2CLConfig.RollupAltDAConfig, which is applied +// inside op-node's AfterDeploy hook (after the deployer registers the L2 network). +// This avoids ordering dependencies with the deployer's AfterDeploy. +func WithAltDA(l2ChainID eth.ChainID) stack.Option[*Orchestrator] { + return stack.Deploy(func(orch *Orchestrator) { + p := orch.P() + logger := p.Logger() + + // Start DA server with in-memory storage (Keccak256 commitment mode) + store := altda.NewMemStore() + server := altda.NewDAServer("127.0.0.1", 0, store, logger, false) + p.Require().NoError(server.Start(), "failed to start DA server") + p.Cleanup(func() { + logger.Info("Stopping DA server") + _ = server.Stop() + }) + + endpoint := server.HttpEndpoint() + logger.Info("Started AltDA server", "endpoint", endpoint) + + // Export DA server URL for op-succinct proposer subprocess. + // The Rust host reads ALTDA_SERVER_URL from env to fetch batch data. + os.Setenv("ALTDA_SERVER_URL", endpoint) + + altDACLICfg := altda.CLIConfig{ + Enabled: true, + DAServerURL: endpoint, + VerifyOnRead: true, + GenericDA: false, + } + + // Keccak256 mode requires a non-zero DAChallengeAddress to pass rollup config validation. + // The address is unused — challenges are never triggered in e2e tests. + altDAConfig := &rollup.AltDAConfig{ + CommitmentType: altda.KeccakCommitmentString, + DAChallengeAddress: common.HexToAddress("0x0000000000000000000000000000000000000001"), + DAChallengeWindow: 10, + DAResolveWindow: 10, + } + + // Append batcher option — consumed by WithBatcher's AfterDeploy + orch.batcherOptions = append(orch.batcherOptions, func(id stack.L2BatcherID, cfg *bss.CLIConfig) { + if id.ChainID() == l2ChainID { + cfg.AltDA = altDACLICfg + } + }) + + // Append L2CL option — consumed by WithOpNode's AfterDeploy. + // Sets both the CLI config and the rollup config override. + orch.l2CLOptions = append(orch.l2CLOptions, L2CLOptionFn(func(p devtest.P, id stack.L2CLNodeID, cfg *L2CLConfig) { + if id.ChainID() == l2ChainID { + cfg.AltDA = altDACLICfg + cfg.RollupAltDAConfig = altDAConfig + } + })) + }) +} diff --git a/op-devstack/sysgo/deployer_succinct.go b/op-devstack/sysgo/deployer_succinct.go index 9ae8704b99..ea65c3ecd3 100644 --- a/op-devstack/sysgo/deployer_succinct.go +++ b/op-devstack/sysgo/deployer_succinct.go @@ -364,7 +364,7 @@ func (o *Orchestrator) deployOpSuccinctL2OutputOracle( return "", fmt.Errorf("failed to write L1 chain config: %w", err) } - addr, err := execDeployOracle(o.P(), repoRoot, envFile) + addr, err := execDeployOracle(o.P(), repoRoot, envFile, cfgs.Features) if err != nil { return "", err } @@ -373,9 +373,15 @@ func (o *Orchestrator) deployOpSuccinctL2OutputOracle( return addr, nil } -// execDeployOracle runs `just deploy-oracle ` and parses the output -func execDeployOracle(p devtest.P, repoRoot, envFile string) (string, error) { - cmd := exec.CommandContext(p.Ctx(), "just", "deploy-oracle", envFile) +// execDeployOracle runs `just deploy-oracle [features]` and parses the output. +// If features is non-empty, it is passed as an extra argument so that deploy-oracle +// compiles fetch-l2oo-config with the matching Cargo feature flags (e.g., "altda"). +func execDeployOracle(p devtest.P, repoRoot, envFile, features string) (string, error) { + args := []string{"deploy-oracle", envFile} + if features != "" { + args = append(args, features) + } + cmd := exec.CommandContext(p.Ctx(), "just", args...) cmd.Dir = repoRoot logger := p.Logger().New("component", "succinct-deployer") @@ -394,6 +400,7 @@ type L2OOConfigs struct { SubmissionInterval *uint64 RangeProofInterval *uint64 FinalizationPeriodSecs *uint64 + Features string // Cargo features for deploy-oracle (e.g., "altda") } type L2OOOption func(*L2OOConfigs) @@ -428,6 +435,14 @@ func WithL2OOFinalizationPeriodSecs(n uint64) L2OOOption { } } +// WithL2OOFeatures sets Cargo features for the deploy-oracle step (e.g., "altda"). +// This ensures the vkey commitment matches the proposer binary's feature flags. +func WithL2OOFeatures(features string) L2OOOption { + return func(cfg *L2OOConfigs) { + cfg.Features = features + } +} + const defaultFinalizationPeriodSecs = 3600 // resolveFinalizationPeriodSecs returns the configured finalization period or the default. diff --git a/op-devstack/sysgo/l2_cl.go b/op-devstack/sysgo/l2_cl.go index 3e1b4a4e65..0eaed4d1ed 100644 --- a/op-devstack/sysgo/l2_cl.go +++ b/op-devstack/sysgo/l2_cl.go @@ -3,8 +3,10 @@ package sysgo import ( "os" + altda "github.com/ethereum-optimism/optimism/op-alt-da" "github.com/ethereum-optimism/optimism/op-devstack/devtest" "github.com/ethereum-optimism/optimism/op-devstack/stack" + "github.com/ethereum-optimism/optimism/op-node/rollup" nodeSync "github.com/ethereum-optimism/optimism/op-node/rollup/sync" "github.com/ethereum-optimism/optimism/op-service/eth" ) @@ -36,6 +38,13 @@ type L2CLConfig struct { // NoDiscovery is the flag to enable/disable discovery NoDiscovery bool + + // AltDA is the AltDA CLI configuration for this L2 CL node. + AltDA altda.CLIConfig + + // RollupAltDAConfig, when set, overrides the rollup config's AltDAConfig. + // This allows AltDA to be enabled without modifying the L2 network's rollup config directly. + RollupAltDAConfig *rollup.AltDAConfig } func L2CLSequencer() L2CLOption { diff --git a/op-devstack/sysgo/l2_cl_opnode.go b/op-devstack/sysgo/l2_cl_opnode.go index 6f867b2031..d294e65c50 100644 --- a/op-devstack/sysgo/l2_cl_opnode.go +++ b/op-devstack/sysgo/l2_cl_opnode.go @@ -13,7 +13,6 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" - altda "github.com/ethereum-optimism/optimism/op-alt-da" "github.com/ethereum-optimism/optimism/op-chain-ops/devkeys" "github.com/ethereum-optimism/optimism/op-devstack/devtest" "github.com/ethereum-optimism/optimism/op-devstack/shim" @@ -301,10 +300,13 @@ func WithOpNode(l2CLID stack.L2CLNodeID, l1CLID stack.L1CLNodeID, l1ELID stack.L ConductorEnabled: false, ConductorRpc: nil, ConductorRpcTimeout: 0, - AltDA: altda.CLIConfig{}, + AltDA: cfg.AltDA, IgnoreMissingPectraBlobSchedule: false, ExperimentalOPStackAPI: true, } + if cfg.RollupAltDAConfig != nil { + nodeCfg.Rollup.AltDAConfig = cfg.RollupAltDAConfig + } if cfg.SafeDBPath != "" { nodeCfg.SafeDBPath = cfg.SafeDBPath }