Go夜读-IPFS预习资料

xcshuan · · 888 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

IPFS就其本身,没有用到多少新技术,大多数都是已有技术和思路,但并不能说就没有创新,类似比特币。
取名为IPFS,星际文件系统,有点科幻意味,不过思路是,以后组建星际互联网,如果基于用HTTP的基于资源定位符寻址,延时等都不可接受,而基于内容寻址,一定程度上能解决这个问题,缓解光速屏障。

分布式哈希表

思路:KadDHT的节点寻址与内容寻址同构,在KAD基于异或来计算逻辑距离的基础上,节点倾向于连接与自己距离更近的节点,存储与自己距离更近的内容Key值,并在此之上加入了延时等具体物理距离的考量(Coral DHT)。引入S-KadDHT加入了一定的抗女巫攻击能力,因为生成一个公私钥需要计算难度值。

Git

IPFS的基础数据结构与组织方式受Git影响很深,首先Git是一套基于内容寻址的文件系统,git有blob,tree,commit,tag四种对象。blob对象存储各种类型的文件,当你添加一个blob时,会通过sha1算法计算它的hash,以前两位为文件夹,后面的为文件名。

Tree则可以指向其他tree或者blob等,关系的链接是用DAG来做的,按理说也不可能有环,不可能修改git后发现改到原来的一个版本去了。

//检验加入blob
$echo "hello,world" > 1.txt
$git hash-object -w 1.txt                                              2d832d9044c698081e59c322d5a2a459da546469
$echo "hello,world." > 1.txt
$git hash-object -w 1.txt                                             ec03970d236871dd491c61683f3074d3038b4f6e
$find .git/objects -type f  //查看存储的文件,可以看到hash直接当成了路径
.git/objects/2d/832d9044c698081e59c322d5a2a459da546469
.git/objects/ec/03970d236871dd491c61683f3074d3038b4f6e

#检验加入tree
$git update-index --add --cacheinfo 100644 ec03970d236871dd491c61683f3074d3038b4f6e test
$git write-tree
b0de348e9cb3a01b47154a4331678787e29afdff
#加入一个新文件
$echo "new file">new.txt
$git hash-object -w new.txt
fa49b077972391ad58037050f2a75f74e3671e92
$git update-index --add --cacheinfo 100644 fa49b077972391ad58037050f2a75f74e3671e92 test1
$git write-tree
7b452990f69f7751ba805a291549c32dbfeb8716
#对比两棵树的内容
$git cat-file -p b0de348e9cb3a01b47154a4331678787e29afdff
100644 blob ec03970d236871dd491c61683f3074d3038b4f6e    test

#可以看到,两棵树,一个指向一个hash,一个指向两个hash,且有一个被两棵树指向,故而是图
#但因为无环,所以为DAG有向无环图。
$git cat-file -p 7b452990f69f7751ba805a291549c32dbfeb8716
100644 blob ec03970d236871dd491c61683f3074d3038b4f6e    test
100644 blob fa49b077972391ad58037050f2a75f74e3671e92    test1

而IPFS也设计了相应的对象,blob、list、tree、commit,同时也用DAG来组织。

SFS 自验证文件系统

为了发布动态内容,需要一种映射机制,将某个name动态映射到一个IPFS哈希。在HTTP的世界里,PKI需要一个可信机构颁发证书来维持,但因为IPFS的志向是去中心化,所以不设计可信机构,而采用SFS的方案,具体思路为,将公钥以及私钥签名等信息,放到这个动态映射中,即文件名本身包含公钥信息,既可以实现密钥分发。

IPFS根据此想法设计了IPNS。

Block

Block是IPFS里一个非常基本的抽象,大部分结构都实现了接口,用于表示IPFS中,不可分的最小单位。

// ipfs/go-block-format/blocks.go
// Block provides abstraction for blocks implementations.
type Block interface {
    RawData() []byte
    Cid() cid.Cid
    String() string
    Loggable() map[string]interface{}
}

// A BasicBlock is a singular block of data in ipfs. It implements the Block interface.
type BasicBlock struct {
    cid  cid.Cid
    data []byte
}

// ipfs/go-ipfs/core/coreapi/block.go

//基本的put块方法,简单的取一个hash当做块的寻址ID(CID),然后就存起来了。
func (api *BlockAPI) Put(ctx context.Context, src io.Reader, opts ...caopts.BlockPutOption) (coreiface.BlockStat, error) {
    settings, pref, err := caopts.BlockPutOptions(opts...)
    if err != nil {
        return nil, err
    }
    //读取所有数据
    data, err := ioutil.ReadAll(src)
    if err != nil {
        return nil, err
    }
    //求出数据hash当成块的ID
    bcid, err := pref.Sum(data)
    if err != nil {
        return nil, err
    }
    //包装成一个块
    b, err := blocks.NewBlockWithCid(data, bcid)
    if err != nil {
        return nil, err
    }

    if settings.Pin {
        defer api.blockstore.PinLock().Unlock()
    }
    //存到磁盘上去
    err = api.blocks.AddBlock(b)
    if err != nil {
        return nil, err
    }

    if settings.Pin {
        api.pinning.PinWithMode(b.Cid(), pin.Recursive)
        if err := api.pinning.Flush(); err != nil {
            return nil, err
        }
    }

    return &BlockStat{path: path.IpldPath(b.Cid()), size: len(data)}, nil
}

Object

对象与DAG是紧密联合的,可以理解为是DAG中元素的另一种抽象

type Link struct {
    Name, Hash string
    Size       uint64
}

type Node struct {
    Links []Link
    Data  string
}
# 测试,data为一个有两个子文件夹的文件夹
$ipfs add data -r
added QmXkiQUQ5xkC471K29MsL3tKHb9dsFgXernV9hbr4vDP3C data/1/testobject
added QmREfzcCsuHpzUvWUEzgiqHSNpiKnc18s4RfQXqvW669E8 data/2/1.txt
added QmZFYyMVQMXbis5spJkifRtoPoPz9pNo5NyiVSo9UhWTLa data/2/11.pdf
added QmcN88vyVJwfLRreojfT7qdVezShxF1JMCLQBxd3TwL3uz data/1
added QmV14YGVhYS1WyCvmmTnnFzJF4zC587LMCnufjox5r7zHd data/2
added QmdMwuSVU3NKQUy4YgxKWcAm9z6fWXYb6ampbWC442nBgE data
 8.99 MiB / 9.00 MiB [====================================================]  99.96%
 // 查看links,可以看到指向两个子文件夹
$ipfs object links QmdMwuSVU3NKQUy4YgxKWcAm9z6fWXYb6ampbWC442nBgE
QmcN88vyVJwfLRreojfT7qdVezShxF1JMCLQBxd3TwL3uz 82      1
QmV14YGVhYS1WyCvmmTnnFzJF4zC587LMCnufjox5r7zHd 9432929 2

DAG

上面说过,ipfs有blob、list、tree、commit等数据类型,通过DAG有向无环图链接。还是以上面add的文件夹为例。

$ipfs dag get QmdMwuSVU3NKQUy4YgxKWcAm9z6fWXYb6ampbWC442nBgE
{"data":"CAE=",
"links":
    [
    {"Cid":{"/":"QmcN88vyVJwfLRreojfT7qdVezShxF1JMCLQBxd3TwL3uz"},"Name":"1","Size":82},
    #链接的hash
    {"Cid":{"/":"QmV14YGVhYS1WyCvmmTnnFzJF4zC587LMCnufjox5r7zHd"},"Name":"2","Size":9432929}
    #链接的另一个hash
    ]
}
# 再查看其中一个hash,可以看到指向一个文件testobject
$ipfs dag get QmcN88vyVJwfLRreojfT7qdVezShxF1JMCLQBxd3TwL3uz
{"data":"CAE=",
"links":
[{"Cid":{"/":"QmXkiQUQ5xkC471K29MsL3tKHb9dsFgXernV9hbr4vDP3C"},"Name":"testobject","Size":26}]}

代码

// Link represents an IPFS Merkle DAG Link between Nodes.
type Link struct {
    // utf string name. should be unique per object
    Name string // utf8

    // cumulative size of target object
    Size uint64

    // multihash of the target object
    Cid cid.Cid
}

DAG会递归进行,生成一个node后,会成为上一层节点的一个link,直到结束。

ipfs支持两种dag,一种是balanced dag,适合随机访问;另一个种是trickledag,适合顺序访问

var nd ipld.Node
if adder.Trickle {
    nd, err = trickle.Layout(db)
} else {
    nd, err = balanced.Layout(db)
}

以balanced为例,内部会递归将数据分割,成为DAG上的节点,下面的fillNodeRec函数是递归进行的,并且为了避免块太大,作为中间节点一次存的hash也是有限的。

// Each time a DAG of a certain `depth` is filled (because it
    // has reached its maximum capacity of `db.Maxlinks()` per node)
    // extend it by making it a sub-DAG of a bigger DAG with `depth+1`.
    for depth := 1; !db.Done(); depth++ {

        // Add the old `root` as a child of the `newRoot`.
        newRoot := db.NewFSNodeOverDag(ft.TFile)
        newRoot.AddChild(root, fileSize, db)

        // Fill the `newRoot` (that has the old `root` already as child)
        // and make it the current `root` for the next iteration (when
        // it will become "old").
        root, fileSize, err = fillNodeRec(db, newRoot, depth)
        if err != nil {
            return nil, err
        }
    }

IPNS

$ipfs add helloworld
added QmVtZPoeiqpREqkpTTNMzXkUt74SgQA4JYMG8zPjMVULby helloworld
12 B / ? [-----------------------------------------------------] 
$ipfs name publish QmVtZPoeiqpREqkpTTNMzXkUt74SgQA4JYMG8zPjMVULby
Published to QmdcFVhY4m3NyYfiMbosr3ZbW9EHCkcBk4xySt4uQozXb6: /ipfs/QmVtZPoeiqpREqkpTTNMzXkUt74SgQA4JYMG8zPjMVULby
#解析IPNS
$ipfs name resolve QmdcFVhY4m3NyYfiMbosr3ZbW9EHCkcBk4xySt4uQozXb6
/ipfs/QmVtZPoeiqpREqkpTTNMzXkUt74SgQA4JYMG8zPjMVULby
#查看IPNS的内容
$ipfs cat /ipns/QmdcFVhY4m3NyYfiMbosr3ZbW9EHCkcBk4xySt4uQozXb6
hello,world
#修改文件内容重新发布
$echo "ipfs" >> helloworld
$ipfs add helloworld
added Qmb8sAnXHQYpuAwCTFmHvgz2P5AGJH4y5uVosSdKzcHSbN helloworld
17 B / 17 B [=====================================================] 100.00%
$ipfs name publish Qmb8sAnXHQYpuAwCTFmHvgz2P5AGJH4y5uVosSdKzcHSbN
Published to QmdcFVhY4m3NyYfiMbosr3ZbW9EHCkcBk4xySt4uQozXb6:
/ipfs/Qmb8sAnXHQYpuAwCTFmHvgz2P5AGJH4y5uVosSdKzcHSbN
#查看IPNS的内容
$ipfs cat /ipns/QmdcFVhY4m3NyYfiMbosr3ZbW9EHCkcBk4xySt4uQozXb6
hello,world
ipfs

//其实就是节点ID
$ipfs id -f="<id>\n"
QmdcFVhY4m3NyYfiMbosr3ZbW9EHCkcBk4xySt4uQozXb6

看一看代码实现,其实还是比较好理解的

// 基本用处就是签一个名
// Create creates a new IPNS entry and signs it with the given private key.
//
// This function does not embed the public key. If you want to do that, use
// `EmbedPublicKey`.
func Create(sk ic.PrivKey, val []byte, seq uint64, eol time.Time) (*pb.IpnsEntry, error) {
    entry := new(pb.IpnsEntry)

    entry.Value = val
    typ := pb.IpnsEntry_EOL
    entry.ValidityType = &typ
    entry.Sequence = &seq
    entry.Validity = []byte(u.FormatRFC3339(eol))

    //签名
    sig, err := sk.Sign(ipnsEntryDataForSig(entry))
    if err != nil {
        return nil, err
    }
    entry.Signature = sig

    return entry, nil
}

//验证IPNS项,验证签名
// Validates validates the given IPNS entry against the given public key.
func Validate(pk ic.PubKey, entry *pb.IpnsEntry) error {
    // Check the ipns record signature with the public key
    if ok, err := pk.Verify(ipnsEntryDataForSig(entry), entry.GetSignature()); err != nil || !ok {
        return ErrSignature
    }

    eol, err := GetEOL(entry)
    if err != nil {
        return err
    }
    if time.Now().After(eol) {
        return ErrExpiredRecord
    }
    return nil
}

//嵌入公钥信息,形成自验证
// EmbedPublicKey embeds the given public key in the given ipns entry. While not
// strictly required, some nodes (e.g., DHT servers) may reject IPNS entries
// that don't embed their public keys as they may not be able to validate them
// efficiently.
func EmbedPublicKey(pk ic.PubKey, entry *pb.IpnsEntry) error {
    // Try extracting the public key from the ID. If we can, *don't* embed
    // it.
    id, err := peer.IDFromPublicKey(pk)
    if err != nil {
        return err
    }
    if _, err := id.ExtractPublicKey(); err != peer.ErrNoPublicKey {
        // Either a *real* error or nil.
        return err
    }

    // We failed to extract the public key from the peer ID, embed it in the
    // record.
    pkBytes, err := pk.Bytes()
    if err != nil {
        return err
    }
    entry.PubKey = pkBytes
    return nil
}

Kad寻址

kad的思路前面讲过了,下面看看核心代码。

//计算距离的函数
func CommonPrefixLen(a, b ID) int {
    return ks.ZeroPrefixLen(u.XOR(a, b))
}

// 在bucket里寻找距离最近的节点信息
// NearestPeers returns a list of the 'count' closest peers to the given ID
func (rt *RoutingTable) NearestPeers(id ID, count int) []peer.ID {
    //计算异或距离
    cpl := CommonPrefixLen(id, rt.local)

    // It's assumed that this also protects the buckets.
    rt.tabLock.RLock()

    // Get bucket at cpl index or last bucket
    var bucket *Bucket
    //确保距离不超过当前桶大小
    if cpl >= len(rt.Buckets) {
        cpl = len(rt.Buckets) - 1
    }
    //取出桶
    bucket = rt.Buckets[cpl]

    pds := peerDistanceSorter{
        peers:  make([]peerDistance, 0, 3*rt.bucketsize),
        target: id,
    }
    //将节点追加进去
    pds.appendPeersFromList(bucket.list)
    //如果数量不够,从前后两个桶补充
    if pds.Len() < count {
        // In the case of an unusual split, one bucket may be short or empty.
        // if this happens, search both surrounding buckets for nearby peers
        if cpl > 0 {
            pds.appendPeersFromList(rt.Buckets[cpl-1].list)
        }
        if cpl < len(rt.Buckets)-1 {
            pds.appendPeersFromList(rt.Buckets[cpl+1].list)
        }
    }
    rt.tabLock.RUnlock()

    //进行一次排序
    // Sort by distance to local peer
    pds.sort()

    if count < pds.Len() {
        pds.peers = pds.peers[:count]
    }

    out := make([]peer.ID, 0, pds.Len())
    for _, p := range pds.peers {
        out = append(out, p.p)
    }

    return out
}

//更新节点信息
// Update adds or moves the given peer to the front of its respective bucket
func (rt *RoutingTable) Update(p peer.ID) (evicted peer.ID, err error) {
    peerID := ConvertPeerID(p)
    //首先和本地ID异或计算距离
    cpl := CommonPrefixLen(peerID, rt.local)

    rt.tabLock.Lock()
    defer rt.tabLock.Unlock()
    bucketID := cpl
    if bucketID >= len(rt.Buckets) {
        bucketID = len(rt.Buckets) - 1
    }

    bucket := rt.Buckets[bucketID]
    if bucket.Has(p) {
        // If the peer is already in the table, move it to the front.
        // This signifies that it it "more active" and the less active nodes
        // Will as a result tend towards the back of the list
        bucket.MoveToFront(p)
        return "", nil
    }
    
    //保证时延要求
    if rt.metrics.LatencyEWMA(p) > rt.maxLatency {
        // Connection doesnt meet requirements, skip!
        return "", ErrPeerRejectedHighLatency
    }

    // We have enough space in the bucket (whether spawned or grouped).
    if bucket.Len() < rt.bucketsize {
        bucket.PushFront(p)
        rt.PeerAdded(p)
        return "", nil
    }

    if bucketID == len(rt.Buckets)-1 {
        // if the bucket is too large and this is the last bucket (i.e. wildcard), unfold it.
        rt.nextBucket()
        // the structure of the table has changed, so let's recheck if the peer now has a dedicated bucket.
        bucketID = cpl
        if bucketID >= len(rt.Buckets) {
            bucketID = len(rt.Buckets) - 1
        }
        bucket = rt.Buckets[bucketID]
        if bucket.Len() >= rt.bucketsize {
            // if after all the unfolding, we're unable to find room for this peer, scrap it.
            return "", ErrPeerRejectedNoCapacity
        }
        bucket.PushFront(p)
        rt.PeerAdded(p)
        return "", nil
    }

    return "", ErrPeerRejectedNoCapacity
}

Bitswap

由于IPFS相当于一个全局的种子,自然会有一些策略,防止有人搭便车过于严重,于是就有了bitswap,bitswap会维护ledger,记录临近节点的收发情况(wantlist与havelist),来进行决策(bitswap下的engine)。

代码比较复杂,异步逻辑过多,粗略的讲,不贴代码了,到时候可以对着代码讲讲。

逻辑如下,当节点想获取一堆块的时候,首先在本地看看有没有,没有进入bitswap建立一个session(会话),然后在bitswap的pub/sub机制中订阅块(会写进一个块的channel)。然后将这些块的cid加入wantlist,让bitswap的worker开始工作,期间会异步进行很多事,首先会定时用p2p层的contentRouting的FindProviderAsync寻找拥有块的节点,然后bitswap这里会不断去取provider的ID,并向其发送请求,如果块返回了,检查一下hash值检验完整性,然后通过pub/sub的channel将块publish,通过channel发布过去,然后上层的逻辑,即channel的另一端就可以返回结果了。

超越IPFS-区块链存储

IPFS不能解决的问题,少部分节点用爱发电,大部分节点搭便车,没有经济激励,单凭bitswap机制,是不可能真正完整的。

P2P存储在没有区块链的年代,只能用于文件分享,以及被某些视频公司内置用来CDN(注意,视频网站占了你的上传带宽,但并没有向你支付费用,而你观看视频反而是需要费用的)。

于是,协议实验室提出了Filecoin项目,旨在让所有的参与方都有利可图,无论是存储空间还是带宽都可以盈利,用户也能以更低廉的价格获得更安全的服务。

区块链存储相对于其他项目,是最好落地的,因为存储内容和分发内容,在某种意义上是数字的,可以通过数学与密码学验证,而其他的区块链项目,只要和实体经济挂钩,链上链下如何交互是个很难的问题。

IPFS Filecoin
类别 去中心化文件系统(无激励) 去中心化存储网络
功能 基于内容寻址的分布式存储基础设施 IPFS网络之上的激励层,提供一个云存储领域的自由交易市场
存储权限 对有所有权的IPFS节点具备存储权限 1. 除对有所有权的IPFS节点具备存储权限外 2. 还可以通过支付的方式,在其供应商的节点之上具备存储权限
读取权限 ALL(只要知道内容cid) ALL(只要知道内容cid)

两个市场

存储市场

本质上是一个去中心化的云存储,用户通过Filecoin的Token,去租用存储矿工的存储空间,矿工持续提交存储证明,这里销售购买的是存储空间。

检索市场

可以理解为一个去中心化的CDN服务,用户通过Token,让检索矿工检索数据返回,这里销售购买的是带宽、流量。

存储证明

如何证明矿工存储数据?

如何证明矿工存储了冗余数据?

如何持续地证明矿工存储了数据?

区块链采用何种共识?

基础存储证明(见PPT)

1,本地存分片哈希

2,本地存Merkle树根,将文件分片及merkle树发给存储方。

3,利用同态Hash函数生成数据片的同态哈希发给存储方。

上面三个为什么不适用Filecoin?

考虑维度:通信复杂度?计算复杂度?能否抗三种攻击(女巫攻击,生成攻击,外源攻击)?用户和存储方能否合谋,比如利用同态hash,那存储方可以任意刷数据生成证明抢出块权了。

Filecoin采用的,一存储方在存储空间有一个sector(扇区)大小时,对其进行编码,zigzag深度鲁棒图,一个sector内一个文件块的编码可能同时和好几个其他文件块相关,然后做一次VDE,如此从左到右,从右到左编码多次,每次为一层,z字形,故成为zigzag,如果删除了其中一块,由于同时还需要很多其他块,导致生成攻击成本很高,其中用到了VDE(可验证的延迟编码),最后证明用到了zksnark零知识证明。

连续的复制证明即可实现时空证明,所以时空证明与复制证明其实是一起实现的。

但到此,时空证明只有对存储空间的证明,没有设计出块规则,filecoin采用了类似VRF的共识算法(最初白皮书为POW)。

预期共识

共识采用类VRF的秘密领导人选举,计算一个input的hash,计算其小于某一个target,但是不同的存储矿工需要的target不同,与其占全网占的存储算力成正比,及生成的存储证明占比越高,越可能爆块。所以前期,filecoin需求不足时,矿工可以自己付费刷数据提高爆块率。

$$ H(<t||rand(t)>_{M_i})/2^L \leq p_i^t/ \sum_j p_j^t $$

H-哈希函数

rand(t):时间t时的随机数,可用VDF生成连续随机数序列。

$M_i$:对其签名

$2^L$:让最后的值在0~100%之间

另一边是存储算力与全网算力的比值,即占比值越大者机会越大,某种POS。

代码抽象后,如下,

func Mine(minerKey PrivateKey) {
    for r := range rounds { // for each round
        bestTipset, tickets := ChainTipsMgr.GetBestTipsetAtRound(r - 1)

        ticket := GenerateTicket(minerKey, bestTipset)
        tickets.Append(ticket)

        // Generate an ElectionProof and check if we win
        // Note: even if we don't win, we want to make a ticket
        // in case we need to mine a null round
        win, proof := CheckIfWinnerAtRound(minerKey, r, bestTipset)
        if win {
            GenerateAndBroadcastBlock(bestTipset, tickets, proof)
        } else {
            // Even if we don't win, add our ticket to the tracker in
            // case we need to mine on it later.
            ChainTipsMgr.AddFailedTicket(bestTipset, tickets)
        }
    }
}

检查自己是不是被选中的。

const RandomnessLookback = 1 // Also referred to as "K" in many places

func CheckIfWinnerAtRound(key PrivateKey, n Integer, parentTipset Tipset) (bool, ElectionProof) {
  lbt := ChainTipsMgr.TicketFromRound(parentTipset, n-RandomnessLookback)

  eproof := ComputeElectionProof(lbt, key)

  minerPower := GetPower(miner)
  totalPower := state.GetTotalPower()

  if IsProofAWinner(eproof, minerPower, totalPower)
    return True, eproof
  else
    return False, None
}

问题:因为是秘密领导人选举,此共识为预期共识,有可能某一轮无人选中,有可能选中多人(会打包多个区块生成tipset),目前并不很完美。


有疑问加站长微信联系(非本文作者)

本文来自:Segmentfault

感谢作者:xcshuan

查看原文:Go夜读-IPFS预习资料

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

888 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传