乐趣区

Go夜读IPFS预习资料

IPFS 就其本身,没有用到多少新技术,大多数都是已有技术和思路,但并不能说就没有创新,类似比特币。

分布式哈希表

思路: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),目前并不很完美。

退出移动版