Changing a spork

Part of Board instructions. How two board members propose and co-sign a change of a spork’s value.

A change takes two board members. The first signs a proposal. The second, using a different board key, co-signs it by its digest, and only then does the change become an accepted spork. The digest is the SHA-512 of the bytes both signatures cover, written as 128 lower-case hex characters.

sequenceDiagram
    participant A as Board member A
    participant NA as Node of A
    participant P as Other nodes
    participant NB as Node of B
    participant B as Board member B
    A->>NA: read the stored spork and the pending proposal
    A->>A: check, show, sign with key A
    A->>NA: hand back the signed spork
    NA-->>A: held as a proposal, with its digest
    NA->>P: publish the proposal
    P->>NB: pass on the proposal
    Note over NA,NB: every node holds it for up to 60 minutes
    A-->>B: send the digest
    B->>NB: read the proposal in wire form
    B->>B: check, show, co-sign with key B
    B->>NB: hand back the co-signed spork
    NB->>NB: check both signatures and the history, store
    NB->>P: publish the accepted spork
    P->>P: check, store and pass on

The steps

  1. Member A proposes on A’s node with the command for the spork type, for example hedgehog cli gridspork-set mint-supply -D 1000000000 -k <key of A>. The command reads what the node holds, prints it in full, signs and hands the spork back. The node answers 202 Accepted, and the command prints Accepted and the proposal, with its digest and expires.
  2. A sends B the digest and the value they agreed, over a channel they trust.
  3. B runs hedgehog cli gridspork-pending and compares type and data with what was agreed, and signer with A’s public key. The node checks the keys and that the data can be acted on, not what was agreed. If it prints No sporks await a co-signature, or the proposal is not listed, it has not reached B’s node yet. Nodes publish their proposals every three minutes.
  4. B co-signs within 60 minutes of A’s proposal: hedgehog cli gridspork-cosign -k <key of B> <digest>. A digest that is not 128 lower-case hex characters ends the command with exit code 2 and the usage, before the node is asked. It takes several digests and tries each one, even after an error.
  5. Both check the result with hedgehog cli gridspork-get <type> and hedgehog cli gridspork-log.

The storage, ledger-blacklist, ledger-fee and communication subcommands are hidden from --help. A node run without the hidden --preview switch refuses their sporks, POST /gridspork/wire answering 400 with This node takes no <TYPE> spork, so both members propose and co-sign them on nodes started with it, see Grid sporks.

gridspork-set and gridspork-grow make a first version only with --first-version. Without it, when the node reports no spork of the type, the command reads the JSON endpoint of the type, such as GET /gridspork/ledger-fee. If the node answers 503 with a reason, the command prints the status and that reason, such as 503 Service Unavailable: This release does not take the LEDGER_FEE spork from a node run without --preview for one of the four types above, and ends with exit code 1. Any other answer, or none, ends the command with The node holds no <TYPE> spork, so nothing was signed; give --first-version to make the first version of it and exit code 2. A node can hide what it holds, and a first version takes the place of any other, so give the option only for a type the network has no spork of. With it, the command skips that request, prints Making the first version of the <TYPE> spork, as the node holds none and signs, and a node that takes no spork of the type refuses the signed spork with 400.

What the command checks before it signs

Anything between the node and this machine can change what it hands out, so the command checks it first and sends nothing when it fails:

Check Message
The stored spork is signed and co-signed by two different keys that were ever board keys, with a log those keys signed The <TYPE> spork that the node holds is not one that board keys signed, so nothing is signed on top of it
The stored spork is not stamped more than 2 minutes ahead The <TYPE> spork that the node holds is stamped ahead of the clock of this machine, made at <time>, so nothing is signed on top of it
The proposal is signed by a current board key and made over the stored spork as its next version or its renewal The <TYPE> proposal that the node waits on is not one that a current board key made from the spork it holds, so nothing is signed on top of it
The proposal is at most 60 minutes old and not more than 2 minutes ahead The <TYPE> proposal that the node waits on was made at <time>, longer ago than a proposal waits, so nothing is signed on top of it, or the same with stamped ahead of the clock of this machine in place of longer ago than a proposal waits

gridspork-set and gridspork-grow print a failed check as Nothing was signed: <message>, and a spork that cannot be made or signed as The spork was not proposed: <message>; gridspork-cosign prints Co-signing <digest> refused: <message>. When the node refuses the proposal of gridspork-set or gridspork-grow with 400, the command prints the status and the node’s reason on standard error, such as 400 Bad Request: This node takes no STORAGE spork. When it refuses the proposal with 401 for its signatures, or with 409, the command prints the name of the status, such as Conflict, and the node’s reason on the next line. Both end with exit code 1.

A proposal must be made from what the node holds. A node holds a proposal made from the spork it stores, as the next version or as a renewal, whose last log entry names the stored head by its timestamp and digest. A proposal made over another copy of the stored version, which differs from it only in its signatures, is held as well. A node that stores none holds a first version made over nothing. A node that lags behind takes the proposal only after it has caught up; a sibling made over the stored version, and a first version over a stored spork, are not held. A first version is co-signed only when the operator says so: Co-signing <digest> refused: the node holds no <TYPE> spork, and a version over nothing takes the place of the one the network holds; give --first-version <TYPE> to co-sign the first version of it.

When co-signing fails

Message Cause What to do
The board key is not one of the current board keys of the network (see --network-keys) The key is not a current board key, or is malformed Use a current board key
No spork awaits a co-signature under <digest> The digest is unknown, the proposal expired, or it was already co-signed Check gridspork-pending and gridspork-get <type>; ask A to propose again
Co-signing <digest> refused: A second, different network key has to co-sign the spork B used A’s key A different board member must co-sign
The node answered <digest> with another spork, which was not co-signed The node holds no proposal under the digest, or hands out another one Read gridspork-pending again; the proposal may have been replaced
Co-signing <digest> refused: The spork does not follow on the stored spork and the proposal held The stored spork moved on, for example because a newer one was stored Read the stored spork, then ask A to propose again
Co-signing <digest> refused: the <TYPE> spork that the node holds is <signing>; give --switched-off <TYPE> to co-sign a renewal of it A key that signed the stored spork was switched off since Name the type with --switched-off; only a renewal of that spork is co-signed
Co-signing <digest> refused: the <TYPE> proposal is no renewal of the spork that the node holds, which is <signing>, and only a renewal is co-signed over it The stored spork was signed by a switched-off key and the proposal changes its data Use a node that stores the spork; the change must be proposed over a renewal of it

A spork that a switched-off key signed reads signed by <16 hex> (switched off) and <16 hex>. A refused co-signature leaves the proposal pending.

Proposals

stateDiagram-v2
    [*] --> Pending: member A signs with gridspork-set or gridspork-grow
    Pending --> Accepted: member B co-signs within 60 minutes
    Pending --> Replaced: a proposal that wins over it arrives
    Pending --> Dropped: 60 minutes pass, or the node restarts
    Accepted --> [*]: every node stores it and passes it on
    Replaced --> [*]
    Dropped --> [*]
  • A node holds one proposal per type. A newer proposal replaces the older one on every node, with these exceptions. A proposal for the board keys which holds less of the chain does not replace one that holds more. A renewal does not replace a waiting next version. A next version replaces a waiting renewal, even a newer one, when the renewal is stamped at most 60 minutes after it. A grow started while one is pending carries the pending entries forward, while a set replaces the pending value. Either way the digest changes, so take it from the newest answer.
  • A proposal lives 60 minutes from its own timestamp, in memory only; it is dropped without notice after that, and a node that restarts loses it. A node refuses a proposal stamped more than two minutes ahead of its clock, POST /gridspork/wire answering 409 with The spork is stamped further ahead of the clock of this node than clocks drift apart. It refuses one stamped more than 60 minutes before its clock, such as one stamped at the epoch, with 409 and The proposal is stamped more than 60 minutes from the clock of this node. After either answer, check the clock of the machine that signed.

When two changes race

Two board members may propose different changes over the same stored version, and each may be co-signed on a node that has not seen the other. Both versions are then valid, made over the same version with logs of the same length, so nodes hold one or the other for a moment. A proposal waits at most 60 minutes for its co-signature, so both fall within the change window, and every node ends up with the newer one; the older change is lost and has to be proposed again over the newer version. Members avoid a race by checking gridspork-pending before they propose and by changing one spork type at a time. Two changes of the board keys cannot race: each must hold the chain of the version it follows on.

flowchart LR
    S["Stored version"] --> X["Change X<br/>co-signed at 10:05"]
    S --> Y["Change Y<br/>co-signed at 10:20"]
    X -. "older, so it gives way" .-> W["Every node ends up with Y"]
    Y --> W
    W --> P["X is proposed again,<br/>over Y"]

Two changes made over two copies of one version that differ in their signatures, as when its proposal was co-signed twice, have logs that differ. They do not settle, and each node keeps the one it took first.

The 60-minute window in the acceptance rules exists because a node that shows a member an older version than the network holds could otherwise get a new version signed that drops the newest change. With the window, that works only within the hour after a change, and only when both members who sign use such a node.

Before signing, the signing commands print one line about the stored version: Stored <TYPE> made at <time>, digest <16 hex>, signed by <16 hex> and <16 hex>. gridspork-log on a second node shows the same time, digest and signers, so a member can compare the two before signing.

Examples

The keys, address, digests and times below are made up, and output that repeats data is abbreviated.

Mint storage. Member A wants 500 coins credited to a ledger account at 00:00 UTC on 1 December 2026. The account is 64 lower-case hex characters, and its entry names the time in epoch seconds. The command takes an ISO-8601 instant and turns it into 1796083200. An entry for an old-style Unigrid address names a height of the legacy chain instead, see Ledger sporks.

$ hedgehog cli gridspork-grow mint-storage --address 3f2a5b7c9d1e4f6081a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708 \
    --time 2026-12-01T00:00:00Z -D 500 -k <key of A>
Stored MINT_STORAGE made at 2026-10-01T09:00:00Z, digest 5d0c7e21b4a96f3e, signed by 44f21a0c9b7e6d18 and 7c0e35b9a1d24f68
Stored MINT_STORAGE data:
{
  "mints" : {
    …
  }
}
Proposed MINT_STORAGE data, signed now:
{
  "mints" : {
    …
    "3f2a…f708/1796083200" : 500
  }
}
Accepted
{
  "type" : "MINT_STORAGE",
  "timeStamp" : "2026-10-16T10:00:00Z",
  "expires" : "2026-10-16T11:00:00Z",
  "signer" : "<public key of A>",
  "digest" : "9c41d7e00123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef01234567",
  "data" : {
    "mints" : {
      …
      "3f2a…f708/1796083200" : 500
    }
  }
}

A sends B the digest with the account, amount and time. B checks the pending time and co-signs:

$ hedgehog cli gridspork-pending
$ hedgehog cli gridspork-cosign -k <key of B> \
    9c41d7e00123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef01234567
Proposal 9c41d7e0…01234567 of MINT_STORAGE, made at 2026-10-16T10:00:00Z and signed by <public key of A>
Stored MINT_STORAGE made at 2026-10-01T09:00:00Z, digest 5d0c7e21b4a96f3e, signed by 44f21a0c9b7e6d18 and 7c0e35b9a1d24f68
Stored data:
{
  "mints" : {
    …
  }
}
Proposed data:
{
  "mints" : {
    …
    "3f2a…f708/1796083200" : 500
  }
}
Co-signed 9c41d7e0…01234567

Storage. Member A raises the quota of a gridnode from 10 GiB to 20 GiB, on a node started with --preview. A set replaces the whole value, so the JSON carries every parameter:

$ hedgehog cli gridspork-set storage -k <key of A> -D '{
    "maxBytesPerNode": 21474836480, "chunkSize": 1048576, "fragmentSize": 65536,
    "outerParityPercent": 50, "maxOuterDataChunks": 32, "innerParityPercent": 50,
    "maxParityPercent": 100, "repairIntervalMinutes": 60, "tombstoneDays": 30,
    "manifestCopies": 3, "placementSlack": 8, "repairThresholdPercent": 50,
    "extraPoolPercent": 20 }'
Stored STORAGE made at 2026-10-01T09:00:00Z, digest 7b1e04aa9d2c8f31, signed by 44f21a0c9b7e6d18 and 7c0e35b9a1d24f68
Stored STORAGE data:
{
  "maxBytesPerNode" : 10737418240,
  …
}
Proposed STORAGE data, signed now:
{
  "maxBytesPerNode" : 21474836480,
  …
}
Accepted
{
  "type" : "STORAGE",
  "timeStamp" : "2026-10-16T10:00:00Z",
  "expires" : "2026-10-16T11:00:00Z",
  "signer" : "<public key of A>",
  "digest" : "e208ab550123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef01234567",
  "data" : {
    "maxBytesPerNode" : 21474836480,
    …
  }
}

B compares the proposed data with the agreed values:

$ hedgehog cli gridspork-cosign -k <key of B> \
    e208ab550123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef01234567
Proposal e208ab55…01234567 of STORAGE, made at 2026-10-16T10:00:00Z and signed by <public key of A>
Stored STORAGE made at 2026-10-01T09:00:00Z, digest 7b1e04aa9d2c8f31, signed by 44f21a0c9b7e6d18 and 7c0e35b9a1d24f68
Stored data:
{
  "maxBytesPerNode" : 10737418240,
  …
}
Proposed data:
{
  "maxBytesPerNode" : 21474836480,
  …
}
Co-signed e208ab55…01234567

If B had co-signed with A’s key, the command would have printed Co-signing <digest> refused: A second, different network key has to co-sign the spork.

In the source

  • application/src/main/java/org/unigrid/hedgehog/command/cli/GridSporkCosign.java: the co-sign command; GridSporkPending.java and GridSporkLog.java sit beside it
  • application/src/main/java/org/unigrid/hedgehog/command/cli/spork/SporkProposal.java: fetching, checking, showing, signing and submitting a change; gridspork-get, gridspork-set and gridspork-grow sit beside it
  • application/src/main/java/org/unigrid/hedgehog/client/rest/SporkClient.java: the checks a command makes on what the node hands out
  • application/src/main/java/org/unigrid/hedgehog/model/spork/PendingSporks.java: proposals waiting for a second key
  • application/src/main/java/org/unigrid/hedgehog/model/spork/SporkSuccession.java: which version or proposal follows on the stored spork, and which of two wins
  • application/src/main/java/org/unigrid/hedgehog/model/spork/PendingSporkInfo.java: how a proposal is listed, with its digest
  • application/src/main/java/org/unigrid/hedgehog/model/spork/SignatureLog.java: the signature history