Post

How to properly and immutably create a timestamp for a document (using Blockchain technology)

2025-02-03

I had written a Math paper which was not good enough to be published in a journal, yet I wanted to have proof of the date I wrote it on so I could post the paper on the web without other people later being able to question who first came up with the idea.

The way I thought was more correct to do this was to publish the paper's sha256 checksum on the Bitcoin blockchain. Posting the SHA on social media (such as x.com) was not enough, as paid members can edit their old posts.

I got in touch with BBRTJ, the maintainer of Bitcoin::Crypto, who was very helpful in teaching me how to use his module and achieving my aim.

The goal, he told me, was to include the SHA256 checksum as the comment (technically the 'NULLDATA') of a small bitcoin transaction.

  1. I created a BTC address with Perl, using Bitcoin::Crypto
  2. I transferred a very few dollars worth of BTC to this address
  3. I found the transaction ID at mempool
  4. I found the index number of the ouptut of the transaction that got crypto into my address, because this number together with the transaction ID are what form the UTXO
  5. I used this UTXO as input (funds) in a new transaction that had two outputs: One output was the NULLDATA string (the "comment" that contains the SHA256 checksum of the PDF document), and the other was a "change" address of mine, which would receive the rest of the money from the UTXO (full amount minus transaction fee (around $1)).

Here are these steps in more detail, and with code:

1. I created a BTC address with Perl, using Bitcoin::Crypto

$ carton exec -- perl -M Bitcoin::Crypto::Util=generate_mnemonic -E 'say generate_mnemonic;'

night under common media spare rotate employ air shell member fade similar

Store these randomly-chosen 12 words somewhere privately, as they are necessary to generate addresses and their private keys which are needed to spend money from those addresses.

Inspired by the SYNOPSIS on this page (https://metacpan.org/pod/Bitcoin::Crypto) I created the following script:

#!/usr/bin/env perl

use v5.40;
use FindBin '$RealBin';
use lib "$RealBin/local/lib/perl5";

use Bitcoin::Crypto qw(btc_extprv);
use Bitcoin::Crypto::Util qw(to_format);
use Bitcoin::Crypto::Constants;

my $mnemonic = 'night under common media spare rotate employ air shell member fade similar';
my $master_key = btc_extprv->from_mnemonic($mnemonic);

# you can generate many different addresses from the same mnemonic
# by changing the index number in this statement below, each time:
my $derived_key = $master_key->derive_key_bip44(
    purpose => Bitcoin::Crypto::Constants::bip44_segwit_purpose,
    index => 0,
);

my $priv = $derived_key->get_basic_key;
my $pub = $priv->get_public_key;
say 'private key: ' . $priv->to_wif;
say 'public key: ' . to_format [hex => $pub->to_serialized];
say 'address: ' . $pub->get_address;

By running the script above, I get the bitcoin address at which I can receive bitcoin to be used by my perl scripts, as well as other useful info:

private key: L4FSVoestknfy8Mtxf3wjb3AovqDjr7Di5D4i27kRB8JMvcFxRdp
public key: 026df4b6a9bdea3ed03c43077693b330031c539223b102c7e580a59d91115dd190
address: bc1q98248fugn8xnzxg53mmxgrzu9ll07u0z5ajw69

The private key is used to sign transactions from this address (a signature is needed because we don't want anyone spending our crypto-money).

2. I transferred a very few dollars worth of BTC to this address

I logged in to a crypto exchange and send some bitcoin to this address.

3. I found the transaction ID at mempool

I searched for my bitcoin address at https://mempool.space and got this page https://mempool.space/address/bc1q98248fugn8xnzxg53mmxgrzu9ll07u0z5ajw69

I waited for several minutes for the bitcoin amount to reach the address (it appeared on that page), but then I got the transaction ID: c7fc5929f88bd381a06f0407f70bff66acfcd751382aa66938ec42eeb9619356

4. I found the index number of the ouptut of the transaction that got crypto into my address, because this number together with the transaction ID are what form the UTXO

First I got the full hex of transaction, by clicking on "transaction hex" on this page: https://mempool.space/tx/c7fc5929f88bd381a06f0407f70bff66acfcd751382aa66938ec42eeb9619356 :

01000000000101167b17163a...ed8ebb7ad5300000000

Then I ran this script to see which of the outputs of this transaction, from my exchange to a bunch of btc addresses, goes to my address:

#!/usr/bin/env perl

use v5.40;
use FindBin '$RealBin';
use lib "$RealBin/local/lib/perl5";

use Bitcoin::Crypto qw(btc_transaction btc_utxo);
use Bitcoin::Crypto::Network;

my $me = 'bc1q98248fugn8xnzxg53mmxgrzu9ll07u0z5ajw69';
my $txn_hex = '01000000000101167b17163a...ed8ebb7ad5300000000'; # a rather large string
my $txn_id = 'c7fc5929f88bd381a06f0407f70bff66acfcd751382aa66938ec42eeb9619356';

Bitcoin::Crypto::Network->get('bitcoin')->set_default;

btc_utxo->extract([ hex => $txn_hex ]);

for (my $i = 0; ; $i++) {
    say $i;
    my $utxo = btc_utxo->get([hex => $txn_id], $i);
    if ($utxo->output->locking_script->get_address eq $me) {
        say 'This is it.';
        last;
    }
}

Output was:

75
76
77
78
79
80
This is it.

So the index of the output of that transaction (which has outputs to many addresses) that relates to my address is: 80 (we'll use this to form the UTXO).

5. I used this UTXO as input (funds) in a new transaction that had two outputs: One output was the NULLDATA string (the "comment" that contains the SHA256 checksum of the PDF document), and the other was a "change" address of mine, which would receive the rest of the money from the UTXO (full amount minus transaction fee (around $1)).

When a transaction has multiple outputs, it means the bitcoin from the inputs (which also may be more than 1 UTXO) get split to many addresses.

So now we're going to form (locally) a transaction that accepts as input (ie source of bitcoin funds) the UTXO that consists of the previous transaction and the index "80", and as its two outputs: the comment (NULLDATA, sha256 checksum) and a destination address that will receive the "change" from the entire amount of the UTXO minus the bitcoin network fee (around $1).

First step is that we create this destination address, by re-running the inital script that created our first address, but only after modifying the "index" number from 0 to 1 (because we want a different address, which, as an added bonus, looks independent of the first one). Output:

private key: KygMogKnrjALtjRnexiZmRDrZZ8T4kvpPB25NmrGfB9NbiDvcqt7
public key: 0393617f491bd7a201edbbf39129664b8d39629ee530feaf073770839f602798dc
address: bc1qzsxyhawtmqww8m673udsewnr7x8luk4mx8p4ct

And then we run this script (that uses the address above, and which was inspired by: https://metacpan.org/release/BRTASTIC/Bitcoin-Crypto-3.001/source/ex/tx/nulldata.pl):

#!/usr/bin/env perl

use v5.40;
use FindBin '$RealBin';
use lib "$RealBin/local/lib/perl5";

use Bitcoin::Crypto qw(btc_transaction btc_utxo btc_prv);
use Bitcoin::Crypto::Util qw(to_format);
use Bitcoin::Crypto::Network;

my $privkey_me = 'L4FSVoestknfy8Mtxf3wjb3AovqDjr7Di5D4i27kRB8JMvcFxRdp';
my $txn_hex = '01000000000101167b17163ae...ed8ebb7ad5300000000'; # rather large string
my $txn_id = 'c7fc5929f88bd381a06f0407f70bff66acfcd751382aa66938ec42eeb9619356';
my $dst_address = 'bc1qzsxyhawtmqww8m673udsewnr7x8luk4mx8p4ct';

Bitcoin::Crypto::Network->get('bitcoin')->set_default;

my $tx = btc_transaction->new;

btc_utxo->extract([hex => $txn_hex]);

# 80
$tx->add_input(
    utxo => [[hex => $txn_id], 80],
);

$tx->add_output(
    locking_script => [ NULLDATA => 'pdf: ab75051b7a...06860c2fff' ], # our SHA256 string!!
    value          => 0,
);

$tx->add_output(
    locking_script => [ address => $dst_address ],
    value          => 0,
);

$tx->set_rbf;

my $wanted_fee_rate = 6;
btc_prv->from_wif($privkey_me)->sign_transaction($tx, signing_index => 0);
$tx->outputs->[1]->set_value($tx->fee - int($tx->virtual_size * $wanted_fee_rate));
btc_prv->from_wif($privkey_me)->sign_transaction($tx, signing_index => 0);
$tx->verify;

say $tx->dump;
say "\n\n======================================================\n\n";
say to_format [hex => $tx->to_serialized];

Values in outputs are measured in satoshis (there are 100mn satoshis in a bitcoin), and we set the value of the second output to 0 in the script above, because we later set it to the correct value.

This here line: $tx->outputs->[1]->set_value($tx->fee - int($tx->virtual_size * $wanted_fee_rate)); changes the value of the second output from 0 to $tx->fee (which is a synonym for "all the remaining input bitcoin") minus our desired wanted fee ($rate * size in virtual bytes). All the remaining amount (i.e. wanted fee) becomes the transaction's fee, to be collected by the bitcoin miners.

The reason the transaction is signed twice is that if the first signing didn't happen, then the transaction would be smaller than the transaction's final size during the ->set_value call, which would mean ->virtual_size would be smaller than the final size, and the total fee amount (which is a product of virtual size with wanted fee rate) would be smaller than needed. However, by signing the transaction before calculating the total fee, the transaction's size reaches its final size and the total fee calculated is correct (because the second signature, which is necessary as we have set the value in the meantime, replaces the first and doesn't change the total size of the transaction).

Running this script gives the following output:

Transaction 335fcf3ce5d182c4d06b501e3688909ce2e0c920b8ad584c811163f01cf8db30
version: 1
size: 189.5vB, 758WU
fee: 1137 sat (~6 sat/vB)
replace-by-fee: yes
locktime: 0

1 inputs:
P2WPKH Input from bc1q98248fugn8xnzxg53mmxgrzu9ll07u0z5ajw69
spending output #80 from c7fc5929f88bd381a06f0407f70bff66acfcd751382aa66938ec42eeb9619356
value: 3000
sequence: 0xFFFFFFFD
locking script: 001429d553a78899cd3119148ef6640c5c2ffeff71e2
witness:
3045022100d71c8cddb350481a4a981cef7ec347d8e587985854584c68c5d53da4db...
026df4b6a9bdea3ed03c43077693b330031c539223b102c7e580a59d91115dd190

2 outputs:
NULLDATA Output to "pdf: ab75051b7aed1e1f44d388cd83205c8eecb06b79ca2d49c474c7bb06860c2fff"
value: 0
locking script: 6a457064663a20616237353035316237616564316531663434643338386364383332...

P2WPKH Output to bc1qzsxyhawtmqww8m673udsewnr7x8luk4mx8p4ct
value: 1863
locking script: 0014140c4bf5cbd81ce3ef5e8f1b0cba63f18ffe5abb



======================================================


01000000000101569361b9ee42ec3869a62a3851d7fcac66ff0bf707046fa081d38bf82959fcc
75000000000fdffffff020000000000000000476a457064663a20616237353035316237616564
31653166343464333838636438333230356338656563623036623739636132643439633437346
3376262303638363063326666664707000000000000160014140c4bf5cbd81ce3ef5e8f1b0cba
63f18ffe5abb02483045022100d71c8cddb350481a4a981cef7ec347d8e587985854584c68c5d
53da4db521d8802206269a44e93f017584c83416a511f86d5d3f498c9b389c120d33f5391b7cc
69170121026df4b6a9bdea3ed03c43077693b330031c539223b102c7e580a59d91115dd190000
00000

First there's a human-readable dump of the transaction we create locally, and then, after the horizontal line, there's the transaction in hex form which you can submit to the bitcoin network via the mempool site.

The dump looks good, so we go to https://mempool.space/ and click on "Broadcast Transaction" in its footer, to get to: https://mempool.space/tx/push , where we paste the above long hex string.

And we get this transaction ID as output: 335fcf3ce5d182c4d06b501e3688909ce2e0c920b8ad584c811163f01cf8db30

The URL at which we can observe this new transaction is: https://mempool.space/tx/335fcf3ce5d182c4d06b501e3688909ce2e0c920b8ad584c811163f01cf8db30

At this URL the SHA256 checksum appears, and will always appear there (or in similar sites) for all eternity. So we bookmark this transaction.

Cost to timestamp my pdf: $1 (could also have been achieved with half that amount!) Please feel free to snatch the $1-2 remaining in my address, using Perl.

Comments

-- No one has left a comment --
Write your comment: