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:

  1. Be a memecoin. My goal is not to attract "serious" applications.
  2. Use accounts model, not UTXO.
  3. Not utilize a pre-mine, pre-farm, "strategic reserve", etc.
  4. 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.
  1. 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.
  1. Use a novel proof of space and work consensus algorithm, based on rainbow tables.
  2. 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.
  3. 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.
  4. 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:

  1. It can store a hash.
  2. It can store a private key.
  3. It can store a public key.
  4. It can store a rainbow id.
  5. 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 Hunks, and most importantly, the raindrop/evaporate functions, we can actually build some Rainbows.

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 0s, 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.