IPFS文件地址链上存储

IPFS 是什么

IPFS(InterPlanetary File System,星际文件系统)是永久的、去中心化保存和共享文件的方法,这是一种内容可寻址、版本化、点对点超媒体的分布式协议。
内容可寻址:通过文件内容生成唯一哈希值来标识文件,而不是通过文件保存位置来标识。相同内容的文件在系统中只会存在一份,节约存储空间
版本化:可追溯文件修改历史
点对点超媒体:P2P 保存各种各样类型的数据
可以把 IPFS 想象成所有文件数据是在同一个 BitTorrent 群并且通过同一个 Git 仓库存取。
总之,它集一些成功系统(分布式哈希表、BitTorrent、Git、自认证文件系统)的优势于一身,是一套很厉害的文件存取系统。

区块链和IPFS的结合

大家都知道区块链的数据是可以永久保存的,但是如果在区块链上存储大量的数据是非常昂贵的。如果结合IPFS,我们可以在IPFS以文件的形式存储数据并把文件的地址保存在区块链上,这样就可以做到大量数据的永久存储并可以有效追踪。当然再结合区块链上的智能合约就可以发挥更大的想象力了。

IPFS地址分析

为了在链上存储IPFS的文件地址,我们对IPFS的文件地址做一下简单的分析。由于IPFS的数据块大小为256字节,因此如果文件大小超过此大小会被拆分为多个数据块。多个数据块要涉及到Merkle DAG(Directed Acyclic Graph) 默克有向无环图,相对复杂一点,后续的文章再做分析。这里我们简单分析一下单数据区块的情况。
IPFS的源码分离出部分代码,可以生成文件的IPFS地址

function ipfsHash(filePath) {
    var buffer = fs.readFileSync(filePath);
    const unixFs = new Unixfs('file', buffer);
    DAGNode.create(unixFs.marshal(), (err, dagNode) => {
        let json = dagNode.toJSON();
        console.log("File:0x" + buffer.toString('hex'));
        console.log("UnixFs:0x" + unixFs.marshal().toString('hex'));
        console.log("Header+UnixFS:0x" + dagNode.serialized.toString('hex'));
        console.log("Multihash:0x" + dagNode.multihash.toString('hex'));
        console.log("Address:" + json.multihash);
        console.log("---------------------------------------------------------------------");
    });
}
ipfsHash('/Users/Kirn/Documents/Workspace/Dawn/ethereum/assets/test.txt');

File:0x310a320a330a340a350a360a370a0a
UnixFs:0x0802120f310a320a330a340a350a360a370a0a180f
Header+UnixFS:0x0a150802120f310a320a330a340a350a360a370a0a180f
Multihash:0x1220a1001394f749d9a0c5f27761b2f08e9432ce215dad6f01dbe26e468857169cbb
Address:QmZB8R7T5xvKJDUJ6pXtUym6frQx1r6bQPcwquR1rtGHL6

为了能更清楚的了解IPFS的地址构成我们把整个构造过程拆解一下

function customHash(filePath) {
    // 读取文件Buffer
    var buffer = fs.readFileSync(filePath);

    // 转为Unix File System
    const unixFs = new Unixfs('file', buffer).marshal();

    // 添加tag
    let tag = Buffer.from([10])
    
    // 添加File Size
    let size = Buffer.from([unixFs.length]);
    var newBuffer = Buffer.concat([tag, size, unixFs]);

    // sha2-256
    let sha256 = crypto.createHash('sha256').update(newBuffer).digest();

    // multihash
    let multihash = multihashes.encode(sha256, 'sha2-256');

    // base58
    let base58 = bs58.encode(multihash).toString('hex');
    console.log("File:0x" + buffer.toString('hex'));
    console.log("UnixFs:0x" + unixFs.toString('hex'));
    console.log("Header+UnixFS:0x" + newBuffer.toString('hex'));
    console.log("Sha256:0x" + sha256.toString('hex'));
    console.log("Multihash:0x" + multihash.toString('hex'));
    console.log("Address:" + base58);
    console.log("---------------------------------------------------------------------");
}
customHash('/Users/Kirn/Documents/Workspace/Dawn/ethereum/assets/test.txt');

File:0x310a320a330a340a350a360a370a0a
UnixFs:0x0802120f310a320a330a340a350a360a370a0a180f
Header+UnixFS:0x0a150802120f310a320a330a340a350a360a370a0a180f
Sha256:0xa1001394f749d9a0c5f27761b2f08e9432ce215dad6f01dbe26e468857169cbb
Multihash:0x1220a1001394f749d9a0c5f27761b2f08e9432ce215dad6f01dbe26e468857169cbb
Address:QmZB8R7T5xvKJDUJ6pXtUym6frQx1r6bQPcwquR1rtGHL6

大致可以分解为以下步骤

  • 读取文件数据为Buffer
  • 把文件数据转为Unix文件格式
  • 数据流头部增加Metadata数据
    • tag: 0x0a=10(此处也不知为何,后续再做研究)
    • 文件大小
  • sha2-256编码
  • 转为multihash格式,目前IPFS采用的是32位sha2-256编码,因此数据头部需要增加0x1220,0x12代表sha256,0x20=32代表hash位数
  • Base58编码

IPFS链上存储方案

针对于上面对IPFS地址的分析,我们可以在链上采取两种存取方案

  • 存储方案一
    以string的形式直接存储IPFS地址,优点:简单明了,读取和存储都很方便,缺点:占用空间大,gas消耗可能会比较大
  • 存储方案二
    以bytes32的形式只存储IPFS地址的sha256之后的结果,优点:占用空间少,gas消耗较少,缺点:读取和存储相对比较麻烦

写一个简单的合约测试一下

pragma solidity ^0.4.21;

contract IPFSAddress {
    mapping(address => bytes32) public bytesIpfs;
    mapping (address=>string) public stringIpfs;
    
    // save as bytes32
    function saveBytes(bytes32 ipfs) public {
        bytesIpfs[msg.sender] = ipfs;
    }
    
    // save as string 
    function saveString(string ipfs) public {
        stringIpfs[msg.sender] = ipfs;
    }
}

string存储交易回执

QmZB8R7T5xvKJDUJ6pXtUym6frQx1r6bQPcwquR1rtGHL6
{
  blockHash: "0x1385ab689055d504d98b675da4803708be856368e8dd2799a917b1153d5712e2",
  blockNumber: 185768,
  contractAddress: null,
  cumulativeGasUsed: 85962,
  from: "0x262bab6a90aa1741390c4a3ec58855c81d9728e1",
  gasUsed: 85962,
  logs: [],
  logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
  root: "0xc37b7485014a3fc7ff943c4f753b77e649526633d490ca815ff2abd385699c88",
  to: "0xb6093ecf6a2ae6b94bb2e45186da0f2bcfa315a5",
  transactionHash: "0xfe6162aceeb211ace9b2c689135778ec28c1c5d778785f20a0a246220b18cfa4",
  transactionIndex: 0
}

bytes32存储交易回执

0xa1001394f749d9a0c5f27761b2f08e9432ce215dad6f01dbe26e468857169cbb
{
  blockHash: "0xf7f47ef6d77773b54fe143c933d67c49ee0694851bb7fa7cafb2b594d14c1d0e",
  blockNumber: 185766,
  contractAddress: null,
  cumulativeGasUsed: 43595,
  from: "0x262bab6a90aa1741390c4a3ec58855c81d9728e1",
  gasUsed: 43595,
  logs: [],
  logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
  root: "0x476961f34dbbfc86c47a2f4935a5840b23fee03578994c56625ca83af75acf7f",
  to: "0xb6093ecf6a2ae6b94bb2e45186da0f2bcfa315a5",
  transactionHash: "0x0a8b740c94deae4df8df76852d03f74461843235e0c05b78f31c8f06ee2f81a3",
  transactionIndex: 0
}

对比两次链上交易的结果:

  • string存储消耗:gasUsed: 85962
  • bytes32存储消耗:gasUsed: 43595

bytes32存储差不多是string存储gas消耗的一半,算是一个较优的存储方案,当然前提是multihash采用的hash算法不变的情况下。因为IPFS的地址采用了multihash,在sha256算法不安全的情况下可以随时更换其他hash算法而不需要更改设计方案。

解析链上IPFS地址

为了方便查询链上的IPFS地址,可以把base58编码的算法在合约里实现一下,这里用library实现。

library IPFSLib {
    bytes constant ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";

    /**
     * @dev Base58 encoding
     * @param _source Bytes data
     * @return Encoded bytes data
     */
    function base58Address(bytes _source) internal pure returns (bytes) {
        uint8[] memory digits = new uint8[](_source.length * 136/100 + 1);
        digits[0] = 0;
        uint8 digitlength = 1;
        for (uint i = 0; i < _source.length; ++i) {
            uint carry = uint8(_source[i]);
            for (uint j = 0; j<digitlength; ++j) {
                carry += uint(digits[j]) * 256;
                digits[j] = uint8(carry % 58);
                carry = carry / 58;
            }
            
            while (carry > 0) {
                digits[digitlength] = uint8(carry % 58);
                digitlength++;
                carry = carry / 58;
            }
        }
        return toAlphabet(reverse(truncate(digits, digitlength)));
    }

    /**
     * @dev Truncate `_array` by `_length`
     * @param _array The source array
     * @param _length The target length of the `_array`
     * @return The truncated array 
     */
    function truncate(uint8[] _array, uint8 _length) internal pure returns (uint8[]) {
        uint8[] memory output = new uint8[](_length);
        for (uint i = 0; i < _length; i++) {
            output[i] = _array[i];
        }
        return output;
    }
    
    /**
     * @dev Reverse `_input` array 
     * @param _input The source array 
     * @return The reversed array 
     */
    function reverse(uint8[] _input) internal pure returns (uint8[]) {
        uint8[] memory output = new uint8[](_input.length);
        for (uint i = 0; i < _input.length; i++) {
            output[i] = _input[_input.length - 1 - i];
        }
        return output;
    }

    /**
     * @dev Convert the indices to alphabet
     * @param _indices The indices of alphabet
     * @return The alphabets
     */
    function toAlphabet(uint8[] _indices) internal pure returns (bytes) {
        bytes memory output = new bytes(_indices.length);
        for (uint i = 0; i < _indices.length; i++) {
            output[i] = ALPHABET[_indices[i]];
        }
        return output;
    }

    /**
     * @dev Convert bytes32 to bytes
     * @param _input The source bytes32
     * @return The bytes
     */
    function toBytes(bytes32 _input) internal pure returns (bytes) {
        bytes memory output = new bytes(32);
        for (uint8 i = 0; i < 32; i++) {
            output[i] = _input[i];
        }
        return output;
    }

    /**
     * @dev Concat two bytes to one
     * @param _byteArray The first bytes
     * @param _byteArray2 The second bytes
     * @return The concated bytes
     */
    function concat(bytes _byteArray, bytes _byteArray2) internal pure returns (bytes) {
        bytes memory returnArray = new bytes(_byteArray.length + _byteArray2.length);
        for (uint16 i = 0; i < _byteArray.length; i++) {
            returnArray[i] = _byteArray[i];
        }
        for (i; i < (_byteArray.length + _byteArray2.length); i++) {
            returnArray[i] = _byteArray2[i - _byteArray.length];
        }
        return returnArray;
    }
}
contract IPFSAddress {
    using IPFSLib for bytes;
    using IPFSLib for bytes32;
    mapping(address => bytes32) public bytesIpfs;
    mapping (address=>string) public stringIpfs;
    
    function saveBytes(bytes32 ipfs) public {
        bytesIpfs[msg.sender] = ipfs;
    }
    
    function saveString(string ipfs) public {
        stringIpfs[msg.sender] = ipfs;
    }
    
    function ipfsAddress() external view returns (string) { 
        bytes memory prefix = new bytes(2);
        prefix[0] = 0x12;
        prefix[1] = 0x20;
        bytes memory value = prefix.concat(bytesIpfs[msg.sender].toBytes());
        bytes memory ipfsBytes = value.base58Address();
        return string(ipfsBytes);
    }
}

合约已部署Ropsten,可作参考

https://ropsten.etherscan.io/address/0x0581d89b0b4edf171a199937b1d16a1033ba7538

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容