【在 Nervos CKB 上做開發(fā)】Nervos CKB 腳本編程簡介[3]:自定義代幣

原文作者:Xuejie
原文鏈接:https://xuejie.space/2019_09_06_introduction_to_ckb_script_programming_udt/

Nervos CKB 腳本編程簡介[3]:自定義代幣

CKB 的 Cell 模型和 VM 支持許多新的用例抹恳。然而饭寺,這并不意味著我們需要拋棄現(xiàn)有的一切父晶。如今區(qū)塊鏈中的一個常見用途是 Token 發(fā)行者發(fā)布具有特殊目的/意義的新 Token点额。在以太坊中轿偎,我們稱之為 ERC20 Token,下面讓我們看看我們如何在 CKB 中構建類似的概念笼蛛。為了與 ERC20 區(qū)分芽唇,在 CKB中的 Token 我們稱之為 user defined token,簡稱 UDT疤坝。

本文使用 CKB v0.20.0 版本來演示兆解。具體來說,我會在每個項目中使用以下提交的版本:

  • ckb: 472252ac5333b2b19ea3ec50d54e68b627bf6ac5
  • ckb-duktape: 55849c20b43a212120e0df7ad5d64b2c70ea51ac
  • ckb-sdk-ruby: 1c2a3c3f925e47e421f9e3c07164ececf3b6b9f6

數(shù)據(jù)模型

以太坊會為每個合約賬戶提供單獨的存儲空間跑揉,CKB 與之不同锅睛,CKB 是在多個 Cell 之間傳遞數(shù)據(jù)。Cell 的 Lock Sript 和 Type Sript 會標明 Cell 屬于哪個帳戶历谍,以及如何與 Cell 進行交互现拒。其結果是,CKB 不會像 ERC20 那樣將所有 Token 用戶的余額存儲在 ERC20 合約的存儲空間中望侈,在 CKB 中印蔬,我們需要一種新的設計來存儲 UDT 用戶的余額。

當然脱衙,我們也可以構造一個特殊的 Cell 來保存所有 UDT 用戶的余額侥猬。這個解決方案看起來很像以太坊的 ERC20 設計。但是這中間存在幾個問題:

  • Token 的發(fā)行者必須提供足夠的存儲空間以保存所有用戶的余額捐韩。隨著用戶數(shù)量的增長退唠,存儲空間也將增長,這在 CKB 的經濟模型中荤胁,不是一個高效的設計瞧预。
    考慮到 CKB 中 Cell 的更新實際上是在銷毀舊 Cell 并重新生成新的 Cell ,因此保存所有余額的單個 Cell 會遇到一個困境:一旦更新 UDT 余額那就不得不更新這一個且是唯一的 Cell仅政,而且每一步的操作都需要更新, 那么用戶在使用過程中將會產生沖突垢油。

  • 雖然有一些解決方案確實可以緩解甚至能解決上述問題,但我們還是開始質疑這里的基本設計:將所有 UDT 保存在一個地方真的有意義嗎圆丹?一旦轉賬滩愁,UDT 應該真的屬于收款人,為什么余額仍然留在一個中心呢辫封?

這引出了我們提出的設計:

  • 一個特殊的 Type Script 表示此 Cell 存儲 UDT惊楼;
  • Cell 數(shù)據(jù)的前 4 個字節(jié)包含當前 Cell 中的 UDT 數(shù)量玖瘸。

這種設計有幾個含義:

  • UDT Cell 的存儲成本始終是恒定的,與存儲在 Cell 中的 UDT 數(shù)量無關檀咙;
  • 用戶可以將 Cell 中的全部或部分 UDT 轉賬給其他人雅倒;
  • 實際上,可能有許多 Cell 包含相同的 UDT弧可;
  • 用于保護 UDT 的 Lock Script 與 UDT 本身分離蔑匣。

每個 Token 用戶將其 UDT 保存在自己的 Cell 中。他們負責為 UDT 提供存儲空間棕诵,并確保他們自己的 Token 是安全的裁良。這樣 UDT 就可以真正屬于每個 UDT 用戶。

但這里還有一個問題:如果 Token 存儲在屬于各個用戶的眾多不同 Cell 中校套,而不是統(tǒng)一存儲价脾,我們如何確定這個 Token 確實由這個發(fā)行者發(fā)行呢?如果有人自己偽造 Token 怎么辦笛匙?在以太坊中侨把,這可能是一個問題,但正如我們將在本文中看到的妹孙,CKB 中的 Type Script 可以防止所有這些攻擊秋柄,確保 Token 是安全的。

編寫 UDT 腳本

鑒于上述設計蠢正,最小 UDT Type Script 應該遵循以下規(guī)則:

  • 在一個 UDT 轉賬交易中骇笔,輸出 Cell 中的 UDT 總和應等于輸入 Cell 中 UDT 的總和;
  • 只有發(fā)行者可以在初始Token創(chuàng)建過程中生成新的 Token嚣崭。

這可能聽起來有點狂妄笨触,但我們會證明,通過 Type Script 和 CKB 獨特的設計模式雹舀,一切都可以搞定芦劣。

為簡單起見,我們這里將在純 JavaScript 中編寫 UDT 腳本葱跋,雖說 C 版本可能有助于節(jié)省 Cycles 持寄,但功能其實是一樣的源梭。

首先娱俺,我們需要遍歷所有輸入 Cell 并收集 UDT 的總和:

diff --git a/udt.js b/udt.js
index e69de29..4a20bd0 100644
--- a/udt.js
+++ b/udt.js
@@ -0,0 +1,17 @@
+var input_index = 0;
+var input_coins = 0;
+var buffer = new ArrayBuffer(4);
+var ret = CKB.CODE.INDEX_OUT_OF_BOUND;
+
+while (true) {
+  ret = CKB.raw_load_cell_data(buffer, 0, input_index, CKB.SOURCE.GROUP_INPUT);
+  if (ret === CKB.CODE.INDEX_OUT_OF_BOUND) {
+    break;
+  }
+  if (ret !== 4) {
+    throw "Invalid input cell!";
+  }
+  var view = new DataView(buffer);
+  input_coins += view.getUint32(0, true);
+  input_index += 1;
+}

正如前一篇文章中所解釋的,CKB 要求我們使用循環(huán)來迭代同一 group 中所有的 Input 并獲取數(shù)據(jù)废麻。在 C 中我們將使用 ckb_load_cell_data荠卷,它被包裝到 JS 函數(shù) ckb.raw_load_cell_data 中。正如 ArrayBuffer 所示烛愧,我們只對 Cell 數(shù)據(jù)的前 4 個字節(jié)感興趣油宜,因為這 4 個字節(jié)將包含 UDT 的數(shù)量掂碱。

注意,這里我們對 input_coins 執(zhí)行了一個簡單的 Add 操作慎冤,這風險很高疼燥。之所以這樣做只是為了簡單起見。在真實的生產環(huán)境中蚁堤,您應該檢查該值是否保持在 32 位整數(shù)值中醉者。如果有必要,應使用更高精度的數(shù)字類型披诗。

同樣地撬即,我們可以獲取輸出的UDT的總和并進行比較:

diff --git a/udt.js b/udt.js
index 4a20bd0..e02b993 100644
--- a/udt.js
+++ b/udt.js
@@ -15,3 +15,23 @@ while (true) {
   input_coins += view.getUint32(0);
   input_index += 1;
 }
+
+var output_index = 0;
+var output_coins = 0;
+
+while (true) {
+  ret = CKB.raw_load_cell_data(buffer, 0, output_index, CKB.SOURCE.GROUP_OUTPUT);
+  if (ret === CKB.CODE.INDEX_OUT_OF_BOUND) {
+    break;
+  }
+  if (ret !== 4) {
+    throw "Invalid output cell!";
+  }
+  var view = new DataView(buffer);
+  output_coins += view.getUint32(0, true);
+  output_index += 1;
+}
+
+if (input_coins !== output_coins) {
+  throw "Input coins do not equal output coins!";
+}

以上幾乎就是驗證第一條規(guī)則所需的全部內容:輸出 Cell 中 UDT 的總和應等于輸入 Cell 中 UDT 的總和。換句話說呈队,使用這種 Type Script剥槐,等于沒有人能夠偽造任何新的 Token。這不是很棒嗎宪摧?

但有一個問題:當我們說沒有人能夠偽造新的 Token 時候粒竖,我們說的真的是指沒有任何人,包括代幣發(fā)行人绍刮!那就不太好了温圆,所以我們需要添加一個例外,讓 Token 發(fā)行者可以先發(fā)行 Token孩革,但之后就沒有人能夠這么做了岁歉。那有沒有辦法做到這一點?

當然有膝蜈!但答案就像一個謎語锅移,所以請仔細閱讀本段:Type Script 由兩部分組成:表示實際代碼的代碼哈希,以及 Type Script 使用的參數(shù)饱搏。具有不同參數(shù)的兩種 Type Script 將被視為兩種不同 Type Script非剃。這里的技巧是允許 Token 發(fā)行者創(chuàng)建一個具有新 Type Script 的 Cell,但沒有人能夠再次創(chuàng)建推沸,所以如果我們在參數(shù)部分放置一些不能再包含的東西备绽,那么問題就被解決了~

現(xiàn)在想想這個問題:什么東西不能被包含在區(qū)塊鏈中兩次?交易輸入中的 OutPoint鬓催!第一次的時候肺素,我們將 OutPoint 作為交易輸入包含在內,引用的 Cell 將被消耗宇驾,如果有人稍后再次包含它倍靡,則會產生雙花錯誤,這正是我們使用區(qū)塊鏈的原因课舍。

我們現(xiàn)在有答案了塌西!CKB 中最小 UDT 的 Type Script 完整驗證流程如下:

  • 首先收集輸入 Cell 中所有 UDT 的總和以及輸出 Cell 中所有 UDT 的總和他挎,如果它們相等,則 Type Script 將以成功狀態(tài)退出捡需;
  • 檢查 Type Script 的第一個參數(shù)是否與當前交易中的第一個 OutPoint 匹配办桨,如果它們匹配,則以成功狀態(tài)退出站辉;
  • 否則以失敗狀態(tài)退出崔挖。

如果你現(xiàn)在還跟得上我,那么一定可以看出:步驟 1 對應于正常的 UDT 交易庵寞,而步驟 2 對應于初始 Token 創(chuàng)建過程狸相。

這就是我們所說的 CKB 獨特的設計模式:通過使用輸入 OutPoint 作為 Script 參數(shù),我們可以創(chuàng)建一個無法再偽造的獨特 Script:

  • 如果攻擊者試圖使用相同的參數(shù)捐川,則 Script 將驗證出交易中的第一個輸入 OutPoint 與參數(shù)不匹配脓鹃,從而使交易無效;
  • 如果攻擊者試圖使用相同的參數(shù)并填充參數(shù)作為第一個輸入 OutPoint古沥,它將造成一個雙花錯誤瘸右,也會使交易無效;
  • 如果攻擊者試圖使用不同的參數(shù)岩齿,CKB 將識別出不同的參數(shù)導致不同的 Type Script太颤,從而生成不同的UDT。

這種簡單而強大的模式確保了 UDT 的安全盹沈,同時也帶來了可以在不同 Cell 之間自由交易的好處龄章。據(jù)我們所知,這種模式在其他很多聲稱靈活或可編程的區(qū)塊鏈中是不可能實現(xiàn)的乞封。

現(xiàn)在我們終于可以完成我們的 UDT 腳本了:

diff --git a/contract.js b/contract.js
deleted file mode 100644
index e69de29..0000000
diff --git a/udt.js b/udt.js
index e02b993..cd443bf 100644
--- a/udt.js
+++ b/udt.js
@@ -1,3 +1,7 @@
+if (CKB.ARGV.length !== 1) {
+  throw "Requires only one argument!";
+}
+
 var input_index = 0;
 var input_coins = 0;
 var buffer = new ArrayBuffer(4);
@@ -33,5 +37,17 @@ while (true) {
 }

 if (input_coins !== output_coins) {
-  throw "Input coins do not equal output coins!";
+  if (!((input_index === 0) && (output_index === 1))) {
+    throw "Invalid token issuing mode!";
+  }
+  var first_input = CKB.load_input(0, 0, CKB.SOURCE.INPUT);
+  if (typeof first_input === "number") {
+    throw "Cannot fetch the first input";
+  }
+  var hex_input = Array.prototype.map.call(
+    new Uint8Array(first_input),
+    function(x) { return ('00' + x.toString(16)).slice(-2); }).join('');
+  if (CKB.ARGV[0] != hex_input) {
+    throw "Invalid creation argument!";
+  }
 }

就是上面這樣做裙,一共有 53 行代碼 1372 字節(jié),我們就在 CKB 中完成了一個最小的 UDT Type Script肃晚。注意锚贱,在這里我甚至還沒有使用壓縮工具,如果使用任何一個合適的 JS 壓縮工具关串,我們應該能夠獲得更緊湊的 Type Script 拧廊。當然了,這是一個可用于生產環(huán)境的 Type Script 晋修,但它足以顯示一個簡單的 Type Script 可以處理 CKB 中的很多重要任務吧碾。

部署到 CKB 網絡

我不像某些項目,只知道扔出來一個視頻或者非常氣人的帖子飞蚓,也不說清楚是怎么做的或者怎么解決問題的滤港。如果沒有實際的代碼和使用它的步驟廊蜒,那我覺得這個帖子其實是很沒意思的趴拧。以下是如何在 CKB 上使用上述 UDT 腳本:

這里還有沒有 Diff 格式的完整 UDT 腳本溅漾,有需自取:

$ cat udt.js
if (CKB.ARGV.length !== 1) {
  throw "Requires only one argument!";
}

var input_index = 0;
var input_coins = 0;
var buffer = new ArrayBuffer(4);
var ret = CKB.CODE.INDEX_OUT_OF_BOUND;

while (true) {
  ret = CKB.raw_load_cell_data(buffer, 0, input_index, CKB.SOURCE.GROUP_INPUT);
  if (ret === CKB.CODE.INDEX_OUT_OF_BOUND) {
    break;
  }
  if (ret !== 4) {
    throw "Invalid input cell!";
  }
  var view = new DataView(buffer);
  input_coins += view.getUint32(0, true);
  input_index += 1;
}

var output_index = 0;
var output_coins = 0;

while (true) {
  ret = CKB.raw_load_cell_data(buffer, 0, output_index, CKB.SOURCE.GROUP_OUTPUT);
  if (ret === CKB.CODE.INDEX_OUT_OF_BOUND) {
    break;
  }
  if (ret !== 4) {
    throw "Invalid output cell!";
  }
  var view = new DataView(buffer);
  output_coins += view.getUint32(0, true);
  output_index += 1;
}

if (input_coins !== output_coins) {
  if (!((input_index === 0) && (output_index === 1))) {
    throw "Invalid token issuing mode!";
  }
  var first_input = CKB.load_input(0, 0, CKB.SOURCE.INPUT);
  if (typeof first_input === "number") {
    throw "Cannot fetch the first input";
  }
  var hex_input = Array.prototype.map.call(
    new Uint8Array(first_input),
    function(x) { return ('00' + x.toString(16)).slice(-2); }).join('');
  if (CKB.ARGV[0] != hex_input) {
    throw "Invalid creation argument!";
  }
}

為了能在 CKB 上運行 JavaScript著榴,讓我們首先在 CKB 上部署 duktape:

pry(main)> data = File.read("../ckb-duktape/build/duktape")
pry(main)> duktape_tx_hash = wallet.send_capacity(wallet.address, CKB::Utils.byte_to_shannon(300000), CKB::Utils.bin_to_hex(duktape_data))
pry(main)> duktape_data_hash = CKB::Blake2b.hexdigest(duktape_data)
pry(main)> duktape_out_point = CKB::Types::CellDep.new(out_point: CKB::Types::OutPoint.new(tx_hash: duktape_tx_hash, index: 0))

首先添履,讓我們創(chuàng)建一個包含 1000000 Token 的 UDT:

pry(main)> tx = wallet.generate_tx(wallet.address, CKB::Utils.byte_to_shannon(20000))
pry(main)> tx.cell_deps.push(duktape_out_point.dup)
pry(main)> arg = CKB::Utils.bin_to_hex(CKB::Serializers::InputSerializer.new(tx.inputs[0]).serialize)
pry(main)> duktape_udt_script = CKB::Types::Script.new(code_hash: duktape_data_hash, args: [CKB::Utils.bin_to_hex(File.read("udt.js")), arg])
pry(main)> tx.outputs[0].type = duktape_udt_script
pry(main)> tx.outputs_data[0] = CKB::Utils.bin_to_hex([1000000].pack("L<"))
pry(main)> tx.witnesses[0].data.clear
pry(main)> signed_tx = tx.sign(wallet.key, api.compute_transaction_hash(tx))
pry(main)> root_udt_tx_hash = api.send_transaction(signed_tx)

如果我們再次嘗試提交相同的交易,雙花將阻止我們偽造相同的 Token :

pry(main)> api.send_transaction(signed_tx)
CKB::RPCError: jsonrpc error: {:code=>-3, :message=>"UnresolvableTransaction(Dead(OutPoint(0x0b607e9599f23a8140d428bd24880e5079de1f0ee931618b2f84decf2600383601000000)))"}

無論我們如何嘗試脑又,我們都無法創(chuàng)建另一個想要偽造相同 UDT Token 的 Cell暮胧。

現(xiàn)在我們可以嘗試將 UDT 轉移到另一個帳戶。首先讓我們嘗試創(chuàng)建一個輸出 UDT 比輸入 UDT 更多的 UDT 交易:

pry(main)> udt_out_point = CKB::Types::OutPoint.new(tx_hash: root_udt_tx_hash, index: 0)
pry(main)> tx = wallet.generate_tx(wallet2.address, CKB::Utils.byte_to_shannon(20000))
pry(main)> tx.cell_deps.push(duktape_out_point.dup)
pry(main)> tx.witnesses[0].data.clear
pry(main)> tx.witnesses.push(CKB::Types::Witness.new(data: []))
pry(main)> tx.outputs[0].type = duktape_udt_script
pry(main)> tx.outputs_data[0] = CKB::Utils.bin_to_hex([1000000].pack("L<"))
pry(main)> tx.inputs.push(CKB::Types::Input.new(previous_output: udt_out_point, since: "0"))
pry(main)> tx.outputs.push(tx.outputs[1].dup)
pry(main)> tx.outputs[2].capacity = CKB::Utils::byte_to_shannon(20000)
pry(main)> tx.outputs[2].type = duktape_udt_script
pry(main)> tx.outputs_data.push(CKB::Utils.bin_to_hex([1000000].pack("L<")))
pry(main)> signed_tx = tx.sign(wallet.key, api.compute_transaction_hash(tx))
pry(main)> api.send_transaction(signed_tx)
CKB::RPCError: jsonrpc error: {:code=>-3, :message=>"InvalidTx(ScriptFailure(ValidationFailure(-2)))"}

現(xiàn)在我們嘗試發(fā)送 1000000 UDT 給另一個用戶问麸,同時為發(fā)送者本人保留 1000000 UDT往衷,很明顯這會觸發(fā)錯誤,因為我們正在嘗試偽造更多的 Token严卖。但是如果稍作修改席舍,我們可以看到,如果您遵守總和驗證規(guī)則哮笆,UDT 轉移交易是有效的:

pry(main)> tx.outputs_data[0] = CKB::Utils.bin_to_hex([900000].pack("L<"))
pry(main)> tx.outputs_data[2] = CKB::Utils.bin_to_hex([100000].pack("L<"))
pry(main)> signed_tx = tx.sign(wallet.key, api.compute_transaction_hash(tx))
pry(main)> api.send_transaction(signed_tx)

靈活的規(guī)則

這里顯示的 UDT 腳本僅作為示例来颤,實際上,dApp 可能更復雜并且需要更多功能稠肘。您還可以根據(jù)需要為 UDT 腳本添加更多功能福铅,其中一些示例包括:

  • 這里,我們嚴格確保輸出 UDT 的總和等于輸入 UDT 的總和项阴,但在某些情況下滑黔,僅僅確保輸出 UDT 的總和不超過輸入 UDT的總和就足夠了。換句話說环揽,當不需要時拷沸,用戶可以選擇為空間燒毀一部分 UDT;
  • 上述 UDT 腳本不允許在初始創(chuàng)建完成后再發(fā)行更多的 Token薯演,但可能存在另一種類型的 UDT撞芍,允許 Token 發(fā)行者繼續(xù)增發(fā)。這當然也可以在 CKB 上運行跨扮,但是我想把這個解決方案的探索任務留給大家序无,當做練習;
  • 在這里衡创,我們將腳本限制為僅在初始 Token 創(chuàng)建過程中創(chuàng)建一個 Cell 帝嗡,實際上也可以創(chuàng)建多個 Cell , 分別用于不同的用途;
  • 雖然我們只在這里介紹 ERC20璃氢,但 ERC721 也應該是完全可能的哟玷。

請注意,這里只是一些例子,CKB 腳本的實際應用方式是沒有邊界的巢寡。我們非常高興看到將來有更多的 CKB dApp 開發(fā)者創(chuàng)造出讓我們震驚的有趣用法喉脖。

加入 Nervos Community

Nervos Community 致力于成為最好的 Nervos 社區(qū),我們將持續(xù)地推廣和普 及 Nervos 技術抑月,深入挖掘 Nervos 的內在價值树叽,開拓 Nervos 的無限可能, 為每一位想要深入了解 Nervos Network 的人提供一個優(yōu)質的平臺谦絮。

添加微信號:BitcoinDog 即可加入 Nervos Community题诵,如果是程序員請備注,還會將您拉入開發(fā)者群层皱。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末性锭,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子叫胖,更是在濱河造成了極大的恐慌篷店,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件臭家,死亡現(xiàn)場離奇詭異疲陕,居然都是意外死亡,警方通過查閱死者的電腦和手機钉赁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進店門蹄殃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人你踩,你說我怎么就攤上這事诅岩。” “怎么了带膜?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵吩谦,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么啤它? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任苹熏,我火速辦了婚禮剩岳,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好蠕趁,可當我...
    茶點故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著辛馆,像睡著了一般俺陋。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天腊状,我揣著相機與錄音诱咏,去河邊找鬼。 笑死寿酌,一個胖子當著我的面吹牛,可吹牛的內容都是我干的硕蛹。 我是一名探鬼主播醇疼,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼法焰!你這毒婦竟也來了秧荆?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤埃仪,失蹤者是張志新(化名)和其女友劉穎乙濒,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體卵蛉,經...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡颁股,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了傻丝。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片甘有。...
    茶點故事閱讀 40,117評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖葡缰,靈堂內的尸體忽然破棺而出亏掀,到底是詐尸還是另有隱情,我是刑警寧澤泛释,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布滤愕,位于F島的核電站,受9級特大地震影響怜校,放射性物質發(fā)生泄漏间影。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一茄茁、第九天 我趴在偏房一處隱蔽的房頂上張望宇智。 院中可真熱鬧,春花似錦胰丁、人聲如沸随橘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽机蔗。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間萝嘁,已是汗流浹背梆掸。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留牙言,地道東北人酸钦。 一個月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像咱枉,于是被迫代替她去往敵國和親卑硫。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,060評論 2 355

推薦閱讀更多精彩內容