Signed Operations
Every time you perform an operation that involves money movement or is otherwise a sensitive operation, the Lightspark SDK will use your private key to sign the operation and guarantee that the instructions:
- Come from the node’s owner
- Have not been tampered with
- Are not being replayed
- Are still valid
The Lightspark SDK client (instance of
LightsparkClient) stores the keys to your nodes in memory to make it easy for you to send instructions to your nodes.
Before performing sensitive operations like sending payments, you need to load the signing private key for your node.
You have a few options to load the private keys:- Recover the keys from Lightspark (OSK)
- Manually load the keys (OSK)
- Load a master seed (Remote Signing)
When you first create your node, the SDK automatically encrypts the private key locally and saves it on Lightspark services.
This means, that using your node password, you can recover the private key if needed, while Lightspark never has access to the unencrypted version.
Since Lightspark does not have your node password, Lightspark cannot decrypt the node signing key and thus cannot instruct your node to
perform money-movement operations or other sensitive operations ("Key Operations").
If you are using the SDK in test mode. The node password is
1234!@#$.await client.loadNodeSigningKey(
/* nodeID */ "LightsparkNode:0185789d-9948-f96b-0000-4ae0b696c75f",
/* loaderArgs */ { password: "s3cr37" },
);
The loaded key will be saved in memory in the Lightspark client instance for use in future operations.
If you saved your private key to a file, you can read its bytes and load them in the Lightspark client instance:
client.loadNodeKey(
/* nodeID */ "LightsparkNode:0185789d-9948-f96b-0000-4ae0b696c75f",
KeyOrAlias.key(signingPrivateKeyBytesPEM)
);
This section is only relevant if your account has been configured to use remote signing as described in the Remote Signing section.
Instead of recovering the private key from Lightspark, load in your master seed and the SDK will use it to generate the correct signing key.
await client.loadNodeSigningKey(
/* nodeID */ "LightsparkNode:0185789d-9948-f96b-0000-4ae0b696c75f",
/* loaderArgs */ { masterSeed: Uint8Array.from(bytes), network: BitcoinNetwork.REGTEST },
);
We strongly suggest that you use our tested open source SDKs to generate signed statements rather than manually generating the Signed Statements yourself.
Although we discourage this approach, if you nonetheless want to manually generate Signed Statements, this is how to do it.
Make the following GraphQL request to retrieve the encrypted private key and cipher information for your node.
query {
entity(id: $node_id) {
... on LightsparkNode {
encrypted_signing_private_key {
encrypted_value
cipher
}
}
}
}
Use PBKDF2/SHA256 with the node password to derive a 32 byte key and 12 byte nonce, and then use AES-GCM to decrypt the data. Sample implementation in Python:
import base64, hashlib, json
from Crypto.Cipher import AES
header = json.loads(cipher)
assert header["v"] == 4
decoded = base64.b64decode(encrypted_value)
salt = decoded[:16]
ciphertext = decoded[16:-16]
tag = decoded[-16:]
derived = hashlib.pbkdf2_hmac("sha256", password.encode("utf8"), salt, iterations=header["i"], dklen=44)
key = derived[:32]
nonce = derived[32:]
aes = AES.new(key, AES.MODE_GCM, nonce=nonce)
private_key = aes.decrypt_and_verify(ciphertext, tag)
Serialize the HTTP request body with the additional nonceand expires_at fields at the top level.
Then sign this with RSA-PSS/SHA256. You must sign the exact bytes which get sent in the request body.
Sample implementation in Python:
import json, secrets
from datetime import datetime, timedelta, timezone
from Crypto.Signature import pss
from Crypto.Hash import SHA256
from Crypto.PublicKey import RSA
request_body = json.dumps({
"query": query,
"variables": variables,
"nonce": secrets.randbelow(2**32),
"expires_at": (datetime.now(timezone.utc) + timedelta(hours=1)).isoformat(),
}).encode()
key = RSA.import_key(private_key)
signature = pss.new(key).sign(SHA256.new(request_body))
In this above example:
queryrepresents the GraphQL request sent in the request.variablesrepresents the GraphQL variables sent in the request.noncerepresents a random 32 bits integer. Your Lightning Node will record it and use it to make sure the operation is not replayed.expires_atis an ISO 8601 representation of the time when the instructions should expire. Usually we set it to 1 hour in the future.
For Key Operations, you will need to pass a header
X-LIGHTSPARK-SIGNING that contains the Signed Statement.Construct the header with the Base64-encoded signature.
X-Lightspark-Signing: {"v": 1, "signature": "<base64 encoded signature>"}