Go夜读-IPFS预习资料
分布式哈希表
思路:KadDHT的节点寻址与内容寻址同构,在KAD基于异或来计算逻辑距离的基础上,加入了延时等具体物理距离的考量(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 //查看存储的文件 .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 $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/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,然后就存起来了。 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 } 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
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等数据类型。还是以上面add的文件夹为例。
$ipfs dag get QmdMwuSVU3NKQUy4YgxKWcAm9z6fWXYb6ampbWC442nBgE {"data":"CAE=", "links": [ {"Cid":{"/":"QmcN88vyVJwfLRreojfT7qdVezShxF1JMCLQBxd3TwL3uz"},"Name":"1","Size":82}, {"Cid":{"/":"QmV14YGVhYS1WyCvmmTnnFzJF4zC587LMCnufjox5r7zHd"},"Name":"2","Size":9432929} ] } $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函数是递归进行的。
// 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="\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() 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 = 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() = 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(_{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),目前并不很完美。