Proof-of-Work on Waves


#1

I’ve created a smart account for modeling Proof-of-Work on Waves: 3P3tLiDmaAiTZD7sRA3djWqYazpoNcg6drT

hugo1

You can transfer Hugo asset from this account to your account (1 coin at once), but only if the TransferTransaction’s id starts with N zero bits.

N is the current difficulty, which is calculated depending on how the account balance has changed over the last 15 blocks (i.e., how many coins were “mined”).

Hugo is a sponsored asset, so after receiving this asset you can use it to pay fees when sending TransferTransactions.


#2

Account’s State (Data)

The smart account’s state contains:

  1. N - current computational difficulty (number of leading zero bits that TransferTransaction id must have).
  2. lastUpdateHeight - the blockchain height at which N was last updated.
  3. lastUpdateBalance - the account’s balance of Hugo at the time when N was last updated.

#3

Account script

The account script only allows a TransferTransaction if the following requirements are met:

  1. The transaction id starts with at least N zero bits.
  2. The transferred amount equals 1 Hugo.
  3. The fee is exactly 0.005 Waves.
  4. N was updated during the last 15 blocks.

The account script allows updating N via sending DataTransactions with exactly three entries (N, lastUpdateHeight, lastUpdateBalance):

  1. N must be calculated by the rules defined in the smart contract.
  2. lastUpdateHeight must be equal to the current blockchain height.
  3. balanceOnLastUpdate must be equal to the current balance of Hugo on the account.
  4. This update can only be done 15 blocks after the last update.

#4

Script code

# Proof-of-Work account
let sender = tx.sender
let asset = base58'BXSAEa9jm9Qrkmn2XPqqKBkukZoBkJ8YpQ9EZywjdnnx'

let difficulty = extract(getInteger(sender, "difficulty"))
let lastUpdateHeight = extract(getInteger(sender, "lastUpdateHeight"))
let lastUpdateBalance = extract(getInteger(sender, "lastUpdateBalance"))
let currentBalance = assetBalance(sender, asset)

let validityPeriod = 15
let transferAmount = 1
let miningRate = 100

let minDifficulty = 20
let maxDifficulty = 64

match tx {
    case tx : TransferTransaction =>
        let byte0LeadingZeros = extract(getInteger(sender, toBase58String(take(tx.id, 1))))
        let byte1LeadingZeros = extract(getInteger(sender, toBase58String(takeRight(take(tx.id, 2), 1))))
        let byte2LeadingZeros = extract(getInteger(sender, toBase58String(takeRight(take(tx.id, 3), 1))))
        let byte3LeadingZeros = extract(getInteger(sender, toBase58String(takeRight(take(tx.id, 4), 1))))
        let byte4LeadingZeros = extract(getInteger(sender, toBase58String(takeRight(take(tx.id, 5), 1))))
        let byte5LeadingZeros = extract(getInteger(sender, toBase58String(takeRight(take(tx.id, 6), 1))))
        let byte6LeadingZeros = extract(getInteger(sender, toBase58String(takeRight(take(tx.id, 7), 1))))
        let byte7LeadingZeros = extract(getInteger(sender, toBase58String(takeRight(take(tx.id, 8), 1))))

        let firstZeroBits = if byte0LeadingZeros != 8 then byte0LeadingZeros else ( 8 +
                            if byte1LeadingZeros != 8 then byte1LeadingZeros else ( 8 +
                            if byte2LeadingZeros != 8 then byte2LeadingZeros else ( 8 +
                            if byte3LeadingZeros != 8 then byte3LeadingZeros else ( 8 +
                            if byte4LeadingZeros != 8 then byte4LeadingZeros else ( 8 +
                            if byte5LeadingZeros != 8 then byte5LeadingZeros else ( 8 +
                            if byte6LeadingZeros != 8 then byte6LeadingZeros else ( 8 + 
                                                           byte7LeadingZeros)))))))

        height - lastUpdateHeight < validityPeriod
        && firstZeroBits >= difficulty
        && tx.amount == transferAmount && tx.assetId == asset
        && tx.fee == 500000 && !isDefined(tx.feeAssetId)
    case tx : DataTransaction =>
        let delta = if lastUpdateBalance - currentBalance > 0 then lastUpdateBalance - currentBalance else 1
        let sgn = if (delta < miningRate) then -1 else 1
        let k = if (delta < miningRate) then miningRate/delta else delta/miningRate
        let log2 = if (k < 2) then 0 else (
                   if (k < 4) then 1 else (
                   if (k < 8) then 2 else (
                   if (k < 16) then 3 else (
                   if (k < 32) then 4 else (
                   if (k < 64) then 5 else (
                   if (k < 128) then 6 else (
                   if (k < 256) then 7 else (
                   if (k < 512) then 8 else (
                   if (k < 1024) then 9 else (10))))))))))
            
        let newDifficulty0 = difficulty + sgn * log2
        let newDifficulty = if (newDifficulty0 < minDifficulty) then minDifficulty
                   else (if (newDifficulty0 > maxDifficulty) then maxDifficulty else newDifficulty0)
        
        height - lastUpdateHeight >= validityPeriod
        && size(tx.data) == 3
        && extract(getInteger(tx.data, "difficulty")) == newDifficulty
        && extract(getInteger(tx.data, "lastUpdateHeight")) == height
        && extract(getInteger(tx.data, "lastUpdateBalance")) == currentBalance
        && tx.fee == 500000
    case _ => sigVerify(tx.bodyBytes, tx.proofs[0], tx.senderPublicKey)
}

#5

How to mine
Here’s a sample program written in C# that “mines” the asset, that is, generates transactions until the transaction satisfies all specified conditions, and then sends the transaction to the blockchain. If N was updated earlier than 15 blocks back, then the program sends a DataTransaction that updates N.

using System;
using System.Collections.Generic;
using WavesCS;

namespace Miner
{
    class MainClass
    {
        static Node node = new Node(Node.MainNetChainId);
        static Asset asset = node.GetAsset("BXSAEa9jm9Qrkmn2XPqqKBkukZoBkJ8YpQ9EZywjdnnx");

        static byte[] senderPublicKey = node.GetTransactionById(asset.Id).SenderPublicKey;
        static string sender = AddressEncoding.GetAddressFromPublicKey(senderPublicKey, node.ChainId);
        static string recipient = ""; // put your address here

        static long lastUpdateHeight = (long)node.GetAddressData(sender)["lastUpdateHeight"];
        static long difficulty = (long)node.GetAddressData(sender)["difficulty"];

        static int validityPeriod = 15;
        static int transferAmount = 1;
        static int miningRate = 100;

        static int minDifficulty = 20;
        static int maxDifficulty = 64;

        public static void UpdateDifficulty()
        {
            try
            {
                var currentHeight = (long)node.GetHeight();
                lastUpdateHeight = (long)node.GetAddressData(sender)["lastUpdateHeight"];

                if (currentHeight - lastUpdateHeight >= validityPeriod)
                {
                    var currentBalance = asset.AmountToLong(node.GetBalance(sender, asset));
                    var lastUpdateBalance = (long)node.GetAddressData(sender)["lastUpdateBalance"];

                    difficulty = (long)node.GetAddressData(sender)["difficulty"];

                    var delta = lastUpdateBalance - currentBalance > 0 ? lastUpdateBalance - currentBalance : 1;
                    var sgn = (delta < miningRate) ? -1 : 1;
                    var k = (delta < miningRate) ? miningRate / delta : delta / miningRate;
                    var log2 =  Math.Min(10, (long)Math.Floor(Math.Log(k, 2)));
                    difficulty = difficulty + sgn * log2;
                    difficulty = Math.Max(minDifficulty, difficulty);
                    difficulty = Math.Min(maxDifficulty, difficulty);

                    var data = new Dictionary<string, object>
                    {
                        { "difficulty", difficulty },
                        { "lastUpdateHeight", currentHeight },
                        { "lastUpdateBalance", currentBalance }
                    };
                    var tx = new DataTransaction(node.ChainId, senderPublicKey, data, 0.005m);

                    Http.Tracing = true;
                    node.Broadcast(tx.GetJsonWithSignature());

                    Console.WriteLine($"Mining difficulty was updated");
                    Console.WriteLine($"Current mining difficulty: {difficulty}");
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }
            finally
            {
                Http.Tracing = false;
            }
        }

        public static void Main(string[] args)
        {
            Http.Tracing = false;
            Console.WriteLine($"Current mining difficulty: {difficulty}");
            
            for (var iterations = 0; ; iterations++)
            {
                if (iterations % 1000000 == 0)
                {
                    UpdateDifficulty();
                }

                var tx = new TransferTransaction
                (
                    chainId: node.ChainId,
                    senderPublicKey: senderPublicKey,
                    recipient: recipient,
                    asset: asset,
                    amount: asset.LongToAmount(transferAmount),
                    fee: 0.005m
                );

                var id = tx.GenerateBinaryId();
                
                var leadingZeros = 0;
                for (int i = 0; i < 32; i++)
                {
                    if (id[i] == 0)
                        leadingZeros += 8;
                    else
                    {
                        leadingZeros += 7 - (int)Math.Floor(Math.Log(id[i], 2));
                        break;
                    }
                }

                if (leadingZeros >= difficulty)
                {
                    Console.Write("Generated transaction id: " + id.ToBase58());

                    try
                    {
                        Http.Tracing = true;
                        node.Broadcast(tx);
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine(e.Message);
                    }
                    finally
                    {
                        Http.Tracing = false;
                    }
                }
            }
        }
    }
}

#6

Interesting! Are you in any way associated with 5th Planet Games?


#7

No, I’m not
I just like this character :slightly_smiling_face:


#8

what is the 5th planet game?


#9

5th Planet Games, formerly known as Hugo Games, is the company that holds the commercial rights (at least within gaming) to the Hugo Troll


#11

Firstly, you should provide a mechanism to update the difficulty bit by bit, +8 bit step is too hard.

Do not think it is even possible at 5-6 bytes, because there is no room for a salt to randomize transaction id.

So for example you can verify per byte by your 256 predefined key values to get zero bits in id, there key is byte in base58 and value is number of zero bits in byte, json:

{
    "1": 8,
    "2": 7,
    "3": 6,
    "4": 6,
    "5": 5,
    "6": 5,
    "7": 5,
    "8": 5,
    "9": 4,
    "A": 4,
    "B": 4,
    "C": 4,
    "D": 4,
    "E": 4,
    "F": 4,
    "G": 4,
    "H": 3,
    "J": 3,
    "K": 3,
    "L": 3,
    "M": 3,
    "N": 3,
    "P": 3,
    "Q": 3,
    "R": 3,
    "S": 3,
    "T": 3,
    "U": 3,
    "V": 3,
    "W": 3,
    "X": 3,
    "Y": 3,
    "Z": 2,
    "a": 2,
    "b": 2,
    "c": 2,
    "d": 2,
    "e": 2,
    "f": 2,
    "g": 2,
    "h": 2,
    "i": 2,
    "j": 2,
    "k": 2,
    "m": 2,
    "n": 2,
    "o": 2,
    "p": 2,
    "q": 2,
    "r": 2,
    "s": 2,
    "t": 2,
    "u": 2,
    "v": 2,
    "w": 2,
    "x": 2,
    "y": 2,
    "z": 2,
    "21": 2,
    "22": 2,
    "23": 2,
    "24": 2,
    "25": 2,
    "26": 2,
    "27": 1,
    "28": 1,
    "29": 1,
    "2A": 1,
    "2B": 1,
    "2C": 1,
    "2D": 1,
    "2E": 1,
    "2F": 1,
    "2G": 1,
    "2H": 1,
    "2J": 1,
    "2K": 1,
    "2L": 1,
    "2M": 1,
    "2N": 1,
    "2P": 1,
    "2Q": 1,
    "2R": 1,
    "2S": 1,
    "2T": 1,
    "2U": 1,
    "2V": 1,
    "2W": 1,
    "2X": 1,
    "2Y": 1,
    "2Z": 1,
    "2a": 1,
    "2b": 1,
    "2c": 1,
    "2d": 1,
    "2e": 1,
    "2f": 1,
    "2g": 1,
    "2h": 1,
    "2i": 1,
    "2j": 1,
    "2k": 1,
    "2m": 1,
    "2n": 1,
    "2o": 1,
    "2p": 1,
    "2q": 1,
    "2r": 1,
    "2s": 1,
    "2t": 1,
    "2u": 1,
    "2v": 1,
    "2w": 1,
    "2x": 1,
    "2y": 1,
    "2z": 1,
    "31": 1,
    "32": 1,
    "33": 1,
    "34": 1,
    "35": 1,
    "36": 1,
    "37": 1,
    "38": 1,
    "39": 1,
    "3A": 1,
    "3B": 1,
    "3C": 1,
    "3D": 0,
    "3E": 0,
    "3F": 0,
    "3G": 0,
    "3H": 0,
    "3J": 0,
    "3K": 0,
    "3L": 0,
    "3M": 0,
    "3N": 0,
    "3P": 0,
    "3Q": 0,
    "3R": 0,
    "3S": 0,
    "3T": 0,
    "3U": 0,
    "3V": 0,
    "3W": 0,
    "3X": 0,
    "3Y": 0,
    "3Z": 0,
    "3a": 0,
    "3b": 0,
    "3c": 0,
    "3d": 0,
    "3e": 0,
    "3f": 0,
    "3g": 0,
    "3h": 0,
    "3i": 0,
    "3j": 0,
    "3k": 0,
    "3m": 0,
    "3n": 0,
    "3o": 0,
    "3p": 0,
    "3q": 0,
    "3r": 0,
    "3s": 0,
    "3t": 0,
    "3u": 0,
    "3v": 0,
    "3w": 0,
    "3x": 0,
    "3y": 0,
    "3z": 0,
    "41": 0,
    "42": 0,
    "43": 0,
    "44": 0,
    "45": 0,
    "46": 0,
    "47": 0,
    "48": 0,
    "49": 0,
    "4A": 0,
    "4B": 0,
    "4C": 0,
    "4D": 0,
    "4E": 0,
    "4F": 0,
    "4G": 0,
    "4H": 0,
    "4J": 0,
    "4K": 0,
    "4L": 0,
    "4M": 0,
    "4N": 0,
    "4P": 0,
    "4Q": 0,
    "4R": 0,
    "4S": 0,
    "4T": 0,
    "4U": 0,
    "4V": 0,
    "4W": 0,
    "4X": 0,
    "4Y": 0,
    "4Z": 0,
    "4a": 0,
    "4b": 0,
    "4c": 0,
    "4d": 0,
    "4e": 0,
    "4f": 0,
    "4g": 0,
    "4h": 0,
    "4i": 0,
    "4j": 0,
    "4k": 0,
    "4m": 0,
    "4n": 0,
    "4o": 0,
    "4p": 0,
    "4q": 0,
    "4r": 0,
    "4s": 0,
    "4t": 0,
    "4u": 0,
    "4v": 0,
    "4w": 0,
    "4x": 0,
    "4y": 0,
    "4z": 0,
    "51": 0,
    "52": 0,
    "53": 0,
    "54": 0,
    "55": 0,
    "56": 0,
    "57": 0,
    "58": 0,
    "59": 0,
    "5A": 0,
    "5B": 0,
    "5C": 0,
    "5D": 0,
    "5E": 0,
    "5F": 0,
    "5G": 0,
    "5H": 0,
    "5J": 0,
    "5K": 0,
    "5L": 0,
    "5M": 0,
    "5N": 0,
    "5P": 0
}

#12

Thanks for your help!
Smart contract and miner code were updated


#13

Nice, let’s see how high we can get now.

I bet on 42 bits.


#14

Looks like it’s just 26.


#15

Yeah, i was wrong, it turned out i calculate things in my mind in base58, so 5-6 symbols in a raw became 5-6 bytes for me but it is 4 bytes actually ~ 32 bits.

And i thought blake2b is much faster.


#16

Oops: Error while executing account-script: / by zero


#17

Hi! I can’t understand. Is it a game? Can I play it or it’s just a devs talk? :slight_smile:


#18

Why just not to use Base64? :slight_smile:


#19

Ok, i released my miner Huger: https://github.com/deemru/Huger

Just paste your address to config and run.

Use PHP >= 7.2 for reasonable results.


#20

Hi! Anyone can participate and get Hugo asset (just send a Transfer transaction with sender = my address, recipient = your address, asset = Hugo, amount = 1, id starting with difficulty bits).

You can use my miner, miner made by @deemru or make your own :slight_smile: