上一篇文章中初步介绍了IPFS的基本概念和使用方法,今天更深入一些了解IPFS的设计理念。

根据 第三版 白皮书,IPFS体系可以分为7层:

  1. 身份:负责节点的身份生成和认证。
  2. 网络:负责节点间的网络连接,可以配置使用各种网络协议。
  3. 路由:维护路由信息来找出特定的节点和对象。默认使用DHT,可以替换。
  4. 交换:一个新型的块交换协议(BitSwap)来高效管理块分发。有点类似超市,对数据复制有激励。交易策略可以替换。
  5. 对象:带有链接的、内容寻址的不可变对象组成的Merkle DAG,可以代表任意数据结构。比如文件层级和通信系统。
  6. 文件:受到Git启发的版本控制文件层级系统。
  7. 命名:自验证的可变命名系统。

身份

节点间通过NodeID来识别彼此之间的身份,而NodeID是根据公钥和S/Kademlia静态加密算法生成的,一个节点可以生成多个NodeID,但这种行为将失去某些“好处”,至于什么好处在BitSwap时候细说。

文中还给了生成NodeID的伪代码:

1
2
3
4
5
6
7
8
difficulty = <integet parameter>
n = Node{}

do{
n.PubKey,n.PrivKey = PKI.genKeyPair()
n.NodeID = hash(n.PubKey)
p = count_preceding_zero_bits((hash(n.NodeId)))
}while (p < difficulty)

代码比较好理解这里就不多解释,当节点间第一次建立链接时候会交换公钥,并且计算哈希值,如果不匹配则终止链接。另外IPFS更倾向于使用能自解释的哈希格式,比如使用了哪个哈希函数、摘要长度:

1
<function code><digest length><digest bytes>

这样做的好处有2个:

  1. 可以根据实际情况选择合适的函数(安全性Vs高性能)
  2. 可以根据选择函数的不同而演变。自描述的值准许使用兼容的、不同的参数。

网络和路由

这两个相对好理解就放一起记录了。

IPFS可以使用任何传输协议,但最适合的是WebRTC DataChannelsuTP,同时基于uTP提供了可靠传输。并且支持ICE NAT穿透技术,还可以选择检查哈希值来保证消息完整性,并通过使用HAMC以及发送方的公钥来保证真实性。

正是因为可以使用各种传输协议,所以IPFS也可使用覆盖网络(overlay networks)。因此IPFS使用多层地址的格式来存储地址信息。比如:

1
2
3
4
5
6
# sctp/ipv4链接

/ip4/10.20.30.40/sctp/1234/

# 通过tcp/ipv4代理的sctp/ipv4链接
/ipo/5.6.7.8/tcp/5678/ip4/1.3.4.5/sctp/1234/

IPFS通过使用路由来寻找其他节点的网络地址和谁可以提供特定的对象。基于DSHT、Coral和S/Kademila。IPFS DHT根据大小来使用不同的存储方式,比如小文件(小于1KB)直接存在DHT中,而大文件则存储那些可以提供数据的节点的引用。

另外不同的场景可以使用不同的路由系统,比如DHT用于广域网,静态HT用于本地网络。所以根据不同场景路由系统是可以改变的,只要实现了相应的接口。

块交换——BitSwap协议

BitSwap协议灵感来源于Bittorrent,使用want_list来记录自己想要什么,have_list来记录自己有什么。和Bittorrent不同的是BitSwap不关注数据块来源于哪个文件。为了解决“吸血节点”问题,IPFS引入了信用机制:

  1. 节点通过账本记录和其他节点之间的收(负债)发(信用)字节长度。
  2. 根据不同的负债率,是否发送数据的概率不同。

如果一个节点决定不发送数据给请求方,随后则会根据ignore_cooldown时间来忽略接下来这个节点的请求,以便防止DDos攻击。

负债率的计算公式为:

$$r=\frac{bytes sent}{bytes recv+1}$$

分母加1防止出现除以0的异常。已知r后,发送数据的概率P为:

$$P(send|r)=1-\frac{1}{1+\exp(6-3r)}$$

这里有个点要注意,节点A的负债(debt)就是节点B的信用(credit),以go-bitswap中关于账本的代码为例,来源于decision\ledger.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
type ledger struct {
// Partner is the remote Peer.
Partner peer.ID

// Accounting tracks bytes sent and received.
Accounting debtRatio

// lastExchange is the time of the last data exchange.
lastExchange time.Time

// exchangeCount is the number of exchanges with this peer
exchangeCount uint64

// wantList is a (bounded, small) set of keys that Partner desires.
wantList *wl.Wantlist

// sentToPeer is a set of keys to ensure we dont send duplicate blocks
// to a given peer
sentToPeer map[string]time.Time

// ref is the reference count for this ledger, its used to ensure we
// don't drop the reference to this ledger in multi-connection scenarios
ref int

lk sync.Mutex
}

type debtRatio struct {
BytesSent uint64
BytesRecv uint64
}

func (dr *debtRatio) Value() float64 {
return float64(dr.BytesSent) / float64(dr.BytesRecv+1)
}

func (l *ledger) SentBytes(n int) {
l.exchangeCount++
l.lastExchange = time.Now()
l.Accounting.BytesSent += uint64(n)
}

func (l *ledger) ReceivedBytes(n int) {
l.exchangeCount++
l.lastExchange = time.Now()
l.Accounting.BytesRecv += uint64(n)
}

举个例子方便理解:假设有A、B两个节点,目前A向B发送了60个字节的数据,从B接收了100字节数据,那么对于B来说则是向A发送100字节数据、接收60字节数据。当A再次向B请求下载数据时,B就会根据自己的账本计算负债率,并决定是否接受这次请求。

块交换生命周期大体分成4种:

  1. open:发送账本直到对方同意。
  2. sending:交换want_list和数据块。
  3. close:关闭链接。
  4. ignore:这个只有在策略是避免发送时候才会出现。

白皮书中说,在open阶段节点A会创建一个新账本或者使用旧账本发送给节点B,而B则在ignore_cooldown超时时间内概率性的决定忽略还是接受这次请求。(原文是:This should be done probabilistically with an ignore_cooldown timeout)。此外,如果决定了接受这次请求,则使用本地账本新建一个peer对象并修改账本的last_seen时间戳,然后比较接收到的账本和对方发来的账本是否一致,一致则建立链接,否则初始化一个空账本并发送。

这里Roy我有个疑惑,在go-ipfs源码中找了很多地方都没找到这个”概率性忽略或接受请求”以及对比账本是否一致部分的代码,甚至上面计算概率的公式都没找到,希望知道这部分代码实现位置的大佬能够指点一下小弟,先谢谢了。

建立链接后就可以开始交换数据了。在go-ipfs中,want_list是一个字典,定义如下:

1
2
3
4
5
6
7
8
9
10
type Wantlist struct {
set map[string]*Entry
}

type Entry struct {
Cid cid.Cid
Priority int

SesTrk map[uint64]struct{}
}

block则更简洁:

1
2
3
4
type BasicBlock struct {
cid cid.Cid
data []byte
}

在IPFS中,文件被分为若干Blocks,Block使用一个叫做CID标识符来索引,这是一个自描述的索引结构体,用来唯一标识一个Block。所以要想从节点下载一个Block,只需在知道CID即可,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Cid represents a self-describing content adressed
// identifier. It is formed by a Version, a Codec (which indicates
// a multicodec-packed content type) and a Multihash.
type Cid struct{ str string }

// NewCidV0 returns a Cid-wrapped multihash.
// They exist to allow IPFS to work with Cids while keeping
// compatibility with the plain-multihash format used used in IPFS.
// NewCidV1 should be used preferentially.
func NewCidV0(mhash mh.Multihash) Cid {
// Need to make sure hash is valid for CidV0 otherwise we will
// incorrectly detect it as CidV1 in the Version() method
dec, err := mh.Decode(mhash)
if err != nil {
panic(err)
}
if dec.Code != mh.SHA2_256 || dec.Length != 32 {
panic("invalid hash for cidv0")
}
return Cid{string(mhash)}
}

// NewCidV1 returns a new Cid using the given multicodec-packed
// content type.
func NewCidV1(codecType uint64, mhash mh.Multihash) Cid {
hashlen := len(mhash)
// two 8 bytes (max) numbers plus hash
buf := make([]byte, 2*binary.MaxVarintLen64+hashlen)
n := binary.PutUvarint(buf, 1)
n += binary.PutUvarint(buf[n:], codecType)
cn := copy(buf[n:], mhash)
if cn != hashlen {
panic("copy hash length is inconsistent")
}

return Cid{string(buf[:n+hashlen])}
}

当执行ipfs daemon后,可以使用ipfs bitswap stat来查看整体的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
-> % ipfs bitswap stat
bitswap status
provides buffer: 0 / 256
blocks received: 760
blocks sent: 11
data received: 195407444
data sent: 2883738
dup blocks received: 75
dup data received: 18 MB
wantlist [6 keys]
QmeUUfUS4DMsKzMkTda3iunruYMvJEwDfGnF7KBAt2smwJ
QmcQQyNtMy3Z1fekutzjwkZCPHXwVvs4ZqgiboMBbc2H3A
Qmc4XdqSLbyjYGsaZihb1nGhNAWbU8hKmmUNeUjExsLQyi
QmYxE2fXENrJvuzES3Tyive4o1zp9VoNJD5bft4soyv1H2
QmPmLZxuK4hAk9kx6mipvXvUjbtMxTXXnbbahP2VicYLeu
QmRtJugMr7yPPS7Kw5azRQaj3VB774ARSLJfw7gsKM1fzt
partners [936]
QmNMTsUaKTDdunSUN44TZuMPDpGTCLG8R2ooEo1ZmxJadq
QmNPNDYybTYcZK2KBrrn24M4Lc7VcPnGDNX8ueur9TRCmK
QmNQC6nB2LACt1h6nkJdFz1vaFRP5iTHeB1p9nsfpk4nnM
QmNRPRpbgM1WTEUo4T4sadsASmBudPKqYupdtZcSoFkvSL
QmNRSBnfjE6PMZ5AfnLccWpMjX2LtRFYedxQ2UqQxvozfV
QmNRV7kyUxYaQ4KQxFXPYm8EfuzJbtGn1wSFenjXL6LD8y
QmNUCfyL67rqBesyCLon9kRUpXtPEfVUzf7aGp1VXqGqW4
QmNV721hfbzxuLdBvLaBpzmKtnPP4duf59SZ3AU1fV5CrK

也可以使用ipfs bitswap ledger NodeID来具体查看和某个节点之间的债务关系。

本文最后来个彩蛋吧:

1
2
3
%ipfs get Qmdsrpg2oXZTWGjat98VgpFQb5u1Vdw5Gun2rgQ2Xhxa2t                                                                                                                 
Saving file(s) to Qmdsrpg2oXZTWGjat98VgpFQb5u1Vdw5Gun2rgQ2Xhxa2t
322.39 MiB / 322.39 MiB[====================================================================================================================================================] 100.00% 17m47s

然后把下载好的文件后缀改成mp4,慢慢欣赏。