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="\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(
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),目前并不很完美。