Environment
- arc-node version: v0.7.3 (90d71dc, HEAD of main as of 2026-08-16)
- Affected crate:
arc-remote-signer (crates/remote-signer)
- Signing config:
SigningConfig::Remote (consensus node configured to use an external gRPC signing service — the HSM/KMS-backed signing path)
- Component:
crates/malachite-app (Node::consensus_identity())
Description
RemoteSignerClient exposes two RPCs to the remote signing service, but only one is retried.
sign_message() → sign_message_with_retry() (crates/remote-signer/src/client.rs:172-215) wraps the gRPC call in backon's RetryableWithContext, using the configured RetryConfig (default: 3 retries, exponential backoff 100ms → 5s).
get_public_key() (client.rs:127-130) calls get_public_key_internal() (client.rs:133-170) exactly once. Any transient failure returns Err(RemoteSigningError::Status(...)) immediately, with zero retry.
get_public_key() is called exactly once, on the consensus startup path, from Node::consensus_identity() when SigningConfig::Remote is configured:
// crates/malachite-app/src/node.rs:373-376
let public_key = remote_provider
.public_key()
.await
.wrap_err("Failed to get public key from remote signer")?;
RemoteSigningProvider::public_key() (provider.rs:73-108) is a cache-then-fetch wrapper around client.get_public_key() — it does not add its own retry either.
If this single call fails, the error propagates via ? through setup_node_identity() → start() → run():
// crates/malachite-app/src/node.rs:769-781
Err(e) => {
let startup_error = e.wrap_err("Node failed to start");
error!("{startup_error:?}");
error!("Manual intervention required! Waiting for termination signal (SIGTERM)...");
wait_for_termination().await;
return Err(startup_error);
}
The process does not exit and does not retry — it blocks indefinitely in wait_for_termination(). Because the process stays alive, a supervisor watching for exit codes (systemd Restart=on-failure, k8s liveness via process exit) will not automatically restart it.
This creates an asymmetric reliability profile: consensus signing during steady-state is resilient to transient connectivity blips (3 retries with backoff). Node startup is not.
Steps to Reproduce
- Configure a consensus node with
SigningConfig::Remote pointing at a working remote-signer gRPC endpoint.
- Arrange for the first
public_key() gRPC call in consensus_identity() to fail — e.g.:
- Start the node before the remote-signer service has finished initializing, or
- Briefly block/reset the connection (e.g.
iptables -j REJECT on the signer port for ~1 request), then let it become reachable again moments later.
- Observe the node logs.
Expected Behavior
Given sign_message()'s existing retry precedent, get_public_key() should retry the same way — a transient failure that resolves within the retry window should not prevent node startup.
Actual Behavior
error: Failed to get public key from remote signer: ...
error: Node failed to start
error: Manual intervention required! Waiting for termination signal (SIGTERM)...
The node hangs indefinitely, even though the remote signer becomes reachable moments later.
Impact
For a validator running with a remote signer (the intended production HSM/KMS-backed setup), any transient hiccup between the validator and the remote signer at the exact moment of (re)startup — both restarted together during a deploy, remote signer still initializing, a brief network partition, a load-balancer health-check gap — causes the validator to fail to (re)join consensus and sit idle until an operator manually restarts it.
This is a liveness/availability gap, not a fund-loss or consensus-safety issue. It was evaluated as not meeting the bar for a private HackerOne report and is filed as a public reliability bug per SECURITY.md.
Proposed Fix
Extract a generic fetch_with_retry() helper and have both get_public_key() and sign_message() call it, so get_public_key() gets the same RetryConfig-driven exponential backoff. A fix is included in the accompanying PR.
Environment
arc-remote-signer(crates/remote-signer)SigningConfig::Remote(consensus node configured to use an external gRPC signing service — the HSM/KMS-backed signing path)crates/malachite-app(Node::consensus_identity())Description
RemoteSignerClientexposes two RPCs to the remote signing service, but only one is retried.sign_message()→sign_message_with_retry()(crates/remote-signer/src/client.rs:172-215) wraps the gRPC call inbackon'sRetryableWithContext, using the configuredRetryConfig(default: 3 retries, exponential backoff 100ms → 5s).get_public_key()(client.rs:127-130) callsget_public_key_internal()(client.rs:133-170) exactly once. Any transient failure returnsErr(RemoteSigningError::Status(...))immediately, with zero retry.get_public_key()is called exactly once, on the consensus startup path, fromNode::consensus_identity()whenSigningConfig::Remoteis configured:RemoteSigningProvider::public_key()(provider.rs:73-108) is a cache-then-fetch wrapper aroundclient.get_public_key()— it does not add its own retry either.If this single call fails, the error propagates via
?throughsetup_node_identity()→start()→run():The process does not exit and does not retry — it blocks indefinitely in
wait_for_termination(). Because the process stays alive, a supervisor watching for exit codes (systemdRestart=on-failure, k8s liveness via process exit) will not automatically restart it.This creates an asymmetric reliability profile: consensus signing during steady-state is resilient to transient connectivity blips (3 retries with backoff). Node startup is not.
Steps to Reproduce
SigningConfig::Remotepointing at a working remote-signer gRPC endpoint.public_key()gRPC call inconsensus_identity()to fail — e.g.:iptables -j REJECTon the signer port for ~1 request), then let it become reachable again moments later.Expected Behavior
Given
sign_message()'s existing retry precedent,get_public_key()should retry the same way — a transient failure that resolves within the retry window should not prevent node startup.Actual Behavior
error: Failed to get public key from remote signer: ...
error: Node failed to start
error: Manual intervention required! Waiting for termination signal (SIGTERM)...
The node hangs indefinitely, even though the remote signer becomes reachable moments later.
Impact
For a validator running with a remote signer (the intended production HSM/KMS-backed setup), any transient hiccup between the validator and the remote signer at the exact moment of (re)startup — both restarted together during a deploy, remote signer still initializing, a brief network partition, a load-balancer health-check gap — causes the validator to fail to (re)join consensus and sit idle until an operator manually restarts it.
This is a liveness/availability gap, not a fund-loss or consensus-safety issue. It was evaluated as not meeting the bar for a private HackerOne report and is filed as a public reliability bug per
SECURITY.md.Proposed Fix
Extract a generic
fetch_with_retry()helper and have bothget_public_key()andsign_message()call it, soget_public_key()gets the sameRetryConfig-driven exponential backoff. A fix is included in the accompanying PR.