Documentation
¶
Overview ¶
internal/membridge/capacity.go — PRD 4 §3.4 capacity enforcement.
EnforceHardCap fails closed at k>500 per §3.4.1. GenesisAssign samples pod_salt up to R_max=8 attempts seeking a pairwise-distinct index assignment for the initial thought set (§3.4.2). The caller provides a sampler closure so callers can inject CSPRNG, deterministic test fixtures, or a brute-force search. AssignCapture handles the intra-lineage capture path (§3.4.3): a derived-index collision fails closed without relocation.
internal/membridge/constants.go
internal/membridge/derive.go — PRD 4 §3.2 (B.1) byte-exact derivation.
Chain:
logical_key = DTAG_KEY || ULEB128(32) || pod_salt
|| ULEB128(len(tid_bytes)) || tid_bytes
index = be_u32(SHA-256(IndexDomainSep || logical_key)[0:4]) & 0x0FFFFF
payload = DTAG_CELL || ULEB128(len(logical_key)) || logical_key
|| content_hash (32 bytes, no length prefix)
value_hash = SHA-256(payload)
leaf_hash = SHA-256(LEAF_TAG || value_hash)
internal/membridge/disclosure.go — PRD 4 §B.2 + §B.2.1 disclosure controls.
PolicyTypeC: bundle-disclosed salt; external re-derivation enabled. PolicyTypeD: salt committed via h_manifest but redacted from the
bundle. Audit trails are witness-confidential and MUST NOT travel on the public channel.
Package membridge implements PRD 4 Part B byte-exactly: the bridge from the Phase-0 set-semantic MemorySnapshot model to the PRD 1 § 5.5 depth-20 sparse address-indexed ZK memory tree.
Public surface (B.1 derivation, B.5 import, B.6 lineage):
- LogicalKey, CellIndex, CanonicalCellPayload, ValueHash, LeafHash
- SparseRoot, BuildAuthPath, VerifyAuthPath
- GenesisAssign, AssignCapture, EnforceHardCap
- ImportSnapshot, AuditTrailEntry
- Clone, Fork, Migrate, NewLineage
- DisclosurePolicy, PublicBundleView
Every rejection is fatal and returns a *MembridgeError whose Code is a stable PRD 4 § 6 identifier (e.g. ERR_CELL_SALT_LEN). Callers branch on the code via errors.Is(err, ErrCellSaltLen).
internal/membridge/errors.go — stable sentinels matching PRD 4 §6.
Every named code in PRD 4 §6 has a sentinel here. The Code() helper extracts the string for golden-vector comparison; errors.Is matches even when the sentinel is wrapped with fmt.Errorf("...: %w", e).
internal/membridge/import.go — PRD 4 §B.5 snapshot import + audit trail.
Procedure (ordered):
- Enumerate MemorySnapshotLeaf records by leaf_index ASC.
- Derive (index, value_hash) per B.1 for each leaf.
- Assemble the depth-20 sparse Merkle tree.
- Read off the root → r_init.
- Verify r_init == manifest.r_init; mismatch → ErrRInitMismatch.
- Any structural failure → ErrSnapshotImportFailed.
internal/membridge/leb128.go — minimal unsigned LEB128 (PRD 4 §3.2.3).
EncodeThoughtID emits the minimal encoding for any uint64. The runtime owns the integer value at derivation time so the output is always valid.
ParseThoughtIDBytes consumes externally-supplied bytes (snapshots, conformance vectors, on-wire messages, audit trails) and rejects non-minimal encodings per the WI-6 normative separation. The encoder path is tautological; the parser path is the real validation surface.
internal/membridge/lineage.go — PRD 4 §B.6 lineage operations.
Salt-handling matrix:
Clone → preserve salt (read-only copy) Fork → preserve salt for both branches Migrate → preserve salt (same lineage, new runtime) NewLineage → reseed via genesis assign (R_max=8)
Mid-lineage reseed is prohibited in all non-NewLineage operations.
internal/membridge/provisional.go — PRD 4 §B.6.1 (WI-5) normative.
provisional_id = be_u64(SHA-256(
DTAG_PROV || content_hash || be_u64(task_id) || be_u32(ordinal)
)[0:8])
Reconciliation is byte-exact equality against the eventual persistent thought_id; mismatch fails closed.
internal/membridge/sparse_merkle.go — PRD 1 §5.5 depth-20 sparse tree.
Construction: bottom-up, only populated subtrees are computed explicitly; empty siblings reuse EmptyRoots[k]. Authentication path is leaf→root ordered with LSB-first direction (bit j of index at level j).
Fail-closed validation (Hermes review on MR !7): callers MUST supply indices < 2^D. Out-of-range indices return ErrSparseIndexOutOfRange. Masking belongs only at the normative CellIndex derivation boundary (PRD 4 §3.2.5 specifies `& 0x0FFFFF` happens at index derivation, not inside the tree). Two callers differing only in bits >= 20 would otherwise collide silently.
Index ¶
- Constants
- Variables
- func AssignCapture(occupied map[uint32]uint64, podSalt []byte, thoughtID uint64) (uint32, error)
- func BuildAuthPath(cells map[uint32][32]byte, index uint32) ([D][32]byte, error)
- func CanonicalCellPayload(podSalt []byte, thoughtID uint64, contentHash []byte) ([]byte, error)
- func CellIndex(podSalt []byte, thoughtID uint64) (uint32, error)
- func Clone(src []byte) []byte
- func Code(err error) string
- func EncodeThoughtID(n uint64) []byte
- func EnforceHardCap(k int) error
- func Fork(src []byte) ([]byte, []byte)
- func GenesisAssign(sampler SaltSampler, thoughtIDs []uint64) ([]byte, int, error)
- func LeafHash(valueHash [32]byte) [32]byte
- func LogicalKey(podSalt []byte, thoughtID uint64) ([]byte, error)
- func Migrate(src []byte) []byte
- func NewLineage(sampler SaltSampler, initialThoughtIDs []uint64) ([]byte, int, error)
- func ParseThoughtIDBytes(b []byte) (uint64, error)
- func ProvisionalID(contentHash []byte, taskID uint64, ordinal uint32) (uint64, error)
- func ReconcileProvisional(provisional, persistent uint64) error
- func SparseRoot(cells map[uint32][32]byte) ([32]byte, error)
- func ValueHash(podSalt []byte, thoughtID uint64, contentHash []byte) ([32]byte, error)
- func VerifyAuthPath(index uint32, leafHash [32]byte, path [D][32]byte) [32]byte
- type AuditTrailEntry
- type DisclosurePolicy
- type MembridgeError
- type MemorySnapshotLeaf
- type PublicBundleView
- type SaltSampler
- type SerializedView
Constants ¶
const ( DTAG_KEY byte = 0x20 // logical-key domain separator DTAG_CELL byte = 0x21 // canonical-cell-payload domain separator DTAG_PROV byte = 0x22 // provisional-id domain separator (B.6.1) LEAF_TAG byte = 0x10 // PRD 1 §5.5 leaf hashing tag NODE_TAG byte = 0x11 // PRD 1 §5.5 internal-node hashing tag )
Domain-tag constants. DTAG_KEY/DTAG_CELL/DTAG_PROV are Part-B only (PRD 4 §3.2.1 + §3.7.1). LEAF_TAG/NODE_TAG are PRD 1 §5.5 tree tags. All five are byte-disjoint from PRD 1 §5.4 T_log tags (0x00, 0x01, 0x03).
const D = 20
D is the fixed sparse Merkle tree depth per PRD 1 §5.5.
const HardCapK = 500
HardCapK is the v1 per-pod cell cap (PRD 4 §3.4.1). Fail-closed.
const RMax = 8
RMax is the genesis salt-resample bound (PRD 4 §3.4.2).
Variables ¶
var ( ErrCellSaltLen = &MembridgeError{CodeStr: "ERR_CELL_SALT_LEN"} ErrLeb128NonMinimal = &MembridgeError{CodeStr: "ERR_LEB128_NONMINIMAL"} ErrCellCapExceeded = &MembridgeError{CodeStr: "ERR_CELL_CAP_EXCEEDED"} ErrGenesisReseedCollision = &MembridgeError{CodeStr: "ERR_GENESIS_RESEED_COLLISION"} ErrCellIndexCollision = &MembridgeError{CodeStr: "ERR_CELL_INDEX_COLLISION"} ErrRInitMismatch = &MembridgeError{CodeStr: "ERR_RINIT_MISMATCH"} ErrSnapshotImportFailed = &MembridgeError{CodeStr: "ERR_SNAPSHOT_IMPORT_FAILED"} ErrProvisionalIDMismatch = &MembridgeError{CodeStr: "ERR_PROVISIONAL_ID_MISMATCH"} ErrTypeDConfidentialLeak = &MembridgeError{CodeStr: "ERR_TYPE_D_CONFIDENTIAL_LEAK"} // ErrSparseIndexOutOfRange is raised when a caller passes a tree // index >= 2^D into the sparse Merkle API. Masking belongs only at // the normative CellIndex derivation boundary; the tree itself is // fail-closed on out-of-range caller input (Hermes review on MR !7). ErrSparseIndexOutOfRange = &MembridgeError{CodeStr: "ERR_SPARSE_INDEX_OUT_OF_RANGE"} )
Sentinel values. The string field equals the stable PRD 4 §6 code.
var EmptyRoots = func() [21][32]byte { var out [21][32]byte h := sha256.New() h.Write([]byte{LEAF_TAG}) h.Write(EmptyValueHash[:]) copy(out[0][:], h.Sum(nil)) for k := 1; k <= D; k++ { h.Reset() h.Write([]byte{NODE_TAG}) h.Write(out[k-1][:]) h.Write(out[k-1][:]) copy(out[k][:], h.Sum(nil)) } return out }()
EmptyRoots is the precomputed PRD 1 §5.5 empty-subtree root table. EmptyRoots[0] = SHA-256(LEAF_TAG || 0x00*32) EmptyRoots[k] = SHA-256(NODE_TAG || EmptyRoots[k-1] || EmptyRoots[k-1]) EmptyRoots[20] is the root of the all-empty depth-20 tree.
var EmptyValueHash = [32]byte{}
EmptyValueHash is the reserved sentinel for an unpopulated cell (PRD 1 §5.5). Distinct from SHA-256 of any well-formed payload.
var IndexDomainSep = []byte("konareef-mem-idx/v1")
IndexDomainSep is the literal 19 ASCII bytes prefixed to logical_key before SHA-256 to derive the cell index (PRD 4 §3.2.5). No null terminator, no length prefix — raw bytes only.
Functions ¶
func AssignCapture ¶
AssignCapture derives the index for thoughtID under the fixed lineage salt and rejects with ErrCellIndexCollision when the slot is occupied. On success, returns the index; the caller is responsible for inserting into `occupied`. occupied is not mutated on failure.
func BuildAuthPath ¶
BuildAuthPath returns the 20 sibling hashes (leaf→root order) needed to authenticate the leaf at index against the root of cells. The returned slice always has length D; siblings at empty subtrees use the corresponding EmptyRoots[k]. Out-of-range index or leaf-map key returns ErrSparseIndexOutOfRange.
func CanonicalCellPayload ¶
CanonicalCellPayload constructs the §3.2.6 payload. contentHash MUST be exactly 32 bytes.
func Clone ¶
Clone returns a defensive copy of src — preserves the pod_salt bytes without aliasing the underlying buffer.
func Code ¶
Code extracts the stable error string. Returns "" if err is nil or not (a wrap of) a *MembridgeError.
func EncodeThoughtID ¶
EncodeThoughtID emits the minimal unsigned LEB128 encoding of n.
func EnforceHardCap ¶
EnforceHardCap returns ErrCellCapExceeded when k > HardCapK.
func Fork ¶
Fork returns two independent copies of src — both branches preserve the original salt; each may evolve r_init independently.
func GenesisAssign ¶
func GenesisAssign(sampler SaltSampler, thoughtIDs []uint64) ([]byte, int, error)
GenesisAssign performs the §3.4.2 reseed loop. Returns the accepted salt, the number of attempts taken (1-indexed for the success attempt), or ErrGenesisReseedCollision after RMax exhaustion.
Per §3.4.1 the input set size is also bounded; callers MUST gate k ≤ HardCapK before invoking GenesisAssign.
func LogicalKey ¶
LogicalKey constructs the canonical logical-key byte string for (podSalt, thoughtID). podSalt MUST be exactly 32 bytes; otherwise ErrCellSaltLen is returned and no other field is computed.
func Migrate ¶
Migrate returns a defensive copy of src — same lineage, new runtime host; salt invariant.
func NewLineage ¶
func NewLineage(sampler SaltSampler, initialThoughtIDs []uint64) ([]byte, int, error)
NewLineage runs §3.4.2 genesis assign to produce a fresh salt for a deliberately-new lineage. Use the runtime CSPRNG-backed sampler in production; tests inject deterministic sequences.
func ParseThoughtIDBytes ¶
ParseThoughtIDBytes decodes b as standard minimal unsigned LEB128 per PRD 4 §3.2.3.
Minimality rule (post-Hermes-review correction): the parser performs a standard LSB-first ULEB128 decode and then re-encodes the decoded value via EncodeThoughtID. Input bytes are minimal iff they equal the canonical encoding. This is the textbook ULEB128 minimality check.
This rule correctly accepts encodings such as 0x80 0x80 0x01 (canonical minimal encoding of 16384, well within the v1 thought_id range of 2^21-1 = 2097151) and correctly rejects encodings such as 0x80 0x00 (non-minimal 0; canonical minimal encoding of 0 is 0x00).
The earlier interior-0x80 rejection rule was incorrect: it would fail-closed on legitimate 16384 thought_ids. See paygate-zk issue #1 for the upstream conformance-vector text mismatch.
Rejects with ErrLeb128NonMinimal when:
- b is empty,
- b is truncated (final byte still has the continuation bit set),
- trailing bytes follow the terminator,
- the decoded value's canonical encoding differs from the input,
- decode would consume more than 10 bytes (64-bit overflow guard).
func ProvisionalID ¶
ProvisionalID computes the deterministic capture-time provisional thought_id per §B.6.1. contentHash MUST be exactly 32 bytes.
func ReconcileProvisional ¶
ReconcileProvisional asserts the persistent thought_id matches the provisional id byte-for-byte. Mismatch is the §B.6.1 fail-closed gate.
func SparseRoot ¶
SparseRoot computes the depth-20 sparse Merkle root for the set of (index, value_hash) pairs in cells. nil or empty cells returns EmptyRoots[20]. Any index >= 2^D is rejected fail-closed with ErrSparseIndexOutOfRange — callers MUST mask at the CellIndex derivation boundary, not rely on the tree to mask.
func VerifyAuthPath ¶
VerifyAuthPath recomputes the root by walking leafHash up to the root using LSB-first direction bits from index. Indices >= 2^D produce undefined output; callers MUST validate range upstream (the normative CellIndex boundary).
Types ¶
type AuditTrailEntry ¶
type AuditTrailEntry struct {
ThoughtID uint64
LogicalKey []byte
Index uint32
ValueHash [32]byte
ContentHash []byte
}
AuditTrailEntry is one row of the §B.5 ordered audit record. Salt is the runtime's responsibility to keep alongside (Type-C: bundled; Type-D: out-of-band confidential).
func ImportSnapshot ¶
func ImportSnapshot(podSalt []byte, leaves []MemorySnapshotLeaf, manifestRInit []byte) ([32]byte, []AuditTrailEntry, error)
ImportSnapshot performs §B.5 steps 1-5.
Returns the computed r_init and the audit trail in leaf_index ASC order. On r_init mismatch the trail is still returned so callers can diagnose; the error fully unwraps to ErrRInitMismatch.
type DisclosurePolicy ¶
type DisclosurePolicy uint8
DisclosurePolicy is the bundle-side confidentiality posture.
const ( PolicyUnspecified DisclosurePolicy = 0 PolicyTypeC DisclosurePolicy = 1 PolicyTypeD DisclosurePolicy = 2 )
type MembridgeError ¶
type MembridgeError struct {
CodeStr string
}
MembridgeError is the concrete sentinel type. Each exported Err* variable is a *MembridgeError, so wrapped errors compare equal to the sentinel via errors.Is (the default *T pointer-equality unwrap path).
func (*MembridgeError) Code ¶
func (e *MembridgeError) Code() string
Code returns the stable PRD 4 §6 identifier string for the sentinel. Equivalent to e.CodeStr; provided so callers can read .Code on *MembridgeError values surfaced through errors.As without naming the field directly.
func (*MembridgeError) Error ¶
func (e *MembridgeError) Error() string
type MemorySnapshotLeaf ¶
type MemorySnapshotLeaf struct {
LeafIndex uint64 // PRD 4 §B.5 step-1 sort key
ThoughtID uint64
ContentHash []byte // 32 bytes
}
MemorySnapshotLeaf is one row from the Phase-0 MemorySnapshot.
type PublicBundleView ¶
type PublicBundleView struct {
Policy DisclosurePolicy
PodSalt []byte
RInit [32]byte
AuditTrail []AuditTrailEntry // §B.5 — Type-D MUST be empty here.
}
PublicBundleView is the raw input to the public-bundle serializer. Serialize emits a confidentiality-aware SerializedView; passing a Type-D bundle with any confidential field populated (audit trail, per-cell logical_key, per-cell index) returns ErrTypeDConfidentialLeak.
func (PublicBundleView) Serialize ¶
func (v PublicBundleView) Serialize() (SerializedView, error)
Serialize applies PRD 4 §B.2.1 redaction rules.
type SaltSampler ¶
SaltSampler is the runtime hook that produces a candidate pod_salt. attempt is zero-based (first call: 0). Returning a salt of length != 32 causes GenesisAssign to surface ErrCellSaltLen.
type SerializedView ¶
type SerializedView struct {
PolicyStr string `json:"policy"`
PodSaltHex string `json:"pod_salt,omitempty"`
RInitHex string `json:"r_init"`
}
SerializedView is the post-redaction view safe to publish.
func (SerializedView) MarshalJSON ¶
func (s SerializedView) MarshalJSON() ([]byte, error)
MarshalJSON emits the public bundle JSON. Wrapper exists so tests can scan for leaked bytes in the canonical serialized form.