이제부터는 코드를 가져올 예정이기 때문에 글 하나하나가 굉장히 길 것 같다.
그럼 geth 클라이언트의 시작점은 대체 어디일까?
main.go 파일을 찾아야 하는데 다양한 패키지가 있고 많은 main.go가 있다.
geth 클라이언트를 실행할 때 터미널에서 geth ~~~ 명령어를 입력하게 된다.
이것과 관련된 명령어를 치는 부분인 cmd/geth/main.go 가 바로 시작점이다.
본격적인 흐름을 보기 전에 자주 나오는 타입에 대해서는 한 번 보고가는게 좋을 것 같다.
main()
첫 시작 함수인 main()
여기서는 app.Run 을 실행하고, 에러가 있으면 에러 출력과 함께 종료한다.
이 때 app.Run의 argument인 os.Args 는 geth 명령어를 입력할 때 들어오는 값이다.
func main() {
if err := app.Run(os.Args); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
geth [global options] command [command options] [arguments...]
geth 뒤에 있는 값들을 받아와서 그에 맞는 명령어를 실행시켜주는 것이다.
app
그럼 app은 뭐고 Run을 실행하면 어떤 일이 일어날까?
app은 아래의 변수 선언과 init() 함수를 통해 초기화를 해준다고 볼 수 있다.
app 변수 선언
var app = flags.NewApp("the go-ethereum command line interface")
flags 패키지의 NewApp 함수(flags.newApp)는 다음과 같다.
// NewApp creates an app with sane defaults.
func NewApp(usage string) *cli.App {
git, _ := version.VCS()
app := cli.NewApp()
app.EnableBashCompletion = true
app.Version = params.VersionWithCommit(git.Commit, git.Date)
app.Usage = usage
app.Copyright = "Copyright 2013-2024 The go-ethereum Authors"
app.Before = func(ctx *cli.Context) error {
MigrateGlobalFlags(ctx)
return nil
}
return app
}
우리가 이전 글에서 본 것 처럼 cli.NewApp을 통해 기본 App을 만들어주는 것을 볼 수 있다.
그리고 기본 설정을 App에 넣어준 후 반환해준다.
init()
init 함수는 프로그램이 시작될 때 자동으로 호출된다.
cli.App타입인 app이라는 이름을 가진 변수에 action, commands, flags, before, after 넣어줌
각각의 특성이 뭘하는지 이전 글에서 확인할 수 있었다.
func init() {
// Initialize the CLI app and start Geth
app.Action = geth
app.Commands = []*cli.Command{
// See chaincmd.go:
initCommand,
importCommand,
exportCommand,
importPreimagesCommand,
removedbCommand,
dumpCommand,
dumpGenesisCommand,
// See accountcmd.go:
accountCommand,
walletCommand,
// See consolecmd.go:
consoleCommand,
attachCommand,
javascriptCommand,
// See misccmd.go:
versionCommand,
versionCheckCommand,
licenseCommand,
// See config.go
dumpConfigCommand,
// see dbcmd.go
dbCommand,
// See cmd/utils/flags_legacy.go
utils.ShowDeprecated,
// See snapshot.go
snapshotCommand,
// See verkle.go
verkleCommand,
}
if logTestCommand != nil {
app.Commands = append(app.Commands, logTestCommand)
}
sort.Sort(cli.CommandsByName(app.Commands))
app.Flags = flags.Merge(
nodeFlags,
rpcFlags,
consoleFlags,
debug.Flags,
metricsFlags,
)
flags.AutoEnvVars(app.Flags, "GETH")
app.Before = func(ctx *cli.Context) error {
maxprocs.Set() // Automatically set GOMAXPROCS to match Linux container CPU quota.
flags.MigrateGlobalFlags(ctx)
if err := debug.Setup(ctx); err != nil {
return err
}
flags.CheckEnvVars(ctx, app.Flags, "GETH")
return nil
}
app.After = func(ctx *cli.Context) error {
debug.Exit()
prompt.Stdin.Close() // Resets terminal mode.
return nil
}
}
여기까지 하면 app에 대한 정보가 들어갔다.
app.Run
이제 다시 메인함수에 있었던 app.Run으로 돌아가보자
app.Run은 Run -> RunContext 함수를 실행하고 에러를 반환해준다.
Run()
Run() 함수는 단순히 RunContext 함수를 실행시켜준다.
context.Background() 는 빈 context.Context 를 만들어주는 함수이다.
빈 Context와 실행시에 받았던 arguments 들로 RunContext 함수를 실행하는 것이다.
// Run is the entry point to the cli app. Parses the arguments slice and routes
// to the proper flag/args combination
func (a *App) Run(arguments []string) (err error) {
return a.RunContext(context.Background(), arguments)
}
RunContext()
// RunContext is like Run except it takes a Context that will be
// passed to its commands and sub-commands. Through this, you can
// propagate timeouts and cancellation requests
func (a *App) RunContext(ctx context.Context, arguments []string) (err error) {
a.Setup()
// handle the completion flag separately from the flagset since
// completion could be attempted after a flag, but before its value was put
// on the command line. this causes the flagset to interpret the completion
// flag name as the value of the flag before it which is undesirable
// note that we can only do this because the shell autocomplete function
// always appends the completion flag at the end of the command
shellComplete, arguments := checkShellCompleteFlag(a, arguments)
cCtx := NewContext(a, nil, &Context{Context: ctx})
cCtx.shellComplete = shellComplete
a.rootCommand = a.newRootCommand()
cCtx.Command = a.rootCommand
return a.rootCommand.Run(cCtx, arguments...)
}
app을 Setup 해주는 것부터 시작한다.
Setup 함수는 중요도에 비해 코드가 길어서 주석만 첨부했다.
Run 하기전에 필요한 설정들을 넣어준다고 생각하면 된다.
// Setup runs initialization code to ensure all data structures are ready for
// `Run` or inspection prior to `Run`. It is internally called by `Run`, but
// will return early if setup has already happened.
func (a *App) Setup() {}
그 다음은 rootCommand 를 설정해주고 실행해준다.
a.rootCommand = a.newRootCommand()
cCtx.Command = a.rootCommand
return a.rootCommand.Run(cCtx, arguments...)
a.rootCommand.Run() 함수는 상당히 긴데 맨 마지막 부분에 보면 이런 코드가 있다.
err = c.Action(cCtx)
cCtx.App.handleExitCoder(cCtx, err)
return err
Command 타입의 Action 에 정의해놨던 함수를 실행시켜주는 것이다..!!
geth()
우리가 init 함수에서 cli.App 의 Action을 app.Action = geth과 같이 정의해주었다.
그리고 Run을 하게 되면 Action 함수를 실행하는 것을 보았다.
그럼 이 Action 함수에 해당하는 geth 함수를 알아보자!
geth에서는 prepare, makeFullNode, startNode 세 가지 함수를 실행시켜 주는 것을 볼 수 있다!
이번 글에서는 makeFullNode와 startNode 함수를 제외한 나머지 부분을 살펴볼 예정이다.
// geth is the main entry point into the system if no special subcommand is run.
// It creates a default node based on the command line arguments and runs it in
// blocking mode, waiting for it to be shut down.
func geth(ctx *cli.Context) error {
if args := ctx.Args().Slice(); len(args) > 0 {
return fmt.Errorf("invalid command: %q", args[0])
}
prepare(ctx)
stack, backend := makeFullNode(ctx)
defer stack.Close()
startNode(ctx, stack, backend, false)
stack.Wait()
return nil
}
상단의 if문은 커맨드가 잘못 입력되었을 때 에러를 반환해준다.
이렇게 커맨드를 잘못 입력하면 해당 코드가 실행되는 것을 확인할 수 있다.
prepare()
prepare 함수는 flag에 따른 로그를 보여주고, metric 수집을 시작한다.
// prepare manipulates memory cache allowance and setups metric system.
// This function should be called before launching devp2p stack.
func prepare(ctx *cli.Context) {
// If we're running a known preset, log it for convenience.
switch {
case ctx.IsSet(utils.GoerliFlag.Name):
log.Info("Starting Geth on Görli testnet...")
case ctx.IsSet(utils.SepoliaFlag.Name):
log.Info("Starting Geth on Sepolia testnet...")
case ctx.IsSet(utils.HoleskyFlag.Name):
log.Info("Starting Geth on Holesky testnet...")
case ctx.IsSet(utils.DeveloperFlag.Name):
log.Info("Starting Geth in ephemeral dev mode...")
log.Warn(`You are running Geth in --dev mode. Please note the following:
1. This mode is only intended for fast, iterative development without assumptions on
security or persistence.
2. The database is created in memory unless specified otherwise. Therefore, shutting down
your computer or losing power will wipe your entire block data and chain state for
your dev environment.
3. A random, pre-allocated developer account will be available and unlocked as
eth.coinbase, which can be used for testing. The random dev account is temporary,
stored on a ramdisk, and will be lost if your machine is restarted.
4. Mining is enabled by default. However, the client will only seal blocks if transactions
are pending in the mempool. The miner's minimum accepted gas price is 1.
5. Networking is disabled; there is no listen-address, the maximum number of peers is set
to 0, and discovery is disabled.
`)
case !ctx.IsSet(utils.NetworkIdFlag.Name):
log.Info("Starting Geth on Ethereum mainnet...")
}
// If we're a full node on mainnet without --cache specified, bump default cache allowance
if !ctx.IsSet(utils.CacheFlag.Name) && !ctx.IsSet(utils.NetworkIdFlag.Name) {
// Make sure we're not on any supported preconfigured testnet either
if !ctx.IsSet(utils.HoleskyFlag.Name) &&
!ctx.IsSet(utils.SepoliaFlag.Name) &&
!ctx.IsSet(utils.GoerliFlag.Name) &&
!ctx.IsSet(utils.DeveloperFlag.Name) {
// Nope, we're really on mainnet. Bump that cache up!
log.Info("Bumping default cache on mainnet", "provided", ctx.Int(utils.CacheFlag.Name), "updated", 4096)
ctx.Set(utils.CacheFlag.Name, strconv.Itoa(4096))
}
}
// Start metrics export if enabled
utils.SetupMetrics(ctx)
// Start system runtime metrics collection
go metrics.CollectProcessMetrics(3 * time.Second)
}
metrics.CollectProcessMetrics()에서 처음으로 고루틴이 나온다.
CollectProcessMetrics 함수를 살펴보면 3초에 한 번씩 sleep을 걸어주며 for문을 돌며 수집해준다.
3초마다 정보를 수집해주는 경량 스레드가 처음으로 생성되었다.
stack.Close() , stack.Wait()
stack은 node.Node 타입인데, makeFullNode를 통해 받은 node를 wait 하다가 종료되면 close 해주는 것이다.
// Close stops the Node and releases resources acquired in
// Node constructor New.
func (n *Node) Close() error {
n.startStopLock.Lock()
defer n.startStopLock.Unlock()
n.lock.Lock()
state := n.state
n.lock.Unlock()
switch state {
case initializingState:
// The node was never started.
return n.doClose(nil)
case runningState:
// The node was started, release resources acquired by Start().
var errs []error
if err := n.stopServices(n.lifecycles); err != nil {
errs = append(errs, err)
}
return n.doClose(errs)
case closedState:
return ErrNodeStopped
default:
panic(fmt.Sprintf("node is in unknown state %d", state))
}
}
// Wait blocks until the node is closed.
func (n *Node) Wait() {
<-n.stop
}
정리
이번 글에서 본 건 geth cli app이 어떻게 켜지고 있는지 보았다.
geth 클라이언트가 실행될 때 cli.App 타입 변수인 app에 정보를 넣어주고 app.Run 함수를 통해 app.Action에 해당하는 함수인 geth 함수를 실행시켜주는 것이 전부이다.
다음 글에서는 makeFullNode 함수를 알아보며 node를 어떻게 만드는지 알아보고
그 다음 글에서 startNode 함수를 통해 node가 시작되는 과정을 살펴보겠다.
'blockchain > geth 소스코드 분석' 카테고리의 다른 글
Geth 소스코드 분석 4 - makeFullNode 1편 - makeConfigNode() (0) | 2024.02.07 |
---|---|
Geth 소스코드 분석 2 - 몇 가지 타입 살펴보기 (0) | 2024.01.29 |
Geth 소스코드 분석 1 - 시작하기 및 전체 구조 (0) | 2024.01.27 |