智能合約教程

https://segmentfault.com/a/1190000015295148

流程

合約代碼編寫(Solidity)-> 合約編譯(solc)-> 合約部署(web3)

開發語言及工具:

  • 區塊鏈節點:ganache-cli
  • 基礎環境:node
  • 合約開發語言:Solidity
  • 合約編譯器:solc
  • 合約訪問庫:web3.js

基礎環境安裝

  • 1、安裝 node.js
  • 2、安裝 ganache-cli

ganache-cli

sudo npm install -g ganache-cli

ganache 默認會自動創建 10 個賬戶,每個賬戶有 100 個以太幣(ETH:Ether)。 可以把賬戶視為銀行賬戶,以太幣就是以太坊生態系統中的貨幣。

面輸出的最后一句話,描述了節點仿真器的監聽地址和端口為localhost:8545,在使用 web3.js 時,需要傳入這個地址來告訴web3js庫應當連接到哪一個節點。

合約設計

合約中的屬性用來聲明合約的狀態,而合約中的方法則提供修改狀態的訪問接口。

重點:

  • 合約狀態是持久化到區塊鏈上的,因此對合約狀態的修改需要消耗以太幣。
  • 只有在合約部署到區塊鏈的時候,才會調用構造函數,并且只調用一次。
  • 與 web 世界里每次部署代碼都會覆蓋舊代碼不同,在區塊鏈上部署的合約是不可改變的,也就是說,如果你更新 合約并再次部署,舊的合約仍然會在區塊鏈上存在,并且合約的狀態數據也依然存在。新的部署將會創建合約的一 個新的實例。

狀態變量和整數

Solidity

顯著特點:

  1. 后綴.sol
  2. 強類型語言
  3. 語法和javascript類似

函數類型

view: 讀取區塊鏈上的數據,但是不修改區塊鏈上面的數據

pure:不修改也不讀取區塊鏈上面的數據

一筆事務的控制臺信息

地址類型

合約代碼編寫(Solidity)-> 合約編譯(solc)-> 合約部署(web3)

開發語言及工具:

  • 區塊鏈節點:ganache-cli
  • 基礎環境:node
  • 合約開發語言:Solidity
  • 合約編譯器:solc
  • 合約訪問庫:web3.js

版本指令

標明 Solidity 編譯器的版本. 以避免將來新的編譯器可能破壞你的代碼。

pragma solidity ^0.4.19;

^0.4.20 0.4.x

~0.4.20 0.x.x

EVM數據存儲

  • storage:狀態變量(全局變量)會存儲在這里。永久性存儲數據(因為會把數據寫入區塊鏈當中),

  • stack:函數中的本地變量(局部變量)默認會存儲在這里,暫時性存儲數據。

  • memory:暫時性存儲數據,實參(函數中實際傳遞的參數)默認會存儲在這里。

tips:storage和memory都需要消耗gas,但是storage更貴。

結構體作為函數參數,函數必須是internal類型

數組

// 固定長度為2的靜態數組:

uint[2] fixedArray;

// 固定長度為5string類型的靜態數組:

string[5] stringArray;

// 動態數組,長度不固定,可以動態添加元素:

uint[] dynamicArray;

公共數組

你可以定義 public 數組, Solidity 會自動創建 getter 方法. 語法如下:

Person[] public people;

其它的合約可以從這個數組讀取數據(但不能寫入數據),所以這在合約中是一個有用的保存公共數據的模式。

定義函數

function eatHamburgers(string _name, uint _amount) {

}

注:: 習慣上函數里的變量都是以(_)開頭 (但不是硬性規定) 以區別全局變量。我們整個教程都會沿用這個習慣。

私有 / 公共函數

Solidity 定義的函數的屬性默認為公共。 這就意味著任何一方 (或其它合約) 都可以調用你合約里的函數。

顯然,不是什么時候都需要這樣,而且這樣的合約易于受到攻擊。

uint[] numbers;

function _addToArray(uint _number) private {

numbers.push(_number);

}

返回值

string greeting = "What's up dog";

function sayHello() public returns (string) {

return greeting;

}

函數的修飾符****view,returns

把函數定義為 view, 意味著它只能讀取數據不能更改數據:

function sayHello() public view returns (string) {}

Solidity 還支持 pure 函數, 表明這個函數甚至都不訪問應用里的數據,例如:

function _multiply(uint a, uint b) private pure returns (uint) {

return a * b;

}

Keccak256 和 類型轉換

Ethereum 內部有一個散列函數keccak256,它用了SHA3版本。一個散列函數基本上就是把一個字符串轉換為一個256位的16進制數字。字符串的一個微小變化會引起散列數據極大變化。

注: 在區塊鏈中安全地產生一個隨機數是一個很難的問題, 本例的方法不安全,但是在我們的Zombie DNA算法里不是那么重要,已經很好地滿足我們的需要了。

事件

事件 是合約和區塊鏈通訊的一種機制。你的前端應用監聽某些事件,并做出反應。

// 這里建立事件

event IntegersAdded(uint x, uint y, uint result);

function add(uint _x, uint _y) public {

uint result = _x + _y;

//觸發事件,通知app

IntegersAdded(_x, _y, result);

return result;

}

YourContract.IntegersAdded(function(error, result) {

// 干些事

}

映射(****Mapping)和地址(Address)

Addresses(地址)

以太坊區塊鏈由 account (賬戶)組成,你可以把它想象成銀行賬戶。一個帳戶的余額是 以太 (在以太坊區塊鏈上使用的幣種),你可以和其他帳戶之間支付和接受以太幣,就像你的銀行帳戶可以電匯資金到其他銀行帳戶一樣。

每個帳戶都有一個“地址”,你可以把它想象成銀行賬號。這是賬戶唯一的標識符,它看起來長這樣:

0x0cE446255506E92DF41614C46F1d6df9Cc969183

Mapping(映射)

//對于金融應用程序,將用戶的余額保存在一個 uint類型的變量中:

mapping (address => uint) public accountBalance;

//或者可以用來通過userId 存儲/查找的用戶名

mapping (uint => string) userIdToName;

msg.sender

在 Solidity 中,有一些全局變量可以被所有函數調用。 其中一個就是 msg.sender,它指的是當前調用者(或智能合約)的 address。

注意:在 Solidity 中,功能執行始終需要從外部調用者開始。 一個合約只會在區塊鏈上什么也不做,除非有人調用其中的函數。所以 msg.sender 總是存在的。

注意:在 Solidity 中,功能執行始終需要從外部調用者開始。 一個合約只會在區塊鏈上什么也不做,除非有人調用其中的函數。所以 msg.sender 總是存在的。

以下是使用**** msg.sender 來更新 mapping 的例子:

mapping (address => uint) favoriteNumber;

function setMyNumber(uint _myNumber) public {

// 更新我們的 favoriteNumber 映射來將 _myNumber存儲在 msg.sender名下

favoriteNumber[msg.sender] = _myNumber;

// 存儲數據至映射的方法和將數據存儲在數組相似

}

function whatIsMyNumber() public view returns (uint) {

// 拿到存儲在調用者地址名下的值

// 若調用者還沒調用 setMyNumber,則值為 0

return favoriteNumber[msg.sender];

}

Require

function sayHiToVitalik(string _name) public returns (string) {

// 比較 _name 是否等于 "Vitalik". 如果不成立,拋出異常并終止程序

// (敲黑板: Solidity 并不支持原生的字符串比較, 我們只能通過比較

// 兩字符串的 keccak256 哈希值來進行判斷)

require(keccak256(_name) == keccak256("Vitalik"));

// 如果返回 true, 運行如下語句

return "Hi!";

}

繼承 Inheritance

有個讓**** Solidity 的代碼易于管理的功能,就是合約 inheritance (繼承):

contract Doge {

function catchphrase() public returns (string) {

return "So Wow CryptoDoge";

}

}

contract BabyDoge is Doge {

function anotherCatchphrase() public returns (string) {

return "Such Moon BabyDoge";

}

}

由于 BabyDoge 是從 Doge 那里 inherits (繼承)過來的。 這意味著當你編譯和部署了 BabyDoge,它將可以訪問 catchphrase() 和 anotherCatchphrase()和其他我們在 Doge 中定義的其他公共函數。

引入****Import

Solidity 中,當你有多個文件并且想把一個文件導入另一個文件時,可以使用 import 語句:

import "./someothercontract.sol";

contract newContract is SomeOtherContract {

}

Storage與Memory

在 Solidity 中,有兩個地方可以存儲變量 —— storage 或 memory。

Storage 變量是指永久存儲在區塊鏈中的變量。 Memory 變量則是臨時的,當外部函數對某合約調用完成時,內存型變量即被移除。 你可以把它想象成存儲在你電腦的硬盤或是RAM中數據的關系。

大多數時候你都用不到這些關鍵字,默認情況下 Solidity 會自動處理它們。 狀態變量(在函數之外聲明的變量)默認為“存儲”形式,并永久寫入區塊鏈;而在函數內部聲明的變量是“內存”型的,它們函數調用結束后消失。

internal 和 external

internal 和 private 類似,不過, 如果某個合約繼承自其父合約,這個合約即可以訪問父合約中定義的內部函數。(嘿,這聽起來正是我們想要的那樣!)。

external 與public 類似,只不過這些函數只能在合約之外調用 - 它們不能被合約內的其他函數調用。稍后我們將討論什么時候使用 external 和 public。

Ownable

下面是一個 Ownable 合約的例子: 來自 OpenZeppelin Solidity 庫的 Ownable 合約。 OpenZeppelin 是主打安保和社區審查的智能合約庫,您可以在自己的 DApps中引用。等把這一課學完,您不要催我們發布下一課,最好利用這個時間把 OpenZeppelin 的網站看看,保管您會學到很多東西!

  • 構造函數:function Ownable()是一個 constructor (構造函數),構造函數不是必須的,它與合約同名,構造函數一生中唯一的一次執行,就是在合約最初被創建的時候。
  • 函數修飾符:modifier onlyOwner()。 修飾符跟函數很類似,不過是用來修飾其他已有函數用的, 在其他語句執行前,為它檢查下先驗條件。 在這個例子中,我們就可以寫個修飾符 onlyOwner 檢查下調用者,確保只有合約的主人才能運行本函數。我們下一章中會詳細講述修飾符,以及那個奇怪的_;。
  • indexed 關鍵字:別擔心,我們還用不到它。

    • @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;

}

}

Gas-驅動以太坊DApps的能源

在 Solidity 中,你的用戶想要每次執行你的 DApp 都需要支付一定的 gasgas 可以用以太幣購買,因此,用戶每次跑 DApp 都得花費以太幣。

一個 DApp 收取多少 gas 取決于功能邏輯的復雜程度。每個操作背后,都在計算完成這個操作所需要的計算資源,(比如,存儲數據就比做個加法運算貴得多), 一次操作所需要花費的 gas 等于這個操作背后的所有運算花銷的總和。

由于運行你的程序需要花費用戶的真金白銀,在以太坊中代碼的編程語言,比其他任何編程語言都更強調優化。同樣的功能,使用笨拙的代碼開發的程序,比起經過精巧優化的代碼來,運行花費更高,這顯然會給成千上萬的用戶帶來大量不必要的開銷。

為何要****gas來驅動?

以太坊就像一個巨大、緩慢、但非常安全的電腦。當你運行一個程序的時候,網絡上的每一個節點都在進行相同的運算,以驗證它的輸出 —— 這就是所謂的”去中心化“ 由于數以千計的節點同時在驗證著每個功能的運行,這可以確保它的數據不會被被監控,或者被刻意修改。

可能會有用戶用無限循環堵塞網絡,抑或用密集運算來占用大量的網絡資源,為了防止這種事情的發生,以太坊的創建者為以太坊上的資源制定了價格,想要在以太坊上運算或者存儲,你需要先付費。

注意:如果你使用側鏈,倒是不一定需要付費,比如咱們在 Loom Network 上構建的 CryptoZombies 就免費。你不會想要在以太坊主網上玩兒“魔獸世界”吧? - 所需要的 gas 可能會買到你破產。但是你可以找個算法理念不同的側鏈來玩它。我們將在以后的課程中咱們會討論到,什么樣的 DApp 應該部署在太坊主鏈上,什么又最好放在側鏈。

時間單位

readyTime 稍微復雜點。我們希望增加一個“冷卻周期”,表示僵尸在兩次獵食或攻擊之之間必須等待的時間。如果沒有它,僵尸每天可能會攻擊和繁殖1,000次,這樣游戲就太簡單了。

Solidity 使用自己的本地時間單位。

變量 now 將返回當前的unix時間戳(自1970年1月1日以來經過的秒數)。我寫這句話時 unix 時間是 1515527488。

注意:Unix時間傳統用一個32位的整數進行存儲。這會導致“2038年”問題,當這個32位的unix時間戳不夠用,產生溢出,使用這個時間的遺留系統就麻煩了。所以,如果我們想讓我們的 DApp 跑夠20年,我們可以使用64位整數表示時間,但為此我們的用戶又得支付更多的 gas。真是個兩難的設計啊!

Solidity 還包含秒(seconds),分鐘(minutes),小時(hours),天(days),周(weeks) 和 年(years) 等時間單位。它們都會轉換成對應的秒數放入 uint 中。所以 1分鐘 就是 60,1小時是 3600(60秒×60分鐘),1天是86400(24小時×60分鐘×60秒),以此類推

函數修飾符

  • 1、我們有決定函數何時和被誰調用的可見性修飾符: private 意味著它只能被合約內部調用; internal 就像 private 但是也能被繼承的合約調用; external 只能從合約外部調用;最后 public 可以在任何地方調用,不管是內部還是外部。
  • 2、我們也有狀態修飾符, 告訴我們函數如何和區塊鏈交互: view 告訴我們運行這個函數不會更改和保存任何數據; pure 告訴我們這個函數不但不會往區塊鏈寫數據,它甚至不從區塊鏈讀取數據。這兩種在被從合約外部調用的時候都不花費任何gas(但是它們在被內部其他函數調用的時候將會耗費gas)。
  • 3、然后我們有了自定義的 modifiers,例如在第三課學習的: onlyOwner 和 aboveLevel。 對于這些修飾符我們可以自定義其對函數的約束邏輯。

payable修飾符

payable 方法是讓 Solidity 和以太坊變得如此酷的一部分 —— 它們是一種可以接收以太的特殊函數。

先放一下。當你在調用一個普通網站服務器上的API函數的時候,你無法用你的函數傳送美元——你也不能傳送比特幣。

但是在以太坊中, 因為錢 (以太), 數據 (事務負載), 以及合約代碼本身都存在于以太坊。你可以在同時調用函數 并付錢給另外一個合約。

這就允許出現很多有趣的邏輯, 比如向一個合約要求支付一定的錢來運行一個函數。

示例

contract OnlineStore {

function buySomething() external payable {

// 檢查以確定0.001以太發送出去來運行函數:

require(msg.value == 0.001 ether);

// 如果為真,一些用來向函數調用者發送數字內容的邏輯

transferThing(msg.sender);

}

}

提現

在上一節,我們學習了如何向合約發送以太,那么在發送之后會發生什么呢?

在你發送以太之后,它將被存儲進以合約的以太坊賬戶中, 并凍結在哪里 —— 除非你添加一個函數來從合約中把以太提現。

你可以寫一個函數來從合約中提現以太,類似這樣:

contract GetPaid is Ownable {

function withdraw() external onlyOwner {

owner.transfer(this.balance);

}

}

隨機數

優秀的游戲都需要一些隨機元素,那么我們在 Solidity 里如何生成隨機數呢?

真正的答案是你不能,或者最起碼,你無法安全地做到這一點。

我們來看看為什么

用** keccak256 **來制造隨機數
Solidity 中最好的隨機數生成器是 keccak256 哈希函數.

我們可以這樣來生成一些隨機數

// 生成一個0到100的隨機數:

uint randNonce = 0;

uint random = uint(keccak256(now, msg.sender, randNonce)) % 100;

randNonce++;

uint random2 = uint(keccak256(now, msg.sender, randNonce)) % 100;

這個方法首先拿到 now 的時間戳、 msg.sender、 以及一個自增數 nonce (一個僅會被使用一次的數,這樣我們就不會對相同的輸入值調用一次以上哈希函數了)。

然后利用 keccak 把輸入的值轉變為一個哈希值, 再將哈希值轉換為 uint, 然后利用 % 100 來取最后兩位, 就生成了一個0到100之間隨機數了。

這個方法很容易被不誠實的節點攻擊
在以太坊上, 當你在和一個合約上調用函數的時候, 你會把它廣播給一個節點或者在網絡上的 transaction 節點們。 網絡上的節點將收集很多事務, 試著成為第一個解決計算密集型數學問題的人,作為“工作證明”,然后將“工作證明”(Proof of Work, PoW)和事務一起作為一個 block 發布在網絡上。

一旦一個節點解決了一個PoW, 其他節點就會停止嘗試解決這個 PoW, 并驗證其他節點的事務列表是有效的,然后接受這個節點轉而嘗試解決下一個節點。

這就讓我們的隨機數函數變得可利用了

我們假設我們有一個硬幣翻轉合約——正面你贏雙倍錢,反面你輸掉所有的錢。假如它使用上面的方法來決定是正面還是反面 (random >= 50 算正面, random < 50 算反面)。

如果我正運行一個節點,我可以 只對我自己的節點 發布一個事務,且不分享它。 我可以運行硬幣翻轉方法來偷窺我的輸贏 — 如果我輸了,我就不把這個事務包含進我要解決的下一個區塊中去。我可以一直運行這個方法,直到我贏得了硬幣翻轉并解決了下一個區塊,然后獲利。

所以我們該如何在以太坊上安全地生成隨機數呢?

因為區塊鏈的全部內容對所有參與者來說是透明的, 這就讓這個問題變得很難,它的解決方法不在本課程討論范圍,你可以閱讀 這個 StackOverflow 上的討論 來獲得一些主意。 一個方法是利用 oracle 來訪問以太坊區塊鏈之外的隨機數函數。

ERC20 ERC721標準(七)

預防溢出

僵尸轉移給0 地址(這被稱作 “燒幣”, 基本上就是把代幣轉移到一個誰也沒有私鑰的地址,讓這個代幣永遠也無法恢復)

假設我們有一個 uint8, 只能存儲8 bit數據。這意味著我們能存儲的最大數字就是二進制 11111111 (或者說十進制的 2^8 - 1 = 255).

來看看下面的代碼。最后 number 將會是什么值?

uint8 number = 255;

number++;

在這個例子中,我們導致了溢出 — 雖然我們加了1, 但是 number 出乎意料地等于 0了。 (如果你給二進制 11111111 加1, 它將被重置為 00000000,就像鐘表從 23:59 走向 00:00)。

下溢(underflow)也類似,如果你從一個等于 0 的 uint8 減去 1, 它將變成 255 (因為 uint 是無符號的,其不能等于負數)。

使用 SafeMath

為了防止這些情況,OpenZeppelin 建立了一個叫做 SafeMath 的 庫(library),默認情況下可以防止這些問題。

不過在我們使用之前…… 什么叫做庫?

一個庫是 Solidity 中一種特殊的合約。其中一個有用的功能是給原始數據類型增加一些方法。

比如,使用 SafeMath 庫的時候,我們將使用 using SafeMath for uint256 這樣的語法。 SafeMath 庫有四個方法 — add, sub, mul, 以及 div。現在我們可以這樣來讓 uint256 調用這些方法:

using SafeMath for uint256;

uint256 a = 5;

uint256 b = a.add(3); // 5 + 3 = 8

uint256 c = a.mul(2); // 5 * 2 = 10

assertrequire區別

assert 和 require 相似,若結果為否它就會拋出錯誤。 assert 和 require 區別在于,require 若失敗則會返還給用戶剩下的 gasassert 則不會。所以大部分情況下,你寫代碼的時候會比較喜歡 require,assert 只在代碼可能出現嚴重錯誤的時候使用,比如 uint 溢出。

Solidity 社區所使用的一個標準是使用一種被稱作 natspec 的格式,看起來像這樣:

/// @title 一個簡單的基礎運算合約

/// @author H4XF13LD MORRIS

/// @notice ???ú£??a??o?????ìí?óò???3?·¨

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,333評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,491評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,263評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,946評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,708評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,409評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,939評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,774評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,641評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,872評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,650評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容