Summary
KeystoreBase.changeKeystorePassphrase destroys any non-standard top-level field in a V3 keystore. Celo validator keystores generated by geth carry a blspublickey field, so changing such a keystore's passphrase silently loses it.
Cause
packages/sdk/keystores/src/keystore-base.ts re-encrypts by decrypting the private key and building a fresh V3 object. That object only ever contains version, id, address and crypto, so everything else in the original file is dropped:
in : {address, blspublickey, crypto, id, version}
out: {address, crypto, id, version}
^^^^^^^^^^^^ silently lost
There is no error and no warning — the write succeeds and the field is simply gone.
Reproduce
GETH_GEN_KEYSTORE1 in packages/sdk/keystores/src/test-constants.ts is a real geth-generated fixture containing a blspublickey. Persist it, call changeKeystorePassphrase, then re-read the raw entry: the field is absent.
Not a regression
This predates the removal of ethereumjs-wallet (#792). That library's toV3 had exactly the same behaviour, so the migration neither introduced nor worsened it. It was left alone there deliberately to keep that PR strictly behaviour-neutral and easy to review.
Suggested fix
Preserve unknown top-level fields across re-encryption — spread the parsed original and overwrite only crypto (and id), rather than constructing the object from scratch. Add a regression test using the GETH_GEN_KEYSTORE1 fixture asserting blspublickey survives a passphrase change.
Worth deciding at the same time whether importPrivateKey should be able to write such fields, or whether preservation on re-encryption is the whole scope.
Impact
Limited. @celo/keystores has no consumers inside this monorepo — celocli never reads or writes keystore files — so this only affects external npm consumers of the package who manage geth-generated validator keystores.
Summary
KeystoreBase.changeKeystorePassphrasedestroys any non-standard top-level field in a V3 keystore. Celo validator keystores generated by geth carry ablspublickeyfield, so changing such a keystore's passphrase silently loses it.Cause
packages/sdk/keystores/src/keystore-base.tsre-encrypts by decrypting the private key and building a fresh V3 object. That object only ever containsversion,id,addressandcrypto, so everything else in the original file is dropped:There is no error and no warning — the write succeeds and the field is simply gone.
Reproduce
GETH_GEN_KEYSTORE1inpackages/sdk/keystores/src/test-constants.tsis a real geth-generated fixture containing ablspublickey. Persist it, callchangeKeystorePassphrase, then re-read the raw entry: the field is absent.Not a regression
This predates the removal of
ethereumjs-wallet(#792). That library'stoV3had exactly the same behaviour, so the migration neither introduced nor worsened it. It was left alone there deliberately to keep that PR strictly behaviour-neutral and easy to review.Suggested fix
Preserve unknown top-level fields across re-encryption — spread the parsed original and overwrite only
crypto(andid), rather than constructing the object from scratch. Add a regression test using theGETH_GEN_KEYSTORE1fixture assertingblspublickeysurvives a passphrase change.Worth deciding at the same time whether
importPrivateKeyshould be able to write such fields, or whether preservation on re-encryption is the whole scope.Impact
Limited.
@celo/keystoreshas no consumers inside this monorepo — celocli never reads or writes keystore files — so this only affects external npm consumers of the package who manage geth-generated validator keystores.