diff --git a/validator/proofenhancement/proof_enhancer.go b/validator/proofenhancement/proof_enhancer.go new file mode 100644 index 0000000000..b38bd9bedb --- /dev/null +++ b/validator/proofenhancement/proof_enhancer.go @@ -0,0 +1,127 @@ +// Copyright 2025, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md +package proofenhancement + +import ( + "context" + "fmt" + + "github.com/offchainlabs/nitro/arbutil" + "github.com/offchainlabs/nitro/daprovider" + "github.com/offchainlabs/nitro/staker" +) + +const ( + // Enhancement flag in machine status byte (first byte in proof) + ProofEnhancementFlag = 0x80 + + // Marker bytes for different enhancement types (last byte in an un-enhanced proof) + MarkerCustomDAReadPreimage = 0xDA + MarkerCustomDAValidateCertificate = 0xDB + + // SequencerMessageHeaderSize is the size of the sequencer message header + // (MinTimestamp + MaxTimestamp + MinL1Block + MaxL1Block + AfterDelayedMessages = 8+8+8+8+8) + SequencerMessageHeaderSize = 40 + + // Sizes for proof enhancement marker data + CertificateHashSize = 32 // Size of keccak256 hash of the certificate + OffsetSize = 8 // Size of uint64 offset + MarkerSize = 1 // Size of marker byte + CertificateSizeFieldSize = 8 // Size of uint64 certificate size field + + // MinCertificateSize is the minimum size of a certificate (just the header byte). + // Real certificates will have more data, but the proof enhancer system doesn't + // put any further constraints on certificate structure. + MinCertificateSize = 1 +) + +// ProofMarker identifies the type of proof enhancement needed +type ProofMarker byte + +// ProofEnhancer enhances one-step proofs with additional data. +// For proving certain opcodes, like for CustomDA, Arbitrator doesn't have enough information +// to generate the full proofs. In the case of CustomDA, daprovider implementations usually +// need network access to talk to external DA systems to get full proof details. To indicate +// that a proof needs enhancement, Arbitrator sets the ProofEnhancementFlag on the machine +// status byte of the proof that it returns, and also appends one of the Marker bytes to +// indicate which ProofEnhancer is required. +type ProofEnhancer interface { + // EnhanceProof checks if enhancement is needed and applies it + // Returns the enhanced proof or the original if no enhancement needed + EnhanceProof(ctx context.Context, messageNum arbutil.MessageIndex, proof []byte) ([]byte, error) +} + +// ProofEnhancementManager allows registration of ProofEnhancers and provides forwarding of EnhanceProof +// requests to the appropriate ProofEnhancer. +type ProofEnhancementManager struct { + enhancers map[ProofMarker]ProofEnhancer +} + +// NewProofEnhancementManager creates a new proof enhancement manager +func NewProofEnhancementManager() *ProofEnhancementManager { + return &ProofEnhancementManager{ + enhancers: make(map[ProofMarker]ProofEnhancer), + } +} + +// NewCustomDAProofEnhancer creates a ProofEnhancementManager pre-configured with both +// CustomDA proof enhancers (ReadPreimage and ValidateCertificate). This is the recommended +// constructor for production use with CustomDA systems. +// +// For testing or custom configurations, use NewProofEnhancementManager and RegisterEnhancer directly. +func NewCustomDAProofEnhancer( + daValidator daprovider.Validator, + inboxTracker staker.InboxTrackerInterface, + inboxReader staker.InboxReaderInterface, +) *ProofEnhancementManager { + manager := NewProofEnhancementManager() + + // Register both CustomDA enhancers + manager.RegisterEnhancer( + MarkerCustomDAReadPreimage, + NewReadPreimageProofEnhancer(daValidator, inboxTracker, inboxReader), + ) + manager.RegisterEnhancer( + MarkerCustomDAValidateCertificate, + NewValidateCertificateProofEnhancer(daValidator, inboxTracker, inboxReader), + ) + + return manager +} + +// RegisterEnhancer registers an enhancer for a specific marker byte +func (m *ProofEnhancementManager) RegisterEnhancer(marker ProofMarker, enhancer ProofEnhancer) { + m.enhancers[marker] = enhancer +} + +// EnhanceProof implements ProofEnhancer interface to forward EnhanceProof requests +// to the appropriate registered ProofEnhancer implementation, if there is any +// and proof enhancement was requested. +func (m *ProofEnhancementManager) EnhanceProof(ctx context.Context, messageNum arbutil.MessageIndex, proof []byte) ([]byte, error) { + if len(proof) == 0 { + return proof, nil + } + + // Check if enhancement flag is set + if proof[0]&ProofEnhancementFlag == 0 { + return proof, nil // No enhancement needed + } + + // Find marker at end of proof + if len(proof) < 2 { // Need at least the marker byte after the enhancement flag + return nil, fmt.Errorf("proof too short for enhancement: %d bytes", len(proof)) + } + marker := ProofMarker(proof[len(proof)-1]) + enhancer, exists := m.enhancers[marker] + if !exists { + return nil, fmt.Errorf("unknown enhancement marker: 0x%02x", marker) + } + + // Remove enhancement flag from machine status + enhancedProof := make([]byte, len(proof)) + copy(enhancedProof, proof) + enhancedProof[0] &= ^byte(ProofEnhancementFlag) + + // Let specific enhancer handle the proof + return enhancer.EnhanceProof(ctx, messageNum, enhancedProof) +} diff --git a/validator/proofenhancement/proof_enhancer_test.go b/validator/proofenhancement/proof_enhancer_test.go new file mode 100644 index 0000000000..6a55d5a9cd --- /dev/null +++ b/validator/proofenhancement/proof_enhancer_test.go @@ -0,0 +1,504 @@ +package proofenhancement + +import ( + "context" + "encoding/binary" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/offchainlabs/nitro/arbutil" + "github.com/offchainlabs/nitro/daprovider" + "github.com/offchainlabs/nitro/staker" + "github.com/offchainlabs/nitro/util/containers" +) + +// Mock implementations for testing - only implementing the methods we actually use +type mockInboxTracker struct { + batchForMessage uint64 + found bool + err error +} + +// Implement staker.InboxTrackerInterface - only the methods we use +func (m *mockInboxTracker) SetBlockValidator(v *staker.BlockValidator) {} +func (m *mockInboxTracker) GetDelayedMessageBytes(ctx context.Context, seqNum uint64) ([]byte, error) { + return nil, nil +} +func (m *mockInboxTracker) GetBatchMessageCount(seqNum uint64) (arbutil.MessageIndex, error) { + return 0, nil +} +func (m *mockInboxTracker) GetBatchAcc(seqNum uint64) (common.Hash, error) { + return common.Hash{}, nil +} +func (m *mockInboxTracker) GetBatchCount() (uint64, error) { + return 0, nil +} +func (m *mockInboxTracker) FindInboxBatchContainingMessage(msgNum arbutil.MessageIndex) (uint64, bool, error) { + return m.batchForMessage, m.found, m.err +} + +type mockInboxReader struct { + sequencerMessage []byte + err error +} + +// Implement staker.InboxReaderInterface - only the methods we use +func (m *mockInboxReader) GetSequencerMessageBytes(ctx context.Context, batchNum uint64) ([]byte, common.Hash, error) { + return m.sequencerMessage, common.Hash{}, m.err +} +func (m *mockInboxReader) GetFinalizedMsgCount(ctx context.Context) (arbutil.MessageIndex, error) { + return 0, nil +} + +type mockValidator struct { + generateReadPreimageProofResult []byte + generateCertValidityProofResult []byte + err error +} + +func (m *mockValidator) GenerateReadPreimageProof(certHash common.Hash, offset uint64, certificate []byte) containers.PromiseInterface[daprovider.PreimageProofResult] { + if m.err != nil { + return containers.NewReadyPromise(daprovider.PreimageProofResult{}, m.err) + } + return containers.NewReadyPromise(daprovider.PreimageProofResult{ + Proof: m.generateReadPreimageProofResult, + }, nil) +} + +func (m *mockValidator) GenerateCertificateValidityProof(certificate []byte) containers.PromiseInterface[daprovider.ValidityProofResult] { + if m.err != nil { + return containers.NewReadyPromise(daprovider.ValidityProofResult{}, m.err) + } + return containers.NewReadyPromise(daprovider.ValidityProofResult{ + Proof: m.generateCertValidityProofResult, + }, nil) +} + +func createTestCertificate(t *testing.T, data []byte) []byte { + // Create a simple test certificate + // Format: [header(1), dataHash(32), v(1), r(32), s(32)] + cert := make([]byte, 1+32+1+32+32) + cert[0] = daprovider.DACertificateMessageHeaderFlag + + // Use Keccak256 for data hash + dataHash := crypto.Keccak256(data) + copy(cert[1:33], dataHash) + + // Mock signature values (v, r, s) + cert[33] = 27 // v + // r and s are left as zeros for simplicity + + return cert +} + +func TestCustomDAProofEnhancement(t *testing.T) { + ctx := context.Background() + + // Test data + testData := []byte("test custom DA preimage data") + testCertificate := createTestCertificate(t, testData) + certHash := crypto.Keccak256Hash(testCertificate) + testOffset := uint64(10) + + // Create sequencer message with 40-byte header + certificate + sequencerMessage := make([]byte, 40+len(testCertificate)) + copy(sequencerMessage[40:], testCertificate) + + // Mock components + inboxTracker := &mockInboxTracker{ + batchForMessage: 123, + found: true, + } + + inboxReader := &mockInboxReader{ + sequencerMessage: sequencerMessage, + } + + // Mock validator that returns a simple proof + mockProof := []byte{0x01, 0x02, 0x03, 0x04} // Simple test proof + mockValidator := &mockValidator{ + generateReadPreimageProofResult: mockProof, + } + + // Create proof enhancer + enhancerManager := NewProofEnhancementManager() + customDAEnhancer := NewReadPreimageProofEnhancer(mockValidator, inboxTracker, inboxReader) + enhancerManager.RegisterEnhancer(MarkerCustomDAReadPreimage, customDAEnhancer) + + // Create a mock proof with enhancement flag and marker + // Format: [machine_status | 0x80, ...proof data..., certHash(32), offset(8), marker(1)] + originalProofSize := 100 + originalProof := make([]byte, originalProofSize+32+8+1) + originalProof[0] = 0x00 | ProofEnhancementFlag // Running status with enhancement flag + // Fill with some dummy proof data + for i := 1; i < originalProofSize; i++ { + originalProof[i] = byte(i) + } + // Add certificate hash + copy(originalProof[originalProofSize:originalProofSize+32], certHash[:]) + // Add offset + binary.BigEndian.PutUint64(originalProof[originalProofSize+32:originalProofSize+40], testOffset) + // Add marker + originalProof[originalProofSize+40] = MarkerCustomDAReadPreimage + + // Enhance the proof + testMessageNum := arbutil.MessageIndex(42) + enhancedProof, err := enhancerManager.EnhanceProof(ctx, testMessageNum, originalProof) + if err != nil { + t.Fatalf("Failed to enhance proof: %v", err) + } + + // Verify the enhanced proof: + // 1. Machine status should have enhancement flag removed + if enhancedProof[0]&ProofEnhancementFlag != 0 { + t.Error("Enhancement flag not removed from machine status") + } + + // 2. The marker data (certHash, offset, marker) should be removed + // Expected format: [...original proof..., certSize(8), certificate, customProof] + expectedSize := originalProofSize + 8 + len(testCertificate) + len(mockProof) + if len(enhancedProof) != expectedSize { + t.Errorf("Enhanced proof has wrong length: got %d, expected %d", len(enhancedProof), expectedSize) + } + + // 3. Verify original proof is preserved (minus enhancement flag) + for i := 1; i < originalProofSize; i++ { + if enhancedProof[i] != byte(i) { + t.Errorf("Original proof data modified at position %d: got %d, expected %d", i, enhancedProof[i], i) + break + } + } + + // 4. Verify certificate size + offset := originalProofSize + certSize := binary.BigEndian.Uint64(enhancedProof[offset : offset+8]) + if certSize != uint64(len(testCertificate)) { + t.Errorf("Wrong certificate size: got %d, expected %d", certSize, len(testCertificate)) + } + offset += 8 + + // 5. Verify certificate + // #nosec G115 + gotCertificate := enhancedProof[offset : offset+int(certSize)] + if !equal(gotCertificate, testCertificate) { + t.Errorf("Wrong certificate in enhanced proof") + } + // #nosec G115 + offset += int(certSize) + + // 6. Verify custom proof from validator + gotCustomProof := enhancedProof[offset:] + if !equal(gotCustomProof, mockProof) { + t.Errorf("Wrong custom proof: got %v, expected %v", gotCustomProof, mockProof) + } +} + +func TestNoEnhancementNeeded(t *testing.T) { + ctx := context.Background() + enhancerManager := NewProofEnhancementManager() + + // Create a proof without enhancement flag + mockProof := make([]byte, 100) + mockProof[0] = 0x00 // Running status without enhancement flag + for i := 1; i < 100; i++ { + mockProof[i] = byte(i) + } + + // Should return the same proof + result, err := enhancerManager.EnhanceProof(ctx, arbutil.MessageIndex(1), mockProof) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if len(result) != len(mockProof) { + t.Error("Proof was modified when no enhancement was needed") + } + for i := range result { + if result[i] != mockProof[i] { + t.Error("Proof content was modified when no enhancement was needed") + break + } + } +} + +func TestValidateCertificateProofEnhancement(t *testing.T) { + ctx := context.Background() + + // Test data + testData := []byte("test data for certificate validation") + testCertificate := createTestCertificate(t, testData) + certHash := crypto.Keccak256Hash(testCertificate) + + // Create sequencer message with 40-byte header + certificate + sequencerMessage := make([]byte, 40+len(testCertificate)) + copy(sequencerMessage[40:], testCertificate) + + // Mock components + inboxTracker := &mockInboxTracker{ + batchForMessage: 456, + found: true, + } + + inboxReader := &mockInboxReader{ + sequencerMessage: sequencerMessage, + } + + // Mock validator that returns a validity proof + mockValidityProof := []byte{0x01, 0x01} // Valid certificate, version 1 + mockValidator := &mockValidator{ + generateCertValidityProofResult: mockValidityProof, + } + + // Create proof enhancer + enhancerManager := NewProofEnhancementManager() + certEnhancer := NewValidateCertificateProofEnhancer(mockValidator, inboxTracker, inboxReader) + enhancerManager.RegisterEnhancer(MarkerCustomDAValidateCertificate, certEnhancer) + + // Create a mock proof with enhancement flag and marker + // Format: [machine_status | 0x80, ...proof data..., certHash(32), marker(1)] + originalProofSize := 100 + mockProof := make([]byte, originalProofSize+32+1) + mockProof[0] = 0x00 | ProofEnhancementFlag // Running status with enhancement flag + for i := 1; i < originalProofSize; i++ { + mockProof[i] = byte(i) + } + copy(mockProof[originalProofSize:originalProofSize+32], certHash[:]) + mockProof[originalProofSize+32] = MarkerCustomDAValidateCertificate + + // Enhance the proof + testMessageNum := arbutil.MessageIndex(789) + enhancedProof, err := enhancerManager.EnhanceProof(ctx, testMessageNum, mockProof) + if err != nil { + t.Fatalf("Failed to enhance proof: %v", err) + } + + // Verify the enhanced proof + if enhancedProof[0]&ProofEnhancementFlag != 0 { + t.Error("Enhancement flag not removed from machine status") + } + + // Expected format: [...original proof..., certSize(8), certificate, validityProof] + expectedSize := originalProofSize + 8 + len(testCertificate) + len(mockValidityProof) + if len(enhancedProof) != expectedSize { + t.Errorf("Enhanced proof has wrong length: got %d, expected %d", len(enhancedProof), expectedSize) + } + + // Verify certificate size and data + offset := originalProofSize + certSize := binary.BigEndian.Uint64(enhancedProof[offset : offset+8]) + if certSize != uint64(len(testCertificate)) { + t.Errorf("Wrong certificate size: got %d, expected %d", certSize, len(testCertificate)) + } + offset += 8 + + // #nosec G115 + gotCertificate := enhancedProof[offset : offset+int(certSize)] + if !equal(gotCertificate, testCertificate) { + t.Errorf("Wrong certificate in enhanced proof") + } + // #nosec G115 + offset += int(certSize) + + // Verify validity proof + gotValidityProof := enhancedProof[offset:] + if !equal(gotValidityProof, mockValidityProof) { + t.Errorf("Wrong validity proof: got %v, expected %v", gotValidityProof, mockValidityProof) + } +} + +// Helper function to compare byte slices +func equal(a, b []byte) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +func TestNewCustomDAProofEnhancer(t *testing.T) { + ctx := context.Background() + + // Test data + testData := []byte("test custom DA data") + testCertificate := createTestCertificate(t, testData) + certHash := crypto.Keccak256Hash(testCertificate) + testOffset := uint64(10) + + // Create sequencer message + sequencerMessage := make([]byte, 40+len(testCertificate)) + copy(sequencerMessage[40:], testCertificate) + + inboxTracker := &mockInboxTracker{ + batchForMessage: 123, + found: true, + } + + inboxReader := &mockInboxReader{ + sequencerMessage: sequencerMessage, + } + + mockReadProof := []byte{0x01, 0x02, 0x03, 0x04} + mockValidityProof := []byte{0x01, 0x01} + mockValidator := &mockValidator{ + generateReadPreimageProofResult: mockReadProof, + generateCertValidityProofResult: mockValidityProof, + } + + // Create enhancer using convenience constructor + enhancer := NewCustomDAProofEnhancer(mockValidator, inboxTracker, inboxReader) + + // Test ReadPreimage enhancement + t.Run("ReadPreimageEnhancement", func(t *testing.T) { + // Create proof with ReadPreimage marker + originalProofSize := 100 + proof := make([]byte, originalProofSize+32+8+1) + proof[0] = ProofEnhancementFlag + for i := 1; i < originalProofSize; i++ { + proof[i] = byte(i) + } + copy(proof[originalProofSize:], certHash[:]) + binary.BigEndian.PutUint64(proof[originalProofSize+32:], testOffset) + proof[originalProofSize+40] = MarkerCustomDAReadPreimage + + enhanced, err := enhancer.EnhanceProof(ctx, arbutil.MessageIndex(42), proof) + if err != nil { + t.Fatalf("ReadPreimage enhancement failed: %v", err) + } + + // Verify it was enhanced (flag removed) + if enhanced[0]&ProofEnhancementFlag != 0 { + t.Error("Enhancement flag not removed") + } + }) + + // Test ValidateCertificate enhancement + t.Run("ValidateCertificateEnhancement", func(t *testing.T) { + // Create proof with ValidateCertificate marker + originalProofSize := 100 + proof := make([]byte, originalProofSize+32+1) + proof[0] = ProofEnhancementFlag + for i := 1; i < originalProofSize; i++ { + proof[i] = byte(i) + } + copy(proof[originalProofSize:], certHash[:]) + proof[originalProofSize+32] = MarkerCustomDAValidateCertificate + + enhanced, err := enhancer.EnhanceProof(ctx, arbutil.MessageIndex(789), proof) + if err != nil { + t.Fatalf("ValidateCertificate enhancement failed: %v", err) + } + + // Verify it was enhanced (flag removed) + if enhanced[0]&ProofEnhancementFlag != 0 { + t.Error("Enhancement flag not removed") + } + }) +} + +func TestProofEnhancerErrorCases(t *testing.T) { + ctx := context.Background() + + t.Run("UnknownMarker", func(t *testing.T) { + enhancerManager := NewProofEnhancementManager() + // Don't register any enhancers + + mockProof := make([]byte, 10) + mockProof[0] = ProofEnhancementFlag // Set enhancement flag + mockProof[9] = 0xFF // Unknown marker + + _, err := enhancerManager.EnhanceProof(ctx, 0, mockProof) + if err == nil { + t.Error("Expected error for unknown marker") + } + if err.Error() != "unknown enhancement marker: 0xff" { + t.Errorf("Wrong error message: %v", err) + } + }) + + t.Run("ProofTooShort", func(t *testing.T) { + enhancerManager := NewProofEnhancementManager() + + // Empty proof with enhancement flag + mockProof := []byte{} + + result, err := enhancerManager.EnhanceProof(ctx, 0, mockProof) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if len(result) != 0 { + t.Error("Empty proof should be returned unchanged") + } + }) + + t.Run("CertificateHashMismatch", func(t *testing.T) { + testCertificate := createTestCertificate(t, []byte("test data")) + wrongHash := crypto.Keccak256Hash([]byte("wrong data")) + + sequencerMessage := make([]byte, 40+len(testCertificate)) + copy(sequencerMessage[40:], testCertificate) + + inboxTracker := &mockInboxTracker{ + batchForMessage: 1, + found: true, + } + + inboxReader := &mockInboxReader{ + sequencerMessage: sequencerMessage, + } + + validator := &mockValidator{} + + enhancerManager := NewProofEnhancementManager() + enhancer := NewReadPreimageProofEnhancer(validator, inboxTracker, inboxReader) + enhancerManager.RegisterEnhancer(MarkerCustomDAReadPreimage, enhancer) + + // Create proof with wrong hash + mockProof := make([]byte, 100+32+8+1) + mockProof[0] = ProofEnhancementFlag + copy(mockProof[100:132], wrongHash[:]) + binary.BigEndian.PutUint64(mockProof[132:140], 0) + mockProof[140] = MarkerCustomDAReadPreimage + + _, err := enhancerManager.EnhanceProof(ctx, 0, mockProof) + if err == nil { + t.Error("Expected error for certificate hash mismatch") + } + if !strings.Contains(err.Error(), "certificate hash mismatch") { + t.Errorf("Wrong error message: %v", err) + } + }) + + t.Run("BatchNotFound", func(t *testing.T) { + inboxTracker := &mockInboxTracker{ + found: false, + } + + inboxReader := &mockInboxReader{} + validator := &mockValidator{} + + enhancerManager := NewProofEnhancementManager() + enhancer := NewReadPreimageProofEnhancer(validator, inboxTracker, inboxReader) + enhancerManager.RegisterEnhancer(MarkerCustomDAReadPreimage, enhancer) + + certHash := crypto.Keccak256Hash([]byte("test")) + mockProof := make([]byte, 100+32+8+1) + mockProof[0] = ProofEnhancementFlag + copy(mockProof[100:132], certHash[:]) + mockProof[140] = MarkerCustomDAReadPreimage + + _, err := enhancerManager.EnhanceProof(ctx, 42, mockProof) + if err == nil { + t.Error("Expected error when batch not found") + } + if !strings.Contains(err.Error(), "Couldn't find batch") { + t.Errorf("Wrong error message: %v", err) + } + }) +} diff --git a/validator/proofenhancement/readpreimage_proof_enhancer.go b/validator/proofenhancement/readpreimage_proof_enhancer.go new file mode 100644 index 0000000000..e8c0020b75 --- /dev/null +++ b/validator/proofenhancement/readpreimage_proof_enhancer.go @@ -0,0 +1,131 @@ +// Copyright 2025, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md +package proofenhancement + +import ( + "bytes" + "context" + "encoding/binary" + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/offchainlabs/nitro/arbutil" + "github.com/offchainlabs/nitro/daprovider" + "github.com/offchainlabs/nitro/staker" +) + +// ReadPreimageProofEnhancer enhances proofs that involve CustomDA preimage operations +type ReadPreimageProofEnhancer struct { + daValidator daprovider.Validator + inboxTracker staker.InboxTrackerInterface + inboxReader staker.InboxReaderInterface +} + +// NewReadPreimageProofEnhancer creates a new CustomDA proof enhancer +func NewReadPreimageProofEnhancer( + validator daprovider.Validator, + inboxTracker staker.InboxTrackerInterface, + inboxReader staker.InboxReaderInterface, +) *ReadPreimageProofEnhancer { + return &ReadPreimageProofEnhancer{ + daValidator: validator, + inboxTracker: inboxTracker, + inboxReader: inboxReader, + } +} + +// EnhanceProof implements ProofEnhancer for CustomDA +func (e *ReadPreimageProofEnhancer) EnhanceProof(ctx context.Context, messageNum arbutil.MessageIndex, proof []byte) ([]byte, error) { + batchContainingMessage, found, err := e.inboxTracker.FindInboxBatchContainingMessage(messageNum) + if err != nil { + return nil, err + } + if !found { + return nil, fmt.Errorf("Couldn't find batch for message #%d to enhance proof", messageNum) + } + + sequencerMessage, _, err := e.inboxReader.GetSequencerMessageBytes(ctx, batchContainingMessage) + if err != nil { + return nil, fmt.Errorf("failed to get sequencer message for batch %d: %w", batchContainingMessage, err) + } + + // Extract and validate certificate from sequencer message + if len(sequencerMessage) < SequencerMessageHeaderSize+1 { + return nil, fmt.Errorf("sequencer message too short: expected at least %d bytes, got %d", SequencerMessageHeaderSize+1, len(sequencerMessage)) + } + + // Extract certificate (skip sequencer message header) + certificate := sequencerMessage[SequencerMessageHeaderSize:] + + // Validate certificate format + if len(certificate) < MinCertificateSize { + return nil, fmt.Errorf("certificate too short: expected at least %d bytes, got %d", MinCertificateSize, len(certificate)) + } + + if certificate[0] != daprovider.DACertificateMessageHeaderFlag { + return nil, fmt.Errorf("invalid certificate header: expected 0x%02x, got 0x%02x", + daprovider.DACertificateMessageHeaderFlag, certificate[0]) + } + + // Extract keccak256 of the certificate and offset from end of proof + // Format: [...proof..., certKeccak256(32), offset(8), marker(1)] + minProofSize := CertificateHashSize + OffsetSize + MarkerSize + if len(proof) < minProofSize { + return nil, fmt.Errorf("proof too short for CustomDA enhancement: expected at least %d bytes, got %d", minProofSize, len(proof)) + } + + // The entire proof is of variable length, so we work backwards from + // final marker byte to find all the marker data added by serialize_proof() for CustomDA ReadPreImage. + markerPos := len(proof) - MarkerSize + offsetPos := markerPos - OffsetSize + certKeccak256Pos := offsetPos - CertificateHashSize + + // Verify marker + if proof[markerPos] != MarkerCustomDAReadPreimage { + return nil, fmt.Errorf("invalid marker for CustomDA enhancer: 0x%02x", proof[markerPos]) + } + + // Extract certKeccak256 and offset + var certKeccak256 [32]byte + copy(certKeccak256[:], proof[certKeccak256Pos:offsetPos]) + offset := binary.BigEndian.Uint64(proof[offsetPos:markerPos]) + + // Verify the certificate hash matches what's in the proof + certHash := crypto.Keccak256Hash(certificate) + if !bytes.Equal(certHash[:], certKeccak256[:]) { + return nil, fmt.Errorf("certificate hash mismatch: expected %x, got %x", certKeccak256, certHash) + } + + // Generate custom proof with certificate + promise := e.daValidator.GenerateReadPreimageProof(common.BytesToHash(certKeccak256[:]), offset, certificate) + result, err := promise.Await(ctx) + if err != nil { + return nil, fmt.Errorf("failed to generate custom DA proof: %w", err) + } + customProof := result.Proof + + // Build standard CustomDA proof preamble: + // [...proof..., certSize(8), certificate, customProof] + // We're dropping the CustomDA marker data (certKeccak256, offset, marker byte) from the original proof. + // It was only needed here to call GenerateReadPreimageProof above, the same information is + // available to the OSP in the instruction arguments. + certSize := uint64(len(certificate)) + markerDataStart := certKeccak256Pos // Start of CustomDA marker data that we'll drop + enhancedProof := make([]byte, markerDataStart+CertificateSizeFieldSize+len(certificate)+len(customProof)) + + // Copy original proof up to the CustomDA marker data + copy(enhancedProof, proof[:markerDataStart]) + + // Add certSize + binary.BigEndian.PutUint64(enhancedProof[markerDataStart:], certSize) + + // Add certificate + copy(enhancedProof[markerDataStart+CertificateSizeFieldSize:], certificate) + + // Add custom proof + copy(enhancedProof[markerDataStart+CertificateSizeFieldSize+len(certificate):], customProof) + + return enhancedProof, nil +} diff --git a/validator/proofenhancement/validatecertificate_proof_enhancer.go b/validator/proofenhancement/validatecertificate_proof_enhancer.go new file mode 100644 index 0000000000..0b58edf687 --- /dev/null +++ b/validator/proofenhancement/validatecertificate_proof_enhancer.go @@ -0,0 +1,113 @@ +// Copyright 2025, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md +package proofenhancement + +import ( + "context" + "encoding/binary" + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/offchainlabs/nitro/arbutil" + "github.com/offchainlabs/nitro/daprovider" + "github.com/offchainlabs/nitro/staker" +) + +type ValidateCertificateProofEnhancer struct { + daValidator daprovider.Validator + inboxTracker staker.InboxTrackerInterface + inboxReader staker.InboxReaderInterface +} + +func NewValidateCertificateProofEnhancer( + daValidator daprovider.Validator, + inboxTracker staker.InboxTrackerInterface, + inboxReader staker.InboxReaderInterface, +) *ValidateCertificateProofEnhancer { + return &ValidateCertificateProofEnhancer{ + daValidator: daValidator, + inboxTracker: inboxTracker, + inboxReader: inboxReader, + } +} + +func (e *ValidateCertificateProofEnhancer) EnhanceProof(ctx context.Context, messageNum arbutil.MessageIndex, proof []byte) ([]byte, error) { + // Extract the hash and marker from the proof + // Format: [...proof..., certHash(32), marker(1)] + minProofSize := CertificateHashSize + MarkerSize + if len(proof) < minProofSize { + return nil, fmt.Errorf("proof too short for ValidateCertificate enhancement: expected at least %d bytes, got %d", minProofSize, len(proof)) + } + + markerPos := len(proof) - MarkerSize + hashPos := markerPos - CertificateHashSize + + // Verify marker + if proof[markerPos] != MarkerCustomDAValidateCertificate { + return nil, fmt.Errorf("invalid marker for ValidateCertificate enhancer: 0x%02x", proof[markerPos]) + } + + // Extract certificate hash + var certHash [32]byte + copy(certHash[:], proof[hashPos:markerPos]) + + // Find the batch containing this message + batchContainingMessage, found, err := e.inboxTracker.FindInboxBatchContainingMessage(messageNum) + if err != nil { + return nil, err + } + if !found { + return nil, fmt.Errorf("couldn't find batch for message #%d to enhance proof", messageNum) + } + + // Get the sequencer message + sequencerMessage, _, err := e.inboxReader.GetSequencerMessageBytes(ctx, batchContainingMessage) + if err != nil { + return nil, fmt.Errorf("failed to get sequencer message for batch %d: %w", batchContainingMessage, err) + } + + // Extract certificate from sequencer message (skip sequencer message header) + if len(sequencerMessage) < SequencerMessageHeaderSize+1 { + return nil, fmt.Errorf("sequencer message too short: expected at least %d bytes, got %d", SequencerMessageHeaderSize+1, len(sequencerMessage)) + } + certificate := sequencerMessage[SequencerMessageHeaderSize:] + + // Verify the certificate hash matches what's requested + actualHash := crypto.Keccak256Hash(certificate) + if actualHash != common.BytesToHash(certHash[:]) { + return nil, fmt.Errorf("certificate hash mismatch: expected %x, got %x", certHash, actualHash) + } + + // Generate certificate validity proof + promise := e.daValidator.GenerateCertificateValidityProof(certificate) + result, err := promise.Await(ctx) + if err != nil { + return nil, fmt.Errorf("failed to generate certificate validity proof: %w", err) + } + validityProof := result.Proof + + // Build enhanced proof: [...originalProof..., certSize(8), certificate, validityProof] + // Remove the marker data (hash + marker) from original proof + originalProofLen := hashPos + certSize := uint64(len(certificate)) + enhancedProof := make([]byte, originalProofLen+CertificateSizeFieldSize+len(certificate)+len(validityProof)) + + // Copy original proof (without marker data) + copy(enhancedProof, proof[:originalProofLen]) + + // Add certSize + offset := originalProofLen + binary.BigEndian.PutUint64(enhancedProof[offset:], certSize) + offset += CertificateSizeFieldSize + + // Add certificate + copy(enhancedProof[offset:], certificate) + offset += len(certificate) + + // Add validity proof + copy(enhancedProof[offset:], validityProof) + + return enhancedProof, nil +}