This is version 3, and hopefully the final version of the coinZdense signature format. This post will be a bit dense without much of the explanation (will do a more verbose post later), I'll assume the reader has read one of my earlier posts on the structure of the coinZdense hash-based signature design and has a basic understanding of the base ideas of hash-based signature and of the reasons we will soon need them for quantum resistance.
Signing key parameters
When you decide to use coinZdense signatures in your crypto project, you will need to choose the dimentioning of your signing keys, and for projects like HIVE that use multiple priviledge levels for keys derived from a single passphrase, you will need to define those. The key signing-parameters, that you will need to choose for your project, and that shouldn't change are:
- hashlength : The size (in bytes) for hashes used throughout the implementation.
- otsbits : The number of bits signed with a single One-Time-Signature hash-pair
- heights : A list of values for the Merkle-Tree height for keys at a given level
- privpath : A priviledge path (more on this later)
- levels : The number of level keys your application uses.
The privilege level will be zero for the highest level of privilege defined, or if your project doesn't distinguish between privilege levels.
The general signature structure.
The coinZdense signatures are rather big compared to conventional signatures from for example ECDSA. And they are not all the same size. As you may remember, coinZdense signatures use a chain of level-keys, and if the client is known to already have full knowledge of most of the chain, the signature will be a lot shorter than if the whole chain needs to be communicated in the signature.
- privhash : hashlenght bytes
- sigcount : 8 bit unsigned integer
- sigindex : 64 bit unsigned integer
- msgsalt : hashlength bytes
- msgdigest : hashlength bytes
- pubkeys : levels pubkeys of hashlength bytes each
- sigchain : sigcount levelsig structures
The sigchain part of the signature consists of one or more of the following structures:
- merkleheader : height merkle-tree node values of hashlength bytes each
- otssigs : later more on this one
privhash
The privhash is meant for scenario's like in HIVE where different keys, derived from the same passphrase, can be used to sign operations requiring different privileges. While for HIVE these privileges are linear, coinZdense aims to turn them into a DAG of privilege decomposition.
Think of it as a file system directory structure where at the deepest level you find operations. The top-level contains a path to each operation, and deeper levels less and less. This way, the privileges can be decomposed using multiple levels of granularity. Each set of privileges can be described by a path. For HIVE the OWNER key could be considered the root of our file-system. The ACTIVE key could be seen as an ACTIVE sub directory, and the POSTING key could be considered to have a path of ACTIVE/POSTING. Bit it doesn't have to stop there. We could decompose further into for example individual operations. We could define a key for the path ACTIVE/create_claimed_account or for ACTIVE/POSTING/vote just as easily.
But that should all be up to whoever creates the application. For the signatures, what we care about is that we can create a root key from a passphrase and can create a less privileges key from a more privileges key, like we can with HIVE. The secure hash of the priviledge path is the privhash used in the signature.
sigcount
Imagine that your signing key, able to sign one million transactions in total, consists of four level keys each able to sign 32 times.
- Your level 0 level-key can sign up to 32 level-1 keys
- Your level 1 level-key can sign up to 32 level-2 keys
- Your level 2 level-key can sign up to 32 level-3 keys
- Your level-3 level-key can sign up to 32 transactions
Now imagine your level-3 level key just singed it's 4th transaction. When it signs a 5th one, the blockchain already knows the level-3 key was signed by the level-2 key, and so on, all the way up to the level-0 key. This means the client could do with just sending the signature for the transaction. The sigchain would look something like this:
- level-3 signature of the transaction
Now imagine your level-3 level key just singed it's 4th, and not that long ago, so did the level-2 level-key. The next signature will need to be made by a brand new level-3 level-key that the blochchain knows nothing about. The level-2 key is exausted as well, so we also need a brand new level-2 level key, that also the blockchain knows nothing about. So now our sigchain looks quite a bit longer:
- level-3 signature of the transaction
- level-2 signature of the pubkey of the level-3 level-key
- level-1 signature of the pubkey of the level-2 level key
Your project may also choose to always communicate the full chain, or to first try the shortest possible chain, and on request communicate the full chain:
- level-3 signature of the transaction
- level-2 signature of the pubkey of the level-3 level-key
- level-1 signature of the pubkey of the level-2 level key
- level-0 signature of the pubkey of the level-1 level key
Now all the sigcount header does is communicate how many signatures the sigchain contains.
sigindex
This one is straight forward. Every time we create a signature, we use up an index number. In our example we had 1 milion, or 1,048,576 to be exact, signatures to spent. This 64 bit number is the number of the signature.
msgsalt
Where level-key pubkeys are high entropy, a transaction might not be. The msgsalt is a high entropy salt we use while hashing a transaction prior to signing it.
msgdigest
This is the salted digest of the transaction we are signing.
pubkeys
An upside-down list of level-key pubkeys, starting with the transaction signing level-key and ending with the root key.
merkleheader
The merkle header contains a chain of merkle-tree nodes the receiver of the signature can use to rebuild the merkle root that doubles as public key. Note that the length of the merkleheader may and often will differ between levels. In our example though, our merkle height is five, so ghe merkle header will consist of five hashlengt long node values.
otssigs
The otssigs array contains a number of OTS pairs just long enough to sign hashlength times 8 bits. One OTS-pair, consisting of an up hashlength long One-Time-Signature and an hashlength long down One-Time-Signature for the same otsbits bits.
So if our hashlength is 24 bytes, translating to 192 bits, and our value for otsbits is 11, we shall be needing 18 (not 17.45) OTS-pairs, combining to 18 x 2 x 24 is a 864 bytes long value for otssigs at each level.
Conclusion
I hope this post isn't too confusing, as it focused mostly on the binary format of the signatures. It is meant mostly as a reference. I'll be posting a post on the why and the how in a few weeks, as soon as the Python implementation of what is described here is fully implemented for both signing and validation.
Will this be the last and final version of the binary signature format specification for coinZdense? Well, I don't know, but it's most definetely comming closer to something ready to be frozen. Your input though is highly welcome and if your input leads to a need to change things around, so be it. It's about creating something robust and flexible, and when it's frozen it's frozen, so rushing a freeze isn't likely a good idea just yet.
Again, if you think I'm doing good work with this slow-moving unfunded project, consider upvoting my mosts, paying a visit to my tippingjar page or joining me on discord