I am writing code in C, and I need to do various operations with Bitcoin, from generating key pairs, to sign messages to post transactions. I found libraries for C++, but nothing in C that does all that, so I resorted to write functions and code to achieve those results. Any of you know a good C library that takes care of all this? I am manually doing it all.
After searching for libraries that could do this, and/or code that could do this, all around the internet, and also AI code assistants failing me, I have managed to write C code (not C++, simple C) that generates a valid Bitcoin message signature! I share this here, so someone more used to coding in C can give me some feedback on how doing this better or in a more direct manner.
Compile with (assuming you have all the libs in your path):
gcc btcsign.c -o btcsign -lsecp256k1 -lssl -lcrypto -lm
Execute with:
./btcsign "Private Key WIF" "Text message or SHA256 hash of a file"
For example:
./btcsign "5JkH4WGyg1XcUAzcfqLhKwpfs5A4v4Jdw6gWpgTLFhQW7wcnUMo" "Hello"
Verify using a public tool such as:
https://bitaps.com/signature
or
https://tools.qz.sg/
Here is the full code:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <openssl/sha.h>
#include <openssl/bio.h>
#include <openssl/buffer.h>
#include <openssl/evp.h>
#include <openssl/ripemd.h>
#include <secp256k1.h>
#include <secp256k1_recovery.h>
const char BASE58_CHARS[] = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
void base58c_encode(uint8_t *input, size_t input_len, char **output) {
// Count leading zeros
size_t leading_zeros = 0;
while (leading_zeros < input_len && input[leading_zeros] == 0) {
++leading_zeros;
}
// Determine the size of the output buffer
size_t output_size = (input_len - leading_zeros) * 138 / 100 + 1;
uint8_t buffer[output_size];
memset(buffer, 0, output_size);
// Encode the input data
for (size_t i = leading_zeros, j = output_size - 1; i < input_len; ++i, j = output_size - 1) {
for (int carry = input[i]; j >= 0 || carry != 0; --j) {
carry += 256 * buffer[j];
buffer[j] = carry % 58;
carry /= 58;
if (j == 0) {
break;
}
}
}
// Determine the start index of the encoded data
size_t start_index = 0;
while (start_index < output_size && buffer[start_index] == 0) {
++start_index;
}
// Allocate memory for the output string
*output = malloc(leading_zeros + output_size - start_index + 1);
if (*output == NULL) {
fprintf(stderr, "Memory allocation failed\n");
return;
}
// Add leading zeros to the output string
memset(*output, '1', leading_zeros);
// Copy the encoded data to the output string
for (size_t i = start_index, j = leading_zeros; i < output_size; ++i, ++j) {
(*output)[j] = BASE58_CHARS[buffer[i]];
}
(*output)[leading_zeros + output_size - start_index] = '0円';
}
// Base58 decoding
int base58_decode(const char *base58, unsigned char *output, int *out_len) {
unsigned char buffer[50] = {0};
int i, j, carry;
for (i = 0; i < strlen(base58); i++) {
const char *ptr = strchr(BASE58_CHARS, base58[i]);
if (!ptr) return -1;
carry = ptr - BASE58_CHARS;
for (j = sizeof(buffer) - 1; j >= 0; j--) {
carry += 58 * buffer[j];
buffer[j] = carry & 0xff;
carry >>= 8;
}
}
// Find the starting non-zero position
for (i = 0; i < sizeof(buffer) && buffer[i] == 0; i++);
int leading_zeros = i;
int size = sizeof(buffer) - leading_zeros;
memcpy(output, buffer + leading_zeros, size);
*out_len = size;
return 0;
}
// Decode WIF private key to hex
int decode_wif_to_hex(const char *wif_key, unsigned char *hex_key) {
unsigned char buffer[37];
int out_len;
if (base58_decode(wif_key, buffer, &out_len) != 0) {
return -1;
}
// Validate checksum
unsigned char checksum[SHA256_DIGEST_LENGTH];
SHA256(buffer, out_len - 4, checksum);
SHA256(checksum, SHA256_DIGEST_LENGTH, checksum);
if (memcmp(checksum, buffer + out_len - 4, 4) != 0) {
return -1;
}
// Extract private key (strip first and last byte)
memcpy(hex_key, buffer + 1, 32);
return 32;
}
// Base64 encoding
char *base64_encode(const unsigned char *input, int length) {
BIO *bmem, *b64;
BUF_MEM *bptr;
char *buff;
b64 = BIO_new(BIO_f_base64());
BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
bmem = BIO_new(BIO_s_mem());
b64 = BIO_push(b64, bmem);
BIO_write(b64, input, length);
BIO_flush(b64);
BIO_get_mem_ptr(b64, &bptr);
buff = (char *)malloc(bptr->length + 1);
memcpy(buff, bptr->data, bptr->length);
buff[bptr->length] = '0円';
BIO_free_all(b64);
return buff;
}
// Helper function to convert a recovered ECDSA signature to Bitcoin format
int ecdsa_signature_to_bitcoin(const unsigned char *sig, unsigned int sig_len, int recid, unsigned char *output) {
if (recid < 0 || recid > 3) return -1;
output[0] = 27 + recid + (recid & 2 ? 4 : 0);
memcpy(output + 1, sig, sig_len);
return sig_len + 1;
}
// Function to print hexadecimal data
void print_hex(const char *label, const unsigned char *data, int len) {
printf("%s: ", label);
for (int i = 0; i < len; i++) {
printf("%02x", data[i]);
}
printf("\n");
}
// Helper function to print the uncompressed public key
void print_uncompressed_pubkey(secp256k1_pubkey *pubkey, secp256k1_context *ctx) {
unsigned char pubkey_serialized[65];
size_t pubkey_len = 65;
secp256k1_ec_pubkey_serialize(ctx, pubkey_serialized, &pubkey_len, pubkey, SECP256K1_EC_UNCOMPRESSED);
print_hex("Uncompressed Public Key", pubkey_serialized, pubkey_len);
}
void double_sha256(const unsigned char *input, size_t length, unsigned char *output) {
unsigned char hash[SHA256_DIGEST_LENGTH];
SHA256(input, length, hash);
SHA256(hash, SHA256_DIGEST_LENGTH, output);
}
int main(int argc, char *argv[]) {
// Check if the correct number of arguments is provided
if (argc != 3) {
printf("Usage: %s \"wif_key\" \"message\"\n", argv[0]);
return 1;
}
// Get the wif_key and message from command-line arguments
const char *wif_key = argv[1];
const char *message = argv[2];
// const char *message = "Hello";
const char *bitcoin_prefix = "\x18" "Bitcoin Signed Message:\n";
unsigned char private_key_hex[32];
// Decode WIF to private key hex
if (decode_wif_to_hex(wif_key, private_key_hex) != 32) {
printf("Invalid WIF key\n");
return 1;
}
// Print the decoded private key
print_hex("Decoded Private Key", private_key_hex, 32);
// Initialize libsecp256k1
secp256k1_context *secp256k1_ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN | SECP256K1_CONTEXT_VERIFY);
// Load the private key
secp256k1_ecdsa_recoverable_signature recoverable_sig;
secp256k1_pubkey pubkey;
if (!secp256k1_ec_pubkey_create(secp256k1_ctx, &pubkey, private_key_hex)) {
printf("Error creating public key\n");
return 1;
}
// Print the uncompressed public key
print_uncompressed_pubkey(&pubkey, secp256k1_ctx);
// Generate un_Bitcoin address from public key
unsigned char un_serialized_pubkey[65]; // Increase the buffer size to 65 bytes
size_t un_serialized_pubkey_len = 65;
secp256k1_ec_pubkey_serialize(secp256k1_ctx, un_serialized_pubkey, &un_serialized_pubkey_len, &pubkey, SECP256K1_EC_UNCOMPRESSED);
const unsigned char *d = un_serialized_pubkey;
uint8_t un_sha256_hash[32];
uint8_t un_pubkey_hash[20];
SHA256(un_serialized_pubkey, sizeof(un_serialized_pubkey), un_sha256_hash);
RIPEMD160(un_sha256_hash, sizeof(un_sha256_hash), un_pubkey_hash);
uint8_t un_address_payload[21];
un_address_payload[0] = 0x00; // Main net version byte
memcpy(un_address_payload + 1, un_pubkey_hash, 20);
uint8_t un_address_checksum[4];
uint8_t un_address_hash[32];
SHA256(un_address_payload, 21, un_address_hash);
SHA256(un_address_hash, 32, un_address_hash);
memcpy(un_address_checksum, un_address_hash, 4);
uint8_t un_address_pre_base58[25];
memcpy(un_address_pre_base58, un_address_payload, 21);
memcpy(un_address_pre_base58 + 21, un_address_checksum, 4);
size_t un_address_base58_len = 25;
char* un_base58_address = malloc(34 * sizeof(char));
if (un_base58_address == NULL) {
exit(EXIT_FAILURE);
}
base58c_encode(un_address_pre_base58, un_address_base58_len, &un_base58_address);
// Print the Address
printf("Address: %s\n", un_base58_address);
free(un_base58_address);
// Create the prefixed message
unsigned char prefixed_message[256];
size_t prefix_len = strlen(bitcoin_prefix);
size_t msg_len = strlen(message);
prefixed_message[0] = (unsigned char)msg_len;
memcpy(prefixed_message + 1, message, msg_len);
unsigned char full_prefixed_msg[256];
size_t full_prefixed_len = prefix_len + msg_len + 1;
memcpy(full_prefixed_msg, bitcoin_prefix, prefix_len);
memcpy(full_prefixed_msg + prefix_len, prefixed_message, msg_len + 1);
// Hash the prefixed message using SHA256 twice
unsigned char hash2[SHA256_DIGEST_LENGTH];
double_sha256(full_prefixed_msg, full_prefixed_len, hash2);
// Print the message hash
print_hex("Message Hash", hash2, SHA256_DIGEST_LENGTH);
// Sign the message hash
if (!secp256k1_ecdsa_sign_recoverable(secp256k1_ctx, &recoverable_sig, hash2, private_key_hex, NULL, NULL)) {
printf("Error signing the message\n");
return 1;
}
// Serialize the recoverable signature
unsigned char sig[64];
int recid;
secp256k1_ecdsa_recoverable_signature_serialize_compact(secp256k1_ctx, sig, &recid, &recoverable_sig);
printf("sig: ");
for (int i = 0; i < 64; i++) {
printf("%02x", sig[i]);
}
printf("\n");
// Convert to Bitcoin format
unsigned char bitcoin_sig[65];
int bitcoin_sig_len = ecdsa_signature_to_bitcoin(sig, sizeof(sig), recid, bitcoin_sig);
printf("bitcoin_sig: ");
for (int i = 0; i < 65; i++) {
printf("%02x", bitcoin_sig[i]);
}
printf("\n");
// Encode the signature in base64
char *signature_base64 = base64_encode(bitcoin_sig, bitcoin_sig_len);
printf("signature_base64: %s\n", signature_base64);
// Clean up
free(signature_base64);
secp256k1_context_destroy(secp256k1_ctx);
return 0;
}
-
5\$\begingroup\$ The title should state what the code does. If someone is aware of a library that does this already, it'd be mentioned. Don't pose it as a question. \$\endgroup\$Madagascar– Madagascar2024年05月07日 13:35:35 +00:00Commented May 7, 2024 at 13:35
-
1\$\begingroup\$ What version of C are you aiming to conform to? C89/C99/C11/C17/C2X? \$\endgroup\$Madagascar– Madagascar2024年05月07日 14:51:48 +00:00Commented May 7, 2024 at 14:51
-
\$\begingroup\$ I am on macOS, here is the version I have installed: Apple clang version 15.0.0 (clang-150010.2.5) Target: arm64-apple-darwin23.4.0 Thread model: posix \$\endgroup\$capodieci– capodieci2024年05月08日 17:07:48 +00:00Commented May 8, 2024 at 17:07
3 Answers 3
base58c_encode()
bounces everything through buffer[]
, but that doesn't seem to be necessary - we could write directly to *output
. The function does definitely need a comment explaining its unusual error reporting (i.e. that *ouput
is set to a null pointer on failure - why don't we just return the pointer?). I don't think we should be printing to stderr
from within the function - just report the failure to the caller, who can then decide on the appropriate response.
I had expected base58_decode()
to use the initial value of *out_len
as a limit on how much to write. It's hard to see that this function is safe to use without writing beyond the bounds of the supplied output array.
It would be nice to see some unit tests of the encode and decode functions, as these can be well-specified and the tests would give us confidence, particularly in the edge-cases. The same is true of the other functions in this program, too.
In main()
we have
if (argc != 3) { printf("Usage: %s \"wif_key\" \"message\"\n", argv[0]); return 1; }
I think that this message should go to stderr
, and EXIT_FAILURE
would be a clearer return value.
We have
exit(EXIT_FAILURE);
In main()
, it's generally better to return
rather than exit()
.
-
\$\begingroup\$ Thank you for the comments, you are right, I need to get back to perfection :) \$\endgroup\$capodieci– capodieci2024年05月08日 17:09:59 +00:00Commented May 8, 2024 at 17:09
Error messages go to stderr
, not stdout
:
// const char *message = "Hello";
const char *bitcoin_prefix = "\x18" "Bitcoin Signed Message:\n";
unsigned char private_key_hex[32];
#if 0
// Decode WIF to private key hex
if (decode_wif_to_hex(wif_key, private_key_hex) != 32) {
printf("Invalid WIF key\n");
return 1;
}
#else
if (decode_wif_to_hex(wif_key, private_key_hex) != 32) {
fprintf(stderr, "Invalid WIF key\n");
return EXIT_FAILURE;
}
#endif
Prefer standard exit status codes to non-standard ones:
stdlib.h
defines EXIT_SUCCESS
and EXIT_FAILURE
. I suggest using them instead of 0, 1, et cetera.
Eliminate superfluous comments:
In main()
, you have:
// Check if the correct number of arguments is provided
if (argc != 3) {
and:
// Get the wif_key and message from command-line arguments
const char *wif_key = argv[1];
const char *message = argv[2];
and:
// Decode WIF to private key hex
if (decode_wif_to_hex(wif_key, private_key_hex) != 32) {
and so on. None of these comments are useful, or provide any helpful information. The code is clear enough. Elide them all.
Eliminate commented-out code:
In main()
, you have:
// const char *message = "Hello";
which seems to be a relic from the past. Commented out code is code not ready for review.
Eliminate magic values:
In main()
, you have:
// Generate un_Bitcoin address from public key
unsigned char un_serialized_pubkey[65]; // Increase the buffer size to 65 bytes
size_t un_serialized_pubkey_len = 65;
secp256k1_ec_pubkey_serialize(secp256k1_ctx, un_serialized_pubkey, &un_serialized_pubkey_len, &pubkey, SECP256K1_EC_UNCOMPRESSED);
const unsigned char *d = un_serialized_pubkey;
uint8_t un_sha256_hash[32];
uint8_t un_pubkey_hash[20];
I do not know what meaning 65, 32, and 20 hold. Consider defining some named constants for these:
#define UNSERIALIZED_PUBKEY_BUFSIZE 65
#define SHA256_BUFSIZE 32
#define SERIALIZED_PUBKEY_BUFSIZE 30
// Or in C2X:
static constexpr const size_t UNSERIALIZED_PUBKEY_BUFSIZE = 65;
static constexpr const size_t SHA256_BUFSIZE = 65;
static constexpr const size_t SERIALIZED_PUBKEY_BUFSIZE = 30;
This would make the code easier to read and comprehend.
sizeof(char)
is defined by the ISO C Standard to be 1:
char* un_base58_address = malloc(34 * sizeof(char));
if (un_base58_address == NULL) {
exit(EXIT_FAILURE);
}
So you can eliminate sizeof(char)
from the malloc()
call. It would also be better to print some helpful diagnostic before exiting.
Break main()
down into smaller functions:
Currently, main()
is too long and does too much. It is responsible for:
- Validating command-line arguments.
- Decoding WIF to private key hex.
- Initializing libsecp256k1.
- Generating un_Bitcoin address.
- Creating the prefixed message.
- Hashing the prefixed message using SHA256 twice.
- Signing the message hash.
- Serializing the recoverable signature.
- Conversion to Bitcoin format.
- Encoding the signature in base64.
And a whole lot of print statements. I'd expect all of these operations to reside in their own separate functions. This would make them reusable, reduce duplicity, and simplify main()
.
Do not cast the return value of malloc()
:
Since C89, or around 35 years, malloc()
and family have returned a generic void *
that is implicitly converted to and from any other pointer type. As such, casting the return value is redundant and only serves to clutter the code.
Check the return value of library functions:
In base64_encode()
, you've not checked the return value of malloc()
for failure. malloc()
and family return a null pointer on failure. Failing to check for it risks invoking undefined behavior by a subsequent null pointer dereference.
-
\$\begingroup\$ Amazing good points, I will slowly go back to write clean and nice code, thank you again for the annotations. \$\endgroup\$capodieci– capodieci2024年05月08日 17:10:47 +00:00Commented May 8, 2024 at 17:10
It looks like you've put significant effort into manually handling Bitcoin operations in C, which is commendable given the complexity! For C libraries specifically tailored to Bitcoin, libsecp256k1 is a well-known option developed primarily for Bitcoin core. It supports operations like key pair generation, message signing, and signature verification, which seem to be your core needs.
The usage of libsecp256k1 might streamline some of your current implementations, especially if you're handling cryptographic functions manually. This library is highly optimized for safety and performance, which is crucial in blockchain-related tasks. Here's a GitHub link for more detailed documentation and examples: libsecp256k1 GitHub.
Also, integrating OpenSSL for cryptographic functions is a solid choice, as you've done. For enhancing your current code, you might want to look into optimizing memory management and error handling, as these are critical in secure and efficient C programming.
If you need more specific advice on optimizing certain parts of your code, feel free to share more details!
-
4\$\begingroup\$ But OP is already using the library you're suggesting. \$\endgroup\$Madagascar– Madagascar2024年05月07日 15:02:04 +00:00Commented May 7, 2024 at 15:02
-
\$\begingroup\$ I will read more on libsecp256k1 but I am failing to do many of the things that I opted to do with other functions... I cannot find indications on how to use it to do more things :( \$\endgroup\$capodieci– capodieci2024年05月08日 17:12:06 +00:00Commented May 8, 2024 at 17:12
Explore related questions
See similar questions with these tags.