bitcoin mempool 杂记

新构建的项目

新项目的构建文件:txmempool.go; txentry.go

已经完成的方法:

mempool :

AddTx(); 添加交易;此时交易都已检查完。此处按照现阶段的设计,OK。
RemoveForBlock() 从矿池中移走块中的所有交易;1.先移除块包含的在交易池中的交易,2.再移除交易池中,与该交易冲突的交易(即该交易已经使用了那个引用输入); OK
Expire() 限制交易池大小,移除过期的交易; OK
RemoveForReorg() 重组链时,对交易池进行修改。注意:此处需要修改,现在是放在blockchain/validation.go 这个文件中。这块涉及到循环引用; OK。
TrimToSize() 限制交易池大小,移除多余的交易; 按照最新设计,删除交易费率最低的分支。 OK。
Check() 检查交易池中的所有交易,如果与交易池的状态不符,或该交易检查通不过,直接断言退出。OK。

内部方法

CalculateDescendants():计算一个交易在交易池的所有后代交易。OK
CalculateMemPoolAncestors() 此处OK,与Core一样。OK
CalculateMemPoolAncestors(): 计算一个交易在交易池的所有祖先交易。OK。
RemoveStaged():删除交易(一个或多个)。OK
removeConflicts():从交易池中删除交易,并递归删除它所有的后代交易。OK
removeRecursive():递归删除交易与它的所有后代交易。OK

TxEntry 结构

type TxEntry struct {
    tx       *core.Tx   //存储的交易
    txHeight int        //认为这个字段应该删除,因为进入交易池的交易应该是无高度的。
    txSize   int        //存储交易本身的大小
    txFee     int64     //该交易的交易费
    sumTxCountWithDescendants uint64    //该交易所有的后代交易数量
    sumFeeWithDescendants int64     //该交易所有后代交易的总交易费
    sumSizeWithDescendants uint64   //该交易所有后代交易的总字节大小
    
    //Note:此时可以依据所有后代的交易费和交易字节数,算出后代交易的平均费率。此时有两种挖矿策略,挖平均费率高的交易链,或挖总交易费高的交易链。

    sumTxCountWithAncestors    uint64   //该交易所有祖先交易的数量
    sumSizeWitAncestors        uint64   //该交易所有祖先交易的字节大小
    sumSigOpCountWithAncestors uint64   //该交易所有祖先交易的签名操作码个数。
    sumFeeWithAncestors        int64
    // Total sigop plus P2SH sigops count
    sigOpCount uint64
    time int64      //交易进交易池的本地时间,当内存池过大时,用来移除一些陈旧的交易
    usageSize int       //一个交易在交易池中的内存使用量
    
    childTx map[*TxEntry]struct{}   //这个交易的所有子交易
    parentTx map[*TxEntry]struct{}  //这个交易的所有父交易
    
    lp core.LockPoints              //该交易的检测点
    spendsCoinbase bool             //该交易是否花费了coinbase交易
}

上述结构是对交易池中每个交易的描述。一个交易进交易后,会先创建一个上述对象,然后将该对象存储在交易池中,该对象中的其他属性,主要用来进行检测交易进入交易池的限制,挖矿的优先设置。

TxMempool 结构

type TxMempool struct {
    sync.RWMutex        //交易池中的内部资源锁
    fee utils.FeeRate   //当前交易池的最低费率;可能此处需要修该,认为应该改成
    PoolData        map[utils.Hash]*TxEntry     //交易池中所有的交易都存储在该结构中,以key: txID; value: *TxEntry(即一个交易在交易池的对象).
    NextTx          map[core.OutPoint]*TxEntry  //key : 交易池中每个交易花费的引用输出; value : 花费该引用输出的交易(即当前在内存池中的交易)。
    timeSortData    btree.BTree     //交易池中的交易按时间排序后的集合。主要用来在交易池过大时,删除太陈旧的交易时使用。
    cacheInnerUsage int64       //该交易的内存使用量
    checkFrequency  float64     //交易池进行检查的时间间隔,用来执行定时任务。
    transactionsUpdated uint64
    totalTxSize     uint64
}

需要重新梳理的

  1. 有关交易祖先的所有状态都需要进行梳理。
  2. 有关交易祖先和后代的状态的使用 场景进行重新梳理
  3. 确定挖矿的策略
  4. 进行最后的确认

共识

coinbase交易的共识(不依赖上下文的检查)

// 检查coinbase交易
bool CheckCoinbase(const CTransaction &tx, CValidationState &state, bool fCheckDuplicateInputs) {
    //先检查是否为coinbase交易
    if (!tx.IsCoinBase()) {
        return state.DoS(100, false, REJECT_INVALID, "bad-cb-missing", false, "first tx is not coinbase");
    }
    
    //2. 检查交易的通用部分
    if (!CheckTransactionCommon(tx, state, fCheckDuplicateInputs)) {
        return false;
    }
    
    //3. 检查交易输入脚本的字节数,合理范围在[2, 100]
    if (tx.vin[0].scriptSig.size() < 2 || tx.vin[0].scriptSig.size() > 100) {
        return state.DoS(100, false, REJECT_INVALID, "bad-cb-length");
    }
    return true;
}

该方法用来检测一个coinbase交易是否符合共识,此处只检查交易的本身数据内容,不依赖于上下文。

  • 非coinbase交易的情况,直接报错。
  • 进行交易部分的通用检查
  • 对coinbase交易的输入字段脚本字节数进行检查,必须在[2, 100]范围内。

普通交易的共识(不依赖上下文的检查)

//检查普通的的交易;
bool CheckRegularTransaction(const CTransaction &tx, CValidationState &state, bool fCheckDuplicateInputs) {
    //1. 必须为非coinbase交易
    if (tx.IsCoinBase()) {
        return state.DoS(100, false, REJECT_INVALID, "bad-tx-coinbase");
    }
    
    //2. 检查交易的通用部分
    if (!CheckTransactionCommon(tx, state, fCheckDuplicateInputs)) {
        // CheckTransactionCommon fill in the state.
        return false;
    }
    
    //3. 检查交易的所有引用输出,必须合格。
    for (const auto &txin : tx.vin) {
        if (txin.prevout.IsNull()) {
            return state.DoS(10, false, REJECT_INVALID,
                             "bad-txns-prevout-null");
        }
    }

    return true;
}

该方法用来检测一个普通交易是否符合共识,此处只检查交易的本身数据内容,不依赖于上下文。

  • 如果是coinbase交易,直接报错。
  • 进行交易的通用部分检查
  • 对交易输入部分,进行合格性检查,不合理的引用输出报错退出。

检查交易的通用部分(不依赖上下文的检查)

//检查所有交易的通用部分
static bool CheckTransactionCommon(const CTransaction &tx, CValidationState &state, bool fCheckDuplicateInputs) {
    //1. 检查交易输入和输出数量,都不许为空。
    // Basic checks that don't depend on any context
    if (tx.vin.empty()) {
        return state.DoS(10, false, REJECT_INVALID, "bad-txns-vin-empty");
    }

    if (tx.vout.empty()) {
        return state.DoS(10, false, REJECT_INVALID, "bad-txns-vout-empty");
    }

    //2. 检查交易的字节限制,必须在最大范围内 MAX_TX_SIZE = ONE_MEGABYTE = 1000000Byte ≈ 1M 。
    // Size limit
    if (::GetSerializeSize(tx, SER_NETWORK, PROTOCOL_VERSION) > MAX_TX_SIZE) {
        return state.DoS(100, false, REJECT_INVALID, "bad-txns-oversize");
    }

    //3. 检查交易输入,输出金额。
    // Check for negative or overflow output values
    Amount nValueOut = 0;
    for (const auto &txout : tx.vout) {
        if (txout.nValue < 0) {
            return state.DoS(100, false, REJECT_INVALID,
                             "bad-txns-vout-negative");
        }

        if (txout.nValue > MAX_MONEY) {
            return state.DoS(100, false, REJECT_INVALID,
                             "bad-txns-vout-toolarge");
        }

        nValueOut += txout.nValue;
        if (!MoneyRange(nValueOut)) {
            return state.DoS(100, false, REJECT_INVALID,
                             "bad-txns-txouttotal-toolarge");
        }
    }
    
    //4. 检查交易的签名签名操作码数量,此处检查的是非精确P2SH脚本操作码数量。
    if (GetSigOpCountWithoutP2SH(tx) > MAX_TX_SIGOPS_COUNT) {
        return state.DoS(100, false, REJECT_INVALID, "bad-txn-sigops");
    }

    // Check for duplicate inputs - note that this check is slow so we skip it in CheckBlock
    // 检查重复交易输入
    if (fCheckDuplicateInputs) {
        std::set<COutPoint> vInOutPoints;
        for (const auto &txin : tx.vin) {
            if (!vInOutPoints.insert(txin.prevout).second) {
                return state.DoS(100, false, REJECT_INVALID,
                                 "bad-txns-inputs-duplicate");
            }
        }
    }

    return true;
}

  • 检查交易的输入和输出数量,必须为非空。
  • 检查交易的字节数,必须在最大范围内MAX_TX_SIZE ≈ 1M。
  • 检查交易的输入,输出金额,必须在合理范围内,MAX_MONEY=比特币的总数。
  • 检查非精确P2SH 签名操作码数量必须在合理范围内。MAX_TX_SIGOPS_COUNT=20000
  • 检查本交易的所有引用输入,是否多次过引用同一个输出,如果存在,直接报错。(因为一笔交易输出只可以花费一次)

什么是OP_RETURN?

OP_RETURN的中文名字叫“数据输出操作符”,这是比特币交易数据结构里输出部分的一个字段。字段就是比特币交易数据结构中的一部分。OP_RETURN这一部分是可选的,你可以啥都不写。

比特币交易输出一般是一定数量的UTXO,就是未花费输出,这就是比特币本身。但OP_RETURN输出不是UTXO,这里面可以是别的数据。

比特币主要运用是支付领域,但使用OP_RETURN的数据存储功能,就可以让比特币运用领域大大超越支付领域。

和OP_RETURN相似的是Coinbase交易的输入字段可以写任意的信息,比如矿工就会利用Coinbase交易的输入字段做投票支持哪个版本的软件。Coinbase交易的输入字段还被矿工拿来做刻字服务,号称永不可删除的留言,就有很多人用它来给别人发狗粮。OP_RETURN是只要发交易的人就可以留言,而Coinbase交易只能是矿工才能写入留言。

OP_RETURN现在可用的空间只有80字节,也就只够20多个汉字,Coinbase交易的输入字段有100字节的空间。这一次BCH硬分叉要把OP_RETURN的字节空间扩大到220字节。

OP_RETURN一开始是80字节,后来被Core开发组缩小到40字节,但在2015年Core开发组又恢复到了80字节。现在BCH要提升到220字节。

数据存储

DRAM:
SRAM:宝存 使用的方式。FPGA 做主控。FPGA烧在芯片上,半固化。 ASIC 全固化。
NAND颗粒; SSD硬盘的颗粒。

open channel

检查一个交易的sequence;判断一个交易是否成熟,可以被打包。

//检查交易的时间锁;通过交易输入的sequence字段
//tx(in):检查的交易; flags(in):检查该交易的flag; lp(out):为该交易创建锁定点;
//useExistingLockPoints(in): 默认为false,该值标识,参三是否为已含有数据的 LockPoints.
bool CheckSequenceLocks(const CTransaction &tx, int flags, LockPoints *lp, bool useExistingLockPoints) {
    AssertLockHeld(cs_main);
    AssertLockHeld(mempool.cs);

    //1. 获取当前链的高度;
    CBlockIndex *tip = chainActive.Tip();
    // 创建当前链的下一个块的索引;并设置它的属性;父索引和高度; 假设当前交易会在这个区块中被打包
    CBlockIndex index;
    index.pprev = tip;
    index.nHeight = tip->nHeight + 1;

    std::pair<int, int64_t> lockPair;
    //2. 如果参三已含有数据,为true; 默认false,即参三无数据。
    if (useExistingLockPoints) {
        assert(lp);
        lockPair.first = lp->height;
        lockPair.second = lp->time;
    } else {
        //3. 未含有数据,需要给参三加入数据。
        // 创建一个包含 UTXO集合和mempool的 视角
        CCoinsViewMemPool viewMemPool(pcoinsTip, mempool);

        std::vector<int> prevheights;       //这个变量存储该交易的引用输出的交易的高度(即它所花费的UTXO的高度);
        // 这个UTXO可以是UTXO集合中的,也可以是mempool中未打包的交易,(注意:所有mempool中的未打包的交易充当UTXO时,高度都设置为 MEMPOOL_HEIGHT)
        prevheights.resize(tx.vin.size());
        //  遍历该交易的所有交易输入
        for (size_t txinIndex = 0; txinIndex < tx.vin.size(); txinIndex++) {

            const CTxIn &txin = tx.vin[txinIndex];
            Coin coin;
            // 在创建的视角中查找 该引用输出的UTXO。
            if (!viewMemPool.GetCoin(txin.prevout, coin)) {
                return error("%s: Missing input", __func__);
            }
            // 如果查找的UTXO是在 交易池中,即该交易依赖于一个还未打包的交易; 所有交易池的UTXO的高度都为  MEMPOOL_HEIGHT。
            if (coin.GetHeight() == MEMPOOL_HEIGHT) {
                // Assume all mempool transaction confirm in the next block;
                // 假设mempool中的所有交易都会在下一个块中被打包。
                prevheights[txinIndex] = tip->nHeight + 1;
            } else {
                // 该UTXO存在于UTXO集合中;则获取该UTXO的高度
                prevheights[txinIndex] = coin.GetHeight();
            }
        }
        // 计算 该交易的锁定时间戳(包含高度和时间)
        lockPair = CalculateSequenceLocks(tx, flags, &prevheights, index);
        if (lp) {
            lp->height = lockPair.first;
            lp->time = lockPair.second;
           
            // 查找最大的 引用交易输入的 高度;
            int maxInputHeight = 0;

            for (int height : prevheights) {
                // Can ignore mempool inputs since we'll fail if they had
                // non-zero locks
                if (height != tip->nHeight + 1) {
                    maxInputHeight = std::max(maxInputHeight, height);
                }
            }
            // 获取该交易的最大交易输入 块索引。
            lp->maxInputBlock = tip->GetAncestor(maxInputHeight);
        }
    }
    // 执行获取该交易的时间戳。看该交易是否可以被打包。
    return EvaluateSequenceLocks(index, lockPair);
}

static bool EvaluateSequenceLocks(const CBlockIndex &block, std::pair<int, int64_t> lockPair) {
    assert(block.pprev);
    // 获取这个块父区块的中值时间
    int64_t nBlockTime = block.pprev->GetMedianTimePast();
    // 判断这个交易的锁定时间点是否可以在这个块中被打包。
    if (lockPair.first >= block.nHeight || lockPair.second >= nBlockTime)
        return false;

    return true;
}

// * 计算传入的交易 被打包时需要到达的 时间,高度。(因为一个交易可以有多个交易输入,每个交易输入含有不同的锁定条件,高度/时间, 所以该锁定点记录的是该交易的最迟打包时间)
static std::pair<int, int64_t> CalculateSequenceLocks(const CTransaction &tx, int flags, std::vector<int> *prevHeights, const CBlockIndex &block) {

    //1. 必须与交易输入的数量相同。
    assert(prevHeights->size() == tx.vin.size());
    int nMinHeight = -1;
    int64_t nMinTime = -1;

    //2. 判断该交易是否支持 BIP68; 版本2 号以后的交易,且flag设置了LOCKTIME_VERIFY_SEQUENCE 都支持BIP68。
    bool fEnforceBIP68 = static_cast<uint32_t>(tx.nVersion) >= 2 &&( flags & LOCKTIME_VERIFY_SEQUENCE != 0);

    // Do not enforce sequence numbers as a relative lock time
    // unless we have been instructed to;
    // 不支持BIP68,直接返回-1.
    if (!fEnforceBIP68) {
        return std::make_pair(nMinHeight, nMinTime);
    }

    //3. 遍历该交易的所有交易输入
    for (size_t txinIndex = 0; txinIndex < tx.vin.size(); txinIndex++) {
        const CTxIn &txin = tx.vin[txinIndex];
        // 如果交易输入的sequence字段 设置了SEQUENCE_LOCKTIME_DISABLE_FLAG,
        // 标识这个字段不表示锁定时间戳含义(详细信息:查看BIP68),所以此处直接返回。
        if (txin.nSequence & CTxIn::SEQUENCE_LOCKTIME_DISABLE_FLAG) {
            // 同时,该交易输入所对应的高度设置为0,标识这个输入可以立即被花费。
            (*prevHeights)[txinIndex] = 0;
            continue;
        }

        //获取这个交易输入的高度;
        int nCoinHeight = (*prevHeights)[txinIndex];
        // 这个flag被设置,标识sequence字段是以时间,或高度进行锁定的。
        // 即这个交易需要到指定时间 或 块后,才可以被打包。
        if (txin.nSequence & CTxIn::SEQUENCE_LOCKTIME_TYPE_FLAG) {
            //查找引用交易的 父块高度的中位数时间。
            int64_t nCoinTime = block.GetAncestor(std::max(nCoinHeight - 1, 0))->GetMedianTimePast();
            // 计算这个最低的时间
            nMinTime = std::max(
                nMinTime, nCoinTime + (int64_t)((txin.nSequence & CTxIn::SEQUENCE_LOCKTIME_MASK)
 << CTxIn::SEQUENCE_LOCKTIME_GRANULARITY) -  1);   //获取锁定的时间
        } else {
            //标识sequence字段是以高度作为锁定的
            nMinHeight = std::max(
                nMinHeight, nCoinHeight + (int)(txin.nSequence & CTxIn::SEQUENCE_LOCKTIME_MASK) - 1); //获取锁定的高度
        }
    }

    // 返回求得的该交易的所有的交易输入中最迟的可以花费的时间 或 高度。
    // (即什么时候这个交易此时才可以打包进区块) 因为一个交易可能含有多个交易输入,
    // 每个交易输入可能采用了不同的锁定方式(时间和高度),所以返回这个交易中所有交易输入 标识的锁定最大值。
    // 只有最大值检查通过后(高度和时间),就表示这个交易都可以被打包了。
    return std::make_pair(nMinHeight, nMinTime);
}

CheckSequenceLocks() 获取一个交易的最晚锁定点(即到哪个块,时间点才可以被打包),并检查下一个块是否符合这个锁定点,是否可以打包在下个块中。

  • Note : 传入该方法的交易都是待确认的交易.
  • 先虚构下个要挖的块,并假设这个交易要被打包在这个块中。
  • 如果已传入该交易的检查点,直接使用这个检查点,否则先计算这个交易的检查点。同时将该交易最高的引用输入赋值给该检查点(此处没有用)。
  • 然后对拿到的检查点进行检测,看该交易是否成熟,可以在这个区块中被打包。

CalculateSequenceLocks() 计算一个交易的锁定点;

  • flags(in):交易检查的标识;
  • prevHeights(in/out):这个交易的所有引用输出所在的高度, 当这个交易输入的sequence字段不表示相对锁定时间时,将指定的交易输入的高度设为0。
  • block(in):这个交易将在这个块中被打包(假设)。
    1. 先查看该交易是否支持BIP68,不符合返回锁定点,退出。
  1. 遍历该交易的所有交易输入,检查它的sequence字段,根据sequence所表示的含义(依据时间锁定,还是依据块高度锁定),获取所有交易输入中最大的锁定值,赋值给锁定点。

EvaluateSequenceLocks(): 依据一个块,对锁定点进行检测,看该交易是否可以打包进这个区块中。

  • 先获取这个块父区块的MTP时间。Note : 此处是该块的父区块(因为这个块一般是假设的,还没有打包)
  • 当这个交易的锁定点比这个区块大时,交易不可以被打包进这个区块中,返回false。

检查交易的时间戳,看是否为可以立即打包的交易

//检查是否为可以立即打包的交易;
// nBlockHeight(in): 当前主链的要挖的区块高度; nBlockTime(in): 标识当前主链区块的最新打包时间。
static bool IsFinalTx(const CTransaction &tx, int nBlockHeight,
                      int64_t nBlockTime) {
    //1. 交易时间戳等于0, 标识该交易可以立即打包
    if (tx.nLockTime == 0) {
        return true;
    }

    int64_t lockTime = tx.nLockTime;
    //2. 获取该交易时间戳字段,最终标识的含义,高度/时间。交易小于参数限制,标识可以立即打包。
    int64_t lockTimeLimit =
        (lockTime < LOCKTIME_THRESHOLD) ? nBlockHeight : nBlockTime;
    if (lockTime < lockTimeLimit) {
        return true;
    }

    //3. 时间戳不为0, 但是如果交易输入的所有sequence字段都为最大值,这个交易也可以立即打包。
    for (const auto &txin : tx.vin) {
        if (txin.nSequence != CTxIn::SEQUENCE_FINAL) {
            return false;
        }
    }
    return true;
}

从上述代码可以看出:

  • 当一个交易的时间戳字段为0时,改交易可以立即被打包,是个成熟的交易
  • 或者该交易的时间戳小于限制,标识该交易成熟,可以被打包。
  • 如果该交易在时间戳字段不成熟的情况下,但是它所有的交易输入的sequence字段都为默认最大值,该交易可以立即被打包,视为成熟交易 。

bitcoin-abc
创建块时的设计:

  1. 逐个交易进行遍历;
  2. 如果这些交易是否已被打包到块中,是否已在缓冲集合中,是否已在失败集合中,如果存在,继续遍历下一个交易。
  3. 判断这个交易是来自

UpdatePackagesForAdded()

  1. 查找已添加到块中的交易的所有后代交易,并且将这些后代交易不在块中的交易添加进 传出集合中。
  2. 遍历这些后代交易,如果交易已被添加到块中,则跳过该交易,继续下一次循环。否则,查看该交易是否在传出集合中,如果不在,将它添加到传出集合中。

本文由 Copernicus团队 姚永芯写作,转载无需授权。

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

推荐阅读更多精彩内容

  • 一、快速术语检索 比特币地址:(例如:1DSrfJdB2AnWaFNgSbv3MZC2m74996JafV)由一串...
    不如假如阅读 15,917评论 4 87
  • 作者 Kevin 简介:“比特币技术进阶”由知名比特币技术专家Kevin原创的三篇文章《比特币交易构成》、《时间...
    西部之歌阅读 995评论 0 0
  • 钱包、密钥 私钥衍生家族----适用于各种树形权限分配,比如企业钱包 交易 交易费按交易数据字节大小计算,交易费市...
    观星客阅读 980评论 0 2
  • 随着中国经济发展,中国从能源出口国变成了能源进口国,对外依存度越来越高,原油依存度从2005年的39.5%上升到了...
    蝽笙阅读 306评论 0 0
  • 愁思演笑嘻 孤母苦无依 两望天涯畔 归心伴欲啼
    蓝色汪星人阅读 174评论 2 5