Turbo Blinds

Table of Contents

Preface

On Handshake, Turbo Blinds (aka Lockup Loans!) aims to change the bidding landscape by allowing turbo-charged blinds to hide your true bid amount. Moreover, it will bring opportunities to Stake your holdings of HNS in a low-risk manner. It does so by making it possible to take cryptographically secure loans for your blinds.

This blog post is oriented towards the technical audience who are interested in the inner workings of it or wish to aid in its future development.

Screenshot of Bob Wallet showing an ongoing auction
Figure 1: A Screenshot of Bob Wallet with an ongoing auction

Introduction

This blog post will document the journey of the development of the Turbo Blinds protocol on the Handshake Network. It serves as an elementary1 guide, providing insights into the protocol’s development and aims to allow you, the reader, to implement your own protocols!

I am going to give what I will call an elementary demonstration. But elementary does not mean easy to understand. Elementary means that very little is required to know ahead of time in order to understand it, except to have an infinite amount of intelligence. There may be a large number of steps that hard to follow, but to each does not require already knowing the calculus or Fourier transforms.
- Richard P. Feynman1

This Blog Post will be covering the following things, you should feel free to skip ahead:

  1. An Overview of the Handshake Protocol and Scripting.
  2. Two challenges with small bounties (100 HNS + 200 HNS).
  3. A Quick Summary of the Protocol.

What is Handshake?

Handshake is a decentralized naming protocol designed to revolutionize the current infrastructure of the Internet. Traditionally, naming and addressing are managed by centralized entities, like ICANN, that delegates access to parts of it to other centralized entities2, controlling the root DNS naming zone.

Handshake flips the script by distributing this control among individual peers. This decentralization mitigates vulnerabilities associated with a single point of control, such as susceptibility to hacking, censorship, and corruption.

I can’t give it justice, so I’d recommend reading up more on it here

Under the hood, Handshake is a blockchain-based Cryptocurrency, very similar to (and is based on an implementation of) Bitcoin, with some added bells and whistles3 to make the magic possible.

The Problem

Auctions on Handshake are like Vickrey Auctions, where bidders submit sealed bids and the highest bidder wins but pays the second-highest bid. However, Handshake auctions aren’t exactly Vickrey Auctions because on the blockchain, where everything is public, it’s tricky to hide the actual bid amount while securing the bid funds.

Therefore, to hide the true bid, it’s often combined with a “lockup”, adding extra money that you get back after the auction, but concealing your actual bid with a “blind”. It still sets a higher bar for what your true bid could be, which is less ideal than it being unbounded.4

This however has the downside of locking significant portions of your funds until an auction is over. It also makes onboarding extremely annoying, as you have to buy a surplus supply, go through an auction, and then sell your surplus after the auction is over.

The Solution

There’s a straightforward real-world fix for this issue! Approach your closest friend and request a “loan”. However, that relies on having a friend who’s willing to trust you. Another option is asking someone else to bid on your behalf and then transferring the domain from them later, but again, trust is key. And Cryptocurrencies are about being trustless!

Turbo Blinds aims to solve this problem in a trustless and decentralized way!

Deep Dive into Handshake Protocol

Before we dive in, let me quickly run through a few basic concepts you need to get the hang of the protocol. If you’re already familiar with them, feel free to skip ahead. Alternatively, Mastering Bitcoin5 (By Andreas M. Antonopoulos) is an excellent source of information.

What is a Transaction?

Transactions are the fundamental building blocks of a “block”6. Think of a transaction as spending money from one (or more) “pocket(s)” to another “pocket(s)”. Let’s dive in a little deeper.

This is what a typical transaction looks like:

# # # 1 2 3 p w s p w s V r i e r i e e I e t q e t q r n v n u v n u s p o e e o e e i u u s n u s n o t t s c t s c n : : e : : e C : : o t [ t [ u x ] 0 x ] 0 n i x i x t d f d f : f f T + f + f r 2 f f a i f i f n n f n f s d f d f a e f e f c x x t i o n v a c } v a c } v a c } H a d o a d o a d o a O l d v l d v l d v s u u r e u r e u r e h t e e n t i e e n t i e e n t i p : s a y t : s a y t : s a y t u s n p e s n p e s n p e t 2 : t e m 1 : t e m 1 : t e m 3 : : s 3 : : s 2 : : s n C < : < : < : L o h { B h { N h { N o u a I [ a O [ a O [ c n s D . s N ] s N ] k t h . h E h E t : > . > > i ] m 3 e

and here is how it’s implemented in code:

class TX extends bio.Struct {
  constructor(options) {

    this.version = 0; // Reserved for future usage
    this.inputs = [];
    this.outputs = [];
    this.locktime = 0;

    this._hash = null; 
    ...
  }
}

More accurately, Transactions move value from transaction inputs to transaction outputs, where an input is spending value stored in a previous transaction output7. An output sets conditions for spending the coin, usually in the form of a public key or a script that must be solved. We will be discussing this in detail.

The Output

Here is how the output is implemented:

class Output extends bio.Struct {
  constructor(options) {
    ...
    this.value = 0;
    this.address = new Address();
    this.covenant = new Covenant();
    ...
  }
}

An Output has the following parts:

  • The Value: The value (in dollarydoo8) of HNS being locked to the output.
  • The Address: The address is just a hash of the public key (P2WPKH) or the script9 (P2WSH) the money is being locked to.
  • The Covenant: Covenants are the real magic behind HNS which allows Auctions and everything behind them to be held. They allow you to put extra restrictions on spending the output that just isn’t possible with scripts. We’ll discuss this further in more detail later.

The Input

Here is how the input is implemented:

class Input extends bio.Struct {
  constructor(options) {
    ...
    this.prevout = new Outpoint();
    this.witness = new Witness();
    this.sequence = 0xffffffff;
    ...
  }
}

An input has the following parts:

  • The Prevout: A reference to the output it’s attempting to spend, That is, the transaction hash of the previous transaction, and the index of the output.
  • The Witness10: It’s the serialization of data that is used to solve the locking script hash or the signature (and the public key!) for the public key the output was locked to.
  • The Sequence: Often referred to as nSequence, has some unique properties, It activates the nLockTime of the transaction if it’s set to any value < 0xffffffff. This had the unfortunate side effect of causing the Shakedex Locktime Bug. nSequence is also used for RBF signaling on Bitcoin11.

Scripting!

Scripting on Bitcoin (and Handshake) happens in a very simple stack-based language. It is intentionally not Turing-Complete, figure out why that might be the case 😉. A script is essentially a list of instructions evaluated from left to right. A transaction is considered valid if there is a non-zero value on top of the stack after script evaluation. Let’s look at it with an example:

Multisigs

You might run into a scenario where you want multiple people to have control of funds, these problems can be generalized into a m of n Multisig problem. Where we have n public key holders, out of which we need approval from at least m of them to spend the funds. We can solve this problem with the help of scripts!

Here is what a 2-of-3 Multisig looks like:

OP_2 <pubkey1> <pubkey2> <pubkey3> OP_3 OP_CHECKMULTISIG

Here,

  • OP_2 and OP_3: Push Values 0x02 and 0x03 on the stack respectively.
  • <pubkeyX>: Pushes the raw public key value on the stack,
    • Here is typically what a compressed public key looks like: 0x02b4632d08485ff1df2db55b9dafd23347d1c47a457072a1e87be26896549a8737.[^serialized]
  • OP_CHECKMULTISIG: Verifies the signatures and leaves OP_1 on the stack if they’re valid, and OP_0 on the stack if they’re invalid. We’ll take a closer look at this in a second.

Now the SHA3 digest of the serialized script can be used as the address hash in a transaction output, and funds can be transferred to this address.

OP_CHECKMULTISIG expects the stack to look like this:

... <dummy value> <sig1> <sig2> OP_2 <pubkey1> <pubkey2> <pubkey3> OP_3 [OP_CHECKMULTISIG]

When this runs successfully aka the signatures are valid, we have OP_1 on top of the stack, which allows the transaction to be spent! So let’s look into how script evaluation works!

Note: The dummy value is required because of a bug in the original OP_CHECKMULTISIG implementation that popped an extra value off the stack, it’s now required to be OP_0.

Script Evaluation

As discussed earlier, The Witness field in the input acts as the solution to the locking script. You’ll also notice, we do not lock the output to the script, but instead the script hash! So in order to spend the output, we make an input with a witness that looks like this:

witness: [
  OP_0, // dummy value
  sig1,
  sig2,
  serialized_script
]

In the case of a P2WSH Address, the last element of the witness is hashed and verified against the address, then it is popped off and deserialized as a script, and then the final script is evaluated as such:

OP_0 <sig1> <sig2> OP_2 <pubkey1> <pubkey2> <pubkey3> OP_3 OP_CHECKMULTISIG
└───────────────┘  └───────────────────────────────────────────────────────┘
   Witness Items                     Deserialized Script

Which would evaluate to OP_1 with valid signatures, giving us a valid transaction. Hooray!

Challenge 1

I know that was a lot, but at the same time, we skipped a lot of details. This is why I have a very simple challenge ready for this! I have locked up 100 HNS to this P2WSH address. (In case there are multiple transactions, see if there’s an unspent one).

The P2WSH address has the following script behind it:

OP_5 OP_ADD OP_13 OP_EQUALVERIFY

Address: hs1qysy9dp9qj9qk85j46kt2f9ejg647lxe38gvzs4e0ftgj8eq6z02qmwptfr

If you can figure out how to spend it, it’s yours! I’d really like someone new to handshake to get it though, so for peeps already familiar with how this works, there’s a different challenge for you, spend the input with 2 outputs, one returning it back, and one with a nulldata address with your name in it.

If you have no idea where to get started, don’t worry, here are some tips:

  1. Get hsd, and check out the tests, tx-test.js surely looks interesting.
  2. Even though having a full node would be ideal; if you’re worried about time, this challenge can be done solely with the help of blockchain explorers, you can launch hsd --spv to quickly broadcast your transaction.
  3. You do not need any HNS to do this challenge, you can use the above funds for fees.
  4. Check out this Bitcoin Wiki if you’re a little hazy on scripting, Remember Handshake is SegWit only, so there’s no scriptSig or scriptPubKey.
  5. You can also find someone on Handshake Dev Telegram or Discord if you want any hints 😊.
  6. Last, but I can’t stress this enough hsd-dev.org is god sent for any and all documentation.

Challenge 2

Here’s another slightly harder one, this one has 200 HNS locked up.

Script:

OP_6 OP_PICK OP_TOALTSTACK
OP_3 OP_PICK OP_NOT OP_FROMALTSTACK OP_BOOLOR OP_TOALTSTACK
OP_4 OP_PICK OP_FROMALTSTACK OP_BOOLOR OP_TOALTSTACK
OP_1 OP_PICK OP_NOT OP_TOALTSTACK
OP_7 OP_PICK OP_FROMALTSTACK OP_BOOLOR OP_FROMALTSTACK OP_BOOLAND OP_TOALTSTACK
OP_2 OP_PICK OP_NOT OP_TOALTSTACK
OP_6 OP_PICK OP_NOT OP_FROMALTSTACK OP_BOOLOR OP_FROMALTSTACK OP_BOOLAND OP_TOALTSTACK
OP_0 OP_PICK OP_NOT OP_TOALTSTACK
OP_6 OP_PICK OP_NOT OP_FROMALTSTACK OP_BOOLOR OP_FROMALTSTACK OP_BOOLAND OP_TOALTSTACK
OP_5 OP_PICK OP_NOT OP_FROMALTSTACK OP_BOOLAND OP_TOALTSTACK
OP_4 OP_PICK OP_NOT OP_TOALTSTACK
OP_5 OP_PICK OP_FROMALTSTACK OP_BOOLOR OP_FROMALTSTACK OP_BOOLAND OP_TOALTSTACK
OP_3 OP_PICK OP_TOALTSTACK
OP_4 OP_PICK OP_FROMALTSTACK OP_BOOLOR OP_TOALTSTACK
OP_5 OP_PICK OP_FROMALTSTACK OP_BOOLOR OP_FROMALTSTACK OP_BOOLAND OP_TOALTSTACK
OP_7 OP_PICK OP_NOT OP_TOALTSTACK
OP_4 OP_PICK OP_FROMALTSTACK OP_BOOLOR OP_FROMALTSTACK OP_BOOLAND 
OP_VERIFY OP_2DROP OP_2DROP OP_2DROP OP_2DROP

Encoded:

56796b5379916c9b6b54796c9b6b5179916b57796c9b6c9a6b5279916b5679916c9b6c9a6b0079916b5679916c9b6c9a6b5579916c9a6b5479916b55796c9b6c9a6b53796b54796c9b6b55796c9b6c9a6b5779916b54796c9b6c9a696d6d6d6d

Address: hs1qywhe8tlddzqwj6xmw5xuyjmsefjs9yqqag2wp0zuyatvuurrrwlqh8cn0s

Note, that one can derive the address from the script itself, I’ve linked it as it would allow you to easily check if the challenge has been claimed by someone else already.

SIGHASH

We’ve discussed signatures before, However, we didn’t really discuss what these signatures were signing. You see, our challenge above suffers from a very annoying problem, when we broadcast our transaction, anyone can swap out the output in the transaction and rebroadcast it as their transaction. We need some way to make sure our solution to the input somehow ensures that our outputs (or inputs, or a combination of them) are untampered. SIGHASH flags allow us to solve this issue! They signal what part of the transaction a signature signs for. On Handshake we have the following SIGHASH flags:

  1. SIGHASH_ALL: Signs all Inputs and Outputs
  2. SIGHASH_SINGLE: Signs all inputs and the output with the same index as the input.
  3. SIGHASH_SINGLEREVERSE Signs all inputs and the opposite with the opposite ((outputs.length - 1) - index) index as the input.
  4. SIGHASH_NONE: Signs all inputs and NO outputs.

with the following masks:

  1. ANYONECANPAY: Modifies the SIGHASH flag to only sign the corresponding input.
  2. NOINPUT: NOINPUT tries to implement the NOINPUT flag as proposed in the original eltoo paper. However, the implementation is a little buggy12 and one needs to be careful. It does not commit the prevout, and the sequence value of an input, and thus allows the signature to be valid for ALL Coins (of the same value) in an address. One has to be really careful with its usage.

The Version and nLocktime of the transaction are always signed.

Covenants

Covenants put further restrictions on how an input can be spent, a covenant contains a type and an array of items. These are what allow the magic of auctions to take place.

Let’s look at the simplest covenant OPEN.

OPEN

OPEN as the name suggests, start an auction for a name. It looks like this:

value : 0, // can be anything
type: types.OPEN,
items: [
  <namehash>,
  <height*>,
  <name>
]

OPEN by itself doesn’t introduce any restrictions, it can be spent just like NONE, in fact funnily enough, OPEN is not limited by network dust policy either, Because of this reason there are a lot of 0 value OPEN outputs sitting in the UTXO.

BID

BID covenant is used to place bids on a name, it’s value includes both the true bid and the lockup. It’s structured like this:

value: true_bid + lockup,
type: types.BID,
items: [
  <namehash>,
  <height>,
  <name>,
  <blind>
]

blind here is generated by hashing the true bid with a nonce. As long the hash function (which is blake2b in this case) is cryptographically safe, we can be sure that someone can’t manipulate the true bid. If you don’t understand why, read up on Preimage Resistance

REVEAL

The Bid covenant can only be spent by a REVEAL, more accurately the output at the same index as a BID input must be a REVEAL. Not doing so in the appropriate reveal window leaves the BID unspendable. The REVEAL reveals the nonce and the true bid and is structured like this:

value: true_bid,
type: types.REVEAL,
items: [
  <namehash>,
  <height>,
  <nonce>
]

Another output can be used to return the lockup.

REGISTER and REDEEM

Depending on if you win or lose the auction, you can use REGISTER or REDEEM respectively (spending the REVEAL). REGISTER will refund you the difference between the first and second bid, while REDEEM will return you the whole amount. REGISTER also lets you set the DNS record for the first time. From this point on, the value in your output is “locked” or “burnt”, it cannot be put back into circulation and is forever bound to this chain of outputs. You can only use UPDATE, RENEW, TRANSFER (and FINALIZE or REVOKE) to spend these outputs.

UPDATE and RENEW

UPDATE can be used for future DNS updates, A RENEW is necessary every 2 block-years, as a heartbeat transaction.

TRANSFER, FINALIZE, and REVOKE

The other covenants also have another restriction, the output address needs to match the input address. For this reason, in order to transfer a domain to another address, we need to make use of TRANSFER. There is a 2 block-days window while one can spend it with REVOKE, returning it to circulation, After which one can FINALIZE the transfer. The REVOKE covenant exists for use in the case of a key-compromise situation where having the domain return to circulation may be better than paying the hostage fees to get it back.

Turbo Blinds

A small technical summary of the protocol has been available here for quite a while, I’ll be highlighting some key design choices going ahead. I’d recommend reading it first.

I have it linked here:

The Address

From all the above information, there are three major takeaways:

  • The address we place in our BID output can’t be changed unless we can transfer.
  • This means we’d need a script where either party can’t unilaterally control the address.
  • To presign a TRANSFER, we need to know the value of the domain’s REGISTER output, (which depends on the value of the second largest bid), which is impossible to know in advance.

One way to achieve this would be to make a 2-of-2 which decays into a 1-of-1 address after the auction is over, and this is what I went with.

Contract:

OP_IF
  <pubKeyB> OP_CHECKSIGVERIFY
OP_ELSE
  <reveal phase end height> OP_CHECKLOCKTIMEVERIFY OP_DROP
OP_ENDIF
<pubKeyA1> OP_CHECKSIG

Here pubkeyA would belong to the bidder, while pubKeyB belongs to the staker. This would be the address where we’d be placing our bid. Going ahead I’d be referring to them as Alice and Bob.

The Chain Analysis Problem

If we somehow consolidate funds on-chain before placing the bid, they’d be very visible in chain analysis, at the same time, we need to ensure that Alice cannot run away from the transaction. Therefore we need a way to lock up Alice’s funds until the reveal phase ends. We also need a way for her to reclaim her funds in case there’s an issue. Luckily our previous 2-of-2 contract can be reused here (with different public keys to keep them uncorrelated).

So we have two contracts with the same content, just different public keys.

The NOINPUT

The use of NOINPUT has been one of the more unpopular design choices. This is because NOINPUT hasn’t been used before, and its implementation has been riddled with unintended behaviours12. However, I believe it is not as dangerous and somewhat necessary, and hope to convince you of the same.

Using it simplifies the setup for stakers considerably, since presigns depend on previous transaction hashes, stakers would be required to lock input coins13 used in these transactions until the funding address is filled and confirmed. This prevents them from using these coins to fund other transactions until this one is complete, this also introduces DoS vectors.

NOINPUT allows you to fund these transactions using any prevout matching the script. This maks the whole scheme much more robust.

The Final Construction

The Final Protocol Flow looks like this:

Block Diagram of the Turbo Blinds Protocol
Figure 2: Block Diagram of the Protocol

and here is the code that documents the procedure:

Future (and Bounties!)

Future work and development on turbo blinds will be happening in this repository. I would also be setting up bounties for reports of any design flaws in the current draft of the protocol. Details for them will soon be available in the repository.

I will be exploring the use of Adaptor Signatures to avoid the need for a trusted third party in a blog post later.

Acknowledgment

The work covered in this blog post was made possible through the support of Handshake Micro Grants run by Eric Cederwall, aiming to assist new developers in entering the Handshake ecosystem.

Thank you Eric for your support and help.

Also, had it not been for [Matthew Zipkin], I might never have developed an interest in this field; a significant portion of what I’ve shared here is thanks to him. He came up with the original idea, and the plans to implement it.

Footnotes


  1. Feynman on What “Elementary” Means↩︎ ↩︎

  2. Like Verisign, or Donuts (now Identity Digital Inc.) which often abuse their position for profit or censorship. ↩︎

  3. Handshake adds a few covenants, and is Segwit-only. ↩︎

  4. It’s definitely possible to make a protocol that does not require making this compromise, but would require using zero-knowledge proofs like Zcash or Monero ↩︎

  5. It’s a little unclear from the webpage, but the whole book seems to be readable online. ↩︎

  6. A block is really just a signed and stamped set of transactions, done every 10 minutes. ↩︎

  7. With the exception of Coinbase transactions, where the money comes from seemingly “nothing”. ↩︎

  8. 1 HNS = 106 dollarydoos. ↩︎

  9. The length of the hash determines if a particular output is P2WPKH or P2WSH. A 20-byte long blake2b is used for P2WPKH, while a 32-byte long SHA3 is used for P2WSH. You can also use this to store 40 bytes of arbitrary information by setting the address version to 31, this is called the nulldata output. ↩︎

  10. It’s called the witness cause it can be discarded after transaction verification is done. saving space for node operators! This feature was added in Bitcoin’s SegWit soft fork. ↩︎

  11. On Bitcoin, it is also used to signal opt-in RBF. However, Handshake does not have any miners that support RBF as of writing this article. ↩︎

  12. PR#653 and PR#691 ↩︎ ↩︎

  13. Here Coins refer to Unspent Transaction Outputs. Note that these may be much higher valued than our lockup. ↩︎