blockchain/geth 소스코드 분석

Geth 소스코드 분석 2 - 몇 가지 타입 살펴보기

uzzam 2024. 1. 29. 02:02

지난 글에서 geth 클라이언트를 실행할 때 어떤 일이 일어나는지, 시작점부터 실행 순서대로 보도록 하겠다고 했다.

하지만 코드를 살펴본 결과 들어가기에 앞서 geth에서 사용하는 몇 가지 타입에 대해 살펴보면 이해가 더 쉬울 것 같다.

이번 글 부터는 이해를 위해 필요한 코드가 있으면 전부 가져올 예정이라 앞으로의 글들은 좀 길 예정이다.

 

cli 패키지를 살짝 알아보고 아래 4가지 타입을 먼저 확인해보도록 하겠다.

- cli 패키지의 App, Context

- Node 패키지의 node

- ethapi 패키지의 Backend

 

cli 패키지

import 문을 보면 https://github.com/urfave/cli/v2 에서 가져온 것을 확인할 수 있는데,,

# https://cli.urfave.org/
urfave/cli is a simple, fast, and fun package for building command line apps in Go

Go에서 command line app을 만드는 가볍고 빠르고 재밌는 패키지라고 한다.

스타 수가 2만개가 넘긴 하지만, 월드 컴퓨터인 이더리움이 외부 패키지에 의존하고 있다니 보안적으로 취약한 거 아닌가 싶다가서도 모든 기능을 자체적으로 구현할 수는 없지 싶었다.

// https://cli.urfave.org/v2/getting-started/

package main

import (
    "fmt"
    "log"
    "os"

    "github.com/urfave/cli/v2"
)

func main() {
    app := &cli.App{
        Name:  "boom",
        Usage: "make an explosive entrance",
        Action: func(*cli.Context) error {
            fmt.Println("boom! I say!")
            return nil
        },
    }

    if err := app.Run(os.Args); err != nil {
        log.Fatal(err)
    }
}

이건 패키지에서 지원하는 docs에서 가져온 예시로 이런 형태로 사용한다.

이 형태는 후에 이더리움 앱의 시작점에서도 볼 수 있다.

cli.App

App이라는 타입은 아래와 같다.

// App is the main structure of a cli application. It is recommended that
// an app be created with the cli.NewApp() function
type App struct {
	// The name of the program. Defaults to path.Base(os.Args[0])
	Name string
	// Full name of command for help, defaults to Name
	HelpName string
	// Description of the program.
	Usage string
	// Text to override the USAGE section of help
	UsageText string
	// Description of the program argument format.
	ArgsUsage string
	// Version of the program
	Version string
	// Description of the program
	Description string
	// DefaultCommand is the (optional) name of a command
	// to run if no command names are passed as CLI arguments.
	DefaultCommand string
	// List of commands to execute
	Commands []*Command
	// List of flags to parse
	Flags []Flag
	// Boolean to enable bash completion commands
	EnableBashCompletion bool
	// Boolean to hide built-in help command and help flag
	HideHelp bool
	// Boolean to hide built-in help command but keep help flag.
	// Ignored if HideHelp is true.
	HideHelpCommand bool
	// Boolean to hide built-in version flag and the VERSION section of help
	HideVersion bool
	// categories contains the categorized commands and is populated on app startup
	categories CommandCategories
	// flagCategories contains the categorized flags and is populated on app startup
	flagCategories FlagCategories
	// An action to execute when the shell completion flag is set
	BashComplete BashCompleteFunc
	// An action to execute before any subcommands are run, but after the context is ready
	// If a non-nil error is returned, no subcommands are run
	Before BeforeFunc
	// An action to execute after any subcommands are run, but after the subcommand has finished
	// It is run even if Action() panics
	After AfterFunc
	// The action to execute when no subcommands are specified
	Action ActionFunc
	// Execute this function if the proper command cannot be found
	CommandNotFound CommandNotFoundFunc
	// Execute this function if a usage error occurs
	OnUsageError OnUsageErrorFunc
	// Execute this function when an invalid flag is accessed from the context
	InvalidFlagAccessHandler InvalidFlagAccessFunc
	// Compilation date
	Compiled time.Time
	// List of all authors who contributed
	Authors []*Author
	// Copyright of the binary if any
	Copyright string
	// Reader reader to write input to (useful for tests)
	Reader io.Reader
	// Writer writer to write output to
	Writer io.Writer
	// ErrWriter writes error output
	ErrWriter io.Writer
	// ExitErrHandler processes any error encountered while running an App before
	// it is returned to the caller. If no function is provided, HandleExitCoder
	// is used as the default behavior.
	ExitErrHandler ExitErrHandlerFunc
	// Other custom info
	Metadata map[string]interface{}
	// Carries a function which returns app specific info.
	ExtraInfo func() map[string]string
	// CustomAppHelpTemplate the text template for app help topic.
	// cli.go uses text/template to render templates. You can
	// render custom help text by setting this variable.
	CustomAppHelpTemplate string
	// SliceFlagSeparator is used to customize the separator for SliceFlag, the default is ","
	SliceFlagSeparator string
	// DisableSliceFlagSeparator is used to disable SliceFlagSeparator, the default is false
	DisableSliceFlagSeparator bool
	// Boolean to enable short-option handling so user can combine several
	// single-character bool arguments into one
	// i.e. foobar -o -v -> foobar -ov
	UseShortOptionHandling bool
	// Enable suggestions for commands and flags
	Suggest bool
	// Allows global flags set by libraries which use flag.XXXVar(...) directly
	// to be parsed through this library
	AllowExtFlags bool
	// Treat all flags as normal arguments if true
	SkipFlagParsing bool

	didSetup  bool
	separator separatorSpec

	rootCommand *Command
}

 

App은 Cli 앱의 메인 구조로 cli.NewApp()을 통해 만들기를 권장한다고 한다.

그럼 후에 cli.NewApp()을 통해 App을 만들어주는 과정이 있을 것이라고 추측해 볼 수 있다.

 

App타입에는 다양한 필드들이 있는데 짚고 넘어가면 좋을 것은

Commands, Flags, Action, Before, After 이다.

Commands []*Command

Commands 필드는 Command 타입의 포인터로 구성된 슬라이스인데 Command 타입은 아래와 같다.

Command 타입은 우리가 geth를 실행할 때 입력하는 명령어를 정의해 놓은 것이다.

// Command is a subcommand for a cli.App.
type Command struct {
	// The name of the command
	Name string
	// A list of aliases for the command
	Aliases []string
	// A short description of the usage of this command
	Usage string
	// Custom text to show on USAGE section of help
	UsageText string
	// A longer explanation of how the command works
	Description string
	// A short description of the arguments of this command
	ArgsUsage string
	// The category the command is part of
	Category string
	// The function to call when checking for bash command completions
	BashComplete BashCompleteFunc
	// An action to execute before any sub-subcommands are run, but after the context is ready
	// If a non-nil error is returned, no sub-subcommands are run
	Before BeforeFunc
	// An action to execute after any subcommands are run, but after the subcommand has finished
	// It is run even if Action() panics
	After AfterFunc
	// The function to call when this command is invoked
	Action ActionFunc
	// Execute this function if a usage error occurs.
	OnUsageError OnUsageErrorFunc
	// List of child commands
	Subcommands []*Command
	// List of flags to parse
	Flags          []Flag
	flagCategories FlagCategories
	// Treat all flags as normal arguments if true
	SkipFlagParsing bool
	// Boolean to hide built-in help command and help flag
	HideHelp bool
	// Boolean to hide built-in help command but keep help flag
	// Ignored if HideHelp is true.
	HideHelpCommand bool
	// Boolean to hide this command from help or completion
	Hidden bool
	// Boolean to enable short-option handling so user can combine several
	// single-character bool arguments into one
	// i.e. foobar -o -v -> foobar -ov
	UseShortOptionHandling bool

	// Full name of command for help, defaults to full command name, including parent commands.
	HelpName        string
	commandNamePath []string

	// CustomHelpTemplate the text template for the command help topic.
	// cli.go uses text/template to render templates. You can
	// render custom help text by setting this variable.
	CustomHelpTemplate string

	// categories contains the categorized commands and is populated on app startup
	categories CommandCategories

	// if this is a root "special" command
	isRoot bool

	separator separatorSpec
}

 

우리가 터미널 창에 geth 관련 명령어를 입력하면 아래와 같이 하게 된다.

이 때 'COMMANDS'에 표시된 account 등등이 모두 Command 타입인 것이다.

그리고 Command 타입에 Subcommands 라는 필드가 있다.

예를 들어 ' geth account list' 같이 account 아래에도 하위 커맨드가 있는데 그것 또한 Command 타입으로 되어 있다.

NAME:
   geth - the go-ethereum command line interface

USAGE:
   geth [global options] command [command options] [arguments...]

COMMANDS:
   account                Manage accounts
   attach                 Start an interactive JavaScript environment (connect to node)
   ...(생략)
   help, h                Shows a list of commands or help for one command

GLOBAL OPTIONS:
   ACCOUNT

    --allow-insecure-unlock        (default: false)
          Allow insecure account unlocking when account-related RPCs are exposed by http

    --keystore value
          Directory for the keystore (default = inside the datadir)
          
    (생략)

 

Flags []Flag

geth를 실행하면서 옵션이나 매개변수를 전달하게 되는데 이 때 쓰이는 것이 플래그다.

아래와 같이 인터페이스가 있으며 패키지에서 기본적으로 제공하는 flag(stringflag, intflag, boolflag 등)도 있고 사용자 정의 flag를 만들어서 사용할 수도 있다.

// Flag is a common interface related to parsing flags in cli.
// For more advanced flag parsing techniques, it is recommended that
// this interface be implemented.
type Flag interface {
	fmt.Stringer
	// Apply Flag settings to the given flag set
	Apply(*flag.FlagSet) error
	Names() []string
	IsSet() bool
}

 

예를 들자면 아래에서 GLOBAL OPTIONS 가 flag라고 보면 된다.

NAME:
   geth - the go-ethereum command line interface

USAGE:
   geth [global options] command [command options] [arguments...]

COMMANDS:
   ...(생략)

GLOBAL OPTIONS:
    (생략)

 

즉,

global options (flag 타입) - cli의 flag

command (command타입) - cli의 command

subcommand (command 타입) - command 의 subcommand

command options (flag 타입) - command 의 flag

위와 같은 형태를 띄고 있다.

 

cmd/utils/flags.go 에서 현재 사용하는 모든 flag를 확인할 수 있고, DirectoryFlag가 있는데 이건 cli 패키지에서 제공하는 것이 아닌 직접 만든 것이다.

Action, Before, After

Action 은 메인 액션을 정의해주고, before과 after는 메인 액션 혹은 서브커맨드 전 후로 실행되는 함수라고 생각하면 될 것 같다.

중요한 부분은 Action이다. (Action == 메인)

Action ActionFunc

// ActionFunc is the action to execute when no subcommands are specified
type ActionFunc func(*Context) error

Before BeforeFunc

// BeforeFunc is an action to execute before any subcommands are run, but after
// the context is ready if a non-nil error is returned, no subcommands are run
type BeforeFunc func(*Context) error

After AfterFunc

// AfterFunc is an action to execute after any subcommands are run, but after the
// subcommand has finished it is run even if Action() panics
type AfterFunc func(*Context) error

cli.Context

// Context is a type that is passed through to
// each Handler action in a cli application. Context
// can be used to retrieve context-specific args and
// parsed command-line options.
type Context struct {
	context.Context
	App           *App
	Command       *Command
	shellComplete bool
	flagSet       *flag.FlagSet
	parentContext *Context
}

 

context 타입은 cli application에서 각 핸들러에 전달되는 타입으로 특정한 인자를 검색하거나 명령줄 옵션을 파싱하는데 사용된다.

App - 현재 실행 중인 앱에 대한 포인터

Command - 현재 실행 중인 커맨드에 대한 포인터

Context 는 아래와 같은 interface로 이루어져있다.

// 주석이 많아 주석 전부 제거

type Context interface {
	Deadline() (deadline time.Time, ok bool)
    
	Done() <-chan struct{}

	Err() error
    
	Value(key any) any
}

 

즉 cli.Context의 경우 어떠한 실행이 있을 때 전달되는 것 정도로 이해하면 될 것 같다.


node.Node

Node는 서비스를 등록할 수 있는 컨테이너다.

그러니까 흔히 우리가 얘기하는 노드,, 이더리움의 노드 그 자체인 것이다.

이벤트 구독, 설정, 계정 관리, 로깅, 등등 다양한 기능과 노드에 대한 정보를 갖고 있다고 보면 될 것 같다.

// Node is a container on which services can be registered.
type Node struct {
	eventmux      *event.TypeMux
	config        *Config
	accman        *accounts.Manager
	log           log.Logger
	keyDir        string        // key store directory
	keyDirTemp    bool          // If true, key directory will be removed by Stop
	dirLock       *flock.Flock  // prevents concurrent use of instance directory
	stop          chan struct{} // Channel to wait for termination notifications
	server        *p2p.Server   // Currently running P2P networking layer
	startStopLock sync.Mutex    // Start/Stop are protected by an additional lock
	state         int           // Tracks state of node lifecycle

	lock          sync.Mutex
	lifecycles    []Lifecycle // All registered backends, services, and auxiliary services that have a lifecycle
	rpcAPIs       []rpc.API   // List of APIs currently provided by the node
	http          *httpServer //
	ws            *httpServer //
	httpAuth      *httpServer //
	wsAuth        *httpServer //
	ipc           *ipcServer  // Stores information about the ipc http server
	inprocHandler *rpc.Server // In-process RPC request handler to process the API requests

	databases map[*closeTrackingDB]struct{} // All open databases
}

ethapi.Backend

백엔드 타입은 api service 를 제공해주는 인터페이스라고 한다.

백엔드라 하면 사용자가 직접적으로 보거나 상호작용하지 않는 서버, 애플리케이션, 데이터베이스 등의 시스템이라고 볼 수 있는데 그런 시스템과 상호작용할 수 있는 api에 대한 인터페이스라고 생각하면 될 것 같다.

backend는 블록체인의 데이터 관리, 네트워크 통신, 트랜잭션 처리 등 다양한 기능을 하는 부분이 담겨있다고 보면 될 것 같다.

// Backend interface provides the common API services (that are provided by
// both full and light clients) with access to necessary functions.
type Backend interface {
	// General Ethereum API
	SyncProgress() ethereum.SyncProgress

	SuggestGasTipCap(ctx context.Context) (*big.Int, error)
	FeeHistory(ctx context.Context, blockCount uint64, lastBlock rpc.BlockNumber, rewardPercentiles []float64) (*big.Int, [][]*big.Int, []*big.Int, []float64, error)
	ChainDb() ethdb.Database
	AccountManager() *accounts.Manager
	ExtRPCEnabled() bool
	RPCGasCap() uint64            // global gas cap for eth_call over rpc: DoS protection
	RPCEVMTimeout() time.Duration // global timeout for eth_call over rpc: DoS protection
	RPCTxFeeCap() float64         // global tx fee cap for all transaction related APIs
	UnprotectedAllowed() bool     // allows only for EIP155 transactions.

	// Blockchain API
	SetHead(number uint64)
	HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error)
	HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error)
	HeaderByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*types.Header, error)
	CurrentHeader() *types.Header
	CurrentBlock() *types.Header
	BlockByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Block, error)
	BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error)
	BlockByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*types.Block, error)
	StateAndHeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*state.StateDB, *types.Header, error)
	StateAndHeaderByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*state.StateDB, *types.Header, error)
	PendingBlockAndReceipts() (*types.Block, types.Receipts)
	GetReceipts(ctx context.Context, hash common.Hash) (types.Receipts, error)
	GetTd(ctx context.Context, hash common.Hash) *big.Int
	GetEVM(ctx context.Context, msg *core.Message, state *state.StateDB, header *types.Header, vmConfig *vm.Config, blockCtx *vm.BlockContext) *vm.EVM
	SubscribeChainEvent(ch chan<- core.ChainEvent) event.Subscription
	SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription
	SubscribeChainSideEvent(ch chan<- core.ChainSideEvent) event.Subscription

	// Transaction pool API
	SendTx(ctx context.Context, signedTx *types.Transaction) error
	GetTransaction(ctx context.Context, txHash common.Hash) (bool, *types.Transaction, common.Hash, uint64, uint64, error)
	GetPoolTransactions() (types.Transactions, error)
	GetPoolTransaction(txHash common.Hash) *types.Transaction
	GetPoolNonce(ctx context.Context, addr common.Address) (uint64, error)
	Stats() (pending int, queued int)
	TxPoolContent() (map[common.Address][]*types.Transaction, map[common.Address][]*types.Transaction)
	TxPoolContentFrom(addr common.Address) ([]*types.Transaction, []*types.Transaction)
	SubscribeNewTxsEvent(chan<- core.NewTxsEvent) event.Subscription

	ChainConfig() *params.ChainConfig
	Engine() consensus.Engine

	// This is copied from filters.Backend
	// eth/filters needs to be initialized from this backend type, so methods needed by
	// it must also be included here.
	GetBody(ctx context.Context, hash common.Hash, number rpc.BlockNumber) (*types.Body, error)
	GetLogs(ctx context.Context, blockHash common.Hash, number uint64) ([][]*types.Log, error)
	SubscribeRemovedLogsEvent(ch chan<- core.RemovedLogsEvent) event.Subscription
	SubscribeLogsEvent(ch chan<- []*types.Log) event.Subscription
	SubscribePendingLogsEvent(ch chan<- []*types.Log) event.Subscription
	BloomStatus() (uint64, uint64)
	ServiceFilter(ctx context.Context, session *bloombits.MatcherSession)
}

정리

이 정도만 알면 이러한 타입들을 볼 때 무슨 일을 하는 지 어렴풋이 알고 있기 때문에 geth가 처음 시작될 때 어떻게 되는지도 이해하기 훨씬 편하다.

후에 node.Node 타입은 stack이라는 이름으로 쓰이고 ethapi.Backend 타입은 backend 라는 이름으로 쓰이게 될 것이다.

그럼 다음 글에서는 진짜 geth의 시작점으로 가서 코드를 순차적으로 보도록 하겠다.