Some first steps on native CoinZdense code

in Programming & Devlast month

It's been a while since I've written C++ template code, and it's been a while since I've worked on CoinZdense, but recently I have been doing both at once. As a reminder, so far CoiZdense has been mostly proof of concept and demonstrator level code written purely in Python, and while Python on it's own isn't ideal from a performance perspective, it is great for testing out concepts ang getting a feel for stuff.

Because Python was showing it's performance limitation, especialy in CoinZdense post-quantum hash-based key generation, and because Python won't be my only target language, I have been learning Rust, and untill recently was planning on rewriting my Python efforts and more in Rust to target multiple languages including Python, C++ and Web languages like TypeScript and ClojureScript through WASM. My recent Rust learning effords were focused on just that. Writing a small Rust core and building it into something that could be used from most of my target languages so I don't have to rewrite as much code as I was originally planning.

As a recap, the languages I intent to support with CoinZdense are divided into four tiers:

tier 1 languages

Tier One languages are the languages needed for showing the world things are serious with the project. A performant Python implementation for scripting, and a C++ implementation so there is at least potential to move things into a C++ based blockchain like HIVE.

  • Python
  • C++

tier 2 languages

Still very high priority are the tier 2 languages. These are needed to branch out to web frontends and to branch out to the posibility of reaching a wider range of Web3 and Web3-like blockchains. Chains and blockchain tech that I'm really interested in are FlureeDB, Agoric, Rune and Atom. To get at least one foot in towards porentialy providing enough to create signing infrastructure migration incentive, the tier 2 languages need to come in.

tier 3 languages

These languages are languages I have harly used myself, but I probably should learn them enough to support them because they play an important role in some Web 3.o ecosystems at least:

  • TypeScript
  • Go
  • C

tier 4 languages

The final tier are languages that I am not going to write any libs for myself, but for that I want to commit to supporting any developer in getting things working.

  • Ruby
  • PHP
  • Java
  • Swift

Rust or C++ or both

After experimenting with Rust code and making bindings for different languages in the list above, I discovered Rusnt isn't as mature in interoperability tooling and possibilities as I would like it to be. Next to that, my experience with C++ is almost two decades and thus an order of magnitude greater than my experience with Rust so far. As such I recently had to make the choice to give up on the idea of building everything from Rust. I still want to learn more Rust and do some actual open source work, so it is likely I'll eventualy write a Rust port of the C++ code. Because one thing is clear, Rust is too great a language to limit the CoinZdense API in Rust to just what a C style inter-language interface allows for. I'm going full C++ for the C++ API with plenty of template code for type and memory safety and native code flexability.

First bits of C++ code

So far things are moving both bottom up and top down, both with libsodium at the core. On one end I'm playing with the libsodium key-derivation functionality, shaping it into a static least-authority template API. At the other end I'm optimizing the dual-chain WOTS code that relies on the libsodium generic hash function. Eventualy the two will meet in the middle, but I'm still far from that point so far.

Let me share some of the code.

#pragma once
// Copyright (c) 2025, Rob J Meijer
// Licensed under the BSD 3-Clause License. See LICENSE
// file in the project root for full license information.
#include <sodium.h>
#include <memory>
#include <stdexcept>
#include <string>
#include "value.hpp"
namespace coinzdense {
namespace sodium {
struct SodiumInitException : std::runtime_error {
  explicit SodiumInitException(const std::string& message):
        std::runtime_error("Sodium initialization failed: " + message) {}
};
struct KdfDeriveException : std::runtime_error {
  explicit KdfDeriveException(const std::string& message):
        std::runtime_error("Key derivation failed: " + message) {}
};
}  // namespace sodium
namespace entropy {
template <uint8_t S>
struct AbstractEntropy {
  virtual ~AbstractEntropy() = default;
  virtual std::array<uint8_t, S> operator () (uint64_t subkey_id) = 0;
};

template <uint8_t S>
struct SecretEntropy: AbstractEntropy<S> {
    explicit SecretEntropy(std::array<uint8_t, crypto_kdf_KEYBYTES> masterkey):
        mContext{'C', 'o', 'i', 'n', 'Z', 'd', 'n', 's'}, mMasterKey(
            masterkey) {
      if (sodium_init() == -1) {
        throw coinzdense::sodium::SodiumInitException(
            "Failed to initialize libsodium");
      }
    }
    std::array<uint8_t, S> operator () (uint64_t subkey_id) {
      uint8_t subkey[S];
      if (crypto_kdf_derive_from_key(subkey, S, subkey_id, mContext.data(),
                                     mMasterKey.data()) == -1) {
        throw coinzdense::sodium::KdfDeriveException(
            "Invalid parameters or system error");
      }
      return std::to_array(subkey);
    }
 private:
    std::array<char, crypto_kdf_CONTEXTBYTES> mContext;
    std::array<uint8_t, crypto_kdf_KEYBYTES> mMasterKey;
};

template <uint8_t S>
coinzdense::value::ValueSemantics<AbstractEntropy<S>, uint64_t, S>
    make_secret_entropy(std::array<uint8_t, 32> masterkey) {
  return coinzdense::value::ValueSemantics<AbstractEntropy<S>,
                                           uint64_t, S>(
        std::make_unique<SecretEntropy<S>>(masterkey));
}
}  // namespace entropy
}  // namespace coinzdense

The code starts with the definition of two exceptions that we'll need in case libsodium C functions fail. Then comes an abstract baseclass AbstractEntropy. Finaly we have the real implementation in the SecretEntropy class, followed by a builder function that wraps the implementation class in a ValueSemantics wrapper (that we will discuss later).

The make_secret_entropy takes a 32 bytes master key to build a wrapped SecretEntropy object that will return an S bytes long derived key for any 64 bit integer provided to it's call operator.

We can create and invoke our deterministic entropy source like this:

std::array<uint8_t, 32> key{0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1};
auto entropy = coinzdense::entropy::make_secret_entropy<20>(key);
auto sub = entropy(1234567);

Ignore the fact that the 32 byte key is a silly hardcoded array for now. Note how we create our entropy object with the master key, providing the subkey size (in bytes) as a static template parameter.
In this case 20 is the minimum size libsodium recomends for a safe full 64 bit addressable subkey space. The last line shows how we can request a subkey.

This is all fine and dandy, but access to all subkeys isn't exactly safe according to the principle of least authority.

We fix this with a second bit of C++ code:

#pragma once
// Copyright (c) 2025, Rob J Meijer
// Licensed under the BSD 3-Clause License. See LICENSE
// file in the project root for full license information.
#include <array>
#include <stdexcept>
#include <memory>
#include <type_traits>
#include <limits>
#include <utility>
namespace coinzdense {
namespace value {
template <typename Abstract, typename I, uint8_t S, I Min, I Max>
struct Ranged: Abstract {
  static_assert(std::is_integral_v<I>, "I must be an integral type");
    explicit Ranged(std::weak_ptr<Abstract> ptr): pImpl(ptr) {}
    std::array<uint8_t, S> operator () (I id) {
      if (id < 0 || id + Min > Max) {
        throw std::out_of_range("Ranged invocation outside of defined range");
      }
      auto sp = pImpl.lock();  //  Convert weak_ptr to shared_ptr
      if (!sp) {
        throw std::runtime_error("Ranged: Underlying object no longer exists");
      }
      return (*sp)(id+Min);
    }
    template<I Start, I End>
    Ranged<Abstract, I, S, Min + Start, Min + End> ranged() {
      static_assert(
         Start >= 0 && Start + Min < Max && End > 0 &&
             End + Min <= Max && End > Start,
         "Ranged decomposition outside of defined range");
      return Ranged<Abstract, I, S, Min + Start, Min + End>(pImpl);
    }
 private:
    std::weak_ptr<Abstract> pImpl;
};
template <typename Abstract, typename I, uint8_t S>
struct ValueSemantics: Abstract {
  static_assert(std::is_integral_v<I>, "I must be an integral type");
    explicit ValueSemantics(std::unique_ptr<Abstract> ptr):
          pImpl(std::move(ptr)) {}
    std::array<uint8_t, S> operator () (I id) {
      return (*pImpl)(id);
    }
    template<I Start, I End>
    Ranged<Abstract, I, S, Start, End> ranged() {
      static_assert(End > Start && End <= std::numeric_limits<I>::max() &&
                      Start >= std::numeric_limits<I>::min(),
                      "Ranged decomposition outside of type limits");
      return Ranged<Abstract, I, S, Start, End>(pImpl);
    }
 private:
    std::shared_ptr<Abstract> pImpl;
};
}  // namespace value
}  // namespace coinzdense

Here we see a nice bit of templae library magic. Remember how before the implemmentation got wrapped in a ValueSemantics wrapper. Here we see that wrapper template at is base provides just a Pointer To Implementation or pImpl pattern. But it does so with a tiny twist. A template function ranged allows the user to create a proxy to the deterministic entropy source that limits access to only a subset ot the full 64 bit index space. So now we can do something like this:

auto range1 = entropy.ranged<4000, 8000>();
auto sub2 = range1(1500);
try {
  auto sub2b = range1(5000);
  throw std::runtime_error("Failure in first range test");
}
catch (std::out_of_range const &e) {}

We statically get a proxy to the deterministic entropy source that will only give us access to the subkeys from id 4000 upto 8000. Note that this range is maped to zero based, so the proxy will allow indices from 0 upto 2000 that will get mapped to 4000 upto 8000 on usage. In this sample code, range1(1500) should succeed, but range1(5000) will raise an out of range exception.

This can be done in a nested way because the Ranged template has a ranged method too:

auto range2 = range1.ranged<50, 150>();
auto sub3 = range2(75);
try {
      auto sub3b = range2(110);
      throw std::runtime_error("Failure in second range test");
}
catch (std::out_of_range const &e) {}

This does the same thing on the next level. Note that 0 at this second level will first get mapped to 50 at level 1, what then will get mapped to 4050 at level 0.
This nesting allows us to first divide the whole index space up into prime key-space and POLA subkey-space. Then to divide prime key-space into per level-key key-space, then divide per level-key key-space into per onetime-key keyspace. Then divide per onetime-key keyspace into the low level WOTS keyspace chunks.

So far the failure was runtime, but a lot should get cought at compiletime already. Let's shange the first line:

auto range2 = range1.ranged<50, 8150>();

This code won't compile because static asserts will cause compile errors:

coinzdense/value.hpp:31:24: error: static assertion failed: Ranged decomposition outside of defined range

I'll post more about the progress on the C++ code as things progress. For now this is a sample of the humble beginnings of where I'm heading with this. If you want to keep up with things, please star or watch the project on github.

Sort:  

Thanks for your contribution to the STEMsocial community. Feel free to join us on discord to get to know the rest of us!

Please consider delegating to the @stemsocial account (85% of the curation rewards are returned).

Consider setting @stemsocial as a beneficiary of this post's rewards if you would like to support the community and contribute to its mission of promoting science and education on Hive.