Plasma Cash 合約解讀
蟲洞社區·簽約作者 steven bai
Plasma Cash 合約解讀
- Plasma Cash 合約解讀
- 1. 合約代碼
- 2. 合約文件簡單介紹
- 3. Plasma Cash 的基礎數據結構
- 3.1 Plasma Cash 中的資產
- 3.2 Plasma Cash中的交易
- 3.3 Plasma Cash 中的 Block
- 3.4 Plasma Cash 中資產的回歸主鏈以太坊
- 3.4.1 資產擁有證明
- 3.4.2 等待其他人來挑戰我
- 3.4.3 挑戰期過了, Bob 拿回資產 uid
- 4. Plasma Cash 中的退出示例
- 5. 其他問題
此文來自 SmartMesh 團隊,轉載請聯系作者。
Plasma 由 V 神在2017年8月提出,希望通過鏈下交易來大幅提高以太坊的 TPS.
每條 Plasma 鏈都會將有關交易順序的消息換算成一個哈希值存儲在根鏈上。比特幣和以太坊都屬於根鏈——這兩條區塊鏈具有很高的安全性,並且通過去中心化保證了(安全性和活性)。
Plasma 設計模型有兩個主要的分支:Plasma MVP 和 Plasma Cash 。這裏我們來研究 SmartPlasma 實現的 Plasma Cash 合約,並通過合約分析來回答大家關於 Plasma Cash 的一系列疑問.
1. 合約代碼
SmartPlasma的合約代碼肯定會不斷升級,我針對他們在今天(2018-09-14)最新版本進行分析,這份代碼目前保存在我的 github 上 plasma cash.
2. 合約文件簡單介紹
文件夾中有不少與 Plasma Cash 無關的合約,這裏只關註直接與 Plasma Cash 相關合約,像 ERC20Token 相關合約就忽略,自行查看.
- Mediator.sol 是 Plasma Cash 鏈中資產的進出口
- RootChain.sol 處理 Plasma Cash 子鏈(相對於以太坊而言)中的交易以及打包等
- libraries/MerkleProof.sol 是子鏈中交易用到的默克爾樹,用於子鏈參與方進行欺詐證明 fraud proof.
- libraris/RLP.sol RLP編碼支持,可以暫時忽略,子鏈中所有的交易都是用RLP 編碼的.
- libraries/PlasmaLib.sol 生成 uid 的輔助函數
- ECRecovery.sol 簽名驗證
- datastructures/Transaction.sol 描述交易的數據結構
3. Plasma Cash 的基礎數據結構
Plasma Cash 是一種子鏈結構,可以認為 Plasma Cash 是以太坊的一個是基於 =一種簡化的UTXO模型的子鏈.
3.1 Plasma Cash 中的資產
Plasma Cash 中的資產都來自於以太坊,但是一旦進入 Plasma Cash 就會擁有唯一的 ID,並且不可分割.
可以參考 Mediator.sol的deposit函數. Mediator就是 Plasma Cash 資產存放的地方.
/** @dev Adds deposits on Smart Plasma.
* @param currency Currency address.
* @param amount Amount amount of currency.
*/
function deposit(address currency, uint amount) public {
require(amount > 0);
Token token = Token(currency);
token.transferFrom(msg.sender, this, amount); /// deposit test1
bytes32 uid = rootChain.deposit(msg.sender, currency, amount); /// deposit test2
cash[uid] = entry({
currency: currency,
amount: amount
});
}
通過合約可以看出進入 Plasma Cash 的資產必須是 ERC20 Token,這些資產實際上是存在 Mediator 這個合約上,然後由 RootChain 為其分配一個唯一的 ID, 也就是 uid. 這個 uid 代表著什麽 token, 有多少個.
3.2 Plasma Cash中的交易
關鍵代碼在 Transaction.sol中.
struct Tx {
uint prevBlock;
uint uid;
uint amount;
address newOwner;
uint nonce;
address signer;
bytes32 hash;
}
這裏可能不太明顯,需要解釋才能看出來這是一個 UTXO 交易的模型. 這裏面的amount 和 hash 實際上都有點啰唆,可以忽略. 那麽剩下的成員需要來解釋.
prevBlock
就是 UTXO 中的輸入,來自於哪塊. 至於為什麽沒有像比特幣一樣的OutPoint 結構,也就是 TxHash+Index, 後續會講到.uid
就是交易的資產 IDnewOwner
交易輸出給誰, 這裏也不支持像 比特幣一樣的腳本.nonce
是這筆資產的第多少次交易,在雙花證明中有重要作用.signer
必須由資產原擁有者的簽名.
amount
不重要,是因為資產不可分割,導致這裏的 Amount 不會隨交易發生而發生變化. 而 hash
則是可以直接計算出來.
3.3 Plasma Cash 中的 Block
如果一般區塊鏈中的 Block 一樣,他是交易的集合.但是不同於一般鏈的是,這裏面的礦工(不一定是 Operator)不僅需要維護好子鏈,還需要周期性的將每一個 Block 對應的默克爾樹根保存到以太坊中,這個工作只能有 Operator 來完成.
具體代碼可見 RootChain.sol的.
function newBlock(bytes32 hash) public onlyOperator {
blockNumber = blockNumber.add(uint256(1));
childChain[blockNumber] = hash;
NewBlock(hash);
}
交易證據提交者只能是 Operator, 也就是合約的創建者. 這個 Operator 既可以是普通賬戶,這時他就是這個子鏈的管理員.也可以是一份合約,那麽就可以通過合約來規定子鏈的出塊規則.
3.4 Plasma Cash 中資產的回歸主鏈以太坊
當資產在 Plasma 中交易一段時間以後,持有者Bob如果想退出Plasma Cash 子鏈,那麽就需要向以太坊合約也就是 RootChain證明,他確實擁有這一筆資產.
3.4.1 資產擁有證明
這個思路和 UTXO 的思路是一樣的,Bob能證明這筆資產是從哪裏轉給我的即可.具體見[RootChain.sol]()中的startExit
函數. 其思路非常簡單,證明
- 這筆資產來自哪裏(在哪 M塊中轉移到了 Alice 手中)
-
經過 Alice 簽名轉移給了Bob(在N塊中 Alice 做了簽名給我)
具體看代碼 startExit/** @dev Starts the procedure for withdrawal of the deposit from the system. * @param previousTx Penultimate deposit transaction. * @param previousTxProof Proof of inclusion of a penultimate transaction in a Smart Plasma block. * @param previousTxBlockNum The number of the block in which the penultimate transaction is included. * @param lastTx Last deposit transaction. * @param lastTxProof Proof of inclusion of a last transaction in a Smart Plasma block. * @param lastTxBlockNum The number of the block in which the last transaction is included. */ function startExit( bytes previousTx, bytes previousTxProof, uint256 previousTxBlockNum, bytes lastTx, bytes lastTxProof, uint256 lastTxBlockNum ) public { Transaction.Tx memory prevDecodedTx = previousTx.createTx(); Transaction.Tx memory decodedTx = lastTx.createTx(); // 證明在 prevBlock的時候 Alice 擁有資產 uid require(previousTxBlockNum == decodedTx.prevBlock); require(prevDecodedTx.uid == decodedTx.uid); //amount 不變,證明資產不可分割 require(prevDecodedTx.amount == decodedTx.amount); //Alice 確實簽名轉移給了我,並且交易是相鄰的兩筆交易 require(prevDecodedTx.newOwner == decodedTx.signer); require(decodedTx.nonce == prevDecodedTx.nonce.add(uint256(1))); //緊挨著的兩筆交易 //我是 Bob, 我要來拿走這筆資產 require(msg.sender == decodedTx.newOwner); require(wallet[bytes32(decodedTx.uid)] != 0); bytes32 prevTxHash = prevDecodedTx.hash; bytes32 prevBlockRoot = childChain[previousTxBlockNum]; bytes32 txHash = decodedTx.hash; bytes32 blockRoot = childChain[lastTxBlockNum]; require( prevTxHash.verifyProof( prevDecodedTx.uid, prevBlockRoot, previousTxProof ) ); require( txHash.verifyProof( decodedTx.uid, blockRoot, lastTxProof ) ); /// Record the exit tx. require(exits[decodedTx.uid].state == 0); require(challengesLength(decodedTx.uid) == 0); exits[decodedTx.uid] = exit({ state: 2, exitTime: now.add(challengePeriod), exitTxBlkNum: lastTxBlockNum, exitTx: lastTx, txBeforeExitTxBlkNum: previousTxBlockNum, txBeforeExitTx: previousTx }); StartExit(prevDecodedTx.uid, previousTxBlockNum, lastTxBlockNum); }
代碼的前一半都是在用來證明在
lastTxBlockNum
的時候,資產 uid 歸Bob所有.
然後後一半就是提出來,Bob想把資產 uid 提走. 我的這個想法會暫時保存在合約中,等待別人來挑戰.
3.4.2 等待其他人來挑戰我
有了以上信息, 就可以證明在 N 塊時,這筆資產歸Bob所用.但是這肯定不夠,無法證明現在資產仍然屬於Bob,也無法證明Alice 沒有在 M 塊以後再給別人.
更加不能證明在 M 塊的時候 Alice 真的是 uid 的擁有者?
這些問題,看起來很難回答,其實思路也很簡單.
這個思路和雷電網絡中解決問題的辦法是一樣的, 讓這筆資產的利益攸關者站出來舉證.
比如: 如果 Carol能夠舉證這筆資產Bob 後來又轉移給了 Carol, 那麽實際上 Bob 就是在雙花.
具體的挑戰以及迎戰代碼比較復雜,但是這也是 Plasma Cash 的核心安全性所在.如果沒有這些,所有的參與者都將無法保證自己的權益.
//challengeExit 挑戰資產uid 其實不屬於 Bob
/** @dev Challenges a exit.
* @param uid Unique identifier of a deposit.
* @param challengeTx Transaction that disputes an exit.
* @param proof Proof of inclusion of the transaction in a Smart Plasma block.
* @param challengeBlockNum The number of the block in which the transaction is included.
*/
function challengeExit(
uint256 uid,
bytes challengeTx,
bytes proof,
uint256 challengeBlockNum
)
public
{
require(exits[uid].state == 2);
Transaction.Tx memory exitDecodedTx = (exits[uid].exitTx).createTx();
Transaction.Tx memory beforeExitDecodedTx = (exits[uid].txBeforeExitTx).createTx();
Transaction.Tx memory challengeDecodedTx = challengeTx.createTx();
require(exitDecodedTx.uid == challengeDecodedTx.uid);
require(exitDecodedTx.amount == challengeDecodedTx.amount);
bytes32 txHash = challengeDecodedTx.hash;
bytes32 blockRoot = childChain[challengeBlockNum];
require(txHash.verifyProof(uid, blockRoot, proof));
// test challenge #1 & test challenge #2 最後一筆交易後面又進行了其他交易, Bob 在進行雙花
if (exitDecodedTx.newOwner == challengeDecodedTx.signer &&
exitDecodedTx.nonce < challengeDecodedTx.nonce) {
delete exits[uid];
return;
}
// test challenge #3, 雙花了, Alice 給了兩個人,並且挑戰者 Carol的BlockNumer 更小,也就是發生的更早.
if (challengeBlockNum < exits[uid].exitTxBlkNum &&
(beforeExitDecodedTx.newOwner == challengeDecodedTx.signer &&
challengeDecodedTx.nonce > beforeExitDecodedTx.nonce)) {
delete exits[uid];
return;
}
// test challenge #4 在 M塊之前,還有一筆交易,Alice 需要證明自己在 M 塊確實擁有 uid
if (challengeBlockNum < exits[uid].txBeforeExitTxBlkNum ) {
exits[uid].state = 1;
addChallenge(uid, challengeTx, challengeBlockNum);
}
require(exits[uid].state == 1);
ChallengeExit(uid);
}
//Bob應戰,再次舉證,實際上這個過程就是要不斷的追加證據,將所有的交易連起來,最終證明 Alice 在 M塊確實擁有 uid
/** @dev Answers a challenge exit.
* @param uid Unique identifier of a deposit.
* @param challengeTx Transaction that disputes an exit.
* @param respondTx Transaction that answers to a dispute transaction.
* @param proof Proof of inclusion of the respond transaction in a Smart Plasma block.
* @param blockNum The number of the block in which the respond transaction is included.
*/
function respondChallengeExit(
uint256 uid,
bytes challengeTx,
bytes respondTx,
bytes proof,
uint blockNum
)
public
{
require(challengeExists(uid, challengeTx));
require(exits[uid].state == 1);
Transaction.Tx memory challengeDecodedTx = challengeTx.createTx();
Transaction.Tx memory respondDecodedTx = respondTx.createTx();
require(challengeDecodedTx.uid == respondDecodedTx.uid);
require(challengeDecodedTx.amount == respondDecodedTx.amount);
require(challengeDecodedTx.newOwner == respondDecodedTx.signer);
require(challengeDecodedTx.nonce.add(uint256(1)) == respondDecodedTx.nonce);
require(blockNum < exits[uid].txBeforeExitTxBlkNum);
bytes32 txHash = respondDecodedTx.hash;
bytes32 blockRoot = childChain[blockNum];
require(txHash.verifyProof(uid, blockRoot, proof));
removeChallenge(uid, challengeTx);
if (challengesLength(uid) == 0) {
exits[uid].state = 2;
}
RespondChallengeExit(uid);
}
3.4.3 挑戰期過了, Bob 拿回資產 uid
挑戰期過後,Bob 在Mediator.sol 中提出將資產退回到以太坊中
/** @dev withdraws deposit from Smart Plasma.
* @param prevTx Penultimate deposit transaction.
* @param prevTxProof Proof of inclusion of a penultimate transaction in a Smart Plasma block.
* @param prevTxBlkNum The number of the block in which the penultimate transaction is included.
* @param txRaw lastTx Last deposit transaction.
* @param txProof Proof of inclusion of a last transaction in a Smart Plasma block.
* @param txBlkNum The number of the block in which the last transaction is included.
*/
function withdraw(
bytes prevTx,
bytes prevTxProof,
uint prevTxBlkNum,
bytes txRaw,
bytes txProof,
uint txBlkNum
)
public
{
bytes32 uid = rootChain.finishExit(
msg.sender,
prevTx,
prevTxProof,
prevTxBlkNum,
txRaw,
txProof,
txBlkNum
);
entry invoice = cash[uid];
Token token = Token(invoice.currency);
token.transfer(msg.sender, invoice.amount); /// 真正的資產轉移
delete(cash[uid]);
}
RootChain 再次驗證
/** @dev Finishes the procedure for withdrawal of the deposit from the system.
* Can only call the owner. Usually the owner is the mediator contract.
* @param account Account that initialized the deposit withdrawal.
* @param previousTx Penultimate deposit transaction.
* @param previousTxProof Proof of inclusion of a penultimate transaction in a Smart Plasma block.
* @param previousTxBlockNum The number of the block in which the penultimate transaction is included.
* @param lastTx Last deposit transaction.
* @param lastTxProof Proof of inclusion of a last transaction in a Smart Plasma block.
* @param lastTxBlockNum The number of the block in which the last transaction is included.
*/
function finishExit(
address account,
bytes previousTx,
bytes previousTxProof,
uint256 previousTxBlockNum,
bytes lastTx,
bytes lastTxProof,
uint256 lastTxBlockNum
)
public
onlyOwner
returns (bytes32)
{
Transaction.Tx memory prevDecodedTx = previousTx.createTx();
Transaction.Tx memory decodedTx = lastTx.createTx();
require(previousTxBlockNum == decodedTx.prevBlock);
require(prevDecodedTx.uid == decodedTx.uid);
require(prevDecodedTx.amount == decodedTx.amount);
require(prevDecodedTx.newOwner == decodedTx.signer);
require(account == decodedTx.newOwner);
bytes32 prevTxHash = prevDecodedTx.hash;
bytes32 prevBlockRoot = childChain[previousTxBlockNum];
bytes32 txHash = decodedTx.hash;
bytes32 blockRoot = childChain[lastTxBlockNum];
require(
prevTxHash.verifyProof(
prevDecodedTx.uid,
prevBlockRoot,
previousTxProof
)
);
require(
txHash.verifyProof(
decodedTx.uid,
blockRoot,
lastTxProof
)
);
require(exits[decodedTx.uid].exitTime < now); //挑戰期過了
require(exits[decodedTx.uid].state == 2); //並且沒有人挑戰或者我都給出了合適的證據
require(challengesLength(decodedTx.uid) == 0);
exits[decodedTx.uid].state = 3;
delete(wallet[bytes32(decodedTx.uid)]);
FinishExit(decodedTx.uid);
return bytes32(decodedTx.uid);
}
4. Plasma Cash 中的退出示例
5. 其他問題
-
- 為什麽 Plasma Cash 中的資產是不可分割的?
進入 rootchain 的資產類似於比特幣的 UTXO 模型,但是是不可分割的,這個可以通過交易驗證時每次都要求 amount 不變可以得出.
- 為什麽 Plasma Cash 中的資產是不可分割的?
-
- operator 的作用是什麽
operator 負責將子鏈中的交易證據(默克爾樹)提交證明到以太坊主鏈中.
- operator 的作用是什麽
-
- operator 是否必須可信任的?
是的. 雖然 operator 不能將他人資產隨意轉移,但是卻可以阻止他人資產轉移.也就是說無法從 Plasma 子鏈中退回到以太坊中.
當然這部分是可以改進,降低 operator 作惡帶來的風險.
- operator 是否必須可信任的?
-
- operator 是否可以是一個合約呢?
是的. 如果 operator 是一個 Pos 共識合約,那麽可以降低問題3中的風險
- operator 是否可以是一個合約呢?
添加微信(cdong1024),加入區塊鏈開發者技術交流群
蟲洞社區:https://www.uzanapp.com/
Plasma Cash 合約解讀