关于IPFS

400 阅读21分钟

概念:

分布式存储的定义:
将数据存储在多个独立的存储设备中,通过网络连接的方式协同工作,共同提供数据存储和访问服务的系统。

数据分片:将数据切分成多个小块,每个小块可以分别存储在不同的存储节点上。数据分片可以提高存储效率和数据可靠性。
数据冗余:分布式存储系统通常会将数据进行多份备份存储,以提高数据的可靠性和可用性。当某个存储节点发生故障时,可以通过备份节点进行数据恢复。
数据一致性:由于数据分散在不同的节点上,需要采用一些数据一致性算法来确保分布式存储系统中的数据一致性。
数据访问::分布式存储系统中,数据的访问通常需要经过多个存储节点。需要设计一些数据访问协议和路由算法来确保数据的高效访问。
分布式存储的架构:分布式存储系统通常采用多个存储节点构成的集群进行存储和访问。不同的分布式存储架构包括中心化架构、对等网络架构混合架构等。

http存在的问题:
1.易受攻击,防攻击成本高
2.数据存储成本高
3.中心化带来的泄露风险
4.大规模数据存储传输为维护难

IPFS的优势:
1.下载速度快
2.优化全球存储
3.更安全
4.数据的可持续保存

IPFS协议栈

image.png

身份层

每一个节点在IPFS代码中都由 Node 结构体来表示,其中只包含Nodeld及一组公私钥对。

  • Nodeld 是一个公钥的哈希
  • IPFS 使用S/Kademlia 中的算法增加创建新身份的成本
  • IPFS使用的哈希算法比较灵活,允许用户根据使用自定义
    <function code><digest length><digest bytes>

身份系统功能:
1.标识 IPFS 网络中的节点
2.节点首次建立连接时,节点之间首先交换公钥,并且进行身份信息验证。
例如:检查hash(other.PublicKey)是否等于 other.NodeId 的值

网络层

特点:

  • 传输
  • 可靠性
  • 可连接性
  • 完整性
  • 可验证性

路由层

功能

  • 节点路由
  • 内容路由
  • 数据存取

核心技术:分布式哈希表DHT

分布式哈希表常见的算法

  • Kademlia DHT
  • Coral DHT
  • S/Kademlia

Kademlia

Kademlia的特性:

  1. 节点 ID 与KEY是同样的值域,都是使用 SHA-1 算法生成的 160 位摘要,这样大大简化了查询时的信息量,更便于查询。
  2. 可以使用 XOR,计算任意两个节点的距离或节点和关键字的距离
  3. 查找一条请求路径的时候,每个节点的信息是完备的,只需要进行Log(n)量级次跳转
  4. 可根据查询速度和存储量的需求调整每个节点需要维护的 DHT 大小。

Kademlia二叉状态树
Kademlia 网络的节点 ID 是由一棵二叉树维护的,最终生成的二叉树的特点如下:

  • 每个网络节点从根节点出发,沿着它的最短唯一前缀到达
  • 每个网络节点是叶子节点。对于任意的一个树的节点我们可以沿着它的前缀作为路径,向下分解成一系列不包含自己的子树

image.png 节点A的ID(010)
节点B的ID(110) 求一下两个叶子节点的虚拟间距:A ⊕ B = 100(二进制) = 4(十进制)
由于节点是动态增加减少的,如果知道的节点恰好宕机或者下线了就会出现问题,于是引入了K桶(K-bucket)的机制。

每一条路由信息由如下3部分组成:IPAddress、UDP Port、NodeID

KAD路由表:

将距离分成160个K桶(存放K个数据的桶),分开存储。
编号为i的路由表,存放着距离为[2^i, 2^(i+1)-1]的K条路由信息
K桶内部信息存放位置是根据上次看到的时间顺序排列(最早看到的放在头部,最后看到的放在尾部)

因为网络中节点可能处于在线或者离线状态,而在之前经常在线的节点,我们需要访问的时候在线的概率更大,那么我们会优先访问它(尾部的节点)。

更新节点信息

1)计算自己和发送者的ID 距离: d(x,y)=x⊕y。
2)通过距离d选择对应的K进行更新操作。
3)如果y的IP地址已经存在于这个K桶中,则把对应项移到该K桶的尾部;如果y的IP 地址没有记录在该K桶中,则:
①如果该桶的记录项小于k个,则直接把y的(IP address,UDP port,NodeID) 信息插入队列尾部。
②如果该K桶的记录项大于k个,则选择头部的记录项(假如是节点z)进行 RPC PING 操作。 如果z没有响应,则从桶中移除z的信息,并把的信息插人队列尾部. 如果z有响应,则把z的信息移到队列尾部,同时忽略,y的信息。

查找ID值为t的节点,步骤:
1)计算到t的距离:d(x,t)=x⊕t。
2)从x的第log(d)个K桶中取出a个节点的信息,同时进行FIND_NODE操作。如果这个K桶中的信息少于a个,则从附近多个桶中选择距离最接近d的总共 a 个节点。
3)对接收到查询操作的每个节点,如果发现自己就是t,则回答自己是最接近t的;否则测量自己和t的距离,并从自己对应的K桶中选择a个节点的信息给 x。
4)x对新接收到的每个节点都再次执行FIND_NODE操作,此过程不断重复执行,直到每一个分支都有节点响应自己是最接近t的。
5)通过上述查找操作,x得到了k个最接近t的节点信息。

一个崭新的节点希望加入Kad网络
1.新节点A生成一个随机的节点ID,直到离开网络一直使用。
2.新节点A需要一个种子节点B作为引导,并把该种子节点加入到K桶中。
3.向节点B发送FIND_NODE请求。
4.节点B在收到节点A的FIND_NODE请求后,会根据FIND_NODE请求的约定,找到K个距离A最近的节点,并返回给A节点,A收到这些节点以后,就把它们加入到自己的K桶中
5.然后节点A会继续向这些刚拿到节点发起FIND_NODE请求,如此往复,直到A建立了足够详细的路由表。

Kademlia攻击方式

包括日蚀攻击、女巫攻击、流失攻击和对抗路由攻击。

(1)日蚀攻击

如果一个节点在网络中能够自由选择它的ID,攻击者可以在网络中安放一些恶意节点,使得信息都必须经由恶意节点传递。那么这样一来,恶意节点就能够在网络中将一个或几个节点从网络中隐藏掉。

(2)女巫攻击

在开放的对等网络里,攻击者可以假冒多个ID,用少数网络节点控制多个虚假身份。KAD 网络难以控制节点的数量,那么攻击者伪造大量虚假节点身份,就能控制部分网络。通常情况下可以通过消耗一定的系统和计算资源提高女巫攻击者的成本。当然,这也只能改善并不能杜绝。

(3)流失攻击
攻击者拥有网络的一些节点,即恶意节点,这可能会在网络中引发大量流量流失,从而导致网络稳定性降低。

(4)对抗路由攻击
恶意节点在收到查询指令后,不是按照KAD的要求返回距离Key最接近的网络节点,而是转移给同伙节点。同伙节点也做同样的操作,而不返回给查询节点所需要的信息,那么这样一来查询就会失效。我们发现,整个过程中必须将查询信息传递给恶意节点,这一攻击才能发动。那么我们可以在查询时设计算法并行地查询,并且每一条查询路径不相交。这样一来,只要并行查询的路径中有一条不碰到恶意节点,查询就能成功了。

S/Kadmlia算法

S/K节点ID分配策略方案有3个要求:

  • 节点不能自由选择其ID;
  • 不能生成多个ID;
  • 不能伪装和窃取其他节点的ID。

每个节点在接入前必须解决两个密码学问题
静态问题:产生一对公钥和私钥,并且将公钥做两次哈希运算后,具有 c1 个前导零。那么公钥的一次哈希值,就是这个节点的NodeID。 【保证节点不再能自由选择节点 ID】
动态问题:不断生成一个随机数X,将X与NodeID求XOR后再求哈希,哈希值要求有 c2个前导零。 【提高了大量生成ID 的成本】

为确保节点身份不被窃取,节点需要对发出的消息进行签名。
其他节点接收到消息时,首先验证签名的合法性,然后检查节点ID是否满足上述两个难题的要求。
对于网络其他节点验证信息的合法性,它的时间复杂度仅有O(1);但是对于攻击者,为了生成这样一个合法的攻击信息。合理选取c1和c2,就能有效避免这3种攻击方式了。

在KAD协议中,我们进行一次查询时,会访问节点中的a个K-Bucket中的节点,这个K-Bucket 是距离我们需要查询的 Key 最近的。收到回复后,我们再进一步对返回的节点信息排序,选择前a 个节点继续迭代进行请求。很明显这样做的缺点是,一旦返回的其他节点信息是一组恶意节点,那么这个查询很可能就会失败了。

解决办法:
每次查询从d个不同的 Bucket选择k个节点。这d个Bucket并行查找,Bucket内部查找方式和KAD协议完全相同。这样一来,d条查找路径就能做到不相交。对于任意一个 Bucket,有失效的可能,但是只要d个Bucket中有一条查询到了所需要的信息,这个过程就完成了。【解决了对抗路由攻击】

交换层

主要协议:BitSwap
功能:利用信用机制在节点之间进行数据交换

每个节点在下载的同时不断向其他节点上传已下载的数据。
BitSwap 协议中存在一个数据交换市场,这个市场包括各个节点想要获取的所有块数据,这些块数据可能来自文件系统中完全不相关的文件,同时这个市场是由 IPFS 网络中所有节点组成的。

BitSwap

向其他节点请求需要的数据块列表 ( want_list),以及为其他节点提供已有的数据块列表(have_list)。
源码结构如下所示:

type BitSwap struct {
ledgers map[NodeId]Ledger	// 节点账单
active map[NodeId] Peer	// 当前已经连接的节点
need_list [Multihash]		// 此节点需要的块数据校验列表
have_list [Multihash]		// 此节点已收到的块数据校验列表
}

当我们需要向其他节点请求数据块或者为其他节点提供数据块时,都会发送 BitSwap Message 消息。 其中主要包含了两部分内容: 想要的数据块列表(want_list)及对应数据块。消息使用 Protobuf 进行编码。

IPFS 根据节点之间的数据收发建立了一个信用体系:

  • 给其他节点发送数据可以增加信用值;
  • 从其他节点接收数据将降低信用值。

根据上面的信用体系,BitSwap 可以采取不同的策略来实现,每一种策略都会对系统的整体性能产生不同的影响。策略的目标是:

  • 节点数据交换的整体性能和效率力求最高;
  • 阻止空载节点“吃白食”现象,即不能够只下载数据不上传数据;
  • 可以有效地防止一些攻击行为;
  • 对信任节点建立宽松机制。

IPFS 在白皮书中提供了一个可参考的策略机制:
每个节点根据和其他节点的收发数据,计算信用分和负债率( debtratio,r ):

负债率的计算公式:r = bytes_sent / (bytes_recv + 1) 数据发送率:P(send|r) = 1 - ( 1/ ( 1 + exp(6 - 3r)))

如果r大于 2 时,发送率 P(send|r)会变得很小,从而 A 就不会继续给 B 发送数据。

账单(数据收发记录)

BitSwap 节点会记录下来和其他节点通信的账单(数据收发记录)。账单数据结构如下:

type Ledger struct (
owner NodeId
partner NodeId
bytes_sent int
bytes_recv int
timestamp Timestamp
}

BitTorrent

BitTorrent 是一种内容分发协议

BitTorrent 网络里,每个用户需要同时上传和下载数据。文件的持有者将文件发送给其中一个或多个用户,再由这些用户转发给其他用户,用户之间相互转发自己所拥有的文件部分,直到每个用户的下载全部完成。

BitTorrent 中涉及的术语:
torrent: 它是服务器接收的元数据文件,这个文件记录了下载数据的信息 (但不包括文件自身),例如文件名、文件大小、文件的哈希值,以及 Tracker 的 URL 地址。
tracker :是指互联网上负责协调 BitTorrent 客户端行动的服务器。tracker 的作用仅是帮助 peers 相互达成连接,而不参与文件本身的传输。
peer : peer 是互联网上的另一台可以连接并传输数据的计算机。通常情况下,peer 没有完整的文件。peer 之间相互下载、上传。
seed: 有一个特定 torrent 完整拷贝的计算机称为 seed。文件初次发布时需要一个 seed 进行初次共享。
swarm: 连接一个 torrent 的所有设备群组。
Chocking: Chocking 阻塞是一种临时的拒绝上传策略,虽然上传停止了,但是下载仍然继续。
Pareto 效率: 是指资源分配已经到了物尽其用的阶段,对任意一个个体进一步提升效率只会导致其他个体效率下降。此时说明系统已经达到最优状态了。
针锋相对 (Tit-fot-Tat):它强调的是永远不先背叛对方,除非自己被背叛。在 BitTorrent 中表现为,Peer 给自己贡献多少下载速度,那么也就贡献多少上传速度给他。

内容的发布:

1.从 seed 开始进行初次分享 seed 会生成一个扩展名为 .torrent 的文件,它包含如下信息: 文件名、大小、tracker 的 URL
2.tracker 保存文件信息和 seed 的连接信息,而 seed 保存文件本身
3.seed 向 tracker 注册
4.seed等待为需要这个 torrent 的 peer 上传相关信息
5.通过 .torrent 文件,peer 会访问 tracker,获取其他 peer/seed 的连接信息
tracker 和 peer 之间只需要通过简单的远程通信,peer 就能使用连接信息,与其他 peer/seed 沟通,并建立连接下载文件。

分块交换

peer 大多是没有完整的拷贝节点的,BitTorrent 把文件切割成大小为 256KB 的小片.
每一个下载者需要向他的 peer 提供其拥有的片,已经下载的片段必须通过 SHA-1 算法验证。只有当片段被验证是完整的时,才会通知其他 peer 自己拥有这个片段,可以提供上传。

片段选择算法

如何合理地选择下载片段的顺序,对提高整体的速度和性能非常重要.

一系列片段选择的策略:

  • 优先完成单一片段: 如果请求了某一片段的子片段,那么本片段会优先被请求。这样做是为了尽可能先完成一个完整的片段,避免出现每一个片段都请求了同一个子片段,但是都没有完成的情况

  • 优先选择稀缺片段: 选择新的片段时,优先选择下载全部 peer 中拥有者最少的片段。拥有者最少的片段意味着是大多数 peer 最希望得到的片段。这样也就降低了两种风险,其一,某个 peer 正在提供上传,但是没有人下载(因为大家都有了这一片段); 其二,拥有缺片段的 peer 停止上传,所有 peer 都不能得到完整的文件。

  • 第一个片段随机选择: 下载刚开始进行的时候,并不需要优先最稀缺的。此时,下载者没有任何片段可供上传,所以,需要尽快获取一个完整的片段。而最少的片段通常只有某一个 peer 拥有,所以,它可能比多个 peer 都拥有的那些片段下载得慢。因此,第一个片段是随机选择的,直到第一个片段下载完成,才切换到“优先选择稀缺片段”的策略。

结束时取消子片段请求: 有时候,遇到从一个速率很慢的 peer 请求一个片断的情况,peer 会向它的所有的 peer 都发送对某片段的子片断的请求,一旦某些节点发送的子片断到了,那么就会向其他 peer 发送取消消息,取消对这些子片段的请求,以避免浪费带宽。

对象层

IPFS 使用Merkle DAG 技术构建了一个有向无环图数据结构,用来存储对象数据。

Merkle DAG

属性:
内容可寻址:所有内容由多重哈希校验并唯一标识。
防止篡改:所有内容都通过哈希验证,如果数据被篡改或损坏,在 IPFS网络中将会被检测到。
重复数据删除:保存完全相同内容的所有对象都是相同的,并且只存储1次。这对于索引对象特别有用。

Merkle DAG是IPFS 的存储对象的数据结构,Merkle Tree 则用于区块链交易的验证。

Merkle DAG

节点包括两个部分:
Data :二进制数据 【本对象内容】
Link:包含 Name、Hash 和 Size 【保存其他的分块数据的引用】
【用于验证数据完整性】

Hash 是一个把任意长度的数据映射成固定长度数据的函数
Hash List:每个数据块计算 Hash 值,在下载到真正数据之前,我们会先下载一个 Hash 列表
把每个小块数据的 Hash 值拼到一起,然后对这个长字符串再做一次 Hash 运算,这样就得到 Hash 列表的根 Hash ( Top Hash 或 Root Hash)。

下载数据的时候,首先从可信的数据源得到正确的根 Hash,就可以用它来校验Hash 列表了,然后即可通过校验后的 Hash 列表校验数据块的完整性。

功能:
内容寻址: 使用多重 Hash 来唯一识别一个数据块的内容。 防篡改: 可以方便地检查 Hash 值来确认数据是否被篡改。 去重: 由于内容相同的数据块 Hash 值是相同的,很容易去掉重复的数据,节省存储空间。

在 IPFS 系统中,每个 Blob的大小限制在 256KB(暂定为 256KB,这个值可以根据实际的性能需求进行修改)以内,那些相同的数据就能通过 Merkle DAG 过滤掉,只需增加一个文件引用,而不需要占据存储空间。

Merkle Tree
叶子是数据块的哈希值
非叶节点是其对应子节点串联字符串的哈希
【大多是用于文件系统】

从数据结构上看,Merkle DAG 是 Merkle Tree 更普适的情况,换句话说,Merkle Tree 是特殊的 Merkle DAG。

在最底层,和 Hash 列表一样,把数据分成小的数据块,有相应的 Hash 与它对应。往上走,把相邻的两个 Hash 合并成一个字符串,然后运算这个字符串的 Hash。
这样每两个 Hash 就“结婚生子”得到了一个“子 Hash”。
如果最底层的 Hash 总数是单数,那到最后必然出现一个“单身 Hash”,这种情况就直接对它进行 Hash 运算,所以也能得到它的“子Hash”
于是往上推,依然是一样的方式,可以得到数目更少的新一级 Hash,最终形成一棵倒挂的树,树根位置就是树的根 Hash,我们把它称为 Merkle Root。

Merkle Tree 和 Hash List 的主要区别是:
Merkle Tree 可以直接下载并立即验证一个分支。如果文件很大,Merkle Tree 可以一次下载一个分支,然后立即验证这个分支,如果分支验证通过,就可以下载数据了。 而 Hash List 只有下载整个 Hash List 才能验证。

Merkle Tree的应用: 数字签名、P2P 网络、比特币

文件层

IPFS 还定义了一组对象,用于在 Merkle DAG 之上对版本化文件系统进行建模。

块(block):一个可变大小的数据块。 列表(list):一个块或其他列表的集合 树(tree):块、列表或其他树的集合。 提交 (commit): 树版本历史记录中的快照.

Blob 对象包含一个可寻址的数据单元,表示一个文件。 当文件比较小,不足以大到需要分片时,就以 Blob 对象的形式存储于 IPFS 网络之中,如下所示:

{
	"data": "some data here",//Blobs无links
}

List 对象由多个连接在一起的 Blob 组成,通常存储的是一个大文件。

{
data": ["blob","list","blob"],
"links": [
{"hash": "XLYkgq61DYaQ8Nhkcqyu7rLcnSa7dSHQ16x""size": 189458},
{"hash":"XLHBNmRQ5sJJrdMPuu48pzeyTtRo39tNDR5""size": 19441},
{"hash":"XLWVQDqxo9Km9zLyquoC9gAP8CLlgWnHZ7z""size": 5286}
]
}

Tree 对象代表一个目录,或者一个名字到哈希值的映射表。

{
"data":["blob", "list", "blob"],
"links":[
{"hash":"XLYkgq61DYaQ8Nhkcqyu7rLcnSa7dsHQ16x", "name":"less", "size": 189458 },
{"hash":"XLHBNmRQ5sJJrdMPuu48pzeyTtRo39tNDR5","name":"script","size": 19441},
{"hash":"XLWVQDqxo9Km9zLyquoC9gAP8CL1gWnHZ7z","name": "template","size": 5286}
]
}

在IPFS 中,Commit 对象代表任何对象在版本历史记录中的一个快照,它可以指向任何类型的对象

树缓存 ( tree cache):由于所有的对象都是哈希寻址的,它们可以被无限地缓存。另外,Tree 一般比较小,所以比起 Blob,IPFS 会优先缓存Tree。

扁平树 ( fattened tree):对于任何给定的 Tree,一个特殊的扁平树可以构建一个链表,所有对象都可以从这个 Tree 中访问得到。在扁平树中name 就是一个从原始 Tree 分离的路径,用斜线分隔。

命名层

IPFS 形成了一个内容可寻址的 DAG 对象,我们可以在 IPFS 网络中发布不可更改的数据,甚至可以跟踪这些对象的版本历史记录。

使用自验证的命名方案给了我们一种在加密环境下、在全局命名空间中构建可自行认证名称的方式。

模式:

  • 通过 Nodeld = hash(node.PubKey),生成 IPFS 节点信息
  • 给每个用户分配一个可变的命名空间,由之前生成的节点 ID 信息作为地址名称,在此路径下: /ipns/。
  • 一个用户可以在此路径下发布一个用自己私钥签名的对象,比如:/ipns/XLF2ipQ4jD3UdeX5xp1KBgeHRhemUtaA8Vm/。
  • 当其他用户获取对象时,他们可以检测签名是否与公钥和节点信息相匹配,从而验证用户发布对象的真实性,达到了可变状态的获取。