Skip to content

Bitcoin Faces Indexing #14

@whoabuddy

Description

@whoabuddy

whoabuddy-sha256-localized-250

After seeing a ton of new Bitcoin Faces on ord.io and the successful launch of bitcoinfaces.xyz this week, it's time to talk about indexing.

The goals here should be to identify:

  • what constitutes a Bitcoin Face as part of this collection
  • how to verify if a Bitcoin Face and given string match
  • how to verify first is first for minted Bitcoin Faces

Here we go!

Table of Contents

What is a Bitcoin Face?

A Bitcoin Face is a deterministically generated SVG image based on an input string.

A Bitcoin Face consists of:

  • an SVG file that contains references for each layer
  • for each layer, an image tag with href set to /content/{inscriptionId}

For example: Bitcoin Face for firstisfirst

first-is-first

<svg
  id="bitcoin-face-for-firstisfirst"
  width="100%"
  height="100%"
  viewBox="0 0 1025 1025"
  xmlns="http://www.w3.org/2000/svg"
  xmlns:xlink="http://www.w3.org/1999/xlink"
>
  <image
    id="background-1"
    xlink:href="/content/6c6b83ccf5db73fd371f0dcaa62fb607c58dc86c99b068982f9c28e112328781i0"
    x="0"
    y="0"
    width="100%"
    height="100%"
  ></image>
  <image
    id="body-1"
    xlink:href="/content/4891fc56d297684275f904cbd5747537d02f3c8fefe5731d3f2797cc28589b31i0"
    x="0"
    y="0"
    width="100%"
    height="100%"
  ></image>
  <image
    id="head-1"
    xlink:href="/content/b88b81635faad19f654f28fd5502c4a5008a073cb24529568d7bc9e9f6584420i0"
    x="0"
    y="0"
    width="100%"
    height="100%"
  ></image>
  <image
    id="ears-1"
    xlink:href="/content/49f8014d388de06928f604d2ac6eb7380d2fcfa441570ce96004e62da21945b5i0"
    x="0"
    y="0"
    width="100%"
    height="100%"
  ></image>
  <image
    id="mouth-1"
    xlink:href="/content/f46333c7aae731f501743274b0b73f59c1863233a2948a4d42ca580b4a315600i0"
    x="0"
    y="0"
    width="100%"
    height="100%"
  ></image>
  <image
    id="nose-1"
    xlink:href="/content/87191d4187f614618f37629169f5b552bdaa9a2aac4b226e693885023a181d7di0"
    x="0"
    y="0"
    width="100%"
    height="100%"
  ></image>
  <image
    id="eyes-1"
    xlink:href="/content/4bd9a298dd80421f63753d25a3316753f9bd38a4a1eb57e2b1175ff20ea8ee93i0"
    x="0"
    y="0"
    width="100%"
    height="100%"
  ></image>
</svg>

What are the layers?

The layers are based on the attributes for each Bitcoin Face, which includes:

  • 7 required layers
  • 4 optional layers
  • 3 eye types
export const REQUIRED_LAYERS = [
  "background",
  "body",
  "head",
  "ears",
  "eyes",
  "nose",
  "mouth",
] as const;
export const OPTIONAL_LAYERS = ["chain", "earring", "glasses", "hat"] as const;
export const EYE_TYPES = ["normal", "laser", "starburst"] as const;

The layers are defined in this constant on GitHub:

// inscribed attributes
export const INSCRIBED_ATTRIBUTES: LayerAttributes = {
background: [
"c8f8e2e179fcbec4624d52b9118349bc01414a839e01e399a6ccfa42ce1c150ai0", // background-1
"8723751cfc76f030dcd8c7be883fc0ab95eec8a4274f55caf00d20f151fbfe64i0", // background-2
"3b3d044b82de333e8310c2df1d55e02936ae1e5d16966ce86e9269904b50da31i0", // background-3
"327e7279dc7a69d2a28bafcc7d215511a8292f5b9514ab57756d57d8cb385463i0", // background-4
"b1d6a465edc6bf876e1f67adb6951e7041168ca2686cc0e970901a7fce59b044i0", // background-5
"e4e62d2d1f7d0c8a16b8ed5f832f08ed12c60903723d2c091189156c2289f4cai0", // background-6
"d93d0b543ed765906e882da47cac7969efcb9930abe5495e52b1c777675aac32i0", // background-7
"2c82987ab59df2077bd9b2522f823155cc3ca306a14094c1ce3ca94ad13bb915i0", // background-8
"6c6b83ccf5db73fd371f0dcaa62fb607c58dc86c99b068982f9c28e112328781i0", // background-9
],
body: [
"e12c3c1efb548de352387fed30413fc07ae27137ce9d81b9e0b06bd1f67655e2i0", // body-1
"0991d1034aa2f40b13b82774f9b6faccee23b037ad352b5edcca27e8cb05bde9i0", // body-2
"703b6358a3ae55f190a653cd51bcca5251b85cb99a2c5d5b130f1285df33c0afi0", // body-3
"0f478d4ea74e1fb8ddfd73b36d9f89a6c8f9fdd05192b0d817f372c2deecc4d5i0", // body-4
"8f27e473312d9442441fd2cc30b328400517648cce605701411f6ad00b114e58i0", // body-5
"4891fc56d297684275f904cbd5747537d02f3c8fefe5731d3f2797cc28589b31i0", // body-6
"d31c63df52207f90f969688b147c31492fc697bec3b2f7367e02c16e0004a0d4i0", // body-7
"7bde2d6715aaa157ee7f92259d8c966c63d7033e25e5d4a6aef806337412c616i0", // body-8
],
head: [
"b88b81635faad19f654f28fd5502c4a5008a073cb24529568d7bc9e9f6584420i0", // head-1
"14fead64cb0dfbd58addb5537a567aa643606d3a7989e92431e37072421f552ai0", // head-2
"bd098d6bc951530e66fdd0847ff1cc4ce0a61cf19b930e584f87d4e7f7820bfai0", // head-3
"f08ab33010681eaacee2200d6403de61cc68e7fa3ab97afc33452e47879512ddi0", // head-4
"7b8de69d565b4d69b4232fc76ed029acb82bce05f0dd65a72da3027cd6f374aai0", // head-5
"a34c2814fdbf1667f7dc10a891b3613d53595863b1f286afdbd94084c8964608i0", // head-6
"57dec6b488d4a9c216193fa286b9eb4a70efe550fc08a4a4e791c27736088ad4i0", // head-7
// head-8 intentionally missing, too many embedded images
"1bb84b7b32be7acd53ff29c21f8c3254fc557cb9dcc7df9a72bc9e4ed021ab19i0", // head-9
"c029f0afeea66091a573fba60dbe3e5e7301828dc520610a9eae120364a6b5d0i0", // head-10
"fa4e61a854c673a223dda13ec8b934db01069feff6652134824e2d2c6913febci0", // head-11
"f0b78ca9093ca8eccab4a521a1691d640a20ad8caa8083d1f329420c4964b0c1i0", // head-12
],
ears: [
"49f8014d388de06928f604d2ac6eb7380d2fcfa441570ce96004e62da21945b5i0", // ears-1
"94dcdf1067cde2ac108d41d82bf6ab06fe08f7985572a57bf9c24a9d13baa77bi0", // ears-2
"cbc7d3b7a0f01aee2bb7474bb74bc87ff3969e65f3761b8d92412f2942ca95d7i0", // ears-3
"cf392227692106ca35c1d02b0f65dafbef129e3c42f568ecf4845dcd386aae30i0", // ears-4
"917d6ef4102d95122adcceb1482b78545d4bb13ff835c109c2016a5ff919483ei0", // ears-5
"d2b74f9ba8e9af35ae7b1ac8e2f9a91aaa46a1cad971ed2255ec3e8f55fe25eci0", // ears-6
"f20b28f2e7bbb7af2dc727c860161d9c56725b7b5d89c3ccc50790f87fdbf38ci0", // ears-7
],
nose: [
"b439feeb2356b77bdc63617dc8a3437b0cbbe4d261b8f2041eb4d81bfec92da7i0", // nose-1
"3e98ea13246556fdf97bbff57b88a764d3e3a5c27de900877fb42f4c6b23c6f8i0", // nose-2
"87191d4187f614618f37629169f5b552bdaa9a2aac4b226e693885023a181d7di0", // nose-3
"25fda933bd0e592c486bfa880e50970178a048c6df5c50b1741a2f1d6585cad7i0", // nose-4
"8845fd90d6d13c76b59c4c769104160eac451f478cfd4efdf3b40ca8fb024c7ai0", // nose-5
"86e63c18bd14c72f6ff877e587818e626df2c80627e36b3db9f70fac507f842ei0", // nose-6
],
mouth: [
"b1be0eefa03bb914c7d7f32ae613b4a4f0be3b1c6d9439e211488269d85b3e86i0", // mouth-1
"2508ea3801a435aade9f998c8e600b17d6f1695a3e9f2f892175785c73e2ab8di0", // mouth-2
"f46333c7aae731f501743274b0b73f59c1863233a2948a4d42ca580b4a315600i0", // mouth-3
"7b16e48fbc6658a6646b660d50af0f955abaa37bb1b8f0532ba53158bf71a6dbi0", // mouth-4
"fe59c820ccaf747c17e3fbbd88d5050b347de35481dfac1522506116f7175868i0", // mouth-5
"fbb61aeca3fe079da78cf701b56150c63e83014eb07f5ad834692aca5f0ca3aei0", // mouth-6
"4e7f5ec1f5b9d7329436a4221daed1bdd75becefd4f4c082b05daaf0e9ff6566i0", // mouth-7
"59efa6f17a8c331e9439ddce026fa058754f95f3a1e02d534f6e7f2ef89c15afi0", // mouth-8
"f3216c3ef151c8c2814f1ab675ff7cb3f85b612beffb98d8e5a1a7ab4b51d5bei0", // mouth-9
],
chain: [
"6ec67f9a3061fc866c3cbcd9c1a30b2ae1c9e3c20f486bd689d3e3886bbf725di0", // face-accessory-1
],
earring: [
"62b558e9d4000390aab0e06dde66b6671c6e473d033724f9db31c46536642f9fi0", // earrings-1
],
hat: [
"853db99e14d11f5abd62bd621b488bc507a887bf441bef1a1c773c2f78b70d2fi0", // hat-1
"f582a86538f754f1d41f3a91106eaf7c1b2b645988cd786a5b6dae4442053b5ei0", // hat-2
"807f95d11e688a31a7769ed902bde7f71bea2983e17e32b5e98a2f2d1e186637i0", // hat-3
"5e47b9e95cadf202b5c51f0e40851c63b74b57eaf4392102a0198927bd7b9325i0", // hat-4
"d65bbbaf2f1e193492c801c33066b19953a36248acf50d3b52e581a13ee43c88i0", // hat-5
],
eyes: {
normal: [
"9dd4f5aa1287ddb695eb4b2381f58dd7a6e8706ed1e7f376ed18535b81923e1bi0", // eyes-1
"1af1acdf4834c0c8aa73c4c6dc44c30df3213bf95d7c08c9bc51fb1bd1084febi0", // eyes-2
"a730de0bf8343fbf21abc2376d83359ca1a6afe23614a00b2046902a8b11df02i0", // eyes-3
"cca92c88e93d5bbcc98c63c26d9e2b02df76a23f8d85e91ef3a888d62f86ab27i0", // eyes-4
"4401e04f9748fcfa0e07347f88b1d3ac3fa8ee3f5401cc1cc7dc3d49471ac40ei0", // eyes-5
"629079513bdae4bc955fbb309fdbc8092ed4761f43e2c46144cc8cb04644921bi0", // eyes-6
"616a8c9ee80d36e5bc2bf214c2b0729176d9f2dc0187ad8d72cbd5ff2311d204i0", // eyes-7
"2cde18753b4ea17923dffce90e369039351070b433fd1b15eab6ef31587d3fe0i0", // eyes-8
"2630332bbc0bc3bc1b17b4be225ba41ae58dae5cb74cbf33f150182363f13d02i0", // eyes-9
],
laser: [
"9e7cd2891721515bfc8cd59240090e385078630e3ce1c3a907011cffd374855di0", // eyes-6-laser-eyes-3
"544056931898458410473610eaef98c5cf6b1fe2c696f22ef65aed77a4445a39i0", // eyes-4-laser-eyes-3
"0128d17f0c0dd2cc14acbb029ee15d22757b38ab30cf4ff4ce9afb837da49a85i0", // eyes-2-laser-eyes-2
"612f727425302b50b30e84fb54fdcd17ebc58649b3583c39c5b12828ab907312i0", // eyes-7-laser-eyes-2
"4e98536baf44cd5aa85f994935f9b25443bb51059f54b7ed6cc43d009e123834i0", // eyes-9-laser-eyes-3
"a2013a8b7bce76e108e60a0338efeaf35d1eb4f1499e2dae071eff9266d3c2d2i0", // eyes-1-laser-eyes-4
"554d0583f70b024c754bc22f529822c6f69b7a4dee6d7894fff0d10bf4069592i0", // eyes-6-laser-eyes-4
"459b18ab7d5cf0feec4f05a07ed54ba6c3e9c0a2e9c081b85557fbc7d404dfe1i0", // eyes-8-laser-eyes-2
"192e54e66ac1559f6f596e68a491fb423a95e38bc275029f9afb642658eb7e4ci0", // eyes-3-laser-eyes-4
"8122b055864f80e3362803cb8feb170dc879969b5819a7f5d8abe7a39718faa9i0", // eyes-5-laser-eyes-2
"b3f04261d6035985227421d00eb4c9ab83bb2a246d5d3d785089d22a1fe258bai0", // eyes-4-laser-eyes-4
"beb52dea3c1696b27cf17cbc87896b03f42b77c1d659e336619b6cf2eed9fdd1i0", // eyes-3-laser-eyes-3
"c68476a8d53ce950475ad4044df16087f0dddaf2af244e190b6b288ec88c7038i0", // eyes-1-laser-eyes-3
"c897eb21979fe6a9e5c7b8cb04713478082dce92b8c6f2a348c23f16542aea07i0", // eyes-9-laser-eyes-4
],
starburst: [
"98e99ca432e19dea6ba035d68501204549c4051911182ac689baba65727b2c89i0", // eyes-6-starburst-laser-eyes-4
"71551f31f2dd84dddf4f977f45dda483f42fc03ead17c4e79a4ea0ac4df4c5bai0", // eyes-5-starburst-laser-eyes-2
"d0caf0b8fc41b8fa2adaef05eff6f04d58ca2c1dab967be9bcc449c087ef3cd2i0", // eyes-1-starburst-laser-eyes-3
"81d57836b6a3485d5d39c7c083f1ed829ddd92aabd309c9b752e6c87e37a0531i0", // eyes-8-starburst-laser-eyes-2
"d4c469f2902ede17d1907c167db721a3f0e3cfef7a8305333a5fcb6db343aa12i0", // eyes-4-starburst-laser-eyes-3
"c099d0562e0aee188fffb864dba2f5ef066de8a5524e8d951c30dc9a48525357i0", // eyes-7-starburst-laser-eyes-2
"d99bd2b865dadf20b59c231a4afc738e995752027592aa53e125aa8825b5884di0", // eyes-3-starburst-laser-eyes-4
"eff313023bdf091a672e72c94b4c200b47514871520c3cdd1fb90d151362dd65i0", // eyes-2-starburst-laser-eyes-1
"4bd9a298dd80421f63753d25a3316753f9bd38a4a1eb57e2b1175ff20ea8ee93i0", // eyes-9-starburst-only
],
},
glasses: [
"51318d541ccc19345873152f5804171942e9a088bb19f2ff6ea7ac8977dbc7ebi0", // glasses-1
"c31ebd0d68e065e640f37365ea912fd8d40ad260d964d2f02c8db7016913cc8ai0", // glasses-2
"1dc0482c2595e4e4dc5aedc73165c9efce0a12735933e9bb171de91b8ca5d0b8i0", // glasses-3
],
};

Inscriptions

The layers are inscribed at the address below:

bc1pr7znvmc9drttp5d2hwuexxaggm9tk594nq8gptqltvfqendzfh4sthsml4

Layer Order

Layer order is defined in the layer creation process:

const order: Layers[] = [
  "background",
  "body",
  "head",
  "ears",
  "chain",
  "mouth",
  "nose",
  "hat",
  "eyes",
];

How a Bitcoin Face is created

The generator for Bitcoin Faces currently takes the input string and goes through the following steps:

Normalization

Transforms the input string into a standardized format by decoding URI components, converting to lowercase, and removing leading/trailing whitespaces. This ensures consistency before hashing.

const normalizedName = decodeURIComponent(name).toLowerCase().trim();

Hashing

Converts the normalized string into a hash array using SHA-256. The string is converted to a Uint8Array which is provided to SHA-256 to produce a hash, and the result is returned as a new Uint8Array.

const encoder = new TextEncoder();
const data = encoder.encode(input);
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
const hashArray = Array.from(new Uint8Array(hashBuffer));

Layer Selection

Selects face layers from a list of all possible combinatinos, based on hash array, by iterating over attributes using hash values.

// calculates layer index
function calculateLayerIndex(
  hashArray: number[],
  hashIndex: number,
  length: number
): number {
  return hashArray[hashIndex % hashArray.length] % length;
}

Create Layers

Iterates over selected layers to create <image /> tags with links to /content/inscriptionId.

return order
  .map((key: Layers) => {
    const value = layers[key];
    if (value === undefined) return "";
    if (onchain) {
      return `<image id="${key}-1" xlink:href="${host}/${value}" x="0" y="0" width="100%" height="100%"></image>`;
    }
    return value;
  })
  .filter((str) => str !== "")
  .join("\n");

Create SVG

Wraps <image /> tags in final SVG format.

return `<svg id="bitcoin-face-for-${name}" width="${width}" height="${height}" viewBox="0 0 1025 1025" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">\n${svgLayers}\n</svg>`;

Parsing the SVG code

Identifying as part of the collection

SVG attribute tags do not have a defined order, and therefore some parsing is required in order to consistently identify if a Bitcoin Face SVG file is part of the collection.

For example, the API uses a local version of the SVG files to serve quick responses, and there is a test that makes sure it matches what's on-chain bytaking the inscription ID, pulling the SVG from an API, then comparing the local SVG content to it.

In order to do this, the SVG image code must be normalized and returned as a string, which is done by this function:

const cleanedSVG = data
  .replace(/>\s+/g, ">")
  .replace(/\s+</g, "<")
  .replace(/\s+/g, " ")
  .trim();

If we take that a step further, we could strip down one of these SVGs to a list of it's layer hashes, e.g. for firstisfirst:

const firstisfirstContentHashes = [
  "6c6b83ccf5db73fd371f0dcaa62fb607c58dc86c99b068982f9c28e112328781", // "background-9"
  "4891fc56d297684275f904cbd5747537d02f3c8fefe5731d3f2797cc28589b31", // "body-6"
  "b88b81635faad19f654f28fd5502c4a5008a073cb24529568d7bc9e9f6584420", // "head-1"
  "49f8014d388de06928f604d2ac6eb7380d2fcfa441570ce96004e62da21945b5", // "ears-1"
  "f46333c7aae731f501743274b0b73f59c1863233a2948a4d42ca580b4a315600", // "nose-3"
  "87191d4187f614618f37629169f5b552bdaa9a2aac4b226e693885023a181d7d", // "mouth-3"
  "4bd9a298dd80421f63753d25a3316753f9bd38a4a1eb57e2b1175ff20ea8ee93", // "eyes-starburst-9"
];

As long as the array of hashes (or "recursive content") matches the available hashes in the collection, then we know it's a valid Bitcoin Faces SVG.

If we hash the content hashes, we have a single string that we can verify a Bitcoin Face against and use as it's identity.

Identifying first is first

Considering the approach above for identifying a Bitcoin Face from the collection, determining first is first would require:

  1. Detecting Bitcoin Face SVGs from inscription content
  2. Recording the order of Bitcoin Face SVG inscriptions

This approach is favorable to just a hash of the content, considering a valid (but minimal) version of the SVG could be:

<svg>
  <image
    id="background-1"
    xlink:href="/content/6c6b83ccf5db73fd371f0dcaa62fb607c58dc86c99b068982f9c28e112328781i0"
  ></image>
  <image
    id="body-1"
    xlink:href="/content/4891fc56d297684275f904cbd5747537d02f3c8fefe5731d3f2797cc28589b31i0"
  ></image>
  <image
    id="head-1"
    xlink:href="/content/b88b81635faad19f654f28fd5502c4a5008a073cb24529568d7bc9e9f6584420i0"
  ></image>
  <image
    id="ears-1"
    xlink:href="/content/49f8014d388de06928f604d2ac6eb7380d2fcfa441570ce96004e62da21945b5i0"
  ></image>
  <image
    id="mouth-1"
    xlink:href="/content/f46333c7aae731f501743274b0b73f59c1863233a2948a4d42ca580b4a315600i0"
  ></image>
  <image
    xlink:href="/content/87191d4187f614618f37629169f5b552bdaa9a2aac4b226e693885023a181d7di0"
  ></image>
  <image
    xlink:href="/content/4bd9a298dd80421f63753d25a3316753f9bd38a4a1eb57e2b1175ff20ea8ee93i0"
  ></image>
</svg>

There can also be discrepencies between the tag attribute order, depending on image compression/optimization software commonly used by inscription services. An SVG <use> tag could be implemented instead of <image>.

Creating the Standard

The outline above would allow us to be as inclusive as possible while still supporting anything people want to create with the layers already inscribed.

Would really love to hear from @bruffstar @jnapp18 or anyone else closer to indexing/verification/minting.


Note

We welcome input from anyone who wants to get involved! We want to identify standards that will apply to marketplaces, service providers, and any other potential integrations for Bitcoin Faces.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions