Zero dependency Bitcoin math implementation in C
WARNING: THIS PROGRAM USES THE rand_s FUNCTION FROM THE WINDOWS stdlib.h TO GENERATE PSEUDO RANDOM ENTROPY. MICROSOFT STATES THAT rand_s PRODUCES CRYPTOGRAPHICALLY SECURE RANDOM NUMBERS. HOWEVER, USERS SENDING COINS TO RANDOM ADDRESSES GENERATED BY THIS PROGRAM DO SO AT THEIR OWN RISK!
I started the bitcoin_math project in order to teach myself the basics of Bitcoin math from first principles, without having to wade through the source code of any of the crypto or "bignum" libraries on which standard Bitcoin implementations in Python depend.
My goal was to collect together a minimal set of functions in a single C source code file with no dependencies other than the following standard C libraries: ctype.h, math.h, stdint.h, stdio.h, stdlib.h, and string.h.
Given a choice between efficiency and readability, I have opted for the latter, for example by avoiding inline functions and macros.
The result is bitcoin_math.exe, a simple menu driven console application which implements functions for the generation of mnemonic phrases, seeds, private keys, extended keys, public keys, and Bitcoin addresses (P2PKH, P2SH-P2WPKH and P2WPKH), together with basic Secp256k1 ECDSA signing and verification, using various cryptographic hash functions, arbitrary precision integer math, elliptic curve math, and radix conversions, all built from standard C data types and a few custom structs.
Wherever possible, hash digests, MACs, seeds, keys, and addresses are manipulated as arbitrary precision integers, reflecting their essentially numerical nature. These numbers are typically rendered onscreen in hex or Bitcoin base 58 format, but can be rendered in any base between 2 and 64 using the base conversion function.
The November 18, 2025 update includes improved ECDSA signing and verification functions.
The signing function accepts a private key and the SHA256 hash of the relevant message as parameters, both in hex format. The user can choose between a random nonce, or a deterministic nonce calculated using the RFC6979 algorithm (https://www.rfc-editor.org/rfc/rfc6979). The function which implements the RFC6979 algorithm treats all intermediary values as bnz_t numbers, meaning that their bytes are stored in little endian order. However, the RFC6979 algorthim processes the K, V, Key and Message variables as big endian numbers. This necessitates multiple calls to the bnz_reverse_digits function, obviously advsersely impacting efficiency, but necessary for consistency with the remainder of the code where all big integer variables are stored as bnz_t type.
The ECDSA signing function outputs the signature in DER format and also as separate S and R components. There are two corresponding verification functions which accept a public key, a SHA256 hash of the message, and either the ECDSA signature in DER format or the separate S and R components, in each case in hex format.
The ECDSA signing function produces signatures that are identical to those given in the 20 test cases set out here:
The inclusion of a function implementing the RFC6979 algorithm necessitated updating the third party hash functions used in the bicoin_math.c source code to include a HMAC-SHA256 function. I took this ooportunity to overhaul the hash functions to use a single consistent source (Olivier Gay [email protected]) whose (c) 2005 SHA2 implementation in C is widely used throughout GitHub and which I gratefully acknowlegde here.
In the earliest versions of bitcoin_math, public keys were generated by elliptic curve scalar multiplication of the Secp256k1 generator point using a standard binary "double-and-add" algorithm that is notoriously slow due to its repeated calling of a computationally expensive modular multiplicative inverse function. Faster algorithms exist, but require standard affine xy curve coordinates to be converted into Jacobian xyz coordinates during execution of the double-and-add process, with the final result being converted back into affine xy coordinates. Elliptic curve scalar multiplication efficiency can be further improved by using a precalculated array of Secp256k1 generator point doublings modulo the Secp256k1 curve's prime.
The August 29, 2025 update implements these efficiency improvements, resulting in a noticeable increase in the speed of calculation of hierarchical deterministic wallet addresses in accordance with BIP44, BIP49 and BIP84 of approximately 30x. This is consistent with experimental analysis: before the improvement, a single elliptic curve scalar multiplication operation entailed around 50,000 calls to the bnz_divide_bnz function (some of which are in the context of unavoidable bnz_mod_bnz operations). After the improvement, that number was down to around 1,000. Note that the improved performance is still paltry when compared with the highly optimised elliptic curve library that forms part of the Bitcoin core codebase (https://github.com/bitcoin/bitcoin/tree/master/src/secp256k1). The Bitcoin core library is hundreds of times faster than the original bitcoin_math at elliptic curve scalar multiplication.
Implementation of the improved elliptic curve scalar multiplication algorithm required some reorganisation of the bitcoin_math source code. First, I renamed the PT struct, which holds the xy coordinates of an affine point as a pair of bnz_t variables, to APT to reflect the need to differentiate it from the new JPT struct which holds the xyz Jacobian coordinates required by the efficient algorithms as three bnz_t variables. The pre-calculated values of the 256 Secp256k1 generator point doublings modulo the Secp256k1 curve's prime are now stored as a static array of APT structs within the SECP256K1 struct. The array is populated during execution of the secp256k1_init() function from an external binary file (dat.bin). The file comtains 16,384 bytes representing the 32 byte digits arrays (in little endian order) of the 512 bnz_t structs of the 256 ACT structs that make up the array of 256 generator point doublings modulo the Secp256k1 curve's prime.
Due to the computational expense of populating the array when the SECP256K1 struct is initiated, I have limited the use of the secp256k1_init() function to the menu functions. Within each menu function, the SECP256K1 struct is initiated (and its generator point doublings array is populated) only once and then the resultant SECP256K1 struct is passed around the dependent functions as a const variable.
I have retained the original elliptic curve scalar multiplication, doubling, and addition functions for use in the context of multiplication of points other than the Secp256k1 generator point e.g. in the ECDSA signing and verification functions.
P2PKH addresses have at least one leading zero. When rendered on screen in Bitcoin base 58, each leading zero is indicated by a 1 character. In bitcoin_math, P2PKH addresses are stored as bnz_t values but the regular bnz_print function ignores leading zeros at the MSB end. Therefore, bitcoin_math implements a dedicated P2PKH address print function, print_p2pkh_address, which ensures that, when a P2PKH address is rendered on screen, leading zeros are automatically added as 1 characters before the non-zero numercial part.
Storing P2WPKH addresses as bnz_t numbers is not straightforward: the bc1q prefix (which contains non Bech32 characters) is concatenated with a numerical part which consists of a Bech32-encoded RIPEMD160-SHA256 double hash digest of the relevant compressed public key, concatenated with a corresponding special P2WPKH checksum. Adding to the complication, this numerical part can have zeroes at the MSB end. Leading zeroes are typically stripped out when bitcoin_math prints bnz_t numbers, which would lead to invalid P2WPKH addresses. bitcoin_math solves these issues by storing the numercial part as a bnz_t number, and using a dedicated print function to add the "bc1q" prefix, and any necessary zero padding (in the form of Bech32 q digits), prior to printing, leveraging the fact that P2WPKH addresses are 42 characters long.
Outputs from bitcoin_math can be verified using online tools. My preferred sources of verification are the excellent https://iancoleman.io/bip39/ and https://learnmeabitcoin.com/technical/keys/. I have also checked the entropy-to-seed outputs of bitcoin_math.exe against the 256 bit entropy examples in the "english" section of https://github.com/trezor/python-mnemonic/blob/master/vectors.json, using "TREZOR" as the passphrase. The validity of P2PKH, P2SH-P2WPKH and P2WPKH addresses can be checked using a blockchain explorer such as https://www.blockchain.com/explorer/search
Compilation: The source code of bitcoin_math compiles with gcc under Windows using the following simple command:
gcc -o bitcoin_math.exe bitcoin_math.c
The compilation command works on Linux, although you may have to append -lm to ensure that the math.h functions log10 and log are recognised. Note that the system(cls); command in the menu functions is not recognised by Linux, so non-fatal errors will be raised on execution.
Linux developers should note that the source code has been updated to use the crytographically secure random number generation function rand_s, which is part of the Microsoft stdlib (see the function entitled bnz_256_bit_rnd under the /* BITCOIN */ heading). Linux developers will need to replace rand_s with functions based on /dev/random or /dev/urandom, or delete the random number based functions altogether.
I have found bitcoin_math.exe to be fast enough for its intended illustrative / educational purposes, particulaly following the August 29, 2025 version's switch to efficient elliptic curve scalar multiplication algorithm. However, it can be trivially speeded-up using compiler optimisation flags such as -O3. Conversion to a library should also be relatively simple. Obviously, many further improvements in efficiency are potentially available.
There are four menus, some with sub-menus:
1. Master keys This function takes 256 bits of random entropy (typed or pasted, in a specified base between 2 and 64) and generates the corresponding master private key, master chain code, and the corresponding first 20 P2PKH, P2SH-P2WPKH and P2WPKH heirarchical deterministic wallet addresses in accordance with BIP44, BIP49 and BIP84 respectively. There is an option to generate "random" entropy. This option uses the function rand_s() from the Windows stdlib - see disclaimer above.
2. Child keys These functions take a parent private key or public key and a corresponding parent chain code (typed or pasted, in hex only), together with a numerical index. Depending on the function chosen and the value range of the index, these functions output a normal or hardened child private key and corresponding child chain code, or a child public key. The range of index values for hardened child keys is from 2147483648 to 4294967295. The function get_child_hardened, and the corresponding menu commands, will accept index values of less than 2147483648 and will automatically add 2147483648 if necessary. This makes it easier to use an offset rather than an absolute value, as in the 44', 49' and 84' hardened child keys generated in the get_wallet_p2pkh_addresses, get_wallet_p2sh_p2wpkh and get_wallet_p2wpkh_addresses functions which generate series of heirarchical deterministic wallet addresses from a master private key and master chain code in accordance with BIP44, BIP49 and BIP84.
The get_hdk_intermediate_values function calculates and displays the intermediate private key and chain code values derived from a master private key and master chain code pair, and a hierarchical deterministic key derivation path string e.g. m/44'/0'/0'/0/0. The function takes a master private key, a master chain code, and a string representing the derivation path. The string must start with "m" and be "/" delimited. Index values can be absolute or offset, with hardened child keys being designated by a ' character. bitcoin_math does not perform any validation of the derivation path string. Errors are therefore likely to cause segmentation faults.
3. Base converter This function takes a number of arbitrary length (typed or pasted) in any specified base between 2 and 64, and outputs a radix converted version of the number for each base between 2 and 64. Note that, in bitcoin_math base "-2" corresponds to a binary representation with a space between each set of eight bits, whereas base "2" corresponds to an unbroken binary string. Likewise, base "16" corresponds to lower case hex preceded by "0x", whereas base "-16" corresponds to upper case hex with no prefix. Finally, base "58" corresponds to Bitcoin base 58, whereas base "-58" corresponds to regular base 58.
4. Functions This menu enables some individual functions, including P2PKH, P2SH-P2WPKH and P2WPKH serialisation, two-way WIF format conversion, mnemonic phrase checksum validation, and Secp256k1 functions for point addition, point doubling, and scalar multiplication, and basic Secp256k1 ECDSA signing and verification functions to be independently executed. Parameters such as private keys, public keys, chain codes, Secp256k1 coordinates, and message hashes must be typed or pasted in hex format. DO NOT ENTER ANY MNEMONIC PHRASE THAT CORRESPONDS TO ANY PRIVATE KEY / BITCOIN ADDRESS TO WHICH COINS WILL BE SENT!
The BIP39 word list, and the source code for the cryptographic hash functions (RIPEMD160, SHA256, SHA512, HMAC-SHA512), were copied from various third party Github repositories, with a few minor modifications for readability e.g. conversion of inline functions into standalone functions. The pricipal source was Olivier Gay's 2005 implementation of sha2 and hmac_sha2 in C.
The majority of the original source code in bitcoin_math relates to arbitrary precision integer math which is required to implement the elliptic curve functions used in Bitcoin to convert private keys into public keys.
This arbitrary precision integer math code was heavily influenced by the source code of the GNU Multiple Precision Arithmetic Library, and DI Management Services' BigDigits multiple-precision arithmetic library.
My version of arbitrary precision integer division is adapted from the well known Hacker's Delight version, where the digits are processed in pairs of 16 bit integers ("half words") combined into 32 bit integers. I modified the code to use pairs of 8 bit integers combined into 16 bit integers in order to enable the uint8_t arrays that comprise the bnz_t digits to be consumed directly.
The algorithms for the elliptic curve functions were adapted from a paper entitled Implementation of Elliptic Curve Cryptography in 'C' by Kuldeep Bhardwaj and Sanjay Chaudhary (International Journal on Emerging Technologies 3(2): 38-51 (2012)), which used GMP for the arbitrary precision integer functions. It was straightforward to adapt the algorithms to work with my own arbitrary precision integer math code.
The algorithms for the modular power and modular multiplicative inverse functions were adapted from the pseudocode provided in the corresponding Wikipedia pages.
The improved elliptic curve scalar multiplication algorithm depends on the Jacobian elliptic curve addition algorthim known as "madd-2004-hmv", which is specific to curves of the form y^2 = x^3 + ax^2 + b where a = 0 and assumes Z2 = 1. This fits the Secp256k1 curve formula (y^2 = x^3 + 7) and enables the precalculated Secp256k1 doublings to be passed as an array of type ACT rather than JCT. For more details, see the releavnt page on the Explicit Functions Database website.
The source code file bitcoin_math.c is divided into the following sections:
The BIP39 word list, formatted as a 2D array (2048 x 9) of type char, and a utility function (init_uint8_array) which initiates and zeroes a fixed length 1D dynamic array of type uint8_t.
Standard cryptographic hash function with a 20 byte digest.
Standard cryptographic hash function with 32 byte digest.
Standard cryptographic hash function with 64 byte digest.
Standard cryptographic message authentication function returning a 64 byte MAC from a combination of a message and a key, each formatted as 1D fixed array of type uint8_t.
Code for implementing arbitrary precision integer math based around a custom type (bnz_t) and associated functions to manipulate signed integers of arbitrary size.
The 8 bit "digits" are stored, in little endian order, in the digits property of the bnz_t struct, formatted as a 1D dynamic array of type uint8_t. The number of digits is stored in the size property, type size_t. The sign is stored in the sign property, type size_t, with a zero value corresponding to positive and a non-zero value corresponding to negative.
There are basic functions to initiate, free, set, align, trim, reverse, concatenate, and resize bnz_t integers. When resizing to a higher number of digits, new digits are zeroed and the values of existing digits are either preserved or zeroed depending on the value of the preserve parameter.
The bnz_print function is used to write a bnz_t number to the screen in the specified base, optionally preceded by a specified string.
Various comparison functions are implemented, returning -1, 0, or 1 according to standard C numerical comparison rules.
Functions for addition, subtraction, multiplication and division (quotient and remainder) are implemented, with pre-processing of signs as appropriate.
Finally, arbitrary precision implementations of the special functions of mod, mod power, and modular multiplicative inverse are implemented for use in the Secp256k1 elliptic curve math.
Elliptic curve math, built around two custom structs: PT, comprising two bnz_t numbers, representing a point on Secp256k1, and SECP256K1 representing the elliptic curve itself. The a and h parameters of the curve are included for completeness, but play no role in the functions.
Bitcoin specific functions for processing entropy, generating mnemonic phrases, seeds, private keys (including two-way WIF format conversion), chain codes, public keys, and P2PKH addresses.
If a user generates a private key that is not less than the value of the order of Secp256k1, the user will be prompted to rerun the private key generation step with a different entropy value.
Secp256k1 ECDSA signing and signature verification functions. The signing function requires the user to supply a private key and the hash of the relevant message in hex format. The function outputs the signature in DER format, together with the corresponding r and s components of the signature. If the most significant bit of r and/or s is set (which would ordinarily cause a DER formatted integer to be treated as a negative number), a 0x0 byte is prepended before the most significant byte. In addition, where the initially calculated value of s is greater than the floor value of Secp256k1.n / 2 (known as "high s"), the value of s is negated by subtracting it from Secp256k1.n to give the corresponding "low s" value and the signature is recalculated to ensure that the user is presented with a signature incorporating the "low s" value. The verification functions require the user to supply the compressed public key corresponding to the private key used to sign the message, the hash of the relevant message, and either a signature in DER format, or the r and s components of the signature, in each case in hex format.
Menu functions.
Main function.