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、啓動流程
從主程序的接口來分析源碼:
//gaia/cmd/gaiad/main.go
func main() {
//生成需要註冊的編解碼器
cdc := app.MakeCodec()
ctx := server.NewDefaultContext()
cobra.EnableCommandSorting = false
rootCmd := &cobra.Command{
Use: "gaiad",
Short: "Gaia Daemon (server)",
PersistentPreRunE: server.PersistentPreRunEFn(ctx),
}
server.AddCommands(ctx, cdc, rootCmd, app.GaiaAppInit(),
//第一步,創建APP,在其中準備啓動網絡的各個條件
server.ConstructAppCreator(newApp, "gaia"),
//第二步,導出相生成的創世文件相關狀態
server.ConstructAppExporter(exportAppState, "gaia"))
// 第三步,prepare and add flags設置並啓動Tendermint
executor := cli.PrepareBaseCmd(rootCmd, "GA", app.DefaultNodeHome)
executor.Execute()
}
func newApp(logger log.Logger, db dbm.DB) abci.Application {
return app.NewGaiaApp(logger, db)
}
func exportAppState(logger log.Logger, db dbm.DB) (json.RawMessage, error) {
gapp := app.NewGaiaApp(logger, db)
return gapp.ExportAppStateJSON()
}
這裏只分析前兩步,最後一步等分析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的持有者可以是驗證人也可以是委託人,委託人可以根據他們對驗證人的認知和具體的情況將幣委託給驗證人,驗證人即可代理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中的例程,只關心區塊鏈功能的開發,短時間內就可以方便的開發出一條公鏈來。