Signer API
The Signer API exposes Hydra App's SignerService — signing with the two key families a node holds, and verifying signatures produced by anyone.
JSON-RPC namespace: signer
Every method takes a network, because the network decides the signing scheme: a Bitcoin network signs the Bitcoin way, an EVM network the EVM way. The same message signed on two networks produces two different signatures.
Endpoints
The two key families
A node holds two unrelated sets of keys, and picking the wrong one produces a valid signature that the verifier rejects.
| Key | Signed with | What it is |
|---|---|---|
| Wallet (on-chain) | SignMessage | The key that holds and spends on-chain funds — a Bitcoin P2WPKH key, an Ethereum EOA key. This is the key an address belongs to |
| Node (off-chain) | NodeSignMessage | The key that identifies this node to its peers — the Lightning node key, the Lithium node key. This is the node_id half of a <node_id>@<host>:<port> peer string |
Neither is the identity key that authenticates to the Hydranet services — that one is Ed25519, derived from the seed by HKDF, and is not exposed for signing. Read it with
app.GetPublicKey.
The Message type
Both signing calls take a Message, a oneof over the three things a chain might ask you to sign:
| Arm | Payload | Use for |
|---|---|---|
data | { data: bytes } | Raw bytes. The signer applies the network's own message prefix / domain separation |
hash | { hash: bytes } | A pre-computed digest, signed directly |
typed | { payload: bytes } | A structured payload, e.g. EIP-712 typed data |
⚠️hashskips the prefix — that is the point, and the hazard
datais what you want for an ordinary "prove you own this address" message: the signer wraps it the way the chain expects, so wallets and verifiers agree.hashsigns exactly the 32 bytes you hand over, with no wrapping — which is what a protocol needs, and also what makes a strayhashsignature usable as a transaction signature. Only ever sign a hash you computed yourself, over data you fully control.
Sign Message
Signs a message with the wallet's on-chain key for a network.
Method: SignMessage
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Target network — determines the signing scheme |
message | Message | YES | What to sign — see Message |
Response:
| Field | Type | Description |
|---|---|---|
signature | bytes | The signature. Base64 over JSON-RPC, raw bytes over gRPC |
Example Request:
import { SignerServiceClient } from './proto/SignerServiceClientPb'
import { SignMessageRequest } from './proto/signer_pb'
const signer = new SignerServiceClient('http://localhost:5003')
const request = new SignMessageRequest()
request.setNetwork({ protocol: 2, id: '11155111' })
request.setMessage({ data: { data: new TextEncoder().encode('hello hydra') } })
const sig = (await signer.signMessage(request, {})).getSignature_asU8()
console.log('signature:', Buffer.from(sig).toString('hex'))
Node Sign Message
Signs a message with the node's off-chain key for a network — the key peers know this node by.
Method: NodeSignMessage
Parameters and response: identical to SignMessage; only the key differs.
Use this to prove control of a
node_id— for instance to a counterparty that knows you only by your peer string. A wallet-key signature will not verify against anode_id.
Verify Signature
Checks a signature against a message and a public key. Verifies anyone's signature, not just this node's.
Method: VerifySignature
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Target network — determines the verification scheme |
message | Message | YES | The message that was signed. Must be the same arm the signer used |
public_key | string | YES | The signer's public key, hex or the protocol's own format |
signature | bytes | YES | The signature to check |
Response:
| Field | Type | Description |
|---|---|---|
valid | bool | true when the signature is valid for that message and key |
A
falseis a plain answer, not an error. The call succeeds; the boolean is the result. Treat an RPC error as "could not check", which is a different outcome from "checked and invalid".
Verifying a
datasignature against ahashmessage (or vice versa) returnsfalseeven when the underlying bytes match — the arm is part of what was signed.
Sign Transaction
Signs an unsigned transaction offline and returns the serialized signed bytes. Does not broadcast.
Method: SignTransaction
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Target network |
raw_tx | bytes | YES | The serialized unsigned transaction request — the signable_data from a Create*Transaction response |
Response:
| Field | Type | Description |
|---|---|---|
signed_tx | bytes | The serialized signed transaction, ready to broadcast |
signed_txisbytes, not a structured message. It changed from aSignedTransactionRequestto serialized bytes on 2026-06-11 — see the changelog.
This fails for a broadcast-only signer. A signer that cannot produce standalone signed bytes — MetaMask and anything else that signs and broadcasts as one action — is rejected here. Use
SignAndBroadcastTxfor those.
Feed the result to client.FinalizeAndBroadcastTransaction, or broadcast it yourself with blockchain.BroadcastRawTransaction.
Sign And Broadcast Tx
Signs (or authorises) an unsigned transaction and makes sure it reaches the chain, returning the txid.
Method: SignAndBroadcastTx
Parameters: identical to SignTransaction — network and raw_tx.
Response:
| Field | Type | Description |
|---|---|---|
txid | string | The broadcast transaction id |
This is the universal path. An offline signer signs and the node broadcasts; a self-broadcasting authority signs and broadcasts in one step. It works for every signer, which
SignTransactiondoes not — prefer it unless you specifically need the signed bytes in hand.
Error Handling
| Error Code | Description | Solution |
|---|---|---|
INVALID_ARGUMENT | No message arm set, malformed raw_tx, or an unparseable public_key | Set exactly one arm; pass the signable_data from a Create*Transaction unmodified |
FAILED_PRECONDITION | SignTransaction on a broadcast-only signer | Use SignAndBroadcastTx |
UNIMPLEMENTED | The network does not support that signing scheme | Check the arm against the protocol — typed is EVM-shaped |
Best Practices
- Pick the key family deliberately — wallet key for an address, node key for a
node_id. A signature from the wrong one verifies against nothing. - Prefer
dataoverhash. Let the signer apply the network's prefix; sign a bare digest only for a protocol that requires it, and only over bytes you produced. - Prefer
SignAndBroadcastTxunless you need the signed bytes — it is the one path that works for every signer. - Treat
valid: falseas an answer, not a failure, and an RPC error as "unknown". - Never log a signature alongside the message and key in a place you would not log a credential; together they are a reusable proof.