1
\$\begingroup\$

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;
}
Toby Speight
87.3k14 gold badges104 silver badges322 bronze badges
asked May 7, 2024 at 13:32
\$\endgroup\$
3
  • 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\$ Commented May 7, 2024 at 13:35
  • 1
    \$\begingroup\$ What version of C are you aiming to conform to? C89/C99/C11/C17/C2X? \$\endgroup\$ Commented 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\$ Commented May 8, 2024 at 17:07

3 Answers 3

3
\$\begingroup\$

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().

answered May 8, 2024 at 8:19
\$\endgroup\$
1
  • \$\begingroup\$ Thank you for the comments, you are right, I need to get back to perfection :) \$\endgroup\$ Commented May 8, 2024 at 17:09
3
\$\begingroup\$

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.

Toby Speight
87.3k14 gold badges104 silver badges322 bronze badges
answered May 8, 2024 at 10:41
\$\endgroup\$
1
  • \$\begingroup\$ Amazing good points, I will slowly go back to write clean and nice code, thank you again for the annotations. \$\endgroup\$ Commented May 8, 2024 at 17:10
1
\$\begingroup\$

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!

answered May 7, 2024 at 13:42
\$\endgroup\$
2
  • 4
    \$\begingroup\$ But OP is already using the library you're suggesting. \$\endgroup\$ Commented 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\$ Commented May 8, 2024 at 17:12

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.