As a proof-of-stake network, 33% of the stake can censor the network, therefore the minimum number of validators with stake adding up to 33% could collude to censor the network. The Stake-o-Matic is a program that automatically delegates stake according to predefined rules. The idea is that if validators write their own versions (according to their preferences), and staking via their Stake-o-Matic is incentivised, the global stake will decentralise across many validators. I have no idea if that will actually happen, but I’ve been playing with the Stake-o-Matic reference implementation.

Running the Reference Implementation

Clone and build the Solana Stake-o-Matic from the github:

NB: If you get u64::MAX errors, your Rust is out of date; rustup update stable

Check the binary was built:

Run again with the -h flag to see all the options.

We can run the program on testnet, we need to set up payment keypair and make a stake account. (Ask in discord for test tokens).

First I made a keypair for paying + funded the account with 10,000 Sol:

Second I made a stake account and put 8,000 Sol into it:

We can see that the stake authority is both the Staking and Withdrawing Authority on the stake account.

Third we run the program. I’ve formatted this for readability, but it is all one line:

Obviously we can’t use such large amounts as the Foundation use, so we override the defaults on the command line. 1 Sol “baseline” for a non-delinquent validator, plus 5 Sol “bonus” for a quality validator. When I ran the above command it found 340 quality block producers and 13 delinquents, adding on account ‘rent’ it split out around 2,100 Sol from the stake account.

That -confirm is what makes it actually send the transactions to a node, you can run without that as a dry run. FYI it’s pulling other things it uses, like the RPC endpoint from your default config file, if you don’t have that setup, specify the arguments on the command line. Also note that if over 20% of the testnet validators fail the bonus block production requirement (default 75% of blocks), the program won’t change bonuses; I’ve set the quality threshold at 10%, so almost all were counted as quality producers.

If the program fails (which it seems to for various reasons), just run it again, it won’t recreate accounts or anything like that. I had to run it 4 times, when it does nothing anymore, you’re done.

You can look at all the stake splits by piping the solana stakes output to a file:

Open the file in a text editor and search for your stake_authority public key:

Obviously there are very many accounts that we have splits for, this is just one example — it is a 5 Sol bonus account, that has been delegated to a Validator’s vote account, we can get the Validator’s identity like this:

And we can look up that Validator’s details in various places, for example here we learn the name (“IZO”), website and IP of the Validator:

Anyway, the stake splits will not change unless we run the program again in the next epoch.

Overview of the Reference Implementation

The program does a bunch of things, such as making a lists of quality and poor block producers, making large numbers of transactions, checking the confirmations. But the heart of what is happening can be found in the main() function. For every vote_account on the validator list, two addresses are determined, a baseline and a bonus. These are derived deterministically relative to the authorized_staker plus a seed. This means a particular authorized_staker and vote_account, will always map to the baseline and bonus addresses (seed differentiates baseline from bonus). Here is the code for the baseline address:

If the account doesn’t exist, a transaction is made to create the account using split_with_seed()

This will create an undelegated stake account at the baseline_stake_address, with the amount transferred from the source_stake_address. The node will enforce the derived address. The authorized_staker will be both the Withdraw and Stake Authority on the new account:

What is going on programatically is basically the same as doing this by hand.

The same thing happens with the bonus account. To be clear: both accounts are created once only, no other accounts are needed per validator; after this the stake accounts are delegated and undelegated as often as the script runs, but those accounts don’t change.

At this point the transactions have not be sent yet, they are saved in the create_stake_transactions array. The next step is delegation. If the validator is not delinquent it should get the baseline stake account delegated to it (if it isn’t already):

Alternatively, if the validator is delinquent, the stake is undelegated:

The same procedure happens with the bonus stake account. Then the create_stake_transactions array of transactions is sent to the node, followed by the delegate_stake_transactions array.

And that’s pretty much it. Each validator on the fixed list has two accounts with stake on them (baseline and bonus) all the time, the accounts are just delegated or undelegated (deactivated) depending on performance.

Tweaking the Quality Criteria

An easy way to tweak the program is not to change it at all but just pass in a modified list, the -cluster argument, to which we passed testnet uses a pre-defined list in, but we can also pass in a customised yaml list.

I’ve decided that the most important thing for decentralisation is a broad range of datacentres, so I won’t be delegating to anyone in a datacenter with more than 3 Solana nodes in it. To do this I will determine the ASN of every node, then count them up.

First we get the node IP addresses:

Then we get the ASN:

I’ve written a little script that works it out for each node and counts how many nodes are at which location.

The script also lets us discard nodes at locations with more than <max> nodes. I’m going with a max of 3 at any location.

No more than 3 nodes at any location

Finally, we can tell the script to just output the nodeIDs and pipe that into a file, this is my validator list. I will only be delegating to these validators.

Let’s set the baseline amount to 100 Sol and the Bonus to 900 Sol… so the total stake we need is 37,000 SOL.

I create a stake authority with 37,000 + 10 Sol to pay the fees:

And then the stake account:

First I do a dry run:

This reports 68 stake accounts will be split out — if all the validators got a baseline or bonus it would be 74, but a few have missed out on one or both.

I made a small change to the reference implementation code to avoid the bonus check and recompiled rust:

I run it for real:

3,000 Sol is left in the parent stake account:

I do a little check, we expect 68 + 1 (the parent):

Managing stake splits is difficult right now. To work out why exactly there is 3,000 unallocated:

So 3 validators missed out on the baseline and 3 missed out on the bonus. Stake is delegated to a validator’s vote account not the nodeID, we can find our validators like so:

So we can see the same 3 validators were delinquent, and so got neither the base or the bonus; all the numbers add up.

Don’t forget to add to the crontab, I run mine once a day:

Thoughts on the Reference Implementation

One thing that I noticed was that there is no withdrawing and redistributing of the undelegated accounts.

Each account on the list (if it got the bonus even once) forever has that stake split allocated to it. If the validators performance is good, it is delegated, otherwise not. This is fine for a Foundation who don’t care about profits, but what about about a regular holder? Undelegated stake does not earn rewards, you’d want your stake delegated all the time, so that you earn the maximum.

This would become especially problematic if you used a quality metric that selected a validator set that changed frequently, you could end up with a lot of undelegated stake.

Probably a really good Stake-o-Matic would dynamically redistribute stake splits, i.e. it would withdraw from undelegated stake-splits and re-stake those funds elsewhere. But that would be a quite different program from the reference implementation.