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
- 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 answers202 Accepted, and the command printsAcceptedand the proposal, with itsdigestandexpires. - A sends B the digest and the value they agreed, over a channel they trust.
- B runs
hedgehog cli gridspork-pendingand comparestypeanddatawith what was agreed, andsignerwith A’s public key. The node checks the keys and that the data can be acted on, not what was agreed. If it printsNo 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. - 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. - Both check the result with
hedgehog cli gridspork-get <type>andhedgehog 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/wireanswering 409 withThe 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 andThe 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.javaandGridSporkLog.javasit beside itapplication/src/main/java/org/unigrid/hedgehog/command/cli/spork/SporkProposal.java: fetching, checking, showing, signing and submitting a change;gridspork-get,gridspork-setandgridspork-growsit beside itapplication/src/main/java/org/unigrid/hedgehog/client/rest/SporkClient.java: the checks a command makes on what the node hands outapplication/src/main/java/org/unigrid/hedgehog/model/spork/PendingSporks.java: proposals waiting for a second keyapplication/src/main/java/org/unigrid/hedgehog/model/spork/SporkSuccession.java: which version or proposal follows on the stored spork, and which of two winsapplication/src/main/java/org/unigrid/hedgehog/model/spork/PendingSporkInfo.java: how a proposal is listed, with its digestapplication/src/main/java/org/unigrid/hedgehog/model/spork/SignatureLog.java: the signature history