| Internet-Draft | MUSUBI Settlement | October 2026 |
| Oga | Expires 25 April 2027 | [Page] |
MUSUBI settlement is a deterministic function that takes a contract signed by two parties, a set of signed records of what an agent did under that contract, and a view of Bitcoin block headers, and computes whether the recorded acts stayed within the grant the contract gives. Anyone holding the same bytes can recompute the result without trusting the party that produced it. This document specifies one well defined part of that function, settle core v0, precisely enough that a second implementation can be written from the text and the published test vectors alone. Settlement is not runtime enforcement and is not a judgement of liability.¶
This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79.¶
Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet-Drafts is at https://datatracker.ietf.org/drafts/current/.¶
Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress."¶
This Internet-Draft will expire on 25 April 2027.¶
Copyright (c) 2026 IETF Trust and the persons identified as the document authors. All rights reserved.¶
This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect to this document. Code Components extracted from this document must include Revised BSD License text as described in Section 4.e of the Trust Legal Provisions and are provided without warranty as described in the Revised BSD License.¶
A principal and a contractor sign a contract. The contract carries a grant: the actions the contractor is authorized to take, the actions it is prohibited from taking, the actions that need a separate approval from the principal, the parties to whom work may be delegated, an expiry height and a revocation policy. While the contractor (or a witness named in the grant) works, it signs records of what it did, and each record is committed to the Bitcoin block chain [BITCOIN] through an anchor. Later, anyone who holds the contract, the records and a view of the relevant block headers can settle: walk the records in the order the block chain gives them, compare each act with the grant, and produce a verdict, a status and a bond outcome.¶
In the terms of the agent auditing architecture [I-D.kuehlewind-audit-architecture], settlement is an Auditor's evaluation of whether recorded actions stayed within the authorization in force at the time; Section 17.1 maps the two.¶
The record schema names carry the prefix "a2a" because the format was designed for agents that interact over the Agent2Agent (A2A) protocol [A2A]. Settlement reads no A2A message and does not depend on A2A.¶
Until this document, the MUSUBI settle function was defined only by Python code (settle_v1.py to settle_v1_16.py, each layer importing the ones below it) [MUSUBI-REF]. A function that only one program can compute is a tool, not a protocol. This document states one well defined part of it, settle core v0, in plain language. A stranger should be able to write a second implementation from this text and the vectors alone, without reading the reference.¶
The text was written from the reference code, not from intent. Writing it against settle_v1_10.py found places where the code accepted input a careful second implementation would read differently, and one place where a record nobody signed could change the verdict (items N1 to N11 of [MUSUBI-NOTES]). settle_v1_16.py was written to close them, and this document describes it. Section 14 lists what changed. Where the reference still does something surprising, this document says so and marks it WRITTEN BEHAVIOUR. Written behaviour is still normative for core v0: a second implementation that differs on it will disagree with the vectors.¶
This document is an adaptation of [MUSUBI-SETTLE-CORE] into the Internet-Draft format. Where the two differ in meaning, that is an error in this document. This version follows revision 0.8 of that text. Revisions 0.1 to 0.3 answered questions and implementation notes from the first separately written implementation (Section 16.1); revision 0.4 checked which underspecified reasons the vectors actually produce (Section 15.3); revisions 0.5 to 0.8 corrected statements about the reference, stated two sentences a third implementation asked for, stated a consequence of the unsigned anchor, and named the commit of the reference's file list. No rule of the reference and no vector changed in any of them. Section 14.3 lists each place.¶
Settlement is not runtime enforcement. It never stops an agent from acting; it reads records after the fact and says whether the recorded acts were within the grant.¶
Settlement is not a judgement of liability or fault, and it does not make the contract a legal contract. The verdict is a function anyone can recompute from the same bytes. Section 18.5 lists what the verdict does not establish.¶
Settle core v0 is exactly what the reference computes when the contract carries none of the following:¶
requirements.spine (the spine gate, settle v1.8),¶
requirements.convergence (drawn measurers, settle v1.9),¶
grant.approval_policy.approvers (pinned approvers, settle v1.10),¶
grant.limits (which forces requirements.admission),¶
requirements.admission (settle v1.11 to v1.14 read it; the
admissions input is passed through unchanged).¶
It is computed in binding mode "strict" over exactly one header view.¶
Out of scope, and not defined here:¶
choosing between several header views (fork choice by work above the checkpoint),¶
binding mode "legacy" (records bound by contract_id only),¶
fetching anything. Settle core v0 never touches the network. Every key it uses is inside the contract.¶
A key is carried when it is present with a value other than null,
with two exceptions. requirements.spine and
requirements.convergence are carried whenever the key is present,
null included: their gates then answer spine_requirement_incomplete
and convergence_requirement_incomplete. And
grant.approval_policy.approvers is carried only when it is a well
formed approver list (rule 10 of Section 5.1); any other value
that is present, null and [] included, refuses the contract in
Section 5.1 (WRITTEN BEHAVIOUR). Vectors synth_limits_null,
synth_admission_null, synth_approvers_object,
synth_approvers_string, synth_approvers_zero.¶
If a contract carries any of the keys above, this document does not define the result. An implementation MUST NOT claim core v0 conformance for such an input.¶
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.¶
Most rules in this document are stated in the indicative ("is", "gives", "adds"). They are normative all the same: they determine the core fields (Section 10.1) that a second implementation MUST reproduce exactly.¶
A JSON object with schema a2a-contract-v0, signed by a principal
and a contractor, that carries the grant.¶
The two parties to a contract, identified by their role and holding
the Ed25519 [RFC8032] keys pinned in the contract.¶
A key named in grant.witnesses that may sign execution records.¶
An execution (a2a-execution-v0), a revocation
(a2a-revocation-v0) or a revocation acknowledgement
(a2a-revocation-ack-v0).¶
A list of raw 80 byte Bitcoin block headers [BITCOIN-HEADERS] with their heights.¶
A NENRIN walk record [NENRIN-VERIFIER] cited by an execution
through nenrin_ref.¶
settle_v1_16.py and the files it imports, at the versions listed in Section 16 [MUSUBI-REF].¶
A behaviour of the reference that may surprise a reader, kept because the vectors pin it, or would.¶
A JSON value written as an integer token: no fraction, no exponent.
true and false are never integers.¶
The following notation is used for byte strings. A string in double
quotes, such as "a2a-contract-v0\n", denotes the ASCII bytes between
the quotes, where \n stands for one line feed (0x0A). When A and B
are byte strings, A + B is their concatenation. sha256(x) is SHA-256
[FIPS180-4] of the bytes x. Byte ranges such as "bytes 4 to 36" are
zero-based and exclude the upper bound, so they name the 32 bytes at
offsets 4 through 35. "Lowercase hex" means the characters 0 to 9
and a to f, two per byte.¶
contract: one JSON object (schema a2a-contract-v0).¶
events: a list of JSON values. Each is meant to be a record: an
execution (a2a-execution-v0), a revocation (a2a-revocation-v0)
or a revocation acknowledgement (a2a-revocation-ack-v0). Anything
else is ignored. Order carries no meaning.¶
view: one JSON object
{"headers": [{"height": H, "hex": "<160 lowercase hex>"}, ...]},
raw 80 byte Bitcoin block headers. Other keys are ignored.¶
nenrin_records: optional list of JSON values (walk records cited
by executions). Absent and empty mean the same thing. A value that
is not a list is treated as absent.¶
parent: optional, the parent contract, needed only when
contract.parent_contract is present (Section 5.3).¶
If contract is not an object, the settlement is underspecified with
the single reason contract_not_verified. If events is not a list,
it is underspecified with input_not_settleable. Nothing else is
computed in either case.¶
Keys are not a separate input. The principal's and contractor's
Ed25519 public keys are parties[i].public_key_ed25519_b64 inside the
contract, and witness keys are grant.witnesses[i].public_key_ed25519_b64.
key_url is never fetched. Checking that the key served at key_url
equals the pinned key is outside settle core.¶
Inputs are parsed as JSON [RFC8259] with one extra rule: an object
with a duplicate key is an error and no settlement is produced. Text
nested deeper than 512 levels of [ or { (counted outside strings)
is also an error.¶
Numbers MUST be integers within plus or minus (2^53 - 1). The contract is checked for this in Section 5.1, records in Section 7.1, the view in Section 6.1 and walks in Section 12. A JSON value with a non-integer number is not "strict" in the sense of Section 7.1.¶
The tokens NaN, Infinity and -Infinity are read as numbers that
are not integers (WRITTEN BEHAVIOUR of the reference's reader,
Python's json module). A value holding one is therefore not strict. It
is not a parse error. Vectors synth_nan_in_signed_body,
synth_infinity_in_signed_body,
synth_negative_infinity_in_signed_body. Exactly these three
spellings are read, case sensitive. nan, +Infinity or infinity
is a parse error, and no settlement is produced. A number token whose
value is too large for a double (for example 1e400) is read as an
infinity, so it too is a non-integer number.¶
An integer token longer than 4300 digits is an error, and no settlement is produced: the reader raises (WRITTEN BEHAVIOUR). There is no vector, because there is no settlement to compare.¶
A JSON value is an "integer" in this document only if it is written
as an integer token: no fraction, no exponent. true and false are
never integers.¶
These rules are close to, but not the same as, I-JSON [RFC7493].¶
Compute contract_sha256 (Section 4.3) and bind records by it (Section 7.2). Triage every bound record (Section 7.1): records not strictly signed by a party are rejected or set aside here, and no later step sees them. Check the contract's terms (Section 5) and verify the header view (Section 6). Every problem found so far is collected; none of them stops the other checks in this paragraph. If a problem was found in Section 5.1, Section 5.2 or rules 1 to 8 of Section 6.1, no record is examined further. Otherwise: collapse copies of one signed body (Section 7.5), check conformance (Section 7.7), check anchors (Section 8). Records that pass are the event set, ordered by anchor height then record sha256 (Section 9). Walk the event set against the grant (Section 11), add the NENRIN checks (Section 12), and decide the verdict (Section 13), status (Section 9.5) and bond outcome (Section 10.3). The checks of Section 5.3, rule 9 of Section 6.1, Section 7.1 (a record that fails closed), Section 8.4 (below the lower bound) and Section 12 (a cited walk) are collected in the same way and make the settlement underspecified; they never stop the computation of the event set.¶
The output is one JSON object. Section 10 lists the core fields a second implementation MUST reproduce exactly. Everything else the reference writes is informative.¶
canonical(v) is the UTF-8 [RFC3629] encoding of this text:¶
Objects: keys sorted by Unicode code point, at every level.
Written {"k":v,...} with no whitespace.¶
Arrays: [a,b,...] with no whitespace.¶
Strings: written between ". Escape only " as \", \ as \\,
and U+0000 to U+001F. Of those, U+0008 is \b, U+000C is \f,
U+000A is \n, U+000D is \r, U+0009 is \t, and the rest are
\u00xx with lowercase hex. Every other character is written as
itself, including U+007F, U+2028, U+2029 and all non-ASCII. A lone
surrogate code unit (U+D800 to U+DFFF not part of a valid pair) is
written \udxxx with lowercase hex.¶
Integers: decimal, - only for negatives, no leading zeros, no
fraction, no exponent.¶
true, false, null as literals.¶
A number that is not an integer can only occur in a value that is
not strict, but such a value can still be signed (Section 7.1 needs
its signing bytes). It is written as Python's json module writes
it: NaN, Infinity, -Infinity, and a finite float as Python's
repr (1.5, 1e+16, 1.0). Not nan, not null. A signature
over a body holding one verifies only over these spellings (vectors
synth_nan_in_signed_body, synth_infinity_in_signed_body,
synth_negative_infinity_in_signed_body).¶
The reference ships fixed inputs for this rule (musubi-v0/canonical_vectors.json) and a Node twin (canonical_v0.mjs) [MUSUBI-REF].¶
This form is not the JSON Canonicalization Scheme [RFC8785]. Among other differences, RFC 8785 sorts keys by UTF-16 code units, which gives a different order for keys with characters above U+FFFF, and serializes non-integer numbers by the rules of ECMAScript, while this form writes a non-integer number only inside a value that is not strict, in the spellings of item 6, and does so only so that a signature over such a value can be checked.¶
Base64 is standard base64 (Section 4 of [RFC4648]) with padding, and MUST be canonical: decoding then re-encoding gives the same text. A public key is 32 bytes after decoding, a signature 64 bytes. Any other length, any non-canonical text, or any non-string is "unusable".¶
A public key is also unusable if it is not the canonical encoding of a point on edwards25519, if it is the identity point, or if the point is not in the prime order subgroup (small order or mixed order points).¶
| name | definition |
|---|---|
| contract signing bytes |
"a2a-contract-v0\n" + canonical(contract without its "signatures" key)
|
| contract_sha256 | lowercase hex sha256 of the contract signing bytes |
| record_sha256 | lowercase hex sha256 of canonical(record), the whole record including anchor and signatures
|
| body digest | sha256 of canonical(record without its "anchor" key). It includes signatures. Anchor proofs start from these 32 bytes |
| record signing bytes | context + canonical(record without "signatures" and without "anchor"). Context is "a2a-execution-v0\n", "a2a-revocation-v0\n" or "a2a-revocation-ack-v0\n" by the record's schema |
contract_sha256 is the identity of the terms. It does not change when a signature is added. A record is bound to a contract only by it (Section 7.2).¶
verify(pub_b64, sig_b64, message) returns one of: true, false,
unusable. It returns unusable when the key or signature fails
Section 4.2. Otherwise it is Ed25519 verification ([RFC8032]) of
the 64 byte signature over the message bytes. The reference uses the
Python cryptography package (OpenSSL). See item N9 of
[MUSUBI-NOTES] on libraries that differ for adversarial signatures.¶
A 64 byte signature is "strict" when its last 32 bytes, read as a little-endian integer S, are below the group order L = 2^252 + 27742317777372353535851937790883648493, and its first 32 bytes R pass the public key test of Section 4.2 (a canonical encoding of a point of prime order, not the identity). "Strictly verifies" means the signature is strict and verify returned true. Libraries that disagree on lax signatures all agree on strict ones.¶
Everywhere below, "verifies" means verify returned true. False and unusable are both "does not verify", except in the contract door (Section 5.1), which names them differently.¶
The contract is accepted only if every check below passes. Any failure
gives the single underspecified reason contract_not_verified
(Section 5.2 is then skipped). The reference reports a list
of refusal codes alongside; those codes are informative. Only
contract_not_verified is core.¶
schema is "a2a-contract-v0".¶
contract_id and nonce are each 32 lowercase hex characters.¶
parties is a list of exactly two objects whose role values are
exactly one "principal" and one "contractor".¶
Each party's domain normalizes to a bare hostname: leading and
trailing whitespace removed (every character Python's str.strip()
removes, not only U+0020), then lowercased, then one or more
trailing dots removed. The result has no /, @, : or space, at
least two labels, each label matching
^[a-z0-9]([a-z0-9-]*[a-z0-9])?$. The two normalized domains
differ.¶
Each party's key_url is an https:// URL. (Whether its host is
under the domain is a finding, not a refusal.)¶
Each party's public_key_ed25519_b64 decodes as canonical base64
to 32 bytes. (The point check of Section 4.2 is applied later,
when signatures are verified.)¶
task is an object with a non-empty string purpose.
task.payload_digest, when present and not null, is 64 lowercase
hex.¶
grant is an object. Its keys are only: authorized_actions,
prohibited_actions, conditional, delegation, data_access,
max_hops, privacy, revocation, finality, witnesses,
expiry_height, ordering, approval_policy, limits. Any other
key refuses the contract.¶
grant.authorized_actions and grant.prohibited_actions are both
lists of non-empty strings, and no action is in both.¶
Typed grant keys, each checked only when present and not null:¶
data_access: list of non-empty strings.¶
max_hops, expiry_height: integer >= 0.¶
delegation: object with exactly one key, allowed, a list of
non-empty strings. {} refuses the contract (vector
synth_delegation_empty_object).¶
conditional: list of objects, each with a non-empty string
action; actions unique; requires, when present, a string.¶
finality: object with keys only from depth,
max_target_bits. depth, when present, integer >= 1.
max_target_bits, when present, 8 lowercase hex that decodes
(Section 6.2) to a positive target.¶
witnesses: list of objects, each with a non-empty string
name and a public_key_ed25519_b64 that decodes to 32 bytes.¶
privacy: non-empty string.¶
ordering: object with no key other than same_height, which
is optional ({} is accepted, vector
synth_ordering_empty_object); same_height, when present, a
string or null.¶
approval_policy: object with keys only from allow_unscoped
(boolean) and approvers. approvers, when present, must be a
list of 1 to 16 objects, each with keys only from name,
public_key_ed25519_b64 and actions: a non-empty string
name, a canonical 32 byte public_key_ed25519_b64, names and
keys unique across the list, and actions a non-empty list of
distinct non-empty strings, each the action of an entry of
conditional. Anything else refuses. A list that passes is
carried and out of scope (Section 1.4).¶
revocation: an object whose effective_at is "anchor" or
"delivery_ack", with keys only from effective_at,
ack_window; ack_window, when present, integer >= 1.¶
No number anywhere in the contract (signatures included) is a non-integer, NaN, an infinity, or an integer outside plus or minus (2^53 - 1).¶
Every object key anywhere in the contract is printable ASCII (U+0020 to U+007E).¶
establishes is a non-empty list. No string in it matches,
case-insensitively, any of the patterns listed after this list.¶
does_not_establish is a non-empty list. Lowercased, its strings
together contain: one of runtime or enforce; one of judge,
liability or fault; deviat; and legal. Each of the four
groups must be hit by at least one string.¶
signatures is a list. For each object in it: its domain
normalizes (rule 4) to a party's normalized domain, or the contract
is refused. Its signature is verified (Section 4.4) with that
party's key over the contract signing bytes. False or unusable
refuses the contract, even if the same party also has a good
signature (WRITTEN BEHAVIOUR). alg is not read.¶
Each party has at least one signature that verifies.¶
The patterns of rule 13, one per line, are:¶
\bperformed\b \bdeliver(ed|y)\b \bmoney (moved|was sent)\b \bfunds? (moved|were sent|were transferred)\b \bpaid\b(?!\s+for\s+(this|the)\s+record) \bpayment (was|has been) (made|completed|settled|received)\b \bcontract\b \bbinding\b \bguarantee \bescrow\b \bcustody\b \bsolvent\b \blawful\b \bfair\b \bcertified\b
Not read by the door, and not read anywhere in core v0: expiry (a
wall clock), agreed_at, requirements.recovery,
selection_provenance, liability_boundary, settlement.
lower_bound is not checked here; Section 6 checks it.
requirements.evidence is read in Section 12, bond and
parent_contract in Section 5.3.¶
Only when Section 5.1 accepts, each of these adds the underspecified reason shown:¶
| condition | reason |
|---|---|
grant.authorized_actions is not a list |
authorized_actions_missing
|
an action name in authorized_actions, prohibited_actions or conditional[].action does not match ^[a-z][a-z0-9_.:-]{0,63}$ (see Section 14 and item N1 of [MUSUBI-NOTES]) |
grant_action_name_invalid
|
| more than 32 witnesses |
too_many_witnesses
|
| a witness key does not decode to 32 bytes |
witness_key_invalid
|
| two witnesses share a name, or any two of {principal key, contractor key, each witness key} decode to the same 32 bytes |
key_conflict
|
grant.ordering.same_height is present and is neither null nor "conservative"
|
tie_policy_refused
|
grant.revocation is an object and effective_at is not "anchor" or "delivery_ack"
|
revocation_policy_not_recomputable
|
effective_at is "delivery_ack" and ack_window is not an integer >= 1 |
ack_window_missing
|
grant.finality.depth is not an integer >= 1 (including a missing finality or depth) |
finality_policy_missing
|
grant.expiry_height is present, not null, and not an integer >= 0 |
bad_expiry_height
|
allow_unscoped in grant.approval_policy is truthy |
unscoped_approvals_refused
|
An empty authorized_actions list means nothing is authorized. It is
not an error.¶
Four rows of this table cannot add their reason in core v0:
authorized_actions_missing, witness_key_invalid,
revocation_policy_not_recomputable and bad_expiry_height. Rules 9
and 10 of Section 5.1 refuse the same inputs first, and this
table applies only when Section 5.1 accepts, so the input
settles as contract_not_verified. The rows are kept so that no code
changes meaning; an implementation that never emits these four agrees
with the reference (revision 0.4, exercise_inputs.py cases D1 to D6,
Section 15.3).¶
Whether or not Section 5.1 accepts, each of these adds the underspecified reason shown:¶
| condition | reason |
|---|---|
a signatures[].signature decodes to 64 bytes and is not strict (Section 4.4) |
contract_signature_not_strict
|
bond is present and not null, and is not an object whose amount is an integer from 1 to 2^53 - 1 (true is not an integer) |
bond_malformed
|
parent_contract is present and not null, and no parent was supplied |
parent_not_supplied
|
parent_contract is present and not null, and the parent is not an object, or does not pass Section 5.1 on its own, or the contract does not pass Section 5.1 with the parent supplied (the reference's door then also checks that parent_contract.contract_id equals the parent's contract_id and that the child's grant is a subset of the parent's: contract_v0.grant_subset, stated in Section 5.3.1) |
parent_narrowing_failed
|
These checks do not stop the walk; the event set and dispositions are
still computed (Section 3.3). In the last row, "does not pass
Section 5.1 with the parent supplied" is judged by
Section 5.3.1 and adds only parent_narrowing_failed; whether
the settlement is contract_not_verified is decided by
Section 5.1 on the contract on its own (vectors
synth_parent_full_*).¶
This applies when parent_contract is present and not null. The
contract passes Section 5.1 with the parent supplied if and only
if it passes Section 5.1 on its own, parent_contract is an
object whose contract_id equals the parent's contract_id (a
missing contract_id fails), and the child grant C is within the
parent grant P. C is within P when none of the following holds. In
each rule, a list that is absent or null counts as empty, "has an
integer" means the key is present with an integer that is not a
boolean, and "the parent sets" a key means the key is present with a
value other than null. Both grants have already passed rule 10 of
Section 5.1, so every value has its declared type.¶
An entry of C.authorized_actions is not in
P.authorized_actions.¶
An entry of P.prohibited_actions is not in
C.prohibited_actions.¶
An entry of C.data_access is not in P.data_access. A child
without data_access is narrower, not wider.¶
P has an integer max_hops p, and C has no integer max_hops or
one above p - 1. A child that copies the parent's max_hops is not
within (vector synth_parent_full_identical), and under a parent
with max_hops 0 no child is within.¶
An entry of C.delegation.allowed is not in
P.delegation.allowed.¶
For each entry of P.conditional, with a its action and r its
requires (a missing requires is null, on either side): skip it
when either (i) a is in C.prohibited_actions, or (ii) a is
not in C.authorized_actions and is not the action of any entry
of C.conditional (the child cannot do a at all). Otherwise it
is a violation if no entry of C.conditional has the action a,
or the one that has it (actions are unique, rule 10 of
Section 5.1) has a requires other than r. An action that C
both authorizes and makes conditional is judged by this rule like
any other.¶
The action of an entry of C.conditional is in neither
P.authorized_actions nor the actions of P.conditional.¶
P has an integer expiry_height e, and C has no integer
expiry_height or one above e.¶
P has a revocation object; let m be its effective_at. Then C
must have a revocation object with an effective_at. If m is
"anchor", C's must be "anchor". If m is "delivery_ack", C's
may be "anchor" (authority ends sooner) or "delivery_ack"; in
the second case, when P has an integer ack_window w, C must have
an integer ack_window of at most w (a child without one is a
violation), and when P has none, C's ack_window is not compared.
Anything else is a violation.¶
P has a finality object. Then C must have one; when P has an
integer depth d, C must have an integer depth of at least d;
when P's max_target_bits decodes (Section 6.2), C's must
be present and decode to a target no larger (absent, or not
decoding, is a violation). Both grants passed rule 10 of
Section 5.1, so a target that is present is positive.¶
P has a witness with a string public_key_ed25519_b64. Then C
must have at least one witness with such a key, and every such key
of P must equal, as a string, the key of one of C's witnesses.
Witnesses are compared by key; the names are labels (vector
synth_parent_full_witness_renamed).¶
For each of privacy and ordering separately: when the parent
sets it, C must set it to an equal JSON value. After rule 10 of
Section 5.1 these values hold only strings, objects and
null, so equality is exact: {} is not equal to
{"same_height": null}. No order is defined for these two, so
they are held equal. An ordering of {"same_height": null}
counts as set (vector synth_parent_full_without_ordering).¶
C.approval_policy.allow_unscoped is true and
P.approval_policy.allow_unscoped is not (absent counts as not
true). (In core v0 a true allow_unscoped already gives
unscoped_approvals_refused, Section 5.2.)¶
The pinned approvers differ: the parent's
approval_policy.approvers and the child's, each taken as the
empty list when absent, compared as sets of canonical entries. The
order of the entries does not matter; each entry is compared as
its canonical bytes (Section 4.1), so the order of the strings
in its actions does, as for any array. A child that carries
approvers is out of scope (Section 1.4), so in core v0 this applies
when the parent pins approvers and the child has none: a violation
(vector synth_parent_approvers_child_drops). No core v0 input
can reach the order of actions.¶
The parent must also pass Section 5.1 on its own. Vectors
synth_parent_full_* take a parent that sets every axis and change
one axis per vector.¶
The view is checked under two contract terms: the difficulty floor
grant.finality.max_target_bits and the checkpoint lower_bound
(height, hash). Any failure below gives the underspecified reason
chain_view_rejected. The view is checked even when Section 5
already failed.¶
grant.finality.max_target_bits is 8 lowercase hex and decodes
(Section 6.2) to a target, the floor.¶
lower_bound is an object with an integer height and a 64
lowercase hex hash.¶
view.headers is a non-empty list.¶
Each header is an object with an integer height >= 0 and a hex
of 160 lowercase hex characters, optionally followed by one line
feed (WRITTEN BEHAVIOUR of the lower layers; rule 9 below then
refuses the line feed under its own reason).¶
Heights are consecutive in list order: each is the previous plus one.¶
From the second header on, bytes 4 to 36 of the header, reversed, as hex, equal the previous header's hash.¶
The header's own nBits (bytes 72 to 76, little-endian unsigned 32 bit) decode to a target; that target is not above the floor; the header hash read as a big-endian 256 bit number is not above that target.¶
The view holds a header at lower_bound.height whose hash equals
lower_bound.hash.¶
The header hash is sha256(sha256(80 raw bytes)), byte-reversed, as lowercase hex (the usual display order). The merkle root used by anchors (Section 8.5) is bytes 36 to 68 of the raw header, not reversed. The tip is the height of the last header. The view may start below the checkpoint. The first header's previous-hash field is not checked. Consensus rules beyond these (retargeting, timestamps, transactions) are not checked.¶
Separately, when view is an object whose headers is a list,
every header is an object with an integer height and a hex of
exactly 160 lowercase hex characters, and every string, key and
number in the view is strict (first paragraph of Section 7.1). If
not, the reason view_not_strict is added. A header in upper case
therefore gives both chain_view_rejected and view_not_strict;
one that ends in a line feed gives only view_not_strict. A view
that is not an object, or whose headers is not a list, gives
chain_view_rejected only (vectors synth_view_not_an_object,
synth_view_headers_not_a_list). Rule 9 does not stop the walk.¶
If the view verifies but holds more than 200000 headers, the reason
is input_too_large instead, and no record is examined.¶
For a 32 bit value bits: exp = bits >> 24,
mant = bits & 0x007fffff. If bits & 0x00800000 is set, or mant
is 0, there is no target (invalid). Otherwise the target is
mant << (8 * (exp - 3)) when exp >= 3, else
mant >> (8 * (3 - exp)).¶
A JSON value is strict when, at every depth up to 512 levels: every string and every object key is free of U+0000 to U+001F and U+007F; every key is ASCII; every number is an integer within plus or minus (2^53 - 1). A value nested deeper is not strict.¶
A record's parts:¶
the body: the record without signatures and anchor. This is what
its signers signed (Section 4.3, record signing bytes).¶
the anchor: anchor. Nobody signed it.¶
the signatures list: signatures. Nobody signed it either.¶
The body is not strict when the body as a value is not strict, or
(when present) nenrin_ref is neither null nor 64 lowercase hex, or
contract_ref.contract_sha256 or contract_ref.payload_digest is
present and not 64 lowercase hex, or, for an execution:
performed_actions or delegated_to is present and not a list of
strings; approvals is present and not a list of objects; or an
approval has a sig_b64 or a signature that decodes to 64 bytes and
is not strict (Section 4.4). Each of the two keys is checked on its
own, whichever are present: a strict sig_b64 does not excuse a
signature that is not strict. This is the only place an approval's
signature is read; Section 11.4 never verifies it.¶
The anchor is not strict when it is present and: it is not strict as
a value; or its block_hash is present and not exactly 64 lowercase
hex; or a proof operation's hex is present and is not exactly pairs
of lowercase hex digits (the empty string is allowed).¶
A record is strictly signed when one entry of signatures has a role
its schema allows (Section 7.6), a signature that strictly
verifies (Section 4.4) with the matching key over the record signing
bytes, and is itself a strict value.¶
First, a record of one of the three schemas whose record_sha256
cannot be computed at all (for example an integer too long to write
out) is set aside with reason record_not_strict. The others are
bound (Section 7.2). Each bound record is then triaged, in this order:¶
| condition | result |
|---|---|
| not strictly signed, body not strict | set aside, record_not_strict
|
| not strictly signed, but a signature verifies under Section 4.4 alone (lax) | set aside, signature_not_strict
|
| not strictly signed otherwise | rejected (disposition rejected) |
| strictly signed, body not strict | set aside, record_not_strict, and the underspecified reason record_not_strict is added: the record is a party's own statement, so setting it aside does not make the input settleable |
| strictly signed, anchor not strict, and another bound record with the same record signing bytes is kept by this table and anchors validly (Section 7.5) | set aside, anchor_not_strict
|
| strictly signed, anchor not strict, otherwise | underspecified reason anchor_not_strict
|
a revocation, and grant.revocation is not an object |
set aside, revocation_not_provided_for (the contract cannot be revoked) |
| otherwise | kept |
Only the fourth row both sets a record aside and adds a reason; the
first three set aside or reject without one (vectors
synth_nan_in_signed_body and synth_unsigned_float_beside_good).¶
Set aside and rejected records are never examined again: no collapse (Section 7.5), size limit (Section 7.3), conformance check, anchor check or NENRIN check sees them. They are listed with their disposition (Section 10.2). Problems in the signatures list of a strictly signed record (an extra entry that does not verify or is not strict) are ignored.¶
A third party holding copies of genuine records can change their anchors and signature lists but not their bodies. The table is arranged so that such copies are set aside or rejected whenever the genuine record is in the input.¶
Let cid = contract.contract_id, pdg = contract.task.payload_digest
(it may be absent), csha = contract_sha256. Walk events in the
given order. Skip anything that is not an object, or whose schema is
not one of the three record schemas. Skip a record whose record_sha256
was already seen (identical bytes are one record). Then, with
ref = contract_ref if it is an object, else empty:¶
| condition, in this order | class |
|---|---|
ref.contract_sha256 absent or null, and ref.contract_id == cid
|
unbound (not settled in strict mode) |
ref.contract_sha256 absent or null otherwise |
not ours: dropped silently |
ref.contract_sha256 is not 64 lowercase hex |
inconsistent |
ref.contract_sha256 != csha
|
foreign |
ref.contract_id != cid
|
inconsistent |
pdg is present and not null, and ref.payload_digest != pdg
|
inconsistent |
| otherwise | bound |
Only bound records go further. Foreign, inconsistent and unbound
records are listed (Section 10) and never settled. Binding runs even
when Section 5 or Section 6 failed; its classes are then still
reported. Binding is by contract_sha256 alone, so a record made
under earlier terms with the same contract_id is foreign and is
never triaged (Section 7.1).¶
If more than 10000 records are kept by Section 7.1, the reason
input_too_large is added and no record is examined further. Records
are counted after identical bytes are merged (Section 7.4):
10001 copies of one record are one record (revision 0.4,
exercise_inputs.py cases E4 to E6, Section 15.3).¶
Two inputs with the same record_sha256 are one record. Supplying a record twice changes nothing.¶
The same signed body can reach the input more than once: anchored
twice (two calendars, two batches), or with another signatures list.
These copies differ only in their unsigned parts. Group the records
kept by Section 7.1 by their record signing bytes (Section 4.3). In a
group with more than one distinct record_sha256:¶
A copy "anchors validly" if: its anchor.height is an integer >= 0
that the view holds; anchor.block_hash equals the view's hash at
that height; the proof (Section 8.5) reaches that block's
merkle root; and the batch leg rule (Section 8.6) accepts it.¶
If at least one copy anchors validly, keep the one with the smallest
(height, record_sha256) and drop the others. Dropped copies are
listed (disposition duplicate_dropped).¶
If none does, keep them all. Each is then examined on its own.¶
This is done when rules 1 to 8 of Section 6.1 passed, whether or not Section 5 did. Note that "anchors validly" does not compare the height with the checkpoint (see item N7 of [MUSUBI-NOTES]).¶
Authentication is part of Section 7.1: only strictly signed records are kept. The roles and keys are:¶
| schema | signer it needs | key |
|---|---|---|
| a2a-execution-v0 | a signature with role "contractor", or with role "witness" and a witness_name
|
the contractor's party key, or the key of the grant witness with that exact name |
| a2a-revocation-v0 |
role "principal"
|
the principal's party key |
| a2a-revocation-ack-v0 |
role "contractor"
|
the contractor's party key |
signatures must be a non-empty list. Each entry is an object with
role, sig_b64 and, for a witness, witness_name. An entry with
another role, or one that does not strictly verify, is skipped.¶
Then, for a kept revocation: if its revoked_by field is not
"principal", it is ignored (disposition ignored), even though the
principal signed it.¶
Allowed top-level fields:¶
execution: schema, contract_ref, performed_actions,
approvals, delegated_to, nenrin_ref, outcomes, anchor,
signatures,¶
revocation: schema, contract_ref, revoked_by, anchor,
signatures,¶
acknowledgement: schema, contract_ref, revocation_sha256,
acked_by, anchor, signatures.¶
A record has a conformance problem if:¶
it has a field outside its list,¶
(execution) performed_actions is not a list of strings,¶
(execution) outcomes is present, not null, and not an object; or
one of its keys is not in performed_actions; or one of its values
is not "ok", "error", "timeout" or "partial",¶
(execution) it lists more than 256 performed actions,¶
(execution) a performed action, or the action of an approval that
is an object, does not match ^[a-z][a-z0-9_.:-]{0,63}$. An
approval object with no action counts as not matching. (A name
ending in a line feed never gets here: its body is not strict,
Section 7.1.)¶
If an execution with a problem has a contractor signature that
verifies, it is a charge: the deviation nonconforming_record is
added, and the record continues to Section 8. Its acts are still
judged. Any other record with a problem is rejected (disposition
rejected). The computation is then repeated without the rejected
records, until no new record is rejected, so no check reads a
rejected record.¶
The types of approvals, delegated_to, nenrin_ref and
contract_ref are checked in Section 7.1.¶
An anchor says where a record's body was committed to Bitcoin:¶
"anchor": {"height": H, "block_hash": "<64 hex>", "proof": [op, ...]}
¶
The anchor is not signed. Anyone holding a record can change it. The checks below decide whether the claim holds against the verified view.¶
The height of a record is anchor.height when anchor is an object
and height is an integer >= 0. Otherwise the record has no height.¶
For each record that passed Section 7, apply Section 8.4, Section 8.5 and Section 8.6 in this order. The first failure decides what happens to the record. Every record that reaches this section is strictly signed by a party (Section 7.1).¶
| condition, in this order | result |
|---|---|
no height (Section 8.2), or anchor.block_hash is not 64 lowercase hex |
underspecified reason anchor_incomplete
|
height < lower_bound.height
|
underspecified reason anchor_below_lower_bound. The body names this contract, and the contract names the checkpoint's block hash, so no honest anchor can be below it |
| height > tip, or the view holds no header at that height | underspecified reason anchor_beyond_observed_chain
|
anchor.block_hash != the view's hash at that height |
disposition orphaned: excluded, no reason added, but its height counts for finality (Section 9.5) |
anchor.proof must be a non-empty list of at most 512 operations.
Start with the 32 byte body digest (Section 4.3) as the current bytes,
and apply each operation:¶
| op | effect |
|---|---|
{"op": "sha256"}
|
current = sha256(current) |
{"op": "hexlify"}
|
current = the ASCII bytes of the lowercase hex of current |
{"op": "append", "hex": X}
|
current = current + bytes(X) |
{"op": "prepend", "hex": X}
|
current = bytes(X) + current |
X is a string of pairs of lowercase hex digits (the empty string is
allowed) of at most 4096 bytes, and bytes(X) is the byte string that X
encodes. (An operand with anything else, a line feed included, never
gets here: the anchor is not strict, Section 7.1.) Any other op, a
missing or malformed operand, a non-object entry, or current bytes
longer than 65536 at any step, fails the proof. The proof holds if the
final bytes equal the merkle root of the header at the record's
height (Section 6.1). If it fails: underspecified reason
anchor_proof_invalid.¶
The operation set resembles a subset of the operations used by OpenTimestamps [OPENTIMESTAMPS]; only the operations above are defined here.¶
A ledger lists records by their hex sha inside a batch, and stamps
the batch. A proof can pass through such a batch with hexlify. A hex
digest that merely appears somewhere in stamped bytes is not a
listing. So:¶
If no op in the proof is hexlify, the rule accepts.¶
If any hexlify is not the first op, or there is more than one,
refuse (hexlify_not_leading).¶
Rebuild the batch: walk the ops after the first one, up to the
first sha256. Each must be append or prepend, else refuse
(batch_leg_malformed). If there is no sha256, refuse
(batch_leg_malformed). The batch is (all prepend operands, the
later one in front) + the ASCII lowercase hex of the body digest +
(all append operands, in order).¶
The batch bytes must be UTF-8 text that parses as strict JSON
(Section 3.2; so NaN is read as a number, vector
synth_batch_bytes_with_nan) into an object whose schema is one of
nenrin-agreement-batch-v1 (typed), nenrin-witness-batch-v1,
nenrin-trace-pin-batch-v0. Else refuse (batch_not_json or
batch_schema_unknown).¶
records is a list of objects (else batch_schema_unknown). If
count is present it equals the length of records (else
batch_count_mismatch).¶
Exactly one records[i] has sha equal to the hex digest. None:
record_not_listed_in_batch. More: record_listed_twice.¶
For a typed batch: the record's schema maps to a kind
(a2a-execution-v0 to execution; revocations and
acknowledgements have no kind, so they are refused with
record_kind_unknown, WRITTEN BEHAVIOUR). records[i].kind equals
that kind (else record_kind_mismatch). If records[i].schema is
present it equals the record's schema (else
record_schema_mismatch).¶
The string value of records[i].sha must be written in the raw
text as the 64 hex characters themselves, with no escapes (else
refuse, code internal), and its first character must sit at byte
offset equal to the total length of the prepend operands (else
refuse, batch_leg_splice_not_at_listing).¶
If the rule refuses: underspecified reason anchor_batch_leg_refused.
The refusal code is informative detail.¶
If all of Section 8.4 to Section 8.6 pass, the record is
accepted (disposition accepted).¶
The event set is the accepted records sorted by (height, record_sha256 as a lowercase hex string). Arrival order plays no part.¶
Two records in the same block are not ordered by their bytes for any rule that matters. Section 11 says how same-block cases are decided. The order of Section 9.1 only fixes the listing and the order in which single use approvals are consumed (Section 11.4).¶
That order is not a signed fact: the anchor is unsigned
(Section 8.1) and an extra key in it passes Section 7.1, so whoever
holds the copies can change a record's record_sha256 without touching
its body or its proof. When two executions in one block compete for
one single use approval carried by an earlier record, the holder can
therefore choose which of the two is reported conditional. The
verdict, the deviation codes and the bond outcome do not change
(Section 18.1).¶
If the verdict is underspecified: undetermined.¶
Otherwise final if every accepted record's height and every
orphaned record's height is at or below the horizon; else
provisional.¶
With no accepted and no orphaned record, the status is final.
Foreign, rejected, ignored and set aside records do not affect status.¶
A second implementation MUST reproduce these exactly. They are what the vectors' expected.json files hold.¶
| field | value |
|---|---|
contract_sha256
|
Section 4.3, computed even for a contract the door refuses; null when the contract is not a JSON object (vector synth_contract_not_object) |
verdict
|
within_grant, deviation, nothing_to_settle or underspecified (Section 13) |
status
|
final, provisional or undetermined (Section 9.5) |
bond_outcome
|
Section 10.3 |
deviation_codes
|
the sorted set of deviation codes (Appendix A). Empty when the verdict is not deviation
|
underspecified_reasons
|
the sorted set of underspecified reasons (Appendix A). Empty unless the verdict is underspecified
|
event_set
|
the list of Section 9.1, each item {"kind", "sha256", "height"} with sha256 the record_sha256. Empty when the walk did not run. A reason from Section 5.3, rule 9 of Section 6.1, Section 7.1, Section 8.4 (below the lower bound) or Section 12 (a cited walk) does not empty it |
dispositions
|
an object mapping record_sha256 to one code, for every record the computation classified (Section 10.2) |
deviation_codes and underspecified_reasons are sets. The reference
may list a code more than once (one per action); that multiplicity is
informative.¶
| code | meaning |
|---|---|
foreign, inconsistent, unbound
|
Section 7.2 |
record_not_strict, signature_not_strict, anchor_not_strict, revocation_not_provided_for
|
Section 7.1, set aside, or (for record_not_strict and anchor_not_strict) failing closed with the same reason added to the settlement |
duplicate_dropped
|
Section 7.5 |
rejected
|
Section 7.1 or Section 7.7 |
ignored
|
Section 7.6, a revocation whose revoked_by is not "principal"
|
anchor_incomplete, anchor_beyond_observed_chain, anchor_proof_invalid, anchor_batch_leg_refused
|
Section 8.4 to Section 8.6, with the same reason added to the settlement |
anchor_below_lower_bound
|
Section 8.4, with the same reason added |
orphaned
|
Section 8.4 |
accepted
|
in the event set |
A bound record that the computation never examined (because
Section 5, Section 6 or Section 7.3 failed) has no disposition. A
record that is not ours (Section 7.2, dropped silently) has no
disposition. A record set aside because it cannot be digested has no
record_sha256 and so no disposition; it is counted in the informative
v1_16.refused. Every record has at most one disposition.¶
If contract.bond is absent or null: n/a. A bond of any other form
than an object with a positive integer amount makes the settlement
underspecified (Section 5.3) and the outcome is then
undetermined when the value is one the lower layers read as a bond
(an object with a truthy amount), else n/a.¶
For a well formed bond: undetermined if the verdict is
underspecified; else pending_finality if the status is
provisional; else held for within_grant and
nothing_to_settle, and forfeited for deviation.¶
Everything else the reference writes is informative: schema,
settled_under, v1_16, signed_body_collapse, contract_id,
binding_mode, bound_by_label_only, ordering_rule,
approval_rule, anchor_rule, view_rules, chain_tip,
finality_horizon, pinned_blocks, revocations, approvals,
binding, orphaned, predates_contract, ignored, rejected,
duplicate_anchorings, establishes, does_not_establish,
spine_gate, convergence_gate, approval_gate, signatures, the
full underspecified and deviations objects (heights, why texts,
details), and their order. The informative fields are useful when
comparing implementations. They are not part of conformance.¶
The walk runs over the event set (Section 9.1) only. Let
g = contract.grant.¶
For each accepted revocation at height h, its effective height E is:¶
effective_at "anchor": E = h.¶
effective_at "delivery_ack": E = the smallest of h +
ack_window and the heights of every accepted acknowledgement whose
revocation_sha256 names this revocation.¶
An acknowledgement names a revocation if its revocation_sha256
equals the lowercase hex body digest of the revocation, or the
record_sha256 of a copy of that revocation that is still in the input
after Section 7.5. A copy Section 7.5 dropped no longer names the
revocation (WRITTEN BEHAVIOUR: settle v1.4 collects every anchored
copy, but since settle v1.15 the copies of one signed body are reduced
before v1.4 sees them). Vector synth_ack_names_dropped_copy.
An acknowledgement anchored
before its revocation makes E earlier than h (WRITTEN BEHAVIOUR, see
item N5 of [MUSUBI-NOTES]).¶
Sort the revocations by (E, record_sha256). An execution at height h
is revoked if some revocation has E <= h. Authority ends at block E
inclusive: an act in the same block as an anchor-mode revocation is
revoked. For a revoked execution, every string in performed_actions
gives the deviation revoked, and nothing else in that record is
judged (no expiry, prohibited, conditional, unauthorized or
delegation check, and no approval nonce is consumed).¶
If the grant has no revocation object, a revocation never gets
here: it is set aside in Section 7.1 (revocation_not_provided_for).¶
If g.expiry_height is present and not null, every string action of
an execution with height h > expiry_height gives after_expiry. This
does not stop the other checks for that action: an expired prohibited
action gives both after_expiry and prohibited. An execution at
exactly expiry_height is not expired.¶
For each accepted, unrevoked execution, for each string a in
performed_actions, in order, after Section 11.2:¶
If a is in g.prohibited_actions: deviation prohibited. Next
action.¶
Else if a is the action of some g.conditional entry: apply
Section 11.4. Next action. (A conditional action does not need to
be in authorized_actions.)¶
Else if a is not in g.authorized_actions: deviation
unauthorized.¶
Every entry of performed_actions and delegated_to is a string here
(Section 7.1). Comparison is exact string equality. There is no case
folding or Unicode normalization.¶
Then, for each entry d of delegated_to not in
g.delegation.allowed (empty when absent): deviation delegation. An
outcome (outcomes) never excuses an act. An attempt that errored is
judged like one that succeeded.¶
Every approval carried by an accepted execution (revoked or not) is
classified. With pk the principal's key and csha this contract's
contract_sha256:¶
Not an object, or no principal key: forged (Section 11.5). (Once
Section 5.1 accepts, the principal always has a usable key,
so in core v0 only "not an object" can apply.)¶
If valid_until_height is an integer >= 0, nonce is a string of
32 lowercase hex characters (a nonce with a line feed never gets
here, Section 7.1), single_use is a boolean, and contract_sha256
is 64 lowercase hex: compute the v2 approval bytes shown after this
list. If sig_b64 verifies over them with pk: counted when its
contract_sha256 equals csha, else other_terms.¶
Otherwise, if sig_b64 verifies with pk over either older form,
it is label_bound:¶
Otherwise forged.¶
The three forms of approval bytes are:¶
v2: "a2a-approval-v2\n" + canonical({
"contract_sha256": <its contract_sha256>,
"action": <its action>,
"valid_until_height": ..,
"nonce": ..,
"single_use": ..})
v1: "a2a-approval-v1\n" + canonical({
"contract_id", "payload_digest", "action",
"valid_until_height", "nonce", "single_use"})
v0: "a2a-approval-v0\n" + canonical({
"contract_id", "payload_digest", "action"})
A missing field is canonicalized as null. The approval's by field
is not read. Only sig_b64 is verified. An approval whose signature
is only under another key, such as signature, is forged
(Section 11.5), even when that value would verify. label_bound and other_terms approvals are listed
(informative) and never count. They are not deviations.¶
A conditional action a in an execution at height h with record sha
s is approved if some counted approval, taken in event set order
(records by Section 9.1, approvals in list order within a record),
satisfies all of:¶
its action equals a,¶
it is carried by the same record s, or by a record at a height strictly below h (a record in the same block does not count),¶
h <= its valid_until_height,¶
if its single_use is true, its nonce has not been consumed yet.¶
The first approval that satisfies all four is used. If it is single
use, its nonce is consumed. The set of consumed nonces is shared
across all actions and all executions (see item N5 of
[MUSUBI-NOTES]). If no approval satisfies all four: deviation
conditional. The value of requires in the conditional entry is
not read: every conditional action needs a counted approval.¶
Every approval classified forged adds the deviation
forged_approval, whatever its action, whether or not the action was
needed.¶
Only when nenrin_records is a non-empty list. A walk that is not
strict (first paragraph of Section 7.1) is left out. Index every other
object in it by its record_sha256 (Section 4.3). For each execution
kept by Section 7.1 and not rejected by Section 7.7, left after
Section 7.5 (once per record_sha256), whatever Section 8 then
decides about its anchor (an orphaned execution is still read:
WRITTEN BEHAVIOUR, vector synth_cited_walk_orphaned_execution), look
up the walk whose
record_sha256 equals the execution's nenrin_ref. If found, and the
walk's context is an object whose contract_sha256 is present, not
null, and not equal to csha: deviation
evidence_names_other_contract. If an execution's nenrin_ref names
a walk that was left out for not being strict: underspecified reason
walk_not_strict. The contractor chose what to cite, and leaving the
walk out silently could hide a deviation.¶
Records that were rejected or set aside are never checked here (item N2 of [MUSUBI-NOTES]).¶
When requirements.evidence is "nenrin_required", each accepted
execution whose nenrin_ref is not 64 lowercase hex gives the
deviation evidence_missing. This is applied after Section 13 found
within_grant or deviation.¶
underspecified if any underspecified reason was found
(Section 5, Section 6, Section 7.1, Section 7.3, Section 8,
Section 12). Then deviations is empty and status is
undetermined.¶
Else deviation if any deviation was found (Section 7.7,
Section 11, Section 12).¶
Else nothing_to_settle if the event set is empty: no record was
read, so nothing was found within the grant or outside it. Status is
as Section 9.5 gives it (final with no orphaned record).¶
Else within_grant.¶
within_grant covers only the accepted records. One strictly signed
record whose anchor fails Section 8.4 (first, second or third
row), Section 8.5 or Section 8.6 makes the whole settlement
underspecified. A record that is not strictly signed never does.¶
These are what the reference still does. They are normative for core v0 because the vectors pin them, or would.¶
| item | vector |
|---|---|
| An extra bad signature from a party refuses the contract even when that party also signed correctly | none |
| Revocations and acknowledgements cannot pass a typed batch leg (no kind) | none |
| An acknowledgement anchored before its revocation ends authority at the acknowledgement's height | none |
| An orphaned record is excluded without making the settlement underspecified, yet its claimed height still counts for status |
real_p31_anchor_names_other_height
|
A header ending in a line feed passes rules 1 to 8 of Section 6.1 and is refused only by rule 9 (view_not_strict); one in upper case is refused by both |
synth_view_header_trailing_newline
|
| A party-signed record that names this contract_sha256 with another contract_id or payload_digest is inconsistent (Section 7.2) and is never triaged, so a deviation it shows is not read | none |
Each item below was written behaviour of settle v1.10 to v1.15; [MUSUBI-NOTES] has the details under the item numbers given.¶
| item | before | now |
|---|---|---|
| N2 | The NENRIN cross check read every bound execution, also rejected and unsigned ones: anyone could add a charge and forfeit a bond | only records strictly signed and not rejected are read by any check |
| N1 | A trailing line feed passed in block_hash, proof operands, headers, action names and approval nonces | strict forms (Section 7.1, rule 9 of Section 6.1) |
| N3, N6 |
delegated_to and approvals were not type checked; an object in delegated_to stopped the reference |
Section 7.1 |
| N4 | Floats and unsafe integers in records, views and walks were accepted | Section 7.1, rule 9 of Section 6.1, Section 12.1 |
| N6 | A revocation against a grant with no revocation block stopped the reference | set aside |
| N8 | The bond test used truthiness, so [] was no bond in Python and a bond in JavaScript |
bond_malformed
|
| N9 | Signatures were checked by OpenSSL alone | strict signatures (Section 4.4) |
| N10 | A copy with a junk signatures entry was a second body that blocked the verdict (fixed in v1.15) | copies of one signed body collapse (Section 7.5) |
| N11 | An anchor below the checkpoint was dropped without a word; parent_contract and requirements.evidence were never read; an empty event set was within_grant
|
anchor_below_lower_bound; Section 5.3; Section 12.2; nothing_to_settle
|
The vectors that pin each change are:¶
| item | vectors |
|---|---|
| N2 |
synth_nenrin_check_reads_rejected_record, synth_unsigned_copies_anchored_first
|
| N1 |
real_p31_proof_operand_trailing_newline, real_p31_proof_operand_trailing_newline_beside_good, real_p31_block_hash_trailing_newline, synth_action_name_trailing_newline, synth_approval_nonce_trailing_newline, synth_view_header_trailing_newline
|
| N3, N6 |
synth_delegated_to_object
|
| N4 |
synth_float_in_signed_body, synth_unsafe_integer_in_signed_body, synth_unsigned_float_beside_good, synth_cited_walk_not_strict
|
| N6 |
synth_revocation_irrevocable_grant
|
| N8 |
synth_bond_empty_list, synth_bond_amount_string
|
| N9 |
synth_signature_s_plus_l, synth_strict_signature_beside_malleated, real_p31_contract_bad_principal_signature
|
| N10 |
real_p31_copy_with_junk_signature_entry
|
| N11 |
real_p31_anchor_below_checkpoint, synth_parent_not_supplied, synth_parent_supplied_narrows, synth_parent_widened, synth_nenrin_required_without_ref, synth_nothing_to_settle
|
None of the revisions below changed the reference. Each says what the reference already did where the text had been silent, unclear or wrong.¶
Revision 0.1. The first separately written implementation (Section 16.1, [ISSUE-49]) asked which reading was meant (item N13 of [MUSUBI-NOTES]). For each, what a reader could take the text to mean, what the reference does, and the vector:¶
An approval signed only under signature counts. The reference
verifies only sig_b64, so such an approval is forged
(Section 11.4, Section 11.5). Vector
synth_approval_signature_key_only.¶
A strict sig_b64 makes a malleated signature beside it harmless.
The reference checks each key for strictness on its own; the body is
not strict (Section 7.1). Vector
synth_approval_lax_signature_beside_good_sig_b64.¶
(Control.) sig_b64 and signature with the same strict value: the
approval is counted. Vector
synth_approval_signature_key_same_value.¶
Revision 0.2, from the 25 implementation notes of the same implementation (item N14 of [MUSUBI-NOTES]):¶
| note | what the text now says | vector |
|---|---|---|
| A1 |
contract_sha256 is null when the contract is not a JSON object (Section 10.1) |
synth_contract_not_object
|
| A3 | an integer token longer than 4300 digits stops the computation, no settlement (Section 3.2) | none |
| A6 | domain normalization strips all leading and trailing whitespace (rule 4 of Section 5.1) |
synth_domain_leading_tab
|
| A7 |
delegation must carry allowed; ordering may be {} (rule 10 of Section 5.1) |
synth_delegation_empty_object, synth_ordering_empty_object
|
| A10 | parent narrowing stated in full (Section 5.3.1) |
synth_parent_full_* (25) |
| A12, A15 |
NaN, Infinity, -Infinity are read as numbers, in records and in batch bytes (Section 3.2, Section 8.6) |
synth_nan_in_signed_body, synth_batch_bytes_with_nan
|
| A14 | the cross check reads an execution whatever its anchor result (Section 12.1) |
synth_cited_walk_orphaned_execution
|
| A16 |
view_not_strict only for a view that is an object with a headers list (rule 9 of Section 6.1) |
synth_view_not_an_object, synth_view_headers_not_a_list
|
| A20 | what "carries" means for each out of scope key (Section 1.4) |
synth_limits_null, synth_admission_null
|
| A21 | a floor whose mantissa is zero decodes to no target: both the door and the view refuse |
synth_floor_zero_mantissa
|
| A22 | a copy dropped by Section 7.5 no longer names its revocation (Section 11.1) |
synth_ack_names_dropped_copy
|
| A24 | step 1 "no principal key" of Section 11.4 cannot apply once Section 5.1 accepts | none |
Revision 0.3, from 10 more notes of the same implementation (item N15 of [MUSUBI-NOTES]):¶
| note | what the text now says | vector |
|---|---|---|
| B2 | non-finite numbers are written NaN, Infinity, -Infinity in canonical form (item 6 of Section 4.1) |
synth_infinity_in_signed_body, synth_negative_infinity_in_signed_body
|
| B7 | only a well formed approver list is carried; {}, "", 0, null and [] refuse in Section 5.1 (Section 1.4, rule 10 of Section 5.1) |
synth_approvers_object, synth_approvers_string, synth_approvers_zero
|
| B5 | rule 14 of Section 5.3.1 applies when the parent pins approvers and the child has none |
synth_parent_approvers_child_drops
|
| C3 | rule 14 of Section 5.3.1 compares each approver entry as canonical bytes, so the order of actions inside an entry counts; core v0 cannot reach it |
none (out of scope) |
| B8 | exactly three spellings; nan and +Infinity stop the reader; 1e400 is an infinity (Section 3.2) |
none (no settlement to compare) |
Revision 0.4 (item N17 of [MUSUBI-NOTES]) checked which underspecified reasons the vectors actually produce. Section 15.3 has the result.¶
Revisions 0.5 to 0.8:¶
0.5 (item N18): two statements about the reference were wrong. The file list named the copy of agreement_verify.py vendored into another package instead of the copy the reference imports (this document already gave the right one), and the source text said settle_v1_15.py gives the same core fields outside a listed set of vectors, which it does not (Section 18.5).¶
0.6 (item N20): a strictly signed record whose body is not strict is
set aside as well as adding its reason (Section 7.1); failing
Section 5.1 with the parent supplied adds only
parent_narrowing_failed (Section 5.3). Both say what the
vectors already expected.¶
0.7 (item N21): the same-block order is not a signed fact (Section 9.3, Section 18.1).¶
0.8 (item N22): two files the reference imports changed after revision 0.4, in layers outside settle core; on the 128 vectors the complete output of the reference is the same before and after. The file list now names its commit (Section 16.3).¶
The vectors are published beside the source text of this document, unchanged since commit dd64d0e2 (revision 0.4) and cited here at commit d7f5a91e [MUSUBI-VECTORS], in the folder settle-core-v0/vectors/ under https://github.com/ogasurfproject-jpg/horizon-shield/tree/d7f5a91e/workers/hs-ledger/nenrin/musubi-v0/settle-core-v0. They are not reproduced here.¶
There are 128 vectors: 22 real and 106 synthetic. Their expected
verdicts are within_grant for 38, deviation for 21,
nothing_to_settle for 8 and underspecified for 61. Their expected
statuses are final for 65, provisional for 2 and undetermined
for 61.¶
The 76 vectors of the first release are unchanged, byte for byte. Revision 0.1 of the source text added 3, revision 0.2 added 38, revision 0.3 added 6 and revision 0.4 added 5 (Section 14.3, Section 15.3). All added vectors are synthetic.¶
The folder holds one subfolder per vector:¶
contract.json, view.json, events/eNN.json (the events, in any
order), nenrin/nNN.json when the vector supplies walks,
parent.json when it supplies a parent contract,¶
expected.json: the core fields (Section 10.1).¶
The index vectors/vectors.json lists, for each vector, its id, its family (real or synthetic), the section of the source text it tests, a description, the expected verdict, status and code sets, and the sha256 of each of its files. It also lists the four synthetic public keys.¶
Real vectors use the real records, contracts and Bitcoin headers of
two settled engagements, with one change each (the record bodies
cannot change without breaking the real Bitcoin anchors).
real_p31_baseline is the first MUSUBI contract settled with no
party from the project that publishes the reference
(agent-passport.org and kuangmi-bit.github.io, Bitcoin block 970480),
with its 359 header view.¶
Synthetic vectors use keys named synthetic-principal,
synthetic-contractor, synthetic-witness and synthetic-stranger,
derived from fixed seeds, and a synthetic header chain at regtest
difficulty (max_target_bits 207fffff, checkpoint height 800001).
The public keys are:¶
| name | public_key_ed25519_b64 |
|---|---|
| synthetic-principal |
rrXBDf5mF249t6wdmMvKtkcBXZmVVfcfYFBBjIpSttE=
|
| synthetic-contractor |
hl3xmxz86HFYXTczn/5HZw4Jkb3i0nL7iig/2GXZrcs=
|
| synthetic-witness |
gMKIYPPIjG6r9mt8ybr3dIGGm6LYxkngSym9ltvHNtY=
|
| synthetic-stranger |
YmzNu/v6K9wUOtcmnssHABVLBDuLBqMcZnM0NL+32rU=
|
gen_vectors.py regenerates every vector from the two real inputs and
the reference; it is deterministic. run_reference.py runs the
reference over every vector and checks every file hash and every core
field. exercise_inputs.py [MUSUBI-EXERCISE] builds in memory the
inputs the vector layout cannot carry (events that are not a list,
10001 records) and the inputs of the four rows of
Section 5.2 that cannot fire, runs the reference on them,
and compares its lines with EXERCISE.txt (--check).¶
expected.json holds exactly the eight core fields of Section 10.1 and nothing else. The reference's full settlement object is projected onto them as follows:¶
contract_sha256, verdict, status and bond_outcome are copied.¶
deviation_codes and underspecified_reasons are the sorted sets
of the codes in the reference's deviations and underspecified
lists. Repetitions are removed.¶
event_set keeps, for each accepted record in the order of
Section 9.1, only kind, sha256 and height.¶
dispositions is one object from record_sha256 to one disposition
code (Section 10.2), with keys sorted. A record that would
receive two different codes is an error of the projection.¶
An implementation conforms on a vector when its eight core fields are equal to expected.json as JSON values. The informative fields (Section 10.4) are not compared.¶
Revision 0.4 of the source text checked every underspecified reason against the expected results of the vectors (item N17 of [MUSUBI-NOTES]). It was prompted by [I-D.he-agentproto-verifier-evidence], under which a rejecting path counts as exercised only when an identified input drove it.¶
All ten deviation codes (Appendix A) are produced by at least one vector. Of the 28 underspecified reasons, the 123 vectors of revision 0.3 produced 18 and did not produce 10. Each of the ten was then driven through the reference. For each, what was missing, and the vector or exercise that now drives it:¶
grant_action_name_invalid: no vector; Section 5.1 accepts any
non-empty string, and Section 5.2 applies the name pattern.
Vector synth_grant_action_name_invalid.¶
too_many_witnesses: no vector; 33 witnesses refuse, 32 are
accepted. Vectors synth_too_many_witnesses and
synth_witnesses_32.¶
ack_window_missing: no vector; Section 5.1 allows
delivery_ack without ack_window. Vector
synth_delivery_ack_without_window.¶
finality_policy_missing: no vector; Section 5.1 allows
finality without depth. Vector synth_finality_without_depth.¶
input_not_settleable: events that are not a list cannot be written
as event files. exercise_inputs.py cases E1 to E3.¶
input_too_large (records): 10001 records would be 10001 files; the
count is after Section 7.4. exercise_inputs.py cases E4 to
E6.¶
input_too_large (headers): more than 200000 verified headers. None:
not exercised.¶
authorized_actions_missing, witness_key_invalid,
revocation_policy_not_recomputable, bad_expiry_height: cannot
fire, because Section 5.1 refuses first
(Section 5.2). exercise_inputs.py cases D1 to D6 show
contract_not_verified.¶
So the 128 vectors produce 22 of the 28 underspecified reasons. Four
reasons are reachable and now have vectors; synth_witnesses_32
(within_grant) pins the boundary beside synth_too_many_witnesses.¶
Two reasons are reachable only through inputs the vector layout cannot
carry, and exercise_inputs.py [MUSUBI-EXERCISE] drives them in
memory. Cases E1 to E3 pass events that are an object, null and the
string "[]", and each gives input_not_settleable. Case E4 passes
10001 distinct records and gives input_too_large; case E5 passes
10000 distinct records and does not give it (the limit is more than
10000); case E6 passes 10001 copies of one record and does not give
it either, because identical bytes are one record
(Section 7.4) and are counted once (Section 7.3). Driving
these inputs is how the last point was found; Section 7.3 did not
say it before revision 0.4. The headers branch of input_too_large
(Section 6.1, more than 200000 verified headers) is still not
exercised by any vector or exercise.¶
Four rows of Section 5.2 can never produce their reason in
core v0: authorized_actions_missing, witness_key_invalid,
revocation_policy_not_recomputable and bad_expiry_height. Each
row describes an input that rules 9 or 10 of Section 5.1
already refuse, and Section 5.2 applies only after
Section 5.1 accepts. Cases D1 to D6 run those inputs (an
authorized_actions that is the string "read", an absent
authorized_actions, a witness key that decodes to 31 bytes, an
effective_at of "immediately", an expiry_height of -1, an
expiry_height that is the string "800010") and each gives
contract_not_verified. The rows are kept so that no code changes
meaning.¶
Each line of EXERCISE.txt names the case, the input, what the text
says, and the verdict, status and codes the reference returns.
python3 exercise_inputs.py --check recomputes the lines and compares
them with EXERCISE.txt byte for byte. These lines are a record of
which input drove which outcome. They are not vectors and have no
expected.json (Section 15.2).¶
Every unexercised path was on the refusal side. A verifier that never
returns underspecified for a malformed policy goes on to compute a
verdict it has no basis for, so these are the paths worth pinning.¶
A disagreement is valuable. It means either the text, the reference or the implementation is wrong, and only a comparison finds out which.¶
Run the implementation over the vectors. For each vector, compare
its core fields (Section 10.1) with expected.json.¶
Run python3 run_reference.py from the folder holding the source
text, to confirm the reference still gives the expected values on
the same machine.¶
For each vector with a disagreement, write down: the vector id; the core field; the expected value and the implementation's; the section of this document followed; and, if possible, the step where the computation first diverged (for example "conformance: the name was treated as nonconforming").¶
Say whether the vector is listed in Section 14. If it is, say whether the behaviour should change.¶
For a disagreement on an input that is not a vector, attach the input files (contract, events, view, walks) so the reference can be run on them.¶
Open an issue on the repository that publishes the source text (at
the time of writing github.com/ogasurfproject-jpg/horizon-shield)
titled settle core v0 disagreement: <vector id>, with the above.¶
An implementer should not change an implementation to match a vector believed to be wrong without saying so. A silent match hides a defect. A reported disagreement is resolved by changing this text, the reference, or the vector, and the change is recorded with a new version of this document.¶
Note to the RFC Editor: please remove this section before publication.¶
This section records the status of known implementations of the protocol defined by this specification at the time of posting of this Internet-Draft, and is based on a proposal described in [RFC7942]. The description of implementations in this section is intended to assist the IETF in its decision processes in progressing drafts to RFCs. Please note that the listing of any individual implementation here does not imply endorsement by the IETF. Furthermore, no effort has been spent to verify the information presented here that was supplied by IETF contributors. This is not intended as, and must not be construed to be, a catalog of available implementations or their features. Readers are advised to note that other implementations may exist.¶
According to [RFC7942], "this will allow reviewers and working groups to assign due consideration to documents that have the benefit of running code, which may serve as evidence of valuable experimentation and feedback that have made the implemented protocols more mature. It is up to the individual working groups to use this information as they see fit".¶
The sections below separate implementations written by others (Section 16.1), runs of the reference by others (Section 16.2), and the reference itself (Section 16.3). A public table of the same rows, with a link for each, is kept at [INTEROP-TABLE].¶
Two implementations of settle core v0 by authors other than the publisher of the reference agree with it on all 128 vectors, on the eight core fields (Section 10.1). Neither was written from the source text alone: one also read NOTES.md, and the other had the expected outputs and let them decide where the text could be read two ways. The Independence entries below say which. No implementation has yet been written from the text and the vector inputs with the expected outputs withheld and its code fixed by hash before comparison; that test is open. The vectors have not changed since revision 0.4; revisions 0.5 to 0.8 of the source text changed no rule, reason or vector (Appendix B).¶
aeoess (Agent Passport System).¶
musubi-settle-core-check [AEOESS-CHECK], written from SETTLE_CORE_v0.md and NOTES.md alone, with an AI coding agent under its author's direction, from an input copy without the expected outputs.¶
The eight core fields on every vector.¶
Updated to revision 0.4 by its author (commit 53cfafb), it agrees with all 128 vectors with no field left unassessed (item N19 of [MUSUBI-NOTES]). Its earlier runs (76, 117 and 123 vectors) and the notes that led to revisions 0.1 to 0.3 are recorded in items N13 to N16.¶
Written without the expected outputs, but from NOTES.md as well as
the source text. Its author declared that, and the public tally
[ERRATA-K001] does not count it as independent for that reason.
Run by its author, who maintains a party to the contract behind
real_p31_baseline (Section 15.1).¶
Apache-2.0.¶
2026-10-22¶
kuangmi-bit.¶
The settle-core-v0 directory of nenrin-independent-verifiers [KUANGMI-CHECK], at commit bbe455c, in Python. Written from revision 0.5 of the source text and the published vectors only, without reading the reference or musubi-settle-core-check. Standard library at run time; Ed25519 through the Python package cryptography when present and through its own RFC 8032 verifier otherwise.¶
The eight core fields on every vector.¶
Agrees with all 128 vectors, including real_p31_baseline,
real_cross_record_names_other_contract and the 25 parent vectors.
Its author also ran the reference on the same machine and got 128 of
128. The publisher of the reference reran it at bbe455c and got the
same. Its README lists the readings where the text could be read two
ways and the vectors decided; revision 0.6 states two of those
places in the text, and neither changed a vector (item N20 of
[MUSUBI-NOTES]).¶
Written without reading the reference or musubi-settle-core-check, as its author records (item N20 of [MUSUBI-NOTES]). The expected outputs were available to it and settled the readings listed in its README, so it is not a test of the text without the vectors. It is the one implementation that [ERRATA-K001] counts as independent under that tally's rule, which allows the vectors.¶
MIT.¶
2026-10-22¶
These are not further implementations of this document. They are outside runs of the reference, or outside checks of records that a settlement reads, listed because each was done on the reader's own machine from published bytes.¶
Poke-nushi recomputed the settlement of the 22 October run of the
cA2A companion pilot (horizon-shield Issue #51, set 2) from the
published bundle at commit b85029de, with the companion's settler
(pilot_v0.py, which reads records of the kinds this document
defines but is not settle core v0 itself) and ca2a-runtime 0.4.0,
and reproduced the published output sha256. The
same reader checked the signatures and the OpenTimestamps proof of
the log checkpoint, and rebuilt the header view from Blockstream and
mempool.space headers up to block 972564: the verdicts did not
change and only anchor_depth moved, from 9 to 14 [RUN-POKE].
This shows which part of a settlement rests on the reader's header
range, a point that applies to settle core as well (Section 9.4).¶
In the same run, aeoess, which ran the same calls through its own
row of the testbed (Agent Passport System), reported that the A and B audit files of its
run are byte identical to those of ours (sha256 c2920477...,
1,532 bytes, and 40bc2a46..., 1,101 bytes) [RUN-AEOESS].¶
babyblueviper1 reported issuing four acceptance records for the same run, A and B of each of the two rows, and that A passes all four criteria and B fails three, matching the expected A accept and B reject [RUN-BBV]. The records themselves are not public; their author will publish them once everyone in the run agrees.¶
Pico, the AI agent that maintains Agent Errata, reproduced its part of a decision record exchange (E6 of the agent governance vocabulary boundary map) on fresh clones: 12 of 12 cases agree with the record published by this document's author [RUN-PICO]. This does not exercise settle core; it is listed because it uses the same publish-and-recompute procedure as Section 15.4.¶
The HORIZONs Co., Ltd.¶
settle_v1_16.py (output schema a2a-settlement-v1.16) and the modules
it imports, at commit d7f5a91e [MUSUBI-REF]. It is called as a
library, settle_v1_16.settle_v1_16(contract, events, view,
parent=..., nenrin_records=...). Its command line (--settle,
--event, --view, --nenrin, --parent) first checks each
view's strict form and runs fork choice, and for a view that does
not pass prints why and writes no settlement. The vectors use the
library.¶
The implementation from which this document was written. It covers all of settle core v0 and the gates outside its scope (Section 1.4).¶
Reference. Its behaviour defines the expected values of the vectors.¶
All of settle core v0. The canonical form (Section 4.1) is also implemented in JavaScript (canonical_v0.mjs), which covers only that section.¶
MIT.¶
run_reference.py reports "128/128 vectors agree with the reference"
with Python 3.14.7 and the Python package cryptography 50.0.1, and
exercise_inputs.py --check reports "EXERCISE.txt agrees"
(Section 15.3).¶
Toshikatsu Oga, thehorizon.nnovation@icloud.com¶
2026-10-22¶
Two imported files changed after draft -01 (commit dd64d0e2): admission_verify_v0.py, which gained a third record version for an admission layer outside settle core, and recovery_verify.py, which gained the v2.4 witness draw of the recovery layer. On the 128 vectors the complete output of the reference, not only the core fields, is the same at dd64d0e2 and at 9ab4a004, and the commit d7f5a91e changed only the text. The reference, as run for the vectors, consists of these files (first 16 hex digits of each file's sha256):¶
| file | sha256 prefix |
|---|---|
| settle_v1.py |
0e51350331c8931c
|
| settle_v1_1.py |
a2391a5db199bcda
|
| settle_v1_2.py |
6c674fd54a1a5c89
|
| settle_v1_3.py |
17119bbc7802647f
|
| settle_v1_4.py |
ef3779d05fc98e40
|
| settle_v1_5.py |
f9b313566713cdc4
|
| settle_v1_6.py |
6752a748ed7b6e3b
|
| settle_v1_7.py |
878a3a3697775d3a
|
| settle_v1_8.py |
fe61f67c1593c776
|
| settle_v1_9.py |
7a84302be1ec38a1
|
| settle_v1_10.py |
34beeebaa1397059
|
| settle_v1_11.py |
fb03687cbacd2713
|
| settle_v1_12.py |
40a8fbb82a8e7680
|
| settle_v1_13.py |
eb02b3a6544b1bf1
|
| settle_v1_14.py |
7d0704fa060adafd
|
| settle_v1_15.py |
a2be5de8ba60656e
|
| settle_v1_16.py |
ec332ecb387e9b56
|
| admission_verify_v0.py |
4bc36aa7a34ec024
|
| agreement_verify.py |
fd8c34ce8a11cf42
|
| anchor_compose.py |
a661f673c05dbe6e
|
| approval_v3.py |
e8dde985dbe6fbf4
|
| clause_eval_v0.py |
fee48c3a66c28fe1
|
| contract_v0.py |
211a568783705e11
|
| convergence_v0.py |
65a267380fa623f5
|
| corroboration_v0.py |
62ae1d6c27a92228
|
| independence_v0.py |
50964883c27896de
|
| recovery_verify.py |
fd4c93f015f6b495
|
| spine_verify.py |
ad2076d49be6763e
|
| terms_v0.py |
6532d5c6160a3974
|
A record has three parts (Section 7.1): the body, which its signer signed, and the anchor and the signatures list, which nobody signed. A third party that holds copies of genuine records, the contract and the walks can therefore change anchors and signature lists at will, but cannot change a body without the signer's key. The rules are arranged around this:¶
A copy whose signatures list holds no strict, verifying entry from an allowed role is never examined: it is set aside or rejected in Section 7.1, and no later step (collapse, size limit, conformance, anchors, NENRIN checks) sees it. An unsigned record cannot add a charge or forfeit a bond.¶
A copy that keeps the genuine signature but adds junk entries to
signatures has the same record signing bytes as the genuine
record. Junk entries are ignored, and copies of one signed body are
collapsed (Section 7.5), keeping the copy that anchors validly at
the smallest (height, record_sha256).¶
A copy with an anchor that is not strict is set aside when a validly anchored copy of the same body is present. A copy whose anchor is strict but wrong is dropped by the collapse when a validly anchored copy is present.¶
Binding is by contract_sha256 alone (Section 7.2): a record made under earlier terms with the same contract_id is foreign and is never triaged.¶
What remains is a liveness property, not a safety one. When the
genuine copy is not in the input, a copy with an incomplete or wrong
anchor from a strictly signed record makes the whole settlement
underspecified (Section 13); a party can delay a verdict in the
same way by handing the settler such a record. A copy whose
block_hash was edited to disagree with the view at a held height is
orphaned and excluded without an underspecified reason
(Section 14.1); a settler that has only such a copy does not see the
act. Settlers that collect records from more than one source reduce
both risks. underspecified must not be read as a statement that
the terms are bad.¶
The record_sha256 covers the unsigned parts, so it identifies a copy, not an act. The record signing bytes identify the act. One consequence reaches the listing: the same-block order of Section 9.3 sorts by record_sha256, so the holder of the copies chooses which of two same-block executions is blamed for a single use approval. A later version that wants this order to be a signed fact would sort same-height records by the body digest instead; that changes the expected results of the vectors with two records in one block, so it is not done in core v0.¶
Ed25519 implementations differ on adversarial signatures: a non-canonical S (for example S + L), a small order or mixed order R or public key, and cofactored versus cofactorless verification equations [EDDSA-TAMING]. Honest signatures verify everywhere. Settle core v0 counts a signature only when it is strict (Section 4.4): S, read as a little-endian integer, is below L, and R and the public key are canonical encodings of points of prime order that are not the identity. With all points in the prime order subgroup and S below L, the libraries that disagree on lax signatures agree.¶
A lax signature that a library accepts is not ignored silently: the
record is set aside as signature_not_strict, and a contract
signature that is not strict makes the settlement underspecified
(contract_signature_not_strict). Anyone can turn a genuine
signature into a lax one (S + L) without a key; that changes the
record_sha256 but not the body, and the genuine copy, if present,
still settles (vectors synth_signature_s_plus_l and
synth_strict_signature_beside_malleated).¶
Item N9 of [MUSUBI-NOTES] records that the behaviour of the reference's OpenSSL on published Ed25519 edge cases was not tested beyond the strictness rule. The strictness rule is what conformance depends on.¶
Settlement depends on byte-exact digests computed by different
runtimes. JSON readers differ: some accept the tokens NaN and
Infinity, some read 970480.0 as a float and others as the integer
970480, integers above 2^53 are exact in some and rounded in others,
duplicate keys are resolved differently, and key order by code point
differs from order by UTF-16 code unit above U+FFFF. A pattern ending
in $ in some regular expression engines also accepts a trailing line
feed, and some hex decoders skip whitespace.¶
The rules of Section 3.2, Section 4.1 and Section 7.1 remove
these differences for the values that matter: only integers within
plus or minus (2^53 - 1) are strict, keys in the contract and in
strict values are ASCII, fixed forms such as hex digests are matched
whole, and a value that is not strict is set aside or makes the
settlement underspecified instead of being read two ways. An
implementer has to parse numbers to an exact integer representation,
read the tokens NaN, Infinity and -Infinity as non-integer
numbers rather than as errors (Section 3.2), write them in the
spellings of item 6 of Section 4.1 when a signature over a value
that is not strict is checked, match fixed forms against the whole
string, and refuse whitespace in hex.¶
Some divergence remains possible outside strict values. The
record_sha256 of a record that is set aside, foreign or unbound is still
reported in dispositions, and its canonical form can depend on the
runtime for values that are not strict. A record whose record_sha256
"cannot be computed at all" is set aside without a disposition; in
the reference this happens, for example, for an integer too long for
the Python runtime to write out, and another runtime may reach that
limit at a different length. Item N4 of [MUSUBI-NOTES] also records
that JSON batches reached through a proof (Section 8.6) are parsed
as the lower layers of the reference parse them.¶
Settle core v0 takes one header view as given. It checks linkage,
proof of work against each header's own nBits, the grant's difficulty
floor and the signed checkpoint, and nothing else of Bitcoin
consensus. It does not establish that the view is the heaviest chain.
A status of final means that every accepted and orphaned record is
at least grant.finality.depth blocks below the tip of the view; a
reorganization deeper than that depth can still change a final
settlement. Choosing between several views (fork choice) is out of
scope (Section 1.4). The difficulty floor and the checkpoint limit what
a party can forge cheaply, but a view supplied by an adversary with
enough work is accepted.¶
That any record is true. A signature proves which key signed, not that the act happened as described.¶
That no key was stolen. A stolen key signs validly. Key rotation and compromise are outside settle.¶
That acts nobody recorded did not happen. within_grant covers the
accepted records only.¶
That a foreign, unbound, inconsistent, rejected, orphaned or predated record is false. Only that it was not settled here.¶
Full Bitcoin consensus validity. Only linkage, proof of work against each header's own nBits, the grant's difficulty floor and the signed checkpoint are checked.¶
That the view is the heaviest chain. Core v0 takes one view as given. A reorganization deeper than the finality depth can change a final settlement.¶
That the key at a party's key_url is the key in the contract.
Settle never fetches.¶
That the walk cited by nenrin_ref is correct. Section 12 only
compares the contract it names.¶
That anyone judged liability or fault, or that this is a legal contract. The verdict is a function anyone can recompute from the same bytes.¶
That the parts out of scope (Section 1.4) hold. A contract that relies on the spine gate, convergence, pinned approvers or limits must be settled by a settler that reads them.¶
That settle layers before v1.16 agree. They do not: over the 128 vectors, settle_v1_15.py gives different core fields (dispositions aside) on 51 and stops with an exception on 3. This document specifies v1.16 only.¶
That a record set aside or rejected is false. Only that it was not strictly signed, or not readable strictly, so it was not settled here.¶
A contract names both parties by domain and pins their public keys, and records carry action names, delegation targets, payload digests and references to walks. Anyone who is given these bytes can settle, so anyone who is given them learns who contracted with whom and what the agent did. Anchoring commits only a digest to the Bitcoin block chain, but the commitment is public and permanent, and publishing a record later links it to a block and so to a time. Parties should treat the inputs of a settlement as disclosed to every settler they hand them to. Settlement itself fetches nothing (Section 1.4), so computing it reveals nothing to a network observer.¶
This document has no IANA actions.¶
Underspecified reasons in core v0:
contract_not_verified, input_not_settleable,
contract_signature_not_strict, bond_malformed,
parent_not_supplied, parent_narrowing_failed, view_not_strict,
record_not_strict, anchor_not_strict, anchor_below_lower_bound,
walk_not_strict, authorized_actions_missing,
grant_action_name_invalid, too_many_witnesses,
witness_key_invalid, key_conflict, tie_policy_refused,
revocation_policy_not_recomputable, ack_window_missing,
finality_policy_missing, bad_expiry_height,
unscoped_approvals_refused, chain_view_rejected,
input_too_large, anchor_incomplete,
anchor_beyond_observed_chain, anchor_proof_invalid,
anchor_batch_leg_refused.¶
Of these, authorized_actions_missing, witness_key_invalid,
revocation_policy_not_recomputable and bad_expiry_height cannot be
reached in core v0 (Section 5.2, Section 15.3).¶
Deviation codes in core v0:
revoked, after_expiry, prohibited, conditional,
unauthorized, delegation, forged_approval,
nonconforming_record, evidence_names_other_contract,
evidence_missing.¶
Verdicts: within_grant, deviation, nothing_to_settle,
underspecified.¶
Dispositions: Section 10.2.¶
Note to the RFC Editor: please remove this section before publication.¶
This version follows revision 0.8 of [MUSUBI-SETTLE-CORE] at commit d7f5a91e; -01 followed revision 0.4 at commit dd64d0e2. No rule of the reference and no vector changed between the two commits. The changes in meaning are:¶
Section 7.1: the row "strictly signed, body not strict" sets the
record aside as well as adding record_not_strict.¶
Section 5.3: the parent row adds only
parent_narrowing_failed; contract_not_verified is decided on the
contract alone.¶
Section 9.3 and Section 18.1: the same-block order can be chosen by whoever holds the copies; the verdict does not move.¶
Section 18.5: the earlier settle layers disagree with v1.16 on 51 of the 128 vectors and stop on 3.¶
New informative section Section 17: settlement as an Auditor's evaluation in the agent auditing architecture, with the roles and record types mapped; SCITT combined with, not replaced by, the Bitcoin anchors; GNAP and OAuth as the source of a grant; and related Internet-Drafts. Section 1.1 carries one sentence pointing to it.¶
Other changes: the date; the pinned commit of every repository reference; the title of [MUSUBI-SETTLE-CORE]; Section 1.2 and Section 14.3 carry revisions 0.5 to 0.8; Section 16 is rewritten, with a second implementation by another author (Section 16.1.2) and what each implementation read before it was written, runs by others (Section 16.2), the run of the first implementation against all 128 vectors, the reference's run environment and its file list at the new commit; new references [KUANGMI-CHECK], [ERRATA-K001], [INTEROP-TABLE], [RUN-POKE], [RUN-AEOESS], [RUN-BBV], [RUN-PICO], [RFC9943], [RFC9635], [RFC8693], [RFC9396], [ISSUE-55] and five Internet-Drafts of related work; and the acknowledgements.¶
Note to the RFC Editor: please remove this section before publication.¶
This version follows revision 0.4 of [MUSUBI-SETTLE-CORE] at commit dd64d0e2; -00 followed the first release at commit 4100d52a. The reference did not change between the two commits. The changes in meaning are:¶
Section 1.4: what it means for a contract to carry an out of scope
key; a malformed approvers value refuses the contract instead of
making the input out of scope.¶
Section 3.2: NaN, Infinity and -Infinity are read as
non-integer numbers; other spellings are parse errors; 1e400 is an
infinity; an integer token over 4300 digits produces no settlement.¶
Section 4.1: item 6, how a non-integer number is written.¶
Section 5.1: rule 4 (domain whitespace); rule 10
(delegation needs allowed, ordering may be {}, well formed
approvers).¶
Section 5.2: four rows that cannot fire are named.¶
Section 5.3.1: new; parent narrowing stated in full, replacing
the pointer to the reference's grant_subset.¶
Section 6.1: rule 9 applies only to an object view with a
headers list.¶
Section 7.1: an approval's sig_b64 and signature are each checked
for strictness on their own.¶
Section 7.3: records are counted after identical bytes are merged.¶
Section 8.6: rule 4 reads NaN as a number.¶
Section 10.1: contract_sha256 is null for a contract that is
not an object.¶
Section 11.1: a copy dropped by Section 7.5 no longer names its revocation.¶
Section 11.4: step 1 can apply only to an approval that is not an
object; only sig_b64 is verified.¶
Section 12.1: an orphaned execution is still read.¶
Appendix A: the four unreachable reasons are named.¶
Other changes: the date, the pinned commit of every repository reference, the vector counts and verdict breakdown (Section 15.1), new sections Section 14.3 and Section 15.3, the independent implementation in Section 16.1, the reference's run results in Section 16.3, references [MUSUBI-EXERCISE], [I-D.he-agentproto-verifier-evidence] and [AEOESS-CHECK], the comparison with RFC 8785 in Section 4.1, the advice to implementers in Section 18.3, and the acknowledgements.¶
The parties behind agent-passport.org and kuangmi-bit.github.io published the contract, the anchored record and the header view of the first MUSUBI contract settled with no party from the project that publishes the reference; the real vectors are built from them. Two independent adversarial reviews of the reference found defects that this document now describes as closed. Tima (aeoess) wrote the first separately written implementation, and its questions and notes led to revisions 0.1 to 0.3 of the source text. Revision 0.4 was prompted by [I-D.he-agentproto-verifier-evidence]. kuangmi-bit wrote a third implementation from the text and the vectors alone, and its two notes led to revision 0.6. Poke-nushi recomputed a live settlement from the published bytes with headers fetched independently.¶