Btcd區(qū)塊鏈的構(gòu)建(一)

我們在介紹Btcd協(xié)議消息時(shí)提到,協(xié)議設(shè)計(jì)的目標(biāo)就是同步transaction或者block,最終在各節(jié)點(diǎn)上形成一致的區(qū)塊鏈。本文開始,我們將逐步分析節(jié)點(diǎn)在收到transaction或者block后如何處理它們,如如何將transaction打包成區(qū)塊并挖礦、如何將block添加到區(qū)塊鏈中等問題。同步transaction后,“礦工”可以將交易打包進(jìn)新的區(qū)塊,并向網(wǎng)絡(luò)傳播新區(qū)塊。所以,對于獨(dú)立“挖礦”的節(jié)點(diǎn)或者通過“礦池”與“礦工”連接的“全節(jié)點(diǎn)”而言,它們有兩方面的區(qū)塊來源,即“挖礦”產(chǎn)生的新區(qū)塊和收到的其它節(jié)點(diǎn)轉(zhuǎn)發(fā)的區(qū)塊,如下圖所示。

同步其它節(jié)點(diǎn)轉(zhuǎn)發(fā)的區(qū)塊,就是通過協(xié)議消息inv-getdata-block實(shí)現(xiàn)的,我們在《Btcd區(qū)塊鏈協(xié)議消息解析》中介紹過這一過程;“挖礦”的過程我們將在后文中詳細(xì)介紹。無論是“挖”出新區(qū)塊還是收到其他節(jié)點(diǎn)轉(zhuǎn)發(fā)的區(qū)塊,都需要經(jīng)過一系列驗(yàn)證后,再加入到區(qū)塊鏈上;同時(shí),當(dāng)區(qū)塊鏈的共識機(jī)制需要作微調(diào),即發(fā)生“軟分叉”時(shí),節(jié)點(diǎn)之間也需要一個(gè)共識機(jī)制來支持新的區(qū)塊或者兼容老的區(qū)塊,這些過程或者機(jī)制的實(shí)現(xiàn)位于btcd/blockchain包中。我們將重點(diǎn)介紹btcd/blockchain包,其中的文件有:

  • fullblocktests文件夾: 包含自動(dòng)生成測試用例的實(shí)現(xiàn)類,用于測試區(qū)塊的共識參數(shù);
  • indexers文件夾: 實(shí)現(xiàn)了transaction到block之間和transaction到關(guān)聯(lián)的bitcoin地址之間的索引關(guān)系;
  • testdata文件夾: 包含用于測試的block數(shù)據(jù);
  • process.go: ProcessBlock()方法所在的文件,它是btcd/blockchain對區(qū)塊進(jìn)行處理的入口方法,也是我們重點(diǎn)分析對象;
  • accept.go: maybeAcceptBlock()方法所在的文件,它是區(qū)塊被鏈接到區(qū)塊鏈上的入口方法;
  • validate.go: 實(shí)現(xiàn)了驗(yàn)證transaction和block的各種方法,如CheckBlockSanity()、CheckProofOfWork、CheckTransactionSanity等等;
  • chain.go: 定義了BlockChain類型,包括了區(qū)塊加入?yún)^(qū)塊鏈,或者區(qū)塊鏈構(gòu)建的主要實(shí)現(xiàn)過程;
  • blockindex.go: 實(shí)現(xiàn)了blockIndex,用于維護(hù)blockchain上block與其hash之間的映射關(guān)系和父block與子block(s)的關(guān)系;
  • chainio.go: 提供了向db中讀寫區(qū)塊或者相關(guān)元數(shù)據(jù)(如ThresholdCaches、BlockIndex等等)的方法;
  • checkpoints.go: 實(shí)現(xiàn)了BlockChain中與Checkpoint相關(guān)的方法,如findPreviousCheckpoint()等等,BlockChain中的checkpoints是一系列指定的區(qū)塊,內(nèi)置在bitcoin節(jié)點(diǎn)中,用于驗(yàn)證區(qū)塊工作量;
  • compress.go: 實(shí)現(xiàn)了一種改進(jìn)的VLQ(Variable Length Quantity)正整數(shù)編碼方式,同時(shí)實(shí)現(xiàn)了對交易輸出額進(jìn)行壓縮編碼和輸出腳本(鎖定腳本)進(jìn)行壓縮編碼的算法,進(jìn)而實(shí)現(xiàn)了對交易輸出TxOut的壓縮;
  • difficulty.go: 提供了與區(qū)塊難度值相關(guān)的方法,如區(qū)塊頭里的難度值與整數(shù)值之間的轉(zhuǎn)換、通過難度值計(jì)算工作量和難度值調(diào)整算法等等;
  • mediatime.go: 定義了MedianTimeSource接口和其實(shí)現(xiàn)mediaTime類型,通過計(jì)算各時(shí)間源的中位值來較正當(dāng)前時(shí)間;
  • merkle.go: 實(shí)現(xiàn)了構(gòu)建Merkle樹的算法;
  • notifications.go: 定義了NotificationCallback和Notification,用于blockchain向其消費(fèi)者回調(diào)NTBlockAccepted、NTBlockConnected及NTBlockDisconnected事件;
  • scriptval.go: 實(shí)現(xiàn)了驗(yàn)證交易中的鎖定腳本與解鎖腳本的機(jī)制,支持并發(fā)地驗(yàn)證多個(gè)腳本對;
  • thresholdstate.go: 定義了新的共識規(guī)則在向區(qū)塊鏈網(wǎng)絡(luò)部署時(shí)的狀態(tài),按部署或者節(jié)點(diǎn)接受的進(jìn)度依次為Defined、Started、LockedIn、Active或者Failed等狀態(tài),更重要地,實(shí)現(xiàn)了狀態(tài)轉(zhuǎn)換的算法和計(jì)算區(qū)塊上某個(gè)共識規(guī)則的部署狀態(tài)的方法,我們將在后文中詳細(xì)介紹;
  • timersort.go: 實(shí)現(xiàn)了支持升序排序的時(shí)間戳集合類型timeSorter,timeSorter實(shí)際上是[]int64類型,這是Go語言特性的一個(gè)體現(xiàn),即通過類型定義可以擴(kuò)展任意類型,包括基礎(chǔ)類型的方法集;
  • versionbits.go: 實(shí)現(xiàn)了BIP9中描述的關(guān)于區(qū)塊版本號的定義,區(qū)塊版本號是一個(gè)32位且按小端模式存儲的整型值,以001開頭,其后的每一位代表一個(gè)BIP部署;同時(shí)也實(shí)現(xiàn)了根據(jù)部署狀態(tài)確定下一個(gè)區(qū)塊的目標(biāo)版本號的方法,用于設(shè)定新“挖”出區(qū)塊的版本號,我們將在后文中詳細(xì)分析;
  • utxoviewpoint.go: 維護(hù)了UTXO的集合,包括有新的transaction時(shí)向集合中添加新的UTXO或者移除已經(jīng)花費(fèi)的UTXO;
  • doc.go: 包blockchain的doc說明;
  • error.go: 定義了與構(gòu)建區(qū)塊鏈相關(guān)的error類型及對應(yīng)的字符串;
  • log.go: 提供了logger相關(guān)的方法;
  • xxx_test.go: 各文件對應(yīng)的測試文件;

ProcessBlock()是btcd/blockchain中開始對block進(jìn)行各種驗(yàn)證并接入?yún)^(qū)塊鏈的核心方法,從上圖中也可以看到,收到其它節(jié)點(diǎn)同步過來的區(qū)塊或者礦工“挖”出塊后均交由ProcessBlock處理。接下來,我們將從ProcessBlock()入手逐步分析區(qū)塊處理的各個(gè)步驟,為了便于后續(xù)分析,我們看介紹BlockChain的定義:

//btcd/blockchain/chain.go

// BlockChain provides functions for working with the bitcoin block chain.
// It includes functionality such as rejecting duplicate blocks, ensuring blocks
// follow all rules, orphan handling, checkpoint handling, and best chain
// selection with reorganization.
type BlockChain struct {
    // The following fields are set when the instance is created and can't
    // be changed afterwards, so there is no need to protect them with a
    // separate mutex.
    checkpoints         []chaincfg.Checkpoint
    checkpointsByHeight map[int32]*chaincfg.Checkpoint
    db                  database.DB
    chainParams         *chaincfg.Params
    timeSource          MedianTimeSource
    notifications       NotificationCallback
    sigCache            *txscript.SigCache
    indexManager        IndexManager

    // The following fields are calculated based upon the provided chain
    // parameters.  They are also set when the instance is created and
    // can't be changed afterwards, so there is no need to protect them with
    // a separate mutex.
    //
    // minMemoryNodes is the minimum number of consecutive nodes needed
    // in memory in order to perform all necessary validation.  It is used
    // to determine when it's safe to prune nodes from memory without
    // causing constant dynamic reloading.  This is typically the same value
    // as blocksPerRetarget, but it is separated here for tweakability and
    // testability.
    minRetargetTimespan int64 // target timespan / adjustment factor
    maxRetargetTimespan int64 // target timespan * adjustment factor
    blocksPerRetarget   int32 // target timespan / target time per block
    minMemoryNodes      int32

    // chainLock protects concurrent access to the vast majority of the
    // fields in this struct below this point.
    chainLock sync.RWMutex

    // These fields are configuration parameters that can be toggled at
    // runtime.  They are protected by the chain lock.
    noVerify bool

    // These fields are related to the memory block index.  The best node
    // is protected by the chain lock and the index has its own locks.
    bestNode *blockNode
    index    *blockIndex

    // These fields are related to handling of orphan blocks.  They are
    // protected by a combination of the chain lock and the orphan lock.
    orphanLock   sync.RWMutex
    orphans      map[chainhash.Hash]*orphanBlock
    prevOrphans  map[chainhash.Hash][]*orphanBlock
    oldestOrphan *orphanBlock

    // These fields are related to checkpoint handling.  They are protected
    // by the chain lock.
    nextCheckpoint  *chaincfg.Checkpoint
    checkpointBlock *btcutil.Block

    // The state is used as a fairly efficient way to cache information
    // about the current best chain state that is returned to callers when
    // requested.  It operates on the principle of MVCC such that any time a
    // new block becomes the best block, the state pointer is replaced with
    // a new struct and the old state is left untouched.  In this way,
    // multiple callers can be pointing to different best chain states.
    // This is acceptable for most callers because the state is only being
    // queried at a specific point in time.
    //
    // In addition, some of the fields are stored in the database so the
    // chain state can be quickly reconstructed on load.
    stateLock     sync.RWMutex
    stateSnapshot *BestState

    // The following caches are used to efficiently keep track of the
    // current deployment threshold state of each rule change deployment.
    //
    // This information is stored in the database so it can be quickly
    // reconstructed on load.
    //
    // warningCaches caches the current deployment threshold state for blocks
    // in each of the **possible** deployments.  This is used in order to
    // detect when new unrecognized rule changes are being voted on and/or
    // have been activated such as will be the case when older versions of
    // the software are being used
    //
    // deploymentCaches caches the current deployment threshold state for
    // blocks in each of the actively defined deployments.
    warningCaches    []thresholdStateCache
    deploymentCaches []thresholdStateCache

    // The following fields are used to determine if certain warnings have
    // already been shown.
    //
    // unknownRulesWarned refers to warnings due to unknown rules being
    // activated.
    //
    // unknownVersionsWarned refers to warnings due to unknown versions
    // being mined.
    unknownRulesWarned    bool
    unknownVersionsWarned bool
}

其中各個(gè)字段的意義如下:

  • checkpoints: 鏈上預(yù)設(shè)的一些checkpoints,節(jié)點(diǎn)啟動(dòng)時(shí)指定,實(shí)際由鏈上的某些高度上的區(qū)塊充當(dāng);
  • checkpointsByHeight: 記錄了checkpoints和對應(yīng)的區(qū)塊的高度之間的映射關(guān)系,初始化時(shí)由checkpoints解析而來;
  • db: 存儲區(qū)塊的database對象;
  • chainParams: 與區(qū)塊鏈相關(guān)的參數(shù)配置,如網(wǎng)絡(luò)號、創(chuàng)世區(qū)塊(Genesis Block)、難度調(diào)整周期、DNSSeeds等等;對這些參數(shù)進(jìn)行定制即可以生成一個(gè)與Bitcoin類似的新的"代幣";
  • timeSource: 用于較正節(jié)點(diǎn)時(shí)鐘的“時(shí)鐘源”,節(jié)點(diǎn)可以在與Peer進(jìn)行version消息交換的時(shí)候把Peer添加到自己的“時(shí)鐘源”中,這樣節(jié)點(diǎn)通過計(jì)算與不同Peer節(jié)點(diǎn)的時(shí)鐘差的中位值,來估計(jì)其與網(wǎng)絡(luò)同步時(shí)間的差,從而在利用timestamp進(jìn)行區(qū)塊較驗(yàn)之前先較正自己的時(shí)鐘,以防節(jié)點(diǎn)時(shí)鐘未同步造成較驗(yàn)失敗;
  • notifications: 向bockchain注冊的回調(diào)事件接收者;
  • sigCache: 用于緩存公鑰與簽名驗(yàn)證的結(jié)果,對于已經(jīng)通過解鎖腳本和鎖定腳本驗(yàn)證的交易,對應(yīng)的公鑰和簽名會被緩存,后續(xù)相同的交易驗(yàn)證時(shí)可以直接從緩存中讀到驗(yàn)證結(jié)果;
  • indexManager: 用于索引鏈上transaction或者block的indexer;
  • minRetargetTimespan: 難度調(diào)整的最小周期,公鏈上其值為3.5天;
  • maxRetargetTimespan: 難度調(diào)整的最大周期,公鏈上其值為56天;
  • blocksPerRetarget: 難度調(diào)整周期內(nèi)的區(qū)塊數(shù),公鏈上難度調(diào)整周期是14天,對應(yīng)的區(qū)塊數(shù)是2016個(gè);
  • minMemoryNodes: 該值暫未用到;
  • chainLock: 保護(hù)訪問區(qū)塊鏈的讀寫鎖;
  • noVerify: 是否關(guān)閉腳本驗(yàn)證的開關(guān);
  • bestNode: 指向主鏈上的“尾”節(jié)點(diǎn),即高度最高的區(qū)塊;
  • index: 指向一個(gè)區(qū)塊索引器,用于索引實(shí)例化后內(nèi)存中的各區(qū)塊;
  • orphanLock: 保護(hù)“孤兒區(qū)塊”相關(guān)對象的讀寫鎖;
  • orphans: 記錄“孤兒區(qū)塊”與其Hash之間的映射;
  • prevOrphans: 記錄“孤兒區(qū)塊”與其父區(qū)塊Hash之間的映射,當(dāng)父區(qū)塊寫入?yún)^(qū)塊鏈后,要檢索prevOrphans將對應(yīng)的區(qū)塊從“孤兒區(qū)塊池”從移除并寫入?yún)^(qū)塊鏈;
  • oldestOrphan: 處于“孤兒”狀態(tài)時(shí)間最長的區(qū)塊,“孤兒區(qū)塊池”最多維護(hù)100個(gè)“孤兒區(qū)塊”,當(dāng)超過限制時(shí),開始移除“最老”的“孤兒區(qū)塊”;
  • stateLock: 保護(hù)stateSnapshot的讀寫鎖;
  • stateSnapshot: 主鏈相關(guān)信息的快照;
  • warningCaches: 緩存區(qū)塊對于所有可能的部署的thresholdState,用于當(dāng)節(jié)點(diǎn)收到大量新的版本的區(qū)塊,且對應(yīng)的共識規(guī)則在新的區(qū)塊里已經(jīng)部署或者即將部署時(shí),發(fā)出警告提示,為了兼容新版本的區(qū)塊,可能需要升級btcd版本;
  • deploymentCaches: 緩存區(qū)塊對于已知的部署提案的thresholdState;
  • unknownRulesWarned: 標(biāo)識是否已經(jīng)警告過未知共識規(guī)則已經(jīng)部署或者將被部署;
  • unknownVersionsWarned: 標(biāo)識是否已經(jīng)警告過收到過多未知新版本的區(qū)塊;當(dāng)有新版本的區(qū)塊時(shí),往往有新的共識規(guī)則正在部署,所以警告未知新版本與未知共識規(guī)則部署是相關(guān)的;

在了解了BlockChain的定義后,我們開始從ProcessBlock()分析處理區(qū)塊的各個(gè)環(huán)節(jié):

//btcd/blockchain/process.go

// ProcessBlock is the main workhorse for handling insertion of new blocks into
// the block chain.  It includes functionality such as rejecting duplicate
// blocks, ensuring blocks follow all rules, orphan handling, and insertion into
// the block chain along with best chain selection and reorganization.
//
// When no errors occurred during processing, the first return value indicates
// whether or not the block is on the main chain and the second indicates
// whether or not the block is an orphan.
//
// This function is safe for concurrent access.
func (b *BlockChain) ProcessBlock(block *btcutil.Block, flags BehaviorFlags) (bool, bool, error) {
    b.chainLock.Lock()
    defer b.chainLock.Unlock()

    fastAdd := flags&BFFastAdd == BFFastAdd
    dryRun := flags&BFDryRun == BFDryRun

    blockHash := block.Hash()
    log.Tracef("Processing block %v", blockHash)

    // The block must not already exist in the main chain or side chains.
    exists, err := b.blockExists(blockHash)                                              (1)
    if err != nil {
        return false, false, err
    }
    if exists {
        str := fmt.Sprintf("already have block %v", blockHash)
        return false, false, ruleError(ErrDuplicateBlock, str)
    }

    // The block must not already exist as an orphan.
    if _, exists := b.orphans[*blockHash]; exists {                                      (2)
        str := fmt.Sprintf("already have block (orphan) %v", blockHash)
        return false, false, ruleError(ErrDuplicateBlock, str)
    }

    // Perform preliminary sanity checks on the block and its transactions.
    err = checkBlockSanity(block, b.chainParams.PowLimit, b.timeSource, flags)           (3)
    if err != nil {
        return false, false, err
    }

    // Find the previous checkpoint and perform some additional checks based
    // on the checkpoint.  This provides a few nice properties such as
    // preventing old side chain blocks before the last checkpoint,
    // rejecting easy to mine, but otherwise bogus, blocks that could be
    // used to eat memory, and ensuring expected (versus claimed) proof of
    // work requirements since the previous checkpoint are met.
    blockHeader := &block.MsgBlock().Header
    checkpointBlock, err := b.findPreviousCheckpoint()
    if err != nil {
        return false, false, err
    }
    if checkpointBlock != nil {
        // Ensure the block timestamp is after the checkpoint timestamp.
        checkpointHeader := &checkpointBlock.MsgBlock().Header
        checkpointTime := checkpointHeader.Timestamp
        if blockHeader.Timestamp.Before(checkpointTime) {                                (4)
            str := fmt.Sprintf("block %v has timestamp %v before "+
                "last checkpoint timestamp %v", blockHash,
                blockHeader.Timestamp, checkpointTime)
            return false, false, ruleError(ErrCheckpointTimeTooOld, str)
        }
        if !fastAdd {
            // Even though the checks prior to now have already ensured the
            // proof of work exceeds the claimed amount, the claimed amount
            // is a field in the block header which could be forged.  This
            // check ensures the proof of work is at least the minimum
            // expected based on elapsed time since the last checkpoint and
            // maximum adjustment allowed by the retarget rules.
            duration := blockHeader.Timestamp.Sub(checkpointTime)
            requiredTarget := CompactToBig(b.calcEasiestDifficulty(
                checkpointHeader.Bits, duration))
            currentTarget := CompactToBig(blockHeader.Bits)
            if currentTarget.Cmp(requiredTarget) > 0 {                                   (5)
                str := fmt.Sprintf("block target difficulty of %064x "+
                    "is too low when compared to the previous "+
                    "checkpoint", currentTarget)
                return false, false, ruleError(ErrDifficultyTooLow, str)
            }
        }
    }

    // Handle orphan blocks.
    prevHash := &blockHeader.PrevBlock
    prevHashExists, err := b.blockExists(prevHash)
    if err != nil {
        return false, false, err
    }
    if !prevHashExists {
        if !dryRun {
            log.Infof("Adding orphan block %v with parent %v",
                blockHash, prevHash)
            b.addOrphanBlock(block)                                                      (6)
        }

        return false, true, nil
    }

    // The block has passed all context independent checks and appears sane
    // enough to potentially accept it into the block chain.
    isMainChain, err := b.maybeAcceptBlock(block, flags)                                 (7)
    if err != nil {
        return false, false, err
    }

    // Don't process any orphans or log when the dry run flag is set.
    if !dryRun {
        // Accept any orphan blocks that depend on this block (they are
        // no longer orphans) and repeat for those accepted blocks until
        // there are no more.
        err := b.processOrphans(blockHash, flags)                                        (8)
        if err != nil {
            return false, false, err
        }

        log.Debugf("Accepted block %v", blockHash)
    }

    return isMainChain, false, nil
}

ProcessBlock()輸入的是指向btcutil.Block類型的block,它對wire.MsgBlock進(jìn)行了封裝,可以看作是訪問wire.MsgBlock的輔助類型,在btcd/blockchain中看到的block類型均是btcutil.Block類型,所以在解析代碼之前,我們先看一下它的定義:

//btcd/vendor/github.com/btcsuite/btcutil/block.go

// Block defines a bitcoin block that provides easier and more efficient
// manipulation of raw blocks.  It also memoizes hashes for the block and its
// transactions on their first access so subsequent accesses don't have to
// repeat the relatively expensive hashing operations.
type Block struct {
    msgBlock        *wire.MsgBlock  // Underlying MsgBlock
    serializedBlock []byte          // Serialized bytes for the block
    blockHash       *chainhash.Hash // Cached block hash
    blockHeight     int32           // Height in the main block chain
    transactions    []*Tx           // Transactions
    txnsGenerated   bool            // ALL wrapped transactions generated
}

ProcessBlock()輸出的第一個(gè)值表示區(qū)塊是否加入了主鏈,第二值表示區(qū)塊是否是“孤兒區(qū)塊”。其主要執(zhí)行步驟是:

  1. 首先檢查區(qū)塊是否已經(jīng)在鏈上,如代碼(1)處所示;
  2. 然后檢查區(qū)塊是否在“孤兒區(qū)塊池”中,如代碼(2)處所示;
  3. 代碼(3)處調(diào)用checkBlockSanity()對區(qū)塊結(jié)構(gòu)進(jìn)行檢查,包括對區(qū)塊頭,如工作量和Merkle樹根等等,和交易集合的檢查;
  4. 通過區(qū)塊結(jié)構(gòu)檢查后,根據(jù)最近的Checkpoint與區(qū)塊之間的時(shí)間差,計(jì)算預(yù)期的最小工作量,如果區(qū)塊的工作量低于預(yù)期的最小工作量則被拒絕,如代碼(5)所示; 這是通過Checkpoint機(jī)制防止偽造工作量證明的過程,需要注意的是,區(qū)塊頭中表示目標(biāo)難度的值越大,則表示工作量越小,反之,其值越小,則表示工作量越大;
  5. 通過Checkpoint檢查后,接著檢測區(qū)塊的父區(qū)塊是否在鏈上,如果不在鏈上,則將區(qū)塊加入“孤兒區(qū)塊池”,如代碼(6)處所示;
  6. 如果父區(qū)塊在鏈上,代碼(7)處調(diào)用maybeAcceptBlock()對區(qū)塊先進(jìn)行上下文檢查,如根據(jù)父區(qū)塊計(jì)算期望工作量、期望的timestamp范圍、區(qū)塊高度是否正確等等,然后根據(jù)父區(qū)塊是否在主鏈上決定是擴(kuò)展主鏈還是側(cè)鏈,并進(jìn)一步對區(qū)塊中的交易進(jìn)行驗(yàn)證;交易驗(yàn)證通過后,最終將區(qū)塊接入?yún)^(qū)塊鏈,如果擴(kuò)展的是側(cè)鏈,還要比較側(cè)鏈擴(kuò)展后的工作量是否超過主鏈,如果超過,則將側(cè)鏈變?yōu)橹麈?,其中詳?xì)的過程將在后文中介紹;
  7. 區(qū)塊加入?yún)^(qū)塊鏈后,最后調(diào)用processOrphans()檢測“孤兒區(qū)塊池”中是否有“孤兒區(qū)塊”的父區(qū)塊是剛剛?cè)腈湹膮^(qū)塊,如果有,則將“孤兒區(qū)塊”也加入?yún)^(qū)塊鏈,并重復(fù)這一檢查;

通過ProcessBlock()我們可以看到區(qū)塊處理的幾個(gè)階段:

其中的checkBlockSanity、maybeAcceptBlock及processOrphans等過程中又有很復(fù)雜的步驟,它們到底作了哪些檢查,如何保證區(qū)塊鏈的一致性,我們將在下一篇文章《Btcd區(qū)塊鏈的構(gòu)建(二)》中展開介紹。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容