Support IPLD Contenthash

Note: This is PoC/draft for feedback…

Intro

*Tldr: IPLD is linked json/data stored in IPFS.

IPLD is InterPlanetary Linked Data, “IPLD is the data model of the content-addressable web. It allows us to treat all hash-linked data structures as subsets of a unified information space, unifying all data models that link data with hashes as instances of IPLD”.

It’s one of core features in IPFS clients to store linked json/data as DAG. ENS already supports IPFS and IPNS, supporting IPLD contenthash doesn’t require any major changes in ENS compatible wallet and services.

Use Cases

  • Static data storage providers
  • IPLD as CCIP gateways
  • Dynamic data storage with IPNS and CCIP.

Compatibility issues

  • IPNS technically supports IPLD. We can publish IPLD hash in IPNS but it doesn’t resolve like IPFS hash set in IPNS. ?Possibly a bug in IPFS clients/not implemented yet.
  • IPLD returns JSON/data as plaintext without proper headers

Example

e.g, ipldx.eth generates an IPLD contenthash and set that on ipldx.eth.

ipld hash : bafyreiggi4ce7wy7eya4smhdewu4dur6hvtemfh2uhlahzq3ir3ltr6u3i
contenthash : e20101711220c647044fdb1f2601c930e325a9c1d23e3d664614faa1d603e61b4476b9c7d4da

{
  "ens":"ipldx.eth",
  "index":{"/":"bafyreict4bntpopkmecqztpflnioiur33ltbjpemlqnyhq5w3g66x44jlm"},
  "ipns":{"/":"bafyreidwkly54277n6j2mgxz4z7khp27elk4zursn7dfxcvsimyxvvd63e"},
  "namehash":{"/":"bafyreiek2zgr3lxz3sivi3doyfubpymwqh3qaxbcxbixcas2r4ecw4b6ru"}
}
"ipldx.eth"
[
  "/ipfs/QmTQU81h63qA7KocVtEEjsqLsua1AGVe2ff3sw5EJbW4Mm", 
  //....
]
"/ipfs/QmTQU81h63qA7KocVtEEjsqLsua1AGVe2ff3sw5EJbW4Mm"

js-ipfs basic example,

Note: might not resolve globally with latest out-of-box js-ipfs client after recent breaking changes.

  let _post = await ipfs.dag.put({
    title: 'DAOs are not corporations: where decentralization in autonomous organizations matters',
    published: 1663635754,
    author: "vitalik.eth",  
    address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
    //...
    tags: ["DAO", "DEFI", "BDFL"],
    url: "https://vitalik.eth.casa/general/2022/09/20/daos.html",
    preview: "Recently, there has been a lot of discourse around the idea that highly decentralized DAOs do not work, and DAO governance should start to more closely resemble that of traditional corporations in order to remain competitive....."
  })
  console.log("ipld://" + _ipld.toString());
  //> ipld://bafyreidive2wt564rh254vxuy7rqtgllt3eaautla6wbn6evpievgzffou
6 Likes

I’m not sure why this hasn’t been acknowledged or commented on.

Is this something that maybe can be more efficiently addressed by creating a ticket in Discord?

2 Likes

Very happy to support more multiformats - but can you clarify what concrete change you are proposing here?

2 Likes
  1. ENS apps and records manager should support IPLD contenthash starting with ipld://<hash> and publish contenthash with 0xe2.. namespace.

  2. ENS gateways like @ethlimo.eth should resolve that 0xe2.. namespace for example.eth.limo/ with plaintext/json data from <ipfs_api>/api/v0/dag/get?arg=<base32-cidv-1>/<path>

It good enough for static IPLD contenthash. Latest Ipfs clients and IPNS service providers can publish ipld://hash in ipns://hash but they still expect ipns to resolve to ipfs only. If that’s fixable issues with IPFS clients/providers it’ll open web3 stack with dynamic & gasless linked data storage. It’s lot easier to read, link and update IPLD DAGs compared to IPFS directory with json file.

  • Note : IPFS is specific type of IPLD with files and directory instead of linked json/data.
/**
* https://github.com/multiformats/multicodec/blob/master/table.csv#L44
* 44    dag-pb	      ipld	0x70	permanent	MerkleDAG protobuf
* 45    dag-cbor      ipld	0x71	permanent	MerkleDAG cbor
* 46    libp2p-key    ipld	0x72	permanent	Libp2p Public Key
*
* https://github.com/multiformats/multicodec/blob/master/table.csv#L85
* 85    ipld	namespace	0xe2	draft	IPLD path
* 86    ipfs	namespace	0xe3	draft	IPFS path
* 87    ipns	namespace	0xe5	draft	IPNS path
*
* 0x72 = IPNS, 0xe5010172....
* 0x71 = IPFS/?IPLD, 0xe2010171..
* 0x70 = IPFS, 0xe3010170..
*/

:pray:

3 Likes

Support it how exactly?

Would this be stored in the same contenthash field as is currently used for IPFS sites?

Can you write up a draft ENSIP that spells this out in detail?

1 Like

Yes & Yes, give us 1+ week :pray:
IPLD contenthash use same contenthash field as IPFS. ipfs:// is prefixed with 0xe3 namespace while ipld:// is 0xe2 prefixed namespace according to multiformats/multicodec. I’ve checked latest go-ipfs “kubo” codes and test files for IPFS and IPLD published under IPNS, all works with IPNS+IPLD.

We’ll write more about that in draft ENSIP soon. it’s not so different from ipfs/ipns contenthash at surface. This is our short list of solutions and problems for now…

IPLD is data container format so Dapps are free to use their own IPLD schemas and decide how they read/write IPLD hash pointing to their *.domain.eth. We’re still exploring more ideas for how clients/services could handle basic plaintext/data uris, and different contenthash pointers linked deep inside IPLD, while also allowing public eth gateways to always fallback to plaintext/json.

There’s plaintextv2. -- multiaddr -- 0x706c61--- draft in multicodec for plaintext, but we could simplify that for ENS contenthash by using ERC721’s tokenuri like string as bytes(…data).

Plaintext/data uris specs are required for clients/ENS apps to resolve deeply linked json/data, so it’d be good idea to work on ENSIP for "plaintext/data URIs in contenthash" before IPLD related ENSIP to recursively use same specs under IPLD. Tbh, that’s one feature stopping our deeper ENS+ETH integration.

  • example :
    <0xaddr>.balanceof.dai.uniswap.eth API could read that balance from on-chain dai contract and return contenthash as bytes("data:application/json;...balance" ).

We already have that token data and ENS resolver on-chain but we use off-chain contenthash and then connect to another service provider/wallet APIs to read that balance. I’m bit surprised by unnecessary +2x loop in core ENS feature.

Here’s some IPLD examples/test files to explore.

One more bit old half-baked idea around namehash+ipld to store preimage.eth in IPLD using namehash as hashing algo. Add ENS Namehash by 0xc0de4c0ffee · Pull Request #184 · multiformats/multicodec · GitHub

&& sorry for bit late reply, I was buidling an IPFS gateway using ENS+CCIP only *.ipfs2.eth, before ens small grants deadline… It’s using all of our CCIP tricks that we’re trying to mix with IPNS+IPLD.

  • updated link
3 Likes

:pray: Sorry for late party…
We’re simplifying our design with IPNS+IPLD and IPNS+IPFS support so we can skip direct ENS+IPLD support for now…

tldr; ENS records for domain.eth is stored in gateway.tld/ipns/f<hash>/.well-known/eth/domain/<record>.json for CCIP read.

It’s fully backwards compatible without any breaking changes to active ENSIPs or contenthash format, it works for both IPNS+IPFS and IPNS+IPLD.

IPFS directory to IPLD json format,
example :

{
    ".well-known": {
        "eth": {
            "domain": {
                "contenthash.json": {
                    "data": "<abi.encode(contenthash)>"
                },
                "_addr": {
                    "60.json": {
                        "data": "<abi.encode(eth_address)>"
                    }
                },
                "avatar.json": {
                    "data": "<abi.encode(string('eip155:1/erc1155:0xb32979486938aa9694bfc898f35dbed459f44424/10063')>"
                }
            }
        }
    }
}

Off-chain records Manager/Resolver contract (WIP),

still missing final signature format/validation.

DNS decode

    uint256 index = 1; // domain level index
    uint256 i = 1; // counter
    uint256 len = uint8(bytes1(name[:1])); // length of label
    bytes[] memory _labels = new bytes[](42); // maximum 42 allowed levels in sub.sub...domain.eth
    _labels[0] = name[1:i += len];
    string memory _path = string(_labels[0]); // suffix after '/.well-known/'
    string memory _domain = _path; // full domain as string

    /// @dev DNSDecode()
    while (name[i] > 0x0) {
        len = uint8(bytes1(name[i:++i]));
        _labels[index] = name[i:i += len];
        _domain = string.concat(_domain, ".", string(_labels[index]));
        _path = string.concat(string(_labels[index]), "/", _path);
        ++index;
    }

    // check if the name contains .eth as root
    // bool dotETH = (keccak256(abi.encodePacked(bytes32(0), keccak256(_labels[index - 1]))) == roothash);

    bytes32 _node;
    bytes32 _namehash = keccak256(abi.encodePacked(bytes32(0), keccak256(bytes(_labels[--index])))); // MUST be equal to roothash of '.eth'
    bytes memory _ipns; // contenthash
    while (index > 0) {
        _namehash = keccak256(abi.encodePacked(_namehash, keccak256(bytes(_labels[--index]))));
        if (contenthash[_namehash].length != 0) {
            _ipns = contenthash[_namehash];
            _node = _namehash;
        }
    }
    
    // require(_node == bytes32(data[4:36]), "BAD_NAMEHASH");

bytes4 selector to file name:

    bytes4 func = bytes4(data[:4]);

    string memory _pathJSON;

    if (bytes(funcToFile[func]).length > 0) {
        _pathJSON = funcToFile[func];
    } else if (func == iResolver.text.selector) {
        _pathJSON = abi.decode(data[36:], (string));
    } else if (func == iOverloadResolver.addr.selector) {
        _pathJSON = string.concat("_addr/", uintToNumString(abi.decode(data[36:], (uint256))));
    } else if (func == iResolver.interfaceImplementer.selector) {
        _pathJSON =
            string.concat("_interface/", bytes2HexString(abi.encodePacked(abi.decode(data[36:], (bytes4))), 0));
    } else if (func == iResolver.ABI.selector) {
        // recheck this
        _pathJSON = string.concat("_abi/", uintToNumString(abi.decode(data[36:], (uint256))));
    } else if (func == iResolver.dnsRecord.selector) {
        (bytes32 _name, uint16 resource) = abi.decode(data[36:], (bytes32, uint16));
        _pathJSON =
            string.concat("_dns/", bytes2HexString(abi.encodePacked(_name), 0), "/", uintToNumString(resource));
    } else {
        revert NotImplemented(func);
    }

IPNS Keys :

Deterministic IPNS keys are generated using domain info, owner info text as part of deterministic signature request (SIWx) *we’ve issues with ABNF URI validation for DApps with multi-entry points as SIWx requires fixed URI, so it’s normal signature request instead of full SIWx feature in wallets.

It’s part of SIWx + HKDF that we’re working on to remix all ENS, ETH/L2s, Nostr & IPFS/IPNS as alternative to Whisper (shh) messaging protocol, e.g., our https://dostr.eth.limo using SIWx in Nostr Client to generate deterministic schnorr/secp256k1 keys.

2 Likes