Signer State
The signer is built on the Validating Lightning Signer (VLS). VLS does not sign blindly: before signing it checks the request against what it knows about the node, for example the current state of each channel, which destinations the on-chain wallet may send to, and which invoices and preimages it has seen. This knowledge is the signer state.
Greenlight signers are stateless by design. They do not own durable storage for the signer state, which is what allows a signer to run on a phone, in a short-lived process, or on several devices at once. Instead, the Greenlight node stores the signer state and attaches it to every request it sends to the signer.
This creates a tension with the security model: the signer protects funds against an attacker with access to the node infrastructure, yet that same infrastructure stores the state the signer relies on. This page describes how the signer can use state it receives from an untrusted store, what is protected today, what is planned, and which options an application embedding the signer controls.
Contents of the signer state
The signer state is a key-value map. Every entry consists of:
| Field | Description |
|---|---|
key |
The entry name, prefixed by its class: nodes/, nodestates/, channels/, allowlists/, trackers/, peers/ |
version |
A counter that increases with every change to this key |
value |
The serialized VLS object |
signature |
The signer's signature over the key, version and value |
Deleting a key leaves a tombstone behind: the entry is kept with the highest possible version and an empty value, so that an older value of the key cannot be merged back in.
Synchronization
The node and the signer keep their copies of the state in sync by exchanging only what changed:
- When a signer connects, the node sends it a heartbeat request that carries a full snapshot of the state.
- Every following request carries only the entries that changed since the node last sent state to that signer.
- The signer merges incoming entries into its in-memory copy. New keys are added, higher versions replace lower ones, lower versions are ignored.
- The signer processes the request and returns the entries it changed, signed.
- The node merges and persists the returned entries before it hands the signer's response on. If the entries cannot be stored, the response is dropped, so the node never acts on a signature whose state was lost.
The node pipelines requests, so several can be in flight at once, each carrying the state as it was when the request was sent. The signer handles them one after the other, and its in-memory state is always at least as new as the state attached to a request, so this is safe.
Multiple signers
A node can have several signers connected at the same time, for example the same wallet on a phone and a desktop. Every connected signer receives every request, the first response wins, and the node merges the state returned by all of them key by key. Because concurrent requests touch different keys, all signers make progress, and the node never needs to create or modify a signature itself.
Some cryptographic operations are not fully determined by their inputs. Two signers handling the same request may therefore return different but equally valid entries. The signer accepts either.
Protecting the state
There are three properties the signer needs from the state it receives:
| Property | Question | Mechanism | Status |
|---|---|---|---|
| Authenticity | Was every entry written by a signer of this node? | State signatures | Available |
| Freshness | Is any entry older than one this signer has already seen? | State anchor | Planned |
| Completeness | Is any entry this signer has seen missing? | State anchor | Planned |
Authenticity: state signatures
Each entry is signed by a state signing key that is derived from the node secret. All signers of the same node derive the same key, so they can verify each other's entries, while the node, which never has the secret, cannot produce valid signatures.
How strictly the signer verifies these signatures is controlled by the state signature mode:
| Mode | Missing signature | Invalid signature | Intended use |
|---|---|---|---|
off |
Accepted | Accepted | Development and tests only |
soft |
Accepted, and signed on the way out | Refused | Migrating nodes whose state predates state signatures |
hard |
Refused | Refused | Production, once all state is signed |
soft is the current default. In soft mode a signer signs unsigned
entries it receives, which gradually migrates nodes created before
state signing existed. Only hard mode guarantees that every entry the
signer acts on was written by a signer of this node.
Planned default
The default will move from soft to hard once the fleet's state
is fully signed. Applications embedding the signer should plan for
running in hard mode.
The mode is part of the signer configuration:
use gl_client::signer::{Signer, SignerConfig, StateSignatureMode};
let signer = Signer::new_with_config(
seed,
network,
creds,
SignerConfig {
state_signature_mode: StateSignatureMode::Hard,
..Default::default()
},
)?;
glcli signer run --state-signature-mode hard
Operator-assisted override
For manual recovery, a signer can be started with an override that
accepts missing and invalid state signatures for the lifetime of the
process. The override has to be acknowledged explicitly
(I_ACCEPT_OPERATOR_ASSISTED_STATE_OVERRIDE), and its use is reported
to Greenlight. It disables the authenticity guarantee and should only
be used together with the Greenlight team.
glcli signer run \
--state-override I_ACCEPT_OPERATOR_ASSISTED_STATE_OVERRIDE \
--state-override-note "support case reference"
Freshness and completeness: the state anchor
Planned
The state anchor is not available yet. Option and function names on this page may change before it ships.
Signatures show that an entry was written by a signer of the node, but not that it is the latest version, or that nothing was left out. A signer starting from scratch, which on mobile happens at every app launch, cannot tell an old, validly signed snapshot from a current one.
The state anchor closes that gap. It is a small record, kept on the device, of the highest version and a short hash of every key the signer has seen, including tombstones. It is not a copy of the state, and it contains no keys or secrets.
When a signer process starts and receives its first snapshot, it compares the snapshot against its anchor:
| Check | Condition | Outcome |
|---|---|---|
| Rollback | An entry is older than the anchored version | Refused |
| Omission | A key in the anchor is missing from the snapshot | Refused |
| Resurrection | A key the anchor knows as deleted comes back | Refused |
| Discrepancy | An entry has the anchored version but a different value | Accepted, reported |
A validly signed entry at the same or a higher version is always accepted: only another signer of the node can have produced it. Keys the anchor has never seen are accepted and are protected by their signatures.
The anchor only advances to versions the node has sent back to the signer, never to versions the signer merely wrote. This way a response that the node failed to store cannot leave the anchor ahead of the node's state and lock the signer out.
Signers without an anchor
A signer without an anchor trusts the first snapshot it receives and anchors it. This is the case on first start, after pairing a new device, after recovery, after reinstalling the application, and after the anchor has been cleared.
Losing the anchor, or restoring an older copy of it from a device backup, is safe: the anchor only ever records versions the node once had, so an older anchor is merely less protective. The anchor needs persistent storage, not secret storage.
Storing the anchor
gl-client itself does not access the filesystem, so that the signer
can also run in environments without one, such as a hardware security
module. It defines a storage interface for the anchor instead, and the
higher-level libraries provide implementations:
| Library | Anchor storage |
|---|---|
gl-client (Rust) |
Storage interface only; bring your own implementation |
gl-sdk |
File-backed, configured with a path in the application's data directory |
gl-client-py, glcli |
File-backed, configured with a path |
gl-testing |
In memory |
Enforcement
Anchor enforcement is a signer option with two values:
report: the checks run and their results are reported to Greenlight, but nothing is refused.enforce: rollback, omission and resurrection are refused.
The anchor is only meaningful together with hard state signature
mode, because soft mode accepts unsigned entries at any version. It
will start out as report and move to enforce after hard mode is
the default.
Like the state signature mode, the enforcement option is chosen by the application embedding the signer. The Greenlight node cannot change it.
Clearing the anchor
If a signer refuses to start because of an anchor check, either the state was tampered with, or the node's stored state went back in time for an operational reason, for example a restore from backup. The signer cannot tell these apart, and there is deliberately no way for the node to tell it.
Clearing the anchor on the device (reset_anchor() in gl-sdk,
--reset-anchor in glcli) makes the signer trust the next snapshot
again, like a signer without an anchor. Applications should only
offer this as an explicit user action, ideally after contacting
Greenlight support.
Limits
- A signer without an anchor trusts the first state it receives.
- The anchor protects what that particular device has seen. A key written by another signer, and never seen by this one, is protected by its signature only.
- A device that has been offline for a long time only refuses state older than what it saw when it was last online.