Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .dockerignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ target/
.env
.env.example
.dockerignore
.temp/
Dockerfile
docker-compose.yml
erpc.yaml
Expand Down
9 changes: 9 additions & 0 deletions .env.example
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,26 @@
# Required bearer token for admin/data API routes.
API_TOKEN=replace-with-a-long-random-token
# Default `postgres-storage` build. A `mongodb-storage` build can instead use
# mongodb://localhost:27017/?replicaSet=rs0 and STORAGE_DATABASE=parseon.
STORAGE_URL=postgres://postgres:postgres@localhost:5432/parseon
# STORAGE_DATABASE=parseon
HTTP_LISTEN=0.0.0.0:8080
RUST_LOG=info,parseon=debug
# Comma-separated browser origins; empty keeps CORS disabled.
CORS_ORIGINS=
MAX_BODY_BYTES=1048576
POLL_INTERVAL_MS=2000
DEFAULT_BATCH_SIZE=10
# Set to 0 to disable Parseon's in-memory block cache.
BLOCK_CACHE_SIZE=512
BLOCK_CONCURRENCY=4
RPC_REQUEST_CONCURRENCY=16
STORAGE_WRITE_CONCURRENCY=4
CONFIRMATION_DEPTH=64
ROLLBACK_RETENTION=256
RPC_BATCH_SIZE=20
# Only enable for local RPC containers; keep false in production.
ALLOW_PRIVATE_RPC_NETWORKS=false
# Required only when built with `webhook-sink`.
# WEBHOOK_URL=http://localhost:9000/parseon
# WEBHOOK_CONCURRENCY=16
128 changes: 128 additions & 0 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
name: CI

on:
push:
pull_request:

permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
fmt:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install Rust toolchain
run: rustup toolchain install 1.96.0 --profile minimal && rustup component add rustfmt --toolchain 1.96.0
- run: cargo +1.96.0 fmt --all -- --check

rust:
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
include:
- name: postgres
features: parseon-server/postgres-storage
- name: postgres-webhook
features: parseon-server/postgres-storage,parseon-server/webhook-sink
- name: mongodb
features: parseon-server/mongodb-storage
- name: mongodb-webhook
features: parseon-server/mongodb-storage,parseon-server/webhook-sink
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install Rust toolchain
run: rustup toolchain install 1.96.0 --profile minimal && rustup component add clippy --toolchain 1.96.0
- run: cargo +1.96.0 clippy -q --workspace --all-targets --locked --no-default-features --features "${{ matrix.features }}" --message-format=short -- -D warnings
- run: cargo +1.96.0 test -q --workspace --all-targets --locked --no-default-features --features "${{ matrix.features }}" --message-format=short
- run: cargo +1.96.0 build -q -p parseon-server --release --locked --no-default-features --features "${{ matrix.features }}"

docker:
runs-on: ubuntu-24.04
strategy:
matrix:
include:
- name: postgres
features: postgres-storage
- name: postgres-webhook
features: postgres-storage,webhook-sink
- name: mongodb
features: mongodb-storage
- name: mongodb-webhook
features: mongodb-storage,webhook-sink
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Build image
run: docker build --build-arg PARSEON_FEATURES='${{ matrix.features }}' --tag parseon:ci .
- name: Generate SBOM when Docker SBOM support is installed
shell: bash
run: |
if docker sbom --help >/dev/null 2>&1; then
docker sbom parseon:ci > "sbom-${{ matrix.name }}.spdx.json"
else
echo 'docker sbom is unavailable on this runner; image SBOM is delegated to the registry scanner.'
fi
- name: Scan image when Docker Scout support is installed
shell: bash
run: |
if docker scout version >/dev/null 2>&1; then
docker scout cves --only-fixed parseon:ci
else
echo 'docker scout is unavailable on this runner; image scanning is delegated to the registry.'
fi

release-gates:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Validate production Compose
shell: bash
run: |
export PARSEON_IMAGE=registry.example/parseon@sha256:$(printf 'a%.0s' {1..64})
export STORAGE_URL='postgres://postgres:5432/parseon'
export POSTGRES_PASSWORD=test
export API_TOKEN=test-token
docker compose -f compose.production.yml config >/dev/null
if [[ ! "$PARSEON_IMAGE" =~ @sha256:[0-9a-fA-F]{64}$ ]]; then
echo 'PARSEON_IMAGE must be digest-pinned' >&2
exit 1
fi
- name: Validate Helm chart and immutable deployment policy
shell: bash
run: |
digest="sha256:$(printf 'a%.0s' {1..64})"
docker run --rm -v "$GITHUB_WORKSPACE:/work" -w /work alpine/helm:3.17@sha256:d899e6316789fec04ee95300a18e454b7942539cbb3d89bde3e0655d6ca2e895 lint deploy/helm/parseon --set image.digest="$digest"
docker run --rm -v "$GITHUB_WORKSPACE:/work" -w /work alpine/helm:3.17@sha256:d899e6316789fec04ee95300a18e454b7942539cbb3d89bde3e0655d6ca2e895 template parseon deploy/helm/parseon --set image.digest="$digest" >/dev/null
if docker run --rm -v "$GITHUB_WORKSPACE:/work" -w /work alpine/helm:3.17@sha256:d899e6316789fec04ee95300a18e454b7942539cbb3d89bde3e0655d6ca2e895 template parseon deploy/helm/parseon --set image.digest="$digest" --set replicaCount=2 >/dev/null 2>&1; then
echo 'replicaCount=2 was accepted' >&2
exit 1
fi
if docker run --rm -v "$GITHUB_WORKSPACE:/work" -w /work alpine/helm:3.17@sha256:d899e6316789fec04ee95300a18e454b7942539cbb3d89bde3e0655d6ca2e895 template parseon deploy/helm/parseon >/dev/null 2>&1; then
echo 'missing image digest was accepted' >&2
exit 1
fi
- name: Validate scripts and monitoring artifacts
shell: bash
run: |
bash -n scripts/backup_postgres.sh scripts/restore_postgres.sh
python3 -m py_compile scripts/gen_erpc.py
python3 -m json.tool deploy/monitoring/parseon-dashboard.json >/dev/null
secrets:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- name: Reject credential-bearing generated config
shell: bash
run: |
if git grep -n -I -i -E '://[^[:space:]/:@]+:[^[:space:]@]+@|(^|[?&])(api[_-]?key|access[_-]?token|token|secret|password)=[^[:space:]&]{8,}' -- ':!CHANGELOG.md' ':!docs/operations.md' ':!.env.example'; then
echo 'credential-bearing URL or query parameter found in tracked source' >&2
exit 1
fi
echo 'Historical credentials still require independent revoke/rotate evidence.'
1 change: 1 addition & 0 deletions .gitignore
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
/target
/.env
/.temp
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,8 @@ All notable changes to Parseon are documented in this file.

### Added

- Add canonical block metadata, provisional/finalized lifecycle, bounded reorg rollback, and finalized promotion for both storage adapters.
- Add production Compose and Helm deployment artifacts, backup/restore operations runbook, Prometheus alerts, Grafana dashboard, and CI release gates.
- Apply chain creation, enable/disable, and deletion to running workers without restarting Parseon; deletions stop the worker before removing its data.
- Rotate a chain's RPC endpoint URL in place on the running worker via Alloy's `Http::set_url`, resetting endpoint capability probes while keeping the cached chain ID; sources that cannot rotate restart their worker instead.
- Allow multiple monitors to target the same chain, contract, and function selector or event topic while retaining independent ranges, filters, cursors, and result storage.
Expand All@@ -15,6 +17,8 @@ All notable changes to Parseon are documented in this file.

### Changed

- Require a bearer API token for protected HTTP routes, disable CORS by default, bound request bodies, add liveness/readiness probes, and reject unsafe RPC destinations by default.
- Treat credential-bearing eRPC URLs as operator-injected configuration instead of generated repository content.
- Pin the Compose eRPC image to the `0.1.1` release tag, set a 3 GiB container memory limit with `GOMEMLIMIT=2700MiB`, add `restart: unless-stopped`, and invoke `/erpc-server` explicitly so the distroless image starts correctly.
- Reuse matching RPC data and compatible ABI decoders across overlapping monitor targets before applying each monitor's filter and persisting its own results.
- Isolate ABI decode failures to the affected monitor layout so an incompatible definition cannot stall other monitors on the chain.
Expand All@@ -32,6 +36,8 @@ All notable changes to Parseon are documented in this file.
### Fixed

- Keep persisted chain state and live workers ordered during concurrent mutations and startup reconciliation, and reject new worker starts once supervisor shutdown begins.
- Reject mixed-branch result identities, fail closed when a retained reorg ancestor is unavailable, preserve finalized state for monitors added over existing blocks, and reset rollback cursors below a monitor's start block.
- Pin validated RPC DNS addresses in the HTTP transport, verify generated endpoint chain IDs, expose worker freshness/state metrics, and require encrypted checksum-verified PostgreSQL backup artifacts.

### Breaking

Expand Down
15 changes: 8 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,7 @@ members = [
resolver = "3"

[workspace.package]
version = "0.8.0"
version = "1.0.0"
edition = "2024"
license = "MIT OR Apache-2.0"

Expand Down
10 changes: 5 additions & 5 deletions Dockerfile
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1

# ---- chef base: shared Rust toolchain + cargo-chef ----
FROM rust:1.96-alpine AS chef
FROM rust:1.96-alpine@sha256:a41f7740f8b45d45795624eec13a8b42263cc700f19f7e4e86e04d3dda08a479 AS chef
RUN apk add --no-cache musl-dev
RUN cargo install cargo-chef --locked
WORKDIR /app
Expand All@@ -18,7 +18,7 @@ ARG PARSEON_FEATURES=postgres-storage
ENV PARSEON_FEATURES=${PARSEON_FEATURES}
RUN apk add --no-cache watchexec
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --recipe-path recipe.json --no-default-features --features "${PARSEON_FEATURES}"
RUN cargo chef cook --locked --recipe-path recipe.json --no-default-features --features "${PARSEON_FEATURES}"
COPY . .
EXPOSE 8080
CMD ["sh", "-c", "exec watchexec --restart --stop-signal SIGINT --exts rs,toml,lock,sql -- cargo run --no-default-features --features \"${PARSEON_FEATURES}\""]
Expand All@@ -29,15 +29,15 @@ ARG PARSEON_FEATURES=postgres-storage
COPY --from=planner /app/recipe.json recipe.json
RUN --mount=type=cache,target=/root/.cargo/registry \
--mount=type=cache,target=/app/target \
cargo chef cook --release --recipe-path recipe.json --no-default-features --features "${PARSEON_FEATURES}"
cargo chef cook --locked --release --recipe-path recipe.json --no-default-features --features "${PARSEON_FEATURES}"
COPY . .
RUN --mount=type=cache,target=/root/.cargo/registry \
--mount=type=cache,target=/app/target \
cargo build --release --no-default-features --features "${PARSEON_FEATURES}" && \
cargo build --release --locked --no-default-features --features "${PARSEON_FEATURES}" && \
cp /app/target/release/parseon /usr/local/bin/parseon

# ---- runtime: minimal alpine, non-root, healthcheck ----
FROM alpine:3.20 AS runtime
FROM alpine:3.20@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc AS runtime
RUN apk add --no-cache ca-certificates wget && \
adduser -D -u 1000 parseon
COPY --from=builder /usr/local/bin/parseon /usr/local/bin/parseon
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,10 +40,14 @@ parseon-server
└── parseon-webhook-sink ────> parseon-core
```

Start PostgreSQL with `docker compose up -d`, or start the MongoDB development replica set and eRPC gateway with `docker compose --profile mongodb --profile erpc up -d`. Configure the selected backend through `STORAGE_URL`, then register direct RPC or complete eRPC URLs through `POST /chains`.
Start PostgreSQL with `docker compose up -d`, or start the MongoDB development replica set and eRPC gateway with `docker compose --profile mongodb --profile erpc up -d`. Configure the selected backend through `STORAGE_URL` and set a non-empty `API_TOKEN`; protected API routes require `Authorization: Bearer <API_TOKEN>`. Register direct RPC or complete eRPC URLs through `POST /chains`. Private/loopback RPC destinations are rejected unless `ALLOW_PRIVATE_RPC_NETWORKS=true` is explicitly enabled for local development.

See [adapter configuration and guarantees](./docs/adapters.md) for feature builds, MongoDB requirements, eRPC smoke checks, the webhook JSON contract, and Compose profiles.

## Production deployment

Use [`compose.production.yml`](./compose.production.yml) or the [`deploy/helm/parseon`](./deploy/helm/parseon) chart. Both require an externally managed `API_TOKEN` and storage URL, keep databases private, and expose liveness/readiness probes. Read the [production operations runbook](./docs/operations.md) before upgrading, restoring, or exposing the API through an ingress.

## License

Licensed under either the [Apache License, Version 2.0](./LICENSE-APACHE) or the [MIT license](./LICENSE-MIT), at your option.
Expand Down
70 changes: 70 additions & 0 deletions compose.production.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
services:
postgres:
image: ${POSTGRES_IMAGE:-postgres:16@sha256:f1c3376c26f2609ab9f29f71f824103fe2fcd8ee0346485cb6122a4f93df6f94}
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-parseon}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB:-parseon}
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 3s
retries: 12
# No host port: PostgreSQL is reachable only by Parseon on the backend network.

parseon:
image: ${PARSEON_IMAGE:?set PARSEON_IMAGE to an immutable Parseon image}
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
STORAGE_URL: ${STORAGE_URL:?set STORAGE_URL}
API_TOKEN: ${API_TOKEN:?set API_TOKEN}
HTTP_LISTEN: 0.0.0.0:8080
RUST_LOG: ${RUST_LOG:-info}
POLL_INTERVAL_MS: ${POLL_INTERVAL_MS:-2000}
DEFAULT_BATCH_SIZE: ${DEFAULT_BATCH_SIZE:-10}
BLOCK_CACHE_SIZE: ${BLOCK_CACHE_SIZE:-512}
BLOCK_CONCURRENCY: ${BLOCK_CONCURRENCY:-4}
RPC_REQUEST_CONCURRENCY: ${RPC_REQUEST_CONCURRENCY:-16}
STORAGE_WRITE_CONCURRENCY: ${STORAGE_WRITE_CONCURRENCY:-4}
CONFIRMATION_DEPTH: ${CONFIRMATION_DEPTH:-64}
ROLLBACK_RETENTION: ${ROLLBACK_RETENTION:-256}
RPC_BATCH_SIZE: ${RPC_BATCH_SIZE:-20}
ALLOW_PRIVATE_RPC_NETWORKS: "false"
CORS_ORIGINS: ${CORS_ORIGINS:-}
MAX_BODY_BYTES: ${MAX_BODY_BYTES:-1048576}
expose:
- "8080"
networks:
- backend
- egress
read_only: true
tmpfs:
- /tmp
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
mem_limit: ${PARSEON_MEMORY_LIMIT:-768m}
cpus: ${PARSEON_CPUS:-2}
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/healthz"]
interval: 10s
timeout: 5s
start_period: 20s
retries: 6

volumes:
pgdata:

networks:
backend:
internal: true
egress:
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
feat: harden indexing for production v1 by ozcnii · Pull Request #1 · melonges/parseon · GitHub
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .dockerignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ target/
.env
.env.example
.dockerignore
.temp/
Dockerfile
docker-compose.yml
erpc.yaml
Expand Down
9 changes: 9 additions & 0 deletions .env.example
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,26 @@
# Required bearer token for admin/data API routes.
API_TOKEN=replace-with-a-long-random-token
# Default `postgres-storage` build. A `mongodb-storage` build can instead use
# mongodb://localhost:27017/?replicaSet=rs0 and STORAGE_DATABASE=parseon.
STORAGE_URL=postgres://postgres:postgres@localhost:5432/parseon
# STORAGE_DATABASE=parseon
HTTP_LISTEN=0.0.0.0:8080
RUST_LOG=info,parseon=debug
# Comma-separated browser origins; empty keeps CORS disabled.
CORS_ORIGINS=
MAX_BODY_BYTES=1048576
POLL_INTERVAL_MS=2000
DEFAULT_BATCH_SIZE=10
# Set to 0 to disable Parseon's in-memory block cache.
BLOCK_CACHE_SIZE=512
BLOCK_CONCURRENCY=4
RPC_REQUEST_CONCURRENCY=16
STORAGE_WRITE_CONCURRENCY=4
CONFIRMATION_DEPTH=64
ROLLBACK_RETENTION=256
RPC_BATCH_SIZE=20
# Only enable for local RPC containers; keep false in production.
ALLOW_PRIVATE_RPC_NETWORKS=false
# Required only when built with `webhook-sink`.
# WEBHOOK_URL=http://localhost:9000/parseon
# WEBHOOK_CONCURRENCY=16
128 changes: 128 additions & 0 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
name: CI

on:
push:
pull_request:

permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
fmt:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install Rust toolchain
run: rustup toolchain install 1.96.0 --profile minimal && rustup component add rustfmt --toolchain 1.96.0
- run: cargo +1.96.0 fmt --all -- --check

rust:
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
include:
- name: postgres
features: parseon-server/postgres-storage
- name: postgres-webhook
features: parseon-server/postgres-storage,parseon-server/webhook-sink
- name: mongodb
features: parseon-server/mongodb-storage
- name: mongodb-webhook
features: parseon-server/mongodb-storage,parseon-server/webhook-sink
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install Rust toolchain
run: rustup toolchain install 1.96.0 --profile minimal && rustup component add clippy --toolchain 1.96.0
- run: cargo +1.96.0 clippy -q --workspace --all-targets --locked --no-default-features --features "${{ matrix.features }}" --message-format=short -- -D warnings
- run: cargo +1.96.0 test -q --workspace --all-targets --locked --no-default-features --features "${{ matrix.features }}" --message-format=short
- run: cargo +1.96.0 build -q -p parseon-server --release --locked --no-default-features --features "${{ matrix.features }}"

docker:
runs-on: ubuntu-24.04
strategy:
matrix:
include:
- name: postgres
features: postgres-storage
- name: postgres-webhook
features: postgres-storage,webhook-sink
- name: mongodb
features: mongodb-storage
- name: mongodb-webhook
features: mongodb-storage,webhook-sink
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Build image
run: docker build --build-arg PARSEON_FEATURES='${{ matrix.features }}' --tag parseon:ci .
- name: Generate SBOM when Docker SBOM support is installed
shell: bash
run: |
if docker sbom --help >/dev/null 2>&1; then
docker sbom parseon:ci > "sbom-${{ matrix.name }}.spdx.json"
else
echo 'docker sbom is unavailable on this runner; image SBOM is delegated to the registry scanner.'
fi
- name: Scan image when Docker Scout support is installed
shell: bash
run: |
if docker scout version >/dev/null 2>&1; then
docker scout cves --only-fixed parseon:ci
else
echo 'docker scout is unavailable on this runner; image scanning is delegated to the registry.'
fi

release-gates:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Validate production Compose
shell: bash
run: |
export PARSEON_IMAGE=registry.example/parseon@sha256:$(printf 'a%.0s' {1..64})
export STORAGE_URL='postgres://postgres:5432/parseon'
export POSTGRES_PASSWORD=test
export API_TOKEN=test-token
docker compose -f compose.production.yml config >/dev/null
if [[ ! "$PARSEON_IMAGE" =~ @sha256:[0-9a-fA-F]{64}$ ]]; then
echo 'PARSEON_IMAGE must be digest-pinned' >&2
exit 1
fi
- name: Validate Helm chart and immutable deployment policy
shell: bash
run: |
digest="sha256:$(printf 'a%.0s' {1..64})"
docker run --rm -v "$GITHUB_WORKSPACE:/work" -w /work alpine/helm:3.17@sha256:d899e6316789fec04ee95300a18e454b7942539cbb3d89bde3e0655d6ca2e895 lint deploy/helm/parseon --set image.digest="$digest"
docker run --rm -v "$GITHUB_WORKSPACE:/work" -w /work alpine/helm:3.17@sha256:d899e6316789fec04ee95300a18e454b7942539cbb3d89bde3e0655d6ca2e895 template parseon deploy/helm/parseon --set image.digest="$digest" >/dev/null
if docker run --rm -v "$GITHUB_WORKSPACE:/work" -w /work alpine/helm:3.17@sha256:d899e6316789fec04ee95300a18e454b7942539cbb3d89bde3e0655d6ca2e895 template parseon deploy/helm/parseon --set image.digest="$digest" --set replicaCount=2 >/dev/null 2>&1; then
echo 'replicaCount=2 was accepted' >&2
exit 1
fi
if docker run --rm -v "$GITHUB_WORKSPACE:/work" -w /work alpine/helm:3.17@sha256:d899e6316789fec04ee95300a18e454b7942539cbb3d89bde3e0655d6ca2e895 template parseon deploy/helm/parseon >/dev/null 2>&1; then
echo 'missing image digest was accepted' >&2
exit 1
fi
- name: Validate scripts and monitoring artifacts
shell: bash
run: |
bash -n scripts/backup_postgres.sh scripts/restore_postgres.sh
python3 -m py_compile scripts/gen_erpc.py
python3 -m json.tool deploy/monitoring/parseon-dashboard.json >/dev/null
secrets:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- name: Reject credential-bearing generated config
shell: bash
run: |
if git grep -n -I -i -E '://[^[:space:]/:@]+:[^[:space:]@]+@|(^|[?&])(api[_-]?key|access[_-]?token|token|secret|password)=[^[:space:]&]{8,}' -- ':!CHANGELOG.md' ':!docs/operations.md' ':!.env.example'; then
echo 'credential-bearing URL or query parameter found in tracked source' >&2
exit 1
fi
echo 'Historical credentials still require independent revoke/rotate evidence.'
1 change: 1 addition & 0 deletions .gitignore
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
/target
/.env
/.temp
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,8 @@ All notable changes to Parseon are documented in this file.

### Added

- Add canonical block metadata, provisional/finalized lifecycle, bounded reorg rollback, and finalized promotion for both storage adapters.
- Add production Compose and Helm deployment artifacts, backup/restore operations runbook, Prometheus alerts, Grafana dashboard, and CI release gates.
- Apply chain creation, enable/disable, and deletion to running workers without restarting Parseon; deletions stop the worker before removing its data.
- Rotate a chain's RPC endpoint URL in place on the running worker via Alloy's `Http::set_url`, resetting endpoint capability probes while keeping the cached chain ID; sources that cannot rotate restart their worker instead.
- Allow multiple monitors to target the same chain, contract, and function selector or event topic while retaining independent ranges, filters, cursors, and result storage.
Expand All@@ -15,6 +17,8 @@ All notable changes to Parseon are documented in this file.

### Changed

- Require a bearer API token for protected HTTP routes, disable CORS by default, bound request bodies, add liveness/readiness probes, and reject unsafe RPC destinations by default.
- Treat credential-bearing eRPC URLs as operator-injected configuration instead of generated repository content.
- Pin the Compose eRPC image to the `0.1.1` release tag, set a 3 GiB container memory limit with `GOMEMLIMIT=2700MiB`, add `restart: unless-stopped`, and invoke `/erpc-server` explicitly so the distroless image starts correctly.
- Reuse matching RPC data and compatible ABI decoders across overlapping monitor targets before applying each monitor's filter and persisting its own results.
- Isolate ABI decode failures to the affected monitor layout so an incompatible definition cannot stall other monitors on the chain.
Expand All@@ -32,6 +36,8 @@ All notable changes to Parseon are documented in this file.
### Fixed

- Keep persisted chain state and live workers ordered during concurrent mutations and startup reconciliation, and reject new worker starts once supervisor shutdown begins.
- Reject mixed-branch result identities, fail closed when a retained reorg ancestor is unavailable, preserve finalized state for monitors added over existing blocks, and reset rollback cursors below a monitor's start block.
- Pin validated RPC DNS addresses in the HTTP transport, verify generated endpoint chain IDs, expose worker freshness/state metrics, and require encrypted checksum-verified PostgreSQL backup artifacts.

### Breaking

Expand Down
15 changes: 8 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,7 @@ members = [
resolver = "3"

[workspace.package]
version = "0.8.0"
version = "1.0.0"
edition = "2024"
license = "MIT OR Apache-2.0"

Expand Down
10 changes: 5 additions & 5 deletions Dockerfile
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1

# ---- chef base: shared Rust toolchain + cargo-chef ----
FROM rust:1.96-alpine AS chef
FROM rust:1.96-alpine@sha256:a41f7740f8b45d45795624eec13a8b42263cc700f19f7e4e86e04d3dda08a479 AS chef
RUN apk add --no-cache musl-dev
RUN cargo install cargo-chef --locked
WORKDIR /app
Expand All@@ -18,7 +18,7 @@ ARG PARSEON_FEATURES=postgres-storage
ENV PARSEON_FEATURES=${PARSEON_FEATURES}
RUN apk add --no-cache watchexec
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --recipe-path recipe.json --no-default-features --features "${PARSEON_FEATURES}"
RUN cargo chef cook --locked --recipe-path recipe.json --no-default-features --features "${PARSEON_FEATURES}"
COPY . .
EXPOSE 8080
CMD ["sh", "-c", "exec watchexec --restart --stop-signal SIGINT --exts rs,toml,lock,sql -- cargo run --no-default-features --features \"${PARSEON_FEATURES}\""]
Expand All@@ -29,15 +29,15 @@ ARG PARSEON_FEATURES=postgres-storage
COPY --from=planner /app/recipe.json recipe.json
RUN --mount=type=cache,target=/root/.cargo/registry \
--mount=type=cache,target=/app/target \
cargo chef cook --release --recipe-path recipe.json --no-default-features --features "${PARSEON_FEATURES}"
cargo chef cook --locked --release --recipe-path recipe.json --no-default-features --features "${PARSEON_FEATURES}"
COPY . .
RUN --mount=type=cache,target=/root/.cargo/registry \
--mount=type=cache,target=/app/target \
cargo build --release --no-default-features --features "${PARSEON_FEATURES}" && \
cargo build --release --locked --no-default-features --features "${PARSEON_FEATURES}" && \
cp /app/target/release/parseon /usr/local/bin/parseon

# ---- runtime: minimal alpine, non-root, healthcheck ----
FROM alpine:3.20 AS runtime
FROM alpine:3.20@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc AS runtime
RUN apk add --no-cache ca-certificates wget && \
adduser -D -u 1000 parseon
COPY --from=builder /usr/local/bin/parseon /usr/local/bin/parseon
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,10 +40,14 @@ parseon-server
└── parseon-webhook-sink ────> parseon-core
```

Start PostgreSQL with `docker compose up -d`, or start the MongoDB development replica set and eRPC gateway with `docker compose --profile mongodb --profile erpc up -d`. Configure the selected backend through `STORAGE_URL`, then register direct RPC or complete eRPC URLs through `POST /chains`.
Start PostgreSQL with `docker compose up -d`, or start the MongoDB development replica set and eRPC gateway with `docker compose --profile mongodb --profile erpc up -d`. Configure the selected backend through `STORAGE_URL` and set a non-empty `API_TOKEN`; protected API routes require `Authorization: Bearer <API_TOKEN>`. Register direct RPC or complete eRPC URLs through `POST /chains`. Private/loopback RPC destinations are rejected unless `ALLOW_PRIVATE_RPC_NETWORKS=true` is explicitly enabled for local development.

See [adapter configuration and guarantees](./docs/adapters.md) for feature builds, MongoDB requirements, eRPC smoke checks, the webhook JSON contract, and Compose profiles.

## Production deployment

Use [`compose.production.yml`](./compose.production.yml) or the [`deploy/helm/parseon`](./deploy/helm/parseon) chart. Both require an externally managed `API_TOKEN` and storage URL, keep databases private, and expose liveness/readiness probes. Read the [production operations runbook](./docs/operations.md) before upgrading, restoring, or exposing the API through an ingress.

## License

Licensed under either the [Apache License, Version 2.0](./LICENSE-APACHE) or the [MIT license](./LICENSE-MIT), at your option.
Expand Down
70 changes: 70 additions & 0 deletions compose.production.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
services:
postgres:
image: ${POSTGRES_IMAGE:-postgres:16@sha256:f1c3376c26f2609ab9f29f71f824103fe2fcd8ee0346485cb6122a4f93df6f94}
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-parseon}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB:-parseon}
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 3s
retries: 12
# No host port: PostgreSQL is reachable only by Parseon on the backend network.

parseon:
image: ${PARSEON_IMAGE:?set PARSEON_IMAGE to an immutable Parseon image}
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
STORAGE_URL: ${STORAGE_URL:?set STORAGE_URL}
API_TOKEN: ${API_TOKEN:?set API_TOKEN}
HTTP_LISTEN: 0.0.0.0:8080
RUST_LOG: ${RUST_LOG:-info}
POLL_INTERVAL_MS: ${POLL_INTERVAL_MS:-2000}
DEFAULT_BATCH_SIZE: ${DEFAULT_BATCH_SIZE:-10}
BLOCK_CACHE_SIZE: ${BLOCK_CACHE_SIZE:-512}
BLOCK_CONCURRENCY: ${BLOCK_CONCURRENCY:-4}
RPC_REQUEST_CONCURRENCY: ${RPC_REQUEST_CONCURRENCY:-16}
STORAGE_WRITE_CONCURRENCY: ${STORAGE_WRITE_CONCURRENCY:-4}
CONFIRMATION_DEPTH: ${CONFIRMATION_DEPTH:-64}
ROLLBACK_RETENTION: ${ROLLBACK_RETENTION:-256}
RPC_BATCH_SIZE: ${RPC_BATCH_SIZE:-20}
ALLOW_PRIVATE_RPC_NETWORKS: "false"
CORS_ORIGINS: ${CORS_ORIGINS:-}
MAX_BODY_BYTES: ${MAX_BODY_BYTES:-1048576}
expose:
- "8080"
networks:
- backend
- egress
read_only: true
tmpfs:
- /tmp
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
mem_limit: ${PARSEON_MEMORY_LIMIT:-768m}
cpus: ${PARSEON_CPUS:-2}
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/healthz"]
interval: 10s
timeout: 5s
start_period: 20s
retries: 6

volumes:
pgdata:

networks:
backend:
internal: true
egress:
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: harden indexing for production v1 by ozcnii · Pull Request #1 · melonges/parseon · GitHub
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .dockerignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ target/
.env
.env.example
.dockerignore
.temp/
Dockerfile
docker-compose.yml
erpc.yaml
Expand Down
9 changes: 9 additions & 0 deletions .env.example
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,26 @@
# Required bearer token for admin/data API routes.
API_TOKEN=replace-with-a-long-random-token
# Default `postgres-storage` build. A `mongodb-storage` build can instead use
# mongodb://localhost:27017/?replicaSet=rs0 and STORAGE_DATABASE=parseon.
STORAGE_URL=postgres://postgres:postgres@localhost:5432/parseon
# STORAGE_DATABASE=parseon
HTTP_LISTEN=0.0.0.0:8080
RUST_LOG=info,parseon=debug
# Comma-separated browser origins; empty keeps CORS disabled.
CORS_ORIGINS=
MAX_BODY_BYTES=1048576
POLL_INTERVAL_MS=2000
DEFAULT_BATCH_SIZE=10
# Set to 0 to disable Parseon's in-memory block cache.
BLOCK_CACHE_SIZE=512
BLOCK_CONCURRENCY=4
RPC_REQUEST_CONCURRENCY=16
STORAGE_WRITE_CONCURRENCY=4
CONFIRMATION_DEPTH=64
ROLLBACK_RETENTION=256
RPC_BATCH_SIZE=20
# Only enable for local RPC containers; keep false in production.
ALLOW_PRIVATE_RPC_NETWORKS=false
# Required only when built with `webhook-sink`.
# WEBHOOK_URL=http://localhost:9000/parseon
# WEBHOOK_CONCURRENCY=16
128 changes: 128 additions & 0 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
name: CI

on:
push:
pull_request:

permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
fmt:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install Rust toolchain
run: rustup toolchain install 1.96.0 --profile minimal && rustup component add rustfmt --toolchain 1.96.0
- run: cargo +1.96.0 fmt --all -- --check

rust:
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
include:
- name: postgres
features: parseon-server/postgres-storage
- name: postgres-webhook
features: parseon-server/postgres-storage,parseon-server/webhook-sink
- name: mongodb
features: parseon-server/mongodb-storage
- name: mongodb-webhook
features: parseon-server/mongodb-storage,parseon-server/webhook-sink
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install Rust toolchain
run: rustup toolchain install 1.96.0 --profile minimal && rustup component add clippy --toolchain 1.96.0
- run: cargo +1.96.0 clippy -q --workspace --all-targets --locked --no-default-features --features "${{ matrix.features }}" --message-format=short -- -D warnings
- run: cargo +1.96.0 test -q --workspace --all-targets --locked --no-default-features --features "${{ matrix.features }}" --message-format=short
- run: cargo +1.96.0 build -q -p parseon-server --release --locked --no-default-features --features "${{ matrix.features }}"

docker:
runs-on: ubuntu-24.04
strategy:
matrix:
include:
- name: postgres
features: postgres-storage
- name: postgres-webhook
features: postgres-storage,webhook-sink
- name: mongodb
features: mongodb-storage
- name: mongodb-webhook
features: mongodb-storage,webhook-sink
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Build image
run: docker build --build-arg PARSEON_FEATURES='${{ matrix.features }}' --tag parseon:ci .
- name: Generate SBOM when Docker SBOM support is installed
shell: bash
run: |
if docker sbom --help >/dev/null 2>&1; then
docker sbom parseon:ci > "sbom-${{ matrix.name }}.spdx.json"
else
echo 'docker sbom is unavailable on this runner; image SBOM is delegated to the registry scanner.'
fi
- name: Scan image when Docker Scout support is installed
shell: bash
run: |
if docker scout version >/dev/null 2>&1; then
docker scout cves --only-fixed parseon:ci
else
echo 'docker scout is unavailable on this runner; image scanning is delegated to the registry.'
fi

release-gates:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Validate production Compose
shell: bash
run: |
export PARSEON_IMAGE=registry.example/parseon@sha256:$(printf 'a%.0s' {1..64})
export STORAGE_URL='postgres://postgres:5432/parseon'
export POSTGRES_PASSWORD=test
export API_TOKEN=test-token
docker compose -f compose.production.yml config >/dev/null
if [[ ! "$PARSEON_IMAGE" =~ @sha256:[0-9a-fA-F]{64}$ ]]; then
echo 'PARSEON_IMAGE must be digest-pinned' >&2
exit 1
fi
- name: Validate Helm chart and immutable deployment policy
shell: bash
run: |
digest="sha256:$(printf 'a%.0s' {1..64})"
docker run --rm -v "$GITHUB_WORKSPACE:/work" -w /work alpine/helm:3.17@sha256:d899e6316789fec04ee95300a18e454b7942539cbb3d89bde3e0655d6ca2e895 lint deploy/helm/parseon --set image.digest="$digest"
docker run --rm -v "$GITHUB_WORKSPACE:/work" -w /work alpine/helm:3.17@sha256:d899e6316789fec04ee95300a18e454b7942539cbb3d89bde3e0655d6ca2e895 template parseon deploy/helm/parseon --set image.digest="$digest" >/dev/null
if docker run --rm -v "$GITHUB_WORKSPACE:/work" -w /work alpine/helm:3.17@sha256:d899e6316789fec04ee95300a18e454b7942539cbb3d89bde3e0655d6ca2e895 template parseon deploy/helm/parseon --set image.digest="$digest" --set replicaCount=2 >/dev/null 2>&1; then
echo 'replicaCount=2 was accepted' >&2
exit 1
fi
if docker run --rm -v "$GITHUB_WORKSPACE:/work" -w /work alpine/helm:3.17@sha256:d899e6316789fec04ee95300a18e454b7942539cbb3d89bde3e0655d6ca2e895 template parseon deploy/helm/parseon >/dev/null 2>&1; then
echo 'missing image digest was accepted' >&2
exit 1
fi
- name: Validate scripts and monitoring artifacts
shell: bash
run: |
bash -n scripts/backup_postgres.sh scripts/restore_postgres.sh
python3 -m py_compile scripts/gen_erpc.py
python3 -m json.tool deploy/monitoring/parseon-dashboard.json >/dev/null
secrets:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- name: Reject credential-bearing generated config
shell: bash
run: |
if git grep -n -I -i -E '://[^[:space:]/:@]+:[^[:space:]@]+@|(^|[?&])(api[_-]?key|access[_-]?token|token|secret|password)=[^[:space:]&]{8,}' -- ':!CHANGELOG.md' ':!docs/operations.md' ':!.env.example'; then
echo 'credential-bearing URL or query parameter found in tracked source' >&2
exit 1
fi
echo 'Historical credentials still require independent revoke/rotate evidence.'
1 change: 1 addition & 0 deletions .gitignore
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
/target
/.env
/.temp
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,8 @@ All notable changes to Parseon are documented in this file.

### Added

- Add canonical block metadata, provisional/finalized lifecycle, bounded reorg rollback, and finalized promotion for both storage adapters.
- Add production Compose and Helm deployment artifacts, backup/restore operations runbook, Prometheus alerts, Grafana dashboard, and CI release gates.
- Apply chain creation, enable/disable, and deletion to running workers without restarting Parseon; deletions stop the worker before removing its data.
- Rotate a chain's RPC endpoint URL in place on the running worker via Alloy's `Http::set_url`, resetting endpoint capability probes while keeping the cached chain ID; sources that cannot rotate restart their worker instead.
- Allow multiple monitors to target the same chain, contract, and function selector or event topic while retaining independent ranges, filters, cursors, and result storage.
Expand All@@ -15,6 +17,8 @@ All notable changes to Parseon are documented in this file.

### Changed

- Require a bearer API token for protected HTTP routes, disable CORS by default, bound request bodies, add liveness/readiness probes, and reject unsafe RPC destinations by default.
- Treat credential-bearing eRPC URLs as operator-injected configuration instead of generated repository content.
- Pin the Compose eRPC image to the `0.1.1` release tag, set a 3 GiB container memory limit with `GOMEMLIMIT=2700MiB`, add `restart: unless-stopped`, and invoke `/erpc-server` explicitly so the distroless image starts correctly.
- Reuse matching RPC data and compatible ABI decoders across overlapping monitor targets before applying each monitor's filter and persisting its own results.
- Isolate ABI decode failures to the affected monitor layout so an incompatible definition cannot stall other monitors on the chain.
Expand All@@ -32,6 +36,8 @@ All notable changes to Parseon are documented in this file.
### Fixed

- Keep persisted chain state and live workers ordered during concurrent mutations and startup reconciliation, and reject new worker starts once supervisor shutdown begins.
- Reject mixed-branch result identities, fail closed when a retained reorg ancestor is unavailable, preserve finalized state for monitors added over existing blocks, and reset rollback cursors below a monitor's start block.
- Pin validated RPC DNS addresses in the HTTP transport, verify generated endpoint chain IDs, expose worker freshness/state metrics, and require encrypted checksum-verified PostgreSQL backup artifacts.

### Breaking

Expand Down
15 changes: 8 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,7 @@ members = [
resolver = "3"

[workspace.package]
version = "0.8.0"
version = "1.0.0"
edition = "2024"
license = "MIT OR Apache-2.0"

Expand Down
10 changes: 5 additions & 5 deletions Dockerfile
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1

# ---- chef base: shared Rust toolchain + cargo-chef ----
FROM rust:1.96-alpine AS chef
FROM rust:1.96-alpine@sha256:a41f7740f8b45d45795624eec13a8b42263cc700f19f7e4e86e04d3dda08a479 AS chef
RUN apk add --no-cache musl-dev
RUN cargo install cargo-chef --locked
WORKDIR /app
Expand All@@ -18,7 +18,7 @@ ARG PARSEON_FEATURES=postgres-storage
ENV PARSEON_FEATURES=${PARSEON_FEATURES}
RUN apk add --no-cache watchexec
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --recipe-path recipe.json --no-default-features --features "${PARSEON_FEATURES}"
RUN cargo chef cook --locked --recipe-path recipe.json --no-default-features --features "${PARSEON_FEATURES}"
COPY . .
EXPOSE 8080
CMD ["sh", "-c", "exec watchexec --restart --stop-signal SIGINT --exts rs,toml,lock,sql -- cargo run --no-default-features --features \"${PARSEON_FEATURES}\""]
Expand All@@ -29,15 +29,15 @@ ARG PARSEON_FEATURES=postgres-storage
COPY --from=planner /app/recipe.json recipe.json
RUN --mount=type=cache,target=/root/.cargo/registry \
--mount=type=cache,target=/app/target \
cargo chef cook --release --recipe-path recipe.json --no-default-features --features "${PARSEON_FEATURES}"
cargo chef cook --locked --release --recipe-path recipe.json --no-default-features --features "${PARSEON_FEATURES}"
COPY . .
RUN --mount=type=cache,target=/root/.cargo/registry \
--mount=type=cache,target=/app/target \
cargo build --release --no-default-features --features "${PARSEON_FEATURES}" && \
cargo build --release --locked --no-default-features --features "${PARSEON_FEATURES}" && \
cp /app/target/release/parseon /usr/local/bin/parseon

# ---- runtime: minimal alpine, non-root, healthcheck ----
FROM alpine:3.20 AS runtime
FROM alpine:3.20@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc AS runtime
RUN apk add --no-cache ca-certificates wget && \
adduser -D -u 1000 parseon
COPY --from=builder /usr/local/bin/parseon /usr/local/bin/parseon
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,10 +40,14 @@ parseon-server
└── parseon-webhook-sink ────> parseon-core
```

Start PostgreSQL with `docker compose up -d`, or start the MongoDB development replica set and eRPC gateway with `docker compose --profile mongodb --profile erpc up -d`. Configure the selected backend through `STORAGE_URL`, then register direct RPC or complete eRPC URLs through `POST /chains`.
Start PostgreSQL with `docker compose up -d`, or start the MongoDB development replica set and eRPC gateway with `docker compose --profile mongodb --profile erpc up -d`. Configure the selected backend through `STORAGE_URL` and set a non-empty `API_TOKEN`; protected API routes require `Authorization: Bearer <API_TOKEN>`. Register direct RPC or complete eRPC URLs through `POST /chains`. Private/loopback RPC destinations are rejected unless `ALLOW_PRIVATE_RPC_NETWORKS=true` is explicitly enabled for local development.

See [adapter configuration and guarantees](./docs/adapters.md) for feature builds, MongoDB requirements, eRPC smoke checks, the webhook JSON contract, and Compose profiles.

## Production deployment

Use [`compose.production.yml`](./compose.production.yml) or the [`deploy/helm/parseon`](./deploy/helm/parseon) chart. Both require an externally managed `API_TOKEN` and storage URL, keep databases private, and expose liveness/readiness probes. Read the [production operations runbook](./docs/operations.md) before upgrading, restoring, or exposing the API through an ingress.

## License

Licensed under either the [Apache License, Version 2.0](./LICENSE-APACHE) or the [MIT license](./LICENSE-MIT), at your option.
Expand Down
70 changes: 70 additions & 0 deletions compose.production.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
services:
postgres:
image: ${POSTGRES_IMAGE:-postgres:16@sha256:f1c3376c26f2609ab9f29f71f824103fe2fcd8ee0346485cb6122a4f93df6f94}
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-parseon}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB:-parseon}
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 3s
retries: 12
# No host port: PostgreSQL is reachable only by Parseon on the backend network.

parseon:
image: ${PARSEON_IMAGE:?set PARSEON_IMAGE to an immutable Parseon image}
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
STORAGE_URL: ${STORAGE_URL:?set STORAGE_URL}
API_TOKEN: ${API_TOKEN:?set API_TOKEN}
HTTP_LISTEN: 0.0.0.0:8080
RUST_LOG: ${RUST_LOG:-info}
POLL_INTERVAL_MS: ${POLL_INTERVAL_MS:-2000}
DEFAULT_BATCH_SIZE: ${DEFAULT_BATCH_SIZE:-10}
BLOCK_CACHE_SIZE: ${BLOCK_CACHE_SIZE:-512}
BLOCK_CONCURRENCY: ${BLOCK_CONCURRENCY:-4}
RPC_REQUEST_CONCURRENCY: ${RPC_REQUEST_CONCURRENCY:-16}
STORAGE_WRITE_CONCURRENCY: ${STORAGE_WRITE_CONCURRENCY:-4}
CONFIRMATION_DEPTH: ${CONFIRMATION_DEPTH:-64}
ROLLBACK_RETENTION: ${ROLLBACK_RETENTION:-256}
RPC_BATCH_SIZE: ${RPC_BATCH_SIZE:-20}
ALLOW_PRIVATE_RPC_NETWORKS: "false"
CORS_ORIGINS: ${CORS_ORIGINS:-}
MAX_BODY_BYTES: ${MAX_BODY_BYTES:-1048576}
expose:
- "8080"
networks:
- backend
- egress
read_only: true
tmpfs:
- /tmp
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
mem_limit: ${PARSEON_MEMORY_LIMIT:-768m}
cpus: ${PARSEON_CPUS:-2}
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/healthz"]
interval: 10s
timeout: 5s
start_period: 20s
retries: 6

volumes:
pgdata:

networks:
backend:
internal: true
egress:
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: harden indexing for production v1 by ozcnii · Pull Request #1 · melonges/parseon · GitHub
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .dockerignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ target/
.env
.env.example
.dockerignore
.temp/
Dockerfile
docker-compose.yml
erpc.yaml
Expand Down
9 changes: 9 additions & 0 deletions .env.example
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,26 @@
# Required bearer token for admin/data API routes.
API_TOKEN=replace-with-a-long-random-token
# Default `postgres-storage` build. A `mongodb-storage` build can instead use
# mongodb://localhost:27017/?replicaSet=rs0 and STORAGE_DATABASE=parseon.
STORAGE_URL=postgres://postgres:postgres@localhost:5432/parseon
# STORAGE_DATABASE=parseon
HTTP_LISTEN=0.0.0.0:8080
RUST_LOG=info,parseon=debug
# Comma-separated browser origins; empty keeps CORS disabled.
CORS_ORIGINS=
MAX_BODY_BYTES=1048576
POLL_INTERVAL_MS=2000
DEFAULT_BATCH_SIZE=10
# Set to 0 to disable Parseon's in-memory block cache.
BLOCK_CACHE_SIZE=512
BLOCK_CONCURRENCY=4
RPC_REQUEST_CONCURRENCY=16
STORAGE_WRITE_CONCURRENCY=4
CONFIRMATION_DEPTH=64
ROLLBACK_RETENTION=256
RPC_BATCH_SIZE=20
# Only enable for local RPC containers; keep false in production.
ALLOW_PRIVATE_RPC_NETWORKS=false
# Required only when built with `webhook-sink`.
# WEBHOOK_URL=http://localhost:9000/parseon
# WEBHOOK_CONCURRENCY=16
128 changes: 128 additions & 0 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
name: CI

on:
push:
pull_request:

permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
fmt:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install Rust toolchain
run: rustup toolchain install 1.96.0 --profile minimal && rustup component add rustfmt --toolchain 1.96.0
- run: cargo +1.96.0 fmt --all -- --check

rust:
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
include:
- name: postgres
features: parseon-server/postgres-storage
- name: postgres-webhook
features: parseon-server/postgres-storage,parseon-server/webhook-sink
- name: mongodb
features: parseon-server/mongodb-storage
- name: mongodb-webhook
features: parseon-server/mongodb-storage,parseon-server/webhook-sink
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install Rust toolchain
run: rustup toolchain install 1.96.0 --profile minimal && rustup component add clippy --toolchain 1.96.0
- run: cargo +1.96.0 clippy -q --workspace --all-targets --locked --no-default-features --features "${{ matrix.features }}" --message-format=short -- -D warnings
- run: cargo +1.96.0 test -q --workspace --all-targets --locked --no-default-features --features "${{ matrix.features }}" --message-format=short
- run: cargo +1.96.0 build -q -p parseon-server --release --locked --no-default-features --features "${{ matrix.features }}"

docker:
runs-on: ubuntu-24.04
strategy:
matrix:
include:
- name: postgres
features: postgres-storage
- name: postgres-webhook
features: postgres-storage,webhook-sink
- name: mongodb
features: mongodb-storage
- name: mongodb-webhook
features: mongodb-storage,webhook-sink
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Build image
run: docker build --build-arg PARSEON_FEATURES='${{ matrix.features }}' --tag parseon:ci .
- name: Generate SBOM when Docker SBOM support is installed
shell: bash
run: |
if docker sbom --help >/dev/null 2>&1; then
docker sbom parseon:ci > "sbom-${{ matrix.name }}.spdx.json"
else
echo 'docker sbom is unavailable on this runner; image SBOM is delegated to the registry scanner.'
fi
- name: Scan image when Docker Scout support is installed
shell: bash
run: |
if docker scout version >/dev/null 2>&1; then
docker scout cves --only-fixed parseon:ci
else
echo 'docker scout is unavailable on this runner; image scanning is delegated to the registry.'
fi

release-gates:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Validate production Compose
shell: bash
run: |
export PARSEON_IMAGE=registry.example/parseon@sha256:$(printf 'a%.0s' {1..64})
export STORAGE_URL='postgres://postgres:5432/parseon'
export POSTGRES_PASSWORD=test
export API_TOKEN=test-token
docker compose -f compose.production.yml config >/dev/null
if [[ ! "$PARSEON_IMAGE" =~ @sha256:[0-9a-fA-F]{64}$ ]]; then
echo 'PARSEON_IMAGE must be digest-pinned' >&2
exit 1
fi
- name: Validate Helm chart and immutable deployment policy
shell: bash
run: |
digest="sha256:$(printf 'a%.0s' {1..64})"
docker run --rm -v "$GITHUB_WORKSPACE:/work" -w /work alpine/helm:3.17@sha256:d899e6316789fec04ee95300a18e454b7942539cbb3d89bde3e0655d6ca2e895 lint deploy/helm/parseon --set image.digest="$digest"
docker run --rm -v "$GITHUB_WORKSPACE:/work" -w /work alpine/helm:3.17@sha256:d899e6316789fec04ee95300a18e454b7942539cbb3d89bde3e0655d6ca2e895 template parseon deploy/helm/parseon --set image.digest="$digest" >/dev/null
if docker run --rm -v "$GITHUB_WORKSPACE:/work" -w /work alpine/helm:3.17@sha256:d899e6316789fec04ee95300a18e454b7942539cbb3d89bde3e0655d6ca2e895 template parseon deploy/helm/parseon --set image.digest="$digest" --set replicaCount=2 >/dev/null 2>&1; then
echo 'replicaCount=2 was accepted' >&2
exit 1
fi
if docker run --rm -v "$GITHUB_WORKSPACE:/work" -w /work alpine/helm:3.17@sha256:d899e6316789fec04ee95300a18e454b7942539cbb3d89bde3e0655d6ca2e895 template parseon deploy/helm/parseon >/dev/null 2>&1; then
echo 'missing image digest was accepted' >&2
exit 1
fi
- name: Validate scripts and monitoring artifacts
shell: bash
run: |
bash -n scripts/backup_postgres.sh scripts/restore_postgres.sh
python3 -m py_compile scripts/gen_erpc.py
python3 -m json.tool deploy/monitoring/parseon-dashboard.json >/dev/null
secrets:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- name: Reject credential-bearing generated config
shell: bash
run: |
if git grep -n -I -i -E '://[^[:space:]/:@]+:[^[:space:]@]+@|(^|[?&])(api[_-]?key|access[_-]?token|token|secret|password)=[^[:space:]&]{8,}' -- ':!CHANGELOG.md' ':!docs/operations.md' ':!.env.example'; then
echo 'credential-bearing URL or query parameter found in tracked source' >&2
exit 1
fi
echo 'Historical credentials still require independent revoke/rotate evidence.'
1 change: 1 addition & 0 deletions .gitignore
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
/target
/.env
/.temp
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,8 @@ All notable changes to Parseon are documented in this file.

### Added

- Add canonical block metadata, provisional/finalized lifecycle, bounded reorg rollback, and finalized promotion for both storage adapters.
- Add production Compose and Helm deployment artifacts, backup/restore operations runbook, Prometheus alerts, Grafana dashboard, and CI release gates.
- Apply chain creation, enable/disable, and deletion to running workers without restarting Parseon; deletions stop the worker before removing its data.
- Rotate a chain's RPC endpoint URL in place on the running worker via Alloy's `Http::set_url`, resetting endpoint capability probes while keeping the cached chain ID; sources that cannot rotate restart their worker instead.
- Allow multiple monitors to target the same chain, contract, and function selector or event topic while retaining independent ranges, filters, cursors, and result storage.
Expand All@@ -15,6 +17,8 @@ All notable changes to Parseon are documented in this file.

### Changed

- Require a bearer API token for protected HTTP routes, disable CORS by default, bound request bodies, add liveness/readiness probes, and reject unsafe RPC destinations by default.
- Treat credential-bearing eRPC URLs as operator-injected configuration instead of generated repository content.
- Pin the Compose eRPC image to the `0.1.1` release tag, set a 3 GiB container memory limit with `GOMEMLIMIT=2700MiB`, add `restart: unless-stopped`, and invoke `/erpc-server` explicitly so the distroless image starts correctly.
- Reuse matching RPC data and compatible ABI decoders across overlapping monitor targets before applying each monitor's filter and persisting its own results.
- Isolate ABI decode failures to the affected monitor layout so an incompatible definition cannot stall other monitors on the chain.
Expand All@@ -32,6 +36,8 @@ All notable changes to Parseon are documented in this file.
### Fixed

- Keep persisted chain state and live workers ordered during concurrent mutations and startup reconciliation, and reject new worker starts once supervisor shutdown begins.
- Reject mixed-branch result identities, fail closed when a retained reorg ancestor is unavailable, preserve finalized state for monitors added over existing blocks, and reset rollback cursors below a monitor's start block.
- Pin validated RPC DNS addresses in the HTTP transport, verify generated endpoint chain IDs, expose worker freshness/state metrics, and require encrypted checksum-verified PostgreSQL backup artifacts.

### Breaking

Expand Down
15 changes: 8 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,7 @@ members = [
resolver = "3"

[workspace.package]
version = "0.8.0"
version = "1.0.0"
edition = "2024"
license = "MIT OR Apache-2.0"

Expand Down
10 changes: 5 additions & 5 deletions Dockerfile
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1

# ---- chef base: shared Rust toolchain + cargo-chef ----
FROM rust:1.96-alpine AS chef
FROM rust:1.96-alpine@sha256:a41f7740f8b45d45795624eec13a8b42263cc700f19f7e4e86e04d3dda08a479 AS chef
RUN apk add --no-cache musl-dev
RUN cargo install cargo-chef --locked
WORKDIR /app
Expand All@@ -18,7 +18,7 @@ ARG PARSEON_FEATURES=postgres-storage
ENV PARSEON_FEATURES=${PARSEON_FEATURES}
RUN apk add --no-cache watchexec
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --recipe-path recipe.json --no-default-features --features "${PARSEON_FEATURES}"
RUN cargo chef cook --locked --recipe-path recipe.json --no-default-features --features "${PARSEON_FEATURES}"
COPY . .
EXPOSE 8080
CMD ["sh", "-c", "exec watchexec --restart --stop-signal SIGINT --exts rs,toml,lock,sql -- cargo run --no-default-features --features \"${PARSEON_FEATURES}\""]
Expand All@@ -29,15 +29,15 @@ ARG PARSEON_FEATURES=postgres-storage
COPY --from=planner /app/recipe.json recipe.json
RUN --mount=type=cache,target=/root/.cargo/registry \
--mount=type=cache,target=/app/target \
cargo chef cook --release --recipe-path recipe.json --no-default-features --features "${PARSEON_FEATURES}"
cargo chef cook --locked --release --recipe-path recipe.json --no-default-features --features "${PARSEON_FEATURES}"
COPY . .
RUN --mount=type=cache,target=/root/.cargo/registry \
--mount=type=cache,target=/app/target \
cargo build --release --no-default-features --features "${PARSEON_FEATURES}" && \
cargo build --release --locked --no-default-features --features "${PARSEON_FEATURES}" && \
cp /app/target/release/parseon /usr/local/bin/parseon

# ---- runtime: minimal alpine, non-root, healthcheck ----
FROM alpine:3.20 AS runtime
FROM alpine:3.20@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc AS runtime
RUN apk add --no-cache ca-certificates wget && \
adduser -D -u 1000 parseon
COPY --from=builder /usr/local/bin/parseon /usr/local/bin/parseon
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,10 +40,14 @@ parseon-server
└── parseon-webhook-sink ────> parseon-core
```

Start PostgreSQL with `docker compose up -d`, or start the MongoDB development replica set and eRPC gateway with `docker compose --profile mongodb --profile erpc up -d`. Configure the selected backend through `STORAGE_URL`, then register direct RPC or complete eRPC URLs through `POST /chains`.
Start PostgreSQL with `docker compose up -d`, or start the MongoDB development replica set and eRPC gateway with `docker compose --profile mongodb --profile erpc up -d`. Configure the selected backend through `STORAGE_URL` and set a non-empty `API_TOKEN`; protected API routes require `Authorization: Bearer <API_TOKEN>`. Register direct RPC or complete eRPC URLs through `POST /chains`. Private/loopback RPC destinations are rejected unless `ALLOW_PRIVATE_RPC_NETWORKS=true` is explicitly enabled for local development.

See [adapter configuration and guarantees](./docs/adapters.md) for feature builds, MongoDB requirements, eRPC smoke checks, the webhook JSON contract, and Compose profiles.

## Production deployment

Use [`compose.production.yml`](./compose.production.yml) or the [`deploy/helm/parseon`](./deploy/helm/parseon) chart. Both require an externally managed `API_TOKEN` and storage URL, keep databases private, and expose liveness/readiness probes. Read the [production operations runbook](./docs/operations.md) before upgrading, restoring, or exposing the API through an ingress.

## License

Licensed under either the [Apache License, Version 2.0](./LICENSE-APACHE) or the [MIT license](./LICENSE-MIT), at your option.
Expand Down
70 changes: 70 additions & 0 deletions compose.production.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
services:
postgres:
image: ${POSTGRES_IMAGE:-postgres:16@sha256:f1c3376c26f2609ab9f29f71f824103fe2fcd8ee0346485cb6122a4f93df6f94}
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-parseon}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB:-parseon}
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 3s
retries: 12
# No host port: PostgreSQL is reachable only by Parseon on the backend network.

parseon:
image: ${PARSEON_IMAGE:?set PARSEON_IMAGE to an immutable Parseon image}
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
STORAGE_URL: ${STORAGE_URL:?set STORAGE_URL}
API_TOKEN: ${API_TOKEN:?set API_TOKEN}
HTTP_LISTEN: 0.0.0.0:8080
RUST_LOG: ${RUST_LOG:-info}
POLL_INTERVAL_MS: ${POLL_INTERVAL_MS:-2000}
DEFAULT_BATCH_SIZE: ${DEFAULT_BATCH_SIZE:-10}
BLOCK_CACHE_SIZE: ${BLOCK_CACHE_SIZE:-512}
BLOCK_CONCURRENCY: ${BLOCK_CONCURRENCY:-4}
RPC_REQUEST_CONCURRENCY: ${RPC_REQUEST_CONCURRENCY:-16}
STORAGE_WRITE_CONCURRENCY: ${STORAGE_WRITE_CONCURRENCY:-4}
CONFIRMATION_DEPTH: ${CONFIRMATION_DEPTH:-64}
ROLLBACK_RETENTION: ${ROLLBACK_RETENTION:-256}
RPC_BATCH_SIZE: ${RPC_BATCH_SIZE:-20}
ALLOW_PRIVATE_RPC_NETWORKS: "false"
CORS_ORIGINS: ${CORS_ORIGINS:-}
MAX_BODY_BYTES: ${MAX_BODY_BYTES:-1048576}
expose:
- "8080"
networks:
- backend
- egress
read_only: true
tmpfs:
- /tmp
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
mem_limit: ${PARSEON_MEMORY_LIMIT:-768m}
cpus: ${PARSEON_CPUS:-2}
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/healthz"]
interval: 10s
timeout: 5s
start_period: 20s
retries: 6

volumes:
pgdata:

networks:
backend:
internal: true
egress:
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' feat: harden indexing for production v1 by ozcnii · Pull Request #1 · melonges/parseon · GitHub
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .dockerignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ target/
.env
.env.example
.dockerignore
.temp/
Dockerfile
docker-compose.yml
erpc.yaml
Expand Down
9 changes: 9 additions & 0 deletions .env.example
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,26 @@
# Required bearer token for admin/data API routes.
API_TOKEN=replace-with-a-long-random-token
# Default `postgres-storage` build. A `mongodb-storage` build can instead use
# mongodb://localhost:27017/?replicaSet=rs0 and STORAGE_DATABASE=parseon.
STORAGE_URL=postgres://postgres:postgres@localhost:5432/parseon
# STORAGE_DATABASE=parseon
HTTP_LISTEN=0.0.0.0:8080
RUST_LOG=info,parseon=debug
# Comma-separated browser origins; empty keeps CORS disabled.
CORS_ORIGINS=
MAX_BODY_BYTES=1048576
POLL_INTERVAL_MS=2000
DEFAULT_BATCH_SIZE=10
# Set to 0 to disable Parseon's in-memory block cache.
BLOCK_CACHE_SIZE=512
BLOCK_CONCURRENCY=4
RPC_REQUEST_CONCURRENCY=16
STORAGE_WRITE_CONCURRENCY=4
CONFIRMATION_DEPTH=64
ROLLBACK_RETENTION=256
RPC_BATCH_SIZE=20
# Only enable for local RPC containers; keep false in production.
ALLOW_PRIVATE_RPC_NETWORKS=false
# Required only when built with `webhook-sink`.
# WEBHOOK_URL=http://localhost:9000/parseon
# WEBHOOK_CONCURRENCY=16
128 changes: 128 additions & 0 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
name: CI

on:
push:
pull_request:

permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
fmt:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install Rust toolchain
run: rustup toolchain install 1.96.0 --profile minimal && rustup component add rustfmt --toolchain 1.96.0
- run: cargo +1.96.0 fmt --all -- --check

rust:
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
include:
- name: postgres
features: parseon-server/postgres-storage
- name: postgres-webhook
features: parseon-server/postgres-storage,parseon-server/webhook-sink
- name: mongodb
features: parseon-server/mongodb-storage
- name: mongodb-webhook
features: parseon-server/mongodb-storage,parseon-server/webhook-sink
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install Rust toolchain
run: rustup toolchain install 1.96.0 --profile minimal && rustup component add clippy --toolchain 1.96.0
- run: cargo +1.96.0 clippy -q --workspace --all-targets --locked --no-default-features --features "${{ matrix.features }}" --message-format=short -- -D warnings
- run: cargo +1.96.0 test -q --workspace --all-targets --locked --no-default-features --features "${{ matrix.features }}" --message-format=short
- run: cargo +1.96.0 build -q -p parseon-server --release --locked --no-default-features --features "${{ matrix.features }}"

docker:
runs-on: ubuntu-24.04
strategy:
matrix:
include:
- name: postgres
features: postgres-storage
- name: postgres-webhook
features: postgres-storage,webhook-sink
- name: mongodb
features: mongodb-storage
- name: mongodb-webhook
features: mongodb-storage,webhook-sink
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Build image
run: docker build --build-arg PARSEON_FEATURES='${{ matrix.features }}' --tag parseon:ci .
- name: Generate SBOM when Docker SBOM support is installed
shell: bash
run: |
if docker sbom --help >/dev/null 2>&1; then
docker sbom parseon:ci > "sbom-${{ matrix.name }}.spdx.json"
else
echo 'docker sbom is unavailable on this runner; image SBOM is delegated to the registry scanner.'
fi
- name: Scan image when Docker Scout support is installed
shell: bash
run: |
if docker scout version >/dev/null 2>&1; then
docker scout cves --only-fixed parseon:ci
else
echo 'docker scout is unavailable on this runner; image scanning is delegated to the registry.'
fi

release-gates:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Validate production Compose
shell: bash
run: |
export PARSEON_IMAGE=registry.example/parseon@sha256:$(printf 'a%.0s' {1..64})
export STORAGE_URL='postgres://postgres:5432/parseon'
export POSTGRES_PASSWORD=test
export API_TOKEN=test-token
docker compose -f compose.production.yml config >/dev/null
if [[ ! "$PARSEON_IMAGE" =~ @sha256:[0-9a-fA-F]{64}$ ]]; then
echo 'PARSEON_IMAGE must be digest-pinned' >&2
exit 1
fi
- name: Validate Helm chart and immutable deployment policy
shell: bash
run: |
digest="sha256:$(printf 'a%.0s' {1..64})"
docker run --rm -v "$GITHUB_WORKSPACE:/work" -w /work alpine/helm:3.17@sha256:d899e6316789fec04ee95300a18e454b7942539cbb3d89bde3e0655d6ca2e895 lint deploy/helm/parseon --set image.digest="$digest"
docker run --rm -v "$GITHUB_WORKSPACE:/work" -w /work alpine/helm:3.17@sha256:d899e6316789fec04ee95300a18e454b7942539cbb3d89bde3e0655d6ca2e895 template parseon deploy/helm/parseon --set image.digest="$digest" >/dev/null
if docker run --rm -v "$GITHUB_WORKSPACE:/work" -w /work alpine/helm:3.17@sha256:d899e6316789fec04ee95300a18e454b7942539cbb3d89bde3e0655d6ca2e895 template parseon deploy/helm/parseon --set image.digest="$digest" --set replicaCount=2 >/dev/null 2>&1; then
echo 'replicaCount=2 was accepted' >&2
exit 1
fi
if docker run --rm -v "$GITHUB_WORKSPACE:/work" -w /work alpine/helm:3.17@sha256:d899e6316789fec04ee95300a18e454b7942539cbb3d89bde3e0655d6ca2e895 template parseon deploy/helm/parseon >/dev/null 2>&1; then
echo 'missing image digest was accepted' >&2
exit 1
fi
- name: Validate scripts and monitoring artifacts
shell: bash
run: |
bash -n scripts/backup_postgres.sh scripts/restore_postgres.sh
python3 -m py_compile scripts/gen_erpc.py
python3 -m json.tool deploy/monitoring/parseon-dashboard.json >/dev/null
secrets:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- name: Reject credential-bearing generated config
shell: bash
run: |
if git grep -n -I -i -E '://[^[:space:]/:@]+:[^[:space:]@]+@|(^|[?&])(api[_-]?key|access[_-]?token|token|secret|password)=[^[:space:]&]{8,}' -- ':!CHANGELOG.md' ':!docs/operations.md' ':!.env.example'; then
echo 'credential-bearing URL or query parameter found in tracked source' >&2
exit 1
fi
echo 'Historical credentials still require independent revoke/rotate evidence.'
1 change: 1 addition & 0 deletions .gitignore
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
/target
/.env
/.temp
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,8 @@ All notable changes to Parseon are documented in this file.

### Added

- Add canonical block metadata, provisional/finalized lifecycle, bounded reorg rollback, and finalized promotion for both storage adapters.
- Add production Compose and Helm deployment artifacts, backup/restore operations runbook, Prometheus alerts, Grafana dashboard, and CI release gates.
- Apply chain creation, enable/disable, and deletion to running workers without restarting Parseon; deletions stop the worker before removing its data.
- Rotate a chain's RPC endpoint URL in place on the running worker via Alloy's `Http::set_url`, resetting endpoint capability probes while keeping the cached chain ID; sources that cannot rotate restart their worker instead.
- Allow multiple monitors to target the same chain, contract, and function selector or event topic while retaining independent ranges, filters, cursors, and result storage.
Expand All@@ -15,6 +17,8 @@ All notable changes to Parseon are documented in this file.

### Changed

- Require a bearer API token for protected HTTP routes, disable CORS by default, bound request bodies, add liveness/readiness probes, and reject unsafe RPC destinations by default.
- Treat credential-bearing eRPC URLs as operator-injected configuration instead of generated repository content.
- Pin the Compose eRPC image to the `0.1.1` release tag, set a 3 GiB container memory limit with `GOMEMLIMIT=2700MiB`, add `restart: unless-stopped`, and invoke `/erpc-server` explicitly so the distroless image starts correctly.
- Reuse matching RPC data and compatible ABI decoders across overlapping monitor targets before applying each monitor's filter and persisting its own results.
- Isolate ABI decode failures to the affected monitor layout so an incompatible definition cannot stall other monitors on the chain.
Expand All@@ -32,6 +36,8 @@ All notable changes to Parseon are documented in this file.
### Fixed

- Keep persisted chain state and live workers ordered during concurrent mutations and startup reconciliation, and reject new worker starts once supervisor shutdown begins.
- Reject mixed-branch result identities, fail closed when a retained reorg ancestor is unavailable, preserve finalized state for monitors added over existing blocks, and reset rollback cursors below a monitor's start block.
- Pin validated RPC DNS addresses in the HTTP transport, verify generated endpoint chain IDs, expose worker freshness/state metrics, and require encrypted checksum-verified PostgreSQL backup artifacts.

### Breaking

Expand Down
15 changes: 8 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,7 @@ members = [
resolver = "3"

[workspace.package]
version = "0.8.0"
version = "1.0.0"
edition = "2024"
license = "MIT OR Apache-2.0"

Expand Down
10 changes: 5 additions & 5 deletions Dockerfile
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1

# ---- chef base: shared Rust toolchain + cargo-chef ----
FROM rust:1.96-alpine AS chef
FROM rust:1.96-alpine@sha256:a41f7740f8b45d45795624eec13a8b42263cc700f19f7e4e86e04d3dda08a479 AS chef
RUN apk add --no-cache musl-dev
RUN cargo install cargo-chef --locked
WORKDIR /app
Expand All@@ -18,7 +18,7 @@ ARG PARSEON_FEATURES=postgres-storage
ENV PARSEON_FEATURES=${PARSEON_FEATURES}
RUN apk add --no-cache watchexec
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --recipe-path recipe.json --no-default-features --features "${PARSEON_FEATURES}"
RUN cargo chef cook --locked --recipe-path recipe.json --no-default-features --features "${PARSEON_FEATURES}"
COPY . .
EXPOSE 8080
CMD ["sh", "-c", "exec watchexec --restart --stop-signal SIGINT --exts rs,toml,lock,sql -- cargo run --no-default-features --features \"${PARSEON_FEATURES}\""]
Expand All@@ -29,15 +29,15 @@ ARG PARSEON_FEATURES=postgres-storage
COPY --from=planner /app/recipe.json recipe.json
RUN --mount=type=cache,target=/root/.cargo/registry \
--mount=type=cache,target=/app/target \
cargo chef cook --release --recipe-path recipe.json --no-default-features --features "${PARSEON_FEATURES}"
cargo chef cook --locked --release --recipe-path recipe.json --no-default-features --features "${PARSEON_FEATURES}"
COPY . .
RUN --mount=type=cache,target=/root/.cargo/registry \
--mount=type=cache,target=/app/target \
cargo build --release --no-default-features --features "${PARSEON_FEATURES}" && \
cargo build --release --locked --no-default-features --features "${PARSEON_FEATURES}" && \
cp /app/target/release/parseon /usr/local/bin/parseon

# ---- runtime: minimal alpine, non-root, healthcheck ----
FROM alpine:3.20 AS runtime
FROM alpine:3.20@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc AS runtime
RUN apk add --no-cache ca-certificates wget && \
adduser -D -u 1000 parseon
COPY --from=builder /usr/local/bin/parseon /usr/local/bin/parseon
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,10 +40,14 @@ parseon-server
└── parseon-webhook-sink ────> parseon-core
```

Start PostgreSQL with `docker compose up -d`, or start the MongoDB development replica set and eRPC gateway with `docker compose --profile mongodb --profile erpc up -d`. Configure the selected backend through `STORAGE_URL`, then register direct RPC or complete eRPC URLs through `POST /chains`.
Start PostgreSQL with `docker compose up -d`, or start the MongoDB development replica set and eRPC gateway with `docker compose --profile mongodb --profile erpc up -d`. Configure the selected backend through `STORAGE_URL` and set a non-empty `API_TOKEN`; protected API routes require `Authorization: Bearer <API_TOKEN>`. Register direct RPC or complete eRPC URLs through `POST /chains`. Private/loopback RPC destinations are rejected unless `ALLOW_PRIVATE_RPC_NETWORKS=true` is explicitly enabled for local development.

See [adapter configuration and guarantees](./docs/adapters.md) for feature builds, MongoDB requirements, eRPC smoke checks, the webhook JSON contract, and Compose profiles.

## Production deployment

Use [`compose.production.yml`](./compose.production.yml) or the [`deploy/helm/parseon`](./deploy/helm/parseon) chart. Both require an externally managed `API_TOKEN` and storage URL, keep databases private, and expose liveness/readiness probes. Read the [production operations runbook](./docs/operations.md) before upgrading, restoring, or exposing the API through an ingress.

## License

Licensed under either the [Apache License, Version 2.0](./LICENSE-APACHE) or the [MIT license](./LICENSE-MIT), at your option.
Expand Down
70 changes: 70 additions & 0 deletions compose.production.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
services:
postgres:
image: ${POSTGRES_IMAGE:-postgres:16@sha256:f1c3376c26f2609ab9f29f71f824103fe2fcd8ee0346485cb6122a4f93df6f94}
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-parseon}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB:-parseon}
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 3s
retries: 12
# No host port: PostgreSQL is reachable only by Parseon on the backend network.

parseon:
image: ${PARSEON_IMAGE:?set PARSEON_IMAGE to an immutable Parseon image}
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
STORAGE_URL: ${STORAGE_URL:?set STORAGE_URL}
API_TOKEN: ${API_TOKEN:?set API_TOKEN}
HTTP_LISTEN: 0.0.0.0:8080
RUST_LOG: ${RUST_LOG:-info}
POLL_INTERVAL_MS: ${POLL_INTERVAL_MS:-2000}
DEFAULT_BATCH_SIZE: ${DEFAULT_BATCH_SIZE:-10}
BLOCK_CACHE_SIZE: ${BLOCK_CACHE_SIZE:-512}
BLOCK_CONCURRENCY: ${BLOCK_CONCURRENCY:-4}
RPC_REQUEST_CONCURRENCY: ${RPC_REQUEST_CONCURRENCY:-16}
STORAGE_WRITE_CONCURRENCY: ${STORAGE_WRITE_CONCURRENCY:-4}
CONFIRMATION_DEPTH: ${CONFIRMATION_DEPTH:-64}
ROLLBACK_RETENTION: ${ROLLBACK_RETENTION:-256}
RPC_BATCH_SIZE: ${RPC_BATCH_SIZE:-20}
ALLOW_PRIVATE_RPC_NETWORKS: "false"
CORS_ORIGINS: ${CORS_ORIGINS:-}
MAX_BODY_BYTES: ${MAX_BODY_BYTES:-1048576}
expose:
- "8080"
networks:
- backend
- egress
read_only: true
tmpfs:
- /tmp
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
mem_limit: ${PARSEON_MEMORY_LIMIT:-768m}
cpus: ${PARSEON_CPUS:-2}
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/healthz"]
interval: 10s
timeout: 5s
start_period: 20s
retries: 6

volumes:
pgdata:

networks:
backend:
internal: true
egress:
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: harden indexing for production v1 by ozcnii · Pull Request #1 · melonges/parseon · GitHub
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .dockerignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ target/
.env
.env.example
.dockerignore
.temp/
Dockerfile
docker-compose.yml
erpc.yaml
Expand Down
9 changes: 9 additions & 0 deletions .env.example
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,26 @@
# Required bearer token for admin/data API routes.
API_TOKEN=replace-with-a-long-random-token
# Default `postgres-storage` build. A `mongodb-storage` build can instead use
# mongodb://localhost:27017/?replicaSet=rs0 and STORAGE_DATABASE=parseon.
STORAGE_URL=postgres://postgres:postgres@localhost:5432/parseon
# STORAGE_DATABASE=parseon
HTTP_LISTEN=0.0.0.0:8080
RUST_LOG=info,parseon=debug
# Comma-separated browser origins; empty keeps CORS disabled.
CORS_ORIGINS=
MAX_BODY_BYTES=1048576
POLL_INTERVAL_MS=2000
DEFAULT_BATCH_SIZE=10
# Set to 0 to disable Parseon's in-memory block cache.
BLOCK_CACHE_SIZE=512
BLOCK_CONCURRENCY=4
RPC_REQUEST_CONCURRENCY=16
STORAGE_WRITE_CONCURRENCY=4
CONFIRMATION_DEPTH=64
ROLLBACK_RETENTION=256
RPC_BATCH_SIZE=20
# Only enable for local RPC containers; keep false in production.
ALLOW_PRIVATE_RPC_NETWORKS=false
# Required only when built with `webhook-sink`.
# WEBHOOK_URL=http://localhost:9000/parseon
# WEBHOOK_CONCURRENCY=16
128 changes: 128 additions & 0 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
name: CI

on:
push:
pull_request:

permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
fmt:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install Rust toolchain
run: rustup toolchain install 1.96.0 --profile minimal && rustup component add rustfmt --toolchain 1.96.0
- run: cargo +1.96.0 fmt --all -- --check

rust:
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
include:
- name: postgres
features: parseon-server/postgres-storage
- name: postgres-webhook
features: parseon-server/postgres-storage,parseon-server/webhook-sink
- name: mongodb
features: parseon-server/mongodb-storage
- name: mongodb-webhook
features: parseon-server/mongodb-storage,parseon-server/webhook-sink
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install Rust toolchain
run: rustup toolchain install 1.96.0 --profile minimal && rustup component add clippy --toolchain 1.96.0
- run: cargo +1.96.0 clippy -q --workspace --all-targets --locked --no-default-features --features "${{ matrix.features }}" --message-format=short -- -D warnings
- run: cargo +1.96.0 test -q --workspace --all-targets --locked --no-default-features --features "${{ matrix.features }}" --message-format=short
- run: cargo +1.96.0 build -q -p parseon-server --release --locked --no-default-features --features "${{ matrix.features }}"

docker:
runs-on: ubuntu-24.04
strategy:
matrix:
include:
- name: postgres
features: postgres-storage
- name: postgres-webhook
features: postgres-storage,webhook-sink
- name: mongodb
features: mongodb-storage
- name: mongodb-webhook
features: mongodb-storage,webhook-sink
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Build image
run: docker build --build-arg PARSEON_FEATURES='${{ matrix.features }}' --tag parseon:ci .
- name: Generate SBOM when Docker SBOM support is installed
shell: bash
run: |
if docker sbom --help >/dev/null 2>&1; then
docker sbom parseon:ci > "sbom-${{ matrix.name }}.spdx.json"
else
echo 'docker sbom is unavailable on this runner; image SBOM is delegated to the registry scanner.'
fi
- name: Scan image when Docker Scout support is installed
shell: bash
run: |
if docker scout version >/dev/null 2>&1; then
docker scout cves --only-fixed parseon:ci
else
echo 'docker scout is unavailable on this runner; image scanning is delegated to the registry.'
fi

release-gates:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Validate production Compose
shell: bash
run: |
export PARSEON_IMAGE=registry.example/parseon@sha256:$(printf 'a%.0s' {1..64})
export STORAGE_URL='postgres://postgres:5432/parseon'
export POSTGRES_PASSWORD=test
export API_TOKEN=test-token
docker compose -f compose.production.yml config >/dev/null
if [[ ! "$PARSEON_IMAGE" =~ @sha256:[0-9a-fA-F]{64}$ ]]; then
echo 'PARSEON_IMAGE must be digest-pinned' >&2
exit 1
fi
- name: Validate Helm chart and immutable deployment policy
shell: bash
run: |
digest="sha256:$(printf 'a%.0s' {1..64})"
docker run --rm -v "$GITHUB_WORKSPACE:/work" -w /work alpine/helm:3.17@sha256:d899e6316789fec04ee95300a18e454b7942539cbb3d89bde3e0655d6ca2e895 lint deploy/helm/parseon --set image.digest="$digest"
docker run --rm -v "$GITHUB_WORKSPACE:/work" -w /work alpine/helm:3.17@sha256:d899e6316789fec04ee95300a18e454b7942539cbb3d89bde3e0655d6ca2e895 template parseon deploy/helm/parseon --set image.digest="$digest" >/dev/null
if docker run --rm -v "$GITHUB_WORKSPACE:/work" -w /work alpine/helm:3.17@sha256:d899e6316789fec04ee95300a18e454b7942539cbb3d89bde3e0655d6ca2e895 template parseon deploy/helm/parseon --set image.digest="$digest" --set replicaCount=2 >/dev/null 2>&1; then
echo 'replicaCount=2 was accepted' >&2
exit 1
fi
if docker run --rm -v "$GITHUB_WORKSPACE:/work" -w /work alpine/helm:3.17@sha256:d899e6316789fec04ee95300a18e454b7942539cbb3d89bde3e0655d6ca2e895 template parseon deploy/helm/parseon >/dev/null 2>&1; then
echo 'missing image digest was accepted' >&2
exit 1
fi
- name: Validate scripts and monitoring artifacts
shell: bash
run: |
bash -n scripts/backup_postgres.sh scripts/restore_postgres.sh
python3 -m py_compile scripts/gen_erpc.py
python3 -m json.tool deploy/monitoring/parseon-dashboard.json >/dev/null
secrets:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- name: Reject credential-bearing generated config
shell: bash
run: |
if git grep -n -I -i -E '://[^[:space:]/:@]+:[^[:space:]@]+@|(^|[?&])(api[_-]?key|access[_-]?token|token|secret|password)=[^[:space:]&]{8,}' -- ':!CHANGELOG.md' ':!docs/operations.md' ':!.env.example'; then
echo 'credential-bearing URL or query parameter found in tracked source' >&2
exit 1
fi
echo 'Historical credentials still require independent revoke/rotate evidence.'
1 change: 1 addition & 0 deletions .gitignore
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
/target
/.env
/.temp
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,8 @@ All notable changes to Parseon are documented in this file.

### Added

- Add canonical block metadata, provisional/finalized lifecycle, bounded reorg rollback, and finalized promotion for both storage adapters.
- Add production Compose and Helm deployment artifacts, backup/restore operations runbook, Prometheus alerts, Grafana dashboard, and CI release gates.
- Apply chain creation, enable/disable, and deletion to running workers without restarting Parseon; deletions stop the worker before removing its data.
- Rotate a chain's RPC endpoint URL in place on the running worker via Alloy's `Http::set_url`, resetting endpoint capability probes while keeping the cached chain ID; sources that cannot rotate restart their worker instead.
- Allow multiple monitors to target the same chain, contract, and function selector or event topic while retaining independent ranges, filters, cursors, and result storage.
Expand All@@ -15,6 +17,8 @@ All notable changes to Parseon are documented in this file.

### Changed

- Require a bearer API token for protected HTTP routes, disable CORS by default, bound request bodies, add liveness/readiness probes, and reject unsafe RPC destinations by default.
- Treat credential-bearing eRPC URLs as operator-injected configuration instead of generated repository content.
- Pin the Compose eRPC image to the `0.1.1` release tag, set a 3 GiB container memory limit with `GOMEMLIMIT=2700MiB`, add `restart: unless-stopped`, and invoke `/erpc-server` explicitly so the distroless image starts correctly.
- Reuse matching RPC data and compatible ABI decoders across overlapping monitor targets before applying each monitor's filter and persisting its own results.
- Isolate ABI decode failures to the affected monitor layout so an incompatible definition cannot stall other monitors on the chain.
Expand All@@ -32,6 +36,8 @@ All notable changes to Parseon are documented in this file.
### Fixed

- Keep persisted chain state and live workers ordered during concurrent mutations and startup reconciliation, and reject new worker starts once supervisor shutdown begins.
- Reject mixed-branch result identities, fail closed when a retained reorg ancestor is unavailable, preserve finalized state for monitors added over existing blocks, and reset rollback cursors below a monitor's start block.
- Pin validated RPC DNS addresses in the HTTP transport, verify generated endpoint chain IDs, expose worker freshness/state metrics, and require encrypted checksum-verified PostgreSQL backup artifacts.

### Breaking

Expand Down
15 changes: 8 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,7 @@ members = [
resolver = "3"

[workspace.package]
version = "0.8.0"
version = "1.0.0"
edition = "2024"
license = "MIT OR Apache-2.0"

Expand Down
10 changes: 5 additions & 5 deletions Dockerfile
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1

# ---- chef base: shared Rust toolchain + cargo-chef ----
FROM rust:1.96-alpine AS chef
FROM rust:1.96-alpine@sha256:a41f7740f8b45d45795624eec13a8b42263cc700f19f7e4e86e04d3dda08a479 AS chef
RUN apk add --no-cache musl-dev
RUN cargo install cargo-chef --locked
WORKDIR /app
Expand All@@ -18,7 +18,7 @@ ARG PARSEON_FEATURES=postgres-storage
ENV PARSEON_FEATURES=${PARSEON_FEATURES}
RUN apk add --no-cache watchexec
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --recipe-path recipe.json --no-default-features --features "${PARSEON_FEATURES}"
RUN cargo chef cook --locked --recipe-path recipe.json --no-default-features --features "${PARSEON_FEATURES}"
COPY . .
EXPOSE 8080
CMD ["sh", "-c", "exec watchexec --restart --stop-signal SIGINT --exts rs,toml,lock,sql -- cargo run --no-default-features --features \"${PARSEON_FEATURES}\""]
Expand All@@ -29,15 +29,15 @@ ARG PARSEON_FEATURES=postgres-storage
COPY --from=planner /app/recipe.json recipe.json
RUN --mount=type=cache,target=/root/.cargo/registry \
--mount=type=cache,target=/app/target \
cargo chef cook --release --recipe-path recipe.json --no-default-features --features "${PARSEON_FEATURES}"
cargo chef cook --locked --release --recipe-path recipe.json --no-default-features --features "${PARSEON_FEATURES}"
COPY . .
RUN --mount=type=cache,target=/root/.cargo/registry \
--mount=type=cache,target=/app/target \
cargo build --release --no-default-features --features "${PARSEON_FEATURES}" && \
cargo build --release --locked --no-default-features --features "${PARSEON_FEATURES}" && \
cp /app/target/release/parseon /usr/local/bin/parseon

# ---- runtime: minimal alpine, non-root, healthcheck ----
FROM alpine:3.20 AS runtime
FROM alpine:3.20@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc AS runtime
RUN apk add --no-cache ca-certificates wget && \
adduser -D -u 1000 parseon
COPY --from=builder /usr/local/bin/parseon /usr/local/bin/parseon
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,10 +40,14 @@ parseon-server
└── parseon-webhook-sink ────> parseon-core
```

Start PostgreSQL with `docker compose up -d`, or start the MongoDB development replica set and eRPC gateway with `docker compose --profile mongodb --profile erpc up -d`. Configure the selected backend through `STORAGE_URL`, then register direct RPC or complete eRPC URLs through `POST /chains`.
Start PostgreSQL with `docker compose up -d`, or start the MongoDB development replica set and eRPC gateway with `docker compose --profile mongodb --profile erpc up -d`. Configure the selected backend through `STORAGE_URL` and set a non-empty `API_TOKEN`; protected API routes require `Authorization: Bearer <API_TOKEN>`. Register direct RPC or complete eRPC URLs through `POST /chains`. Private/loopback RPC destinations are rejected unless `ALLOW_PRIVATE_RPC_NETWORKS=true` is explicitly enabled for local development.

See [adapter configuration and guarantees](./docs/adapters.md) for feature builds, MongoDB requirements, eRPC smoke checks, the webhook JSON contract, and Compose profiles.

## Production deployment

Use [`compose.production.yml`](./compose.production.yml) or the [`deploy/helm/parseon`](./deploy/helm/parseon) chart. Both require an externally managed `API_TOKEN` and storage URL, keep databases private, and expose liveness/readiness probes. Read the [production operations runbook](./docs/operations.md) before upgrading, restoring, or exposing the API through an ingress.

## License

Licensed under either the [Apache License, Version 2.0](./LICENSE-APACHE) or the [MIT license](./LICENSE-MIT), at your option.
Expand Down
70 changes: 70 additions & 0 deletions compose.production.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
services:
postgres:
image: ${POSTGRES_IMAGE:-postgres:16@sha256:f1c3376c26f2609ab9f29f71f824103fe2fcd8ee0346485cb6122a4f93df6f94}
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-parseon}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB:-parseon}
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 3s
retries: 12
# No host port: PostgreSQL is reachable only by Parseon on the backend network.

parseon:
image: ${PARSEON_IMAGE:?set PARSEON_IMAGE to an immutable Parseon image}
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
STORAGE_URL: ${STORAGE_URL:?set STORAGE_URL}
API_TOKEN: ${API_TOKEN:?set API_TOKEN}
HTTP_LISTEN: 0.0.0.0:8080
RUST_LOG: ${RUST_LOG:-info}
POLL_INTERVAL_MS: ${POLL_INTERVAL_MS:-2000}
DEFAULT_BATCH_SIZE: ${DEFAULT_BATCH_SIZE:-10}
BLOCK_CACHE_SIZE: ${BLOCK_CACHE_SIZE:-512}
BLOCK_CONCURRENCY: ${BLOCK_CONCURRENCY:-4}
RPC_REQUEST_CONCURRENCY: ${RPC_REQUEST_CONCURRENCY:-16}
STORAGE_WRITE_CONCURRENCY: ${STORAGE_WRITE_CONCURRENCY:-4}
CONFIRMATION_DEPTH: ${CONFIRMATION_DEPTH:-64}
ROLLBACK_RETENTION: ${ROLLBACK_RETENTION:-256}
RPC_BATCH_SIZE: ${RPC_BATCH_SIZE:-20}
ALLOW_PRIVATE_RPC_NETWORKS: "false"
CORS_ORIGINS: ${CORS_ORIGINS:-}
MAX_BODY_BYTES: ${MAX_BODY_BYTES:-1048576}
expose:
- "8080"
networks:
- backend
- egress
read_only: true
tmpfs:
- /tmp
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
mem_limit: ${PARSEON_MEMORY_LIMIT:-768m}
cpus: ${PARSEON_CPUS:-2}
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/healthz"]
interval: 10s
timeout: 5s
start_period: 20s
retries: 6

volumes:
pgdata:

networks:
backend:
internal: true
egress:
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: harden indexing for production v1 by ozcnii · Pull Request #1 · melonges/parseon · GitHub
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .dockerignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ target/
.env
.env.example
.dockerignore
.temp/
Dockerfile
docker-compose.yml
erpc.yaml
Expand Down
9 changes: 9 additions & 0 deletions .env.example
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,26 @@
# Required bearer token for admin/data API routes.
API_TOKEN=replace-with-a-long-random-token
# Default `postgres-storage` build. A `mongodb-storage` build can instead use
# mongodb://localhost:27017/?replicaSet=rs0 and STORAGE_DATABASE=parseon.
STORAGE_URL=postgres://postgres:postgres@localhost:5432/parseon
# STORAGE_DATABASE=parseon
HTTP_LISTEN=0.0.0.0:8080
RUST_LOG=info,parseon=debug
# Comma-separated browser origins; empty keeps CORS disabled.
CORS_ORIGINS=
MAX_BODY_BYTES=1048576
POLL_INTERVAL_MS=2000
DEFAULT_BATCH_SIZE=10
# Set to 0 to disable Parseon's in-memory block cache.
BLOCK_CACHE_SIZE=512
BLOCK_CONCURRENCY=4
RPC_REQUEST_CONCURRENCY=16
STORAGE_WRITE_CONCURRENCY=4
CONFIRMATION_DEPTH=64
ROLLBACK_RETENTION=256
RPC_BATCH_SIZE=20
# Only enable for local RPC containers; keep false in production.
ALLOW_PRIVATE_RPC_NETWORKS=false
# Required only when built with `webhook-sink`.
# WEBHOOK_URL=http://localhost:9000/parseon
# WEBHOOK_CONCURRENCY=16
128 changes: 128 additions & 0 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
name: CI

on:
push:
pull_request:

permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
fmt:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install Rust toolchain
run: rustup toolchain install 1.96.0 --profile minimal && rustup component add rustfmt --toolchain 1.96.0
- run: cargo +1.96.0 fmt --all -- --check

rust:
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
include:
- name: postgres
features: parseon-server/postgres-storage
- name: postgres-webhook
features: parseon-server/postgres-storage,parseon-server/webhook-sink
- name: mongodb
features: parseon-server/mongodb-storage
- name: mongodb-webhook
features: parseon-server/mongodb-storage,parseon-server/webhook-sink
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install Rust toolchain
run: rustup toolchain install 1.96.0 --profile minimal && rustup component add clippy --toolchain 1.96.0
- run: cargo +1.96.0 clippy -q --workspace --all-targets --locked --no-default-features --features "${{ matrix.features }}" --message-format=short -- -D warnings
- run: cargo +1.96.0 test -q --workspace --all-targets --locked --no-default-features --features "${{ matrix.features }}" --message-format=short
- run: cargo +1.96.0 build -q -p parseon-server --release --locked --no-default-features --features "${{ matrix.features }}"

docker:
runs-on: ubuntu-24.04
strategy:
matrix:
include:
- name: postgres
features: postgres-storage
- name: postgres-webhook
features: postgres-storage,webhook-sink
- name: mongodb
features: mongodb-storage
- name: mongodb-webhook
features: mongodb-storage,webhook-sink
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Build image
run: docker build --build-arg PARSEON_FEATURES='${{ matrix.features }}' --tag parseon:ci .
- name: Generate SBOM when Docker SBOM support is installed
shell: bash
run: |
if docker sbom --help >/dev/null 2>&1; then
docker sbom parseon:ci > "sbom-${{ matrix.name }}.spdx.json"
else
echo 'docker sbom is unavailable on this runner; image SBOM is delegated to the registry scanner.'
fi
- name: Scan image when Docker Scout support is installed
shell: bash
run: |
if docker scout version >/dev/null 2>&1; then
docker scout cves --only-fixed parseon:ci
else
echo 'docker scout is unavailable on this runner; image scanning is delegated to the registry.'
fi

release-gates:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Validate production Compose
shell: bash
run: |
export PARSEON_IMAGE=registry.example/parseon@sha256:$(printf 'a%.0s' {1..64})
export STORAGE_URL='postgres://postgres:5432/parseon'
export POSTGRES_PASSWORD=test
export API_TOKEN=test-token
docker compose -f compose.production.yml config >/dev/null
if [[ ! "$PARSEON_IMAGE" =~ @sha256:[0-9a-fA-F]{64}$ ]]; then
echo 'PARSEON_IMAGE must be digest-pinned' >&2
exit 1
fi
- name: Validate Helm chart and immutable deployment policy
shell: bash
run: |
digest="sha256:$(printf 'a%.0s' {1..64})"
docker run --rm -v "$GITHUB_WORKSPACE:/work" -w /work alpine/helm:3.17@sha256:d899e6316789fec04ee95300a18e454b7942539cbb3d89bde3e0655d6ca2e895 lint deploy/helm/parseon --set image.digest="$digest"
docker run --rm -v "$GITHUB_WORKSPACE:/work" -w /work alpine/helm:3.17@sha256:d899e6316789fec04ee95300a18e454b7942539cbb3d89bde3e0655d6ca2e895 template parseon deploy/helm/parseon --set image.digest="$digest" >/dev/null
if docker run --rm -v "$GITHUB_WORKSPACE:/work" -w /work alpine/helm:3.17@sha256:d899e6316789fec04ee95300a18e454b7942539cbb3d89bde3e0655d6ca2e895 template parseon deploy/helm/parseon --set image.digest="$digest" --set replicaCount=2 >/dev/null 2>&1; then
echo 'replicaCount=2 was accepted' >&2
exit 1
fi
if docker run --rm -v "$GITHUB_WORKSPACE:/work" -w /work alpine/helm:3.17@sha256:d899e6316789fec04ee95300a18e454b7942539cbb3d89bde3e0655d6ca2e895 template parseon deploy/helm/parseon >/dev/null 2>&1; then
echo 'missing image digest was accepted' >&2
exit 1
fi
- name: Validate scripts and monitoring artifacts
shell: bash
run: |
bash -n scripts/backup_postgres.sh scripts/restore_postgres.sh
python3 -m py_compile scripts/gen_erpc.py
python3 -m json.tool deploy/monitoring/parseon-dashboard.json >/dev/null
secrets:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- name: Reject credential-bearing generated config
shell: bash
run: |
if git grep -n -I -i -E '://[^[:space:]/:@]+:[^[:space:]@]+@|(^|[?&])(api[_-]?key|access[_-]?token|token|secret|password)=[^[:space:]&]{8,}' -- ':!CHANGELOG.md' ':!docs/operations.md' ':!.env.example'; then
echo 'credential-bearing URL or query parameter found in tracked source' >&2
exit 1
fi
echo 'Historical credentials still require independent revoke/rotate evidence.'
1 change: 1 addition & 0 deletions .gitignore
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
/target
/.env
/.temp
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,8 @@ All notable changes to Parseon are documented in this file.

### Added

- Add canonical block metadata, provisional/finalized lifecycle, bounded reorg rollback, and finalized promotion for both storage adapters.
- Add production Compose and Helm deployment artifacts, backup/restore operations runbook, Prometheus alerts, Grafana dashboard, and CI release gates.
- Apply chain creation, enable/disable, and deletion to running workers without restarting Parseon; deletions stop the worker before removing its data.
- Rotate a chain's RPC endpoint URL in place on the running worker via Alloy's `Http::set_url`, resetting endpoint capability probes while keeping the cached chain ID; sources that cannot rotate restart their worker instead.
- Allow multiple monitors to target the same chain, contract, and function selector or event topic while retaining independent ranges, filters, cursors, and result storage.
Expand All@@ -15,6 +17,8 @@ All notable changes to Parseon are documented in this file.

### Changed

- Require a bearer API token for protected HTTP routes, disable CORS by default, bound request bodies, add liveness/readiness probes, and reject unsafe RPC destinations by default.
- Treat credential-bearing eRPC URLs as operator-injected configuration instead of generated repository content.
- Pin the Compose eRPC image to the `0.1.1` release tag, set a 3 GiB container memory limit with `GOMEMLIMIT=2700MiB`, add `restart: unless-stopped`, and invoke `/erpc-server` explicitly so the distroless image starts correctly.
- Reuse matching RPC data and compatible ABI decoders across overlapping monitor targets before applying each monitor's filter and persisting its own results.
- Isolate ABI decode failures to the affected monitor layout so an incompatible definition cannot stall other monitors on the chain.
Expand All@@ -32,6 +36,8 @@ All notable changes to Parseon are documented in this file.
### Fixed

- Keep persisted chain state and live workers ordered during concurrent mutations and startup reconciliation, and reject new worker starts once supervisor shutdown begins.
- Reject mixed-branch result identities, fail closed when a retained reorg ancestor is unavailable, preserve finalized state for monitors added over existing blocks, and reset rollback cursors below a monitor's start block.
- Pin validated RPC DNS addresses in the HTTP transport, verify generated endpoint chain IDs, expose worker freshness/state metrics, and require encrypted checksum-verified PostgreSQL backup artifacts.

### Breaking

Expand Down
15 changes: 8 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,7 @@ members = [
resolver = "3"

[workspace.package]
version = "0.8.0"
version = "1.0.0"
edition = "2024"
license = "MIT OR Apache-2.0"

Expand Down
10 changes: 5 additions & 5 deletions Dockerfile
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1

# ---- chef base: shared Rust toolchain + cargo-chef ----
FROM rust:1.96-alpine AS chef
FROM rust:1.96-alpine@sha256:a41f7740f8b45d45795624eec13a8b42263cc700f19f7e4e86e04d3dda08a479 AS chef
RUN apk add --no-cache musl-dev
RUN cargo install cargo-chef --locked
WORKDIR /app
Expand All@@ -18,7 +18,7 @@ ARG PARSEON_FEATURES=postgres-storage
ENV PARSEON_FEATURES=${PARSEON_FEATURES}
RUN apk add --no-cache watchexec
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --recipe-path recipe.json --no-default-features --features "${PARSEON_FEATURES}"
RUN cargo chef cook --locked --recipe-path recipe.json --no-default-features --features "${PARSEON_FEATURES}"
COPY . .
EXPOSE 8080
CMD ["sh", "-c", "exec watchexec --restart --stop-signal SIGINT --exts rs,toml,lock,sql -- cargo run --no-default-features --features \"${PARSEON_FEATURES}\""]
Expand All@@ -29,15 +29,15 @@ ARG PARSEON_FEATURES=postgres-storage
COPY --from=planner /app/recipe.json recipe.json
RUN --mount=type=cache,target=/root/.cargo/registry \
--mount=type=cache,target=/app/target \
cargo chef cook --release --recipe-path recipe.json --no-default-features --features "${PARSEON_FEATURES}"
cargo chef cook --locked --release --recipe-path recipe.json --no-default-features --features "${PARSEON_FEATURES}"
COPY . .
RUN --mount=type=cache,target=/root/.cargo/registry \
--mount=type=cache,target=/app/target \
cargo build --release --no-default-features --features "${PARSEON_FEATURES}" && \
cargo build --release --locked --no-default-features --features "${PARSEON_FEATURES}" && \
cp /app/target/release/parseon /usr/local/bin/parseon

# ---- runtime: minimal alpine, non-root, healthcheck ----
FROM alpine:3.20 AS runtime
FROM alpine:3.20@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc AS runtime
RUN apk add --no-cache ca-certificates wget && \
adduser -D -u 1000 parseon
COPY --from=builder /usr/local/bin/parseon /usr/local/bin/parseon
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,10 +40,14 @@ parseon-server
└── parseon-webhook-sink ────> parseon-core
```

Start PostgreSQL with `docker compose up -d`, or start the MongoDB development replica set and eRPC gateway with `docker compose --profile mongodb --profile erpc up -d`. Configure the selected backend through `STORAGE_URL`, then register direct RPC or complete eRPC URLs through `POST /chains`.
Start PostgreSQL with `docker compose up -d`, or start the MongoDB development replica set and eRPC gateway with `docker compose --profile mongodb --profile erpc up -d`. Configure the selected backend through `STORAGE_URL` and set a non-empty `API_TOKEN`; protected API routes require `Authorization: Bearer <API_TOKEN>`. Register direct RPC or complete eRPC URLs through `POST /chains`. Private/loopback RPC destinations are rejected unless `ALLOW_PRIVATE_RPC_NETWORKS=true` is explicitly enabled for local development.

See [adapter configuration and guarantees](./docs/adapters.md) for feature builds, MongoDB requirements, eRPC smoke checks, the webhook JSON contract, and Compose profiles.

## Production deployment

Use [`compose.production.yml`](./compose.production.yml) or the [`deploy/helm/parseon`](./deploy/helm/parseon) chart. Both require an externally managed `API_TOKEN` and storage URL, keep databases private, and expose liveness/readiness probes. Read the [production operations runbook](./docs/operations.md) before upgrading, restoring, or exposing the API through an ingress.

## License

Licensed under either the [Apache License, Version 2.0](./LICENSE-APACHE) or the [MIT license](./LICENSE-MIT), at your option.
Expand Down
70 changes: 70 additions & 0 deletions compose.production.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
services:
postgres:
image: ${POSTGRES_IMAGE:-postgres:16@sha256:f1c3376c26f2609ab9f29f71f824103fe2fcd8ee0346485cb6122a4f93df6f94}
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-parseon}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB:-parseon}
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 3s
retries: 12
# No host port: PostgreSQL is reachable only by Parseon on the backend network.

parseon:
image: ${PARSEON_IMAGE:?set PARSEON_IMAGE to an immutable Parseon image}
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
STORAGE_URL: ${STORAGE_URL:?set STORAGE_URL}
API_TOKEN: ${API_TOKEN:?set API_TOKEN}
HTTP_LISTEN: 0.0.0.0:8080
RUST_LOG: ${RUST_LOG:-info}
POLL_INTERVAL_MS: ${POLL_INTERVAL_MS:-2000}
DEFAULT_BATCH_SIZE: ${DEFAULT_BATCH_SIZE:-10}
BLOCK_CACHE_SIZE: ${BLOCK_CACHE_SIZE:-512}
BLOCK_CONCURRENCY: ${BLOCK_CONCURRENCY:-4}
RPC_REQUEST_CONCURRENCY: ${RPC_REQUEST_CONCURRENCY:-16}
STORAGE_WRITE_CONCURRENCY: ${STORAGE_WRITE_CONCURRENCY:-4}
CONFIRMATION_DEPTH: ${CONFIRMATION_DEPTH:-64}
ROLLBACK_RETENTION: ${ROLLBACK_RETENTION:-256}
RPC_BATCH_SIZE: ${RPC_BATCH_SIZE:-20}
ALLOW_PRIVATE_RPC_NETWORKS: "false"
CORS_ORIGINS: ${CORS_ORIGINS:-}
MAX_BODY_BYTES: ${MAX_BODY_BYTES:-1048576}
expose:
- "8080"
networks:
- backend
- egress
read_only: true
tmpfs:
- /tmp
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
mem_limit: ${PARSEON_MEMORY_LIMIT:-768m}
cpus: ${PARSEON_CPUS:-2}
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/healthz"]
interval: 10s
timeout: 5s
start_period: 20s
retries: 6

volumes:
pgdata:

networks:
backend:
internal: true
egress:
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); feat: harden indexing for production v1 by ozcnii · Pull Request #1 · melonges/parseon · GitHub
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .dockerignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ target/
.env
.env.example
.dockerignore
.temp/
Dockerfile
docker-compose.yml
erpc.yaml
Expand Down
9 changes: 9 additions & 0 deletions .env.example
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,26 @@
# Required bearer token for admin/data API routes.
API_TOKEN=replace-with-a-long-random-token
# Default `postgres-storage` build. A `mongodb-storage` build can instead use
# mongodb://localhost:27017/?replicaSet=rs0 and STORAGE_DATABASE=parseon.
STORAGE_URL=postgres://postgres:postgres@localhost:5432/parseon
# STORAGE_DATABASE=parseon
HTTP_LISTEN=0.0.0.0:8080
RUST_LOG=info,parseon=debug
# Comma-separated browser origins; empty keeps CORS disabled.
CORS_ORIGINS=
MAX_BODY_BYTES=1048576
POLL_INTERVAL_MS=2000
DEFAULT_BATCH_SIZE=10
# Set to 0 to disable Parseon's in-memory block cache.
BLOCK_CACHE_SIZE=512
BLOCK_CONCURRENCY=4
RPC_REQUEST_CONCURRENCY=16
STORAGE_WRITE_CONCURRENCY=4
CONFIRMATION_DEPTH=64
ROLLBACK_RETENTION=256
RPC_BATCH_SIZE=20
# Only enable for local RPC containers; keep false in production.
ALLOW_PRIVATE_RPC_NETWORKS=false
# Required only when built with `webhook-sink`.
# WEBHOOK_URL=http://localhost:9000/parseon
# WEBHOOK_CONCURRENCY=16
128 changes: 128 additions & 0 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
name: CI

on:
push:
pull_request:

permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
fmt:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install Rust toolchain
run: rustup toolchain install 1.96.0 --profile minimal && rustup component add rustfmt --toolchain 1.96.0
- run: cargo +1.96.0 fmt --all -- --check

rust:
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
include:
- name: postgres
features: parseon-server/postgres-storage
- name: postgres-webhook
features: parseon-server/postgres-storage,parseon-server/webhook-sink
- name: mongodb
features: parseon-server/mongodb-storage
- name: mongodb-webhook
features: parseon-server/mongodb-storage,parseon-server/webhook-sink
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install Rust toolchain
run: rustup toolchain install 1.96.0 --profile minimal && rustup component add clippy --toolchain 1.96.0
- run: cargo +1.96.0 clippy -q --workspace --all-targets --locked --no-default-features --features "${{ matrix.features }}" --message-format=short -- -D warnings
- run: cargo +1.96.0 test -q --workspace --all-targets --locked --no-default-features --features "${{ matrix.features }}" --message-format=short
- run: cargo +1.96.0 build -q -p parseon-server --release --locked --no-default-features --features "${{ matrix.features }}"

docker:
runs-on: ubuntu-24.04
strategy:
matrix:
include:
- name: postgres
features: postgres-storage
- name: postgres-webhook
features: postgres-storage,webhook-sink
- name: mongodb
features: mongodb-storage
- name: mongodb-webhook
features: mongodb-storage,webhook-sink
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Build image
run: docker build --build-arg PARSEON_FEATURES='${{ matrix.features }}' --tag parseon:ci .
- name: Generate SBOM when Docker SBOM support is installed
shell: bash
run: |
if docker sbom --help >/dev/null 2>&1; then
docker sbom parseon:ci > "sbom-${{ matrix.name }}.spdx.json"
else
echo 'docker sbom is unavailable on this runner; image SBOM is delegated to the registry scanner.'
fi
- name: Scan image when Docker Scout support is installed
shell: bash
run: |
if docker scout version >/dev/null 2>&1; then
docker scout cves --only-fixed parseon:ci
else
echo 'docker scout is unavailable on this runner; image scanning is delegated to the registry.'
fi

release-gates:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Validate production Compose
shell: bash
run: |
export PARSEON_IMAGE=registry.example/parseon@sha256:$(printf 'a%.0s' {1..64})
export STORAGE_URL='postgres://postgres:5432/parseon'
export POSTGRES_PASSWORD=test
export API_TOKEN=test-token
docker compose -f compose.production.yml config >/dev/null
if [[ ! "$PARSEON_IMAGE" =~ @sha256:[0-9a-fA-F]{64}$ ]]; then
echo 'PARSEON_IMAGE must be digest-pinned' >&2
exit 1
fi
- name: Validate Helm chart and immutable deployment policy
shell: bash
run: |
digest="sha256:$(printf 'a%.0s' {1..64})"
docker run --rm -v "$GITHUB_WORKSPACE:/work" -w /work alpine/helm:3.17@sha256:d899e6316789fec04ee95300a18e454b7942539cbb3d89bde3e0655d6ca2e895 lint deploy/helm/parseon --set image.digest="$digest"
docker run --rm -v "$GITHUB_WORKSPACE:/work" -w /work alpine/helm:3.17@sha256:d899e6316789fec04ee95300a18e454b7942539cbb3d89bde3e0655d6ca2e895 template parseon deploy/helm/parseon --set image.digest="$digest" >/dev/null
if docker run --rm -v "$GITHUB_WORKSPACE:/work" -w /work alpine/helm:3.17@sha256:d899e6316789fec04ee95300a18e454b7942539cbb3d89bde3e0655d6ca2e895 template parseon deploy/helm/parseon --set image.digest="$digest" --set replicaCount=2 >/dev/null 2>&1; then
echo 'replicaCount=2 was accepted' >&2
exit 1
fi
if docker run --rm -v "$GITHUB_WORKSPACE:/work" -w /work alpine/helm:3.17@sha256:d899e6316789fec04ee95300a18e454b7942539cbb3d89bde3e0655d6ca2e895 template parseon deploy/helm/parseon >/dev/null 2>&1; then
echo 'missing image digest was accepted' >&2
exit 1
fi
- name: Validate scripts and monitoring artifacts
shell: bash
run: |
bash -n scripts/backup_postgres.sh scripts/restore_postgres.sh
python3 -m py_compile scripts/gen_erpc.py
python3 -m json.tool deploy/monitoring/parseon-dashboard.json >/dev/null
secrets:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- name: Reject credential-bearing generated config
shell: bash
run: |
if git grep -n -I -i -E '://[^[:space:]/:@]+:[^[:space:]@]+@|(^|[?&])(api[_-]?key|access[_-]?token|token|secret|password)=[^[:space:]&]{8,}' -- ':!CHANGELOG.md' ':!docs/operations.md' ':!.env.example'; then
echo 'credential-bearing URL or query parameter found in tracked source' >&2
exit 1
fi
echo 'Historical credentials still require independent revoke/rotate evidence.'
1 change: 1 addition & 0 deletions .gitignore
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
/target
/.env
/.temp
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,8 @@ All notable changes to Parseon are documented in this file.

### Added

- Add canonical block metadata, provisional/finalized lifecycle, bounded reorg rollback, and finalized promotion for both storage adapters.
- Add production Compose and Helm deployment artifacts, backup/restore operations runbook, Prometheus alerts, Grafana dashboard, and CI release gates.
- Apply chain creation, enable/disable, and deletion to running workers without restarting Parseon; deletions stop the worker before removing its data.
- Rotate a chain's RPC endpoint URL in place on the running worker via Alloy's `Http::set_url`, resetting endpoint capability probes while keeping the cached chain ID; sources that cannot rotate restart their worker instead.
- Allow multiple monitors to target the same chain, contract, and function selector or event topic while retaining independent ranges, filters, cursors, and result storage.
Expand All@@ -15,6 +17,8 @@ All notable changes to Parseon are documented in this file.

### Changed

- Require a bearer API token for protected HTTP routes, disable CORS by default, bound request bodies, add liveness/readiness probes, and reject unsafe RPC destinations by default.
- Treat credential-bearing eRPC URLs as operator-injected configuration instead of generated repository content.
- Pin the Compose eRPC image to the `0.1.1` release tag, set a 3 GiB container memory limit with `GOMEMLIMIT=2700MiB`, add `restart: unless-stopped`, and invoke `/erpc-server` explicitly so the distroless image starts correctly.
- Reuse matching RPC data and compatible ABI decoders across overlapping monitor targets before applying each monitor's filter and persisting its own results.
- Isolate ABI decode failures to the affected monitor layout so an incompatible definition cannot stall other monitors on the chain.
Expand All@@ -32,6 +36,8 @@ All notable changes to Parseon are documented in this file.
### Fixed

- Keep persisted chain state and live workers ordered during concurrent mutations and startup reconciliation, and reject new worker starts once supervisor shutdown begins.
- Reject mixed-branch result identities, fail closed when a retained reorg ancestor is unavailable, preserve finalized state for monitors added over existing blocks, and reset rollback cursors below a monitor's start block.
- Pin validated RPC DNS addresses in the HTTP transport, verify generated endpoint chain IDs, expose worker freshness/state metrics, and require encrypted checksum-verified PostgreSQL backup artifacts.

### Breaking

Expand Down
15 changes: 8 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,7 @@ members = [
resolver = "3"

[workspace.package]
version = "0.8.0"
version = "1.0.0"
edition = "2024"
license = "MIT OR Apache-2.0"

Expand Down
10 changes: 5 additions & 5 deletions Dockerfile
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1

# ---- chef base: shared Rust toolchain + cargo-chef ----
FROM rust:1.96-alpine AS chef
FROM rust:1.96-alpine@sha256:a41f7740f8b45d45795624eec13a8b42263cc700f19f7e4e86e04d3dda08a479 AS chef
RUN apk add --no-cache musl-dev
RUN cargo install cargo-chef --locked
WORKDIR /app
Expand All@@ -18,7 +18,7 @@ ARG PARSEON_FEATURES=postgres-storage
ENV PARSEON_FEATURES=${PARSEON_FEATURES}
RUN apk add --no-cache watchexec
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --recipe-path recipe.json --no-default-features --features "${PARSEON_FEATURES}"
RUN cargo chef cook --locked --recipe-path recipe.json --no-default-features --features "${PARSEON_FEATURES}"
COPY . .
EXPOSE 8080
CMD ["sh", "-c", "exec watchexec --restart --stop-signal SIGINT --exts rs,toml,lock,sql -- cargo run --no-default-features --features \"${PARSEON_FEATURES}\""]
Expand All@@ -29,15 +29,15 @@ ARG PARSEON_FEATURES=postgres-storage
COPY --from=planner /app/recipe.json recipe.json
RUN --mount=type=cache,target=/root/.cargo/registry \
--mount=type=cache,target=/app/target \
cargo chef cook --release --recipe-path recipe.json --no-default-features --features "${PARSEON_FEATURES}"
cargo chef cook --locked --release --recipe-path recipe.json --no-default-features --features "${PARSEON_FEATURES}"
COPY . .
RUN --mount=type=cache,target=/root/.cargo/registry \
--mount=type=cache,target=/app/target \
cargo build --release --no-default-features --features "${PARSEON_FEATURES}" && \
cargo build --release --locked --no-default-features --features "${PARSEON_FEATURES}" && \
cp /app/target/release/parseon /usr/local/bin/parseon

# ---- runtime: minimal alpine, non-root, healthcheck ----
FROM alpine:3.20 AS runtime
FROM alpine:3.20@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc AS runtime
RUN apk add --no-cache ca-certificates wget && \
adduser -D -u 1000 parseon
COPY --from=builder /usr/local/bin/parseon /usr/local/bin/parseon
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,10 +40,14 @@ parseon-server
└── parseon-webhook-sink ────> parseon-core
```

Start PostgreSQL with `docker compose up -d`, or start the MongoDB development replica set and eRPC gateway with `docker compose --profile mongodb --profile erpc up -d`. Configure the selected backend through `STORAGE_URL`, then register direct RPC or complete eRPC URLs through `POST /chains`.
Start PostgreSQL with `docker compose up -d`, or start the MongoDB development replica set and eRPC gateway with `docker compose --profile mongodb --profile erpc up -d`. Configure the selected backend through `STORAGE_URL` and set a non-empty `API_TOKEN`; protected API routes require `Authorization: Bearer <API_TOKEN>`. Register direct RPC or complete eRPC URLs through `POST /chains`. Private/loopback RPC destinations are rejected unless `ALLOW_PRIVATE_RPC_NETWORKS=true` is explicitly enabled for local development.

See [adapter configuration and guarantees](./docs/adapters.md) for feature builds, MongoDB requirements, eRPC smoke checks, the webhook JSON contract, and Compose profiles.

## Production deployment

Use [`compose.production.yml`](./compose.production.yml) or the [`deploy/helm/parseon`](./deploy/helm/parseon) chart. Both require an externally managed `API_TOKEN` and storage URL, keep databases private, and expose liveness/readiness probes. Read the [production operations runbook](./docs/operations.md) before upgrading, restoring, or exposing the API through an ingress.

## License

Licensed under either the [Apache License, Version 2.0](./LICENSE-APACHE) or the [MIT license](./LICENSE-MIT), at your option.
Expand Down
70 changes: 70 additions & 0 deletions compose.production.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
services:
postgres:
image: ${POSTGRES_IMAGE:-postgres:16@sha256:f1c3376c26f2609ab9f29f71f824103fe2fcd8ee0346485cb6122a4f93df6f94}
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-parseon}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB:-parseon}
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 3s
retries: 12
# No host port: PostgreSQL is reachable only by Parseon on the backend network.

parseon:
image: ${PARSEON_IMAGE:?set PARSEON_IMAGE to an immutable Parseon image}
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
STORAGE_URL: ${STORAGE_URL:?set STORAGE_URL}
API_TOKEN: ${API_TOKEN:?set API_TOKEN}
HTTP_LISTEN: 0.0.0.0:8080
RUST_LOG: ${RUST_LOG:-info}
POLL_INTERVAL_MS: ${POLL_INTERVAL_MS:-2000}
DEFAULT_BATCH_SIZE: ${DEFAULT_BATCH_SIZE:-10}
BLOCK_CACHE_SIZE: ${BLOCK_CACHE_SIZE:-512}
BLOCK_CONCURRENCY: ${BLOCK_CONCURRENCY:-4}
RPC_REQUEST_CONCURRENCY: ${RPC_REQUEST_CONCURRENCY:-16}
STORAGE_WRITE_CONCURRENCY: ${STORAGE_WRITE_CONCURRENCY:-4}
CONFIRMATION_DEPTH: ${CONFIRMATION_DEPTH:-64}
ROLLBACK_RETENTION: ${ROLLBACK_RETENTION:-256}
RPC_BATCH_SIZE: ${RPC_BATCH_SIZE:-20}
ALLOW_PRIVATE_RPC_NETWORKS: "false"
CORS_ORIGINS: ${CORS_ORIGINS:-}
MAX_BODY_BYTES: ${MAX_BODY_BYTES:-1048576}
expose:
- "8080"
networks:
- backend
- egress
read_only: true
tmpfs:
- /tmp
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
mem_limit: ${PARSEON_MEMORY_LIMIT:-768m}
cpus: ${PARSEON_CPUS:-2}
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/healthz"]
interval: 10s
timeout: 5s
start_period: 20s
retries: 6

volumes:
pgdata:

networks:
backend:
internal: true
egress:
Loading