feat(ha-term-safety): add WAL_CONFIG_MARKER entry type + reference payload codec (task 4.1)

This commit is contained in:
Rongsong Shen 2026-06-22 09:37:23 +00:00
parent f1783f7952
commit f447ad4183
5 changed files with 215 additions and 1 deletions

View file

@ -37,6 +37,57 @@
#include "xce/cluster_def.h"
#include "wal_types.h" /* wal_entry_hdr_t, WAL_* constants */
/* ── WAL_CONFIG_MARKER codec (ha-term-safety task 4.1) ───────────────
* Fixed, padding-independent serialization of a config-change marker:
* 8-byte big-endian version_tag followed by the raw 32-byte SHA-256. NO
* configuration bytes are ever encoded (R7.2/R7.4) — the bytes travel
* out-of-band on the config-push transport. The explicit byte layout (rather
* than a struct memcpy) keeps the form stable across compiler padding and host
* endianness, since this payload replicates to standbys via AppendEntries. */
int config_marker_encode(uint64_t version_tag, const uint8_t sha256[32],
uint8_t *buf, size_t bufsz, size_t *out_len)
{
if (!sha256 || !buf || !out_len)
return -EINVAL;
if (bufsz < WAL_CONFIG_MARKER_PAYLOAD_LEN)
return -ENOSPC;
/* version_tag: 8 bytes, big-endian (matches config_repl wire convention). */
buf[0] = (uint8_t)(version_tag >> 56);
buf[1] = (uint8_t)(version_tag >> 48);
buf[2] = (uint8_t)(version_tag >> 40);
buf[3] = (uint8_t)(version_tag >> 32);
buf[4] = (uint8_t)(version_tag >> 24);
buf[5] = (uint8_t)(version_tag >> 16);
buf[6] = (uint8_t)(version_tag >> 8);
buf[7] = (uint8_t)(version_tag);
/* sha256: raw 32 bytes — the configuration *reference*, never the bytes. */
memcpy(buf + 8, sha256, 32);
*out_len = WAL_CONFIG_MARKER_PAYLOAD_LEN;
return 0;
}
int config_marker_decode(const uint8_t *buf, size_t len,
uint64_t *version_tag, uint8_t sha256_out[32])
{
if (!buf || len < WAL_CONFIG_MARKER_PAYLOAD_LEN)
return -EINVAL;
if (version_tag) {
*version_tag = ((uint64_t)buf[0] << 56) | ((uint64_t)buf[1] << 48) |
((uint64_t)buf[2] << 40) | ((uint64_t)buf[3] << 32) |
((uint64_t)buf[4] << 24) | ((uint64_t)buf[5] << 16) |
((uint64_t)buf[6] << 8) | ((uint64_t)buf[7]);
}
if (sha256_out)
memcpy(sha256_out, buf + 8, 32);
return 0;
}
/* SSL context for xce_probe_config_tag.
* Stored as void* to avoid SSL_CTX type at this point in the translation unit. */
static void *s_probe_ssl_ctx = NULL;

View file

@ -9,6 +9,7 @@
#include "clusterconfig.h"
#include "netio.h"
#include "request.h"
#include "wal_types.h" /* WAL_CONFIG_MARKER, WAL_CONFIG_MARKER_PAYLOAD_LEN */
/* Max config blob size — reject before allocating */
#define CONF_MAX_SIZE (1 * 1024 * 1024) /* 1 MiB */
@ -29,6 +30,49 @@
/* Certificate expiry warning threshold (days) */
#define CERT_WARN_DAYS 30
/* ── WAL_CONFIG_MARKER codec (ha-term-safety task 4.1) ───────────────
*
* A config-change marker payload is a *reference* to a configuration — its
* version tag and full SHA-256 content hash — and carries NO configuration
* bytes (R7.2/R7.4). The bytes always travel out-of-band on the config-push
* transport; they are never placed in a WAL payload. This codec produces the
* fixed, padding-independent on-wire form (big-endian version_tag + raw 32-byte
* sha256) that rides inside the raft_entry_blob_t envelope as the payload of a
* WAL_CONFIG_MARKER entry.
*/
/*
* config_marker_encode: serialize a {version_tag, sha256} config-change marker
* into a fixed WAL_CONFIG_MARKER_PAYLOAD_LEN-byte buffer. NO config bytes are
* ever written. version_tag is emitted big-endian; sha256 is the raw 32 bytes.
*
* buf — output buffer (must be >= WAL_CONFIG_MARKER_PAYLOAD_LEN)
* bufsz — capacity of buf
* out_len — receives the number of bytes written (always
* WAL_CONFIG_MARKER_PAYLOAD_LEN on success); untouched on failure
*
* Returns 0 on success; -EINVAL if sha256/buf/out_len is NULL;
* -ENOSPC if bufsz < WAL_CONFIG_MARKER_PAYLOAD_LEN.
*/
int config_marker_encode(uint64_t version_tag, const uint8_t sha256[32],
uint8_t *buf, size_t bufsz, size_t *out_len);
/*
* config_marker_decode: deserialize a WAL_CONFIG_MARKER payload produced by
* config_marker_encode. Reads exactly WAL_CONFIG_MARKER_PAYLOAD_LEN bytes; a
* shorter buffer is rejected without any out-of-bounds read.
*
* buf — input buffer
* len — bytes available in buf
* version_tag — receives the host-order version tag (may be NULL to skip)
* sha256_out — receives the raw 32-byte content hash (may be NULL to skip)
*
* Returns 0 on success; -EINVAL if buf is NULL or len <
* WAL_CONFIG_MARKER_PAYLOAD_LEN.
*/
int config_marker_decode(const uint8_t *buf, size_t len,
uint64_t *version_tag, uint8_t sha256_out[32]);
/* ── Phase 1: Storage ────────────────────────────────────────── */
/*

View file

@ -504,6 +504,92 @@ static void test_conf_save_config_path_consistency(void)
printf("test_conf_save_config_path_consistency: PASS\n");
}
/* ── WAL_CONFIG_MARKER codec (ha-term-safety task 4.1) ───────────────
* The config-change marker payload carries ONLY {version_tag, sha256} —
* a reference, never the configuration bytes (R7.2/R7.4). These tests
* exercise the fixed-size encode/decode codec that rides inside the
* raft_entry_blob_t envelope with type == WAL_CONFIG_MARKER. */
/* Round-trip: encode {version_tag, sha256} then decode → identical, and the
* encoded form is exactly the fixed marker size (tag + 32-byte hash) with NO
* configuration bytes appended. */
static void test_config_marker_encode_decode_roundtrip(void)
{
const uint64_t version_tag = 0x0123456789abcdefULL;
uint8_t sha256[32];
for (int i = 0; i < 32; i++)
sha256[i] = (uint8_t)(0xA0 + i);
uint8_t buf[64];
size_t enc_len = 0;
int rc = config_marker_encode(version_tag, sha256, buf, sizeof(buf), &enc_len);
TEST_ASSERT_EQUAL_INT(0, rc);
/* Encoded form carries NO config bytes: exactly 8 (tag) + 32 (sha256). */
TEST_ASSERT_EQUAL_UINT(WAL_CONFIG_MARKER_PAYLOAD_LEN, enc_len);
TEST_ASSERT_EQUAL_UINT(8u + 32u, enc_len);
uint64_t got_tag = 0;
uint8_t got_sha[32];
memset(got_sha, 0, sizeof(got_sha));
rc = config_marker_decode(buf, enc_len, &got_tag, got_sha);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_HEX64(version_tag, got_tag);
TEST_ASSERT_EQUAL_HEX8_ARRAY(sha256, got_sha, 32);
printf("test_config_marker_encode_decode_roundtrip: PASS\n");
}
/* The WAL entry type value must be exactly 50 and must not collide with the
* neighbouring WAL_NOOP (40) or WAL_CHECKPOINT (100) types (design.md). */
static void test_config_marker_enum_value_is_50(void)
{
TEST_ASSERT_EQUAL_INT(50, WAL_CONFIG_MARKER);
TEST_ASSERT_NOT_EQUAL(WAL_NOOP, WAL_CONFIG_MARKER);
TEST_ASSERT_NOT_EQUAL(WAL_CHECKPOINT, WAL_CONFIG_MARKER);
/* Strictly between WAL_NOOP and WAL_CHECKPOINT. */
TEST_ASSERT_TRUE(WAL_CONFIG_MARKER > WAL_NOOP);
TEST_ASSERT_TRUE(WAL_CONFIG_MARKER < WAL_CHECKPOINT);
printf("test_config_marker_enum_value_is_50: PASS\n");
}
/* A truncated (short) buffer must fail cleanly with -EINVAL, never read past
* the end of the supplied buffer. */
static void test_config_marker_decode_truncated_fails(void)
{
uint8_t sha256[32];
memset(sha256, 0x5c, sizeof(sha256));
uint8_t buf[WAL_CONFIG_MARKER_PAYLOAD_LEN];
size_t enc_len = 0;
TEST_ASSERT_EQUAL_INT(0, config_marker_encode(0xdeadbeefcafef00dULL, sha256,
buf, sizeof(buf), &enc_len));
uint64_t tag = 0;
uint8_t sha_out[32];
/* One byte short of a full marker payload → reject, no OOB read. */
int rc = config_marker_decode(buf, enc_len - 1, &tag, sha_out);
TEST_ASSERT_EQUAL_INT(-EINVAL, rc);
/* Zero-length buffer → reject. */
rc = config_marker_decode(buf, 0, &tag, sha_out);
TEST_ASSERT_EQUAL_INT(-EINVAL, rc);
printf("test_config_marker_decode_truncated_fails: PASS\n");
}
/* A too-small output buffer must fail with -ENOSPC and not overflow. */
static void test_config_marker_encode_short_buffer_fails(void)
{
uint8_t sha256[32];
memset(sha256, 0x11, sizeof(sha256));
uint8_t small[WAL_CONFIG_MARKER_PAYLOAD_LEN - 1];
size_t enc_len = 123; /* sentinel — must be left untouched on failure */
int rc = config_marker_encode(7, sha256, small, sizeof(small), &enc_len);
TEST_ASSERT_EQUAL_INT(-ENOSPC, rc);
printf("test_config_marker_encode_short_buffer_fails: PASS\n");
}
/* ── wait_all_with_timeout (XCE_TESTING shim) ───── */
#ifdef XCE_TESTING
@ -2260,6 +2346,11 @@ void test_config_repl(void)
RUN_TEST(test_sanitise_cn_control_chars);
RUN_TEST(test_sanitise_cn_truncated);
RUN_TEST(test_conf_save_config_path_consistency);
/* Task 4.1: WAL_CONFIG_MARKER type + reference-only codec */
RUN_TEST(test_config_marker_enum_value_is_50);
RUN_TEST(test_config_marker_encode_decode_roundtrip);
RUN_TEST(test_config_marker_decode_truncated_fails);
RUN_TEST(test_config_marker_encode_short_buffer_fails);
#ifdef XCE_TESTING
RUN_TEST(test_load_primary_config_missing_returns_error);
RUN_TEST(test_wait_all_with_timeout_slow_threads);