feat(v4.0.0): every node asks the kernel for its blocks; a born node is not POSTed

A block is a kernel request, as ENGINE.md 3.3 has it: the node puts the
block's number and the address of 256 cells on its stack and writes the
request to port 0, and the kernel leaves the status there.  The requests
are -1, read, and -2, write, the same for every node.  v4/system/blocks.c
serves them from the kernel's block subsystem, which is v3's.  The four
storage registers are gone from the engine.

The device that spoke block messages (4a505a15) is withdrawn with its
test and its message types: Captain Bob ruled on 2026-10-07 that it, a
node's own drive, and nodes with no storage had left the OS as designed
(docs/v4.0.0/MESH.md 8.5).

Hera no longer sends POST to the nodes she births: POST is the kernel's,
once.  Every node has its kernel on port 0; it serves a node's blocks and,
for Hera alone, her requests for nodes and capsules.

Bare metal: the node boots and is POSTed against POST's own block RAM,
and the kernel's chain -- fast RAM, the ramdrive, the virtio disk -- is
set up after POST and before the prompt, as on the v3 path.  The disk is
read and not written: nothing in v4 yet gives the owner's word that it
may be formatted.  A hosted program has the chain's fast RAM, as hosted
v3 has with no disk.  Error 17 is Storage refused.

make -C v4 test and sanitize pass at both widths; hosted-check passes on
three ISAs; amd64, aarch64 and riscv64 boot, POST 538 of 538, with the
typed session: logs/20261007-081603, -081839, -082226.  The hashes are
the same on all six.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rajames
2026-10-07 08:24:47 -04:00
co-authored by Claude Opus 5.5
parent 26a0748455
commit d5b7235464
34 changed files with 953 additions and 725 deletions
+49
View File
@@ -0,0 +1,49 @@
/* blocks.c -- the kernel serves a node's block requests. See blocks.h.
*
* This is the one file of v4's that includes v3's headers: every block goes
* through v3's own calls.
*/
#include "v4/blocks.h"
#include "block_subsystem.h"
int v4_blocks_serve(v4_node *n, v4_cell request)
{
v4_cell addr, num;
uint32_t block;
uint8_t *b;
unsigned i;
int status;
if (request != V4_REQ_BLOCK_READ && request != V4_REQ_BLOCK_WRITE) return 0;
addr = v4_dstack_pop(&n->ds);
num = v4_dstack_pop(&n->ds);
block = (uint32_t)num;
if (!v4_node_addr_ok(addr) || !v4_node_addr_ok(addr + (v4_cell)(V4_BLOCK_CELLS - 1u))) {
status = V4_BLOCK_REFUSED;
} else if (num < 1 || (uint64_t)num > 0xFFFFFFFFull || !blk_is_valid(block)) {
status = V4_BLOCK_RANGE;
} else if (request == V4_REQ_BLOCK_READ) {
b = blk_get_buffer(block, 0);
status = b ? V4_BLOCK_OK : V4_BLOCK_REFUSED;
if (b)
for (i = 0; i < V4_BLOCK_CELLS; i++)
n->mem[addr + (v4_cell)i] = (v4_cell)((v4_ucell)b[4u * i] | ((v4_ucell)b[4u * i + 1u] << 8)
| ((v4_ucell)b[4u * i + 2u] << 16) | ((v4_ucell)b[4u * i + 3u] << 24));
} else {
b = blk_get_buffer(block, 1);
status = V4_BLOCK_REFUSED;
if (b) {
for (i = 0; i < V4_BLOCK_CELLS; i++) {
v4_ucell w = (v4_ucell)n->mem[addr + (v4_cell)i];
b[4u * i] = (uint8_t)(w & 0xFFu);
b[4u * i + 1u] = (uint8_t)((w >> 8) & 0xFFu);
b[4u * i + 2u] = (uint8_t)((w >> 16) & 0xFFu);
b[4u * i + 3u] = (uint8_t)((w >> 24) & 0xFFu);
}
if (blk_update(block) == BLK_OK && blk_flush(block) == BLK_OK) status = V4_BLOCK_OK;
}
}
v4_dstack_push(&n->ds, (v4_cell)status);
return 1;
}
+9 -6
View File
@@ -4,6 +4,7 @@
* It is not part of the engine (v4/src): it needs the capsule directory,
* which only a whole system has.
*/
#include "v4/blocks.h"
#include "v4/boot.h"
#include "v4/message.h"
#include "starkernel/capsule.h"
@@ -109,9 +110,9 @@ static void post_watch(char c)
/* The boot is the node's neighbour on two of its ports. On port 0 it is
* the kernel: the node writes the number of a request there (ENGINE.md
* 3.3). On port 1 it is the console: it writes the node a message of text
* and reads back messages of what the node printed and of how the text
* ended (message.h). */
* 3.3) -- for a block (blocks.h), or for one of the host's words. On port
* 1 it is the console: it writes the node a message of text and reads back
* messages of what the node printed and of how the text ended (message.h). */
#define KERNEL_PORT 0u
#define CONSOLE_PORT 1u
#define CONSOLE_ID 1 /* the console's number as a sender; the node's is 0 until it is given one */
@@ -130,7 +131,9 @@ int v4_boot_line(const v4_boot *b, const char *text, unsigned len)
if (n->stopped) return V4_BOOT_LINE_STOPPED;
if (n->asking && n->ask_port == KERNEL_PORT) { /* a request: the kernel's turn */
if (b->serve && n->request >= 1 && n->request <= (v4_cell)b->word_count) {
if (v4_blocks_serve(n, n->request)) { /* a block, as for any node */
v4_node_port_served(n);
} else if (b->serve && n->request >= 1 && n->request <= (v4_cell)b->word_count) {
b->serve(n, (unsigned)n->request);
v4_node_port_served(n);
} else {
@@ -335,13 +338,13 @@ static int load_nucleus(const v4_boot *b)
/* ---- the whole boot ------------------------------------------------------ */
int v4_boot_run(const v4_boot *b, unsigned char *disk, unsigned blocks)
int v4_boot_run(const v4_boot *b)
{
unsigned i;
post_len = 0; post_seen = 0; post_clean = 0;
/* a node is born with nothing in it, and is sent the nucleus */
if (!v4_image_born(b->n, b->es, b->h, b->im, disk, blocks)) {
if (!v4_image_born(b->n, b->es, b->h, b->im)) {
say(b, "V4: the nucleus is not for this build of the engine\nPARITY:FAIL\nPOST: FAILED\n");
return 0;
}
-161
View File
@@ -1,161 +0,0 @@
/* storage.c -- what is on a storage port. See storage.h.
* docs/v4.0.0/MESH.md section 8.
*
* Nothing here uses the C library: the bare-metal kernel links this file.
*/
#include "v4/storage.h"
void v4_store_init(v4_store *s, const v4_store_ops *ops, void *common, void *private_chain, v4_cell number)
{
unsigned i;
s->ops = ops;
s->common = common;
s->private_chain = private_chain;
s->number = number;
s->in.count = 0;
for (i = 0; i < V4_STORE_PENDING; i++) s->writing[i].used = 0;
s->head = 0;
s->count = 0;
s->out.count = 0;
s->out_at = 0;
s->out_ready = 0;
s->taken = 0;
}
/* The chain a number means and the block in it; or 0, with why in *status. */
static void *chain_for(const v4_store *s, uint64_t n, uint32_t *block, int *status)
{
*status = V4_STORE_RANGE;
if (n == 0 || n > V4_STORE_TOP) return 0;
if (s->private_chain) {
uint64_t k = V4_STORE_TOP - n, have = s->ops->blocks(s->private_chain);
if (have > V4_STORE_FIRST && k < have - V4_STORE_FIRST) { *block = (uint32_t)(V4_STORE_FIRST + k); return s->private_chain; }
}
*block = (uint32_t)n;
if (s->common) return s->common;
if (s->private_chain && n < V4_STORE_FIRST) return s->private_chain;
*status = V4_STORE_NONE;
return 0;
}
static void queue(v4_store *s, v4_cell to, int read, uint64_t block, int status)
{
unsigned at = (s->head + s->count) % V4_STORE_PENDING;
if (s->count == V4_STORE_PENDING) return; /* take does not let it come to this */
s->answer[at].to = to;
s->answer[at].read = read;
s->answer[at].block = block;
s->answer[at].status = status;
s->count++;
}
/* A block of data has come from `from`: it is written where that sender's
* BLOCK_WRITE said, if there was one. */
static int write_block(v4_store *s, v4_cell from)
{
unsigned i, k;
uint32_t block;
int status;
void *chain;
for (i = 0; i < V4_STORE_PENDING && !(s->writing[i].used && s->writing[i].from == from); i++) { }
if (i == V4_STORE_PENDING) return V4_STORE_REFUSED;
s->writing[i].used = 0;
if (v4_message_length(&s->in) != V4_STORE_BYTES) return V4_STORE_REFUSED;
chain = chain_for(s, s->writing[i].block, &block, &status);
if (!chain) return status;
for (k = 0; k < V4_STORE_BYTES; k++) s->bytes[k] = (unsigned char)v4_message_char(&s->in, k);
return s->ops->write(chain, block, s->bytes);
}
/* The message in s->in is whole. */
static void served(v4_store *s)
{
v4_cell from = v4_message_from(&s->in), type = v4_message_type(&s->in);
uint64_t block = (uint64_t)(v4_ucell)s->in.word[V4_MSG_HEADER];
unsigned i, free_at = V4_STORE_PENDING;
s->taken++;
if (v4_message_to(&s->in) != s->number) return;
if (type == V4_MSG_BLOCK_DATA) { queue(s, from, 0, 0, write_block(s, from)); return; }
if (type != V4_MSG_BLOCK_READ && type != V4_MSG_BLOCK_WRITE) return;
if (v4_message_length(&s->in) != 4) { queue(s, from, 0, 0, V4_STORE_REFUSED); return; }
if (type == V4_MSG_BLOCK_READ) { queue(s, from, 1, block, 0); return; }
/* a write: its number is kept until its block comes. A sender's second
* write before then takes the place of its first. */
for (i = 0; i < V4_STORE_PENDING; i++) {
if (s->writing[i].used && s->writing[i].from == from) { free_at = i; break; }
if (!s->writing[i].used && free_at == V4_STORE_PENDING) free_at = i;
}
if (free_at == V4_STORE_PENDING) { queue(s, from, 0, 0, V4_STORE_REFUSED); return; }
s->writing[free_at].used = 1;
s->writing[free_at].from = from;
s->writing[free_at].block = block;
}
int v4_store_take(void *self, v4_cell value)
{
v4_store *s = (v4_store *)self;
if (s->in.count == 0 && s->count == V4_STORE_PENDING) return 0;
if (v4_message_word(&s->in, value)) {
served(s);
s->in.count = 0;
}
return 1;
}
/* The oldest answer waiting becomes the message in s->out. */
static void answer(v4_store *s)
{
v4_cell to = s->answer[s->head].to;
int status = s->answer[s->head].status;
unsigned i;
if (s->answer[s->head].read) {
uint32_t block;
void *chain = chain_for(s, s->answer[s->head].block, &block, &status);
if (chain) status = s->ops->read(chain, block, s->bytes);
} else if (status == V4_STORE_OK) {
status = -1; /* a write that worked: said below */
}
s->head = (s->head + 1) % V4_STORE_PENDING;
s->count--;
s->out.word[0] = to;
s->out.word[1] = s->number;
s->out.word[3] = 0;
s->out.word[4] = 0;
s->out.word[5] = 0;
if (status == V4_STORE_OK) { /* a read that worked: the block */
s->out.word[2] = V4_MSG_BLOCK_DATA;
s->out.word[6] = (v4_cell)V4_STORE_BYTES;
for (i = 0; i < V4_STORE_BYTES / 4u; i++)
s->out.word[V4_MSG_HEADER + i] = (v4_cell)((v4_ucell)s->bytes[4u * i] | (v4_ucell)s->bytes[4u * i + 1u] << 8
| (v4_ucell)s->bytes[4u * i + 2u] << 16 | (v4_ucell)s->bytes[4u * i + 3u] << 24);
s->out.count = V4_MSG_HEADER + V4_STORE_BYTES / 4u;
} else {
s->out.word[2] = V4_MSG_BLOCK_DONE;
s->out.word[6] = 4;
s->out.word[V4_MSG_HEADER] = status < 0 ? V4_STORE_OK : status;
s->out.count = V4_MSG_HEADER + 1u;
}
s->out_at = 0;
s->out_ready = 1;
}
int v4_store_give(void *self, v4_cell *value)
{
v4_store *s = (v4_store *)self;
if (!s->out_ready) {
if (s->count == 0) return 0;
answer(s);
}
*value = s->out.word[s->out_at++];
if (s->out_at == s->out.count) s->out_ready = 0;
return 1;
}
-55
View File
@@ -1,55 +0,0 @@
/* store_v3.c -- the kernel's block subsystem as the chains behind a storage
* port. docs/v4.0.0/MESH.md 8.4, docs/v4.0.0/V3-PARITY.md 1d.
*
* A chain is a blk_chain_t (v3/include/block_subsystem.h). Every block goes
* through v3's own calls, so the header, the allocation map, the block
* cards and relocation are v3's, as they were. Who may have a block is not
* checked here yet (MESH.md 8.5).
*
* This is the one file of v4's that includes v3's headers.
*/
#include "v4/storage.h"
#include "block_subsystem.h"
static int v3_read(void *chain, uint32_t block, unsigned char *bytes)
{
blk_chain_t *was = blk_chain_select((blk_chain_t *)chain);
int status = V4_STORE_RANGE;
unsigned i;
if (blk_is_valid(block)) {
const uint8_t *b = blk_get_buffer(block, 0);
status = b ? V4_STORE_OK : V4_STORE_REFUSED;
if (b) for (i = 0; i < V4_STORE_BYTES; i++) bytes[i] = b[i];
}
blk_chain_select(was);
return status;
}
static int v3_write(void *chain, uint32_t block, const unsigned char *bytes)
{
blk_chain_t *was = blk_chain_select((blk_chain_t *)chain);
int status = V4_STORE_RANGE;
unsigned i;
if (blk_is_valid(block)) {
uint8_t *b = blk_get_buffer(block, 1);
status = V4_STORE_REFUSED;
if (b) {
for (i = 0; i < V4_STORE_BYTES; i++) b[i] = bytes[i];
if (blk_update(block) == BLK_OK && blk_flush(block) == BLK_OK) status = V4_STORE_OK;
}
}
blk_chain_select(was);
return status;
}
static uint32_t v3_blocks(void *chain)
{
blk_chain_t *was = blk_chain_select((blk_chain_t *)chain);
uint32_t n = blk_get_total_blocks();
blk_chain_select(was);
return n;
}
const v4_store_ops v4_store_v3 = { v3_read, v3_write, v3_blocks };