Number: HIP-0008
Title: Recoverable Bid Values
Type: Standards
Status: Draft
Authors: Fernando Falci <http://iamfernando/>
Matthew Zipkin <pinheadmz@gmail.com>
Created: 2021-07-08
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.
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.
||
denotes concatenation.
^
denotes bitwise XOR.
// 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 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;
}
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.
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.
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.
- Begin with the 32-byte
nameHash
.- Slice the
nameHash
into 8 chunks of 4 bytes. - Use the name auction's starting
height
as the 9th chunk. - Bitwise-AND each chunk with
0x7fffffff
.
- Slice the
- 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
. - Compute the Blake2b hash of
BID.address.hash || publicKey
. - Return the first 8 bytes of this hash. This is the
key
.
(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;
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.
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)
})
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.
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.