Cosmos整體流程

買賣虛擬貨幣
Cosmos主要的原始碼其實是在SDK部分,聽名字也可以理解出來,直接用這個SDK就可以寫出一條不考慮底層的區塊鏈來,但是做為中繼鏈的一個代表,理想和現實並不是那麼完美的結合在一起。目前區塊鏈跨鏈的難點在於,網路異構、共識演算法不相容等,而解決這些問題,都意味著巨大的投入和風險。Cosmos的目的是想建立一個區塊鏈網際網路,所以他把網路和共識抽象出來,專門做了一層。但是這樣做的方法,雖然從理論上講是沒有問題的,可開發上難度還是增加了,開發者必須適應新的套路和不同的設計方法,怎麼辦?弄個SDK,隔離變化,軟體界的通用手段。一、SDK的架構

看一下架構圖:

上圖可以看出來,其實SDK就是為了APP服務的,圖中的應用程式其實就是給的例子,讓大家能快速上手。然後另外兩部分一個是和抽象層(共識和P2P)通訊的,另外一個是用來呼叫各種外掛的。

SDK從開始到現在,也進行了好幾次比較大的改動了,至於今後會不會再有大的改動,也不敢肯定。所以說,做成外掛化,是一個最好的選擇,到時候看誰不順眼,直接搞掉就可以了,喜歡誰,把外掛接進來就OK。

1、plugins層

在外掛層其實圖中畫的並不是很完全只是一個示意。主要的幾個外掛包括staking、IBC、 bank、 auth、 governance 、tx、 keys等幾個。staking主要是控制Atom持有者相關貢獻。類似一個匯率機制,動態變化。IBC其實就是鏈間通訊機制,因為各個通訊鏈是透過外掛插入到HUB中進行通訊,所以需要一個相應的通訊機制來保證通訊的安全性。governance這個模組目前在原始碼中看好像註釋了不少,只保留了較少的東西,它主要是治理相關的實現,如提議、投票等。bank其實就是提供了一系列的通訊介面(資產轉移的),所以叫“銀行”。

2、APP層

這一層基本沒啥可說的,應該就是客戶開發的APP,但是為了能讓客戶迅速進入,提供了三個相關的Demo。其中Basecoin是第一個完成的,是一個相對完整的應用,實現了SDK的核心模組的擴充套件,提供了諸如帳戶管理、管理交易型別、處理儲存等。

其它兩個都是相關的擴充套件。

3、BaseApp

這一層主要是ABCI的通訊,和Tendermint進行互動,Cosmos的核心就在這裡。

二、原始碼流程

1、啟動流程

從主程式的介面來分析原始碼:

這裡只分析前兩步,最後一步等分析Tendermint時再展開分析。

func NewGaiaApp(logger log.Logger, db dbm.DB) *GaiaApp {
    cdc := MakeCodec()

    // create your application object
    //建立一個相關的APP,其它所有的APP都可以按照這個方法
    var app = &GaiaApp{
        BaseApp:     bam.NewBaseApp(appName, cdc, logger, db),
        cdc:         cdc,
        keyMain:     sdk.NewKVStoreKey("main"),
        keyAccount:  sdk.NewKVStoreKey("acc"),
        keyIBC:      sdk.NewKVStoreKey("ibc"),
        keyStake:    sdk.NewKVStoreKey("stake"),
        keySlashing: sdk.NewKVStoreKey("slashing"),
    }

    // define the accountMapper
    //帳戶管理--從KVSTROE抽象
    app.accountMapper = auth.NewAccountMapper(
        app.cdc,
        app.keyAccount,      // target store
        &auth.BaseAccount{}, // prototype
    )

    // add handlers
    //新增各種操作——它們都從KVSTORE抽象出來,但是它們的抽象度更高,或者可以認為是accountMapper的更高一層。
    //處理帳戶的操作,再抽象一層
    app.coinKeeper = bank.NewKeeper(app.accountMapper)
    app.ibcMapper = ibc.NewMapper(app.cdc, app.keyIBC, app.RegisterCodespace(ibc.DefaultCodespace))
    //處理Atom
    app.stakeKeeper = stake.NewKeeper(app.cdc, app.keyStake, app.coinKeeper, app.RegisterCodespace(stake.DefaultCodespace))
    //設定懲罰機制操作者
    app.slashingKeeper = slashing.NewKeeper(app.cdc, app.keySlashing, app.stakeKeeper, app.RegisterCodespace(slashing.DefaultCodespace))

    // register message routes
    //這個是重點,在這裡註冊路由的控制代碼
    app.Router().
        AddRoute("bank", bank.NewHandler(app.coinKeeper)).
        AddRoute("ibc", ibc.NewHandler(app.ibcMapper, app.coinKeeper)).
        AddRoute("stake", stake.NewHandler(app.stakeKeeper))

    // initialize BaseApp
    //初始化相關引數
    app.SetInitChainer(app.initChainer)
    app.SetBeginBlocker(app.BeginBlocker)
    app.SetEndBlocker(app.EndBlocker)
    //設定許可權控制控制代碼
    app.SetAnteHandler(auth.NewAnteHandler(app.accountMapper, app.feeCollectionKeeper))
    //從KV資料庫載入相關資料--在當前版本中,IVAL儲存是KVStore基礎的實現
    app.MountStoresIAVL(app.keyMain, app.keyAccount, app.keyIBC, app.keyStake, app.keySlashing)
    err := app.LoadLatestVersion(app.keyMain)
    if err != nil {
        cmn.Exit(err.Error())
    }

    return app
}

// custom tx codec
//將相關的編碼器註冊到相關的各方
func MakeCodec() *wire.Codec {
    var cdc = wire.NewCodec()
    ibc.RegisterWire(cdc)
    bank.RegisterWire(cdc)
    stake.RegisterWire(cdc)
    slashing.RegisterWire(cdc)
    auth.RegisterWire(cdc)
    sdk.RegisterWire(cdc)
    wire.RegisterCrypto(cdc)
    return cdc
}

//其下為具體的上面的HANDLER的設定
// application updates every end block
func (app *GaiaApp) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) abci.ResponseBeginBlock {
    tags := slashing.BeginBlocker(ctx, req, app.slashingKeeper)

    return abci.ResponseBeginBlock{
        Tags: tags.ToKVPairs(),
    }
}

// application updates every end block
func (app *GaiaApp) EndBlocker(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock {
    validatorUpdates := stake.EndBlocker(ctx, app.stakeKeeper)

    return abci.ResponseEndBlock{
        ValidatorUpdates: validatorUpdates,
    }
}

// custom logic for gaia initialization
func (app *GaiaApp) initChainer(ctx sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain {
    stateJSON := req.AppStateBytes
    // TODO is this now the whole genesis file?

    var genesisState GenesisState
    err := app.cdc.UnmarshalJSON(stateJSON, &genesisState)
    if err != nil {
        panic(err) // TODO https://github.com/cosmos/cosmos-sdk/issues/468
        // return sdk.ErrGenesisParse("").TraceCause(err, "")
    }

    // load the accounts
    for _, gacc := range genesisState.Accounts {
        acc := gacc.ToAccount()
        app.accountMapper.SetAccount(ctx, acc)
    }

    // load the initial stake information
    stake.InitGenesis(ctx, app.stakeKeeper, genesisState.StakeData)

    return abci.ResponseInitChain{}
}
這裡面需要說明的是,Mapper和Keeper。記得在寫資料庫程式的時候,有幾種方法,一種是直接連線運算元據庫,拿到結果,這種方法最原始,但是權力也最大,想怎麼操作就怎麼操作。後來有了可以使用封裝物件,這樣訪問資料庫就被控制了起來,但是仍然是可以訪問很多原始的東西。現在主流的使用的是Mybaits什麼的,抽象的更厲害,基本上與你無關的資料,你根本不知道在哪兒了。Mapper和Keeper就是幹這個的,前者抽象度一般,後者更高一些。目的就是限制模組對功能訪問的方式。按照最小許可權原則來提供訪問機制。這樣,安全性和不必要的異常的出現就被控制起來,使得應用上更容易擴充套件。
這裡其實主要是governance和slashing,前者主要是控制提議和投票等,後者主要是防止有人做惡,然後從staking中slash掉你的Atom。說白了就是把你的抵押的錢沒收。這裡順道說一下這個原則:Atom的持有者可以是驗證人也可以是委託人,委託人可以根據他們對驗證人的認知和具體的情況將token委託給驗證人,驗證人即可代理Atom資產並從每個出塊獎勵中得到大部分,另外有一小部分給委託人,還有一小部分供節點的自執行。而為了保證驗證人的誠實,向區塊鏈中釋出不正確的資料的惡意驗證人會失去他們的Atom。這就叫做slashing。2、ABCI介面分析在整個的SDK的流程中,呼叫ABCI同Tendermint進行通訊是一個重要的機制。雖然這篇並不討論Tendermint,但是相關的ABCI的介面得說明一下,否則在SDK的流程呼叫中不明白相關的規則,就會導致對整個流程的理解無法正常進行。ABCI有三種訊息型別,DeliverTx,CheckTx, Commit。其中DeliverTx和BeginBlock和EndBlock兩個介面有關係。1、InitChain在上面的流程介紹過app.initChain的方法,它會被Tendermint在啟動時呼叫一次,用來初始化各種相關的Message,比如共識層的引數和最初的驗證人的集合資料。當然,肯定還會有決定資訊處理的方式。在白皮書中提到,你可以在此處將引進的資料結構進行JSON編碼,然後在呼叫這個函式的期間,對這些資訊進行填充並儲存。
// Implements ABCI
// InitChain runs the initialization logic directly on the CommitMultiStore and commits it.
func (app *BaseApp) InitChain(req abci.RequestInitChain) (res abci.ResponseInitChain) {
    if app.initChainer == nil {
        return
    }

    // Initialize the deliver state and run initChain
    app.setDeliverState(abci.Header{})
    app.initChainer(app.deliverState.ctx, req) // no error

    // NOTE: we don't commit, but BeginBlock for block 1
    // starts from this deliverState
    return
}
func (app *BaseApp) setDeliverState(header abci.Header) {
    ms := app.cms.CacheMultiStore()
    app.deliverState = &state{
        ms:  ms,
        ctx: sdk.NewContext(ms, header, false, nil, app.Logger),
    }
}
當這些資訊被正確的處理後,比如是一個帳戶相關的資訊,那麼就可以使用它來進行交易的處理了。2、BeginBlock在上面提到過Tendermint的三種訊息,其中的交易處理訊息DeliverTx,它就是在區塊開始被呼叫前,在這個介面中處理驗證人簽名的資訊。如果大家寫過資料庫的底層操作,這個東西應該和它非常類似,不外乎是Begin準備,End結束,清掃資源。不過使用它的時候也需要注意,它和其它的相類似的操作一樣,在這兩個函式的處理過程中,不應該包含過多的和過於複雜的操作,導致整個訊息的阻塞。如果在這二者中出現了不合理的迴圈等,就有可能導致應用程式APP的假死。
// application updates every end block
func (app *GaiaApp) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) abci.ResponseBeginBlock {
    tags := slashing.BeginBlocker(ctx, req, app.slashingKeeper)

    return abci.ResponseBeginBlock{
        Tags: tags.ToKVPairs(),
    }
}
// slashing begin block functionality
func BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock, sk Keeper) (tags sdk.Tags) {
    // Tag the height
    heightBytes := make([]byte, 8)
    binary.LittleEndian.PutUint64(heightBytes, uint64(req.Header.Height))
    tags = sdk.NewTags("height", heightBytes)

    // Deal with any equivocation evidence
    for _, evidence := range req.ByzantineValidators {
        pk, err := tmtypes.PB2TM.PubKey(evidence.Validator.PubKey)
        if err != nil {
            panic(err)
        }
        switch string(evidence.Type) {
        case tmtypes.ABCIEvidenceTypeDuplicateVote:
            //處理驗證器在同一高度簽名兩個塊
            sk.handleDoubleSign(ctx, evidence.Height, evidence.Time, pk)
        default:
            ctx.Logger().With("module", "x/slashing").Error(fmt.Sprintf("Ignored unknown evidence type: %s", string(evidence.Type)))
        }
    }

    // Iterate over all the validators  which *should* have signed this block
    for _, validator := range req.Validators {
        present := validator.SignedLastBlock
        pubkey, err := tmtypes.PB2TM.PubKey(validator.Validator.PubKey)
        if err != nil {
            panic(err)
        }
        sk.handleValidatorSignature(ctx, pubkey, present)
    }

    return
}
3、EndBlock
響應上一個函式介面,在DeliverTx訊息處理完成所有的交易後呼叫,主要用來對驗證人集合的結果進行維護。
// Implements ABCI
func (app *BaseApp) EndBlock(req abci.RequestEndBlock) (res abci.ResponseEndBlock) {
    if app.endBlocker != nil {
        res = app.endBlocker(app.deliverState.ctx, req)
    } else {
        res.ValidatorUpdates = app.valUpdates
    }
    return
}
4、Commit當處理完成交易後,應該把完成的交易從記憶體持久化到硬碟上,並根據建立返回被下一個Tendermint區塊需要的默克爾樹的Root雜湊值。這個雜湊值 的作用在區塊鏈中基本是一樣的,用來驗證合法性。
// Implements ABCI
func (app *BaseApp) Commit() (res abci.ResponseCommit) {
    header := app.deliverState.ctx.BlockHeader()
    /*
        // Write the latest Header to the store
            headerBytes, err := proto.Marshal(&header)
            if err != nil {
                panic(err)
            }
            app.db.SetSync(dbHeaderKey, headerBytes)
    */

    // Write the Deliver state and commit the MultiStore
    app.deliverState.ms.Write()
    commitID := app.cms.Commit()
    app.Logger.Debug("Commit synced",
        "commit", commitID,
    )

    // Reset the Check state to the latest committed
    // NOTE: safe because Tendermint holds a lock on the mempool for Commit.
    // Use the header from this latest block.
    app.setCheckState(header)

    // Empty the Deliver state
    app.deliverState = nil

    return abci.ResponseCommit{
        Data: commitID.Hash,
    }
}
5、Query
這個就不多說了吧,你總得給別人一個看一看的機會。
// Implements ABCI.
// Delegates to CommitMultiStore if it implements Queryable
func (app *BaseApp) Query(req abci.RequestQuery) (res abci.ResponseQuery) {
    path := strings.Split(req.Path, "/")
    // first element is empty string
    if len(path) > 0 && path[0] == "" {
        path = path[1:]
    }
    // "/app" prefix for special application queries
    if len(path) >= 2 && path[0] == "app" {
        var result sdk.Result
        switch path[1] {
        case "simulate":
            txBytes := req.Data
            tx, err := app.txDecoder(txBytes)
            if err != nil {
                result = err.Result()
            } else {
                result = app.Simulate(tx)
            }
        default:
            result = sdk.ErrUnknownRequest(fmt.Sprintf("Unknown query: %s", path)).Result()
        }
        value := app.cdc.MustMarshalBinary(result)
        return abci.ResponseQuery{
            Code:  uint32(sdk.ABCICodeOK),
            Value: value,
        }
    }
    // "/store" prefix for store queries
    if len(path) >= 1 && path[0] == "store" {
        queryable, ok := app.cms.(sdk.Queryable)
        if !ok {
            msg := "multistore doesn't support queries"
            return sdk.ErrUnknownRequest(msg).QueryResult()
        }
        req.Path = "/" + strings.Join(path[1:], "/")
        return queryable.Query(req)
    }
    // "/p2p" prefix for p2p queries
    if len(path) >= 4 && path[0] == "p2p" {
        if path[1] == "filter" {
            if path[2] == "addr" {
                return app.FilterPeerByAddrPort(path[3])
            }
            if path[2] == "pubkey" {
                return app.FilterPeerByPubKey(path[3])
            }
        }
    }
    msg := "unknown query path"
    return sdk.ErrUnknownRequest(msg).QueryResult()
}
6、CheckTx所有的擁有交易池的區塊鏈,基本上在進池前後都要搞一些事情,包括對各種合法性的檢查,目的只有一個,防止千辛萬苦才生產出來的區塊打包一些沒用的交易。在Cosmos中也會有這種手段,在前面提到過AnteHandler,透過其對傳送者授權,確定在交易前有足夠的手續費,不過它和以太坊有些類似,如果交易失敗,這筆費用仍然沒有了,收不回去。
// Implements ABCI
func (app *BaseApp) CheckTx(txBytes []byte) (res abci.ResponseCheckTx) {
    // Decode the Tx.
    var result sdk.Result
    var tx, err = app.txDecoder(txBytes)
    if err != nil {
        result = err.Result()
    } else {
        result = app.runTx(runTxModeCheck, txBytes, tx)
    }

    return abci.ResponseCheckTx{
        Code:      uint32(result.Code),
        Data:      result.Data,
        Log:       result.Log,
        GasWanted: result.GasWanted,
        GasUsed:   result.GasUsed,
        Fee: cmn.KI64Pair{
            []byte(result.FeeDenom),
            result.FeeAmount,
        },
        Tags: result.Tags,
    }
}
3、IBC通訊原始碼
在前面的程式碼中初始化時需要對路由進行註冊,在這裡同樣會有路由的實際註冊過程,先看一看提供的命令處理方式:
// IBC transfer command
func IBCTransferCmd(cdc *wire.Codec) *cobra.Command {
    cmd := &cobra.Command{
        Use: "transfer",
        RunE: func(cmd *cobra.Command, args []string) error {
            ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc))

            // get the from address
            from, err := ctx.GetFromAddress()
            if err != nil {
                return err
            }

            // build the message
            msg, err := buildMsg(from)
            if err != nil {
                return err
            }

            // get password
            res, err := ctx.EnsureSignBuildBroadcast(ctx.FromAddressName, msg, cdc)
            if err != nil {
                return err
            }

            fmt.Printf("Committed at block %d. Hash: %s\n", res.Height, res.Hash.String())
            return nil
        },
    }

    cmd.Flags().String(flagTo, "", "Address to send coins")
    cmd.Flags().String(flagAmount, "", "Amount of coins to send")
    cmd.Flags().String(flagChain, "", "Destination chain to send coins")
    return cmd
}
處理傳輸命令,進入中繼環節處理:
// flags--代表從一個空間轉向另外一個窠
const (
    FlagFromChainID   = "from-chain-id"
    FlagFromChainNode = "from-chain-node"
    FlagToChainID     = "to-chain-id"
    FlagToChainNode   = "to-chain-node"
)

type relayCommander struct {
    cdc       *wire.Codec
    address   sdk.Address
    decoder   auth.AccountDecoder
    mainStore string
    ibcStore  string
    accStore  string

    logger log.Logger
}

// IBC relay command
func IBCRelayCmd(cdc *wire.Codec) *cobra.Command {
    cmdr := relayCommander{
        cdc:       cdc,
        decoder:   authcmd.GetAccountDecoder(cdc),
        ibcStore:  "ibc",
        mainStore: "main",
        accStore:  "acc",

        logger: log.NewTMLogger(log.NewSyncWriter(os.Stdout)),
    }

    cmd := &cobra.Command{
        Use: "relay",
        Run: cmdr.runIBCRelay,
    }

    cmd.Flags().String(FlagFromChainID, "", "Chain ID for ibc node to check outgoing packets")
    cmd.Flags().String(FlagFromChainNode, "tcp://localhost:46657", "<host>:<port> to tendermint rpc interface for this chain")
    cmd.Flags().String(FlagToChainID, "", "Chain ID for ibc node to broadcast incoming packets")
    cmd.Flags().String(FlagToChainNode, "tcp://localhost:36657", "<host>:<port> to tendermint rpc interface for this chain")

    cmd.MarkFlagRequired(FlagFromChainID)
    cmd.MarkFlagRequired(FlagFromChainNode)
    cmd.MarkFlagRequired(FlagToChainID)
    cmd.MarkFlagRequired(FlagToChainNode)

    viper.BindPFlag(FlagFromChainID, cmd.Flags().Lookup(FlagFromChainID))
    viper.BindPFlag(FlagFromChainNode, cmd.Flags().Lookup(FlagFromChainNode))
    viper.BindPFlag(FlagToChainID, cmd.Flags().Lookup(FlagToChainID))
    viper.BindPFlag(FlagToChainNode, cmd.Flags().Lookup(FlagToChainNode))

    return cmd
}

//啟動遍歷監聽
func (c relayCommander) runIBCRelay(cmd *cobra.Command, args []string) {
    fromChainID := viper.GetString(FlagFromChainID)
    fromChainNode := viper.GetString(FlagFromChainNode)
    toChainID := viper.GetString(FlagToChainID)
    toChainNode := viper.GetString(FlagToChainNode)
    address, err := context.NewCoreContextFromViper().GetFromAddress()
    if err != nil {
        panic(err)
    }
    c.address = address

    c.loop(fromChainID, fromChainNode, toChainID, toChainNode)
}

func (c relayCommander) loop(fromChainID, fromChainNode, toChainID,
    toChainNode string) {

    ctx := context.NewCoreContextFromViper()
    // get password
    passphrase, err := ctx.GetPassphraseFromStdin(ctx.FromAddressName)
    if err != nil {
        panic(err)
    }

    ingressKey := ibc.IngressSequenceKey(fromChainID)

OUTER:
    for {
        time.Sleep(5 * time.Second)

        processedbz, err := query(toChainNode, ingressKey, c.ibcStore)
        if err != nil {
            panic(err)
        }

        var processed int64
        if processedbz == nil {
            processed = 0
        } else if err = c.cdc.UnmarshalBinary(processedbz, &processed); err != nil {
            panic(err)
        }

        lengthKey := ibc.EgressLengthKey(toChainID)
        egressLengthbz, err := query(fromChainNode, lengthKey, c.ibcStore)
        if err != nil {
            c.logger.Error("Error querying outgoing packet list length", "err", err)
            continue OUTER //TODO replace with continue (I think it should just to the correct place where OUTER is now)
        }
        var egressLength int64
        if egressLengthbz == nil {
            egressLength = 0
        } else if err = c.cdc.UnmarshalBinary(egressLengthbz, &egressLength); err != nil {
            panic(err)
        }
        if egressLength > processed {
            c.logger.Info("Detected IBC packet", "number", egressLength-1)
        }

        seq := c.getSequence(toChainNode)

        for i := processed; i < egressLength; i++ {
            egressbz, err := query(fromChainNode, ibc.EgressKey(toChainID, i), c.ibcStore)
            if err != nil {
                c.logger.Error("Error querying egress packet", "err", err)
                continue OUTER // TODO replace to break, will break first loop then send back to the beginning (aka OUTER)
            }

            err = c.broadcastTx(seq, toChainNode, c.refine(egressbz, i, passphrase))
            seq++
            if err != nil {
                c.logger.Error("Error broadcasting ingress packet", "err", err)
                continue OUTER // TODO replace to break, will break first loop then send back to the beginning (aka OUTER)
            }

            c.logger.Info("Relayed IBC packet", "number", i)
        }
    }
}

func (c relayCommander) broadcastTx(seq int64, node string, tx []byte) error {
    _, err := context.NewCoreContextFromViper().WithNodeURI(node).WithSequence(seq + 1).BroadcastTx(tx)
    return err
}

//處理接收的訊息
func (c relayCommander) refine(bz []byte, sequence int64, passphrase string) []byte {
    var packet ibc.IBCPacket
    if err := c.cdc.UnmarshalBinary(bz, &packet); err != nil {
        panic(err)
    }

    msg := ibc.IBCReceiveMsg{
        IBCPacket: packet,
        Relayer:   c.address,
        Sequence:  sequence,
    }

    ctx := context.NewCoreContextFromViper().WithSequence(sequence)
    res, err := ctx.SignAndBuild(ctx.FromAddressName, passphrase, msg, c.cdc)
    if err != nil {
        panic(err)
    }
    return res
}
透過一箇中繼節點來監聽兩條不同的鏈,進行訊息的路由註冊來達到自動跨鏈交易,Cosmos提供的這個方式還是比較不錯的。至少,不用自己再犯愁怎麼做。但是這個有一個前提,需要註冊一下:
// RegisterRoutes - Central function to define routes that get registered by the main application
func RegisterRoutes(ctx context.CoreContext, r *mux.Router, cdc *wire.Codec, kb keys.Keybase) {
    r.HandleFunc("/ibc/{destchain}/{address}/send", TransferRequestHandlerFn(cdc, kb, ctx)).Methods("POST")
}
三、總結透過上面的程式碼分析,可以看出,ABCI和IBC兩個模組,是執行整個Cosmos的一個基礎。Cosmos-SDK把這幾個模組有機的抽象到一起,並提供了基礎的交易、通訊等功能。新的區塊鏈可以從它上面呼叫或者繼承Example中的例程,只關心區塊鏈功能的開發,短時間內就可以方便的開發出一條公鏈來。

免責聲明:

  1. 本文版權歸原作者所有,僅代表作者本人觀點,不代表鏈報觀點或立場。
  2. 如發現文章、圖片等侵權行爲,侵權責任將由作者本人承擔。
  3. 鏈報僅提供相關項目信息,不構成任何投資建議

推荐阅读

;