<?xml version="1.0" encoding="utf-8"?>
<!-- name="GENERATOR" content="github.com/mmarkdown/mmark Mmark Markdown Processor - mmark.miek.nl" -->
<rfc version="3" ipr="trust200902" docName="draft-nam-ksg-journal-export-00" submissionType="IETF" category="info" xml:lang="en" xmlns:xi="http://www.w3.org/2001/XInclude" indexInclude="true">

<front>
<title abbrev="KSG-Journal-Export">Keysingate Journal Export: Pages under a Portable Seal</title><seriesInfo value="draft-nam-ksg-journal-export-00" stream="IETF" status="informational" name="Internet-Draft"></seriesInfo>
<author initials="Y." surname="Nam" fullname="Yevhenii Nam"><organization>Keysingate</organization><address><postal><street></street>
</postal><email>nam@keysingate.com</email>
<uri>https://github.com/keysingate</uri>
</address></author><date year="2026" month="October" day="1"></date>
<area>General</area>
<workgroup></workgroup>
<keyword>journal</keyword>
<keyword>seal</keyword>
<keyword>anchoring</keyword>
<keyword>Merkle tree</keyword>
<keyword>blind anchor</keyword>

<abstract>
<t>A Keysingate container keeps a work journal: pages without signatures, closed
by one portable seal that covers every page before it and is moved forward by
each new record. This document specifies the journal's pages, records and
seals, the two forms of a journal (unanchored and anchored), the two exports a
third party receives -- one page with its proof, or the whole journal once --
and how a verifier checks them without contacting anyone. It also specifies a
profile for anchoring seals in Solana with a blind memo that shows outsiders
only the fact of a write. The core documents, time format and anchor rule it
relies on are those of draft-nam-ksg-core-00.</t>
</abstract>

</front>

<middle>

<section anchor="introduction"><name>Introduction</name>
<t>A page carries no signature; a <strong>record</strong> of up to 99 content pages and a
service page is closed by one <strong>seal</strong>; the seal covers the whole chain of
pages up to it; an anchor gives a record its time bound and its force. One
verifier procedure reads both exports.</t>
<t>The container itself -- how it is built, who presses what, how its keys are
held -- is not specified here. A verifier sees only pages, seals and anchors.</t>
</section>

<section anchor="conventions-and-terminology"><name>Conventions and Terminology</name>
<t>The key words &quot;<bcp14>MUST</bcp14>&quot;, &quot;<bcp14>MUST NOT</bcp14>&quot;, &quot;<bcp14>REQUIRED</bcp14>&quot;, &quot;<bcp14>SHALL</bcp14>&quot;,
&quot;<bcp14>SHALL NOT</bcp14>&quot;, &quot;<bcp14>SHOULD</bcp14>&quot;, &quot;<bcp14>SHOULD NOT</bcp14>&quot;, &quot;<bcp14>RECOMMENDED</bcp14>&quot;,
&quot;<bcp14>NOT RECOMMENDED</bcp14>&quot;, &quot;<bcp14>MAY</bcp14>&quot;, and &quot;<bcp14>OPTIONAL</bcp14>&quot; in this document are to
be interpreted as described in BCP 14 <xref target="RFC2119"></xref> <xref target="RFC8174"></xref> when, and only
when, they appear in all capitals, as shown here.</t>
<t>The canonical form, document hash, signatures, time, limits and the Bound Rule
are those of <xref target="KSG-CORE"></xref>. All documents here carry
<tt>&quot;@context&quot;: &quot;urn:keysingate:core:v3&quot;</tt> and <tt>&quot;v&quot;: 3</tt>; <tt>v</tt> is the journal
format's version, which today equals the core's and may move separately.</t>

<dl spacing="compact">
<dt>Page:</dt>
<dd>Room for content, up to 2 048 bytes; no signature.</dd>
<dt>Record:</dt>
<dd>The unit of writing: content pages and, when there is more than one page, a
service page; one time mark; one seal.</dd>
<dt>Seal:</dt>
<dd>A signed statement that closes a record and covers all pages before it.</dd>
<dt>Agent, owner:</dt>
<dd>The two parties of a journal, each known by keys pinned in the journal
itself.</dd>
</dl>
</section>

<section anchor="layout"><name>Layout</name>
<t>A journal has <tt>1 + 1000 + 100 + 2</tt> places: page 0 (the opening); 1 000 working
pages; 100 pages of the transfer zone (changes of owner); 2 coupling pages.
All zones form <strong>one</strong> chain of pages in the order of writing.</t>
</section>

<section anchor="pages"><name>Pages</name>

<sourcecode type="json"><![CDATA[{ "@context": "urn:keysingate:core:v3", "type": "BoundPage", "v": 3,
  "seq": 7, "zone": "work", "slot": 7,
  "batch": { "id": 3, "index": 0, "of": 2 },
  "offset_ms": 86400000, "body": { ... }, "prev": "1220..." }
]]></sourcecode>

<ul spacing="compact">
<li><tt>seq</tt> -- the page's place in the one chain; page 0 is the opening;</li>
<li><tt>zone</tt> -- <tt>opening</tt>, <tt>work</tt>, <tt>transfer</tt> or <tt>coupling</tt>; <tt>slot</tt> -- the place
within the zone, from 1 (0 for the opening);</li>
<li><tt>batch</tt> -- the record the page belongs to: its number (<tt>id</tt>, the opening
is record 0), the page's place in it (<tt>index</tt>) and the record's page count
(<tt>of</tt>);</li>
<li><tt>offset_ms</tt> -- one value for the whole record;</li>
<li><tt>prev</tt> -- the hash of the previous page; for page 0, the container
identifier <xref target="KSG-CORE"></xref>.</li>
</ul>
<t>The page hash is the core document hash of the page.</t>

<section anchor="bodies"><name>Bodies</name>
<table>
<thead>
<tr>
<th>Body</th>
<th>Content</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>opening</tt></td>
<td><tt>container</tt>, <tt>mode</tt> (the form the journal opens in), <tt>agent</tt> and <tt>owner</tt> as <tt>{id, keys}</tt> -- pins both parties' keys</td>
</tr>

<tr>
<td><tt>content</tt></td>
<td><tt>kind</tt> (<tt>work</tt>, <tt>artifact_or_rights</tt>, <tt>shares_declared_lost</tt>, <tt>hash_binding</tt>), <tt>content</tt> (<tt>inline</tt> base64url bytes or <tt>digest</tt>), optional <tt>content_type</tt> URI</td>
</tr>

<tr>
<td><tt>manifest</tt></td>
<td>the service page of a multi-page record; always its last page</td>
</tr>

<tr>
<td><tt>transfer</tt></td>
<td>a hand-off between agents (work zone) or a change of owner (transfer zone)</td>
</tr>

<tr>
<td><tt>coupling</tt></td>
<td>one of the two coupling places</td>
</tr>

<tr>
<td><tt>rule</tt></td>
<td>the rule of two seals, set or lifted</td>
</tr>

<tr>
<td><tt>form</tt></td>
<td><tt>to</tt>: the form after this page</td>
</tr>

<tr>
<td><tt>repeat_confirmation</tt></td>
<td>the journal's confirmation of ten identical records</td>
</tr>
</tbody>
</table><t>A content piece commits as <tt>SHA-256(&quot;ksg:bound:inline:v1&quot; || bytes)</tt> or
<tt>SHA-256(&quot;ksg:bound:digest:v1&quot; || digest)</tt> -- plain concatenation, the label
being fixed. A record's hash is
<tt>SHA-256(F(&quot;ksg:bound:record:v1&quot;) || F(c1) || ... || F(cn))</tt> over the
commitments of its pieces in order, with <tt>F</tt> (length-prefixed field) as in
<xref target="KSG-CORE"></xref>. A record is at most 99 content pages and
204 800 bytes.</t>
</section>
</section>

<section anchor="seals"><name>Seals</name>

<sourcecode type="json"><![CDATA[{ "@context": "urn:keysingate:core:v3",
  "type": "JournalSeal", "v": 3,
  "container": "1220...", "number": 8, "batch": 3,
  "head": "1220...", "root": "1220...",
  "counts": { "work": 7, "transfer": 0, "coupling": 0 },
  "agent": "did:example:agent", "owner": "did:example:owner",
  "mode": "pro", "prev": "1220...", "offset_ms": 86400000,
  "signatures": [ ... ] }
]]></sourcecode>

<ul spacing="compact">
<li><tt>number</tt> -- the <tt>seq</tt> of the record's last page: every page written;</li>
<li><tt>head</tt> -- the hash of that page;</li>
<li><tt>root</tt> -- the Merkle tree head <xref target="RFC6962"></xref> over <strong>all</strong> pages from page 0,
each leaf being <tt>SHA-256(0x00 || page hash)</tt>;</li>
<li><tt>prev</tt> -- the hash of the previous seal; absent only on the opening's seal;</li>
<li><tt>rule</tt> -- present while the rule of two seals is in force: its hash;</li>
<li><tt>signatures</tt> -- by the current agent, Ed25519: a seal passes only if it is
present and valid under a key pinned in the journal (page 0 or the latest
transfer page), never a key the presenter brings. The post-quantum
signature is not on the seal: it is on the container as a whole, outside
this export (see Security Considerations).</li>
</ul>
<t>Every seal covers the whole chain before it, so a later seal confirms the
pages of every earlier record.</t>

<section anchor="further-seals"><name>Further Seals</name>

<ul spacing="compact">
<li><strong>Transfer seal</strong> (<tt>TransferSeal</tt>: <tt>page</tt>, <tt>subject</tt>, <tt>signatures</tt>): a
hand-off is sealed by both agents; a change of owner by agent and owner on
both sides. Never removed. The new agent replaces the old one; the old one
seals nothing afterwards.</li>
<li><strong>Co-seal</strong> (<tt>CoSeal</tt>: <tt>batch</tt>, <tt>subject</tt>, <tt>signatures</tt>): the owner's second
seal on a record. Required in the anchored form for <tt>artifact_or_rights</tt>,
and for every record while the rule of two seals is in force; a record that
needs it waits, and nothing else is written meanwhile.</li>
<li><strong>Coupling</strong>: each of the two places is written once; place 2 names the
coupled container and its last seal.</li>
</ul>
</section>
</section>

<section anchor="forms"><name>Forms</name>
<table>
<thead>
<tr>
<th>Form</th>
<th>Anchors</th>
</tr>
</thead>

<tbody>
<tr>
<td><tt>local</tt></td>
<td>none; a seal <bcp14>MUST NOT</bcp14> be anchored</td>
</tr>

<tr>
<td><tt>pro</tt></td>
<td>every seal <bcp14>MUST</bcp14> be anchored</td>
</tr>
</tbody>
</table><t>The form changes only by a <tt>form</tt> page sealed by the owner as well, in either
direction, as long as pages last. Going to <tt>pro</tt>, the seal of that page is the
first anchored one; an anchor made before the owner's seal on the change <bcp14>MUST</bcp14>
be rejected. Without the owner, a journal stays <tt>local</tt>.</t>
<t><strong>Force.</strong> A record has force when its seal is anchored. Two records' link is
confirmed when both are anchored and no unanchored record lies between them.
A report names the runs of consecutive anchored records.</t>
</section>

<section anchor="exports"><name>Exports</name>

<section anchor="final-export"><name>Final Export</name>
<t>The whole journal, once; the container works no more after it.</t>

<sourcecode type="json"><![CDATA[{ "@context": "urn:keysingate:core:v3", "type": "BoundJournalExport",
  "pages": [ ... ], "seal": { ... },
  "anchors": [ { "batch": 3, "anchor": { ... } } ],
  "transfers": [ ... ], "coseals": [ ... ] }
]]></sourcecode>
<t>Empty arrays are omitted.</t>
</section>

<section anchor="pointwise-export"><name>Pointwise Export</name>
<t>One page with its inclusion proof <xref target="RFC6962"></xref> under the current seal and, in
the anchored form, the seal's anchor:</t>

<sourcecode type="json"><![CDATA[{ "@context": "urn:keysingate:core:v3",
  "page": { ... }, "proof": { ... },
  "seal": { ... }, "anchor": { ... } }
]]></sourcecode>
<t>At most 12 pointwise exports per journal.</t>
</section>
</section>

<section anchor="verification"><name>Verification</name>
<t>Input: the export, the container identifier, the class (or the channel it
derives from, <xref target="KSG-CORE"></xref>), the agent's keys from the verifier's own source,
a profile, network readers.</t>
<t><strong>Final export.</strong> The verifier replays the journal from page 0 by the same
rules that wrote it:</t>

<ol spacing="compact">
<li>limits, then strict parsing of every document;</li>
<li>page 0 is the opening, <tt>prev</tt> is the container identifier, its pinned
agent keys equal the ones the verifier holds, its form agrees with the
class;</li>
<li>every page links to the previous one; zones, slots and records are in
place; every record's pages and service page agree;</li>
<li>every seal is recomputed from the pages (number, head, root, counts,
parties, form, rule, prev) and its signatures verified under the pinned
keys; transfer seals and co-seals where the rules require them;</li>
<li>in <tt>pro</tt>, every seal has an anchor, read by the Bound Rule; in <tt>local</tt>, no
anchor is accepted;</li>
<li>the report: number of pages, the current agent and owner, the form,
transfers, co-sealed records, each record with its seal and whether it is
anchored, the anchored runs with their bounds, and an explicit list of what
is not established.</li>
</ol>
<t><strong>Pointwise export.</strong> The page's hash is included under the seal's <tt>root</tt> at
<tt>seq</tt>; the seal's signatures verify under the agent keys the verifier holds;
in <tt>pro</tt>, the anchor is read by the Bound Rule. A pointwise export proves the
page and its time bound; it does not prove the pages around it.</t>
</section>

<section anchor="solana"><name>Solana Seal Profile</name>
<t>A seal is anchored as the memo of an ordinary Solana transaction (the SPL Memo
program). The anchor's <tt>type</tt> is <tt>urn:ksg:anchor:solana:seal:v1</tt>; its <tt>proof</tt>
names the transaction signature, slot and cluster. The memo is <strong>blind</strong> and of
fixed length:</t>

<artwork><![CDATA[ksg:bs:v1 <link> <commit>
link   = PRF(K, "ksg:bs:link:v1"   || prev)
commit = PRF(K, "ksg:bs:commit:v1" || seal)
PRF(K, x) = HKDF-SHA256(salt = "ksg:bs:v1", IKM = K, info = x),
            16 bytes, lowercase hex
]]></artwork>
<t><tt>seal</tt> is the seal's hash (32 bytes); <tt>prev</tt> is the seal's own <tt>prev</tt> -- the
previous seal's hash, whether or not that seal was anchored -- or the
container identifier for the opening's seal, which has no <tt>prev</tt>.</t>
<t><strong>Scan key.</strong> <tt>K</tt> is the holder's scan key for the calendar month (UTC) in
which the seal was made:</t>

<artwork><![CDATA[K(YYYY-MM) = HKDF-SHA256(salt = "ksg:bs:v1", IKM = master,
             info = "ksg:bs:period:v1" || len64(label) || label)
]]></artwork>
<t>with <tt>len64</tt> the label's length as 8 bytes big-endian. A key handed out reads
its month's seals and no other's. <strong>Scan keys are not part of any export or of
this document's public material</strong>: the holder gives one to a verifier together
with an export. Without it, an observer sees that a memo exists and nothing
else.</t>
<t><strong>Reader.</strong> The reader recomputes the memo from the seal and the key and finds
it in the named transaction; a different memo is a rejection. It asks the node
for its genesis hash and accepts only the cluster the verifier named. The
moment it returns is the block time -- a stake-weighted estimate at one-second
grain -- and it says so. A fork (two memos with one <tt>link</tt> and different
<tt>commit</tt>) is visible only to a holder of the key.</t>
<t><strong>Status records</strong> of <xref target="KSG-CORE"></xref> are anchored with an <strong>open</strong> memo,
because the status section is the part of a container that is meant to be
read:</t>

<artwork><![CDATA[ksg:st:v1 <link> <commit>
link   = SHA-256("ksg:st:link:v1"   || prev)
commit = SHA-256("ksg:st:commit:v1" || record hash)
]]></artwork>
<t><tt>prev</tt> is the record's own <tt>prev</tt>; both values are written as multihashes.</t>
<t>anchor <tt>type</tt> <tt>urn:ksg:anchor:solana:status:v1</tt>.</t>
</section>

<section anchor="security-considerations"><name>Security Considerations</name>
<t><strong>Signatures.</strong> Every seal carries Ed25519; a verifier <bcp14>MUST</bcp14> require it.
Keys come from the journal's own pinned pages and the verifier's source, never
from the presenter. The post-quantum signature (ML-DSA-65) is on the stored
container as a whole, not on each seal: an export, once it has left the
container, is protected by its recipient. Against a future forger able to
break Ed25519, an anchored seal is held by its anchor: a page that differs
from the anchored one does not hash to the anchored value.</t>
<t><strong>Time.</strong> Only an anchor read by the Bound Rule gives a bound; <tt>offset_ms</tt>
and anchor statements are not evidence. In the <tt>local</tt> form a journal proves
authorship and order, not time.</t>
<t><strong>Replay.</strong> A verifier replays the journal by the rules that wrote it; a
journal accepted by a verifier is one the writing rules would have produced.</t>
<t><strong>Blind anchors.</strong> The blind memo hides the chain from outsiders and with it
the visibility of forks: a fork is found only by someone holding the key. The
paying account remains visible and groups its memos.</t>
</section>

<section anchor="privacy-considerations"><name>Privacy Considerations</name>
<t>Pages may carry a digest instead of content, so an export can prove a record
without revealing it. The blind memo keeps a journal's existence and pace
from becoming a public time series; status memos are open by design.</t>
</section>

<section anchor="iana-considerations"><name>IANA Considerations</name>
<t>This document has no IANA actions. The anchor types
<tt>urn:ksg:anchor:solana:seal:v1</tt> and <tt>urn:ksg:anchor:solana:status:v1</tt> and the
memo tags <tt>ksg:bs:v1</tt> and <tt>ksg:st:v1</tt> are opaque, byte-compared strings; no
URN namespace is registered by this document.</t>
</section>

</middle>

<back>
<references><name>Normative References</name>
<reference anchor="KSG-CORE" target="https://datatracker.ietf.org/doc/draft-nam-ksg-core/">
  <front>
    <title>Keysingate Core: Channel Documents, Status Records and Anchored Time</title>
    <author fullname="Yevhenii Nam" initials="Y." surname="Nam"></author>
    <date year="2026" month="October"></date>
  </front>
  <seriesInfo name="Internet-Draft" value="draft-nam-ksg-core-00"></seriesInfo>
</reference>
<reference anchor="RFC2119" target="https://www.rfc-editor.org/info/rfc2119">
  <front>
    <title>Key words for use in RFCs to Indicate Requirement Levels</title>
    <author fullname="S. Bradner" initials="S." surname="Bradner"></author>
    <date year="1997" month="March"></date>
  </front>
  <seriesInfo name="BCP" value="14"></seriesInfo>
  <seriesInfo name="RFC" value="2119"></seriesInfo>
</reference>
<reference anchor="RFC6962" target="https://www.rfc-editor.org/info/rfc6962">
  <front>
    <title>Certificate Transparency</title>
    <author fullname="B. Laurie" initials="B." surname="Laurie"></author>
    <author fullname="A. Langley" initials="A." surname="Langley"></author>
    <author fullname="E. Kasper" initials="E." surname="Kasper"></author>
    <date year="2013" month="June"></date>
  </front>
  <seriesInfo name="RFC" value="6962"></seriesInfo>
</reference>
<reference anchor="RFC8174" target="https://www.rfc-editor.org/info/rfc8174">
  <front>
    <title>Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words</title>
    <author fullname="B. Leiba" initials="B." surname="Leiba"></author>
    <date year="2017" month="May"></date>
  </front>
  <seriesInfo name="BCP" value="14"></seriesInfo>
  <seriesInfo name="RFC" value="8174"></seriesInfo>
</reference>
</references>
<references><name>Informative References</name>
<reference anchor="RFC7942" target="https://www.rfc-editor.org/info/rfc7942">
  <front>
    <title>Improving Awareness of Running Code: The Implementation Status Section</title>
    <author fullname="Y. Sheffer" initials="Y." surname="Sheffer"></author>
    <author fullname="A. Farrel" initials="A." surname="Farrel"></author>
    <date year="2016" month="July"></date>
  </front>
  <seriesInfo name="RFC" value="7942"></seriesInfo>
  <seriesInfo name="DOI" value="10.17487/RFC7942"></seriesInfo>
</reference>
</references>

<section anchor="implementation-status"><name>Implementation Status</name>
<t>Per <xref target="RFC7942"></xref>. <tt>ksg-core-v2</tt> (Rust) writes and verifies the journal;
<tt>ksg-verify-v2</tt> verifies both exports and reads seal and status anchors from
Solana. The workspace runs 752 tests; the test vectors are regenerated
and compared byte for byte by the reference build.</t>
</section>

</back>

</rfc>
