eos源码解析(二): bancor算法

近来ram经历了大涨大跌,ram的核心是bancor算法,网上有很多对bancor的介绍,这里就不谈了,eos少数派报告的星主Anima对此有深刻的分析。本文直接从源码解析eos的bancor算法。
最开始,,,我新建了一条链,对,是一条新链。然后在完整的64G内存上购买了10000k 内存。下面的命令是新建一个账户并购买10000k的内存。
!!!!直接要结果的,跳过源码,后文自取,不谢不谢!!!!

cleos system newaccount eosio --transfer accountnum11 EOS8mUftJXepGzdQ2TaCduNuSPAfXJHf22uex4u41ab1EVv9EAhWt --stake-net "100000.0000 SYS" --stake-cpu "100000.0000 SYS" --buy-ram-kbytes 10000

在eos的内部,这条命令触发的函数是这样的:

//payer:eosio receiver:accountnum11 bytes:10000
   void system_contract::buyrambytes( account_name payer, account_name receiver, uint32_t bytes ) {
      auto itr = _rammarket.find(S(4,RAMCORE));
      auto tmp = *itr;
      auto eosout = tmp.convert( asset(bytes,S(0,RAM)), CORE_SYMBOL );

      buyram( payer, receiver, eosout );
   }

先调用convert,convert涉及到很多函数,这是bancor真正实现,一并列在下面,我加了打印,后面会放打印结果。。。。

namespace eosiosystem {
   asset exchange_state::convert_to_exchange( connector& c, asset in ) {

      real_type R(supply.amount);
      real_type C(c.balance.amount+in.amount);
      real_type F(c.weight/1000.0);
      real_type T(in.amount);
      real_type ONE(1.0);
       print("\nconvert_to_exchange \n");
       print( "supply.amount ", supply.amount , "\n" );
       print( "c.balance.amount ", c.balance.amount, "\n" );
       print( "c.weight: ", c.weight, "\n" );
       print( "in.amount: ", in.amount, "\n\n" );

      real_type E = -R * (ONE - std::pow( ONE + T / C, F) );
      print( "E: ", E, "\n");
      int64_t issued = int64_t(E);

      supply.amount += issued;
      c.balance.amount += in.amount;

      return asset( issued, supply.symbol );
   }

   asset exchange_state::convert_from_exchange( connector& c, asset in ) {
      eosio_assert( in.symbol== supply.symbol, "unexpected asset symbol input" );

      real_type R(supply.amount - in.amount);
      real_type C(c.balance.amount);
      real_type F(1000.0/c.weight);
      real_type E(in.amount);
      real_type ONE(1.0);

       print("\nconvert_from_exchange \n");
       print( "supply.amount ", supply.amount , "\n" );
       print( "c.balance.amount ", c.balance.amount, "\n" );
       print( "c.weight: ", c.weight, "\n" );
       print( "in.amount: ", in.amount, "\n\n" );

     // potentially more accurate: 
     // The functions std::expm1 and std::log1p are useful for financial calculations, for example, 
     // when calculating small daily interest rates: (1+x)n
     // -1 can be expressed as std::expm1(n * std::log1p(x)). 
     // real_type T = C * std::expm1( F * std::log1p(E/R) );
      
      real_type T = C * (std::pow( ONE + E/R, F) - ONE);
      print( "T: ", T, "\n");
      int64_t out = int64_t(T);

      supply.amount -= in.amount;
      c.balance.amount -= out;

      return asset( out, c.balance.symbol );
   }

   asset exchange_state::convert( asset from, symbol_type to ) {
      auto sell_symbol  = from.symbol;
      auto ex_symbol    = supply.symbol;
      auto base_symbol  = base.balance.symbol;
      auto quote_symbol = quote.balance.symbol;

      print( "From: ", from, " TO ", asset( 0,to), "\n" );
      print( "base: ", base_symbol, "\n" );
      print("base_amount: ", base.balance.amount , "\n");
      print( "quote: ", quote_symbol, "\n" );
       print( "quote_amount: ", quote.balance.amount, "\n" );
      print( "ex: ", supply.symbol, "\n" );
       print( "ex_amuont: ", supply.amount, "\n" );


      if( sell_symbol != ex_symbol ) {
         if( sell_symbol == base_symbol ) {
            from = convert_to_exchange( base, from );
         } else if( sell_symbol == quote_symbol ) {
            from = convert_to_exchange( quote, from );
         } else { 
            eosio_assert( false, "invalid sell" );
         }
      } else {
         if( to == base_symbol ) {
            from = convert_from_exchange( base, from ); 
         } else if( to == quote_symbol ) {
            from = convert_from_exchange( quote, from ); 
         } else {
            eosio_assert( false, "invalid conversion" );
         }
      }

      if( to != from.symbol )
         return convert( from, to );

      return from;
   }
} /// namespace eosiosystem

然后调用buyram

  void system_contract::buyram( account_name payer, account_name receiver, asset quant )
   {
      require_auth( payer );
      eosio_assert( quant.amount > 0, "must purchase a positive amount" );

      auto fee = quant;
      fee.amount = ( fee.amount + 199 ) / 200; /// .5% fee (round up)
      // fee.amount cannot be 0 since that is on近来ram经历了大涨大跌,ram的核心是bancor算法,网上有很多对bancor的介绍,这里就不谈了,eos少数派报告的星主Anima对此有深刻的分析。本文直接从源码解析eos的bancor算法。
最开始,,,我新建了一条链,对,是一条新链。然后在完整的64G内存上购买了10000k 内存。下面的命令是新建一个账户并购买10000k的内存。
!!!!直接要结果的,跳过源码,后文自取,不谢不谢!!!!

cleos system newaccount eosio --transfer accountnum11 EOS8mUftJXepGzdQ2TaCduNuSPAfXJHf22uex4u41ab1EVv9EAhWt --stake-net "100000.0000 SYS" --stake-cpu "100000.0000 SYS" --buy-ram-kbytes 10000

在eos的内部,这条命令触发的函数是这样的:

//payer:eosio receiver:accountnum11 bytes:10000
   void system_contract::buyrambytes( account_name payer, account_name receiver, uint32_t bytes ) {
      auto itr = _rammarket.find(S(4,RAMCORE));
      auto tmp = *itr;
      auto eosout = tmp.convert( asset(bytes,S(0,RAM)), CORE_SYMBOL );

      buyram( payer, receiver, eosout );
   }

先调用convert,convert涉及到很多函数,这是bancor真正实现,一并列在下面,我加了打印,后面会放打印结果。。。。

namespace eosiosystem {
   asset exchange_state::convert_to_exchange( connector& c, asset in ) {

      real_type R(supply.amount);
      real_type C(c.balance.amount+in.amount);
      real_type F(c.weight/1000.0);
      real_type T(in.amount);
      real_type ONE(1.0);
       print("\nconvert_to_exchange \n");
       print( "supply.amount ", supply.amount , "\n" );
       print( "c.balance.amount ", c.balance.amount, "\n" );
       print( "c.weight: ", c.weight, "\n" );
       print( "in.amount: ", in.amount, "\n\n" );

      real_type E = -R * (ONE - std::pow( ONE + T / C, F) );
      print( "E: ", E, "\n");
      int64_t issued = int64_t(E);

      supply.amount += issued;
      c.balance.amount += in.amount;

      return asset( issued, supply.symbol );
   }

   asset exchange_state::convert_from_exchange( connector& c, asset in ) {
      eosio_assert( in.symbol== supply.symbol, "unexpected asset symbol input" );

      real_type R(supply.amount - in.amount);
      real_type C(c.balance.amount);
      real_type F(1000.0/c.weight);
      real_type E(in.amount);
      real_type ONE(1.0);

       print("\nconvert_from_exchange \n");
       print( "supply.amount ", supply.amount , "\n" );
       print( "c.balance.amount ", c.balance.amount, "\n" );
       print( "c.weight: ", c.weight, "\n" );
       print( "in.amount: ", in.amount, "\n\n" );

     // potentially more accurate: 
     // The functions std::expm1 and std::log1p are useful for financial calculations, for example, 
     // when calculating small daily interest rates: (1+x)n
     // -1 can be expressed as std::expm1(n * std::log1p(x)). 
     // real_type T = C * std::expm1( F * std::log1p(E/R) );
      
      real_type T = C * (std::pow( ONE + E/R, F) - ONE);
      print( "T: ", T, "\n");
      int64_t out = int64_t(T);

      supply.amount -= in.amount;
      c.balance.amount -= out;

      return asset( out, c.balance.symbol );
   }

   asset exchange_state::convert( asset from, symbol_type to ) {
      auto sell_symbol  = from.symbol;
      auto ex_symbol    = supply.symbol;
      auto base_symbol  = base.balance.symbol;
      auto quote_symbol = quote.balance.symbol;

      print( "From: ", from, " TO ", asset( 0,to), "\n" );
      print( "base: ", base_symbol, "\n" );
      print("base_amount: ", base.balance.amount , "\n");
      print( "quote: ", quote_symbol, "\n" );
       print( "quote_amount: ", quote.balance.amount, "\n" );
      print( "ex: ", supply.symbol, "\n" );
       print( "ex_amuont: ", supply.amount, "\n" );


      if( sell_symbol != ex_symbol ) {
         if( sell_symbol == base_symbol ) {
            from = convert_to_exchange( base, from );
         } else if( sell_symbol == quote_symbol ) {
            from = convert_to_exchange( quote, from );
         } else { 
            eosio_assert( false, "invalid sell" );
         }
      } else {
         if( to == base_symbol ) {
            from = convert_from_exchange( base, from ); 
         } else if( to == quote_symbol ) {
            from = convert_from_exchange( quote, from ); 
         } else {
            eosio_assert( false, "invalid conversion" );
         }
      }

      if( to != from.symbol )
         return convert( from, to );

      return from;
   }
} /// namespace eosiosystem

然后调用buyram

  void system_contract::buyram( account_name payer, account_name receiver, asset quant )
   {
      require_auth( payer );
      eosio_assert( quant.amount > 0, "must purchase a positive amount" );

      auto fee = quant;
      fee.amount = ( fee.amount + 199 ) / 200; /// .5% fee (round up)
      // fee.amount cannot be 0 since that is only possible if quant.amount is 0 which is not allowed by the assert above.
      // If quant.amount == 1, then fee.amount == 1,
      // otherwise if quant.amount > 1, then 0 < fee.amount < quant.amount.
      auto quant_after_fee = quant;
      quant_after_fee.amount -= fee.amount;
      // quant_after_fee.amount should be > 0 if quant.amount > 1.
      // If quant.amount == 1, then quant_after_近来ram经历了大涨大跌,ram的核心是bancor算法,网上有很多对bancor的介绍,这里就不谈了,eos少数派报告的星主Anima对此有深刻的分析。本文直接从源码解析eos的bancor算法。
最开始,,,我新建了一条链,对,是一条新链。然后在完整的64G内存上购买了10000k 内存。下面的命令是新建一个账户并购买10000k的内存。
!!!!直接要结果的,跳过源码,后文自取,不谢不谢!!!!


cleos system newaccount eosio --transfer accountnum11 EOS8mUftJXepGzdQ2TaCduNuSPAfXJHf22uex4u41ab1EVv9EAhWt --stake-net "100000.0000 SYS" --stake-cpu "100000.0000 SYS" --buy-ram-kbytes 10000

在eos的内部,这条命令触发的函数是这样的:

//payer:eosio receiver:accountnum11 bytes:10000
   void system_contract::buyrambytes( account_name payer, account_name receiver, uint32_t bytes ) {
      auto itr = _rammarket.find(S(4,RAMCORE));
      auto tmp = *itr;
      auto eosout = tmp.convert( asset(bytes,S(0,RAM)), CORE_SYMBOL );

      buyram( payer, receiver, eosout );
   }

先调用convert,convert涉及到很多函数,这是bancor真正实现,一并列在下面,我加了打印,后面会放打印结果。。。。

namespace eosiosystem {
   asset exchange_state::convert_to_exchange( connector& c, asset in ) {

      real_type R(supply.amount);
      real_type C(c.balance.amount+in.amount);
      real_type F(c.weight/1000.0);
      real_type T(in.amount);
      real_type ONE(1.0);
       print("\nconvert_to_exchange \n");
       print( "supply.amount ", supply.amount , "\n" );
       print( "c.balance.amount ", c.balance.amount, "\n" );
       print( "c.weight: ", c.weight, "\n" );
       print( "in.amount: ", in.amount, "\n\n" );

      real_type E = -R * (ONE - std::pow( ONE + T / C, F) );
      print( "E: ", E, "\n");
      int64_t issued = int64_t(E);

      supply.amount += issued;
      c.balance.amount += in.amount;

      return asset( issued, supply.symbol );
   }

   asset exchange_state::convert_from_exchange( connector& c, asset in ) {
      eosio_assert( in.symbol== supply.symbol, "unexpected asset symbol input" );

      real_type R(supply.amount - in.amount);
      real_type C(c.balance.amount);
      real_type F(1000.0/c.weight);
      real_type E(in.amount);
      real_type ONE(1.0);

       print("\nconvert_from_exchange \n");
       print( "supply.amount ", supply.amount , "\n" );
       print( "c.balance.amount ", c.balance.amount, "\n" );
       print( "c.weight: ", c.weight, "\n" );
       print( "in.amount: ", in.amount, "\n\n" );

     // potentially more accurate: 
     // The functions std::expm1 and std::log1p are useful for financial calculations, for example, 
     // when calculating small daily interest rates: (1+x)n
     // -1 can be expressed as std::expm1(n * std::log1p(x)). 
     // real_type T = C * std::expm1( F * std::log1p(E/R) );
      
      real_type T = C * (std::pow( ONE + E/R, F) - ONE);
      print( "T: ", T, "\n");
      int64_t out = int64_t(T);

      supply.amount -= in.amount;
      c.balance.amount -= out;

      return asset( out, c.balance.symbol );
   }

   asset exchange_state::convert( asset from, symbol_type to ) {
      auto sell_symbol  = from.symbol;
      auto ex_symbol    = supply.symbol;
      auto base_symbol  = base.balance.symbol;
      auto quote_symbol = quote.balance.symbol;

      print( "From: ", from, " TO ", asset( 0,to), "\n" );
      print( "base: ", base_symbol, "\n" );
      print("base_amount: ", base.balance.amount , "\n");
      print( "quote: ", quote_symbol, "\n" );
       print( "quote_amount: ", quote.balance.amount, "\n" );
      print( "ex: ", supply.symbol, "\n" );
       print( "ex_amuont: ", supply.amount, "\n" );


      if( sell_symbol != ex_symbol ) {
         if( sell_symbol == base_symbol ) {
            from = convert_to_exchange( base, from );
         } else if( sell_symbol == quote_symbol ) {
            from = convert_to_exchange( quote, from );
         } else { 
            eosio_assert( false, "invalid sell" );
         }
      } else {
         if( to == base_symbol ) {
            from = convert_from_exchange( base, from ); 
         } else if( to == quote_symbol ) {
            from = convert_from_exchange( quote, from ); 
         } else {
            eosio_assert( false, "invalid conversion" );
         }
      }

      if( to != from.symbol )
         return convert( from, to );

      return from;
   }
} /// namespace eosiosystem

然后调用buyram

  void system_contract::buyram( account_name payer, account_name receiver, asset quant )
   {
      require_auth( payer );
      eosio_assert( quant.amount > 0, "must purchase a positive amount" );

      auto fee = quant;
      fee.amount = ( fee.amount + 199 ) / 200; /// .5% fee (round up)
      // fee.amount cannot be 0 since that is onfee.amount == 0 and the next inline transfer will fail causing the buyram action to fail.

      INLINE_ACTION_SENDER(eosio::token, transfer)( N(eosio.token), {payer,N(active)},
         { payer, N(eosio.ram), quant_after_fee, std::string("buy ram") } );

      if( fee.amount > 0 ) {
         INLINE_ACTION_SENDER(eosio::token, transfer)( N(eosio.token), {payer,N(active)},
                                                       { payer, N(eosio.ramfee), fee, std::string("ram fee") } );
      }

      int64_t bytes_out;

      const auto& market = _rammarket.get(S(4,RAMCORE), "ram market does not exist");
      _rammarket.modify( market, 0, [&]( auto& es ) {
          bytes_out = es.convert( quant_after_fee,  S(0,RAM) ).amount;
      });

      eosio_assert( bytes_out > 0, "must reserve a positive amount" );

      _gstate.total_ram_bytes_reserved += uint64_t(bytes_out);
      _gstate.total_ram_stake          += quant_after_fee.amount;

      user_resources_table  userres( _self, receiver );
      auto res_itr = userres.find( receiver );
      if( res_itr ==  userres.end() ) {
         res_itr = userres.emplace( receiver, [&]( auto& res ) {
               res.owner = receiver;
               res.ram_bytes = bytes_out;
            });
      } else {
         userres.modify( res_itr, receiver, [&]( auto& res ) {
               res.ram_bytes += bytes_out;
            });
      }
      set_resource_limits( res_itr->owner, res_itr->ram_bytes, res_itr->net_weight.amount, res_itr->cpu_weight.amount );
   }

直接,打印输出:

1786293ms thread-0   apply_context.cpp:28          print_debug          ] 
[(eosio,buyrambytes)->eosio]: CONSOLE OUTPUT BEGIN =====================
From: 10240000. RAM TO 0.0000 SYS
base: 0,RAM
quote: 4,SYS
ex: 4,RAMCORE

convert_to_exchange 
supply.amount 100000000000000
c.balance.amount 68719476736
c.weight: 5.000000000000000e-01
in.amount: 10240000
E: 7.448915928520706e+06

From: 744.8915 RAMCORE TO 0.0000 SYS
base: 0,RAM
quote: 4,SYS
ex: 4,RAMCORE

convert_from_exchange 
supply.amount 100000007448915
c.balance.amount 100 0000 0000
c.weight: 5.000000000000000e-01
in.amount: 7448915
T: 1.489893921873264e+06

From: 148.2443 SYS TO 0. RAM
base: 0,RAM
quote: 4,SYS
ex: 4,RAMCORE

convert_to_exchange 
supply.amount 100 0000 0000 0000
c.balance.amount 100 0000 , 0000
c.weight: 5.000000000000000e-01
in.amount: 1482443
E: 7.410567426369141e+06


From: 741.0567 RAMCORE TO 0. RAM
base: 0,RAM
quote: 4,SYS
ex: 4,RAMCORE

convert_from_exchange 
supply.amount 100000007410567
c.balance.amount 68719476736
c.weight: 5.000000000000000e-01
in.amount: 7410567
T: 1.018576016383362e+07


[(eosio,buyrambytes)->eosio]: CONSOLE OUTPUT END   =====================

这三个在后文中常见:
1, base:RAM
2, quote:SYS
3, ex:RAMCORE

我们根据结果倒推算法。
第1步,调用conver_to_exchange将RAM单位换算成RAMCORE单位

CodeCogsEqn.gif

对应变量:

supply.amoumt  : 10^14
base.balance.amount  :   68719476736  即64G
in.amount   :   10240000
结果 E = 74485915
supply.amount += 74485915
base.balance.amount += 10240000

第2步,conver_frome_exchange函数将RAMCORE换算成SYS

CodeCogsEqn (1).gif

对应变量:

//这是ram的初始资金池容量,100万,之所以是10次方,是因为在eos内部,为取整方便,SYS的单位是“分”,不是“元”。 
quote.balance.amount  :  10^10   
supply.amount -= 7448915
quote.balance.amount -= 1489893
结果T=1489893

上面的分析是从ram-->sys ,意思是要购买10000k的ram,大概需要1489893分钱的SYS即149个EOS。但是像“supply.amount -= ” “quote.balance.amount -=”并不会对supply做真正的修改,只是修改的镜像。在调用buyram时使用“_rammarket.modify”才能修改系统的amount。

以下的两步是在buyram函数中进行的。
下面是大家比较关心的手续费问题:之所以要加上199,是要确保1分钱的交易也是需要手续费的,真是雁过拔毛呀!!!除以200 就是乘以0.005。没毛病。。。话说我算术不好。。。1/200 = 0.005 。

          fee.amount = ( fee.amount + 199 ) / 200;

第3步,将SYS转换成RAMCORE,调用conver_to_exchange函数:


CodeCogsEqn (2).gif

对应变量:

supply.amount : 10^14
in.amount  : 1482443
quote.balance.amount  : 10^10
结果E = 7410567

第4步,调用conver_from_exchange 将RAMCORE换算成RAM


CodeCogsEqn (3).gif

对应变量:

base.banalce.amount : 64G
base.banalce.amount -= 10185760

所以,最后,我们想买10240000byte的ram,结果只买了10185760byte的ram,其余的,扣了手续费。
实际上,卖出ram时,过程和前两步一样,不过在最后收取0.005的手续费。
将上文中,前两步的公式整合,化简,可以发现:

image.png

后两步的公式整合,化简可得到:

image.png

所以,

当前ram的价格 ≈ 当前的剩余容量 / 池子中的EOS总量

呵呵!并没有什么0.5/1000之类的幂,也没了什么RAMCORE。
不知道为啥BM一定要两步转换,强行引入RAMCORE,难道是为了确保RAMCORE的总量不变吗?希望知道的童靴答疑。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容