How I Eternalized OLGA on Dogecoin

OLGA on Dogeparty has all associated info in a json file. The only onchain data is a URL:

olganft.com/i/olgaGzrKnjmRW6jrCoNo.json

A simple link, yet sufficient to make the token perfectly immutable. Even if the .com domain expires, the NFT can be recreated by anyone with a copy of the associated files.

Background & Update

In 2014 I minted 1001 OLGA tokens. The non-fungible one on Bitcoin, and a thousand more on Dogecoin.

The onchain data on Dogecoin from 2014 is simply the line “Твои небесные черты” – Your heavenly features. It’s a quote from Aleksander Puskin’s famous love poem “To ***“.

Exactly eight years later I changed the description to “I remember a marvelous moment” – a translation of the poem’s first line. I also added a suitable image; a high resolution version of OLGA’s original image with a colorful filter.

How To Verify The JSON

Open the json link.

Right-click, save.

Google sha256 file online. Choose any of the tools it suggests, e.g. “SHA256 File Checksum“.

The json file’s checksum will read exactly a2581a1b3aca9e39915ba8eb0a8368c87632d49a0ad3766b73c3fd90180c5786.

Now copy this string and google hex to base64. Click on any of the results, for example “Base64 Guru“.

Paste the hex string and it will convert to olgaGzrKnjmRW6jrCoNoyHYy1JoK03Zrc8P9kBgMV4Y=.

The first twenty characters match the token metadata; olganft.com/i/olgaGzrKnjmRW6jrCoNo.json.

This is the proof! There will never be another file with a matching hash.

Technically, this is a sha256 hash, truncated at 120 bits, and encoded as base64.

How To Verify Images

The images have their hashes listed in the json file.

Save the images, open a sha256 file hashing tool, and verify that they all match up.

Since the json hash is onchain, thus immutable, the images are too.

Why Truncated Base64

Dogeparty (like Counterparty) allows virtually any amount of metadata. That’s how I was able to, in 2015, to put OLGA’s image inside a Bitcoin transaction.

This time, however, I wanted to push the envelope in the opposite direction – by making an eternal NFT with a minimal blockchain footprint.

I also did it as a reaction to IPFS and Arweave URLs which are over-engineered, wasteful and quite frankly – plain ugly.

From a very technical level I liked to have the message fit op_return. In plain English, the description should be no longer than 52 characters.

A full hash would take up 64 characters, at least with hex encoding. With base64, the same hash is compressed to 44 characters. Still too much to squeeze inside a 52 char URL!

Thankfully it’s normal practice to shorten a hash when needed. It should not be too short though.

How short would be too short? I got my clues from no other than Satoshi. Bitcoin is essentially a game with the objective of finding a hash with as many trailing zeros as possible. The best hash so far is:

000000000000000000000003681c2df35533c9578fb6aace040b0dfe0d446413

That’s 23 zeros, equivalent to 92 bits. Theoretically, if you were to switch the entire world’s mining infrastructure to try and collide with OLGA’s file hash, the closest you could expect to get is 92 bits in ten years. (I oversimplified a bit, but you see the picture).

The twenty base64 characters in olgaGzrKnjmRW6jrCoNo correspond to 30 hex characters, or 120 bits. That’s 28 bits more than the miners have achieved, i.e. 2^28 or 26,843,545 times more difficult.

Eternal NFT

Although the images are not onchain, this is not an issue for multi-token 1/Ns like OLGA. Every collector has an interest in keeping a backup of the files, and OLGA is preserved as long as at least one copy is available.

With dozens of collectors holding a token, we end up with ultra-strong redundancy. I do not have the slightest fear of the NFT ever getting lost.

Also, if the domain olganft.com expires, gets stolen or otherwise stops functioning, the hash is the real pointer to the token’s contents. The URL should be thought of as a temporary seed.

Python and JavaScript Code

I enjoyed running my computers overnight and wake up to the nice olgaGzrKnjmRW6jrCoNo hash. This is essentially the same as Satoshi’s mining game, only with the twist of beatifying an NFT hash.

I’ve prepared code in case you want to do something similar.

The Python script is way faster than the JavaScript version. However, if you are unfamiliar with Py, the JS version can be copy-pasted into a text editor and saved as a .html file. Modify the prefix and json template, and open it in a browser. No programming needed.

You need to pay attention to prefix length. With Python you can use 3-4 letters and get a result quickly. Five or six is okay if you’re willing to wait. Anything longer would require some serious optimizations, like GPU programming.

The speed on my system:

Py = 310,000 per sec

JS = 4500 per sec

PatternPYPY (ci)JSJS (ci)
L0 sec0 sec0 sec0 sec
LO0 sec0 sec1 sec0 sec
LOV0 sec0 sec1 min7 sec
LOVE1 min3 sec1 hr4 min
LOVEO1 hr2 min3 day2 hr
LOVEOL2 day1 hr6 mnt3 day
LOVEOLG6 mnt1 day30 yr3 mnt
LOVEOLGA30 yr40 day2k yr8 yr

After you’ve generated a json file you should check that the file’s hash is as expected. For example, the JS version gives a wrong output in case of non-ascii characters in the template. On Python I had to specify “newline” for it to work correctly. I cannot guarantee that there won’t be other issues on your system.

Check the json hash again after you’ve uploaded it. On FileZilla I had to set Transfer type to Binary to get the correct result.

An additional note; base64 contains all 62 alphanumericals, “+” and “/”. The last two characters are not suitable for file names. One option would be to use base64url, an identical encoding except the last two characters are “-” and “_”. Not to cause further complications, I instead made the script ignore results containing these two signs. This roughly doubled the computing time.

import hashlib
import codecs
import time

pattern = 'olga'
filename_len = 20
json_template = """{
-->"asset": "OLGA",
-->"description": "I remember a marvelous moment",
-->"name": "OLGA (Dogeparty)",
-->"nonce": "{n}",
-->"images": [{
-->-->"type": "icon",
-->-->"size": "48x48",
-->-->"data": "https://olganft.com/img/olga_xdp_48.png",
-->-->"hash": "a3855606859f5da86c628b4f5c45aad7eb8f8eb46fbc7036f7943852264981ed"
-->},{
-->-->"type": "icon",
-->-->"size": "128x128",
-->-->"data": "https://olganft.com/img/olga_xdp_128.png",
-->-->"hash": "234278eac38d2d8c6ed7f88c8ca57e157293fc23bc77a1a7cd89ac89f96f7de9"
-->},{
-->-->"type": "standard",
-->-->"data": "https://olganft.com/img/olga_xdp_850.png",
-->-->"hash": "788c0e344b6c3b2fafe883d7c1ad689a8f6c562f9c2f9d08b917e3958792ffa9"
-->},{
-->-->"type": "large",
-->-->"data": "https://olganft.com/img/olga_xdp_1700.png",
-->-->"hash": "27cffe8a48a15f2883e0fce350695a648836d5ec1e5f5b9439854ced4e751d98"
-->}]
}"""
json_template = json_template.replace('-->', '    ')

start_time = time.time()
n = 0
while (True):
  n += 1
  json = json_template.replace('{n}', str(n), 1)
  sha = hashlib.sha256(json.encode('utf8'))
  sha_hex = sha.hexdigest()
  sha_b64 = codecs.encode(codecs.decode(sha_hex, 'hex'), 'base64').decode()
  if sha_b64[:len(pattern)] == pattern:
    filename = sha_b64[:filename_len] + '.json'
    if '+' not in filename and '/' not in filename:
      exec_time = time.time() - start_time
      speed = n / exec_time
      print('Time:       ' + str(int(exec_time)) + ' sec')
      print('Iterations: ' + str(int(n)))
      print('Speed:      ' + str(int(speed)) + ' iterations/sec')
      print(json)
      print(sha_hex)
      print(sha_b64)
      print(filename)
      with open(filename, 'w+', newline='\n', encoding='utf-8') as f:
        f.writelines(json)
      break
<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <script>
      
      let pattern = 'olga'
      let filename_len = 20
      let json_template = `{
-->"asset": "OLGA",
-->"description": "I remember a marvelous moment",
-->"name": "OLGA (Dogeparty)",
-->"nonce": "{n}",
-->"images": [{
-->-->"type": "icon",
-->-->"size": "48x48",
-->-->"data": "https://olganft.com/img/olga_xdp_48.png",
-->-->"hash": "a3855606859f5da86c628b4f5c45aad7eb8f8eb46fbc7036f7943852264981ed"
-->},{
-->-->"type": "icon",
-->-->"size": "128x128",
-->-->"data": "https://olganft.com/img/olga_xdp_128.png",
-->-->"hash": "234278eac38d2d8c6ed7f88c8ca57e157293fc23bc77a1a7cd89ac89f96f7de9"
-->},{
-->-->"type": "standard",
-->-->"data": "https://olganft.com/img/olga_xdp_850.png",
-->-->"hash": "788c0e344b6c3b2fafe883d7c1ad689a8f6c562f9c2f9d08b917e3958792ffa9"
-->},{
-->-->"type": "large",
-->-->"data": "https://olganft.com/img/olga_xdp_1700.png",
-->-->"hash": "27cffe8a48a15f2883e0fce350695a648836d5ec1e5f5b9439854ced4e751d98"
-->}]
}`; //NOTE: If template contains non-ascii char, a wrong hash is returned!
      json_template = json_template.replaceAll('-->','    ');

      function generate_json() {
        start(); //measure execution time
        let n = 0;
        while (true) {
          n += 1;
          let json = json_template.replace('{n}', n);
          let sha_hex = forge_sha256(json);
          let sha_b64 = hexToBase64(sha_hex);
          if (sha_b64.substring(0,pattern.length) == pattern) {
            let filename = sha_b64.substring(0, filename_len) + '.json';
            if (filename.includes('+') == false && filename.includes('/') == false) {
              let exec_time = end();
              let speed = Math.round(n / exec_time);
              let out = '';
              out += 'Time       ' + exec_time + ' sec<br>';
              out += 'Iterations ' + n + '<br>';
              out += 'Speed      ' + speed + ' iterations/sec<br>';
              out += json + '<br>';
              out += sha_hex + '<br>';
              out += sha_b64 + '<br>';
              out += filename + '<br>';
              document.getElementById('output').innerHTML = out;
              save_file(filename, json);
              break;
            }
          }
        }
      }
    </script>

  </head>
  <body onload="generate_json()">

    <pre id="output">Loading..</pre>

    <script>
      //Hex to Base64
      function hexToBase64(str) {
        return btoa(String.fromCharCode.apply(null,
                                              str.replace(/\r|\n/g, "").replace(/([\da-fA-F]{2}) ?/g, "0x$1 ").replace(/ +$/, "").split(" "))
                   );
      }

      //Save string as file
      function save_file(filename, text) {
        var element = document.createElement('a');
        element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
        element.setAttribute('download', filename);
        element.style.display = 'none';
        document.body.appendChild(element);
        element.click();
        document.body.removeChild(element);
      }

      //Measure execution time
      let startTime, endTime;
      function start() {
        startTime = new Date();
      };
      function end() {
        endTime = new Date();
        var timeDiff = endTime - startTime; //in ms
        // strip the ms
        timeDiff /= 1000;
        // get seconds 
        var seconds = Math.ceil(timeDiff);
        return seconds;
      }

      //SHA-256
      //https://github.com/brillout/forge-sha256
      (function(){function p(a){this.data="";this.a=0;if("string"===typeof a)this.data=a;else if(b.D(a)||b.L(a)){a=new Uint8Array(a);try{this.data=String.fromCharCode.apply(null,a)}catch(f){for(var v=0;v<a.length;++v)this.M(a[v])}}else if(a instanceof p||"object"===typeof a&&"string"===typeof a.data&&"number"===typeof a.a)this.data=a.data,this.a=a.a;this.v=0}function w(a,f,b){for(var d,c,h,m,g,k,e,r,n,l,t,q,u,p=b.length();64<=p;){for(g=0;16>g;++g)f[g]=b.getInt32();for(;64>g;++g)d=f[g-2],d=(d>>>17|d<<15)^
        (d>>>19|d<<13)^d>>>10,c=f[g-15],c=(c>>>7|c<<25)^(c>>>18|c<<14)^c>>>3,f[g]=d+f[g-7]+c+f[g-16]|0;k=a.g;e=a.h;r=a.i;n=a.j;l=a.l;t=a.m;q=a.o;u=a.s;for(g=0;64>g;++g)d=(l>>>6|l<<26)^(l>>>11|l<<21)^(l>>>25|l<<7),h=q^l&(t^q),c=(k>>>2|k<<30)^(k>>>13|k<<19)^(k>>>22|k<<10),m=k&e|r&(k^e),d=u+d+h+x[g]+f[g],c+=m,u=q,q=t,t=l,l=n+d|0,n=r,r=e,e=k,k=d+c|0;a.g=a.g+k|0;a.h=a.h+e|0;a.i=a.i+r|0;a.j=a.j+n|0;a.l=a.l+l|0;a.m=a.m+t|0;a.o=a.o+q|0;a.s=a.s+u|0;p-=64}}var m,y,e,b=m=m||{};b.D=function(a){return"undefined"!==typeof ArrayBuffer&&
        a instanceof ArrayBuffer};b.L=function(a){return a&&b.D(a.buffer)&&void 0!==a.byteLength};b.G=p;b.b=p;b.b.prototype.H=function(a){this.v+=a;4096<this.v&&(this.v=0)};b.b.prototype.length=function(){return this.data.length-this.a};b.b.prototype.M=function(a){this.u(String.fromCharCode(a))};b.b.prototype.u=function(a){this.data+=a;this.H(a.length)};b.b.prototype.c=function(a){this.u(String.fromCharCode(a>>24&255)+String.fromCharCode(a>>16&255)+String.fromCharCode(a>>8&255)+String.fromCharCode(a&255))};
                  b.b.prototype.getInt16=function(){var a=this.data.charCodeAt(this.a)<<8^this.data.charCodeAt(this.a+1);this.a+=2;return a};b.b.prototype.getInt32=function(){var a=this.data.charCodeAt(this.a)<<24^this.data.charCodeAt(this.a+1)<<16^this.data.charCodeAt(this.a+2)<<8^this.data.charCodeAt(this.a+3);this.a+=4;return a};b.b.prototype.B=function(){return this.data.slice(this.a)};b.b.prototype.compact=function(){0<this.a&&(this.data=this.data.slice(this.a),this.a=0);return this};b.b.prototype.clear=function(){this.data=
                    "";this.a=0;return this};b.b.prototype.truncate=function(a){a=Math.max(0,this.length()-a);this.data=this.data.substr(this.a,a);this.a=0;return this};b.b.prototype.N=function(){for(var a="",f=this.a;f<this.data.length;++f){var b=this.data.charCodeAt(f);16>b&&(a+="0");a+=b.toString(16)}return a};b.b.prototype.toString=function(){return b.I(this.B())};b.createBuffer=function(a,f){void 0!==a&&"utf8"===(f||"raw")&&(a=b.C(a));return new b.G(a)};b.J=function(){for(var a=String.fromCharCode(0),b=64,e="";0<
b;)b&1&&(e+=a),b>>>=1,0<b&&(a+=a);return e};b.C=function(a){return unescape(encodeURIComponent(a))};b.I=function(a){return decodeURIComponent(escape(a))};b.K=function(a){for(var b=0;b<a.length;b++)if(a.charCodeAt(b)>>>8)return!0;return!1};var z=y=y||{};e=e||{};e.A=e.A||{};e.F=e.A.F=z;z.create=function(){A||(n=String.fromCharCode(128),n+=m.J(),x=[1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,
3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,
3204031479,3329325298],A=!0);var a=null,b=m.createBuffer(),e=Array(64),d={algorithm:"sha256",O:64,P:32,w:0,f:[0,0],start:function(){d.w=0;d.f=[0,0];b=m.createBuffer();a={g:1779033703,h:3144134277,i:1013904242,j:2773480762,l:1359893119,m:2600822924,o:528734635,s:1541459225};return d}};d.start();d.update=function(c,h){"utf8"===h&&(c=m.C(c));d.w+=c.length;d.f[0]+=c.length/4294967296>>>0;d.f[1]+=c.length>>>0;b.u(c);w(a,e,b);(2048<b.a||0===b.length())&&b.compact();return d};d.digest=function(){var c=m.createBuffer();
c.u(b.B());c.u(n.substr(0,64-(d.f[1]+8&63)));c.c(d.f[0]<<3|d.f[0]>>>28);c.c(d.f[1]<<3);var h={g:a.g,h:a.h,i:a.i,j:a.j,l:a.l,m:a.m,o:a.o,s:a.s};w(h,e,c);c=m.createBuffer();c.c(h.g);c.c(h.h);c.c(h.i);c.c(h.j);c.c(h.l);c.c(h.m);c.c(h.o);c.c(h.s);return c};return d};var n=null,A=!1,x=null;window.forge_sha256=function(a){var f=e.F.create();f.update(a,b.K(a)?"utf8":void 0);return f.digest().N()}})();

    </script>
  </body>
</html>

OLGA’s Secret

OLGA’s json does not contain a nonce value.

I leave this a mystery. Once someone figures this one out, I will publish Part 2.

Categories: Uncategorized