Handshake logo

HIP-0008: Recoverable Bid Values

Abstract

We propose a method for embedding encrypted bid values on the blockchain using nulldata outputs. Values are encrypted using a combination of a user’s private key and other data recoverable from the blockchain.

Motivation

The Handshake reference implementation (hsd) includes a wallet (hsw) that mostly conforms to BIP44. The goal of BIP44 is to allow a recovery process whereby a user could restore the complete state of their wallet using only their wallet’s master private key (usually encoded by a BIP39 seed phrase) and all the public data available on the blockchain.

If a user executes a BIP44 wallet recovery after they have placed a BID on a name auction but before the REVEAL, their wallet database will be missing the secret nonce which is used to blind the BID. The nonce is required by consensus rules to be included in the REVEAL covenant along with the raw bid value. Since REVEAL transactions are only valid for 10 days and failure to reveal results in permanent loss of funds, the incomplete recovery process may leave users in a dangerous state.

The nonce used in bids (and presented in the REVEAL) has no consensus restrictions except that it has to be 32 bytes. Users can use totally random data for the nonce or even 32 bytes of 0x00. They can re-use the same nonce forever if they want, but of course these options may pose extra challenges to software implementation, wallet recovery, or the secrecy of their bids.

hsw uses a deterministic algorithm to generate nonces. This way they can be stored in the database for optimal performance, but they can also be regenerated if all the inputs to the deterministic algorithm are available. Most of these inputs are available on the blockchain but unfortunately one datum is not: the value of the secret bid.

If a user attempts a BIP44 recovery with unrevealed bids on the blockchain, they will not be able to reveal those bids unless they remember the secret bid value they used in the first place. With 2,040,000,000,000,000 possible values, brute forcing the space is possible but certainly inconvenient!

Therefore, we introduce a method of storing the secret bid value on chain but encrypted in such a way that it can be recovered by the user using BIP44.

Background

|| denotes concatenation. ^ denotes bitwise XOR.

Consensus rules

// BID output:
  value     8 bytes
  address   var

  // covenant items:
  nameHash  32 bytes
  height    4 bytes
  rawName   var
  blind     32 bytes

// REVEAL output:
  value     8 bytes
  address   var

  // covenant items:
  nameHash  32 bytes
  height    4 bytes
  nonce     32 bytes

assert(BID.blind === Blake2b(BID.value || REVEAL.nonce))

hsw nonce generation algorithm

hsw generates a nonce based on the name being bid on, the secret value of the bid, and the address in the output with the BID covenant.

(pseudo-code)

generateNonce(BID, wallet) {
  const index = BID.value;
  const publicKey = wallet.account.derivePublicKeyAtIndex(index);
  const nonce = Blake2b(BID.address.hash || publicKey || BID.nameHash);
  return nonce;
}

nulldata address

Similar to the OP_RETURN opcode in Bitcoin, Handshake allows users to push arbitrary data onto the blockchain using a special type of address:

version: 31 (1 byte)
hash:       (2-40 bytes)

Standardness rules enforce a limit of only 1 nulldata output per transaction.

BIP32 path generation

Recall from BIP32 that keys are derived from a series of 4-byte indexes. Indexes lower than 0x7fffffff are derived using non-hardened derivation, which is required if an algorithm only has access to the public key.

Encrypting bid values for recovery

Since HNS values are always 8 bytes, there is no need for an encrypted value to be any other size (a 32-byte ciphertext doesn’t obfuscate anything about the value that an 8-byte ciphertext wouldn’t). We encrypt the bid value by generating a key from available data, and XORing that key with the bid value. The exact same algorithm is used to decrypt.

Generate key

  1. Begin with the 32-byte nameHash.
    1. Slice the nameHash into 8 chunks of 4 bytes.
    2. Use the name auction’s starting height as the 9th chunk.
    3. Bitwise-AND each chunk with 0x7fffffff.
  2. Starting with the wallet account’s master public key, derive a child public key using the path generated by the array of chunks in step 1. This is the publicKey.
  3. Compute the Blake2b hash of BID.address.hash || publicKey.
  4. Return the first 8 bytes of this hash. This is the key.

Example:

(pseudo-code)

const nameHash = 0x0000000011111111222222223333333344444444555555556666666677777777;
const height = 10000;
const bidAddress= {
  version: 0
  hash: 0x0123456789ABCDEF0123456789ABCDEF01234567
}
const bidValue = 10123456 // 10.123456 HNS

const publicKey = wallet.accountKey.derive(0x00000000)
                                   .derive(0x11111111)
                                   .derive(0x22222222)
                                   .derive(0x33333333)
                                   .derive(0x44444444)
                                   .derive(0x55555555)
                                   .derive(0x66666666)
                                   .derive(0x77777777)
                                   .derive(0x00002710); // height

const hash = Blake2b(bidAddress.hash || publicKey);
const key = hash.slice(0, 8);

const encryptedValue = bidValue ^ key;
const decryptedValue = encryptedValue ^ key;

Transaction structure

This protocol allows for a single transaction to include up to 5 BID outputs plus one single nulldata output that contains encrypted values for all bids in order in which they appear in the transaction itself. This maximum of 5 comes from the 40-byte limit of address hash data and the limit on nulldata outputs per transaction.

Example

Consider a transaction containing 5 BID outputs:

output[0] = bid0
output[1] = bid1
output[2] = bid2
output[3] = bid3
output[4] = bid4
output[5] = nulldata

The nulldata address in output[5] would be constructed like this:

new Address({
  version: 31,
  hash: encryptedValue(bid0) ||
        encryptedValue(bid1) || 
        encryptedValue(bid2) || 
        encryptedValue(bid3) || 
        encryptedValue(bid4)
})

Considerations

A wallet implementing this protocol MUST use a brand new address for every bid. Software that uses the same address for more than one bid on the same name auction will end up using the same key for more than one encrypted value, potentially leaking secret values.

This protocol SHOULD be made optional for users with a setting or flag. The same option MUST be passed to the software attempting to recover such a wallet. That wallet MUST check every transaction containing a BID covenant for the expected transaction structure (only contains 1-5 BIDs + one nulldata). When the encrypted metadata is discovered, the software SHOULD attempt to decrypt the secret value and then use its own generateNonce() function to verify that it has all the necessary data to generate a valid REVEAL.

Implementation

A simple exploration of the algorithms described here is implemented in a test for hsd in this branch: https://github.com/pinheadmz/hsd/blob/recoverable-bids1/test/wallet-recoverable-bids-test.js

Pending further review and feedback, a formal pull request will be written to add this feature as an option to hsd and hsw.


HIP:
0008
Status:
Draft
Type:
Standards
Created:
Thu, 08 Jul 2021
Last commit:
Mon, 13 Mar 2023
Authors:
  • Fernando Falci <http://iamfernando/>
  • Matthew Zipkin <pinheadmz@gmail.com>

Edit on GitHub