diff --git a/cmd/node/main.go b/cmd/node/main.go index 4902c29..70f602c 100644 --- a/cmd/node/main.go +++ b/cmd/node/main.go @@ -1291,23 +1291,53 @@ type keyJSON struct { } 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 - if err := json.Unmarshal(data, &kj); err == nil { - if id, err := identity.FromHexFull(kj.PubKey, kj.PrivKey, kj.X25519Pub, kj.X25519Priv); err == nil { - // If the file is missing X25519 keys, backfill and re-save. - if kj.X25519Pub == "" { - 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 + if err := json.Unmarshal(data, &kj); err != nil { + log.Fatalf("[NODE] key file %s is not valid JSON: %v", keyFile, err) + } + id, err := identity.FromHexFull(kj.PubKey, kj.PrivKey, kj.X25519Pub, kj.X25519Priv) + if err != nil { + log.Fatalf("[NODE] key file %s is valid JSON but identity decode failed: %v", + keyFile, err) + } + // If the file is missing X25519 keys, backfill and re-save (best-effort, + // ignore write failure on read-only mounts). + if kj.X25519Pub == "" { + 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() if err != nil { log.Fatalf("generate identity: %v", err) @@ -1320,7 +1350,9 @@ func loadOrCreateIdentity(keyFile string) *identity.Identity { } data, _ := json.MarshalIndent(kj, "", " ") 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 { log.Printf("[NODE] new identity saved to %s", keyFile) }