Crafting a Blockchain in Go and Rust: A Comparative Journey — Private keys, Public Keys and Signatures [Part 1]

Crafting a Blockchain in Go and Rust: A Comparative Journey — Private keys, Public Keys and Signatures [Part 1]

Setting up the base

Projects available on Github:

View on Github (Pull Requests are Welcome) — Rust Version

View on Github (Pull Requests are Welcome) — Go Version

<- Part 0 — Crafting a Blockchain in Go and Rust: A Comparative Journey — Introduction and Overview

Part 2 - Crafting a Blockchain in Go and Rust: A Comparative Journey — Blocks and Transactions ->

This episode is about private keys, public keys, signatures and adding a CLI interface to the Blockchain.

Today I will walk you through the initials steps I took to start building a Blockchain in both Go and Rust, and while I do that I’ll also try to lay out some of the differences and intricacies between the two programming languages. The plan is to go over the CLI Interface and why I already added one so early in the game, also cover the generation of private and public keys as well as being able to sign chunks of data and verifying with those same keys.

After reading this you will have a good understanding on why I decided to add a CLI interface before I even had anything done in my project and also learn about the steps I took to generate the private and public key, as well as sign and signature verification with both programming languages, Go and Rust.

Who knows? Maybe by the end I’ll even reveal which one is my favorite so far.

But a heads up, this is not a step by step tutorial, I’ll try to explain my thought process and some of decisions I took, but I will not explain line by line of the code. The code is on Github (Go / Rust), and I am happy to answer any questions, so feel free to reach out.

CLI INTERFACE

On this section of the article I want to first explain why I decided to add a CLI Interface, even if it didn’t do much at first, so early in the game. And why I believe it will make my life so much easier as the project progresses.

I am also going to talk briefly about the different implementations, packages and crates I choose for such task, and why I did not go with the standard library.

Let’s start with the why!

“why are you wasting time with this at this stage in the process?”

I actually received the same question when I did my database project (SQLRite) and the first thing I did was add a REPL interface that didn’t do anything, just echoed commands.

And the reason is because I wanted to have an interface ready too call out features of my application without having to pull up some hacky code on my main.go or main.rs for example. And it also forces me to design my code in a way where everything is decoupled and any dependencies I need for anything, they need to be injected it.

Yes, don’t worry, I also have unit tests for a lot of the code that runs every time I push to Github (or manually), but still, I like to have a way to interface directly with my application. And by having a basic CLI interface setup I can easily add commands to it, take arguments and call whatever I want in the application. I can even make the CLIs a different binary if i want, which can be really handy in the future.

Adding a CLI Interface to Go

As I mentioned I decided not to use the Standard Library for this because although the Go and Rust standard library provides all the essential building blocks for working with command-line arguments, flags, and interacting with the operating system. I am already imagining having commands, sub-commands, multiple arguments and flags, for example:

Using the CLI to restore the blockchain address from a mnemonic phrase:

> ./marvin-blockchain address restore --mnemonic 'unhappy describe tuna century because antique close trash bike bread crater notable'
address: f650a946e9ddedcf9ba36105eba9a59d5dd0992d

But I also can have the sub-command create under address , to create a new blockchain address and mnemonic:

> ./marvin-blockchain address create
Generating new address...
mnemonic: interest issue wolf swap father predict define exercise coral forum depart slide
address: 9b1c6518c0906db58f39e5c1410b02b7e486ef80

And if I had to build that from scratch with just the standard library would take focus out of the main target of the project, which is to create a blockchain and not a CLI application from scratch.

That said, I looked into a couple of options of CLI packages for Go. The top 3 ones I looked into it were urfave/cli, alecthomas/kong and finally spf13/cobra which I ended up picking. All of them were alright, I guess urfave/cli and alecthomas/kong are lighter libraries, but at the end of the day I went with cobra for being more feature rich, it is battle tested by a lot of other big cli applications out there and it is the one I am more familiar with.

Adding a CLI Interface to Rust

For Rust the reasoning was the same on why not to use the Standard Library, so I won’t spent time explaining again.

What is worth mentioning is that in the case of Rust I only looked at one crate, and that was Clap. I have used Clap before, it is a battle tested Crate used by a number of large projects and it’s API is super easy to use. On top of being super feature rich. So for me it was a no brainer in choosing Clap for Rust.

Generating the private key and public key

One of the first things to look at when generating a key pair is to look at which digital signature algorithm to choose from and on this section we will dive into a couple of these algorithms, explain how they work, highlight their differences, explain the reasoning behind the one I choose and also dive a little into each of the implementations with Go and Rust.

By the end of this section you should have at least have a high level understanding of these algorithms and also have a good understanding of the ground work I am laying down that will be used later on during the development of the blockchain.

What were our choices?

The two most used algorithms for digital signatures in the blockchain space are the Elliptic Curve Digital Signature Algorithm (ECDSA) with the secp256k1 curve and the Edwards-curve Digital Signature Algorithm (EdDSA) with the curve25519 curve, aka ed25519.

ECDSA with secp256k1

ECDSA is a widely adopted asymmetric cryptographic algorithm used for digital signatures. It operates on elliptic curves and provides a secure mechanism for verifying the authenticity and integrity of data. The secp256k1 curve, specifically, is a popular elliptic curve used in Bitcoin and several other cryptocurrencies.

The secp256k1 curve has a specific mathematical structure that allows for efficient cryptographic operations. It is defined by the equation y² = x³ + 7 and operates over the finite field of prime numbers. ECDSA with secp256k1 utilizes the properties of this curve to generate public and private key pairs, sign messages, and verify signatures. It offers a high level of security and efficiency for cryptographic operations.

EdDSA with curve25519

EdDSA is another cryptographic algorithm based on elliptic curves, specifically the Edwards curves. It was designed to provide improved security, simplicity, and performance compared to earlier algorithms like ECDSA. One commonly used Edwards curve is the curve25519. Oh and it was invented after Bitcoin was created.

Curve25519 is an elliptic curve defined by the equation -x² + y² = 1 — (121665/121666) \ *x² * y². It operates over the prime field and is known for its efficiency and security properties. EdDSA with curve25519 leverages this curve to generate key pairs, sign messages, and verify signatures.

The main advantage of EdDSA is its resistance to certain types of side-channel attacks, improved performance, and enhanced security features. It has gained popularity in various blockchain platforms and cryptographic applications.

Differences between ECDSA secp256k1 and EdDSA curve25519

While both ECDSA with secp256k1 and EdDSA with curve25519 provide secure digital signatures, they differ in several aspects:

  1. Curve Structure: ECDSA secp256k1 operates on the secp256k1 elliptic curve, while EdDSA curve25519 utilizes the curve25519 Edwards curve. These curves have different mathematical equations and properties.

  2. Key Generation: ECDSA secp256k1 and EdDSA curve25519 have different mechanisms for key generation. ECDSA generates key pairs using the secp256k1 curve, while EdDSA uses the curve25519 curve.

  3. Performance: EdDSA with curve25519 is generally more efficient in terms of computational resources and offers faster signature generation compared to ECDSA secp256k1. It achieves this efficiency through the use of twisted Edwards curves.

  4. Security Features: EdDSA is designed to provide enhanced security against certain types of attacks, such as certain side-channel attacks. It incorporates additional security measures compared to ECDSA.

So, which one I went with?

3…2…1… And the winner is?!!!

EdDSA with curve25519, AKA ed25519!

And here is why. To be honest in my opinion and based on my very limited knowledge of cryptography there isn’t a strong enough reason to switch in either direction, both of them are cryptographic algorithms widely used for digital signatures in blockchain applications.

So I basically made the choice based of two things:

  • What do I have more experience and exposure with?

The answer to this one would be definitely ed25519 .

  • Do I have stable and battle tested packages and crates that I could use?

And based on my previous answer I just had to find to make sure I stable enough libraries for both languages, Go and Rust. And the answer is yes.

For Go there is an implementation ofed25519 that is part of the Standard Library and can be found at [crypto/ed25519](https://pkg.go.dev/crypto/ed25519) And if the case of Rust we there is an excellent native Ed25519 implementation called [ed25519-dalek](https://crates.io/crates/ed25519-dalek) . Which has fast and efficient ed25519 EdDSA key generations, signing, and verification in pure Rust.

With the libraries chosen I will now talk about the APIs I created for both Go and Rust and will see that they are actually pretty similar. At a high level this is what you will find on both codebases.

Go API Implementation

Constants used throughout:

const (
    privateKeySize = ed25519.PrivateKeySize // 64
    publicKeySize = ed25519.PublicKeySize // 32
    signatureSize = ed25519.SignatureSize // 64
    seedSize = 32
    addressSize = 20
)

Private Key Implementation:

// PrivateKey represents a private key for the Ed25519 signature scheme.
type PrivateKey struct {
    key ed25519.PrivateKey
}

// GetMnemonicFromEntropy generates a new mnemonic from a given entropy.
func GetMnemonicFromEntropy(entropy []byte) string {...}

// NewPrivateKeyfromMenmonic creates a new private key from a given mnemonic.
func NewPrivateKeyfromMnemonic(mnemonic string) PrivateKey {...}

// SeedFromMnemonic creates a new seed from a given mnemonic.
func SeedFromMnemonic(mnemonic string) []byte {...}

// NewPrivateKeyfromString creates a new private key from a given hex string.
func NewPrivateKeyfromString(s string) PrivateKey {...}

// NewPrivateKeyFromSeed creates a new private key from a given seed byte slice.
func NewPrivateKeyFromSeed(seed []byte) PrivateKey {...}

// Bytes creates a new private key from a byte slice.
func (p *PrivateKey) Bytes() []byte {...}

// Sign signs the data with the private key.
func (p *PrivateKey) Sign(data []byte) *Signature {...}

// PublicKey returns the public key for the private key.
func (p *PrivateKey) PublicKey() *PublicKey {...}

Public Key Implementation:

// PublicKey represents a public key for the Ed25519 signature scheme.
type PublicKey struct {
    key ed25519.PublicKey
}

// Address returns the address for the public key.
func (p *PublicKey) Address() Address {...}

// Bytes creates a new public key from a byte slice.
func (p *PublicKey) Bytes() []byte {...}

Signature Implementation:

// Signature represents a signature for the Ed25519 signature scheme.
type Signature struct {
    value []byte
}

// Bytes creates a new signature from a byte slice.
func (s *Signature) Bytes() []byte {---}

// Verify verifies the signature of the data with the public key.
// It returns true if the signature is valid, and false otherwise.
func (s *Signature) Verify(publicKey *PublicKey, data []byte) bool {...}

Address Implementation:

// Address represents an address for the Ed25519 signature scheme.
type Address struct {
    value []byte
}

// Bytes creates a new address from a byte slice.
func (a *Address) Bytes() []byte {...}

// String returns the address as a hex string.
func (a Address) String() string {...}

Rust API Implementation

Constants used throughout:

const PRIVATE_KEY_SIZE: usize = ed25519_dalek::SECRET_KEY_LENGTH;
const PUBLIC_KEY_SIZE: usize = ed25519_dalek::PUBLIC_KEY_LENGTH;
const SIGNATURE_SIZE: usize = ed25519_dalek::SIGNATURE_LENGTH;
const ADDRESS_SIZE: usize = 20;
const SEED_SIZE: usize = 32;

Private Key Implementation:

pub struct PrivateKey {
    pub key: SigningKey,
}

/// new_entropy generates a new 16 byte entropy
pub fn new_entropy() -> [u8; 16] {...}

/// get_mnemonic_from_entropy generates a new mnemonic from the given entropy
pub fn get_mnemonic_from_entropy(entropy: &[u8; 16]) -> String {...}

/// get_seed_from_mnemonic generates a new seed from the given mnemonic
pub fn get_seed_from_mnemonic(mnemonic: &str) -> [u8; SEED_SIZE] {...}

/// get_private_key_from_mnemonic generates a new private key from the given mnemonic
pub fn get_private_key_from_mnemonic(mnemonic: &str) -> PrivateKey {...}

/// new_private_key_from_string generates a new private key from the given private key string
pub fn new_private_key_from_string(private_key: &str) -> PrivateKey {...}

/// new_private_key_from_seed generates a new private key from the given seed
pub fn new_private_key_from_seed(seed: &[u8; 32]) -> PrivateKey {...}

/// generate_private_key generates a new private key with a random seed
/// (Good for testing purposes)
pub fn generate_private_key() -> PrivateKey {...}

impl PrivateKey {
    pub fn sign(&mut self, data: &[u8]) -> SignatureWrapper {...}

    /// Sign a message with the private key
    pub fn to_bytes(&self) -> [u8; PRIVATE_KEY_SIZE] {...}

    /// Sign a message with the private key
    pub fn public_key(&self) -> PublicKey {...}
}

Public Key Implementation:

pub struct PublicKey {
    pub key: VerifyingKey,
}

impl PublicKey {
    // to_bytes creates a new public key from a byte slice.
    pub fn to_bytes(&self) -> [u8; PUBLIC_KEY_SIZE] {...}

    /// address returns the address for the public key.
    pub fn address(&self) -> Address {...}
}

SignatureWrapper Implementation:

pub struct SignatureWrapper {
    pub signature: Signature,
}

impl SignatureWrapper {
    /// Convert the signature to bytes
    pub fn to_bytes(&self) -> [u8; SIGNATURE_SIZE] {...}

    /// Verify a message with the public key
    pub fn verify(&self, data: &[u8], public_key: &PublicKey) -> bool {...}

Address implementation:

pub struct Address {
    pub value: [u8; ADDRESS_SIZE],
}

impl Address {
    /// Convert the address to a string in hex format
    pub fn to_string(&self) -> String {...}

    /// Convert the address to bytes
    pub fn to_bytes(&self) -> [u8; ADDRESS_SIZE] {...}
}

The full code you will find it on Github (Go / Rust), but I will talk about some key points and some of the differences so far between Go and Rust. Also bare in mind that this code will probably be refactored and improved also as the project evolves.

Signing and Veryfing

The good thing of planing and building a good API is that you are laying the ground work for what is to come, and you are making things easier for yourself. And this is exactly the case here.

At the end of the day everything in a blockchain is just a chunk of bytes, and that could be a transaction, a block or some other piece of private information. What that is doesn’t really matter, what matters is that you are able to Sign and Verify with your Private Key and Public Key. So here we go.

I think the best way to exemplify is just to show the Unit Test I created that does exactly that:

Go:

func TestPrivateKeySign(t *testing.T) {
    privateKey := GeneratePrivateKey() // Generates a private key
    publicKey := privateKey.PublicKey() // Retrives the public key
    invalidPrivateKey := GeneratePrivateKey() // Generates another private key
    invalidPublicKey := invalidPrivateKey.PublicKey() // Retrievs another public key

    data := []byte("hello, world") // Any data converted into a []byte
    // Signs the data with your PrivateKey and generates a Signature Type
    signature := privateKey.Sign(data)

    // Check that the signature is the correct size
    assert.Equal(t, signatureSize, len(signature.value))

    // Check that the signature is valid
    assert.True(t, signature.Verify(publicKey, data)) // Returns True

    // Check that the signature is invalid
    assert.False(t, signature.Verify(invalidPublicKey, data)) // Returns False
}

Rust:

#[test]
fn test_private_key_sign() {
    // Generates a private key
    let mut private_key = generate_private_key();
    // Retrives the public key
    let public_key: PublicKey = private_key.public_key();
    // Generates another private key
    let invalid_private_key = generate_private_key();
    // Retrievs another public key
    let invalid_public_key: PublicKey = invalid_private_key.public_key();

    let data = b"hello world"; // Any data converted into a []byte
    // Signs the data with your PrivateKey and generates a SignatureWrapper Type
    let signature = private_key.sign(data);

    // Verify the signature size
    assert_eq!(SIGNATURE_SIZE, signature.signature.to_bytes().len());

    // Verify the signature - Return True
    assert_eq!(true, signature.verify(data, &public_key));

    // Verify the signature is invalid - Return False
    assert_eq!(false, signature.verify(data, &invalid_public_key));
}

As you can see we just made use of the API that we created so far and very easily we were able to sign and verify a chunk of data.

By now you can start to see how this can all come together. Since when we create a Private and Public Key we can also output a Mnemonic phrase, we can easily retrieve the Private Key from that Mnemonic again in the future and use that to sign transactions, blocks and etc. And that is why is so important for you to keep your Mnemonic Secrets from your wallet actually secret. If someone gets their hand on that, they basically own your wallet.

(Obviously there other security mechanisms that can also be used, but just in case, store your Mnemonic phrase in a place no one will find.

Comparing Go and Rust

One of the things you may have already noticed is that the way we declare methods is very different between Go and Rust. When it comes to the definition no matter the language, they are always going to be similar. This is the one I use.

Methods are similar to functions but methods are defined within the context of a user defined type and provide a way to add behavior to those user-defined types. The way they are declared are slightly different between the two languages. Let’s look at a live example:

Let’s look at the implementation of the method to sign a chunk of data that is associated with the user defined type Private Key .

In Go:

// Sign signs the data with the private key.
func (p *PrivateKey) Sign(data []byte) *Signature {
    return &Signature{
        value: ed25519.Sign(p.key, data),
    }
}

In Rust:

impl PrivateKey {
    /// Sign a message with the private key
    pub fn sign(&mut self, data: &[u8]) -> SignatureWrapper {
        SignatureWrapper {
            signature: self.key.sign(data),
        }
    }
}

Don’t mind the implementation of the method it self, since that is not the focus right now. But do you notice the differences?

In Go, all you need to do it to add that extra parameter that is declared between the keyword func and the function name, and also enclose that with parentheses. You also have the option of making that a reference value or a pointer value, usually depending on wether or not you will be updating any values of the instance.

Now in Rust you do not need that extra parameter, at least not in the same place. But you do need that the first parameter is always &self, which represents the instance of the user defined type the method is being called on. And on top of that all your methods associated with that user defined type need to within an impl block. The &self is actually short for self: &Self. Within an impl block, the type Self is an alias for the type that the impl block is for. Methods must have a parameter named self of type Self for their first parameter, so Rust lets you abbreviate this with only the name self in the first parameter spot. Note that we still need to use the & in front of the self shorthand to indicate that this method borrows the Self instance.

If you have trouble understanding ownership in Rust I highly recommend reading this chapter in The Rust Programming Language Book.

What now?!

On this post we have walked through which packages and crates I used for my CLI interface as well as why I decided to add it to the project at this stage. We also talked about the two most widely use digital signature algorithms used by blockchains today and why I decided to go with the ed25519 for this project. After that I explained what was built so far related to the key pair generation and also how to easily sign and verify chunks of data with those same key pairs. And to end it all out we finished with a quick comparison of Go and Rust and how the two languages approach declaring and implementing methods.

Don’t forget to Like and Subscribe here on Medium to be notified about the next posts in the series and also STAR the projects on Github to follow what is going on over there.

View on Github (Pull Requests are Welcome) — Rust Version

View on Github (Pull Requests are Welcome) — Go Version

Cheers!