FEE wrap material on blob_locations (FIL-480) - #15
Open
Peeja wants to merge 4 commits into
Open
Conversation
There was a problem hiding this comment.
Pull request overview
Extends the local ingot.blob_locations record shape to include the per-blob FEE (FilOne encryption envelope) wrap material needed for range reads to decrypt encrypted blobs without an extra envelope-header fetch, along with SQL migration and store/test updates.
Changes:
- Adds a new SQL migration to extend
ingot.blob_locationswith six nullable FEE wrap-material columns (plus Down migration). - Extends
registry.BlobLocationand updates the Postgres/inmemLocationStoreimplementations to read/write these new fields (including deep-copying byte slices in-memory). - Adds/extends live-Postgres + migration + inmem tests to validate NULL/bytea round-trips, aliasing safety, and re-wrap updates.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| registry/stores.go | Expands BlobLocation to carry FEE wrap material and updates doc comments/contracts. |
| registry/stores_postgres.go | Updates PutLocation/GetLocation SQL to write/read new nullable columns and adds nullInt64. |
| registry/postgres_live_test.go | Adds live-Postgres tests for NULL semantics, bytea round-trips, and re-wrap-in-place behavior. |
| migrations/up_live_test.go | Verifies new columns exist and are nullable after migration. |
| migrations/sql/00004_blob_encryption.sql | Adds the actual schema migration for FEE wrap material columns on blob_locations. |
| inmem/stores.go | Ensures in-memory store deep-copies new byte-slice fields to avoid aliasing. |
| inmem/stores_test.go | Adds inmem tests for wrap-material round-trip, aliasing safety, and re-wrap-in-place behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
frrist
force-pushed
the
feat/arch-migration-phase1
branch
from
July 1, 2026 23:21
be552e3 to
a59b01d
Compare
Extend ingot.blob_locations with the FEE encryption columns a (range) GET needs to decrypt an encrypted blob without fetching the COSE envelope header: region_wrapped_cek, region_key_version, tenant_recipient_kid, base_nonce, chunk_size, and protected_header. All nullable — an unencrypted blob (the appliance topology today) leaves them NULL. Rather than a new object/segment table, these live on blob_locations: it is already keyed by (space, digest), and a fresh CEK per encryption event makes each ciphertext digest unique to one encryption, so the wrap material is a 1:1 fact about the row — like the existing provider/url/size. Object-level values (plaintext size/ETag/identity) stay in the content-addressed ObjectManifest; overwrite already swaps the MST leaf to a new manifest CID atomically, so no plaintext_* columns and no generation counter are needed. Raw CEK bytes are never stored — only the region-KEK-wrapped CEK plus opaque key-version/recipient identifiers, so no schema change is needed if the region-key or Hilt wrap-key cardinality decisions (FIL-572/FIL-574) later go multi-key. Per-blob crypto-shred is nulling these columns or deleting the row; a rotation re-wrap is a PutLocation with a new wrapped CEK + key version. - migration 00004: additive ALTER TABLE ADD COLUMN (+ Down) - registry.BlobLocation gains the six fields; Put/GetLocation carry them (nullString/nullInt64 for optional text/bigint, bytea via nil) - inmem MemStore deep-copies the new byte-slice fields - tests: inmem FEE round-trip + mutation-safety; live-PG bytea/NULL round-trip and re-wrap-in-place; migration column existence + nullability Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XSxwh7XfPtFQtyeoDR65hC
Peeja
force-pushed
the
claude/fil-480-cipgkz
branch
from
July 2, 2026 17:54
bc83565 to
e054d52
Compare
…IL-480) Addresses a PR review note: BlobLocation documents the FEE wrap fields as all-or-nothing, but PutLocation accepted a partial set (e.g. a wrapped CEK with no nonce, or an empty-but-non-nil byte slice stored as a non-NULL empty bytea), silently persisting a row the decrypt path cannot use. - BlobLocation.ValidateFEE enforces the invariant: either all six wrap fields are present (non-empty, chunk_size > 0) or none; a partial set returns ErrPartialFEE. Both PutLocation implementations (Postgres + inmem) call it. - nullBytes maps empty/nil byte slices to SQL NULL so an absent FEE column never lands as a non-NULL empty bytea. - tests: partial sets rejected (and leave no row) in the inmem and live-PG suites; the fully-absent (unencrypted) row is still accepted. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XSxwh7XfPtFQtyeoDR65hC
go-check flagged inmem/stores_test.go as not gofmt-ed: the TestLocations_ PartialFEE_Rejected map keys were aligned against a multi-line entry. Run gofmt -w. No behavior change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XSxwh7XfPtFQtyeoDR65hC
Peeja
marked this pull request as ready for review
July 2, 2026 19:58
pyropy
reviewed
Jul 9, 2026
| // ChunkSize is the FEE chunk size written into the COSE protected header, | ||
| // cached so the read path need not fetch and decode the envelope header. | ||
| // Zero for an unencrypted blob. | ||
| ChunkSize int64 |
There was a problem hiding this comment.
Should we use bigint here since we're using it to store the chunk size in the database? Alternativly we could use uint64 here as per the FEE FIP.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
tl;dr: This adds everything Ingot needs to do the read-path decryption. It stores a wrapped CEK, as well as other info that's in the FEE header that's needed for decryption. That means that Ingot can perform a range read by grabbing just the bytes it needs to decrypt (rounded up to encryption chunk boundaries), and not also fetch the header, which is likely not contiguous.
It's all stored on
ingot.blob_locationsbecause that feels like the right entity shape-wise, but the name may no longer be appropriate. @frrist, should we broaden the name of this table?Summary
Implements FIL-480. Extends the existing
ingot.blob_locationstable with the per-blob FEE (FilOne encryption envelope) wrap material that the read path needs to decrypt an encrypted blob — so a (range) GET can unwrap the CEK and go straight to a body-range fetch, with no COSE envelope-header round-trip.Why extend
blob_locationsrather than add a tableAn earlier draft of the issue proposed a new
object_encryption/ per-segment table withplaintext_size/plaintext_etag/ agenerationcounter. On review those turned out redundant, and the issue was rewritten to an extension:ObjectManifestcarriesETagandBody.Size/MD5/SHA256— properties of the plaintext, computed pre-encryption in the PUT pipeline (FIL-481). No new columns.(space, digest)row. A fresh CEK per encryption event makes every ciphertext digest unique to one encryption (never shared across objects, even for identical plaintext) — exactly like the existingprovider/url/sizecolumns. No region column either: one Ingot instance is one region, soregion_key_versionalone identifies the KEK version.What changed
migrations/sql/00004_blob_encryption.sql— additiveALTER TABLE ingot.blob_locations ADD COLUMNfor six nullable columns (region_wrapped_cek,region_key_version,tenant_recipient_kid,base_nonce,chunk_size,protected_header), plus aDown. An unencrypted blob (the appliance topology today) leaves themNULL.registry.BlobLocationgains the six fields (documented);PutLocation/GetLocationcarry them (nullString/nullInt64for optional text/bigint,byteavia nil). The upsert overwrites full row state, so a rotation re-wrap is a read-modify-write.inmem.MemStoredeep-copies the new byte-slice fields so stored/returned copies don't alias.bytea/NULLround-trip, re-wrap, and the unencrypted (all-NULL) case; migration column existence + nullability.Security / acceptance
region_key_version/tenant_recipient_kidare opaque, so no schema change is needed if the region-key or Hilt wrap-key cardinality decisions (FIL-572 / FIL-574) later go multi-key.DeleteLocation); no separate mechanism. Worth confirming against FIL-489's reference-counted delete.Testing
GOWORK=off go build ./...✅ ·go vet ./...✅go test ./...passes for all packages except the pre-existing, environment-only failure in./testing/(unable to add custom RootCAs HTTPClient …— reproduced identically onmainwith none of these changes; it's the sandbox's HTTPS-proxy CA setup, not this diff).INGOT_TEST_DSNand were not run here (no DB in this sandbox). Please run them in CI/local:Open question for reviewers (non-blocking)
blob_locations's doc comment described it narrowly as the(space, digest) → provider/URLmapping. Adding encryption material broadens its role to "the full per-blob record." I've updated the doc comments to reflect that but left the table name as-is. Should it be renamed (e.g.blob_records)? Flagging per the issue — happy to do the rename in a follow-up if we want it.