Hi
This site is about a new consensus protocol and blockchain. The consensus method is being called Proof of Rainbow.
This site should be viewed as a dev-diary. I intend to update the site as I make further progress.
The goal of this blockchain is to:
- Be a memecoin. My goal is not to attract "serious" applications.
- Use accounts model, not UTXO.
- Not utilize a pre-mine, pre-farm, "strategic reserve", etc.
- Instead, include a dev fee for advanced on-chain activity. If you are interacting with the chain, there may be a dev fee.
- This will not include basic transactions.
- Blocks are created every 10 seconds. The global time system will be utilized for synchronization.
- If all the clocks of the world stop working, we have more serious problems to worry about.
- Use a novel proof of space and work consensus algorithm, based on rainbow tables.
- Promote custom coins and NFTs to actual types of objects on chain. I do not believe a custom smart contract should be required for a custom coin or NFT. I am promoting these objects.
- Still allow scripting with some sort of "visual" interface, to allow average users to interpret the contents of any smart contract. I don't really know what this looks like yet, but I think some ergonomics are needed here.
Allow,Encourage, Effectively require "compression" (actually a time memory trade-off) on all files used for mining.
"Compression"
The last point may surprise anyone coming from a different proof of space-based blockchain. However, I feel there will always be some "cat and mouse" game where proof of space is fighting against some time-memory trade-off. To aliviate this cat and mouse game, we start our blockchain with nearly optimal time-memory tradeoffs: the Rainbow Table.
This has a lot of side-effects, which will be covered in this book. Of particular note:
- It doesn't seem to increase power usage that much (2x over just disks), if you are running it "optimally" (most lookups per KWh). You can absolutely use more power if you want, though!
- It places miners in a stable platform, where on-disk formats are not shifting regularly. We get to live in very well-established theory. We do not need to invent new math.
- We do not need "timelords". They can stay in Doctor Who.
- It allows ASICs to work, although to work most optimally, they must still have a bunch of disks attached to them. Without disks, they lose very badly to a single semi-modern CPU.
- ASICs exist for the hashing algorithm used today, so we don't need to theorize about their impact. Later in the book, we will look at what the best ASIC miner would look like, if one wanted to build one.
- You get to have millions of something (rainbows)!
I am in the very early stages of development. Expect changes to be made to this document over time, as I refine the design.
Terminology
If you are coming from a different PoST chain, please note I have renamed the following items.
- A plot is now a rainbow.
- These both contain the on-disk data needed to find proofs.
- Plots exist in their own files, whereas I recommend Rainbows be stored as a set of files, with all the data mixed together, for more efficient lookups.
- Farming is now mining (again).
- Miners (Farmers) look for proofs which allow them to create a block.
- This work allows the blockchain to get bigger, and process data.
- The miners do this to get block rewards, or payment from the blockchain itself to keep the network alive.
- Plotting is now storming.
- This process was renamed because storms create rainbows.
- It also draws an accurate picture for what is going on inside your computer.
- Harvesters are Leprechauns.
- These find proofs inside the rainbow.
- Timelords do not exist.
- No VDFs either.
Building Blocks
We must start somewhere. In this chapter, I go over the basic data structures and functions used in this blockchain.
The hashing algorithm used is blake3
.
Throughout this book, we will need to use P
-bit integers. I have found that P
should
be somewhere around 32
, so I'm going to stick with that for simplicity, and not refer to P
all the time. We'll just say u32
for an unsigned 32-bit integer, and call it a day.
It's worth noting that from a theoretical perspective, P
could change as time goes on.
The Incredible Hunk
A hunk is a very simple data structure:
#![allow(unused)] fn main() { struct Hunk { data: [u8; 32] } }
It represents 32 bytes of data. This has multiple uses:
- It can store a hash.
- It can store a private key.
- It can store a public key.
- It can store a rainbow id.
- Probably other things.
However, there are some common operations that are often used.
Hash
The first operation we will look at is hashing an integer. A lookup table would be handy, but does take 32 * 2^32 bytes, or 128 GiB just for this one table.
#![allow(unused)] fn main() { impl Hunk { pub fn hash(value: u32) -> Hunk { Hunk { data: blake3::hash(value.to_be_bytes()).into() } } } }
Hunk Hash
One such operation is to hash the Hunk
. We call this the hunk_hash
,
which is simply the blake3
of the Hunk
.
#![allow(unused)] fn main() { impl Hunk { pub fn hunk_hash(&self) -> Hunk { Hunk { data: blake3::hash(self.data).into() ) } } }
Combine
Another operation is to combine two hashes together, to really mix up some bits.
#![allow(unused)] fn main() { impl Hunk { pub fn combine(&self, other: Hunk) -> Hunk { (self ^ other).hunk_hash() } } }
Make It Rain
And, finally, the star of the show. This is the actual hashing algorithm used by
our rainbow tables. We call it raindrop
. This will be used for building
our chains in the rainbow table, if you happen to already know what that means.
#![allow(unused)] fn main() { impl Hunk { pub fn raindrop(&self, value: u32) -> Hunk { self.combine(Hunk::hash(value)) } } }
Getting back to a (different) challenge
We will want to take a Hunk
, and turn it into a 32-bit value. A sort
of "reverse-hash". But, not a reverse hash. We want to generate a new, unique
32-bit value from a hash. This will be the key to making rainbow tables work.
#![allow(unused)] fn main() { impl Hunk { pub fn evaporate(&self) -> u32 { u32::from_be_bytes([self.data[0], self.data[1], self.data[2], self.data[3]]) } } }
Making a Rainbow
Now that we know about Hunk
s, and most importantly, the raindrop
/evaporate
functions,
we can actually build some Rainbow
s.
When a proof is found, the proof must come with some parameters used to create the
Rainbow
. Without the parameters, the proof is invalid (in fact, it can't be validated
at all!)
These parameters are:
#![allow(unused)] fn main() { pub struct RainbowParams { /// A unique, random value. rainbow_id: Hunk, /// Your public key. miner: Hunk, /// The developers public key. dev: Hunk, /// dev_fee/256 * block rewards goes to developer. You get the rest. dev_fee: u8, /// The compression level, 1-32. z: u8, ... }
Note: You'll notice a dev/dev_fee parameter. The reference implemnation will not require a dev fee for rainbow tables. However, I want to make sure other developers can cleanly implement a dev fee. Dev fees are very common in the crypto world, and it would be nice if they weren't random, and both the dev and miner got what they agreed upon in each found block. By putting the fee here, it guarantees that this
Rainbow
will always use this dev fee structure.
However, in addition to those parameters, we must also compute a value called fast_hash
.
This is technically not required, but speeds up generating the rainbow table significantly.
#![allow(unused)] fn main() { ... fast_hash: Hunk, } impl RainbowParams { /// Combine all the parameters into one hash. fn compute_fast_hash(&mut self) { self.fast_hash = Hunk::combine( Hunk::combine( self.rainbow_id, self.miner, ), Hunk::combine( self.dev.combine_hash(self.dev_fee), Hunk::hash(self.z as u32), ) ) } /// Use the raindrop function of fast_hash as our hashing /// algorithm for _this_ rainbow table. fn raindrop(&self, value: u32) -> Hunk { self.fast_hash.raindrop(value) } } }
The Rainbow Chain (not the chain in Blockchain)
We will first create a bunch of chains. A chain is a starting value: start: u32
, and a final value, end: u32
.
We will start with a bunch of arbitrary numbers for start
. To find end
, we will take the raindrop
of start (which
will give us a Hunk
), evaporate
that Hunk
(to get a u32
), then take the raindrop
of that u32
... and so on, calling
raindrop
then evaporate
then raindrop
... until the u32
part matches the terminating condition.
The terminating condition will be, when the first self.z
bits of u32
are all 0
s, that u32
is the end
.
#![allow(unused)] fn main() { impl RainbowParams { pub fn terminator(&self, value: u32) -> bool { value.leading_zeros() >= self.z } } }
This computation will look like the following:
start -> a = raindrop(start) -> b = evaporate(a) => c = raindrop(b) -> d = evaporate(c) -> e = raindrop(d) -> f = evaporate(e)
until the final value has self.z
0's at the start. We will then throw out all the middle-bits, because we can regenerate
them at any time with just the start
value.
Saving the rainbow for later
We will keep a separate sqlite database for the RainbowParams
structure. Each RainbowParams
will be given a sequential,
incrementing id starting at 0. This is important for some space-saving tricks.
Once the RainbowParams
are saved, and the ID is correct, all we actually need to do is append the start
value in a file
called end
.
The index in the file will bring us back to the current RainbowParams
, and the end
is the key we want to look up by.
This end
file will now contain a start
value for all rainbow tables. When we read from it,
we will get many results, which is many chances to win! And, that read is now sequential, rather
than random, so we can read even more!
If a particular Rainbow
does not have an end
, we place a dummy value in the file. We can
filter it out later. It doesn't matter what the dummy value is.
Challenge and Proof
Later on, we will be given a Hunk
called challenge
. We will define challenge_id = challenge.evaporate()
.
The terminator again
Next, we will need to successively hash challenge_id
with Hunk::hash()
and Hunk::evaporate()
until it matches
our terminating condition. We'll call the final value end
.
If it never matches our terminating condition (IE, it loops), we will take the hashes of all items in the loop, xor
them together, and that becomes the new challenge
.
end = end
We also have a file called end
. We can read all rainbow tables that have a final hash that matches our end
. Note
that this organization was useful, because this is a sequential read to find all rainbow tables.
start
Now, we have a bunch of start
values for a bunch of rainbow tables. For each hash in each chain, find hash ^ challenge
and keep the smallest one (as a Hunk
read as a big endian integer).
Proof
The start
value is the proof, along with all RainbowParams
.
Verify
Others will need to verify that you actually had a rainbow table.
They will take challenge and convert it to a end
the same way you did. (2^z hashes)
They will take the start
and turn it into end
the same way you did. (2^z hashes)
And, they will find the smallest proof at the same time.