让智能合约在数据分发中更智能?PPIO 的设计小巧思

在之前的文章中,如《 PPIO 在分发领域的优势》《 PPIO 在流媒体设计上的考虑》,我们分别从从架构层面和流媒体 P2P 的传输技术层面上介绍了PPIO 的设计与优势,但是这部分内容和区块链以及智能合约的关系不密切。在这篇文章中,我们将不仅仅介绍 PPIO 关于数据分发层面的区块链和智能合约设计,而且想发起一次讨论,希望借助社区的力量一起将这个设计变得更加完美。

数据分发场景的支付方式

数据分发的支付方式和数据存储是不同,数据存储的商业模式是用户付费,一般是用户为购买存储而付费。而数据分发场景不同,一般是由开发者或者运营方支付带宽和流量。所以我们在设计数据分发场景的支付方式的时候,重点考虑的是开发者付费的智能合约,在这里,我们首先引入 Owner 的角色。

Owner 的角色

我们在 PPIO 中加入了 Owner 机制。Owner 不是一个 P2P 传输角色,而是一个支付和结算角色。在 PCDN 架构中,每个Peer 都需要指定一个 Owner。这个 Peer 产生的花费由它的 Owner 来承担,而同样该 Peer 赚取的收入也由它的 Owner 来接收。

如下图,同一个 Owner 可以对接多个 Peer。

图:Owner 和 Peer 的关系图

对 Owner 这个角色来说,如果在需求端,可以理解为 CoinPool(代理支付网关,详情可以参考文章《为什么 PPIO 要设计代理支付网关》),如果在供给端,也可以理解为“矿池”。对 PCDN 业务来说,使用带宽的花费一般是由内容发布者来承担,链上结算发生在 Owner 之间。

对开发者来说,既可自己部署代理支付网关 CoinPool(即 Owner ),也可以接入其他第三方代理支付网关 CoinPool(即 Owner)。同一 Owner 内部的 Peer 间数据发生交换,是不会涉及到链上结算的。

简单的数据分发合约

接下来将详细介绍数据分发的智能合约,为了便于读者理解,我们先将模型简化,构建一个最简单的场景,即只有2个 Peer 之间发生通讯,且这两个 Peer 分别隶属于 Owner1 和 Owner2。那么当一个 Peer 从另外一个 Peer 中下载数据的时候,Owner1 和 Owner2 之间会发生怎样的结算呢?

在这个最简单的模型中,其交互流程如下:

  1. Owner 先将足够的预支付款打到智能合约。
  2. User 从 Owner1 处申请一个 Check(类似于支票),Check 中含有这个智能合约的地址,以及最大消费额度,CheckID,过期时间等。
  3. Owner 审阅这个 Check 请求是否符合要求,若符合则将 Check 返还给 User;若不符合,比如申请的 Token 过多,则可以拒绝。
  4. User 向 Miner 请求数据(通过 P2P 数据传输协议)且顺利收到数据。
  5. User 会将 Check 扩展成 Voucher(类似于凭据),并发给 Miner,这个过程(步骤4和5)是多次的,Voucher 涉及的金额会越来越大。
  6. 当下载完成或异常终止时,就会提交该 Voucher 到 Miner 的 Owner,即  Owner2。
  7. Owener2 收到该 Voucher,会向该区块链智能合约发起提款。
  8. 如果 Voucher 正确且未超时,那么就能提款成功。

图:PPIO 最简单的下载流程图

如上图所示,最终通过智能合约,完成了 Owner1 和 Owner2 的交易过程。在这个过程中,只有利用 Voucher(票据)提款是链上的操作行为,其他步骤都是在链下进行交互。而 Peer 和 Peer 之间的通讯,也是采用状态通道的方式,来进行可信传输的。数据分发的状态通道机制是个很巧妙的设计,我们将会在接下来的文章中详细介绍 PPIO 的数据传输的状态通道。

当然,实际情况没有那么简单。在真实的下载流程中可能会发生各种各样的问题:

一个是过期问题。Owner 在给出 Check 的时候,会声明 Check 的过期时间;只有在声明时间内用 Voucher 来进行提款,提款才能成功。如果超出过期时间,Voucher 将无法在链上进行提款。

另一个是信任问题。Peer1(如 User )和 Peer2(如 Miner )之间可能存在欺诈。举例来说,Peer1 先向 Peer2 申请了数据,当 Peer2 给予了所有完整的数据之后,在正常情况下,Peer1 应该将 Voucher 给 Peer2,以便 Peer2 去链上进行合约提款。但是,如果 Peer1 已经下载了所有的数据,却不给 Peer2 Voucher。面对这样的情况,我们该怎么办?

这里有个简单的解决方案,就是 Peer2 以密文的形式将数据传送给 Peer1,密文需要密钥进行解密才可使用。Peer2 在确保拿到 Voucher 之后,再给予Peer1 密钥,Peer1 方可使用数据。但是 PPIO 采用不同的方案,因为

  1. PCDN 对数据传输的实时性有很强的要求,特别在流媒体传输的应用场景中。下载完成再进行解密,这种机制不能适应流媒体类传输,或许在存储场景中更实用。
  2. Peer2 也有作恶的可能性,即骗取了 Voucher 之后没有按时给予密钥,或给出错误的密钥。

综上,这个简单的解决方案也没有完美地解决信任问题。因此,PPIO 采用了这样的微支付方案。即一个 Check 可以对应多个 Voucher,每次传输一小部分数据,Peer1 就可以给予一个 Voucher,即 Voucher1,这个 Voucher 声明可以提款部分;数据接下来继续传输,再传输完成一小部分数据后,Peer1 在给予一个新的 Voucher,即 Voucher2,这个 Voucher 声明可以提款的更多部分 ,反复多次,直到传输完毕,Peer2 收到 VoucherN, Peer2 最后用让 Owner2 用 VoucheN 提款所有的钱。

图:改进后的下载流程图

在改进后的下载流程图中,我们相当于把之前第4步和第5步分成多步来执行。这样的改善大大降低了作恶的风险,试想,如果 Peer1 作恶,即接受数据之后不给 Voucher,那么 Peer2 该情况后就不会再传输新的数据,这样可将 Peer2的损失控制在一个最小限度内。

注意,对于同一个 Check 对应的多个 Voucher 来说,它们之间是覆盖关系。Owner2 尽量只使用最大提款额度的 Voucher 进行提款,不需要使用每个 Voucher 一一提款,因为新的 Voucher 会覆盖旧的 Voucher,这样设计是为了减小对区块链的 TPS 要求。

介绍完了简单的一对一场景,下面将分析更加复杂的场景。

相同 Owner 的策略

相同 Owner,指的是进行传输数据的所有 Peer对接的是同一个 Owner。如果从交易的角度看,即Peer1 和 Peer2 之间发生内部交易,由于 Peer1 的付款和 Peer2 的收款都是由 Owner 来完成的,Owner 既付款,又收款,相当于进行了一次内部交易,其整个过程是不需要上链的。

图:同一 Owner 下载流程图

如上图,如果 Owner 确定只有内部 Peer 之间数据传播,不会从其他 Owner 的 Peer 那里传输数据,那么步骤1 也是可以省去的。

PPIO 为什么要这样设计呢?想象一下,在实际应用场景中,Owner 的角色是开发者,开发者开发了一款应用 App,这个 App 被很多人安装并使用。每个 App 就是一个 Peer,所有这些 App 构建了一个 P2P 网络,当其中一个App(Peer)要下载数据的时候,就会从其他 App(Peer)中获取数据进行下载。而由于这些 App 都隶属于同一个 Owner,所有的传输费用都在 Owner 内部抵消掉了。也可以理解为,对同一个开发者建立的 P2P 传输,是零费用的。因为没有新数据需要上链,所以同时也不会对区块链产生任何 TPS 开销。这样的设计就兼容了经典的 P2P 机制,即我下载我上传,供需一体,如 Bittorrent, EDonkey, PPTV 都使用过该机制。

对于跨开发者的数据传输来说,也就是跨 Owner 的数据传输,就会产生费用。如果该开发者建立的 P2P 网络是对外输出流量的,这个开发者将盈利;反之,如果该开发者的 App,是从其他 Owner 的 Peer 那里获得流量,这个开发者将支付费用给其他开发者。

下面我们将介绍不同 Owner 下的多 Peer 传输策略。

不同 Owner 的多 Peer 传输策略

前面讲得都是比较单一的传输,这里解析一下更为复杂的多 Peer 传输逻辑。

在设计 PPIO 的时候,出于另一个考虑,我们把 Owner 和 Peer 进行了分层设计。Owner 层负责资金流,而 Peer 层则负责数据流。只有不同的 Owner 之间进行资金变化的时候,才需要执行区块链上的智能合约。

图:资金流和数据流

在这个设计中,有个难题,就是 Owner1 从区块链智能合约中申请的 Check 是怎么被分拆给多个 Peer,又是怎么扩展成多个 Voucher,最后汇总到 Peer2 这里,而 Peer2 又是如何从去区块链智能合约中提款的呢?

下面举个相对形象的简单例子,即开发者 Owner1 对接多个 Peer,分别是 Peer1 和 Peer3;矿场组 Owner2 对接多个 Miner,分别为 Peer2 和 Peer4 。在 P2P 网络的调度中,正好出现了 Peer1 从 Peer2 下载数据,同时 Peer3 从 Peer4 下载数据的情况。如下图:

图:多对多的跨 Owner 逻辑图

基于上图来讲解一下整个步骤:

  1. 第1步:正如之前提及的最简单模型的流程图,Owner 先将足够的预支付款打到智能合约。然后拆分这些款项为多个 Check,即 Check_a, Check_b,Check_c,Check_d 等,至于每份 Check 包含多少款项,视业务情况决定。
  2. 第2步:Peer1 连上了 Peer2,并想要下载数据。于是 Peer1 从 Owner1 处申请了 Check ,Owner1 限制了金额,给予了 Check_a。
  3. 第3步:同时,Peer3 连上了 Peer4,并想要下载数据。同步骤二类似,Peer3申请了 Check_b。
  4. 第4步:Peer1 从 Peer2 处不停地下载数据,Peer1 多次给予 Peer2 Check_a的 Voucher,直到 Check_a 中的钱都用完。Peer2 最终获得完整的 Voucher_a。
  5. 第5步:同步骤四类似,Peer3 从 Peer4 不停地下载数据,直到 Check 的钱都用完。Peer4 最终获得完整的 Voucher_b。
  6. 第6步:Peer2 将 Check_a 最终的 Voucher,即 Voucher_a 交给它的 Owner,即 Owner2。
  7. 第7步:Owner2 用这个 Voucher_a 从区块链上的智能合约中提款。
  8. 第8步:Peer4 将 Check_b 最终的 Voucher,即 Voucher_b 交给他的 Owner,即 Owner2。
  9. 第9步:Owner2 用这个 Voucher_b 从区块链上的智能合约中提款。
  10. 第10步:Peer1 已经用完了 Check_a 的额度;但是数据还没有下载完成,继续从 Owner1 申请新的 Check 进行下载,即 Check_c。
  11. 第11步:Peer2 已经用完了 Check_b 的额度;但是数据还没有下载完成,继续从 Owner1 申请新的 Check 进行下载,即 Check_d。
  12. 重复前面的结构。

这样的设计对安全性的提升是有极大的帮助的:

  1. 防止付款方 Owner 的 Peer 薅羊毛:Owner 在收到 Peer 请求新 Check 的要求时,可以根据 Peer 历史请求记录和 Peer 实际已消费的额度来决定是否要授予新的 Check。这样可以有效地防止恶意的 Peer 通过无节制的申请 Check 来薅羊毛。
  2. 杜绝付款方 Peer 将同一 Check 双花(double-spending):Check 中指定了Pool 地址,收款方 Owner 地址,以及服务方 Peer 的公钥,因此作为付款方的 Peer 无法通过申请一个 Check 就可以到不同的收款方 Peer 那里去消费,针对同一 Check 而言,杜绝了双花的可能性。
  3. 微支付方式将可能的损失降到最低:采用类似状态通道的微支付方式,实现  Voucher 与数据的交易。Miner 给一点数据,User 就给相对应钱,Miner 再给一点数据 User 就再给一部分相对应的钱,往复多次,直到下载完成。在交易过程中任何一方如果违约,都可以及时发现问题,将损失降到最小。

智能合约的数据结构

到此我们介绍了 PPIO 数据分发方面的智能合约的设计理念,接下来介绍区块链智能合约的设计。在前面的设计中,我们不难看出,与智能合约相关的只有 Check 和 Voucher。

Check:由于所有 Peer 都没有链上账户,因此当某个 Owner 的某个 Peer 在向其他 Peer 支付时,首先应向其 Owner 申请 Check。Check 中包含了唯一的 Check ID、最大消费的金额、有效期、取款智能合约地址、收款方 Owner 地址、付款 Peer 和收款 Peer 的公钥。并且,Check 中带有 Owner 的签名。

type Check struct {    pool           base.Address     // Contract address    payer          base.PublicKey   // The payer peer’s public key    receiver       base.Address     // The receiver owner’s address    payee          base.PublicKey   // The payee peer’s public key    maxAmount      base.Uint256     // The max amount payer can spend    checkId        base.Uint256     // The unique CheckId    expiry         uint64    // The expiry time of this check    ownerSignature base.Signature   // The signature of payer peer’s owner}

Voucher:完整的 Voucher 在逻辑上包含了整个 Check 的内容,同时还额外包含实际支付的金额数。并且,Voucher 中带有付款 Peer 的签名。

type Check struct {    pool           base.Address     // Contract address    payer          base.PublicKey   // The payer peer’s public key    receiver       base.Address     // The receiver owner’s address    payee          base.PublicKey   // The payee peer’s public key    maxAmount      base.Uint256     // The max amount payer can spend    checkId        base.Uint256     // The unique CheckId    expiry         uint64    // The expiry time of this check    ownerSignature base.Signature   // The signature of payer peer’s owner}

Voucher:完整的 Voucher 在逻辑上包含了整个 Check 的内容,同时还额外包含实际支付的金额数。并且,Voucher 中带有付款 Peer 的签名。

type Voucher struct {    Check          // The Check    amount         base.Uint256  // The amount payer peer spends    payerSignature base.Signature   // The payer peer’s signature}

实际产生的 Voucher 的金额可能会小于 Check 的最大金额的。所以在申请  Check 的时候,申请的金额数字可以大于实际预测发生的。

Owner SDK

PPIO 专门设计了 Owner SDK(CoinPool SDK)。基于下载、分发场景,第三方开发者可以使用 Owner SDK 来快速创建 Owner,即代理支付网关(CoolPool) 或者 矿池(MinePool)。

Owner SDK 基于 gRPC 框架设计并实现了两个协议:

  1. ApplyCheck: Peer 请求 Check 用于从其他 Peer 下载资源
  2. ReportVoucher: Peer 上报其所获得的 Voucher,这些 Voucher 是通过其他  Peer 节点签发所得。

图:第三方应用接入 SDK 后的整体结构图

同样,开发者也要实现两个与协议相对应的回调接口:

  • OnApplyCheck:用于告知申请 Check 的情况
  • OnRecvVoucher:用于告知收到 Voucher 的情况

看到这里,你是否对 PPIO 数据分发层面的区块链和智能合约设计有了更清晰的认知和更详细的了解了呢?我们在原有的智能合约上进行了改善,使其更加灵活,符合 PPIO 数据分发的应用场景,以便保证 QoS,使用户享受最好的使用体验。

当然,如果你有更多关于 PPIO 的想法,欢迎在 PPIO 开发者社区,Discord 群组,或者 Github 上提出你的建议,让我们一起将 PPIO 打磨得更加完善!