Skip to content

Hashing and mining in scripts

This page lets a script compute checksums, signatures, keys and random bytes with the crypto module, and search a Bitcoin block header for a winning nonce. It assumes you know how a script is built. If not, start with Your first script.

Only the TC002 has this module. Add # @needs crypto to the header of a script that imports it, so other models show it as not suitable. On other models the import fails, so a script meant for every model imports it inside try and leaves out @needs. See Scripts for every model.

How it behaves

A hashing call computes its result before it returns, and the display waits meanwhile. Mining works the other way: crypto.mine() searches in the background and calls your callback for each hit. It uses every core of the clock, but only the computing time the clock does not need itself, so the display, the web interface and the sound keep running as before. A search goes on only while your app keeps calling the mining functions, and only one app can search at a time.

Hashing

Add import crypto. Each call takes strings or bytes and returns bytes. .tohex() turns them into text.

Call Returns
crypto.md5(data) the MD5 of data, 16 bytes
crypto.sha1(data) the SHA-1 of data, 20 bytes
crypto.sha256(data) the SHA-256 of data, 32 bytes
crypto.sha256d(data) the SHA-256 of that SHA-256, as Bitcoin uses it
crypto.hmac_sha1(key, data) the HMAC-SHA-1 signature of data with key, 20 bytes
crypto.hmac_sha256(key, data) the HMAC-SHA-256 signature of data with key, 32 bytes
crypto.pbkdf2_hmac_sha256(password, salt, iterations, length?) the PBKDF2 key of password, length bytes (default 32, up to 1024)
crypto.random(length) length random bytes (1 to 1024) for keys and nonces

bytes converts on every model: bytes("48656c6c6f") and .tohex() for hexadecimal, bytes().fromb64("SGVsbG8=") and .tob64() for Base64, .asstring() for text.

Many web services want a request signed with HMAC-SHA-256. Keep the secret in a @config field and sign the body in a method of your app:

  def send(body)
    var signature = crypto.hmac_sha256(store.get("secret"), body).tohex()
    http.post("https://example.com/api", body, def (reply, status) log(str(status)) end,
              {'headers': {'X-Signature': signature}})
  end

A FRITZ!Box login (login_sid.lua?version=2) sends a challenge such as 2$10000$5A1711$2000$5A1722. Its answer is two PBKDF2 rounds:

  def fritz_response(challenge, password)
    import string
    var p = string.split(challenge, "$")
    var key = crypto.pbkdf2_hmac_sha256(password, bytes(p[2]), int(p[1]))
    key = crypto.pbkdf2_hmac_sha256(key, bytes(p[4]), int(p[3]))
    return p[4] + "$" + string.tolower(key.tohex())
  end

Two-factor codes (TOTP) are an HMAC-SHA-1 of the current 30-second step. The secret an authenticator app shows is Base32:

  def totp(secret, ms)
    import string
    var key = bytes(), bits = 0, n = 0
    for i : 0 .. size(secret) - 1
      var v = string.find("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567", string.toupper(secret[i]))
      if v < 0 continue end
      bits = ((bits << 5) | v) & 0xfff
      n += 5
      if n >= 8 n -= 8 key.add((bits >> n) & 0xff) end
    end
    var step = bytes()
    step.add(0, -4)
    step.add(ms / 30000, -4)
    var h = crypto.hmac_sha1(key, step)
    return string.format("%06d", (h.get(h[19] & 0x0f, -4) & 0x7fffffff) % 1000000)
  end

Call it as self.totp(store.get("secret"), epoch_ms()) once the clock knows the time. Before that, epoch_ms() returns -1.

iterations runs from 1 to 1 000 000. A length above 32 lowers that limit in proportion. The display waits while the key is computed, so compute it once and keep it.

Anything else than a string or bytes raises value_error, and so does an iterations or length out of range. Hash in setup(), loop() or a callback, never in draw().

Proof of work

crypto.mine() tries every nonce of an 80-byte Bitcoin block header and reports each hash that meets a target. It does not talk to a mining pool. A script does that with TCP. The Solo Miner app on the AWTRIX Hub is a complete example.

import crypto

class Hashrate
  var header, rate

  def setup()
    self.rate = 0
    self.header = bytes()
    self.header.resize(80)
    crypto.mine(self.header, crypto.target(1.0), def (nonce, hash)
      if nonce == nil log("all nonces tried") else log("hit at " + nonce) end
    end)
  end

  def loop() self.rate = crypto.mine_rate() end

  def draw()
    clear()
    text(0, 6, str(int(self.rate / 1000)) + " kH/s")
  end
end

return Hashrate()
Call Does
crypto.mine(header, target, callback, opts?) Starts searching header (80 bytes, its nonce field is ignored) for hashes at or below target (32 bytes, little-endian) and returns true. A new call replaces your running search. Returns false while another app is searching. opts takes {'threads': n}: 0 or leaving it out uses every core.
crypto.mine_stop() Stops your search.
crypto.mine_rate() Hashes per second over the last five seconds.
crypto.mine_hashes() Hashes since your app started searching.
crypto.mine_best() The highest difficulty any of those hashes reached.
crypto.mine_threads() How many cores the clock has.
crypto.target(difficulty) The 32-byte target of a pool difficulty.
crypto.difficulty(hash) The difficulty a 32-byte hash reaches.

For each hash that meets the target, the callback gets the nonce as eight hexadecimal characters, the way a pool expects it in mining.submit, and the hash as 32 bytes. When every nonce has been tried it gets nil, "done": change the header, for example its extranonce, and start again.

To keep a search running, call crypto.mine_rate() from loop(), which runs every second whether the app is shown or not. Five seconds without a call stop the search. Keep the rate and the best difficulty in members and draw those. draw() alone does not keep the search going.

Removing or switching off the app stops its search. At most 64 hits wait for your callback and 16 are delivered per frame, so with a target that nearly every hash meets, some hits are dropped. A wrong size or type raises value_error.

  • TCP in scripts: talk to a mining pool or any other line-based service
  • Network: HTTP, MQTT and Modbus in scripts
  • Limits: every limit a script runs under