From 5a0a2f80c73364cc2f15bd2fc85e6ee86350eeb9 Mon Sep 17 00:00:00 2001 From: Carl Niklas Rydberg Date: Sun, 18 Jan 2026 10:56:52 +0100 Subject: [PATCH] Add federation registry, replay, and ingest --- CMakeLists.txt | 39 +++ docs/federation-implementation-notes.md | 61 +++- include/amduat/fed/ingest.h | 28 ++ include/amduat/fed/registry.h | 82 +++++ include/amduat/fed/replay.h | 62 ++++ src/near_core/fed/ingest.c | 72 ++++ src/near_core/fed/registry.c | 416 ++++++++++++++++++++++++ src/near_core/fed/replay.c | 295 +++++++++++++++++ tests/fed/test_fed_ingest.c | 81 +++++ tests/fed/test_fed_registry.c | 93 ++++++ tests/fed/test_fed_replay.c | 134 ++++++++ tier1/asl-domain-model-1.md | 3 + tier1/asl-federation-1.md | 3 + tier1/asl-system-1.md | 3 + 14 files changed, 1371 insertions(+), 1 deletion(-) create mode 100644 include/amduat/fed/ingest.h create mode 100644 include/amduat/fed/registry.h create mode 100644 include/amduat/fed/replay.h create mode 100644 src/near_core/fed/ingest.c create mode 100644 src/near_core/fed/registry.c create mode 100644 src/near_core/fed/replay.c create mode 100644 tests/fed/test_fed_ingest.c create mode 100644 tests/fed/test_fed_registry.c create mode 100644 tests/fed/test_fed_replay.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 3542acf..3edc306 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -122,6 +122,12 @@ set(AMDUAT_TGK_SRCS src/tgk_stack/prov/prov.c ) +set(AMDUAT_FED_SRCS + src/near_core/fed/registry.c + src/near_core/fed/replay.c + src/near_core/fed/ingest.c +) + set(AMDUAT_ASL_STORE_FS_SRCS src/adapters/asl_store_fs/asl_store_fs.c src/adapters/asl_store_fs/asl_store_fs_layout.c @@ -169,6 +175,9 @@ amduat_link(pel amduat_asl amduat_enc amduat_hash_asl1 amduat_util) amduat_add_lib(tgk SRCS ${AMDUAT_TGK_SRCS}) amduat_link(tgk amduat_asl amduat_enc amduat_hash_asl1 amduat_util) +amduat_add_lib(fed SRCS ${AMDUAT_FED_SRCS}) +amduat_link(fed amduat_asl) + amduat_add_lib(asl_store_fs SRCS ${AMDUAT_ASL_STORE_FS_SRCS}) amduat_link(asl_store_fs amduat_asl amduat_enc amduat_hash_asl1 amduat_util) target_compile_definitions(amduat_asl_store_fs_obj PRIVATE _POSIX_C_SOURCE=200809L) @@ -533,3 +542,33 @@ target_link_libraries(amduat_test_pel_queue PRIVATE amduat_pel ) add_test(NAME pel_queue COMMAND amduat_test_pel_queue) + +add_executable(amduat_test_fed_registry tests/fed/test_fed_registry.c) +target_include_directories(amduat_test_fed_registry + PRIVATE ${AMDUAT_INTERNAL_DIR} + PRIVATE ${AMDUAT_INCLUDE_DIR} +) +target_link_libraries(amduat_test_fed_registry + PRIVATE amduat_fed +) +add_test(NAME fed_registry COMMAND amduat_test_fed_registry) + +add_executable(amduat_test_fed_replay tests/fed/test_fed_replay.c) +target_include_directories(amduat_test_fed_replay + PRIVATE ${AMDUAT_INTERNAL_DIR} + PRIVATE ${AMDUAT_INCLUDE_DIR} +) +target_link_libraries(amduat_test_fed_replay + PRIVATE amduat_fed +) +add_test(NAME fed_replay COMMAND amduat_test_fed_replay) + +add_executable(amduat_test_fed_ingest tests/fed/test_fed_ingest.c) +target_include_directories(amduat_test_fed_ingest + PRIVATE ${AMDUAT_INTERNAL_DIR} + PRIVATE ${AMDUAT_INCLUDE_DIR} +) +target_link_libraries(amduat_test_fed_ingest + PRIVATE amduat_fed +) +add_test(NAME fed_ingest COMMAND amduat_test_fed_ingest) diff --git a/docs/federation-implementation-notes.md b/docs/federation-implementation-notes.md index 8e61f86..e4a2460 100644 --- a/docs/federation-implementation-notes.md +++ b/docs/federation-implementation-notes.md @@ -43,6 +43,21 @@ The following are explicitly out of scope for core: - Transport protocols (HTTP, IPC, gossip). - Peer discovery and operational orchestration. - Admin UX and deployment wiring. +- Admission workflows, auth, and retries/backoff. +- Cache policy knobs (fetch timing, eviction, prefetch). +- Operational concerns (metrics, admin endpoints). +- Policy evaluation and per-record filtering decisions. + +## Layering note + +Core provides deterministic federation semantics and view construction only. +Middle-layer components are responsible for transport, admission workflows, +policy evaluation (including per-record filtering), caching strategies, and +operational wiring. + +Definition: +- Middle layer: the daemon/service boundary around core logic that owns + network transport, admission workflows, and operational policy. ## Responsibilities @@ -64,6 +79,13 @@ The following are explicitly out of scope for core: ## Data model (suggested) ```c +typedef enum { + AMDUAT_FED_REC_ARTIFACT = 0, + AMDUAT_FED_REC_PER = 1, + AMDUAT_FED_REC_TGK_EDGE = 2, + AMDUAT_FED_REC_TOMBSTONE = 3 +} amduat_fed_record_type_t; + typedef struct { uint32_t domain_id; uint64_t snapshot_id; @@ -72,6 +94,8 @@ typedef struct { uint8_t admitted; // boolean uint8_t policy_ok; // boolean uint8_t reserved[6]; + amduat_hash_id_t policy_hash_id; + amduat_octets_t policy_hash; } amduat_fed_domain_state_t; typedef struct { @@ -82,19 +106,38 @@ typedef struct { uint32_t source_domain; } amduat_fed_record_meta_t; +typedef struct { + amduat_fed_record_type_t type; + union { + amduat_asl_artifact_key_t artifact_key; + amduat_asl_tgk_edge_key_t tgk_edge_key; + amduat_asl_per_key_t per_key; + amduat_asl_artifact_key_t tombstone_key; // key being removed + } id; +} amduat_fed_record_id_t; + typedef struct { amduat_fed_record_meta_t meta; - amduat_asl_artifact_key_t key; + amduat_fed_record_id_t id; amduat_asl_artifact_location_t loc; uint64_t logseq; uint64_t snapshot_id; uint64_t log_prefix; } amduat_fed_index_record_t; + +typedef struct { + amduat_fed_record_id_t id; + uint32_t reason_code; // policy-specific; 0 if unknown +} amduat_fed_policy_deny_t; ``` Notes: - Imported records MUST retain domain_id and cross-domain source metadata. - Tombstones must retain domain_id/visibility for domain-local shadowing. +- Each record MUST include a record type and canonical identity for deterministic + replay across artifacts, PERs, TGK edges, and tombstones. +- PER/TGK canonical identities are currently represented by ASL references + (artifact IDs); no separate edge/PER key types exist yet. ## Core API sketch @@ -111,6 +154,7 @@ bool amduat_fed_set_domain_state(amduat_fed_registry_t *, domain_id, snapshot_id, log_prefix); // Ingest published records for a domain (already transported). +// Each record MUST include its type and canonical identity in the id field. bool amduat_fed_ingest_records(amduat_fed_registry_t *, domain_id, const amduat_fed_index_record_t *records, size_t count); @@ -131,6 +175,9 @@ Notes: - Transport fetch is not part of resolve; it only consumes ingested records. - The daemon can choose to fetch missing bytes when resolve reports a remote location but local bytes are absent. +- If per-record filtering is enabled, it is applied during ingest or view build + and any denials are recorded in view metadata (e.g., a table of + amduat_fed_policy_deny_t). ## Replay and view construction @@ -153,6 +200,12 @@ Federation view storage MAY be: If remote bytes are fetched, they MUST be written to a cache store that is logically separate from the authoritative local store (policy-controlled). +## Policy gating + +- Admission gating is per-domain. +- Per-record filtering is optional and MUST be an explicit, deterministic + policy layer if enabled. + ## Error reporting Core resolve should distinguish: @@ -161,6 +214,12 @@ Core resolve should distinguish: - INTEGRITY_ERROR (hash mismatch on bytes) - POLICY_DENIED (domain admitted but record filtered by policy) +Notes: +- When per-record filtering is enabled, POLICY_DENIED SHOULD surface at ingest + or view-build time by excluding filtered records from the view and recording + the denial in view metadata. Resolve MAY return POLICY_DENIED only when such a + denial is recorded for the queried key; otherwise it MUST return NOT_FOUND. + ## Tests (minimal) 1) Replay ordering determinism across two domains with interleaved logseq. diff --git a/include/amduat/fed/ingest.h b/include/amduat/fed/ingest.h new file mode 100644 index 0000000..bbc60f8 --- /dev/null +++ b/include/amduat/fed/ingest.h @@ -0,0 +1,28 @@ +#ifndef AMDUAT_FED_INGEST_H +#define AMDUAT_FED_INGEST_H + +#include "amduat/fed/replay.h" + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + AMDUAT_FED_INGEST_OK = 0, + AMDUAT_FED_INGEST_ERR_INVALID = 1, + AMDUAT_FED_INGEST_ERR_CONFLICT = 2 +} amduat_fed_ingest_error_t; + +amduat_fed_ingest_error_t amduat_fed_ingest_validate( + const amduat_fed_record_t *records, + size_t count, + size_t *out_error_index, + size_t *out_conflict_index); + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* AMDUAT_FED_INGEST_H */ diff --git a/include/amduat/fed/registry.h b/include/amduat/fed/registry.h new file mode 100644 index 0000000..b102ec6 --- /dev/null +++ b/include/amduat/fed/registry.h @@ -0,0 +1,82 @@ +#ifndef AMDUAT_FED_REGISTRY_H +#define AMDUAT_FED_REGISTRY_H + +#include "amduat/asl/core.h" +#include "amduat/asl/store.h" + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + uint32_t domain_id; + uint64_t snapshot_id; + uint64_t log_prefix; + uint64_t last_logseq; + uint8_t admitted; + uint8_t policy_ok; + uint8_t reserved[6]; + amduat_hash_id_t policy_hash_id; + amduat_octets_t policy_hash; /* Empty when unknown. */ +} amduat_fed_domain_state_t; + +typedef struct { + amduat_fed_domain_state_t *states; + size_t len; + size_t cap; + bool owns_states; +} amduat_fed_registry_value_t; + +void amduat_fed_registry_value_init(amduat_fed_registry_value_t *value, + amduat_fed_domain_state_t *states, + size_t cap); + +bool amduat_fed_registry_value_insert(amduat_fed_registry_value_t *value, + amduat_fed_domain_state_t state); + +const amduat_fed_domain_state_t *amduat_fed_registry_value_lookup( + const amduat_fed_registry_value_t *value, + uint32_t domain_id); + +void amduat_fed_registry_value_free(amduat_fed_registry_value_t *value); + +bool amduat_fed_registry_encode(const amduat_fed_registry_value_t *value, + amduat_octets_t *out_bytes); + +bool amduat_fed_registry_decode(amduat_octets_t bytes, + amduat_fed_registry_value_t *out_value); + +typedef enum { + AMDUAT_FED_REGISTRY_OK = 0, + AMDUAT_FED_REGISTRY_ERR_CODEC = 1, + AMDUAT_FED_REGISTRY_ERR_STORE = 2 +} amduat_fed_registry_error_t; + +typedef struct { + amduat_asl_store_t *store; +} amduat_fed_registry_store_t; + +void amduat_fed_registry_store_init(amduat_fed_registry_store_t *reg, + amduat_asl_store_t *store); + +amduat_fed_registry_error_t amduat_fed_registry_store_put( + amduat_fed_registry_store_t *reg, + const amduat_fed_registry_value_t *value, + amduat_reference_t *out_ref, + amduat_asl_store_error_t *out_store_err); + +amduat_fed_registry_error_t amduat_fed_registry_store_get( + amduat_fed_registry_store_t *reg, + amduat_reference_t ref, + amduat_fed_registry_value_t *out_value, + amduat_asl_store_error_t *out_store_err); + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* AMDUAT_FED_REGISTRY_H */ diff --git a/include/amduat/fed/replay.h b/include/amduat/fed/replay.h new file mode 100644 index 0000000..19fd6c9 --- /dev/null +++ b/include/amduat/fed/replay.h @@ -0,0 +1,62 @@ +#ifndef AMDUAT_FED_REPLAY_H +#define AMDUAT_FED_REPLAY_H + +#include "amduat/asl/core.h" + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + AMDUAT_FED_REC_ARTIFACT = 0, + AMDUAT_FED_REC_PER = 1, + AMDUAT_FED_REC_TGK_EDGE = 2, + AMDUAT_FED_REC_TOMBSTONE = 3 +} amduat_fed_record_type_t; + +typedef struct { + uint32_t domain_id; + uint8_t visibility; + uint8_t has_source; + uint16_t reserved0; + uint32_t source_domain; +} amduat_fed_record_meta_t; + +typedef struct { + amduat_fed_record_type_t type; + amduat_reference_t ref; +} amduat_fed_record_id_t; + +typedef struct { + amduat_fed_record_meta_t meta; + amduat_fed_record_id_t id; + uint64_t logseq; + uint64_t snapshot_id; + uint64_t log_prefix; +} amduat_fed_record_t; + +typedef struct { + amduat_fed_record_t *records; + size_t len; +} amduat_fed_replay_view_t; + +bool amduat_fed_record_validate(const amduat_fed_record_t *record); + +bool amduat_fed_replay_domain(const amduat_fed_record_t *records, + size_t count, + uint32_t domain_id, + uint64_t snapshot_id, + uint64_t log_prefix, + amduat_fed_replay_view_t *out_view); + +void amduat_fed_replay_view_free(amduat_fed_replay_view_t *view); + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* AMDUAT_FED_REPLAY_H */ diff --git a/src/near_core/fed/ingest.c b/src/near_core/fed/ingest.c new file mode 100644 index 0000000..c94b9de --- /dev/null +++ b/src/near_core/fed/ingest.c @@ -0,0 +1,72 @@ +#include "amduat/fed/ingest.h" + +#include + +static bool amduat_fed_record_id_eq(const amduat_fed_record_id_t *a, + const amduat_fed_record_id_t *b) { + if (a->type != b->type) { + return false; + } + return amduat_reference_eq(a->ref, b->ref); +} + +static bool amduat_fed_record_equivalent(const amduat_fed_record_t *a, + const amduat_fed_record_t *b) { + return a->meta.domain_id == b->meta.domain_id && + a->meta.visibility == b->meta.visibility && + a->meta.has_source == b->meta.has_source && + a->meta.source_domain == b->meta.source_domain && + a->logseq == b->logseq && + a->snapshot_id == b->snapshot_id && + a->log_prefix == b->log_prefix; +} + +amduat_fed_ingest_error_t amduat_fed_ingest_validate( + const amduat_fed_record_t *records, + size_t count, + size_t *out_error_index, + size_t *out_conflict_index) { + size_t i; + size_t j; + + if (out_error_index != NULL) { + *out_error_index = (size_t)-1; + } + if (out_conflict_index != NULL) { + *out_conflict_index = (size_t)-1; + } + + if (records == NULL && count != 0u) { + return AMDUAT_FED_INGEST_ERR_INVALID; + } + + for (i = 0; i < count; ++i) { + const amduat_fed_record_t *record = &records[i]; + + if (!amduat_fed_record_validate(record)) { + if (out_error_index != NULL) { + *out_error_index = i; + } + return AMDUAT_FED_INGEST_ERR_INVALID; + } + + for (j = 0; j < i; ++j) { + const amduat_fed_record_t *other = &records[j]; + + if (!amduat_fed_record_id_eq(&record->id, &other->id)) { + continue; + } + if (!amduat_fed_record_equivalent(record, other)) { + if (out_error_index != NULL) { + *out_error_index = i; + } + if (out_conflict_index != NULL) { + *out_conflict_index = j; + } + return AMDUAT_FED_INGEST_ERR_CONFLICT; + } + } + } + + return AMDUAT_FED_INGEST_OK; +} diff --git a/src/near_core/fed/registry.c b/src/near_core/fed/registry.c new file mode 100644 index 0000000..dadf0e5 --- /dev/null +++ b/src/near_core/fed/registry.c @@ -0,0 +1,416 @@ +#include "amduat/fed/registry.h" + +#include +#include + +enum { + AMDUAT_FED_REGISTRY_VERSION = 1, + AMDUAT_FED_REGISTRY_HEADER_LEN = 8, + AMDUAT_FED_REGISTRY_ENTRY_FIXED_LEN = 42 +}; + +static void amduat_fed_store_u16_be(uint8_t *out, uint16_t value) { + out[0] = (uint8_t)((value >> 8) & 0xffu); + out[1] = (uint8_t)(value & 0xffu); +} + +static void amduat_fed_store_u32_be(uint8_t *out, uint32_t value) { + out[0] = (uint8_t)((value >> 24) & 0xffu); + out[1] = (uint8_t)((value >> 16) & 0xffu); + out[2] = (uint8_t)((value >> 8) & 0xffu); + out[3] = (uint8_t)(value & 0xffu); +} + +static void amduat_fed_store_u64_be(uint8_t *out, uint64_t value) { + out[0] = (uint8_t)((value >> 56) & 0xffu); + out[1] = (uint8_t)((value >> 48) & 0xffu); + out[2] = (uint8_t)((value >> 40) & 0xffu); + out[3] = (uint8_t)((value >> 32) & 0xffu); + out[4] = (uint8_t)((value >> 24) & 0xffu); + out[5] = (uint8_t)((value >> 16) & 0xffu); + out[6] = (uint8_t)((value >> 8) & 0xffu); + out[7] = (uint8_t)(value & 0xffu); +} + +static uint16_t amduat_fed_load_u16_be(const uint8_t *data) { + return ((uint16_t)data[0] << 8) | (uint16_t)data[1]; +} + +static uint32_t amduat_fed_load_u32_be(const uint8_t *data) { + return ((uint32_t)data[0] << 24) | ((uint32_t)data[1] << 16) | + ((uint32_t)data[2] << 8) | (uint32_t)data[3]; +} + +static uint64_t amduat_fed_load_u64_be(const uint8_t *data) { + return ((uint64_t)data[0] << 56) | ((uint64_t)data[1] << 48) | + ((uint64_t)data[2] << 40) | ((uint64_t)data[3] << 32) | + ((uint64_t)data[4] << 24) | ((uint64_t)data[5] << 16) | + ((uint64_t)data[6] << 8) | (uint64_t)data[7]; +} + +void amduat_fed_registry_value_init(amduat_fed_registry_value_t *value, + amduat_fed_domain_state_t *states, + size_t cap) { + if (value == NULL) { + return; + } + value->states = states; + value->len = 0; + value->cap = cap; + value->owns_states = false; +} + +bool amduat_fed_registry_value_insert(amduat_fed_registry_value_t *value, + amduat_fed_domain_state_t state) { + size_t left; + size_t right; + + if (value == NULL || value->states == NULL) { + return false; + } + if (value->len >= value->cap) { + return false; + } + + left = 0; + right = value->len; + while (left < right) { + size_t mid; + uint32_t current_id; + + mid = left + (right - left) / 2; + current_id = value->states[mid].domain_id; + if (state.domain_id == current_id) { + return false; + } + if (state.domain_id < current_id) { + right = mid; + } else { + left = mid + 1; + } + } + + if (left < value->len) { + memmove(&value->states[left + 1], + &value->states[left], + (value->len - left) * sizeof(*value->states)); + } + value->states[left] = state; + value->len += 1; + return true; +} + +const amduat_fed_domain_state_t *amduat_fed_registry_value_lookup( + const amduat_fed_registry_value_t *value, + uint32_t domain_id) { + size_t left; + size_t right; + + if (value == NULL || value->states == NULL) { + return NULL; + } + + left = 0; + right = value->len; + while (left < right) { + size_t mid; + uint32_t current_id; + + mid = left + (right - left) / 2; + current_id = value->states[mid].domain_id; + if (domain_id == current_id) { + return &value->states[mid]; + } + if (domain_id < current_id) { + right = mid; + } else { + left = mid + 1; + } + } + + return NULL; +} + +void amduat_fed_registry_value_free(amduat_fed_registry_value_t *value) { + size_t i; + + if (value == NULL) { + return; + } + + if (value->states != NULL) { + for (i = 0; i < value->len; ++i) { + amduat_octets_free(&value->states[i].policy_hash); + } + } + + if (value->owns_states && value->states != NULL) { + free(value->states); + value->states = NULL; + value->cap = 0; + } + + value->len = 0; + value->owns_states = false; +} + +bool amduat_fed_registry_encode(const amduat_fed_registry_value_t *value, + amduat_octets_t *out_bytes) { + size_t i; + size_t total_len; + size_t offset; + uint8_t *buffer; + + if (out_bytes == NULL) { + return false; + } + out_bytes->data = NULL; + out_bytes->len = 0; + + if (value == NULL || value->states == NULL) { + return false; + } + if (value->len > UINT32_MAX) { + return false; + } + + total_len = AMDUAT_FED_REGISTRY_HEADER_LEN; + for (i = 0; i < value->len; ++i) { + const amduat_fed_domain_state_t *state; + size_t entry_len; + + state = &value->states[i]; + if (state->policy_hash.len > UINT32_MAX) { + return false; + } + if (state->policy_hash.len != 0u && state->policy_hash.data == NULL) { + return false; + } + + entry_len = AMDUAT_FED_REGISTRY_ENTRY_FIXED_LEN + state->policy_hash.len; + if (entry_len > SIZE_MAX - total_len) { + return false; + } + total_len += entry_len; + } + + buffer = (uint8_t *)malloc(total_len); + if (buffer == NULL) { + return false; + } + + offset = 0; + amduat_fed_store_u32_be(buffer + offset, AMDUAT_FED_REGISTRY_VERSION); + offset += 4; + amduat_fed_store_u32_be(buffer + offset, (uint32_t)value->len); + offset += 4; + + for (i = 0; i < value->len; ++i) { + const amduat_fed_domain_state_t *state; + + state = &value->states[i]; + amduat_fed_store_u32_be(buffer + offset, state->domain_id); + offset += 4; + amduat_fed_store_u64_be(buffer + offset, state->snapshot_id); + offset += 8; + amduat_fed_store_u64_be(buffer + offset, state->log_prefix); + offset += 8; + amduat_fed_store_u64_be(buffer + offset, state->last_logseq); + offset += 8; + buffer[offset++] = state->admitted; + buffer[offset++] = state->policy_ok; + memcpy(buffer + offset, state->reserved, sizeof(state->reserved)); + offset += sizeof(state->reserved); + amduat_fed_store_u16_be(buffer + offset, state->policy_hash_id); + offset += 2; + amduat_fed_store_u32_be(buffer + offset, + (uint32_t)state->policy_hash.len); + offset += 4; + if (state->policy_hash.len != 0u) { + memcpy(buffer + offset, + state->policy_hash.data, + state->policy_hash.len); + offset += state->policy_hash.len; + } + } + + out_bytes->data = buffer; + out_bytes->len = total_len; + return true; +} + +bool amduat_fed_registry_decode(amduat_octets_t bytes, + amduat_fed_registry_value_t *out_value) { + uint32_t version; + uint32_t count; + size_t offset; + size_t i; + amduat_fed_registry_value_t value; + + if (out_value == NULL) { + return false; + } + if (bytes.len != 0u && bytes.data == NULL) { + return false; + } + if (bytes.len < AMDUAT_FED_REGISTRY_HEADER_LEN) { + return false; + } + + offset = 0; + version = amduat_fed_load_u32_be(bytes.data + offset); + offset += 4; + if (version != AMDUAT_FED_REGISTRY_VERSION) { + return false; + } + count = amduat_fed_load_u32_be(bytes.data + offset); + offset += 4; + + value = *out_value; + if (value.states == NULL) { + if (count != 0u) { + value.states = + (amduat_fed_domain_state_t *)calloc(count, sizeof(*value.states)); + if (value.states == NULL) { + return false; + } + } + value.cap = count; + value.owns_states = true; + } else { + if (value.cap < count) { + return false; + } + value.owns_states = false; + } + value.len = 0; + + for (i = 0; i < count; ++i) { + amduat_fed_domain_state_t state; + uint32_t policy_hash_len; + + if (bytes.len - offset < AMDUAT_FED_REGISTRY_ENTRY_FIXED_LEN) { + amduat_fed_registry_value_free(&value); + return false; + } + + memset(&state, 0, sizeof(state)); + state.domain_id = amduat_fed_load_u32_be(bytes.data + offset); + offset += 4; + state.snapshot_id = amduat_fed_load_u64_be(bytes.data + offset); + offset += 8; + state.log_prefix = amduat_fed_load_u64_be(bytes.data + offset); + offset += 8; + state.last_logseq = amduat_fed_load_u64_be(bytes.data + offset); + offset += 8; + state.admitted = bytes.data[offset++]; + state.policy_ok = bytes.data[offset++]; + memcpy(state.reserved, bytes.data + offset, sizeof(state.reserved)); + offset += sizeof(state.reserved); + state.policy_hash_id = amduat_fed_load_u16_be(bytes.data + offset); + offset += 2; + policy_hash_len = amduat_fed_load_u32_be(bytes.data + offset); + offset += 4; + + if (policy_hash_len > bytes.len - offset) { + amduat_fed_registry_value_free(&value); + return false; + } + + if (policy_hash_len != 0u) { + uint8_t *policy_bytes; + + policy_bytes = (uint8_t *)malloc(policy_hash_len); + if (policy_bytes == NULL) { + amduat_fed_registry_value_free(&value); + return false; + } + memcpy(policy_bytes, bytes.data + offset, policy_hash_len); + state.policy_hash = amduat_octets(policy_bytes, policy_hash_len); + offset += policy_hash_len; + } else { + state.policy_hash = amduat_octets(NULL, 0); + } + + value.states[i] = state; + value.len += 1; + } + + *out_value = value; + return true; +} + +void amduat_fed_registry_store_init(amduat_fed_registry_store_t *reg, + amduat_asl_store_t *store) { + if (reg == NULL) { + return; + } + reg->store = store; +} + +amduat_fed_registry_error_t amduat_fed_registry_store_put( + amduat_fed_registry_store_t *reg, + const amduat_fed_registry_value_t *value, + amduat_reference_t *out_ref, + amduat_asl_store_error_t *out_store_err) { + amduat_octets_t bytes; + amduat_artifact_t artifact; + amduat_asl_store_error_t store_err; + + if (out_store_err != NULL) { + *out_store_err = AMDUAT_ASL_STORE_OK; + } + + if (reg == NULL || reg->store == NULL) { + return AMDUAT_FED_REGISTRY_ERR_STORE; + } + if (!amduat_fed_registry_encode(value, &bytes)) { + return AMDUAT_FED_REGISTRY_ERR_CODEC; + } + + artifact = amduat_artifact(bytes); + store_err = amduat_asl_store_put(reg->store, artifact, out_ref); + amduat_octets_free(&bytes); + if (store_err != AMDUAT_ASL_STORE_OK) { + if (out_store_err != NULL) { + *out_store_err = store_err; + } + return AMDUAT_FED_REGISTRY_ERR_STORE; + } + + return AMDUAT_FED_REGISTRY_OK; +} + +amduat_fed_registry_error_t amduat_fed_registry_store_get( + amduat_fed_registry_store_t *reg, + amduat_reference_t ref, + amduat_fed_registry_value_t *out_value, + amduat_asl_store_error_t *out_store_err) { + amduat_artifact_t artifact; + amduat_asl_store_error_t store_err; + bool ok; + + if (out_store_err != NULL) { + *out_store_err = AMDUAT_ASL_STORE_OK; + } + + if (reg == NULL || reg->store == NULL || out_value == NULL) { + return AMDUAT_FED_REGISTRY_ERR_STORE; + } + + store_err = amduat_asl_store_get(reg->store, ref, &artifact); + if (store_err != AMDUAT_ASL_STORE_OK) { + if (out_store_err != NULL) { + *out_store_err = store_err; + } + return AMDUAT_FED_REGISTRY_ERR_STORE; + } + + ok = amduat_fed_registry_decode(artifact.bytes, out_value); + amduat_artifact_free(&artifact); + if (!ok) { + return AMDUAT_FED_REGISTRY_ERR_CODEC; + } + + return AMDUAT_FED_REGISTRY_OK; +} diff --git a/src/near_core/fed/replay.c b/src/near_core/fed/replay.c new file mode 100644 index 0000000..3829bb1 --- /dev/null +++ b/src/near_core/fed/replay.c @@ -0,0 +1,295 @@ +#include "amduat/fed/replay.h" + +#include +#include + +static int amduat_fed_octets_cmp(amduat_octets_t a, amduat_octets_t b) { + size_t min_len; + int cmp; + + min_len = a.len < b.len ? a.len : b.len; + if (min_len > 0) { + cmp = memcmp(a.data, b.data, min_len); + if (cmp != 0) { + return cmp; + } + } + if (a.len < b.len) { + return -1; + } + if (a.len > b.len) { + return 1; + } + return 0; +} + +static int amduat_fed_record_id_cmp(const amduat_fed_record_id_t *a, + const amduat_fed_record_id_t *b) { + if (a->type != b->type) { + return (a->type < b->type) ? -1 : 1; + } + if (a->ref.hash_id != b->ref.hash_id) { + return (a->ref.hash_id < b->ref.hash_id) ? -1 : 1; + } + return amduat_fed_octets_cmp(a->ref.digest, b->ref.digest); +} + +static int amduat_fed_record_cmp(const void *lhs, const void *rhs) { + const amduat_fed_record_t *a = (const amduat_fed_record_t *)lhs; + const amduat_fed_record_t *b = (const amduat_fed_record_t *)rhs; + + if (a->logseq != b->logseq) { + return (a->logseq < b->logseq) ? -1 : 1; + } + return amduat_fed_record_id_cmp(&a->id, &b->id); +} + +static bool amduat_fed_record_clone(const amduat_fed_record_t *src, + amduat_fed_record_t *out) { + if (src == NULL || out == NULL) { + return false; + } + *out = *src; + if (!amduat_reference_clone(src->id.ref, &out->id.ref)) { + return false; + } + return true; +} + +static void amduat_fed_record_free(amduat_fed_record_t *record) { + if (record == NULL) { + return; + } + amduat_reference_free(&record->id.ref); + memset(record, 0, sizeof(*record)); +} + +static bool amduat_fed_bounds_ok(const amduat_fed_record_t *record, + uint64_t snapshot_id, + uint64_t log_prefix) { + if (record->snapshot_id < snapshot_id) { + return true; + } + if (record->snapshot_id > snapshot_id) { + return false; + } + return record->logseq <= log_prefix; +} + +static bool amduat_fed_ref_eq(amduat_reference_t a, amduat_reference_t b) { + return amduat_reference_eq(a, b); +} + +static bool amduat_fed_tombstone_matches(const amduat_fed_record_t *tombstone, + const amduat_fed_record_t *record) { + if (tombstone->id.type != AMDUAT_FED_REC_TOMBSTONE) { + return false; + } + if (record->id.type == AMDUAT_FED_REC_TOMBSTONE) { + return false; + } + return amduat_fed_ref_eq(tombstone->id.ref, record->id.ref); +} + +static bool amduat_fed_tombstone_list_has( + const amduat_reference_t *refs, + size_t refs_len, + amduat_reference_t candidate) { + size_t i; + + for (i = 0; i < refs_len; ++i) { + if (amduat_fed_ref_eq(refs[i], candidate)) { + return true; + } + } + return false; +} + +static bool amduat_fed_tombstone_list_push(amduat_reference_t **refs, + size_t *len, + size_t *cap, + amduat_reference_t ref) { + amduat_reference_t *next; + + if (*len == *cap) { + size_t next_cap = (*cap == 0u) ? 4u : (*cap * 2u); + next = (amduat_reference_t *)realloc(*refs, + next_cap * sizeof(*next)); + if (next == NULL) { + return false; + } + *refs = next; + *cap = next_cap; + } + + if (!amduat_reference_clone(ref, &(*refs)[*len])) { + return false; + } + (*len)++; + return true; +} + +bool amduat_fed_record_validate(const amduat_fed_record_t *record) { + if (record == NULL) { + return false; + } + if (record->meta.visibility > 1u) { + return false; + } + if (record->meta.has_source > 1u) { + return false; + } + if (record->meta.has_source == 0u && record->meta.source_domain != 0u) { + return false; + } + if (record->id.type < AMDUAT_FED_REC_ARTIFACT || + record->id.type > AMDUAT_FED_REC_TOMBSTONE) { + return false; + } + if (record->id.ref.digest.len != 0u && + record->id.ref.digest.data == NULL) { + return false; + } + return true; +} + +bool amduat_fed_replay_domain(const amduat_fed_record_t *records, + size_t count, + uint32_t domain_id, + uint64_t snapshot_id, + uint64_t log_prefix, + amduat_fed_replay_view_t *out_view) { + amduat_fed_record_t *scratch; + amduat_reference_t *tombstones; + size_t tombstones_len; + size_t tombstones_cap; + size_t scratch_len; + size_t i; + size_t out_len; + bool ok; + + if (out_view == NULL) { + return false; + } + out_view->records = NULL; + out_view->len = 0; + + if (records == NULL && count != 0u) { + return false; + } + + if (count == 0u) { + return true; + } + + scratch = (amduat_fed_record_t *)calloc(count, sizeof(*scratch)); + if (scratch == NULL) { + return false; + } + tombstones = NULL; + tombstones_len = 0; + tombstones_cap = 0; + + scratch_len = 0; + for (i = 0; i < count; ++i) { + const amduat_fed_record_t *record = &records[i]; + + if (record->meta.domain_id != domain_id) { + continue; + } + if (!amduat_fed_record_validate(record)) { + continue; + } + if (!amduat_fed_bounds_ok(record, snapshot_id, log_prefix)) { + continue; + } + if (!amduat_fed_record_clone(record, &scratch[scratch_len])) { + while (scratch_len > 0) { + scratch_len--; + amduat_fed_record_free(&scratch[scratch_len]); + } + free(scratch); + return false; + } + scratch_len++; + } + + qsort(scratch, scratch_len, sizeof(*scratch), amduat_fed_record_cmp); + + ok = true; + out_len = 0; + for (i = 0; i < scratch_len; ++i) { + amduat_fed_record_t current = scratch[i]; + size_t j; + bool tombstoned; + + scratch[i].id.ref.digest.data = NULL; + scratch[i].id.ref.digest.len = 0u; + + if (current.id.type == AMDUAT_FED_REC_TOMBSTONE) { + for (j = 0; j < out_len; ++j) { + if (amduat_fed_tombstone_matches(¤t, &scratch[j])) { + amduat_fed_record_free(&scratch[j]); + memmove(&scratch[j], + &scratch[j + 1], + (out_len - j - 1) * sizeof(*scratch)); + out_len--; + j--; + } + } + if (!amduat_fed_tombstone_list_push(&tombstones, + &tombstones_len, + &tombstones_cap, + current.id.ref)) { + amduat_fed_record_free(¤t); + ok = false; + break; + } + amduat_fed_record_free(¤t); + continue; + } + + tombstoned = amduat_fed_tombstone_list_has(tombstones, + tombstones_len, + current.id.ref); + if (tombstoned) { + amduat_fed_record_free(¤t); + continue; + } + + scratch[out_len++] = current; + } + + for (i = 0; i < tombstones_len; ++i) { + amduat_reference_free(&tombstones[i]); + } + free(tombstones); + + if (!ok) { + for (i = 0; i < scratch_len; ++i) { + amduat_fed_record_free(&scratch[i]); + } + free(scratch); + return false; + } + + out_view->records = scratch; + out_view->len = out_len; + return true; +} + +void amduat_fed_replay_view_free(amduat_fed_replay_view_t *view) { + size_t i; + + if (view == NULL) { + return; + } + if (view->records != NULL) { + for (i = 0; i < view->len; ++i) { + amduat_fed_record_free(&view->records[i]); + } + free(view->records); + } + view->records = NULL; + view->len = 0; +} diff --git a/tests/fed/test_fed_ingest.c b/tests/fed/test_fed_ingest.c new file mode 100644 index 0000000..876da8e --- /dev/null +++ b/tests/fed/test_fed_ingest.c @@ -0,0 +1,81 @@ +#include "amduat/fed/ingest.h" + +#include +#include + +static amduat_reference_t make_ref(amduat_hash_id_t hash_id, + const uint8_t *bytes, + size_t len) { + return amduat_reference(hash_id, amduat_octets(bytes, len)); +} + +static amduat_fed_record_t make_record(uint32_t domain_id, + uint64_t logseq, + amduat_fed_record_type_t type, + amduat_reference_t ref) { + amduat_fed_record_t record; + + memset(&record, 0, sizeof(record)); + record.meta.domain_id = domain_id; + record.meta.visibility = 1; + record.meta.has_source = 0; + record.id.type = type; + record.id.ref = ref; + record.logseq = logseq; + record.snapshot_id = 1; + record.log_prefix = 10; + return record; +} + +static int test_invalid_record(void) { + uint8_t key[] = {0x01}; + amduat_fed_record_t record; + size_t error_index = 0; + + record = make_record(1, 1, AMDUAT_FED_REC_ARTIFACT, + make_ref(1, key, sizeof(key))); + record.meta.visibility = 2; + + if (amduat_fed_ingest_validate(&record, 1, &error_index, NULL) != + AMDUAT_FED_INGEST_ERR_INVALID) { + fprintf(stderr, "expected invalid error\n"); + return 1; + } + if (error_index != 0) { + fprintf(stderr, "expected error index 0\n"); + return 1; + } + return 0; +} + +static int test_conflicting_duplicate(void) { + uint8_t key[] = {0x02}; + amduat_reference_t ref = make_ref(1, key, sizeof(key)); + amduat_fed_record_t records[2]; + size_t error_index = 0; + size_t conflict_index = 0; + + records[0] = make_record(1, 1, AMDUAT_FED_REC_ARTIFACT, ref); + records[1] = make_record(1, 2, AMDUAT_FED_REC_ARTIFACT, ref); + + if (amduat_fed_ingest_validate(records, 2, &error_index, &conflict_index) != + AMDUAT_FED_INGEST_ERR_CONFLICT) { + fprintf(stderr, "expected conflict error\n"); + return 1; + } + if (error_index != 1 || conflict_index != 0) { + fprintf(stderr, "unexpected conflict indices\n"); + return 1; + } + return 0; +} + +int main(void) { + if (test_invalid_record() != 0) { + return 1; + } + if (test_conflicting_duplicate() != 0) { + return 1; + } + return 0; +} diff --git a/tests/fed/test_fed_registry.c b/tests/fed/test_fed_registry.c new file mode 100644 index 0000000..73a4236 --- /dev/null +++ b/tests/fed/test_fed_registry.c @@ -0,0 +1,93 @@ +#include "amduat/fed/registry.h" + +#include +#include + +static bool octets_equal(amduat_octets_t a, amduat_octets_t b) { + if (a.len != b.len) { + return false; + } + if (a.len == 0) { + return true; + } + return memcmp(a.data, b.data, a.len) == 0; +} + +static int test_round_trip(void) { + uint8_t policy_a[] = {0xde, 0xad, 0xbe, 0xef}; + amduat_fed_domain_state_t states[2]; + amduat_fed_registry_value_t value; + amduat_fed_registry_value_t decoded; + amduat_octets_t encoded; + int exit_code = 1; + + memset(states, 0, sizeof(states)); + amduat_fed_registry_value_init(&value, states, 2); + + states[0].domain_id = 42; + states[0].snapshot_id = 10; + states[0].log_prefix = 8; + states[0].last_logseq = 7; + states[0].admitted = 1; + states[0].policy_ok = 1; + states[0].policy_hash_id = 1; + states[0].policy_hash = amduat_octets(policy_a, sizeof(policy_a)); + + states[1].domain_id = 7; + states[1].snapshot_id = 3; + states[1].log_prefix = 2; + states[1].last_logseq = 2; + states[1].admitted = 0; + states[1].policy_ok = 0; + states[1].policy_hash_id = 0; + states[1].policy_hash = amduat_octets(NULL, 0); + + if (!amduat_fed_registry_value_insert(&value, states[0]) || + !amduat_fed_registry_value_insert(&value, states[1])) { + fprintf(stderr, "insert failed\n"); + return exit_code; + } + + if (!amduat_fed_registry_encode(&value, &encoded)) { + fprintf(stderr, "encode failed\n"); + return exit_code; + } + + memset(&decoded, 0, sizeof(decoded)); + if (!amduat_fed_registry_decode(encoded, &decoded)) { + fprintf(stderr, "decode failed\n"); + amduat_octets_free(&encoded); + return exit_code; + } + + if (decoded.len != 2) { + fprintf(stderr, "decoded length mismatch\n"); + goto cleanup; + } + + if (decoded.states[0].domain_id != 7 || + decoded.states[1].domain_id != 42) { + fprintf(stderr, "decoded order mismatch\n"); + goto cleanup; + } + + if (!octets_equal(decoded.states[1].policy_hash, + amduat_octets(policy_a, sizeof(policy_a)))) { + fprintf(stderr, "decoded policy hash mismatch\n"); + goto cleanup; + } + + exit_code = 0; + +cleanup: + amduat_octets_free(&encoded); + amduat_fed_registry_value_free(&decoded); + return exit_code; +} + +int main(void) { + if (test_round_trip() != 0) { + return 1; + } + return 0; +} diff --git a/tests/fed/test_fed_replay.c b/tests/fed/test_fed_replay.c new file mode 100644 index 0000000..5a52ee6 --- /dev/null +++ b/tests/fed/test_fed_replay.c @@ -0,0 +1,134 @@ +#include "amduat/fed/replay.h" + +#include +#include + +static amduat_reference_t make_ref(amduat_hash_id_t hash_id, + const uint8_t *bytes, + size_t len) { + return amduat_reference(hash_id, amduat_octets(bytes, len)); +} + +static amduat_fed_record_t make_record(uint32_t domain_id, + uint64_t logseq, + uint64_t snapshot_id, + uint64_t log_prefix, + amduat_fed_record_type_t type, + amduat_reference_t ref) { + amduat_fed_record_t record; + + memset(&record, 0, sizeof(record)); + record.meta.domain_id = domain_id; + record.meta.visibility = 1; + record.meta.has_source = 0; + record.id.type = type; + record.id.ref = ref; + record.logseq = logseq; + record.snapshot_id = snapshot_id; + record.log_prefix = log_prefix; + return record; +} + +static int test_ordering(void) { + uint8_t d0[] = {0x00}; + uint8_t d1[] = {0x01}; + uint8_t d2[] = {0x02}; + amduat_fed_record_t records[3]; + amduat_fed_replay_view_t view; + + records[0] = make_record(1, 5, 1, 10, AMDUAT_FED_REC_ARTIFACT, + make_ref(1, d1, sizeof(d1))); + records[1] = make_record(1, 5, 1, 10, AMDUAT_FED_REC_ARTIFACT, + make_ref(1, d0, sizeof(d0))); + records[2] = make_record(1, 4, 1, 10, AMDUAT_FED_REC_ARTIFACT, + make_ref(1, d2, sizeof(d2))); + + if (!amduat_fed_replay_domain(records, 3, 1, 1, 10, &view)) { + fprintf(stderr, "replay failed\n"); + return 1; + } + if (view.len != 3) { + fprintf(stderr, "ordering length mismatch\n"); + amduat_fed_replay_view_free(&view); + return 1; + } + if (view.records[0].logseq != 4 || + view.records[1].id.ref.digest.data[0] != 0x00 || + view.records[2].id.ref.digest.data[0] != 0x01) { + fprintf(stderr, "ordering mismatch\n"); + amduat_fed_replay_view_free(&view); + return 1; + } + + amduat_fed_replay_view_free(&view); + return 0; +} + +static int test_tombstone_scope(void) { + uint8_t key[] = {0xaa}; + amduat_reference_t ref = make_ref(1, key, sizeof(key)); + amduat_fed_record_t records[3]; + amduat_fed_replay_view_t view; + + records[0] = make_record(1, 1, 1, 10, AMDUAT_FED_REC_ARTIFACT, ref); + records[1] = make_record(1, 2, 1, 10, AMDUAT_FED_REC_TOMBSTONE, ref); + records[2] = make_record(2, 1, 1, 10, AMDUAT_FED_REC_ARTIFACT, ref); + + if (!amduat_fed_replay_domain(records, 3, 1, 1, 10, &view)) { + fprintf(stderr, "replay domain 1 failed\n"); + return 1; + } + if (view.len != 0) { + fprintf(stderr, "tombstone scope mismatch (domain 1)\n"); + amduat_fed_replay_view_free(&view); + return 1; + } + amduat_fed_replay_view_free(&view); + + if (!amduat_fed_replay_domain(records, 3, 2, 1, 10, &view)) { + fprintf(stderr, "replay domain 2 failed\n"); + return 1; + } + if (view.len != 1) { + fprintf(stderr, "tombstone scope mismatch (domain 2)\n"); + amduat_fed_replay_view_free(&view); + return 1; + } + amduat_fed_replay_view_free(&view); + return 0; +} + +static int test_bounds(void) { + uint8_t key[] = {0xbb}; + amduat_reference_t ref = make_ref(1, key, sizeof(key)); + amduat_fed_record_t records[2]; + amduat_fed_replay_view_t view; + + records[0] = make_record(3, 1, 2, 10, AMDUAT_FED_REC_ARTIFACT, ref); + records[1] = make_record(3, 5, 1, 4, AMDUAT_FED_REC_ARTIFACT, ref); + + if (!amduat_fed_replay_domain(records, 2, 3, 1, 4, &view)) { + fprintf(stderr, "replay bounds failed\n"); + return 1; + } + if (view.len != 0) { + fprintf(stderr, "bounds mismatch\n"); + amduat_fed_replay_view_free(&view); + return 1; + } + amduat_fed_replay_view_free(&view); + return 0; +} + +int main(void) { + if (test_ordering() != 0) { + return 1; + } + if (test_tombstone_scope() != 0) { + return 1; + } + if (test_bounds() != 0) { + return 1; + } + return 0; +} diff --git a/tier1/asl-domain-model-1.md b/tier1/asl-domain-model-1.md index 400bb56..f29d9b3 100644 --- a/tier1/asl-domain-model-1.md +++ b/tier1/asl-domain-model-1.md @@ -162,3 +162,6 @@ ASL/DOMAIN-MODEL/1 does not define: * Encoding formats * Storage layouts or filesystem assumptions * Governance workflows beyond admission and policy compatibility + +Middle layer (informative): the daemon/service boundary around core logic that +owns network transport, admission workflows, and operational policy. diff --git a/tier1/asl-federation-1.md b/tier1/asl-federation-1.md index 900715b..8b796d7 100644 --- a/tier1/asl-federation-1.md +++ b/tier1/asl-federation-1.md @@ -179,6 +179,9 @@ Federation MUST NOT bypass ASL/LOG/1 ordering or ASL/1-CORE-INDEX semantics. * Witness signatures * Domain admission and trust policy +Middle layer (informative): the daemon/service boundary around core logic that +owns network transport, admission workflows, and operational policy. + --- ## 12. Summary diff --git a/tier1/asl-system-1.md b/tier1/asl-system-1.md index 675fd56..a2cbc84 100644 --- a/tier1/asl-system-1.md +++ b/tier1/asl-system-1.md @@ -77,6 +77,9 @@ Non-goals: * New execution operators * Domain policy or governance rules +Middle layer (informative): the daemon/service boundary around core logic that +owns network transport, admission workflows, and operational policy. + --- ## 2. Core Objects (Unified View)