fix(node): fail loudly when key file exists but is unreadable
Operator hit this in the wild: keys/node.json mounted into a container as 600 root:root while the node process runs as an unprivileged user. os.ReadFile returned a permission error, loadOrCreateIdentity fell through to "generate a new identity", and genesis allocation (21M tokens) was credited to the auto-generated key — which then vanished when the container restarted because the read-only mount also couldn't be written. The symptom was a 0-balance import: operators extracted node.json from the host keys dir, imported it into the mobile client, and wondered why the genesis validator's wallet was empty. Fix: distinguish "file doesn't exist" (first boot, generate) from "file exists but can't be read" (operator error, log.Fatalf with a hint about permissions / read-only mount). Also fail loudly on JSON parse errors and decode errors instead of silently generating. When the new-identity path is taken and the save fails (read-only mount), the warning now explicitly says the key is ephemeral and the node's identity will change on restart — operators can catch this before genesis commits to a throwaway key. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1291,23 +1291,53 @@ type keyJSON struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func loadOrCreateIdentity(keyFile string) *identity.Identity {
|
func loadOrCreateIdentity(keyFile string) *identity.Identity {
|
||||||
if data, err := os.ReadFile(keyFile); err == nil {
|
// Key-file handling has a silent-failure mode that cost a genesis
|
||||||
|
// validator 21M tokens in the wild: if the file exists but we can't
|
||||||
|
// read it (e.g. mounted read-only under a different UID), ReadFile
|
||||||
|
// returns an error, we fall through to "generate", and the operator
|
||||||
|
// ends up with an ephemeral key whose pubkey doesn't match what's in
|
||||||
|
// keys/node.json on disk. Genesis allocation then lands on the
|
||||||
|
// ephemeral key that vanishes on restart.
|
||||||
|
//
|
||||||
|
// Distinguish "file doesn't exist" (normal — first boot, create)
|
||||||
|
// from "file exists but unreadable" (operator error — fail loudly).
|
||||||
|
if info, err := os.Stat(keyFile); err == nil {
|
||||||
|
// File is there. Any read failure now is an operator problem,
|
||||||
|
// not a bootstrap case.
|
||||||
|
_ = info
|
||||||
|
data, err := os.ReadFile(keyFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("[NODE] key file %s exists but can't be read: %v\n"+
|
||||||
|
"\thint: check file perms (should be readable by the node user) "+
|
||||||
|
"and that the mount isn't unexpectedly read-only.",
|
||||||
|
keyFile, err)
|
||||||
|
}
|
||||||
var kj keyJSON
|
var kj keyJSON
|
||||||
if err := json.Unmarshal(data, &kj); err == nil {
|
if err := json.Unmarshal(data, &kj); err != nil {
|
||||||
if id, err := identity.FromHexFull(kj.PubKey, kj.PrivKey, kj.X25519Pub, kj.X25519Priv); err == nil {
|
log.Fatalf("[NODE] key file %s is not valid JSON: %v", keyFile, err)
|
||||||
// If the file is missing X25519 keys, backfill and re-save.
|
}
|
||||||
if kj.X25519Pub == "" {
|
id, err := identity.FromHexFull(kj.PubKey, kj.PrivKey, kj.X25519Pub, kj.X25519Priv)
|
||||||
kj.X25519Pub = id.X25519PubHex()
|
if err != nil {
|
||||||
kj.X25519Priv = id.X25519PrivHex()
|
log.Fatalf("[NODE] key file %s is valid JSON but identity decode failed: %v",
|
||||||
if out, err2 := json.MarshalIndent(kj, "", " "); err2 == nil {
|
keyFile, err)
|
||||||
_ = os.WriteFile(keyFile, out, 0600)
|
}
|
||||||
}
|
// If the file is missing X25519 keys, backfill and re-save (best-effort,
|
||||||
}
|
// ignore write failure on read-only mounts).
|
||||||
log.Printf("[NODE] loaded identity from %s", keyFile)
|
if kj.X25519Pub == "" {
|
||||||
return id
|
kj.X25519Pub = id.X25519PubHex()
|
||||||
|
kj.X25519Priv = id.X25519PrivHex()
|
||||||
|
if out, err2 := json.MarshalIndent(kj, "", " "); err2 == nil {
|
||||||
|
_ = os.WriteFile(keyFile, out, 0600)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
log.Printf("[NODE] loaded identity from %s", keyFile)
|
||||||
|
return id
|
||||||
|
} else if !os.IsNotExist(err) {
|
||||||
|
// Something other than "file not found" — permission on the
|
||||||
|
// containing directory, broken symlink, etc. Also fail loudly.
|
||||||
|
log.Fatalf("[NODE] stat %s: %v", keyFile, err)
|
||||||
}
|
}
|
||||||
|
// File genuinely doesn't exist — first boot. Generate + save.
|
||||||
id, err := identity.Generate()
|
id, err := identity.Generate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("generate identity: %v", err)
|
log.Fatalf("generate identity: %v", err)
|
||||||
@@ -1320,7 +1350,9 @@ func loadOrCreateIdentity(keyFile string) *identity.Identity {
|
|||||||
}
|
}
|
||||||
data, _ := json.MarshalIndent(kj, "", " ")
|
data, _ := json.MarshalIndent(kj, "", " ")
|
||||||
if err := os.WriteFile(keyFile, data, 0600); err != nil {
|
if err := os.WriteFile(keyFile, data, 0600); err != nil {
|
||||||
log.Printf("[NODE] warning: could not save key: %v", err)
|
log.Printf("[NODE] warning: could not save key to %s: %v "+
|
||||||
|
"(ephemeral key in use — this node's identity will change on restart!)",
|
||||||
|
keyFile, err)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("[NODE] new identity saved to %s", keyFile)
|
log.Printf("[NODE] new identity saved to %s", keyFile)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user