diff --git a/op-e2e/system_fpp_test.go b/op-e2e/system_fpp_test.go
index 4da5f75ca51c185843e6681d229ee0a160b3b19d..635276fe159a8271a181eb0224690b173b9454fb 100644
--- a/op-e2e/system_fpp_test.go
+++ b/op-e2e/system_fpp_test.go
@@ -6,20 +6,22 @@ import (
 	"testing"
 	"time"
 
+	"github.com/stretchr/testify/require"
+
+	"github.com/ethereum/go-ethereum/accounts/abi/bind"
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/common/hexutil"
+	"github.com/ethereum/go-ethereum/log"
+	"github.com/ethereum/go-ethereum/rpc"
+
 	"github.com/ethereum-optimism/optimism/op-chain-ops/genesis"
 	"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/geth"
-	"github.com/ethereum-optimism/optimism/op-program/client/driver"
+	"github.com/ethereum-optimism/optimism/op-program/client/claim"
 	opp "github.com/ethereum-optimism/optimism/op-program/host"
 	oppconf "github.com/ethereum-optimism/optimism/op-program/host/config"
 	"github.com/ethereum-optimism/optimism/op-service/client"
 	"github.com/ethereum-optimism/optimism/op-service/sources"
 	"github.com/ethereum-optimism/optimism/op-service/testlog"
-	"github.com/ethereum/go-ethereum/accounts/abi/bind"
-	"github.com/ethereum/go-ethereum/common"
-	"github.com/ethereum/go-ethereum/common/hexutil"
-	"github.com/ethereum/go-ethereum/log"
-	"github.com/ethereum/go-ethereum/rpc"
-	"github.com/stretchr/testify/require"
 )
 
 func TestVerifyL2OutputRoot(t *testing.T) {
@@ -320,7 +322,7 @@ func testFaultProofProgramScenario(t *testing.T, ctx context.Context, sys *Syste
 	if s.Detached {
 		require.Error(t, err, "exit status 1")
 	} else {
-		require.ErrorIs(t, err, driver.ErrClaimNotValid)
+		require.ErrorIs(t, err, claim.ErrClaimNotValid)
 	}
 }
 
diff --git a/op-program/client/claim/validate.go b/op-program/client/claim/validate.go
new file mode 100644
index 0000000000000000000000000000000000000000..05655208c6e78a7b4eef38e7a2b908669bd47a5d
--- /dev/null
+++ b/op-program/client/claim/validate.go
@@ -0,0 +1,34 @@
+package claim
+
+import (
+	"context"
+	"errors"
+	"fmt"
+
+	"github.com/ethereum/go-ethereum/log"
+
+	"github.com/ethereum-optimism/optimism/op-service/eth"
+)
+
+var ErrClaimNotValid = errors.New("invalid claim")
+
+type L2Source interface {
+	L2BlockRefByLabel(ctx context.Context, label eth.BlockLabel) (eth.L2BlockRef, error)
+	L2OutputRoot(uint64) (eth.Bytes32, error)
+}
+
+func ValidateClaim(log log.Logger, l2ClaimBlockNum uint64, claimedOutputRoot eth.Bytes32, src L2Source) error {
+	l2Head, err := src.L2BlockRefByLabel(context.Background(), eth.Safe)
+	if err != nil {
+		return fmt.Errorf("cannot retrieve safe head: %w", err)
+	}
+	outputRoot, err := src.L2OutputRoot(min(l2ClaimBlockNum, l2Head.Number))
+	if err != nil {
+		return fmt.Errorf("calculate L2 output root: %w", err)
+	}
+	log.Info("Validating claim", "head", l2Head, "output", outputRoot, "claim", claimedOutputRoot)
+	if claimedOutputRoot != outputRoot {
+		return fmt.Errorf("%w: claim: %v actual: %v", ErrClaimNotValid, claimedOutputRoot, outputRoot)
+	}
+	return nil
+}
diff --git a/op-program/client/claim/validate_test.go b/op-program/client/claim/validate_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..c8a9b263eba5299f1ca664152a6a76de804da6ad
--- /dev/null
+++ b/op-program/client/claim/validate_test.go
@@ -0,0 +1,113 @@
+package claim
+
+import (
+	"context"
+	"errors"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/ethereum/go-ethereum/log"
+
+	"github.com/ethereum-optimism/optimism/op-service/eth"
+	"github.com/ethereum-optimism/optimism/op-service/testlog"
+)
+
+type mockL2 struct {
+	safeL2    eth.L2BlockRef
+	safeL2Err error
+
+	outputRoot    eth.Bytes32
+	outputRootErr error
+
+	requestedOutputRoot uint64
+}
+
+func (m *mockL2) L2BlockRefByLabel(ctx context.Context, label eth.BlockLabel) (eth.L2BlockRef, error) {
+	if label != eth.Safe {
+		panic("unexpected usage")
+	}
+	if m.safeL2Err != nil {
+		return eth.L2BlockRef{}, m.safeL2Err
+	}
+	return m.safeL2, nil
+}
+
+func (m *mockL2) L2OutputRoot(u uint64) (eth.Bytes32, error) {
+	m.requestedOutputRoot = u
+	if m.outputRootErr != nil {
+		return eth.Bytes32{}, m.outputRootErr
+	}
+	return m.outputRoot, nil
+}
+
+var _ L2Source = (*mockL2)(nil)
+
+func TestValidateClaim(t *testing.T) {
+	t.Run("Valid", func(t *testing.T) {
+		expected := eth.Bytes32{0x11}
+		l2 := &mockL2{
+			outputRoot: expected,
+		}
+		logger := testlog.Logger(t, log.LevelError)
+		err := ValidateClaim(logger, uint64(0), expected, l2)
+		require.NoError(t, err)
+	})
+
+	t.Run("Valid-PriorToSafeHead", func(t *testing.T) {
+		expected := eth.Bytes32{0x11}
+		l2 := &mockL2{
+			outputRoot: expected,
+			safeL2: eth.L2BlockRef{
+				Number: 10,
+			},
+		}
+		logger := testlog.Logger(t, log.LevelError)
+		err := ValidateClaim(logger, uint64(20), expected, l2)
+		require.NoError(t, err)
+		require.Equal(t, uint64(10), l2.requestedOutputRoot)
+	})
+
+	t.Run("Invalid", func(t *testing.T) {
+		l2 := &mockL2{
+			outputRoot: eth.Bytes32{0x22},
+		}
+		logger := testlog.Logger(t, log.LevelError)
+		err := ValidateClaim(logger, uint64(0), eth.Bytes32{0x11}, l2)
+		require.ErrorIs(t, err, ErrClaimNotValid)
+	})
+
+	t.Run("Invalid-PriorToSafeHead", func(t *testing.T) {
+		l2 := &mockL2{
+			outputRoot: eth.Bytes32{0x22},
+			safeL2:     eth.L2BlockRef{Number: 10},
+		}
+		logger := testlog.Logger(t, log.LevelError)
+		err := ValidateClaim(logger, uint64(20), eth.Bytes32{0x55}, l2)
+		require.ErrorIs(t, err, ErrClaimNotValid)
+		require.Equal(t, uint64(10), l2.requestedOutputRoot)
+	})
+
+	t.Run("Error-safe-head", func(t *testing.T) {
+		expectedErr := errors.New("boom")
+		l2 := &mockL2{
+			outputRoot: eth.Bytes32{0x11},
+			safeL2:     eth.L2BlockRef{Number: 10},
+			safeL2Err:  expectedErr,
+		}
+		logger := testlog.Logger(t, log.LevelError)
+		err := ValidateClaim(logger, uint64(0), eth.Bytes32{0x11}, l2)
+		require.ErrorIs(t, err, expectedErr)
+	})
+	t.Run("Error-output-root", func(t *testing.T) {
+		expectedErr := errors.New("boom")
+		l2 := &mockL2{
+			outputRoot:    eth.Bytes32{0x11},
+			outputRootErr: expectedErr,
+			safeL2:        eth.L2BlockRef{Number: 10},
+		}
+		logger := testlog.Logger(t, log.LevelError)
+		err := ValidateClaim(logger, uint64(0), eth.Bytes32{0x11}, l2)
+		require.ErrorIs(t, err, expectedErr)
+	})
+}
diff --git a/op-program/client/driver/driver.go b/op-program/client/driver/driver.go
index 7d2235c3a45e071d715e802767cbf25167ce8e69..bbeeb99bba29b661d4133cc9082f8ed479df24bf 100644
--- a/op-program/client/driver/driver.go
+++ b/op-program/client/driver/driver.go
@@ -19,8 +19,6 @@ import (
 	"github.com/ethereum-optimism/optimism/op-service/eth"
 )
 
-var ErrClaimNotValid = errors.New("invalid claim")
-
 type Derivation interface {
 	Step(ctx context.Context) error
 }
@@ -154,16 +152,3 @@ func (d *Driver) Step(ctx context.Context) error {
 func (d *Driver) SafeHead() eth.L2BlockRef {
 	return d.deriver.SafeL2Head()
 }
-
-func (d *Driver) ValidateClaim(l2ClaimBlockNum uint64, claimedOutputRoot eth.Bytes32) error {
-	l2Head := d.SafeHead()
-	outputRoot, err := d.l2OutputRoot(min(l2ClaimBlockNum, l2Head.Number))
-	if err != nil {
-		return fmt.Errorf("calculate L2 output root: %w", err)
-	}
-	d.logger.Info("Validating claim", "head", l2Head, "output", outputRoot, "claim", claimedOutputRoot)
-	if claimedOutputRoot != outputRoot {
-		return fmt.Errorf("%w: claim: %v actual: %v", ErrClaimNotValid, claimedOutputRoot, outputRoot)
-	}
-	return nil
-}
diff --git a/op-program/client/driver/driver_test.go b/op-program/client/driver/driver_test.go
index d61484020289c23cf0cce83a9602e18c3a28e9cb..23516696bc8df76b33c8495b66b519e7447199d8 100644
--- a/op-program/client/driver/driver_test.go
+++ b/op-program/client/driver/driver_test.go
@@ -69,63 +69,6 @@ func TestNoError(t *testing.T) {
 	require.NoError(t, err)
 }
 
-func TestValidateClaim(t *testing.T) {
-	t.Run("Valid", func(t *testing.T) {
-		driver := createDriver(t, io.EOF)
-		expected := eth.Bytes32{0x11}
-		driver.l2OutputRoot = func(_ uint64) (eth.Bytes32, error) {
-			return expected, nil
-		}
-		err := driver.ValidateClaim(uint64(0), expected)
-		require.NoError(t, err)
-	})
-
-	t.Run("Valid-PriorToSafeHead", func(t *testing.T) {
-		driver := createDriverWithNextBlock(t, io.EOF, 10)
-		expected := eth.Bytes32{0x11}
-		requestedOutputRoot := uint64(0)
-		driver.l2OutputRoot = func(blockNum uint64) (eth.Bytes32, error) {
-			requestedOutputRoot = blockNum
-			return expected, nil
-		}
-		err := driver.ValidateClaim(uint64(20), expected)
-		require.NoError(t, err)
-		require.Equal(t, uint64(10), requestedOutputRoot)
-	})
-
-	t.Run("Invalid", func(t *testing.T) {
-		driver := createDriver(t, io.EOF)
-		driver.l2OutputRoot = func(_ uint64) (eth.Bytes32, error) {
-			return eth.Bytes32{0x22}, nil
-		}
-		err := driver.ValidateClaim(uint64(0), eth.Bytes32{0x11})
-		require.ErrorIs(t, err, ErrClaimNotValid)
-	})
-
-	t.Run("Invalid-PriorToSafeHead", func(t *testing.T) {
-		driver := createDriverWithNextBlock(t, io.EOF, 10)
-		expected := eth.Bytes32{0x11}
-		requestedOutputRoot := uint64(0)
-		driver.l2OutputRoot = func(blockNum uint64) (eth.Bytes32, error) {
-			requestedOutputRoot = blockNum
-			return expected, nil
-		}
-		err := driver.ValidateClaim(uint64(20), eth.Bytes32{0x55})
-		require.ErrorIs(t, err, ErrClaimNotValid)
-		require.Equal(t, uint64(10), requestedOutputRoot)
-	})
-
-	t.Run("Error", func(t *testing.T) {
-		driver := createDriver(t, io.EOF)
-		expectedErr := errors.New("boom")
-		driver.l2OutputRoot = func(_ uint64) (eth.Bytes32, error) {
-			return eth.Bytes32{}, expectedErr
-		}
-		err := driver.ValidateClaim(uint64(0), eth.Bytes32{0x11})
-		require.ErrorIs(t, err, expectedErr)
-	})
-}
-
 func createDriver(t *testing.T, derivationResult error) *Driver {
 	return createDriverWithNextBlock(t, derivationResult, 0)
 }
diff --git a/op-program/client/program.go b/op-program/client/program.go
index a471941f2005399ad35c142ebe87777d1798be6d..d9c3531c46529b3a84271528e998cef51ed52399 100644
--- a/op-program/client/program.go
+++ b/op-program/client/program.go
@@ -13,6 +13,7 @@ import (
 
 	"github.com/ethereum-optimism/optimism/op-node/rollup"
 	preimage "github.com/ethereum-optimism/optimism/op-preimage"
+	"github.com/ethereum-optimism/optimism/op-program/client/claim"
 	cldr "github.com/ethereum-optimism/optimism/op-program/client/driver"
 	"github.com/ethereum-optimism/optimism/op-program/client/l1"
 	"github.com/ethereum-optimism/optimism/op-program/client/l2"
@@ -26,7 +27,7 @@ func Main(logger log.Logger) {
 	log.Info("Starting fault proof program client")
 	preimageOracle := CreatePreimageChannel()
 	preimageHinter := CreateHinterChannel()
-	if err := RunProgram(logger, preimageOracle, preimageHinter); errors.Is(err, cldr.ErrClaimNotValid) {
+	if err := RunProgram(logger, preimageOracle, preimageHinter); errors.Is(err, claim.ErrClaimNotValid) {
 		log.Error("Claim is invalid", "err", err)
 		os.Exit(1)
 	} else if err != nil {
@@ -79,7 +80,7 @@ func runDerivation(logger log.Logger, cfg *rollup.Config, l2Cfg *params.ChainCon
 			return err
 		}
 	}
-	return d.ValidateClaim(l2ClaimBlockNum, eth.Bytes32(l2Claim))
+	return claim.ValidateClaim(logger, l2ClaimBlockNum, eth.Bytes32(l2Claim), l2Source)
 }
 
 func CreateHinterChannel() oppio.FileChannel {