Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
e91b05f
Implement Facet V2 batch system with EIP-4844 blob support
RogerPodacter Sep 9, 2025
be9bc92
Merge branch 'main' into batches
RogerPodacter Sep 25, 2025
6c28bbf
Fix tests and other housekeeping
RogerPodacter Sep 25, 2025
da249f8
Add sequencer app
RogerPodacter Sep 25, 2025
a0e9583
End-to-end test using MetaMask
RogerPodacter Sep 25, 2025
473a278
Integrate Spire DA Builder
RogerPodacter Sep 26, 2025
2756147
Fix query
RogerPodacter Sep 26, 2025
8de2ffb
Fixes
RogerPodacter Sep 26, 2025
06741a9
Remove dead code
RogerPodacter Sep 26, 2025
4568ef9
Merge pull request #66 from 0xFacet/da_builder
RogerPodacter Sep 26, 2025
0f06489
Fix build
RogerPodacter Sep 26, 2025
87520b0
Fix type
RogerPodacter Sep 26, 2025
b1b8bbc
Merge pull request #67 from 0xFacet/fix_build
RogerPodacter Sep 26, 2025
6ea8e54
Add sequencer builder
RogerPodacter Sep 26, 2025
a61ffb4
Lowercase
RogerPodacter Sep 26, 2025
f632711
Fix Dockerfile
RogerPodacter Sep 26, 2025
25536b6
Fix blobs
RogerPodacter Sep 26, 2025
1447c62
Add package-lock.json
RogerPodacter Sep 26, 2025
19dcc51
Simplify
RogerPodacter Sep 26, 2025
2013f5f
Simplify
RogerPodacter Sep 26, 2025
0f6a6d6
Fix target block computation
RogerPodacter Sep 26, 2025
785089c
Add test interface for sequencer
RogerPodacter Sep 26, 2025
f2e967b
Rename test-pages to docs for GitHub Pages
RogerPodacter Sep 26, 2025
5ca25d8
Update test page
RogerPodacter Sep 26, 2025
ff1a3ad
Support batch requests
RogerPodacter Sep 28, 2025
477916d
Better handle nil RPC response case
RogerPodacter Sep 29, 2025
ac41ddc
Profile import, fix speed
RogerPodacter Sep 29, 2025
77e2acc
Fix web page, tweaks
RogerPodacter Sep 29, 2025
01f987a
Better chain tip caching
RogerPodacter Sep 29, 2025
32dee95
Support type 0 and 1 txs in sequencer
RogerPodacter Sep 29, 2025
0a49bf8
Update docker compose
RogerPodacter Sep 29, 2025
35804be
Improve Beacon setup
RogerPodacter Sep 30, 2025
a3b15cb
Improve prefetcher
RogerPodacter Sep 30, 2025
2e179ed
Update lib/l1_rpc_prefetcher.rb
RogerPodacter Sep 30, 2025
2ad061d
Merge pull request #68 from 0xFacet/improve_prefetcher
RogerPodacter Sep 30, 2025
2f92e44
Remove dupe RPC calls
RogerPodacter Sep 30, 2025
0a99de6
Improve prefetcher
RogerPodacter Sep 30, 2025
4d2a139
Remove errant rescues
RogerPodacter Oct 1, 2025
b993944
Improve batch wire format
RogerPodacter Oct 1, 2025
2c2d8eb
Fix parsing bug
RogerPodacter Oct 1, 2025
9a88f84
Improve batch parsing
RogerPodacter Oct 1, 2025
271e854
Merge pull request #69 from 0xFacet/improve_wire_format
RogerPodacter Oct 1, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions .github/workflows/build-images.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
name: Build and Publish Images

on:
push:
branches: [ main ]
tags:
- 'node-v*'
- 'sequencer-v*'
workflow_dispatch:
inputs:
build_node:
description: 'Build Node image'
required: false
default: true
type: boolean
build_sequencer:
description: 'Build Sequencer image'
required: false
default: true
type: boolean

env:
REGISTRY: ghcr.io

jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
service:
- name: node
context: .
dockerfile: Dockerfile
image: ${{ github.repository_owner }}/facet-node
- name: sequencer
context: ./sequencer
dockerfile: Dockerfile
image: ${{ github.repository_owner }}/facet-sequencer

steps:
- name: Determine build target
id: should_build
run: |
if [[ "${{ github.event_name }}" != "workflow_dispatch" ]]; then
echo "build=true" >> $GITHUB_OUTPUT
elif [[ "${{ matrix.service.name }}" == "node" && "${{ inputs.build_node }}" == "true" ]]; then
echo "build=true" >> $GITHUB_OUTPUT
elif [[ "${{ matrix.service.name }}" == "sequencer" && "${{ inputs.build_sequencer }}" == "true" ]]; then
echo "build=true" >> $GITHUB_OUTPUT
else
echo "build=false" >> $GITHUB_OUTPUT
fi

- name: Checkout
if: steps.should_build.outputs.build == 'true'
uses: actions/checkout@v4

- name: Log in to GHCR
if: steps.should_build.outputs.build == 'true'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata
if: steps.should_build.outputs.build == 'true'
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ matrix.service.image }}
tags: |
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
type=ref,event=tag,enable=${{ matrix.service.name == 'node' && startsWith(github.ref, 'refs/tags/node-v') }}
type=ref,event=tag,enable=${{ matrix.service.name == 'sequencer' && startsWith(github.ref, 'refs/tags/sequencer-v') }}
type=sha

- name: Set up Docker Buildx
if: steps.should_build.outputs.build == 'true'
uses: docker/setup-buildx-action@v3

- name: Build and push
if: steps.should_build.outputs.build == 'true'
uses: docker/build-push-action@v5
with:
context: ${{ matrix.service.context }}
file: ${{ matrix.service.dockerfile }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=${{ env.REGISTRY }}/0xfacet/${{ matrix.service.name == 'node' && 'facet-node' || 'facet-sequencer' }}:buildcache
cache-to: type=registry,ref=${{ env.REGISTRY }}/0xfacet/${{ matrix.service.name == 'node' && 'facet-node' || 'facet-sequencer' }}:buildcache,mode=max
59 changes: 59 additions & 0 deletions .github/workflows/build-sequencer.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Build Sequencer Image

on:
workflow_dispatch:
inputs:
tag:
description: 'Image tag (default: branch name)'
required: false
type: string

env:
REGISTRY: ghcr.io

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Determine tag
id: tag
run: |
if [[ -n "${{ inputs.tag }}" ]]; then
echo "tag=${{ inputs.tag }}" >> $GITHUB_OUTPUT
else
# Use branch name as tag, replace / with -
BRANCH=${GITHUB_REF#refs/heads/}
echo "tag=${BRANCH//\//-}" >> $GITHUB_OUTPUT
fi

- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/0xfacet/facet-sequencer
tags: |
type=raw,value=${{ steps.tag.outputs.tag }}
type=sha

- name: Build and push
uses: docker/build-push-action@v5
with:
context: ./sequencer
file: ./sequencer/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
# Ignore all environment files (except templates).
/.env*
!/.env*.erb
!/.env.node.example

# Ignore all logfiles and tempfiles.
/log/*
Expand All @@ -34,3 +35,7 @@
/contracts/forge-artifacts/
/contracts/cache/
/contracts/broadcast/*

# Node.js dependencies
node_modules/
package-lock.json
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,5 @@ gem 'ostruct'
gem "oj", "~> 3.16"

gem "retriable", "~> 3.1"

gem "colorize", "~> 1.1"
2 changes: 2 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ GEM
activesupport
tzinfo
coderay (1.1.3)
colorize (1.1.0)
concurrent-ruby (1.3.3)
connection_pool (2.4.1)
crass (1.0.6)
Expand Down Expand Up @@ -347,6 +348,7 @@ DEPENDENCIES
capybara
clipboard (~> 2.0)
clockwork (~> 3.0)
colorize (~> 1.1)
debug
dotenv-rails (~> 3.1)
eth!
Expand Down
38 changes: 38 additions & 0 deletions app/models/eth_blob.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Represents an EIP-4844 blob associated with an L1 transaction
class EthBlob < T::Struct
const :tx_hash, Hash32 # L1 transaction hash that carried this blob
const :l1_tx_index, Integer # Transaction index in L1 block
const :blob_index, Integer # Index of blob within the transaction (0-based)
const :versioned_hash, Hash32 # KZG commitment versioned hash
const :data, T.nilable(ByteString) # Raw blob data (nil if not fetched/available)
const :l1_block_number, Integer # L1 block number for tracking

sig { params(
tx_hash: T.any(String, Hash32),
l1_tx_index: Integer,
blob_index: Integer,
versioned_hash: T.any(String, Hash32),
l1_block_number: Integer,
data: T.nilable(T.any(String, ByteString))
).returns(EthBlob) }
def self.create(tx_hash:, l1_tx_index:, blob_index:, versioned_hash:, l1_block_number:, data: nil)
new(
tx_hash: tx_hash.is_a?(Hash32) ? tx_hash : Hash32.from_hex(tx_hash),
l1_tx_index: l1_tx_index,
blob_index: blob_index,
versioned_hash: versioned_hash.is_a?(Hash32) ? versioned_hash : Hash32.from_hex(versioned_hash),
l1_block_number: l1_block_number,
data: data.nil? ? nil : (data.is_a?(ByteString) ? data : ByteString.from_hex(data))
)
end

sig { returns(T::Boolean) }
def has_data?
!data.nil?
end

sig { returns(String) }
def unique_id
"#{tx_hash.to_hex}-#{blob_index}"
end
end
42 changes: 42 additions & 0 deletions app/models/facet_batch_constants.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Constants for Facet Batch V2 protocol
module FacetBatchConstants
# Magic prefix ("unstoppable sequencing" ASCII -> hex)
MAGIC_PREFIX = ByteString.from_hex("0x756e73746f707061626c652073657175656e63696e67")

# Protocol version
VERSION = 1

# Wire format header sizes (in bytes)
MAGIC_SIZE = MAGIC_PREFIX.to_bin.bytesize
CHAIN_ID_SIZE = 8 # uint64
VERSION_SIZE = 1 # uint8
ROLE_SIZE = 1 # uint8
LENGTH_SIZE = 4 # uint32
HEADER_SIZE = MAGIC_SIZE + CHAIN_ID_SIZE + VERSION_SIZE + ROLE_SIZE + LENGTH_SIZE # 36 bytes
SIGNATURE_SIZE = 65 # secp256k1: r(32) + s(32) + v(1)

# Wire format offsets
MAGIC_OFFSET = 0
CHAIN_ID_OFFSET = MAGIC_SIZE
VERSION_OFFSET = CHAIN_ID_OFFSET + CHAIN_ID_SIZE
ROLE_OFFSET = VERSION_OFFSET + VERSION_SIZE
LENGTH_OFFSET = ROLE_OFFSET + ROLE_SIZE
RLP_OFFSET = HEADER_SIZE

# Size limits
MAX_BATCH_BYTES = Integer(ENV.fetch('MAX_BATCH_BYTES', 131_072)) # 128KB default
MAX_TXS_PER_BATCH = Integer(ENV.fetch('MAX_TXS_PER_BATCH', 1000))
MAX_BATCHES_PER_PAYLOAD = Integer(ENV.fetch('MAX_BATCHES_PER_PAYLOAD', 10))

# Batch roles
module Role
PERMISSIONLESS = 0x00 # Anyone can post, no signature required (formerly FORCED)
PRIORITY = 0x01 # Requires authorized signature
end

# Source types for tracking where batch came from
module Source
CALLDATA = 'calldata'
BLOB = 'blob'
end
end
53 changes: 53 additions & 0 deletions app/models/parsed_batch.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Represents a parsed and validated Facet batch
class ParsedBatch < T::Struct
extend T::Sig

const :role, Integer # PERMISSIONLESS or PRIORITY
const :signer, T.nilable(Address20) # Signer address (nil if not verified or permissionless)
const :l1_tx_index, Integer # Transaction index in L1 block
const :source, String # Where batch came from (calldata/blob)
const :source_details, T::Hash[Symbol, T.untyped] # Additional source info (tx_hash, blob_index, etc.)
const :transactions, T::Array[ByteString] # Array of EIP-2718 typed transaction bytes
const :content_hash, Hash32 # Keccak256 of RLP_TX_LIST for deduplication
const :chain_id, Integer # Chain ID from batch header

sig { returns(T::Boolean) }
def is_priority?
role == FacetBatchConstants::Role::PRIORITY
end

sig { returns(T::Boolean) }
def is_permissionless?
role == FacetBatchConstants::Role::PERMISSIONLESS
end

sig { returns(Integer) }
def transaction_count
transactions.length
end

sig { returns(T::Boolean) }
def has_signature?
!signer.nil?
end

sig { returns(String) }
def source_description
case source
when FacetBatchConstants::Source::CALLDATA
"calldata from tx #{source_details[:tx_hash]}"
when FacetBatchConstants::Source::BLOB
"blob #{source_details[:blob_index]} from tx #{source_details[:tx_hash]}"
else
source
end
end

# Calculate total gas limit for all transactions in batch
sig { returns(Integer) }
def total_gas_limit
# This will be calculated when we parse the actual transaction objects
# For now, return a placeholder
transactions.length * 21000 # Minimum gas per tx
end
end
Loading