The com.codename1.crypto packages hold everything a wallet, an exchange or a payment backend needs to work with Ethereum and the other EVM chains, Bitcoin, and Solana. That covers recovery phrases and key derivation, addresses, transactions, signing, the node APIs that broadcast transactions and read balances, and price feeds. There are no third-party libraries to add, and the same classes run in the app and in a Codename One backend.
The heavy parts run as native code wherever the platform allows it: secp256k1, Schnorr, Ed25519, the hashes and the PBKDF2 behind a recovery phrase. Elsewhere they fall back to an optimized pure-Java engine. A benchmark in CI compares every kernel against libsecp256k1, libsodium, OpenSSL and BouncyCastle on the same machine, and fails a pull request whose crypto changes fall behind.
The packages, from the bottom up:
| Package | What it holds |
|---|---|
| Hex, Base58 and Base58Check, Bech32 and Bech32m, RLP, Base64 |
| BIP-39 recovery phrases ( |
|
|
| P2PKH, P2SH, P2WPKH, P2WSH and Taproot addresses, scripts, transactions, PSBT, WIF |
| Addresses and program-derived addresses, legacy and v0 messages, transactions, System, Token and Compute Budget instructions |
| The |
| JSON-RPC for EVM nodes, Esplora for Bitcoin, Solana JSON-RPC |
|
|
The building blocks
A handful of algorithms do all the work underneath, and the names come up throughout this chapter and in every chain’s documentation. You never call most of them directly, but knowing what each one is for makes the rest easier to follow, and it explains why their speed matters.
Elliptic curves and signatures. A private key is a large random number. The public key is a point on an elliptic curve, computed from it in a way that’s easy forward and infeasible to reverse. A signature proves that the holder of the private key approved a message, and anyone with the public key can check it.
secp256k1 is the curve Bitcoin and Ethereum use. ECDSA is the signature scheme they sign transactions with. Ethereum also recovers the sender’s public key from a signature, which is why an Ethereum transaction carries no sender address.
Schnorr signatures (BIP-340) are the newer scheme on the same curve, used by Bitcoin’s Taproot addresses. They’re simpler than ECDSA and can be combined, which is what makes multi-party Taproot spends compact.
Ed25519 is a signature scheme on a different curve, Curve25519. Solana uses it, as do many protocols outside cryptocurrency. It was designed to be fast and hard to implement wrongly.
Verifying is the expensive direction. A wallet signs a handful of transactions, but a node, an exchange or an indexer verifies every signature it receives, which is where batches and the GPU come in.
Hash functions. A hash turns any input into a short fixed-size fingerprint. Changing one bit of the input changes the whole fingerprint, and nobody can find two inputs with the same one.
SHA-256 is Bitcoin’s hash: transaction IDs, block headers and the Merkle trees that commit to a block’s transactions. Bitcoin addresses take RIPEMD-160 of the SHA-256 of the public key, which gives a shorter 20-byte fingerprint.
Keccak-256 is Ethereum’s hash: addresses, transaction hashes, the selectors of contract functions and the keys of contract storage. It’s the algorithm that became SHA-3, but with the original padding, so SHA-3 libraries give different answers.
SHA-512 and HMAC-SHA512 (a keyed hash) are what key derivation is built on.
A Merkle tree hashes a list of items in pairs, then hashes the pairs, until one root remains. Changing any item changes the root, so a block header commits to all its transactions in 32 bytes. Building a level of one is a batch of independent hashes, which is what the Merkle rows in the benchmarks measure.
From a recovery phrase to keys. BIP-39 turns random bytes into a phrase of 12 or 24 words and back. It stretches the phrase into a 64-byte seed with PBKDF2, which runs HMAC-SHA512 2,048 times on purpose, so that guessing phrases is slow for an attacker. It’s also the one step a user waits for when they restore a wallet. BIP-32 derives a tree of keys from that seed, so one phrase yields every account on every chain, and SLIP-10 does the same for Ed25519. A derivation path such as m/44'/60'/0'/0/0 names one key in the tree.
Engines, kernels, and batches. The public classes are plain Java and the same everywhere. Underneath, a crypto engine supplies the compute-heavy kernels, which are the signature and hash operations above. On iOS, macOS, Android, Windows, Linux and a production backend the engine is native C, which uses the processor’s vector and hash instructions where it has them. On the JVM and in the browser it’s an optimized pure-Java engine. A batch hands many independent operations to the engine at once, so it can spread them across every CPU core and, for large batches, the GPU.
From a recovery phrase to a wallet
A wallet starts as a recovery phrase. Every key on every chain is derived from it, so the phrase is the only thing a user ever needs to back up.
// 24 words; 128 gives 12.
String words = Mnemonic.generate(256);
// Words typed back in by the user are checked before anything is derived from them.
if (!Mnemonic.isValid(words)) {
throw new IllegalArgumentException("not a valid recovery phrase");
}
// The passphrase is the optional "25th word"; "" when the user set none.
Wallet wallet = Wallet.fromMnemonic(words, "");
Mnemonic.isValid checks the words against the English list and verifies the checksum the last word carries, so a typo is caught before it turns into an empty wallet with a different address.
Wallet answers addresses on the paths other wallets use. A phrase restored here shows the same accounts it shows in MetaMask, Electrum, Sparrow or Phantom:
Wallet wallet = Wallet.fromMnemonic(Mnemonic.generate(128), "");
EvmAddress eth = wallet.getEvmAddress(0, 0); // m/44'/60'/0'/0/0
BitcoinAddress btc = wallet.getBitcoinAddress(BitcoinNetwork.MAINNET,
Wallet.BIP84, 0, 0, 0); // m/84'/0'/0'/0/0, bc1q...
SolanaAddress sol = wallet.getSolanaAddress(0); // m/44'/501'/0'/0'
String shown = eth.toChecksumString(); // EIP-55 mixed case
For Bitcoin, the purpose picks the address type: Wallet.BIP44 for legacy 1… addresses, BIP49 for P2SH-wrapped SegWit, BIP84 for native SegWit bc1q… and BIP86 for Taproot bc1p…. The same network object also selects testnet, signet, or regtest.
Sending a transaction
The pattern is the same on every chain. You read what the transaction needs from a node, build it, sign it on the device, and hand the signed bytes back to the node. Private keys never leave the Signer.
The node clients are blocking calls over an RpcTransport. In the app that’s NetworkTransport, which goes through the NetworkManager queue and keeps the UI responsive when called from the EDT. In a backend, com.codename1.backend.WebRpcTransport does the same over the backend’s HTTP client. A provider’s API key goes in a header: new NetworkTransport().header("Authorization", "Bearer …").
Ethereum and EVM chains
Wallet wallet = Wallet.fromMnemonic(Mnemonic.generate(128), "");
String recipient = "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed";
EvmRpc rpc = new EvmRpc(new NetworkTransport(), "https://ethereum-sepolia-rpc.publicnode.com");
DerivationPath path = Wallet.evmPath(0, 0);
EvmAddress from = wallet.getEvmAddress(path);
EvmAddress to = EvmAddress.parse(recipient);
UInt256 value = UInt256.parseUnits("0.01", 18);
long nonce = rpc.getTransactionCount(from, "pending");
UInt256[] fees = rpc.suggestFees(); // {tip, fee cap}
long gas = rpc.estimateGas(from, to, value, null);
EvmTransaction tx = EvmTransaction.eip1559(rpc.chainId(), nonce, fees[0], fees[1],
gas, to, value, null);
String hash = rpc.sendRawTransaction(wallet.signEvmTransaction(tx, path));
Amounts are UInt256, an exact unsigned 256-bit integer, so wei never passes through a double. parseUnits("0.01", 18) and formatUnits(18) convert to and from the decimal amount a person reads. suggestFees sets the fee cap at twice the next base fee plus the tip, which stays valid through several full blocks. The chain ID comes from the node and goes into the signature, so a transaction signed for one chain can’t be replayed on another.
Token transfers, contract calls and EIP-712 typed data use the same flow. Look at Erc20, Abi and TypedData in crypto.evm, and at Wallet.signTypedData and Wallet.personalSign for sign-in messages.
Bitcoin
A Bitcoin transaction spends earlier outputs ("coins") rather than debiting a balance, and a signer has to know the value of each coin it signs for. A PSBT (BIP-174) carries that information, and it’s also the format hardware wallets and multisig co-signers exchange:
Wallet wallet = Wallet.fromMnemonic(Mnemonic.generate(128), "");
String recipient = "tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx";
long feeSats = 1500;
BitcoinNetwork net = BitcoinNetwork.TESTNET;
EsploraClient esplora = new EsploraClient(new NetworkTransport(), EsploraClient.MEMPOOL_TESTNET);
DerivationPath path = Wallet.bitcoinPath(net, Wallet.BIP84, 0, 0, 0);
BitcoinAddress from = wallet.getBitcoinAddress(net, Wallet.BIP84, 0, 0, 0);
EsploraClient.Utxo coin = esplora.getUtxos(from).get(0);
BitcoinTransaction tx = new BitcoinTransaction();
tx.addInput(coin.getTxid(), coin.getVout());
tx.addOutput(coin.getValue() - feeSats, BitcoinAddress.parse(recipient, net));
// The PSBT carries what a signer needs to know about the coin being spent.
Psbt psbt = Psbt.fromTransaction(tx);
psbt.setWitnessUtxo(0, coin.toOutput());
psbt.signAll(wallet.getBitcoinKey(path));
psbt.finalizeAll();
String txid = esplora.broadcast(psbt.extract());
EsploraClient speaks the REST API of mempool.space and blockstream.info, or of your own Esplora server. getFeeEstimates returns sats per virtual byte by confirmation target. Multiply that by the size of the transaction to get feeSats. Every output the snippet doesn’t spend is paid as a fee, so a real wallet adds a change output back to one of its own addresses.
Wallet.signPsbt(psbt) signs any input whose BIP-32 origin matches the wallet’s own fingerprint. That lets a PSBT built elsewhere, by a coordinator or a watch-only wallet, be signed without being told which keys to use. Taproot inputs are signed with BIP-340 Schnorr signatures automatically.
Solana
Wallet wallet = Wallet.fromMnemonic(Mnemonic.generate(128), "");
String recipient = "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM";
SolanaRpc rpc = new SolanaRpc(new NetworkTransport(), SolanaRpc.DEVNET);
SolanaAddress from = wallet.getSolanaAddress(0);
List<Instruction> instructions = new ArrayList<Instruction>();
instructions.add(SystemProgram.transfer(from, SolanaAddress.parse(recipient), 10000000L));
SolanaMessage message = SolanaMessage.compile(from, instructions, rpc.getLatestBlockhash());
SolanaTransaction tx = new SolanaTransaction(message);
wallet.signSolanaTransaction(tx, 0);
String signature = rpc.sendTransaction(tx);
Amounts are in lamports (a billionth of a SOL). A blockhash expires after about a minute, so fetch it right before signing. TokenProgram builds SPL token transfers, and SolanaAddress.associatedTokenAddress finds the account a token is held in. ComputeBudget.setComputeUnitPrice adds a priority fee when the network is busy.
Prices
PriceFeed answers one question: the price of one asset in another. CoinGeckoFeed works without a key at the public rate limit, and takes a demo or pro key for more. JsonPriceFeed turns any JSON price API into a feed from a URL template and the path to the price. Wrap either in CachingPriceFeed, so a screen that prices twenty balances makes one request:
UInt256 weiBalance = UInt256.parseUnits("1.5", 18);
PriceFeed prices = new CachingPriceFeed(new CoinGeckoFeed(new NetworkTransport()),
60000, 600000); // fresh 1 min, usable 10 min
Quote q = prices.getQuote("ETH", "USD");
double usd = q.valueOf(weiBalance, 18);
A cached quote stays fresh for the first window. After that it’s still returned when a refresh fails, until the second window ends. That keeps a balance screen useful through a brief outage without showing a price that’s hours old.
Verifying many signatures at once
A backend that scans a block for deposits, or an indexer that checks every transaction it stores, verifies thousands of signatures at a time. CryptoBatch takes them as arrays and answers one boolean per entry:
// A block's deposits; filled from the transactions being scanned.
byte[][] hashes = new byte[0][];
Secp256k1.EcdsaSignature[] signatures = new Secp256k1.EcdsaSignature[0];
byte[][] publicKeys = new byte[0][];
boolean[] valid = CryptoBatch.verifySecp256k1(hashes, signatures, publicKeys);
int good = 0;
for (boolean v : valid) {
if (v) {
good++;
}
}
With native kernels the whole batch crosses into native code once and runs on every CPU core, which makes it several times faster than the same calls in a loop. verifySchnorr, verifyEd25519, sha256 and keccak256 work the same way. Every entry gets exactly the answer a single call would give, and a malformed entry, such as a key that doesn’t parse, is invalid rather than an exception, so one bad entry never stops the rest.
For the largest batches, every method also has a packed form that takes flat arrays at a fixed stride: 32-byte hashes one after another, 64-byte signatures one after another, and the public keys likewise, with one result byte per entry. On a native target the kernels read those arrays in place, so nothing is copied and no object is created per entry. Allocate them with Simd.get().allocByte(int) and they’re also aligned for the vector units:
int count = 1024;
Simd simd = Simd.get();
byte[] hashes = simd.allocByte(32 * count);
byte[] signatures = simd.allocByte(64 * count);
byte[] publicKeys = simd.allocByte(33 * count);
byte[] results = simd.allocByte(count);
// ... fill entry i at 32 * i, 64 * i and 33 * i ...
CryptoBatch.verifySecp256k1(hashes, signatures, publicKeys, 33, results, count);
// results[i] is 1 for a valid entry and 0 for any other
On iOS, macOS, Android, Windows and Linux, a large enough batch is shared between the CPU cores and the GPU, through Metal on Apple platforms and Vulkan elsewhere. The split follows the measured speed of each: the CPU and the GPU work on their parts at the same time, and a device whose GPU is slower than its CPU keeps the whole batch on the CPU. In a browser the GPU is reached through WebGPU, with the same kernels. The GPU only ever sees public data, meaning signatures, public keys and messages, and never a private key. Your code doesn’t change, and the results are the same either way.
Asynchronous batches
Every CryptoBatch method has an asynchronous form with Async added to its name. It takes the same arguments plus a CryptoBatch.Callback, returns at once, and runs the batch on a background thread:
byte[][] hashes = new byte[0][];
Secp256k1.EcdsaSignature[] signatures = new Secp256k1.EcdsaSignature[0];
byte[][] publicKeys = new byte[0][];
final Label status = new Label("Scanning...");
CryptoBatch.verifySecp256k1Async(hashes, signatures, publicKeys, new CryptoBatch.Callback<boolean[]>() {
public void done(boolean[] valid, Throwable error) {
// on the EDT: safe to update the UI
if (error != null) {
status.setText("Scan failed: " + error.getMessage());
return;
}
int good = 0;
for (boolean v : valid) {
if (v) {
good++;
}
}
status.setText(good + " of " + valid.length + " deposits verified");
}
});
In the app the callback runs on the EDT and can update the UI directly. In a backend it runs on the thread that did the work. The arguments are checked before the call returns, so a malformed batch, such as arrays of different lengths, throws right away instead of reaching the callback. The callback gets exactly what the synchronous form would have returned, or the exception it would have thrown. A packed form hands back the results array you passed in. The batch reads your arrays directly rather than copying them, so until the callback runs they belong to the batch: change one before then and you change what’s being verified.
In a browser, use the asynchronous forms. A large batch there goes to the GPU through WebGPU, which answers only asynchronously. The synchronous form still works, but it holds the calling thread until the GPU is done, and on the EDT that means the UI waits too. When the browser has no WebGPU adapter, both forms run on the CPU. The GPU kernels are about 460 KB of JavaScript, and they’re included only in apps that call CryptoBatch, loaded the first time a batch is large enough to use them.
Performance
Every operation is benchmarked against the library a project would otherwise use for it, in the same process run, on the same machine. The figures below are ratios: 2.00x means Codename One did twice the work of the reference library in the same time, and anything above 1.00x is faster. Ratios stay steady from one machine to the next where absolute speeds don’t, so they’re also what CI checks. A run fails when any ratio drops below the floor recorded for it in scripts/crypto-bench/floors.json.
These numbers come from an Apple M4 Max (12 performance cores, 40-core GPU), and the Android ones from an arm64 API 36 emulator on the same machine.
On the JVM
The simulator, JavaSE desktop apps and the backend in development run the portable Java implementation. It’s compared with BouncyCastle 1.84 and, for Ed25519, with the JDK’s own provider, on JDK 25. CI runs the same benchmark on JDK 17.
| Operation | Codename One (ops/s) | Reference (ops/s) | Ratio |
|---|---|---|---|
secp256k1 public key | 30,583 | 17,391 | 1.76x |
ECDSA sign | 24,109 | 16,220 | 1.49x |
ECDSA verify | 14,252 | 12,748 | 1.12x |
Schnorr sign, with a | 15,297 | 16,051 | 0.95x |
Schnorr verify | 14,299 | 12,816 | 1.12x |
Ed25519 sign | 49,360 | 45,707 | 1.08x |
Ed25519 verify | 17,293 | 16,865 | 1.03x |
Ed25519 sign, against the JDK | 49,946 | 2,484 | 20.10x |
Ed25519 verify, against the JDK | 7,599 | 2,469 | 3.08x |
Keccak-256, 1 KB | 484,694 | 503,524 | 0.96x |
SHA-256, 1 KB | 238,253 | 218,499 | 1.09x |
SHA-512, 1 KB | 339,664 | 297,983 | 1.14x |
RIPEMD-160, 1 KB | 139,370 | 96,919 | 1.44x |
HMAC-SHA512 (a BIP-32 step) | 679,117 | 473,458 | 1.43x |
BIP-39 seed (PBKDF2, 2048 rounds) | 802 | 558 | 1.44x |
Ed25519 verification rewrites its check with two scalars of half the length, found by a lattice reduction, so the multiplication needs half the doublings. The rewrite gives exactly the answer of the direct check for every input, including keys with a small-order component. On x86-64 the field multiplication also takes a Karatsuba split, which needs 75 products instead of 100. arm64 has the registers to keep the direct form faster, so each architecture gets the form that’s faster on it. The CI runners are x86-64, and they’re held to the same floors.
Native targets
On iOS, macOS, Windows, Linux and the production backend the same operations run as C, compared with Bitcoin Core’s libsecp256k1 for the secp256k1 curve and OpenSSL 3.6 for everything else, both built with the same compiler and flags.
| Operation | Codename One (ops/s) | Reference (ops/s) | Ratio |
|---|---|---|---|
secp256k1 public key | 95,819 | 93,050 | 1.03x |
ECDSA sign | 60,770 | 59,453 | 1.02x |
ECDSA verify | 50,857 | 50,302 | 1.01x |
Schnorr sign, one call against | 46,180 | 44,659 | 1.03x |
Schnorr verify | 48,394 | 47,835 | 1.01x |
Ed25519 sign | 105,161 | 43,155 | 2.44x |
Ed25519 verify | 39,129 | 17,521 | 2.23x |
Keccak-256, 1 KB | 614,062 | 647,427 | 0.95x |
SHA-256, 1 KB | 1,675,236 | 1,507,576 | 1.11x |
SHA-512, 1 KB | 1,055,920 | 834,025 | 1.27x |
RIPEMD-160, 1 KB | 429,239 | 318,439 | 1.35x |
HMAC-SHA512 (a BIP-32 step) | 2,174,970 | 889,725 | 2.44x |
BIP-39 seed (PBKDF2, 2048 rounds) | 2,105 | 1,679 | 1.25x |
A batch runs on every core, so it’s measured against the reference library verifying the same entries one at a time, which is what an application without CryptoBatch would do. The figures are batches of 1,024 signatures, or Merkle levels of 4,096 nodes, per second:
| Batch | Codename One | Reference, one at a time | Ratio |
|---|---|---|---|
ECDSA verify | 333 | 45 | 7.46x |
ECDSA verify, packed | 374 | 45 | 8.28x |
Ed25519 verify | 261 | 17 | 15.62x |
SHA-256 Merkle level | 7,164 | 1,053 | 6.80x |
Keccak-256 Merkle level | 5,613 | 796 | 7.06x |
Android
Android has no curve secp256k1 in its platform, so apps there would otherwise ship BouncyCastle. The table shows both the native kernels and the portable Java implementation, each against BouncyCastle on the same device. The two engines ran in turn, each beside its own BouncyCastle run, so each ratio is against the BouncyCastle figure from its own run; the column shows the one from the native run, and the two agree to within 4%.
| Operation | Native kernels | Portable Java | BouncyCastle on ART |
|---|---|---|---|
secp256k1 public key | 116,210 (8.44x) | 45,203 (3.40x) | 13,770 |
ECDSA sign | 71,125 (5.00x) | 31,338 (2.12x) | 14,232 |
ECDSA verify | 59,615 (6.00x) | 16,757 (1.67x) | 9,940 |
Schnorr verify | 60,243 (5.97x) | 17,163 (1.69x) | 10,094 |
Ed25519 sign | 117,185 (1.70x) | 53,194 (0.77x) | 69,038 |
Ed25519 verify | 39,721 (1.44x) | 19,633 (0.72x) | 27,571 |
Keccak-256, 1 KB | 762,149 (1.96x) | 384,908 (0.98x) | 388,709 |
SHA-256, 1 KB | 292,535 (1.08x) | 287,546 (1.08x) | 270,445 |
RIPEMD-160, 1 KB | 214,766 (0.71x) | 205,703 (0.69x) | 304,410 |
BIP-39 seed | 1,037 (1.36x) | 1,033 (1.36x) | 761 |
SHA-256 and the BIP-39 seed go through the platform’s own native implementation in both columns, which is why they match. With the native kernels, RIPEMD-160 is the one row where BouncyCastle stays ahead; it’s hashed once per Bitcoin address, so it’s never the bottleneck. The portable Java column trails BouncyCastle on Ed25519 and RIPEMD-160 and matches it on Keccak-256.
The GPU
A GPU verifies signatures in parallel by the thousand, but each entry first needs some preparation on the CPU, and the GPU’s work has to be dispatched and collected. A batch is therefore split, with the CPU cores and the GPU each taking a share in proportion to their measured speed, and a batch smaller than the crossover stays on the CPU. With Metal on the M4 Max:
| Kernel | CPU, every core | GPU alone | Both together | Crossover |
|---|---|---|---|---|
ECDSA verify | 373,000/s | 181,000/s | 1.02x the CPU alone | 784 entries |
Schnorr verify | 258,000/s | 233,000/s | 1.13x | 539 entries |
Ed25519 verify | 250,000/s | 216,000/s | 1.63x | 552 entries |
Keccak-256 | 7.1 GB/s | 3.6 GB/s | 1.01x | 3 MB |
The chart shows the signature rows; Keccak-256 is measured in bytes rather than signatures, so it appears only in the table.
On this machine the gain is modest, because sixteen CPU cores with vector units are already fast. The split is worked out from the speeds measured on each device, so a GPU that’s strong relative to its CPU takes a larger share, and one that’s slower takes none. SHA-256 has no GPU row: in this run no SHA-256 batch reached the GPU’s minimum size, so they all ran on the CPU.
In the browser
The JavaScript port runs the portable Java implementation, translated to JavaScript. Large batches go to the GPU through WebGPU and arrive through the asynchronous forms. In headless Chromium on the same machine, with batches of 4,096 signatures:
| Batch | WebGPU | Portable Java | Ratio |
|---|---|---|---|
ECDSA verify | 2,174/s | 255/s | 8.5x |
Schnorr verify | 2,698/s | 258/s | 10.5x |
Ed25519 verify | 2,070/s | 240/s | 8.6x |
SHA-256 | 28.3 MB/s | 8.1 MB/s | 3.5x |
Keccak-256 | 174.8 MB/s | 1.9 MB/s | 92x |
In the browser most of the time a WebGPU batch takes is the preparation in JavaScript, not the GPU work, and the browser’s figures move more from run to run than the native ones: across three runs SHA-256 on WebGPU ranged from 4 to 34 MB/s, and in the slowest it trailed the CPU. The answers never differed: the first run compared a sample of the signatures, and the runs after it compared every entry and every hash. Given records prepared in advance, the kernels alone verified about 164,000 ECDSA, 119,000 Schnorr and 146,000 Ed25519 signatures a second there.
The chart puts the ways a signature can be verified side by side, on a logarithmic scale. They don’t measure the same thing: the browser bars are batches on one JavaScript thread, the JVM and native bars are one signature at a time on one core, and the last bar is a native batch across every core and the GPU. Read it as what each setup delivers, not as one benchmark run across platforms.
Running the benchmarks
The harnesses are in scripts/crypto-bench and scripts/crypto-gpu of the Codename One repository:
scripts/crypto-bench/jvm-run.sh # JVM, against BouncyCastle and the JDK
scripts/crypto-bench/native/run.sh # C, against libsecp256k1 and OpenSSL
scripts/crypto-bench/android-run.sh # an attached device or emulator
scripts/crypto-gpu/batch-check.sh # the GPU split, with its crossover
scripts/crypto-gpu/js-batch-check.sh # WebGPU in headless Chromium
python3 scripts/crypto-bench/gate.py jvm <output> # hold a run to its floors
Each prints the compiler, JDK and reference library versions above its numbers. scripts/crypto-bench/charts.py draws the two charts above from the saved results.
Where the keys live
Whoever holds the recovery phrase holds the funds. Decide where it lives before you write the first screen.
On the device, in a vault. VaultSigner keeps the phrase sealed in a vault (see "Vaults: One password, every device" in the security chapter) and opens it only while the vault is unlocked, by biometrics or the vault password. It works anywhere Wallet expects a signer:
Vault vault = Vault.named("wallet");
String words = Mnemonic.generate(256);
// Once, when the wallet is created or restored:
VaultSigner.store(vault, "main", words, "");
// Every later launch: the phrase is opened only while the vault is unlocked.
Wallet wallet = new Wallet(new VaultSigner(vault, "main"));
This is the model for a self-custody wallet. A lost device loses nothing as long as the user kept the phrase, and revealMnemonic shows it again for a backup. A signer whose vault is locked throws rather than signing, so a stolen, locked phone can’t move funds.
On a server, behind your own controls. An exchange or a custodial app derives its deposit addresses and signs its withdrawals in the backend, where the same classes run. Keep the phrase out of the database and the source tree: load it from a secret store or an HSM at startup, and keep withdrawals behind an approval step. The Bastion sample does exactly this. Its implementor’s guide walks through hot and deposit wallets, confirmation depth and reorg handling.
SeedSigner holds the derived master key in memory. Call destroy() when you’re done with it, and the key bytes are overwritten. Strings can’t be wiped, which is why VaultSigner.revealMnemonic answers a char[].