Skip to content

Add grindKeyPair and grindKeyPairSigner for mining vanity addresses#1534

Merged
lorisleiva merged 1 commit into
mainfrom
04-08-add_grindkeypair_and_grindkeypairsigner_for_mining_vanity_addresses
Apr 9, 2026
Merged

Add grindKeyPair and grindKeyPairSigner for mining vanity addresses#1534
lorisleiva merged 1 commit into
mainfrom
04-08-add_grindkeypair_and_grindkeypairsigner_for_mining_vanity_addresses

Conversation

@lorisleiva
Copy link
Copy Markdown
Member

This PR adds grindKeyPair, grindKeyPairs, grindKeyPairSigner, and grindKeyPairSigners for mining vanity Solana addresses whose base58-encoded public key matches a RegExp or a custom predicate. Key pairs are generated in parallel batches of concurrency (defaulting to 32), with each batch also exporting and base58-decoding the public key bytes concurrently so the matching phase is fully synchronous. Cancellation is handled via getAbortablePromise so that firing the abortSignal rejects the grind immediately without waiting for in-flight key generations to settle. Regex matchers have their literal characters validated against the base58 alphabet up front — after stripping escapes, character classes, quantifiers, and groups — to catch common typos like /^sol0/ before an infinite grind starts, with the new SOLANA_ERROR__KEYS__INVALID_BASE58_IN_GRIND_REGEX error code. The signer variants are thin wrappers around the key-pair variants that pipe results through createSignerFromKeyPair.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 8, 2026

🦋 Changeset detected

Latest commit: 206f03e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 46 packages
Name Type
@solana/signers Minor
@solana/errors Minor
@solana/keys Minor
@solana/kit Minor
@solana/plugin-interfaces Minor
@solana/program-client-core Minor
@solana/react Minor
@solana/wallet-account-signer Minor
@solana/accounts Minor
@solana/addresses Minor
@solana/assertions Minor
@solana/codecs-core Minor
@solana/codecs-data-structures Minor
@solana/codecs-numbers Minor
@solana/codecs-strings Minor
@solana/compat Minor
@solana/instruction-plans Minor
@solana/instructions Minor
@solana/offchain-messages Minor
@solana/options Minor
@solana/programs Minor
@solana/rpc-api Minor
@solana/rpc-spec Minor
@solana/rpc-subscriptions-channel-websocket Minor
@solana/rpc-subscriptions-spec Minor
@solana/rpc-subscriptions Minor
@solana/rpc-transformers Minor
@solana/rpc-transport-http Minor
@solana/rpc-types Minor
@solana/rpc Minor
@solana/subscribable Minor
@solana/sysvars Minor
@solana/transaction-confirmation Minor
@solana/transaction-messages Minor
@solana/transactions Minor
@solana/rpc-graphql Minor
@solana/rpc-subscriptions-api Minor
@solana/rpc-parsed-types Minor
@solana/codecs Minor
@solana/fast-stable-stringify Minor
@solana/functional Minor
@solana/nominal-types Minor
@solana/plugin-core Minor
@solana/promises Minor
@solana/rpc-spec-types Minor
@solana/webcrypto-ed25519-polyfill Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link
Copy Markdown
Member Author

lorisleiva commented Apr 8, 2026

@lorisleiva lorisleiva marked this pull request as ready for review April 8, 2026 16:55
@bundlemon
Copy link
Copy Markdown

bundlemon Bot commented Apr 8, 2026

BundleMon

Files updated (13)
Status Path Size Limits
keys/dist/index.native.mjs
2.78KB (+737B +34.88%) -
keys/dist/index.node.mjs
2.78KB (+737B +34.88%) -
keys/dist/index.browser.mjs
2.78KB (+736B +34.8%) -
@solana/kit production bundle
kit/dist/index.production.min.js
47.06KB (+449B +0.94%) -
errors/dist/index.browser.mjs
19.47KB (+110B +0.55%) -
errors/dist/index.native.mjs
19.47KB (+110B +0.55%) -
errors/dist/index.node.mjs
19.49KB (+110B +0.55%) -
wallet-account-signer/dist/index.browser.mjs
16.51KB (+101B +0.6%) -
wallet-account-signer/dist/index.native.mjs
16.51KB (+100B +0.59%) -
wallet-account-signer/dist/index.node.mjs
16.53KB (+100B +0.59%) -
signers/dist/index.node.mjs
3.23KB (+82B +2.54%) -
signers/dist/index.browser.mjs
3.23KB (+81B +2.51%) -
signers/dist/index.native.mjs
3.23KB (+81B +2.51%) -
Unchanged files (129)
Status Path Size Limits
rpc-graphql/dist/index.browser.mjs
18.82KB -
rpc-graphql/dist/index.native.mjs
18.81KB -
rpc-graphql/dist/index.node.mjs
18.81KB -
transaction-messages/dist/index.browser.mjs
11.32KB -
transaction-messages/dist/index.native.mjs
11.32KB -
transaction-messages/dist/index.node.mjs
11.32KB -
instruction-plans/dist/index.browser.mjs
6.58KB -
instruction-plans/dist/index.native.mjs
6.58KB -
instruction-plans/dist/index.node.mjs
6.58KB -
codecs-data-structures/dist/index.browser.mjs
5.04KB -
codecs-data-structures/dist/index.native.mjs
5.03KB -
codecs-data-structures/dist/index.node.mjs
5.03KB -
offchain-messages/dist/index.browser.mjs
4.89KB -
offchain-messages/dist/index.native.mjs
4.89KB -
offchain-messages/dist/index.node.mjs
4.89KB -
transactions/dist/index.browser.mjs
4.07KB -
transactions/dist/index.native.mjs
4.07KB -
transactions/dist/index.node.mjs
4.07KB -
codecs-core/dist/index.browser.mjs
3.62KB -
codecs-core/dist/index.native.mjs
3.62KB -
codecs-core/dist/index.node.mjs
3.62KB -
webcrypto-ed25519-polyfill/dist/index.node.mj
s
3.61KB -
webcrypto-ed25519-polyfill/dist/index.browser
.mjs
3.59KB -
webcrypto-ed25519-polyfill/dist/index.native.
mjs
3.57KB -
rpc-subscriptions/dist/index.browser.mjs
3.37KB -
rpc-subscriptions/dist/index.node.mjs
3.34KB -
rpc-subscriptions/dist/index.native.mjs
3.31KB -
kit/dist/index.browser.mjs
3.29KB -
kit/dist/index.native.mjs
3.29KB -
kit/dist/index.node.mjs
3.29KB -
rpc-transformers/dist/index.browser.mjs
3.16KB -
rpc-transformers/dist/index.native.mjs
3.16KB -
rpc-transformers/dist/index.node.mjs
3.16KB -
react/dist/index.browser.mjs
3.09KB -
react/dist/index.native.mjs
3.09KB -
react/dist/index.node.mjs
3.09KB -
addresses/dist/index.browser.mjs
2.93KB -
addresses/dist/index.native.mjs
2.92KB -
addresses/dist/index.node.mjs
2.92KB -
codecs-strings/dist/index.browser.mjs
2.55KB -
codecs-strings/dist/index.node.mjs
2.51KB -
codecs-strings/dist/index.native.mjs
2.47KB -
transaction-confirmation/dist/index.node.mjs
2.41KB -
sysvars/dist/index.browser.mjs
2.37KB -
sysvars/dist/index.native.mjs
2.37KB -
sysvars/dist/index.node.mjs
2.37KB -
transaction-confirmation/dist/index.native.mj
s
2.36KB -
transaction-confirmation/dist/index.browser.m
js
2.35KB -
rpc-subscriptions-spec/dist/index.node.mjs
2.21KB -
rpc-subscriptions-spec/dist/index.native.mjs
2.17KB -
rpc-subscriptions-spec/dist/index.browser.mjs
2.16KB -
subscribable/dist/index.node.mjs
1.97KB -
rpc/dist/index.node.mjs
1.95KB -
codecs-numbers/dist/index.browser.mjs
1.95KB -
codecs-numbers/dist/index.native.mjs
1.95KB -
codecs-numbers/dist/index.node.mjs
1.94KB -
subscribable/dist/index.native.mjs
1.92KB -
subscribable/dist/index.browser.mjs
1.91KB -
rpc-transport-http/dist/index.browser.mjs
1.91KB -
rpc-transport-http/dist/index.native.mjs
1.9KB -
rpc/dist/index.native.mjs
1.81KB -
rpc/dist/index.browser.mjs
1.8KB -
rpc-transport-http/dist/index.node.mjs
1.72KB -
rpc-types/dist/index.browser.mjs
1.53KB -
rpc-types/dist/index.native.mjs
1.53KB -
rpc-types/dist/index.node.mjs
1.53KB -
rpc-subscriptions-channel-websocket/dist/inde
x.node.mjs
1.33KB -
rpc-subscriptions-channel-websocket/dist/inde
x.native.mjs
1.27KB -
rpc-subscriptions-channel-websocket/dist/inde
x.browser.mjs
1.26KB -
program-client-core/dist/index.browser.mjs
1.21KB -
program-client-core/dist/index.native.mjs
1.21KB -
program-client-core/dist/index.node.mjs
1.21KB -
options/dist/index.browser.mjs
1.18KB -
options/dist/index.native.mjs
1.18KB -
options/dist/index.node.mjs
1.17KB -
accounts/dist/index.browser.mjs
1.17KB -
accounts/dist/index.native.mjs
1.17KB -
accounts/dist/index.node.mjs
1.16KB -
rpc-api/dist/index.browser.mjs
976B -
rpc-api/dist/index.native.mjs
975B -
rpc-api/dist/index.node.mjs
973B -
compat/dist/index.browser.mjs
969B -
compat/dist/index.native.mjs
968B -
compat/dist/index.node.mjs
966B -
rpc-spec-types/dist/index.browser.mjs
962B -
rpc-spec-types/dist/index.native.mjs
961B -
rpc-spec-types/dist/index.node.mjs
959B -
rpc-subscriptions-api/dist/index.native.mjs
870B -
rpc-subscriptions-api/dist/index.node.mjs
869B -
rpc-subscriptions-api/dist/index.browser.mjs
868B -
rpc-spec/dist/index.browser.mjs
852B -
rpc-spec/dist/index.native.mjs
851B -
rpc-spec/dist/index.node.mjs
850B -
promises/dist/index.browser.mjs
799B -
promises/dist/index.native.mjs
798B -
promises/dist/index.node.mjs
797B -
assertions/dist/index.browser.mjs
783B -
instructions/dist/index.browser.mjs
771B -
instructions/dist/index.native.mjs
770B -
instructions/dist/index.node.mjs
768B -
plugin-core/dist/index.browser.mjs
749B -
plugin-core/dist/index.native.mjs
749B -
plugin-core/dist/index.node.mjs
747B -
fast-stable-stringify/dist/index.browser.mjs
726B -
fast-stable-stringify/dist/index.native.mjs
725B -
assertions/dist/index.native.mjs
724B -
fast-stable-stringify/dist/index.node.mjs
724B -
assertions/dist/index.node.mjs
723B -
programs/dist/index.browser.mjs
329B -
programs/dist/index.native.mjs
327B -
programs/dist/index.node.mjs
325B -
event-target-impl/dist/index.node.mjs
230B -
functional/dist/index.browser.mjs
154B -
functional/dist/index.native.mjs
152B -
text-encoding-impl/dist/index.native.mjs
152B -
functional/dist/index.node.mjs
151B -
codecs/dist/index.browser.mjs
137B -
codecs/dist/index.native.mjs
136B -
codecs/dist/index.node.mjs
134B -
event-target-impl/dist/index.browser.mjs
133B -
ws-impl/dist/index.node.mjs
131B -
text-encoding-impl/dist/index.browser.mjs
122B -
text-encoding-impl/dist/index.node.mjs
119B -
ws-impl/dist/index.browser.mjs
113B -
crypto-impl/dist/index.node.mjs
111B -
crypto-impl/dist/index.browser.mjs
109B -
rpc-parsed-types/dist/index.browser.mjs
66B -
rpc-parsed-types/dist/index.native.mjs
65B -
rpc-parsed-types/dist/index.node.mjs
63B -

Total files change +3.45KB +0.71%

Final result: ✅

View report in BundleMon website ➡️


Current branch size history | Target branch size history

@trevor-cortex
Copy link
Copy Markdown

trevor-cortex commented Apr 8, 2026

Re: STRIP_UNVALIDATED_REGEX_PARTS — a few edge cases to consider for packages/keys/src/grind-keypair.ts:

  1. Nested groups aren't fully stripped. [^)]* stops at the first ), so for /((a)0)/ the strip consumes ((a) and leaves 0)0 is then rejected. The rejection happens to be correct here (outer group literally requires a0), but the reasoning inside the validator is wrong: the 0 is not actually at the top level of the regex. For a less contrived example, consider something like /(a(b)c)/ — the strip consumes (a(b) leaving c), which happens to be fine, but the behaviour is fragile. A hand-rolled depth-counting scan over the source would be more robust than a single regex with [^)]*, and wouldn't cost much.

  2. Escaped literal brackets leak their contents. /\[foo\]/ has source \[foo\]. The \\. alternative matches \[ and \] first, so the strip leaves foo. That's fine because foo is base58, but /\[foo0\]/ would surface a validation error whose character: '0' message refers to a character that is inside what the user sees as a literal [foo0]. The rejection is still semantically correct (the regex can never match an address containing [foo0]), but the error message may be confusing. Worth a comment, or adjusting the error wording.

  3. /\0/ is accepted. The test { regex: /\0/ } is in the accepted list — \0 is the NUL character, which of course never appears in a base58 address, so this regex is guaranteed to never match. The escape strip makes this pass validation, which is a false negative. Not a blocker, and detecting this generally is hard, but you might want a separate check for \0, \n, \t, \x00, \u0000, etc. — or just document that the validator only catches literal-character typos, not semantically-impossible regexes.

  4. BASE58_ALPHABET_REGEX uses * instead of +. /^[1-9A-HJ-NP-Za-km-z]*$/ accepts the empty string. In practice isBase58Character is only ever called with a single code point from for (const character of stripped), so this is harmless — but + would be marginally more defensive.

None of these are blockers; the current implementation correctly rejects the common typos it's designed to catch. Just flagging for visibility.

EDIT (Loris): Hmm I'm pretty sure the regex is greedy by default and therefore it will eat the whole group and everything inside of it.

Copy link
Copy Markdown

@trevor-cortex trevor-cortex left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

This PR adds grindKeyPair/grindKeyPairs in @solana/keys and grindKeyPairSigner/grindKeyPairSigners in @solana/signers for mining vanity Solana addresses. The implementation generates key pairs in parallel batches of concurrency (default 32), tests each candidate against a RegExp or predicate, and supports abortSignal-based cancellation via getAbortablePromise so the grind rejects immediately without waiting for in-flight generations. Regex matchers are statically validated against the base58 alphabet up front (after stripping escapes, character classes, quantifiers, and groups) to catch typos like /^sol0/, surfaced via a new SOLANA_ERROR__KEYS__INVALID_BASE58_IN_GRIND_REGEX error code. The signer variants are thin wrappers around the key-pair variants that pipe results through createSignerFromKeyPair.

Things to watch

  • Regex validation soundness. The strip-then-check approach in assertGrindRegexIsValid is pragmatic but lossy. I've flagged a few specific cases inline (notably nested groups and /\0/-style escapes whose semantics can never match base58). No hard correctness bugs that I can construct, but the comment "cannot be analyzed reliably without a full regex parser" is accurate — reviewers should decide if they're comfortable with the current trade-offs.
  • Redundant address derivation in the signer path. grindKeyPairs exports + base58-decodes every candidate's public key during matching, and then createSignerFromKeyPair calls getAddressFromPublicKey again for each winning key pair. Minor; mentioned inline.
  • Test flakiness. The grindKeyPair tests that use the real generateKeyPair with /^1/ are probabilistic (~1/58 per key). With concurrency 32 this should virtually always finish within Jest's default timeout, but it's worth noting.
  • concurrency overshoot. A request for amount: 1 with default concurrency: 32 always runs a full batch, so the "generates key pairs in batches of concurrency" test is correct — but callers targeting rare patterns with very small amount pay for a full batch even after a match is found mid-batch. Documented behaviour, just flagging it.

Notes for subsequent reviewers

  • Please double-check the regex-stripping logic against your own pathological inputs — especially nested groups, escaped brackets/parens (/\[0\]/), and things like /\d/ (currently skipped via the escape rule, which is the right call but worth confirming).
  • Confirm crypto.subtle.exportKey('raw', publicKey) works for Ed25519 on all supported runtimes. Public keys are always extractable regardless of the extractable flag, so this should be fine, but the code path is new.
  • The new @solana/promises workspace dep in packages/keys/package.json should be sanity-checked against the overall dependency graph policy for @solana/keys.
  • Docs look thorough and the examples match the API surface. Changeset is correct (minor bump on three packages).

@trevor-cortex
Copy link
Copy Markdown

Minor perf note on packages/signers/src/grind-keypair-signer.ts:

grindKeyPairs already does crypto.subtle.exportKey('raw', keyPair.publicKey) + base58 decode for every candidate key pair during the grind loop. Then createSignerFromKeyPair (called via Promise.all(keyPairs.map(createSignerFromKeyPair))) calls getAddressFromPublicKey(keyPair.publicKey), which performs the same export + decode again for each winning key pair.

For amount: 1 this is negligible (one extra export), but for large amount it's N redundant exports. If you want to eliminate it, grindKeyPairs could optionally return { keyPair, address } tuples (or expose an internal variant that does), and the signer wrapper could construct the signer without re-deriving the address. Happy to leave this for a follow-up — just flagging since the grind path is explicitly optimized for throughput.

@trevor-cortex
Copy link
Copy Markdown

Re: packages/keys/src/__tests__/grind-keypair-test.ts — the two tests that use the real generateKeyPair ('returns a key pair whose address matches the provided regex' and 'returns a key pair whose address satisfies the provided predicate') both match /^1/, which has ~1/58 probability per candidate. With concurrency: 32 that's >42% chance per batch, so median time to find is ~1 batch, but the tail is long — there's a real chance of 10+ batches occasionally. At default Jest timeouts this should be fine, but if CI is tight you might prefer /^[1-9]/ (prob ~9/58 ≈ 15.5%) for robustness, or mock generateKeyPair and feed it deterministic keys.

The other tests correctly mock generateKeyPair with () => true matchers, which is the right pattern for the rest of the suite.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 8, 2026

Documentation Preview: https://kit-docs-k0ngr7lmx-anza-tech.vercel.app

@lorisleiva lorisleiva requested a review from mcintyre94 April 8, 2026 17:14
Copy link
Copy Markdown
Member

@mcintyre94 mcintyre94 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! Not sure if it'd just be worth a comment in the tests to keep matchers very simple to avoid flakiness there. Makes sense that WebCrypto can't be seeded and we shouldn't make the tests do something else just to make them deterministic.

// - `[...]` character classes
// - `{...}` quantifiers
// - `(...)` groups
const STRIP_UNVALIDATED_REGEX_PARTS = /\\.|\[[^\]]*\]|\{[^}]*\}|\([^)]*\)/g;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yo dawg, I heard you like regex 😅

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imma regex ya regex! 😅

@lorisleiva lorisleiva force-pushed the 04-08-add_grindkeypair_and_grindkeypairsigner_for_mining_vanity_addresses branch from d0e164a to ef02578 Compare April 9, 2026 13:14
This PR adds `grindKeyPair`, `grindKeyPairs`, `grindKeyPairSigner`, and `grindKeyPairSigners` for mining vanity Solana addresses whose base58-encoded public key matches a `RegExp` or a custom predicate. Key pairs are generated in parallel batches of `concurrency` (defaulting to `32`), with each batch also exporting and base58-decoding the public key bytes concurrently so the matching phase is fully synchronous. Cancellation is handled via `getAbortablePromise` so that firing the `abortSignal` rejects the grind immediately without waiting for in-flight key generations to settle. Regex matchers have their literal characters validated against the base58 alphabet up front — after stripping escapes, character classes, quantifiers, and groups — to catch common typos like `/^sol0/` before an infinite grind starts, with the new `SOLANA_ERROR__KEYS__INVALID_BASE58_IN_GRIND_REGEX` error code. The signer variants are thin wrappers around the key-pair variants that pipe results through `createSignerFromKeyPair`.
@lorisleiva lorisleiva force-pushed the 04-08-add_grindkeypair_and_grindkeypairsigner_for_mining_vanity_addresses branch from ef02578 to 206f03e Compare April 9, 2026 13:46
Comment on lines +21 to +29
// Some tests in this file exercise the real `generateKeyPair` implementation,
// which uses WebCrypto under the hood and cannot be seeded. As a result, the
// addresses produced are genuinely random. To keep these tests fast and
// deterministic (i.e. non-flaky), any test that does NOT mock
// `generateKeyPair` must use an extremely permissive matcher — typically a
// single-character regex prefix like `/^1/` or a trivial `() => true`
// predicate — so that the grind loop converges within one batch. Do not
// tighten these matchers: if you need to assert more specific behaviour,
// mock `generateKeyPair` the same way the forwarding tests below do.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mcintyre94 Addressed your comment here.

Copy link
Copy Markdown
Member Author

lorisleiva commented Apr 9, 2026

Merge activity

  • Apr 9, 9:41 PM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Apr 9, 9:41 PM UTC: @lorisleiva merged this pull request with Graphite.

@lorisleiva lorisleiva merged commit 43bc570 into main Apr 9, 2026
14 checks passed
@lorisleiva lorisleiva deleted the 04-08-add_grindkeypair_and_grindkeypairsigner_for_mining_vanity_addresses branch April 9, 2026 21:41
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 9, 2026

🔎💬 Inkeep AI search and chat service is syncing content for source 'Solana Kit Docs'

@github-actions
Copy link
Copy Markdown
Contributor

Because there has been no activity on this PR for 14 days since it was merged, it has been automatically locked. Please open a new issue if it requires a follow up.

@github-actions github-actions Bot locked as resolved and limited conversation to collaborators Apr 24, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants