区块链技术迅猛发展,新想法、新概念、新名词层出不穷。万向区块链因此推出“技术研究报告”专栏,定期与大家分享在区块链行业创新及热门技术方面的研究成果,带领大家第一时间研究学习新技术,紧跟技术发展趋势,探索发掘技术的应用价值。
本期技术研究将带大家了解区块链应用平台Polygon。
本文作者:万向区块链通用架构技术部 杨毅
概述
Polygon 是一个区块链应用平台,提供 PoS 和 Plasma 两种侧链。 Polygon PoS 网络具有三层架构:
- 以太坊层(根合约层):部署在以太坊主网上的一组质押管理合约。
- Heimdall 层(验证节点层):一组 PoS Heimdall 节点,与以太坊主网并行运行,监听部署在以太坊主网上的一组质押合约,并将 Polygon 网络检查点提交给以太坊主网。Heimdall 的共识算法基于 Tendermint。
- Bor 层(出块节点层):一组从 Heimdall 节点中选出的小部分节点作为的 Bor 出块节点,它是一个基本的 Geth 实现。
概念说明
Polygon 架构
质押合约
为了在 Polygon 上启用权益 证明 (PoS)机制,Polygon 在以太坊主网上部署了一组 PoS 管理合约。 质押合约实现以下功能:
- 任何人都可以在以太坊主网上的质押合约上质押 MATIC 资产,并作为 验证者加入系统。
- 通过验证 Polygon 网络上的状态转换获得质押奖励。
- 对双重签名、验证者停机等活动启用惩罚/削减。
- 在以太坊主网上保存 检查点。
PoS 机制还可以缓解 Polygon 侧链的数据不可用问题。
Heimdall(验证节点层)
Heimdall 是 Pos 验证层,负责将 Bor生成的块聚合到 Merkle 树中,并定期将 Merkle 根发布到主链。这一层需要负责的功能如下:
- 验证自上一个检查点以来的所有块。
- 创建块哈希的 Merkle 树。
- 将 Merkle 根哈希发布到以太坊主网。
验证者选择: 验证者是通过链上拍卖过程选择的,该过程按定义的时间定期进行,过程如下所述:
- 在StateManager合约上调用StakeFor() 函数以锁定链上的状态。
- heimdall上的 Bridge 收听此事件并向所有 heimdall 广播
- 共识验证程序添加到 heimdall 但未激活。
- Validator仅在 StartEpoch 之后才开始验证
- 一旦 StartEpoch 到达,验证者就将其添加到验证者集中,并开始参与共识机制
成为验证者: 要成为 Polygon 网络上的验证者,您必须做以下操作:
- Sentry 节点(哨兵节点) 运行 Heimdall 节点和 Bor 节点的独立机器。哨兵节点对 Polygon 网络上的所有节点开放。
- Validator 节点 运行 Heimdall 节点和 Bor 节点的独立机器。验证节点从哨兵节点接收数据并将数据发送到哨兵节点。
- 在部署在以太坊主网上的质押合约中质押 MATIC 资产。
Heimdall 详细说明:https://docs.polygon.technology/docs/pos/heimdall/overview
Bor(出块节点)
Bor 块生产者是验证者的一个子集,并由 Heimdall验证者定期洗牌。 Bor 是 Polygon 的区块生产者层,负责将交易聚合成区块的实体。目前,它是一个基本的 Geth 实现,对共识算法进行了自定义更改。 区块生产者通过 Heimdall 上的委员会选择定期改组,区块生产者的持续时间称为 Polygon 中的一个 span
(跨度) 。区块在Bor节点产生,其 VM 与 EVM 兼容。在 Bor 上产生的块会由 Heimdall 节点定期验证,这些块的哈希会在 Heimdall 层被组成一颗 Merkle 树,这颗 Merkle 树的根哈希会被当作一个检查点由 Heimdall 定期提交给以太坊。 生产者选择: Bor 层的区块生产者是根据权益从验证者池中选出的小委员会。
- 验证人根据质押获得插槽,如果验证人有 100 个 Matic 资产质押,并且每个插槽为 10,他将总共获得 10 个插槽。
- 假设所有验证者插槽为这个数组 [ A, A, A, B, B, C ]
- 使用历史以太坊区块作为种子来对这个数组进行洗牌。
- 使用种子对插槽进行洗牌后,我们得到了这个数组 [ A, B, A, C, B, A, A]
- 现在我们从顶部弹出验证者,例如,如果我们想选择 3 个生产者,我们将生产者设置为 [ A, B, A]
- 因此,下一个跨度的生产者集定义为 [ A: 2, B:1 ]
- 使用这个验证者集和 tendermint 的提议者选择算法,我们为 Bor 上的每个 sprint 选择一个生产者。
检查点
检查点是 Matic 协议中最关键的部分。它代表 Bor 链状态的快照,至少由 ⅔ 的验证者签名后,才能在部署在以太坊上的合约上进行验证和提交。 检查点之所以重要,有两个原因:
- 在根链上提供最终确定性。
- 在提取资产时提供燃烧证明。
流程概览:
- 选择池中的一部分活跃的验证者作为一个Span(跨度)的出块节点。这些出块节点负责打包区块并在网络上广播这些区块。
- 检查点包括在任何给定时间间隔内创建的所有块的 Merkle 根哈希。所有节点都验证 Merkle 根哈希并将其签名附加给它。
- 从验证者集中选出的一个 Proposal 来负责收集特定检查点的所有签名并在以太坊主网上提交检查点。
- 被选中作为区块生产者和提交检查点的 Proposal 的权重取决于验证者在整个池中的股权比例。
检查点数据结构
type CheckpointBlockHeader struct {// Proposer is selected based on stakeProposertypes.HeimdallAddress `json:"proposer"`// StartBlock: The block number on Bor from which this checkpoint startsStartBlockuint64`json:"startBlock"`// EndBlock: The block number on Bor from which this checkpoint endsEndBlockuint64`json:"endBlock"`// RootHash is the Merkle root of all the leaves containing the block // headers starting from start to the end block RootHashtypes.HeimdallHash`json:"rootHash"`// Account root hash for each validator// Hash of data that needs to be passed from Heimdall to Ethereum chain like slashing, withdraw topup etc.AccountRootHash types.HeimdallHash`json:"accountRootHash"`// Timestamp when checkpoint was created on HeimdallTimeStamp uint64`json:"timestamp"`}
- RootHash
RootHash
是从StartBlock
到EndBlock
的 Bor 块哈希的 Merkle 根哈希。
检查点的根哈希是使用以下方式创建的:
blockHash = keccak256([number, time, tx hash, receipt hash])
1
到 n
的区块的 RootHash 的计算伪代码:
B(1) := keccak256([number, time, tx hash, receipt hash]) B(2) := keccak256([number, time, tx hash, receipt hash]) . . . B(n) := keccak256([number, time, tx hash, receipt hash]) // checkpoint is Merkle root of all block hash checkpoint's root hash = Merkel[B(1), B(2), ....., B(n)]
-
AccountRootHash
是需要在每个检查点传递到以太坊链的验证者账户相关信息的哈希值。创建方式如下
// id eachAccountHash := keccak256([validator id, withdraw fee, slash amount])
1
到n
的区块的AccountRootHash
的计算伪代码:
B(1) := keccak256([validator id, withdraw fee, slash amount]) B(2) := keccak256([validator id, withdraw fee, slash amount]) . . . B(n) := keccak256([validator id, withdraw fee, slash amount]) // account root hash is Merkle root of all block hash checkpoint's account root hash = Merkel[B(1), B(2), ....., B(n)]
检查点在主链上的管理
pragma solidity 0.6.6;import {SafeMath} from "@openzeppelin/contracts/math/SafeMath.sol";import {ICheckpointManager} from "./ICheckpointManager.sol";/*** @notice Mock Checkpoint Manager contract to simulate plasma checkpoints while testing*/contract MockCheckpointManager is ICheckpointManager {using SafeMath for uint256;uint256 public currentCheckpointNumber = 0;function setCheckpoint(bytes32 rootHash, uint256 start, uint256 end) public {HeaderBlock memory headerBlock = HeaderBlock({root: rootHash,start: start,end: end,createdAt: now,proposer: msg.sender});currentCheckpointNumber = currentCheckpointNumber.add(1);headerBlocks[currentCheckpointNumber] = headerBlock;}}
检查点生命周期
1. 整体流程
2. 在各层级中的流程
状态同步机制
- 主链 部署在主链的合约在需要发送事件时,会通过 StateSender 状态发送器来触发。
pragma solidity 0.6.6;interface IStateSender {function syncState(address receiver, bytes calldata data) external;}
- 子链 子链节点会监听主链的特定事件并解析,然后通过 系统调用的方式来调用子链上的状态同步合约进行状态同步。只有
0x0000000000000000000000000000000000001001
这个地址才能进行状态同步调用。 系统调用有助于在不进行任何交易的情况下更改合约状态。pragma solidity 0.6.6;interface IStateReceiver {function onStateReceive(uint256 id, bytes calldata data) external;}
资产转移(PoS桥)
简述
将资产从以太坊转移到 Polygon 再回到以太坊的完整周期的过程可以总结如下:
- 资产(ERC20/ERC721/ERC1155) 资产的所有者必须批准 PoS 桥上的特定合约,以质押待转让的资产金额。这个特定的合约称为谓词合约(部署在以太坊网络上),它实际上锁定了要存入的资产数量。
- 一旦获得批准,下一步就是存入资产。必须对RootChainManager合约进行函数调用,从而触发Polygon 链上的ChildChainManager合约。
- 这是通过状态同步机制发生的,可以从 这里详细了解。(大约需要 5~7 分钟)
- ChildChainManager 内部会调用子资产合约的存款函数,将相应数量的资产资产铸造到用户在 Polygon 上的账户。需要注意的是,只有 ChildChainManager 可以访问子资产合约上的存款功能。
- 一旦用户获得资产,立马就可以进行交易,在 Polygon 链上交易费用低到可以忽略不计。
- 将资产撤回以太坊是一个两步过程,其中资产资产必须首先在 Polygon 链上烧毁,然后必须在以太坊链上提交此烧毁交易的证明。
- 烧毁交易被包含到检查点中再提交到以太坊链大约需要 20 分钟到 3 小时。这是由权益证明验证者完成的。
- 将烧毁交易添加到检查点后,可以通过在以太坊的 RootChainManager 合约上调用 exit 函数提交销毁交易的证明。
- 此函数(exit)会调用验证者们去验证对应的检查点是否包含对应的燃烧交易证明,验证成功就会触发在最初存入资产时锁定资产的谓词合约。
- 最后一步,谓词合约释放锁定的资产并将其退还给用户在以太坊上的账户。
详细流程
存款:
取款:
Polygon POS桥支持转移 ERC20/ERC721/ERC1155 资产,每一种资产的转移流程都相差不大,只是调用的具体合约实现有所不同,这里就只以 ERC20 作为示例,具体步骤如下:
1.【主】创建资产映射
创建从主链到子链上资产映射,即在主链质押什么类型的资产,从而在子链上生成同等类型和同等数量的资产。
- rootToken:生成该资产的主链合约地址
- childToken:生成该资产的子链合约地址
- tokenType:资产类型
function mapToken(address rootToken,address childToken,bytes32 tokenType) external;
2. 【主】允许谓词合约扣款
通过调用资产合约的 approve 函数来批准 ERC20Predicate 扣除资产,批准之后才能存款。
- spender:批准使用用户资产的地址(谓词合约地址)
- amount:批准使用的资产数量
await rootTokenContract.methods.approve(erc20Predicate, amount).send({ from: userAddress })
3.【主】存款(质押)
调用 RootChainManager 合约的 depositFor 函数来进行资产质押。
- user:在 Polygon 链上接收存款的用户地址
- rootToken:是被质押的资产的合约在主链上的地址
- depositData: abi 编码后的金额。
注意:在进行此调用之前,需要创建资产映射且必须批准质押金额。
const depositData = mainWeb3.eth.abi.encodeParameter('uint256', amount)await rootChainManagerContract.methods.depositFor(userAddress, rootToken, depositData).send({ from: userAddress })
4.【子】销毁
通过调用子资产合约的取款函数,可以在 Polygon 链上销毁资产。因为在退出步骤中需要提交此燃烧证明,所以需要存储该燃烧交易的哈希。
- amount:表示要燃烧的资产数量。
const burnTx = await childTokenContract.methods.withdraw(amount).send({ from: userAddress })const burnTxHash = burnTx.transactionHash
5.【主】退出
必须调用 RootChainManager 合约上的退出函数来解锁并从 ERC20Predicate 取回资产。必须等待包含销毁交易的检查点被提交到主链后再调用此函数才有用。
- inputData:销毁交易的证明
function exit(bytes calldata inputData) external;
证明由以下字段经过 RLP 编码生成:
- headerNumber – 【主】包含销毁交易数据的检查点号数
- blockProof – 【子】包含销毁交易的区块在检查点中的叶子哈希(区块哈希)
- blockNumber – 【子】包含销毁交易的区块号
- blockTime – 【子】包含销毁交易的区块的时间
- txRoot – 【子】包含销毁交易的区块的txRoot
- receiptRoot – 【子】包含销毁交易的区块的receiptRoot
- receipt – 【子】销毁交易的收据
- receiptProof – 【子】销毁收据的 Merkle 证明
- branchMask – 【子】32 bits,表示该 receipt 在 MPT 树中的路径
- receiveLogIndex – 【子】可以从 receipt 中读取到销毁交易的日志的索引
6. 【主】退出引证
6.1. 生成销毁证明数据 手动生成证明可能很棘手,因此建议使用 Polygon Edge。如果您想手动发送交易,您可以在选项对象 中将 encodeAbi 传递为 true 以获取原始调用数据。
const exitCalldata = await maticPOSClient.exitERC20(burnTxHash, { from, encodeAbi: true })
6.2. 发送退出交易(附带销毁证明数据)到主链合约 将 calldata 发送到 RootChainManager。
await mainWeb3.eth.sendTransaction({from: userAddress,to: rootChainManagerAddress,data: exitCalldata.data})
6.3. 主链RootChainManager.exit()
- 解析 calldata 拿到相关参数
- 只处理没有处理过的退出交易(不能重复退出)
- 设置当前退出交易为已处理
- 验证是否包含 receipt
- 验证是否包含检查点
- 通过谓词合约去释放退出用户的资产
function exit(bytes calldata inputData) external override {ExitPayloadReader.ExitPayload memory payload = inputData.toExitPayload();bytes memory branchMaskBytes = payload.getBranchMaskAsBytes();// checking if exit has already been processed// unique exit is identified using hash of (blockNumber, branchMask, receiptLogIndex)bytes32 exitHash = keccak256(abi.encodePacked(payload.getBlockNumber(),// first 2 nibbles are dropped while generating nibble array// this allows branch masks that are valid but bypass exitHash check (changing first 2 nibbles only)// so converting to nibble array and then hashing itMerklePatriciaProof._getNibbleArray(branchMaskBytes),payload.getReceiptLogIndex()));require(processedExits[exitHash] == false,"RootChainManager: EXIT_ALREADY_PROCESSED");processedExits[exitHash] = true;// 解析 receipt 数据ExitPayloadReader.Receipt memory receipt = payload.getReceipt();ExitPayloadReader.Log memory log = receipt.getLog();// log should be emmited only by the child tokenaddress rootToken = childToRootToken[log.getEmitter()];require(rootToken != address(0),"RootChainManager: TOKEN_NOT_MAPPED");address predicateAddress = typeToPredicate[tokenToType[rootToken]];// branch mask can be maximum 32 bitsrequire(payload.getBranchMaskAsUint() &0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000 ==0,"RootChainManager: INVALID_BRANCH_MASK");// 验证收据内容require(MerklePatriciaProof.verify(receipt.toBytes(),branchMaskBytes,payload.getReceiptProof(),payload.getReceiptRoot()),"RootChainManager: INVALID_PROOF");// 验证检查点内容_checkBlockMembershipInCheckpoint(payload.getBlockNumber(), payload.getBlockTime(), payload.getTxRoot(), payload.getReceiptRoot(), payload.getHeaderNumber(), payload.getBlockProof());// 取款ITokenPredicate(predicateAddress).exitTokens(_msgSender(),rootToken,log.toRlpBytes());}function _checkBlockMembershipInCheckpoint(uint256 blockNumber,uint256 blockTime,bytes32 txRoot,bytes32 receiptRoot,uint256 headerNumber,bytes memory blockProof) private view returns (uint256) {(bytes32 headerRoot,uint256 startBlock,,uint256 createdAt,) = _checkpointManager.headerBlocks(headerNumber);require(keccak256(abi.encodePacked(blockNumber, blockTime, txRoot, receiptRoot)).checkMembership(blockNumber.sub(startBlock),headerRoot,blockProof),"RootChainManager: INVALID_HEADER");return createdAt;}
谓词合约:
/** * @notice Validates log signature, from and to address * then sends the correct amount to withdrawer * callable only by manager * @param rootToken Token which gets withdrawn * @param log Valid ERC20 burn log from child chain */function exitTokens(address,address rootToken,bytes memory log)publicoverrideonly(MANAGER_ROLE){RLPReader.RLPItem[] memory logRLPList = log.toRlpItem().toList();RLPReader.RLPItem[] memory logTopicRLPList = logRLPList[1].toList(); // topicsrequire(bytes32(logTopicRLPList[0].toUint()) == TRANSFER_EVENT_SIG, // topic0 is event sig"ERC20Predicate: INVALID_SIGNATURE");address withdrawer = address(logTopicRLPList[1].toUint()); // topic1 is from addressrequire(address(logTopicRLPList[2].toUint()) == address(0), // topic2 is to address"ERC20Predicate: INVALID_RECEIVER");uint256 amount = logRLPList[2].toUint(); // log data field is the amountIERC20(rootToken).safeTransfer(withdrawer,amount);emit ExitedERC20(withdrawer, rootToken, amount);}
说明:这里之所以能直接将销毁日志中的 amount 转移给退出用户,其意思即为【你销毁了多少资产,我就给你退回多少资产,你最多只能销毁你目前所拥有的全部资产】,它不需要计算之前用户消耗了多少钱,因为在每一个检查点提交到主链时,之前到交易都已经得到了确认。
资产转移(Plasma桥)
简述
是 Polygon 最开始提供的资产转移桥,仅支持 ETH、 ERC20 和 ERC721 资产转移,因为基于 Plasma 退出机制,Plasma 桥比 Pos 桥提供了更高的安全保证,但缺点是子链数据不可用。
- 用户在主链上的 Polygon 合约中存入资产
- 一旦存入的资产在主链上得到确认,相应的资产就会在 Polygon 链上生成(大约需要 5~7 分钟)
- 如果用户需要,他们可以从主链中提取剩余的资产。从 Plasma 侧链开始提取资产设置了 30 分钟的检查点间隔。
- 一旦检查点提交给主链以太坊的合约,就会创建一个等值的 Exit NFT (ERC721) 代币。
- 通过这个 NFT,用户可以从主链合约的退出程序中将资产提取到自己的以太坊账户。(需要等待7天的挑战时间)
存款
与 PoS 链一样。
取款
使用了欺诈性证明,采用了一种叫 MoreVP 的机制来进行挑战验证,证明过程比较复杂,感兴趣的可以看这里: 基于帐户的 Plasma (MoreVP)
相关资料
- Polygon 合约源码:https://github.com/maticnetwork/pos-portal/tree/master/contracts
- Polygon 官方文档:https://docs.polygon.technology/docs/pos/polygon-architecture
- 基于帐户的 Plasma (MoreVP):https://ethresear.ch/t/account-based-plasma-morevp/5480