Skip to content

Commit 43c0928

Browse files
prestwichclaude
andauthored
feat: add SQL-based cold storage backend (#18)
* feat: add SQL-based cold storage backend (PostgreSQL + SQLite) Add signet-cold-sql crate implementing ColdStorage trait with fully decomposed SQL columns for rich queryability. Includes both PostgreSQL (production) and SQLite (testing/lightweight) backends, Docker-based test infrastructure, and conformance test coverage for both. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: unify SQL backends with sqlx::Any Replace separate PostgreSQL and SQLite implementations (~1670 lines of near-duplicate code) with a single SqlColdBackend using sqlx::Any. The database type is auto-detected from the connection URL. - Add sqlx/any feature to both backend feature flags - Unify boolean columns to INTEGER in both migrations for Any driver compat - Add SqlColdBackend::connect(url) and SqlColdBackend::new(AnyPool) - Keep SqliteColdBackend/PostgresColdBackend as type aliases Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: bump workspace version to 0.2.0 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 915c335 commit 43c0928

File tree

12 files changed

+2195
-7
lines changed

12 files changed

+2195
-7
lines changed

CLAUDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ Common pattern across crates:
4444
- ALWAYS run clippy with both `--all-features` and `--no-default-features`.
4545
- ALWAYS run `cargo +nightly fmt`.
4646
- Run tests per-crate (`-p <crate>`) before running repo-wide.
47+
- For `signet-cold-sql`: ALWAYS run `./scripts/test-postgres.sh` to verify
48+
the PostgreSQL backend against the conformance suite.
4749

4850
## Research
4951

Cargo.toml

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ members = ["crates/*"]
33
resolver = "2"
44

55
[workspace.package]
6-
version = "0.1.0"
6+
version = "0.2.0"
77
edition = "2024"
88
rust-version = "1.92"
99
authors = ["init4"]
@@ -35,12 +35,13 @@ incremental = false
3535

3636
[workspace.dependencies]
3737
# internal
38-
signet-hot = { version = "0.1.0", path = "./crates/hot" }
39-
signet-hot-mdbx = { version = "0.1.0", path = "./crates/hot-mdbx" }
40-
signet-cold = { version = "0.1.0", path = "./crates/cold" }
41-
signet-cold-mdbx = { version = "0.1.0", path = "./crates/cold-mdbx" }
42-
signet-storage = { version = "0.1.0", path = "./crates/storage" }
43-
signet-storage-types = { version = "0.1.0", path = "./crates/types" }
38+
signet-hot = { version = "0.2.0", path = "./crates/hot" }
39+
signet-hot-mdbx = { version = "0.2.0", path = "./crates/hot-mdbx" }
40+
signet-cold = { version = "0.2.0", path = "./crates/cold" }
41+
signet-cold-mdbx = { version = "0.2.0", path = "./crates/cold-mdbx" }
42+
signet-cold-sql = { version = "0.2.0", path = "./crates/cold-sql" }
43+
signet-storage = { version = "0.2.0", path = "./crates/storage" }
44+
signet-storage-types = { version = "0.2.0", path = "./crates/types" }
4445

4546
# External, in-house
4647
signet-libmdbx = { version = "0.8.0" }
@@ -69,4 +70,5 @@ tokio = { version = "1.45.0", features = ["full"] }
6970
tokio-util = { version = "0.7", features = ["rt"] }
7071
itertools = "0.14"
7172
lru = "0.16"
73+
sqlx = { version = "0.8", default-features = false }
7274
tracing = "0.1.44"

crates/cold-sql/Cargo.toml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
[package]
2+
name = "signet-cold-sql"
3+
description = "SQL backend for signet-cold storage"
4+
version.workspace = true
5+
edition.workspace = true
6+
rust-version.workspace = true
7+
authors.workspace = true
8+
license.workspace = true
9+
homepage.workspace = true
10+
repository.workspace = true
11+
keywords = ["sql", "storage", "cold-storage", "blockchain"]
12+
categories = ["database-implementations"]
13+
14+
[package.metadata.docs.rs]
15+
all-features = true
16+
rustdoc-args = ["--cfg", "docsrs"]
17+
18+
[dependencies]
19+
alloy = { workspace = true, optional = true }
20+
signet-cold.workspace = true
21+
signet-storage-types = { workspace = true, optional = true }
22+
signet-zenith = { workspace = true, optional = true }
23+
sqlx = { workspace = true }
24+
thiserror.workspace = true
25+
26+
[dev-dependencies]
27+
signet-cold = { workspace = true, features = ["test-utils"] }
28+
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
29+
30+
[features]
31+
default = []
32+
postgres = ["sqlx/postgres", "sqlx/any", "sqlx/runtime-tokio-rustls", "dep:alloy", "dep:signet-storage-types", "dep:signet-zenith"]
33+
sqlite = ["sqlx/sqlite", "sqlx/any", "sqlx/runtime-tokio-rustls", "dep:alloy", "dep:signet-storage-types", "dep:signet-zenith"]
34+
test-utils = ["signet-cold/test-utils", "sqlite", "postgres"]

crates/cold-sql/README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# signet-cold-sql
2+
3+
SQL backend for signet-cold storage.
4+
5+
## Testing
6+
7+
### SQLite (no external dependencies)
8+
9+
```sh
10+
cargo t -p signet-cold-sql --features test-utils
11+
```
12+
13+
### PostgreSQL (requires Docker)
14+
15+
A script is provided to start a Postgres container and run the full test
16+
suite (SQLite + PostgreSQL):
17+
18+
```sh
19+
./scripts/test-postgres.sh
20+
```
21+
22+
This starts a Postgres 16 container via `docker compose`, runs the
23+
conformance tests with `DATABASE_URL` set, and tears down the container
24+
on exit.
25+
26+
**Manual steps** (if you prefer to manage Postgres yourself):
27+
28+
```sh
29+
# 1. Start Postgres (any method)
30+
docker compose up -d --wait postgres
31+
32+
# 2. Run tests with the connection string
33+
DATABASE_URL="postgres://signet:signet@localhost:5432/signet_test" \
34+
cargo t -p signet-cold-sql --features test-utils
35+
36+
# 3. Tear down
37+
docker compose down
38+
```
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
-- Cold storage schema with fully decomposed columns.
2+
--
3+
-- Compatible with both PostgreSQL and SQLite.
4+
-- BLOB is used instead of BYTEA for cross-dialect compatibility
5+
-- (sqlx maps both transparently to Vec<u8> / &[u8]).
6+
7+
CREATE TABLE IF NOT EXISTS headers (
8+
block_number INTEGER PRIMARY KEY NOT NULL,
9+
block_hash BLOB NOT NULL,
10+
parent_hash BLOB NOT NULL,
11+
ommers_hash BLOB NOT NULL,
12+
beneficiary BLOB NOT NULL,
13+
state_root BLOB NOT NULL,
14+
transactions_root BLOB NOT NULL,
15+
receipts_root BLOB NOT NULL,
16+
logs_bloom BLOB NOT NULL,
17+
difficulty BLOB NOT NULL,
18+
gas_limit INTEGER NOT NULL,
19+
gas_used INTEGER NOT NULL,
20+
timestamp INTEGER NOT NULL,
21+
extra_data BLOB NOT NULL,
22+
mix_hash BLOB NOT NULL,
23+
nonce BLOB NOT NULL,
24+
base_fee_per_gas INTEGER,
25+
withdrawals_root BLOB,
26+
blob_gas_used INTEGER,
27+
excess_blob_gas INTEGER,
28+
parent_beacon_block_root BLOB,
29+
requests_hash BLOB
30+
);
31+
32+
CREATE INDEX IF NOT EXISTS idx_headers_hash ON headers (block_hash);
33+
34+
CREATE TABLE IF NOT EXISTS transactions (
35+
block_number INTEGER NOT NULL,
36+
tx_index INTEGER NOT NULL,
37+
tx_hash BLOB NOT NULL,
38+
tx_type INTEGER NOT NULL,
39+
sig_y_parity INTEGER NOT NULL,
40+
sig_r BLOB NOT NULL,
41+
sig_s BLOB NOT NULL,
42+
chain_id INTEGER,
43+
nonce INTEGER NOT NULL,
44+
gas_limit INTEGER NOT NULL,
45+
to_address BLOB,
46+
value BLOB NOT NULL,
47+
input BLOB NOT NULL,
48+
gas_price BLOB,
49+
max_fee_per_gas BLOB,
50+
max_priority_fee_per_gas BLOB,
51+
max_fee_per_blob_gas BLOB,
52+
blob_versioned_hashes BLOB,
53+
access_list BLOB,
54+
authorization_list BLOB,
55+
PRIMARY KEY (block_number, tx_index)
56+
);
57+
58+
CREATE INDEX IF NOT EXISTS idx_tx_hash ON transactions (tx_hash);
59+
60+
CREATE TABLE IF NOT EXISTS receipts (
61+
block_number INTEGER NOT NULL,
62+
tx_index INTEGER NOT NULL,
63+
tx_type INTEGER NOT NULL,
64+
success INTEGER NOT NULL,
65+
cumulative_gas_used INTEGER NOT NULL,
66+
PRIMARY KEY (block_number, tx_index)
67+
);
68+
69+
CREATE TABLE IF NOT EXISTS logs (
70+
block_number INTEGER NOT NULL,
71+
tx_index INTEGER NOT NULL,
72+
log_index INTEGER NOT NULL,
73+
address BLOB NOT NULL,
74+
topic0 BLOB,
75+
topic1 BLOB,
76+
topic2 BLOB,
77+
topic3 BLOB,
78+
data BLOB NOT NULL,
79+
PRIMARY KEY (block_number, tx_index, log_index)
80+
);
81+
82+
CREATE INDEX IF NOT EXISTS idx_logs_address ON logs (address);
83+
CREATE INDEX IF NOT EXISTS idx_logs_topic0 ON logs (topic0);
84+
85+
CREATE TABLE IF NOT EXISTS signet_events (
86+
block_number INTEGER NOT NULL,
87+
event_index INTEGER NOT NULL,
88+
event_type INTEGER NOT NULL,
89+
order_index INTEGER NOT NULL,
90+
rollup_chain_id BLOB NOT NULL,
91+
sender BLOB,
92+
to_address BLOB,
93+
value BLOB,
94+
gas BLOB,
95+
max_fee_per_gas BLOB,
96+
data BLOB,
97+
rollup_recipient BLOB,
98+
amount BLOB,
99+
token BLOB,
100+
PRIMARY KEY (block_number, event_index)
101+
);
102+
103+
CREATE TABLE IF NOT EXISTS zenith_headers (
104+
block_number INTEGER PRIMARY KEY NOT NULL,
105+
host_block_number BLOB NOT NULL,
106+
rollup_chain_id BLOB NOT NULL,
107+
gas_limit BLOB NOT NULL,
108+
reward_address BLOB NOT NULL,
109+
block_data_hash BLOB NOT NULL
110+
);
111+
112+
CREATE TABLE IF NOT EXISTS metadata (
113+
key TEXT PRIMARY KEY NOT NULL,
114+
block_number INTEGER NOT NULL
115+
);
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
-- Cold storage schema with fully decomposed columns (PostgreSQL).
2+
--
3+
-- Uses BYTEA and BIGINT for correct PostgreSQL types.
4+
5+
CREATE TABLE IF NOT EXISTS headers (
6+
block_number BIGINT PRIMARY KEY NOT NULL,
7+
block_hash BYTEA NOT NULL,
8+
parent_hash BYTEA NOT NULL,
9+
ommers_hash BYTEA NOT NULL,
10+
beneficiary BYTEA NOT NULL,
11+
state_root BYTEA NOT NULL,
12+
transactions_root BYTEA NOT NULL,
13+
receipts_root BYTEA NOT NULL,
14+
logs_bloom BYTEA NOT NULL,
15+
difficulty BYTEA NOT NULL,
16+
gas_limit BIGINT NOT NULL,
17+
gas_used BIGINT NOT NULL,
18+
timestamp BIGINT NOT NULL,
19+
extra_data BYTEA NOT NULL,
20+
mix_hash BYTEA NOT NULL,
21+
nonce BYTEA NOT NULL,
22+
base_fee_per_gas BIGINT,
23+
withdrawals_root BYTEA,
24+
blob_gas_used BIGINT,
25+
excess_blob_gas BIGINT,
26+
parent_beacon_block_root BYTEA,
27+
requests_hash BYTEA
28+
);
29+
30+
CREATE INDEX IF NOT EXISTS idx_headers_hash ON headers (block_hash);
31+
32+
CREATE TABLE IF NOT EXISTS transactions (
33+
block_number BIGINT NOT NULL,
34+
tx_index BIGINT NOT NULL,
35+
tx_hash BYTEA NOT NULL,
36+
tx_type INTEGER NOT NULL,
37+
sig_y_parity INTEGER NOT NULL,
38+
sig_r BYTEA NOT NULL,
39+
sig_s BYTEA NOT NULL,
40+
chain_id BIGINT,
41+
nonce BIGINT NOT NULL,
42+
gas_limit BIGINT NOT NULL,
43+
to_address BYTEA,
44+
value BYTEA NOT NULL,
45+
input BYTEA NOT NULL,
46+
gas_price BYTEA,
47+
max_fee_per_gas BYTEA,
48+
max_priority_fee_per_gas BYTEA,
49+
max_fee_per_blob_gas BYTEA,
50+
blob_versioned_hashes BYTEA,
51+
access_list BYTEA,
52+
authorization_list BYTEA,
53+
PRIMARY KEY (block_number, tx_index)
54+
);
55+
56+
CREATE INDEX IF NOT EXISTS idx_tx_hash ON transactions (tx_hash);
57+
58+
CREATE TABLE IF NOT EXISTS receipts (
59+
block_number BIGINT NOT NULL,
60+
tx_index BIGINT NOT NULL,
61+
tx_type INTEGER NOT NULL,
62+
success INTEGER NOT NULL,
63+
cumulative_gas_used BIGINT NOT NULL,
64+
PRIMARY KEY (block_number, tx_index)
65+
);
66+
67+
CREATE TABLE IF NOT EXISTS logs (
68+
block_number BIGINT NOT NULL,
69+
tx_index BIGINT NOT NULL,
70+
log_index BIGINT NOT NULL,
71+
address BYTEA NOT NULL,
72+
topic0 BYTEA,
73+
topic1 BYTEA,
74+
topic2 BYTEA,
75+
topic3 BYTEA,
76+
data BYTEA NOT NULL,
77+
PRIMARY KEY (block_number, tx_index, log_index)
78+
);
79+
80+
CREATE INDEX IF NOT EXISTS idx_logs_address ON logs (address);
81+
CREATE INDEX IF NOT EXISTS idx_logs_topic0 ON logs (topic0);
82+
83+
CREATE TABLE IF NOT EXISTS signet_events (
84+
block_number BIGINT NOT NULL,
85+
event_index BIGINT NOT NULL,
86+
event_type INTEGER NOT NULL,
87+
order_index BIGINT NOT NULL,
88+
rollup_chain_id BYTEA NOT NULL,
89+
sender BYTEA,
90+
to_address BYTEA,
91+
value BYTEA,
92+
gas BYTEA,
93+
max_fee_per_gas BYTEA,
94+
data BYTEA,
95+
rollup_recipient BYTEA,
96+
amount BYTEA,
97+
token BYTEA,
98+
PRIMARY KEY (block_number, event_index)
99+
);
100+
101+
CREATE TABLE IF NOT EXISTS zenith_headers (
102+
block_number BIGINT PRIMARY KEY NOT NULL,
103+
host_block_number BYTEA NOT NULL,
104+
rollup_chain_id BYTEA NOT NULL,
105+
gas_limit BYTEA NOT NULL,
106+
reward_address BYTEA NOT NULL,
107+
block_data_hash BYTEA NOT NULL
108+
);
109+
110+
CREATE TABLE IF NOT EXISTS metadata (
111+
key TEXT PRIMARY KEY NOT NULL,
112+
block_number BIGINT NOT NULL
113+
);

0 commit comments

Comments
 (0)