Skip to content

Commit 76ffd9f

Browse files
committed
feat(storage): export portable ledger archives
1 parent 3102adf commit 76ffd9f

13 files changed

Lines changed: 1458 additions & 12 deletions

File tree

CMakeLists.txt

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,8 @@ target_include_directories(
163163
add_library(
164164
protocol_storage
165165
STATIC
166+
src/storage/archive_v1.cpp
167+
src/storage/sqlite_archive_v1.cpp
166168
src/storage/sqlite_connection.cpp
167169
src/storage/sqlite_block_store_v1.cpp
168170
src/storage/sqlite_history_replay_v1.cpp
@@ -245,6 +247,10 @@ add_executable(
245247
storage_snapshot_v1_tests
246248
tests/storage/snapshot_v1_test.cpp
247249
)
250+
add_executable(
251+
storage_archive_v1_tests
252+
tests/storage/archive_v1_test.cpp
253+
)
248254
target_link_libraries(
249255
storage_sqlite_ledger_tests
250256
PRIVATE
@@ -263,6 +269,12 @@ target_link_libraries(
263269
protocol_storage
264270
protocol_stack_sqlite
265271
)
272+
target_link_libraries(
273+
storage_archive_v1_tests
274+
PRIVATE
275+
protocol_storage
276+
protocol_stack_sqlite
277+
)
266278

267279
set(
268280
PROTOCOL_STACK_TARGETS
@@ -283,6 +295,7 @@ set(
283295
storage_sqlite_ledger_tests
284296
storage_sqlite_history_tests
285297
storage_snapshot_v1_tests
298+
storage_archive_v1_tests
286299
)
287300
if(PROTOCOL_STACK_ENABLE_FUZZING)
288301
add_library(
@@ -312,6 +325,7 @@ if(PROTOCOL_STACK_ENABLE_FUZZING)
312325
add_library(
313326
protocol_storage_fuzz
314327
STATIC
328+
src/storage/archive_v1.cpp
315329
src/storage/snapshot_v1.cpp
316330
)
317331
target_include_directories(
@@ -328,6 +342,15 @@ if(PROTOCOL_STACK_ENABLE_FUZZING)
328342
storage_snapshot_fuzz
329343
tests/fuzz/snapshot_fuzz.cpp
330344
)
345+
add_executable(
346+
storage_archive_fuzz
347+
tests/fuzz/archive_fuzz.cpp
348+
)
349+
target_link_libraries(
350+
storage_archive_fuzz
351+
PRIVATE
352+
protocol_storage_fuzz
353+
)
331354
target_link_libraries(
332355
storage_snapshot_fuzz
333356
PRIVATE
@@ -343,6 +366,7 @@ if(PROTOCOL_STACK_ENABLE_FUZZING)
343366
kernel_genesis_fuzz
344367
protocol_storage_fuzz
345368
storage_snapshot_fuzz
369+
storage_archive_fuzz
346370
)
347371
endif()
348372

@@ -379,6 +403,7 @@ if(PROTOCOL_STACK_ENABLE_FUZZING)
379403
kernel_genesis_fuzz
380404
protocol_storage_fuzz
381405
storage_snapshot_fuzz
406+
storage_archive_fuzz
382407
)
383408
target_compile_options(
384409
${protocol_stack_fuzz_target}
@@ -393,6 +418,7 @@ if(PROTOCOL_STACK_ENABLE_FUZZING)
393418
kernel_address_fuzz
394419
kernel_genesis_fuzz
395420
storage_snapshot_fuzz
421+
storage_archive_fuzz
396422
)
397423
target_link_options(
398424
${protocol_stack_fuzz_executable}
@@ -512,6 +538,13 @@ add_test(
512538
"${PROJECT_SOURCE_DIR}/test-vectors/ledger-transition-v1.txt"
513539
"${CMAKE_CURRENT_BINARY_DIR}/storage-snapshot-v1-test"
514540
)
541+
add_test(
542+
NAME storage-archive-v1
543+
COMMAND
544+
storage_archive_v1_tests
545+
"${PROJECT_SOURCE_DIR}/test-vectors/ledger-transition-v1.txt"
546+
"${CMAKE_CURRENT_BINARY_DIR}/storage-archive-v1-test"
547+
)
515548
set_tests_properties(
516549
protocol-primitives-python
517550
ledger-transition-python
@@ -561,11 +594,21 @@ if(PROTOCOL_STACK_ENABLE_FUZZING)
561594
-max_len=4096
562595
-len_control=0
563596
)
597+
add_test(
598+
NAME storage-archive-fuzz-smoke
599+
COMMAND
600+
storage_archive_fuzz
601+
-seed=824311
602+
-runs=512
603+
-max_len=16384
604+
-len_control=0
605+
)
564606
set_tests_properties(
565607
kernel-admission-fuzz-smoke
566608
kernel-address-fuzz-smoke
567609
kernel-genesis-fuzz-smoke
568610
storage-snapshot-fuzz-smoke
611+
storage-archive-fuzz-smoke
569612
PROPERTIES
570613
LABELS fuzz
571614
TIMEOUT 60

docs/architecture/sqlite-ledger.md

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,17 @@ before account allocation, checks the SHA-256 domain-separated digest, requires
3030
strict account ordering, validates caller-trusted immutable parameters, and
3131
restores through the kernel's state-invariant and root checks.
3232

33+
The public engine-independent archive codec implements ADR 0007's exact
34+
`PSAR` version-one bytes. The encoder accepts canonical genesis, contiguous
35+
canonical block outputs and admitted transaction records, plus a head
36+
snapshot. It independently loads genesis, replays every admitted block through
37+
the kernel, exact-compares transaction IDs, receipts, headers and block IDs,
38+
and requires the decoded snapshot to equal the replayed head before emitting
39+
bytes. The decoder bounds all attacker-controlled lengths and counts before
40+
allocation or advancement, never reserves from the declared block count,
41+
rejects non-exact framing, verifies the archive digest, and performs the same
42+
complete semantic replay.
43+
3344
`SQLiteLedger::create_snapshot` serializes with reads and block application,
3445
verifies that the durable metadata head equals the owned ledger, independently
3546
decodes the candidate bytes, and atomically replaces the one retained snapshot.
@@ -42,6 +53,14 @@ block ID, and requires the recovered state and root to equal authoritative full
4253
replay. A snapshot at the current head exercises the same path with an empty
4354
suffix.
4455

56+
`SQLiteLedger::export_archive` holds the adapter guard and one SQLite read
57+
transaction while it fully validates the durable ledger against
58+
caller-trusted genesis and the owned head. It creates a fresh snapshot of that
59+
verified head, then reads every block and admitted transaction in explicit
60+
height and ordinal order with exact width, count, and contiguity checks. The
61+
public codec replays that independently projected archive before export
62+
returns. Export does not change the retained database snapshot.
63+
4564
The public header exposes no SQLite handle or SQL type. `SQLiteLedger` is
4665
move-constructible but not copyable or assignable. It owns the live
4766
`protocol::v1::Ledger`, serialized connection, normalized path, exact canonical
@@ -132,10 +151,9 @@ before the completed adapter is returned.
132151

133152
## Remaining issue 11 work
134153

135-
The ordinary durable commit, full-genesis-replay path, canonical snapshot
136-
codec, atomic latest-snapshot persistence, and independent snapshot validation
137-
at its recorded replay height are implemented. Independent snapshot-plus-suffix
138-
recovery reaches the identical authoritative head. Portable export/import,
139-
automatic reopen after an ambiguous commit result, fault injection around
140-
every commit phase, long seeded restart sequences, and final issue closure
141-
remain.
154+
The ordinary durable commit, full-genesis-replay path, canonical snapshot and
155+
archive codecs, atomic latest-snapshot persistence, independent
156+
snapshot-plus-suffix recovery, and portable export are implemented. Portable
157+
import into a new database, automatic reopen after an ambiguous commit result,
158+
fault injection around every commit phase, long seeded restart sequences, and
159+
final issue closure remain.

docs/project/current-state.md

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ M1 — Sovereign Devnet Alpha. The deterministic in-memory ledger kernel is
88
merged and verified. The owning storage adapter can now durably create and
99
validate a height-zero ledger, atomically persist a complete block, and reopen
1010
the identical head through full genesis replay. Deterministic multi-block
11-
restart coverage is complete; snapshots, fault injection, and recovery remain
12-
the active roadmap slice.
11+
restart, snapshot recovery, and portable export are complete; archive import,
12+
fault injection, and ambiguous-commit recovery remain the active roadmap
13+
slice.
1314

1415
## Verified facts
1516

@@ -263,14 +264,40 @@ the active roadmap slice.
263264
current-head snapshots, including nonempty and empty suffixes. Focused GCC
264265
debug verification passes this path together with both existing SQLite test
265266
targets, 3/3.
267+
- PR #16 merged snapshot-plus-suffix recovery as `3102adf`; exact-candidate
268+
Actions run 30164373238 and post-merge `main` run 30164507913 both passed GCC
269+
and Clang debug plus ASan/UBSan. The first three jobs passed 17/17 tests and
270+
Clang ASan/UBSan passed 21/21 including all four fuzz targets.
271+
- The public engine-independent archive codec implements ADR 0007's exact
272+
`PSAR` version-one framing. It bounds hostile lengths and counts before
273+
allocation or advancement, verifies the domain-separated digest, loads
274+
canonical genesis, replays every admitted block, exact-compares transaction
275+
IDs, receipts, headers and block IDs, and requires the head snapshot to
276+
equal the replayed state and root.
277+
- `SQLiteLedger::export_archive` serializes with block application and holds
278+
one read transaction while it fully validates the durable ledger against
279+
caller-trusted genesis and the owned head, creates a fresh head snapshot,
280+
reads history in explicit height and ordinal order with exact projection
281+
checks, and semantically validates the projected archive before returning
282+
bytes. Export does not alter the retained database snapshot.
283+
- Focused GCC debug verification passes the archive codec/export test together
284+
with the existing SQLite ledger, history, and snapshot targets, 4/4. The
285+
archive suite freezes a 3,745-byte one-block fixture and digest; covers
286+
deterministic zero-block, empty-block, and populated-block round trips,
287+
truncation, trailing bytes, hostile counts and lengths, wrong
288+
magic/version/digest, history and snapshot corruption; and proves
289+
deterministic multi-block export across repeated calls and reopen. The Clang
290+
sanitizer configuration exposes a fifth bounded libFuzzer target with raw,
291+
structured, and valid archive seeds.
266292

267293
## Exact next action
268294

269295
Continue issue #11:
270296

271-
> Implement the canonical version-one portable archive codec and serialized
272-
> export from one verified database head, with overflow-safe framing, digest
273-
> verification, and exact history-plus-snapshot projections before import.
297+
> Implement portable archive import only into a new database: validate the
298+
> complete archive before touching the target path, persist its exact history,
299+
> materialized head, and snapshot atomically, then reopen through full replay
300+
> and prove byte-identical export.
274301
275302
## Open autonomous decisions
276303

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#pragma once
2+
3+
#include "protocol/v1/ledger.hpp"
4+
5+
#include <cstdint>
6+
#include <span>
7+
#include <variant>
8+
#include <vector>
9+
10+
namespace protocol::storage {
11+
12+
enum class ArchiveV1Error : std::uint8_t {
13+
malformed = 1,
14+
unsupported_version = 2,
15+
size_overflow = 3,
16+
digest_mismatch = 4,
17+
invalid_genesis = 5,
18+
invalid_history = 6,
19+
snapshot_mismatch = 7,
20+
};
21+
22+
struct ArchiveTransactionV1 {
23+
protocol::v1::Bytes transaction;
24+
protocol::v1::TransactionId transaction_id;
25+
protocol::v1::Bytes receipt;
26+
27+
bool operator==(const ArchiveTransactionV1&) const = default;
28+
};
29+
30+
struct ArchiveBlockV1 {
31+
protocol::v1::Bytes header;
32+
protocol::v1::BlockId block_id;
33+
std::vector<ArchiveTransactionV1> transactions;
34+
35+
bool operator==(const ArchiveBlockV1&) const = default;
36+
};
37+
38+
struct ArchiveV1Data {
39+
protocol::v1::Bytes canonical_genesis;
40+
std::vector<ArchiveBlockV1> blocks;
41+
protocol::v1::Bytes head_snapshot;
42+
43+
bool operator==(const ArchiveV1Data&) const = default;
44+
};
45+
46+
struct EncodedArchiveV1 {
47+
protocol::v1::Bytes payload;
48+
protocol::v1::Hash digest;
49+
50+
bool operator==(const EncodedArchiveV1&) const = default;
51+
};
52+
53+
struct DecodedArchiveV1 {
54+
ArchiveV1Data data;
55+
protocol::v1::Ledger ledger;
56+
protocol::v1::StateRoot state_root;
57+
protocol::v1::Hash digest;
58+
};
59+
60+
using ArchiveV1EncodeResult =
61+
std::variant<EncodedArchiveV1, ArchiveV1Error>;
62+
using ArchiveV1DecodeResult =
63+
std::variant<DecodedArchiveV1, ArchiveV1Error>;
64+
65+
ArchiveV1EncodeResult encode_archive_v1(
66+
const ArchiveV1Data& archive);
67+
68+
ArchiveV1DecodeResult decode_archive_v1(
69+
std::span<const std::uint8_t> payload);
70+
71+
} // namespace protocol::storage

include/protocol/storage/sqlite_ledger.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ using SQLiteBlockResult = std::variant<
4040
using SQLiteSnapshotResult = std::variant<
4141
protocol::v1::Bytes,
4242
SQLiteLedgerError>;
43+
using SQLiteArchiveResult = std::variant<
44+
protocol::v1::Bytes,
45+
SQLiteLedgerError>;
4346

4447
class SQLiteLedger {
4548
public:
@@ -55,6 +58,7 @@ class SQLiteLedger {
5558
std::uint64_t height,
5659
std::span<const protocol::v1::Bytes> raw_transactions);
5760
SQLiteSnapshotResult create_snapshot();
61+
SQLiteArchiveResult export_archive();
5862

5963
private:
6064
struct Impl;

0 commit comments

Comments
 (0)