#!/usr/bin/env perl use strict; use warnings; use Crypt::Digest::SHA256 qw(sha256); use Device::SerialPort qw( :PARAM :STAT 0.07 ); use English; use Carp; use Time::HiRes qw(time); use Data::Dumper; my $miner = Device::SerialPort->new('/dev/ttyUSB0') or croak("Modem error $ERRNO"); $miner->baudrate(115_200); $miner->parity('none'); $miner->databits(8); $miner->stopbits(1); # One cycle takes about 12 seconds and there seems no way to # reset the thing. Hardware was designed by crypto-currency people, # so naturally, it is decently fast, very inefficient and has a # very clunky, amateurish interface. # # So, just wait the length of a cycle and empty # the result bytes. initHardware(); my @times; for(my $i = 0; $i < 5; $i++) { my $starttime = time; my $hash; my $loopcount = 0; while(1) { $loopcount++; my $rawdata = getRandomData(); if(1) { # use random data and proper timestamps $hash = generateHash($rawdata, $starttime, $loopcount); } else { # Use fixed data for validation between multiple runs $hash = generateHash('BLA', $i, $loopcount); } if(defined($hash)) { last; } } my $endtime = time; my $elapsed = $endtime - $starttime; my $decodedhash = unpack("H*", $hash); print "Calculated result $i: $decodedhash in $elapsed seconds\n"; push @times, $elapsed; } print "\n"; my $total; foreach my $val (@times) { $total += $val; } $total /= scalar @times; print "Took on average $total seconds to calculate a result.\n"; exit(0); sub initHardware { # This "cleans out" the hardware by running one hash without any new hashing request. # This has the effect of making sure any active loop has time to finish and we empty # out all result bytes from the buffer my $targettime = time + 13.5; print "Hardware initialization...\n"; # Remove previous results from queue (is any) while(time < $targettime) { my ($count, $data) = $miner->read(1); if(!$count) { next; } print "! ", ord($data), "\n"; } print "Ready for work!\n"; return; } sub generateHash { my ($data, $timestamp, $nonce) = @_; my $request = buildRequest($data, $timestamp, $nonce); $miner->write($request); my @resultbytes; my $targettime = time + 13.5; # Remove previous results from queue (is any) while(1) { my ($count, $data) = $miner->read(1); if(!$count) { last; } print "! ", ord($data), "\n"; } while(time < $targettime) { my ($count, $data) = $miner->read(1); if(!$count) { next; } my $bytecount = 0; #print "$bytecount > ", ord($data), "\n"; $bytecount++; push @resultbytes, $data; } if(scalar @resultbytes < 4) { # No result; return; } my $result = ''; for(1..4) { $result .= shift @resultbytes; } return $result; } sub buildRequest { my ($data, $timestamp, $nonce) = @_; my $source = sha256($data); # Add fill my $fill = "0" x 160; $fill = pack("B*", $fill); $source .= $fill; $source .= toBinary($timestamp); # timestamp # Difficulty target in "bits" encoding. No idea how that really works, though. # If i didn't do something stupid, i just copied the one used to mine the first bitcoin. $source .= toBitsBinary("1d00ffff"); $source .= toBinary($nonce); # Nonce return $source; } sub toBinary { my ($val) = @_; if(1) { return pack('N', $val); } else { # "shuffle bytes by hand" version for easier experimentation # compared to fiddling with the fast but confusing pack/unpack function my @bytes; for(1..4) { unshift @bytes, chr($val & 0xff); $val >>= 8; } return join('', @bytes); } } sub toBitsBinary { my ($val) = @_; my @parts = unpack"(A2)*", $val; my @bytes; foreach my $part (@parts) { push @bytes, chr(hex($part)); } return join('', @bytes); } sub getRandomData { my @validchars = split//, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; my $token = 'XPD'; for(1..500) { $token .= $validchars[rand @validchars]; } $token .= 'VT'; return $token; }