【CryptoZombies|編寫區(qū)塊鏈游戲?qū)W智能合約】Lesson3: 高級Solidity理論


CryptoZombies是個在編游戲的過程中學習Solidity智能合約語言的互動教程。本教程是為了Solidity初學者而設計的,會從最基礎開始教起蟀给,即便你從來沒有接觸過Solidity也可以學泌辫,CryptoZombies會手把手地教你。


今天我們來學習第3課高級Solidity理論靖避,這堂課比之前要少些特效潭枣,但是會學一些非常重要的基礎理論,編寫真正的DApp時必知的:智能協(xié)議的所有權(quán)幻捏,Gas的花費盆犁,代碼優(yōu)化,和代碼安全篡九。


1. 智能協(xié)議的永固性

到現(xiàn)在為止谐岁,我們講的Solidity和其他語言沒有質(zhì)的區(qū)別,它長得也很像 JavaScript榛臼。但是伊佃,在有幾點以太坊上的DApp跟普通的應用程序有著天壤之別。

第一個例子沛善,在你把智能協(xié)議傳上以太坊之后航揉,它就變得不可更改,這種永固性意味著你的代碼永遠不能被調(diào)整或更新路呜。你編譯的程序會一直迷捧、永久的、不可更改地存在以太網(wǎng)上胀葱。這就是Solidity代碼的安全性如此重要的一個原因漠秋。如果你的智能協(xié)議有任何漏洞,即使你發(fā)現(xiàn)了也無法補救抵屿。你只能讓你的用戶們放棄這個智能協(xié)議庆锦,然后轉(zhuǎn)移到一個新的修復后的合約上。

但這恰好也是智能合約的一大優(yōu)勢轧葛。 代碼說明一切搂抒。 如果你去讀智能合約的代碼艇搀,并驗證它,你會發(fā)現(xiàn)求晶,一旦函數(shù)被定義下來焰雕,每一次的運行,程序都會嚴格遵照函數(shù)中原有的代碼邏輯一絲不茍地執(zhí)行芳杏,完全不用擔心函數(shù)被人篡改而得到意外的結(jié)果矩屁。

2. Ownable Contracts

上一章中,你有沒有發(fā)現(xiàn)任何安全漏洞呢爵赵?對申明為“外部的”(external)方法吝秕,是任何人都可以調(diào)用它的,這種情況下可能出現(xiàn)安全漏洞空幻。要對付這樣的情況烁峭,通常的做法是指定合約的“所有權(quán)”——就是說,給它指定一個主人秕铛,只有主人對它享有特權(quán)约郁。

OpenZeppelin庫的Ownable合約

下面是一個Ownable合約的例子:來自OpenZeppelin Solidity庫的 Ownable合約。OpenZeppelin是主打安保和社區(qū)審查的智能合約庫如捅,您可以在自己的DApps中引用棍现。這一課學完,可以看看OpenZeppelin镜遣,保管您會學到很多東西!

把樓下這個合約讀通士袄,是不是還有些沒見過代碼悲关?別擔心,隨后會解釋娄柳。

/**
 * @title Ownable
 * @dev The Ownable contract has an owner address, and provides basic authorization control
 * functions, this simplifies the implementation of "user permissions".
 */
contract Ownable {
  address public owner;
  event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

  /**
   * @dev The Ownable constructor sets the original `owner` of the contract to the sender
   * account.
   */
  function Ownable() public {
    owner = msg.sender;
  }

  /**
   * @dev Throws if called by any account other than the owner.
   */
  modifier onlyOwner() {
    require(msg.sender == owner);
    _;
  }

  /**
   * @dev Allows the current owner to transfer control of the contract to a newOwner.
   * @param newOwner The address to transfer ownership to.
   */
  function transferOwnership(address newOwner) public onlyOwner {
    require(newOwner != address(0));
    OwnershipTransferred(owner, newOwner);
    owner = newOwner;
  }
}

上面面有沒有你沒學過的東東寓辱?

構(gòu)造函數(shù)function Ownable()是一個constructor(構(gòu)造函數(shù)),構(gòu)造函數(shù)不是必須的赤拒,它與合約同名秫筏,構(gòu)造函數(shù)一生中唯一的一次執(zhí)行,就是在合約最初被創(chuàng)建的時候挎挖。

函數(shù)修飾符modifier onlyOwner()这敬。 修飾符跟函數(shù)很類似,不過是用來修飾其他已有函數(shù)用的蕉朵, 在其他語句執(zhí)行前崔涂,為它檢查下先驗條件。 在這個例子中始衅,我們就可以寫個修飾符onlyOwner檢查下調(diào)用者冷蚂,確保只有合約的主人才能運行本函數(shù)缭保。我們下一章中會詳細講述修飾符,以及那個奇怪的_;蝙茶。

indexed關(guān)鍵字:別擔心艺骂,我們還用不到它。

所以Ownable合約基本都會這么干:

合約創(chuàng)建隆夯,構(gòu)造函數(shù)先行钳恕,將其owner設置為msg.sender(其部署者)

為它加上一個修飾符onlyOwner,它會限制陌生人的訪問吮廉,將訪問某些函數(shù)的權(quán)限鎖定在 owner 上苞尝。

允許將合約所有權(quán)轉(zhuǎn)讓給他人。

onlyOwner簡直人見人愛宦芦,大多數(shù)人開發(fā)自己的Solidity DApps宙址,都是從復制/粘貼 Ownable開始的,從它再繼承出的子類调卑,并在之上進行功能開發(fā)抡砂。

3.onlyOwner 函數(shù)修飾符

函數(shù)修飾符

函數(shù)修飾符看起來跟函數(shù)沒什么不同,不過關(guān)鍵字modifier告訴編譯器恬涧,這是個modifier(修飾符)注益,而不是個function(函數(shù))。它不能像函數(shù)那樣被直接調(diào)用溯捆,只能被添加到函數(shù)定義的末尾丑搔,用以改變函數(shù)的行為。咱們仔細讀讀onlyOwner:

/**
 * @dev 調(diào)用者不是‘主人’提揍,就會拋出異常
 */
modifier onlyOwner() {
  require(msg.sender == owner);
  _;
}

onlyOwner函數(shù)修飾符是這么用的:

contract MyContract is Ownable {
  event LaughManiacally(string laughter);

  //注意啤月! `onlyOwner`上場 :
  function likeABoss() external onlyOwner {
    LaughManiacally("Muahahahaha");
  }
}

注意likeABoss函數(shù)上的onlyOwner修飾符。 當你調(diào)用likeABoss時劳跃,首先執(zhí)行onlyOwner中的代碼谎仲, 執(zhí)行到onlyOwner中的_;語句時,程序再返回并執(zhí)行likeABoss中的代碼刨仑≈E担可見,盡管函數(shù)修飾符也可以應用到各種場合杉武,但最常見的還是放在函數(shù)執(zhí)行之前添加快速的require檢查辙诞。

因為給函數(shù)添加了修飾符`onlyOwner,使得唯有合約的主人(也就是部署者)才能調(diào)用它艺智。

注意:主人對合約享有的特權(quán)當然是正當?shù)奶纫贿^也可能被惡意使用。比如,萬一封拧,主人添加了個后門志鹃。所以非常重要的是,部署在以太坊上的DApp泽西,并不能保證它真正做到去中心曹铃,你需要閱讀并理解它的源代碼,才能防止其中沒有被部署者惡意植入后門捧杉;作為開發(fā)人員陕见,如何做到既要給自己留下修復bug的余地,又要盡量地放權(quán)給使用者味抖,以便讓他們放心你评甜,從而愿意把數(shù)據(jù)放在你的 DApp中,這確實需要個微妙的平衡仔涩。

4.Gas

現(xiàn)在我們懂了如何在禁止第三方修改我們合約的同時忍坷,留個后門給咱們自己去修改。讓我們來看另一種使得Solidity編程語言與眾不同的特征:Gas——驅(qū)動以太坊DApps的能源熔脂。

在Solidity中佩研,你的用戶想要每次執(zhí)行你的DApp都需要支付一定的gas,gas可以用以太幣購買霞揉,因此旬薯,用戶每次跑DApp都得花費以太幣。一個DApp收取多少gas取決于功能邏輯的復雜程度适秩。每個操作背后绊序,都在計算完成這個操作所需要的計算資源,(比如秽荞,存儲數(shù)據(jù)就比做個加法運算貴得多)政模, 一次操作所需要花費的gas等于這個操作背后的所有運算花銷的總和。

由于運行你的程序需要花費用戶的真金白銀蚂会,在以太坊中代碼的編程語言,比其他任何編程語言都更強調(diào)優(yōu)化耗式。同樣的功能胁住,使用笨拙的代碼開發(fā)的程序,比起經(jīng)過精巧優(yōu)化的代碼來刊咳,運行花費更高彪见,這顯然會給成千上萬的用戶帶來大量不必要的開銷。

為什么要用 gas 來驅(qū)動娱挨?

以太坊就像一個巨大余指、緩慢、但非常安全的電腦。當你運行一個程序的時候酵镜,網(wǎng)絡上的每一個節(jié)點都在進行相同的運算碉碉,以驗證它的輸出——這就是所謂的“去中心化”,由于數(shù)以千計的節(jié)點同時在驗證著每個功能的運行淮韭,這可以確保它的數(shù)據(jù)不會被被監(jiān)控垢粮,或者被刻意修改。

可能會有用戶用無限循環(huán)堵塞網(wǎng)絡靠粪,抑或用密集運算來占用大量的網(wǎng)絡資源蜡吧,為了防止這種事情的發(fā)生,以太坊的創(chuàng)建者為以太坊上的資源制定了價格占键,想要在以太坊上運算或者存儲昔善,你需要先付費。

注意:如果你使用側(cè)鏈畔乙,倒是不一定需要付費君仆。你不會想要在以太坊主網(wǎng)上玩“魔獸世界”吧?所需要的gas可能會買到你破產(chǎn)啸澡。但是你可以找個算法理念不同的側(cè)鏈來玩它袖订。我們將在以后的課程中咱們會討論到,什么樣的 DApp應該部署在太坊主鏈上嗅虏,什么又最好放在側(cè)鏈洛姑。

省gas的招數(shù):結(jié)構(gòu)封裝(Struct packing)

在第1課中,我們提到除了基本版的 uint 外皮服,還有其他變種uint:uint8楞艾,uint16,uint32等龄广。通常情況下我們不會考慮使用uint變種硫眯,因為無論如何定義uint的大小,Solidity為它保留256位的存儲空間择同。例如两入,使用uint8而不是uint(uint256)不會為你節(jié)省任何 gas。除非敲才,把uint綁定到struct里面裹纳。

如果一個struct中有多個uint,則盡可能使用較小的uint紧武,Solidity會將這些 uint打包在一起剃氧,從而占用較少的存儲空間。例如:

struct NormalStruct {
  uint a;
  uint b;
  uint c;
}

struct MiniMe {
  uint32 a;
  uint32 b;
  uint c;
}

因為使用了結(jié)構(gòu)打包阻星,mininormal 占用的空間更少

NormalStruct normal = NormalStruct(10, 20, 30);
MiniMe mini = MiniMe(10, 20, 30);

所以朋鞍,當uint 定義在一個struct中的時候,盡量使用最小的整數(shù)子類型以節(jié)約空間。 并且把同樣類型的變量放一起(即在 struct 中將把變量按照類型依次放置)滥酥,這樣Solidity可以將存儲空間最小化更舞。例如,有兩個 struct:

uint c; uint32 a; uint32 b; 和 uint32 a; uint c; uint32 b;

前者比后者需要的gas更少恨狈,因為前者把uint32放一起了疏哗。

5. 時間單位

時間單位

Solidity使用自己的本地時間單位。變量now將返回當前的unix時間戳(自1970年1月1日以來經(jīng)過的秒數(shù))禾怠。

注意:Unix時間傳統(tǒng)用一個32位的整數(shù)進行存儲返奉。這會導致“2038年”問題,當這個32位的unix時間戳不夠用吗氏,產(chǎn)生溢出芽偏,使用這個時間的遺留系統(tǒng)就麻煩了。所以弦讽,如果我們想讓我們的DApp跑夠20年污尉,我們可以使用64位整數(shù)表示時間,但為此我們的用戶又得支付更多的gas往产。真是個兩難的設計被碗!

Solidity還包含秒(seconds),分鐘(minutes)仿村,小時(hours)锐朴,天(days),周(weeks) 和年(years) 等時間單位蔼囊。它們都會轉(zhuǎn)換成對應的秒數(shù)放入uint 中焚志。所以1分鐘就是 60,1小時是 3600(60秒×60分鐘)畏鼓,1天是86400(24小時×60分鐘×60秒)酱酬,以此類推。

下面是一些使用時間單位的實用案例:

uint lastUpdated;

// 將‘上次更新時間’ 設置為 ‘現(xiàn)在’
function updateTimestamp() public {
  lastUpdated = now;
}

// 如果到上次`updateTimestamp` 超過5分鐘云矫,返回 'true'
// 不到5分鐘返回 'false'
function fiveMinutesHavePassed() public view returns (bool) {
  return (now >= (lastUpdated + 5 minutes));
}

有了這些工具膳沽,我們可以為僵尸設定”冷靜時間“功能。

6. 公有函數(shù)和安全性

你必須仔細地檢查所有聲明為public和external的函數(shù)让禀,一個個排除用戶濫用它們的可能贵少,謹防安全漏洞。請記住堆缘,如果這些函數(shù)沒有類似onlyOwner這樣的函數(shù)修飾符,用戶能利用各種可能的參數(shù)去調(diào)用它們普碎。檢查完這個函數(shù)吼肥,用戶就可以直接調(diào)用這個它。想要防止漏洞,最簡單的方法就是設其可見性為internal缀皱。

7.進一步了解函數(shù)修飾符

接下來斗这,我們將添加一些輔助方法。進一步學習什么是“函數(shù)修飾符”啤斗。

帶參數(shù)的函數(shù)修飾符

之前我們已經(jīng)讀過一個簡單的函數(shù)修飾符了:onlyOwner表箭。函數(shù)修飾符也可以帶參數(shù)。例如:

// 存儲用戶年齡的映射
mapping (uint => uint) public age;

// 限定用戶年齡的修飾符
modifier olderThan(uint _age, uint _userId) {
  require(age[_userId] >= _age);
  _;
}

必須年滿16周歲才允許開車 (至少在美國是這樣的)钮莲,我們可以用如下參數(shù)調(diào)用olderThan 修飾符:

function driveCar(uint _userId) public olderThan(16, _userId) {
  // 其余的程序邏輯
}

看到了吧免钻, olderThan修飾符可以像函數(shù)一樣接收參數(shù),是“宿主”函數(shù) driveCar把參數(shù)傳遞給它的修飾符的崔拥。

8.利用 'View' 函數(shù)節(jié)省Gas

“view”函數(shù)不花“gas”

當玩家從外部調(diào)用一個view函數(shù)极舔,是不需要支付gas的。這是因為view函數(shù)不會真正改變區(qū)塊鏈上的任何數(shù)據(jù)——它們只是讀取链瓦。因此用view標記一個函數(shù)拆魏,意味著告訴web3.js,運行這個函數(shù)只需要查詢你的本地以太坊節(jié)點慈俯,而不需要在區(qū)塊鏈上創(chuàng)建一個事務(事務需要運行在每個節(jié)點上渤刃,因此花費 gas)。

稍后我們將介紹如何在自己的節(jié)點上設置 web3.js贴膘。但現(xiàn)在卖子,你關(guān)鍵是要記住,在所能只讀的函數(shù)上標記上表示只讀的“external view”聲明步鉴,就能為你的玩家減少在DApp中g(shù)as用量揪胃。

注意:如果一個view函數(shù)在另一個函數(shù)的內(nèi)部被調(diào)用,而調(diào)用函數(shù)與view 函數(shù)的不屬于同一個合約氛琢,也會產(chǎn)生調(diào)用成本喊递。這是因為如果主調(diào)函數(shù)在以太坊創(chuàng)建了一個事務,它仍然需要逐個節(jié)點去驗證阳似。所以標記為view的函數(shù)只有同一個合約的外部調(diào)用時才是免費的骚勘。

9.存儲非常昂貴

Solidity使用storage(存儲)是相當昂貴的,”寫入“操作尤其貴撮奏。這是因為俏讹,無論是寫入還是更改一段數(shù)據(jù), 這都將永久性地寫入?yún)^(qū)塊鏈畜吊。需要在全球數(shù)千個節(jié)點的硬盤上存入這些數(shù)據(jù)泽疆,隨著區(qū)塊鏈的增長,拷貝份數(shù)更多玲献,存儲量也就越大殉疼。這是需要成本的梯浪!

為了降低成本,不到萬不得已瓢娜,避免將數(shù)據(jù)寫入存儲挂洛。這也會導致效率低下的編程邏輯——比如每次調(diào)用一個函數(shù),都需要在memory(內(nèi)存) 中重建一個數(shù)組眠砾,而不是簡單地將上次計算的數(shù)組給存儲下來以便快速查找虏劲。

在大多數(shù)編程語言中,遍歷大數(shù)據(jù)集合都是昂貴的褒颈。但是在Solidity中柒巫,使用一個標記了external view的函數(shù),遍歷比storage要便宜太多哈肖,因為view函數(shù)不會產(chǎn)生任何花銷吻育。

在內(nèi)存中聲明數(shù)組

在數(shù)組后面加上memory關(guān)鍵字, 表明這個數(shù)組是僅僅在內(nèi)存中創(chuàng)建淤井,不需要寫入外部存儲布疼,并且在函數(shù)調(diào)用結(jié)束時它就解散了。與在程序結(jié)束時把數(shù)據(jù)保存進storage的做法相比币狠,內(nèi)存運算可以大大節(jié)省gas開銷——把這數(shù)組放在view里用游两,完全不用花錢。以下是申明一個內(nèi)存數(shù)組的例子:

function getArray() external pure returns(uint[]) {
  // 初始化一個長度為3的內(nèi)存數(shù)組
  uint[] memory values = new uint[](3);
  // 賦值
  values.push(1);
  values.push(2);
  values.push(3);
  // 返回數(shù)組
  return values;
}

這個小例子展示了一些語法規(guī)則漩绵,下一章中贱案,我們將通過一個實際用例,展示它和for循環(huán)結(jié)合的做法止吐。

注意:內(nèi)存數(shù)組必須用長度參數(shù)(在本例中為3)創(chuàng)建宝踪。目前不支持 array.push()之類的方法調(diào)整數(shù)組大小,在未來的版本可能會支持長度修改碍扔。

10.For 循環(huán)

在上面我們提到過瘩燥,函數(shù)中使用的數(shù)組是運行時在內(nèi)存中通過for循環(huán)實時構(gòu)建,而不是預先建立在存儲中的不同。for循環(huán)的語法在Solidity和JavaScript中類似厉膀。來看一個創(chuàng)建偶數(shù)數(shù)組的例子:

function getEvens() pure external returns(uint[]) {
  uint[] memory evens = new uint[](5);
  // 在新數(shù)組中記錄序列號
  uint counter = 0;
  // 在循環(huán)從1迭代到10:
  for (uint i = 1; i <= 10; i++) {
    // 如果 `i` 是偶數(shù)...
    if (i % 2 == 0) {
      // 把它加入偶數(shù)數(shù)組
      evens[counter] = i;
      //索引加一, 指向下一個空的‘even’
      counter++;
    }
  }
  return evens;
}

這個函數(shù)將返回一個形為 [2,4,6,8,10] 的數(shù)組二拐。


總結(jié)

本章了解了智能合約一旦部署則不可修改服鹅,所以在編寫完智能合約后,對智能合約的審查是非常重要的事情百新,如果有任何差池企软,將不可逆轉(zhuǎn)且不再受控。不過也不要太過擔心饭望,因為有像OpenZeppelin類似的合約庫澜倦,專門做智能合約安保和審查聚蝶,可以減輕我們不少工作。

另外本課還有一個重要的信息就是:信息的修改存儲到區(qū)塊鏈上是需要花費gas的藻治。這就要求我們在編寫智能合約時將一些中間信息盡量在本地內(nèi)存中處理,同時solidity也提供了像view這樣的函數(shù)以節(jié)省gas巷挥。

從本課知道桩卵,編寫智能合約不是一件簡單的事情,每一行代碼都涉及到真金白銀倍宾,所以除了編寫者要細致外雏节,還需要多人和專業(yè)的工具做檢查和測試,才能產(chǎn)出一份合格的智能合約高职。


系列文章:
【CryptoZombies|編寫區(qū)塊鏈游戲?qū)W智能合約】Lesson1: 搭建僵尸工廠
【CryptoZombies|編寫區(qū)塊鏈游戲?qū)W智能合約】Lesson2: 僵尸攻擊人類
【CryptoZombies|編寫區(qū)塊鏈游戲?qū)W智能合約】Lesson3: 搭建僵尸工廠
【CryptoZombies|編寫區(qū)塊鏈游戲?qū)W智能合約】Lesson4: 僵尸作戰(zhàn)系統(tǒng)
【CryptoZombies|編寫區(qū)塊鏈游戲?qū)W智能合約】Lesson5: ERC721標準和加密收藏品

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末钩乍,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子怔锌,更是在濱河造成了極大的恐慌寥粹,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,695評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件埃元,死亡現(xiàn)場離奇詭異涝涤,居然都是意外死亡,警方通過查閱死者的電腦和手機岛杀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評論 3 399
  • 文/潘曉璐 我一進店門阔拳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人类嗤,你說我怎么就攤上這事糊肠。” “怎么了遗锣?”我有些...
    開封第一講書人閱讀 168,130評論 0 360
  • 文/不壞的土叔 我叫張陵货裹,是天一觀的道長。 經(jīng)常有香客問我黄伊,道長泪酱,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,648評論 1 297
  • 正文 為了忘掉前任还最,我火速辦了婚禮墓阀,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘拓轻。我一直安慰自己斯撮,他們只是感情好,可當我...
    茶點故事閱讀 68,655評論 6 397
  • 文/花漫 我一把揭開白布扶叉。 她就那樣靜靜地躺著勿锅,像睡著了一般帕膜。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上溢十,一...
    開封第一講書人閱讀 52,268評論 1 309
  • 那天垮刹,我揣著相機與錄音,去河邊找鬼张弛。 笑死荒典,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的吞鸭。 我是一名探鬼主播寺董,決...
    沈念sama閱讀 40,835評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼刻剥!你這毒婦竟也來了遮咖?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,740評論 0 276
  • 序言:老撾萬榮一對情侶失蹤造虏,失蹤者是張志新(化名)和其女友劉穎御吞,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體酗电,經(jīng)...
    沈念sama閱讀 46,286評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡魄藕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,375評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了撵术。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片背率。...
    茶點故事閱讀 40,505評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖嫩与,靈堂內(nèi)的尸體忽然破棺而出寝姿,到底是詐尸還是另有隱情,我是刑警寧澤划滋,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布饵筑,位于F島的核電站,受9級特大地震影響处坪,放射性物質(zhì)發(fā)生泄漏根资。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,873評論 3 333
  • 文/蒙蒙 一同窘、第九天 我趴在偏房一處隱蔽的房頂上張望玄帕。 院中可真熱鬧,春花似錦想邦、人聲如沸裤纹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鹰椒。三九已至锡移,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間漆际,已是汗流浹背淆珊。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留奸汇,地道東北人套蒂。 一個月前我還...
    沈念sama閱讀 48,921評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像茫蛹,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子烁挟,可洞房花燭夜當晚...
    茶點故事閱讀 45,515評論 2 359