/provable-names

Human readable names that commit to their content

Primary LanguageJavaScriptMIT LicenseMIT

Provable Name

A provable name is a name that commits to its underlying content with a certain cost associated with forging it. It allows for arbitrary content to be referred to by a human readable name.

For example, the bitcoin address "bc1q0pmqfjyc5ak7jac9z8jjky4g624uagsgwzx00d" has a provable name of "dish loan fabric". An individual could post this provable name to an untrusted 3rd party and then tell others they can be paid at "dish loan fabric". The paying person could ask the untrusted 3rd party for the bitcoin address and then verify the address is correct by checking the relative cost of forging the name. If the cost of forgery is high enough, the paying person can be confident that the address is the correct one the name was referring to.

Definitions

Document: Immutable text content that is being named.

Nonce: 32 random bytes encoded in hex used to generate a Full Name.

Full Name: The bip39 encoding of the digest of a Document combined with a Nonce (ex. "abandon abandon scheme start skull text subject shoulder sudden exclude credit wool anchor rural obscure village rule impact all text real clap any sting").

Name: Any prefix of a bip39 words in a Full Name (ex. "abandon abandon scheme start skull").

Short Name: A Name with all leading "abandon" words removed. (ex. "scheme start skull" is the short name of "abandon flower curve beauty abandon")

Cost: A property of a Name that refers to the amount of bip39 words in the name. A Name can be derived from a Short Name by prepending "abandon" words until the Cost is reached. Cost is an indicator of how much work is required to forge a name.

Name Generation

1. Generating a Full Name from a Document and Nonce.

A full name is a function of a document and a nonce. Because a nonce can be any random value, a document has many full names. The following is the algorithm for generating a full name from a document and nonce.

const crypto = require('crypto');
const bip39 = require('bip39');

const getFullName = (document, nonce) => {
    const getDocumentHash = document => {
        const hash = crypto.createHash('sha256');
        hash.update(document);
        return hash.digest();
    };
    
    const getCombinedHash = (documentHash, nonce) => {
        const combinedHash = crypto.createHash('sha256');
        combinedHash.update(documentHash);
        combinedHash.update(Buffer.from(nonce, 'hex'));
        return combinedHash.digest();
    };

    const documentHash = getDocumentHash(document);
    const combinedHash = getCombinedHash(documentHash, nonce);
    return bip39.entropyToMnemonic(combinedHash.toString('hex'));
};

const document = "bc1q0pmqfjyc5ak7jac9z8jjky4g624uagsgwzx00d";
const nonce = "0da0408c6704c59c01bfb2ad83edcfa9c5e62d9c5f84746a442d3dbc1d5d13e5"

const fullName = getFullName(document, nonce );
console.log(fullName); // 'abandon abandon dish loan fabric alter rack ankle allow special weather flip stick middle evidence rescue hybrid ostrich amateur improve tree piano art symptom'

2. Generating a Name from a Full Name.

A name is any prefix of a full name. A name can be generated by taking the first n words of a full name, where n is the cost of the name.

const getNameFromFullName = (fullName, cost) => {
    return fullName.split(" ").slice(0, cost).join(" ");
}

const fullName = "abandon abandon dish loan fabric alter rack ankle allow special weather flip stick middle evidence rescue hybrid ostrich amateur improve tree piano art symptom"
const cost = 5

const name = getNameFromFullName(fullName, cost);
console.log(name); // 'abandon abandon dish loan fabric'

3. Generating a Short Name from a Name.

A short name is a name with all leading "abandon" words removed ("abandon" is 00000000000 in bip39). Note that a short name with a cost has all the same properties as a name, it just has a different representation.

const getShortNameFromName = name => {
    const nameArray = name.split(" ");
    while (nameArray.length > 0 && nameArray[0] === "abandon") {
        nameArray.shift();
    }
    return nameArray.join(" ");
};

const name = "abandon abandon dish loan fabric"
const shortName = getShortName(name);
console.log(shortName); // 'dish loan fabric'

If you have a short name, you can convert it to a full name by prepending "abandon" words until the cost is reached.

const getNameFromShortName = (shortName, cost) => {
    const nameArray = shortName.split(" ");
    while (nameArray.length < cost) {
        nameArray.unshift("abandon");
    }
    return nameArray.join(" ");
};

const shortName = "dish loan fabric"
const cost = 5
const name = getNameFromShortName(shortName, cost);
console.log(name); // "abandon abandon dish loan fabric"

Verifying a Name

1. Verifying a name

To verify a name, you need to regenerate the name from the document, nonce, and cost. If the name matches the name you are trying to verify, then the name is a provable name for the document.

const isProvableNameForDocument = (name, document, cost, nonce) => {
    const nameForDoc = getName(document, nonce, cost);
    return nameForDoc === name;
};

const name = "abandon abandon dish loan fabric"
const cost = 5
const document = "bc1q0pmqfjyc5ak7jac9z8jjky4g624uagsgwzx00d";
const nonce = "0da0408c6704c59c01bfb2ad83edcfa9c5e62d9c5f84746a442d3dbc1d5d13e5"

const isProvableName = isProvableNameForDocument(name, document, cost, nonce);
console.log(isProvableName); // true

2. Verifying a short name

The same algorithm can be used to verify a short name. The short name can be converted to a name by prepending "abandon" words until the cost is reached. Then the name can be verified as above.

const isProvableShortNameForDocument = (shortName, document, cost, nonce) => {
    const name = getNameFromShortName(shortName, cost);
    return isProvableNameForDocument(name, document, cost, nonce);
};

const shortName = "dish loan fabric"
const cost = 5
const document = "bc1q0pmqfjyc5ak7jac9z8jjky4g624uagsgwzx00d";
const nonce = "0da0408c6704c59c01bfb2ad83edcfa9c5e62d9c5f84746a442d3dbc1d5d13e5"

const isProvableShortName = isProvableShortNameForDocument(shortName, document, cost, nonce);
console.log(isProvableShortName); // true


Explaining the cost of a name

The cost of a name is the length of bip39 words in the name. Each bip39 word has 11 bits of entropy. Since a hash function is random, the probability of another document+nonce combination having the same name is 1/2^(11*cost). Different uses of provable names will have different levels of security required. If the value of forging a name is $X, then the cost of the name should be set such that the cost of forging the name is greater than $X, making the attack unprofitable. It is important to note that if the attack is not targeting a single name, but rather any name, the attack becomes more profitable as the number of names increases.

Algorithm for choosing a desirable name

For human readable names it is preferrable to have names that are short and easy to remember. Because each document can have many names, it is possible to perform work up front in choosing a nonce that will generate a desirable name. The following is an example algorithm for choosing a nonce that will generate a desirable name.

const findDesirableName = (document, cost, desiredShortNameLength) => {
    while (true) {
        const nonce = crypto.randomBytes(32).toString('hex');
        const shortName = getShortName(document, nonce, cost);
        if (shortName.split(" ").length <= desiredShortNameLength) {
            return {
                document,
                nonce,
                cost,
                shortName
            };
        }
    }
}

const document = "bc1q0pmqfjyc5ak7jac9z8jjky4g624uagsgwzx00d";
const cost = 5;
const desiredShortNameLength = 3;
const {
    document,
    nonce,
    cost,
    shortName
} = findDesirableName(document, cost, desiredShortNameLength);