이더리움 코어 API 등록 과정

hop6·2022년 5월 25일
0
eth_getTransactionCount
eth_estimateGas
eth_getbalance
.
.
.

위와 같은 Rpc API를 호출하면 이더리움 코어 안에
GetTransactionCount()
EstimateGas()
GetBalane()
이와 같은 메서드가 호출이 되는데, 어느 부분에서 포팅이 되는지 평소에 알 수 가 없던 터라 이번 기회에 한번 알아 본다.

// go-ethereum/node/node.go
func New(conf *Config) (*Node, error) {
    .
    .
    .
	node.rpcAPIs = append(node.rpcAPIs, node.apis()...)
    .
    .
    .
}

func (s *Ethereum) APIs() []rpc.API {
	apis := ethapi.GetAPIs(s.APIBackend)

	// Append any APIs exposed explicitly by the consensus engine
	apis = append(apis, s.engine.APIs(s.BlockChain())...)

	// Append all the local APIs and return
	return append(apis, []rpc.API{
		{
			Namespace: "eth",
			Version:   "1.0",
			Service:   NewPublicEthereumAPI(s),
			Public:    true,
		}, {
			Namespace: "eth",
			Version:   "1.0",
			Service:   NewPublicMinerAPI(s),
			Public:    true,
		}, {
			Namespace: "eth",
			Version:   "1.0",
			Service:   downloader.NewPublicDownloaderAPI(s.handler.downloader, s.eventMux),
			Public:    true,
		}, {
			Namespace: "miner",
			Version:   "1.0",
			Service:   NewPrivateMinerAPI(s),
			Public:    false,
		}, {
			Namespace: "eth",
			Version:   "1.0",
			Service:   filters.NewPublicFilterAPI(s.APIBackend, false, 5*time.Minute),
			Public:    true,
		}, {
			Namespace: "admin",
			Version:   "1.0",
			Service:   NewPrivateAdminAPI(s),
		}, {
			Namespace: "debug",
			Version:   "1.0",
			Service:   NewPublicDebugAPI(s),
			Public:    true,
		}, {
			Namespace: "debug",
			Version:   "1.0",
			Service:   NewPrivateDebugAPI(s),
		}, {
			Namespace: "net",
			Version:   "1.0",
			Service:   s.netRPCService,
			Public:    true,
		},
	}...)
}

파라미터로 받은 node에 API를 Register해준다.
위 API중 예시로 NewPublicEthereumAPI를 예시로 보겠다.

{	
			Namespace: "eth",
			Version:   "1.0",
			Service:   NewPublicEthereumAPI(s),
			Public:    true,
}

// go-ethereum/eth/api.go
type PublicEthereumAPI struct {
	e *Ethereum
}

func NewPublicEthereumAPI(e *Ethereum) *PublicEthereumAPI {
	return &PublicEthereumAPI{e}
}

// Etherbase is the address that mining rewards will be send to
func (api *PublicEthereumAPI) Etherbase() (common.Address, error) {
	return api.e.Etherbase()
}

// Coinbase is the address that mining rewards will be send to (alias for Etherbase)
func (api *PublicEthereumAPI) Coinbase() (common.Address, error) {
	return api.Etherbase()
}

// Hashrate returns the POW hashrate
func (api *PublicEthereumAPI) Hashrate() hexutil.Uint64 {
	return hexutil.Uint64(api.e.Miner().Hashrate())
}

위와 같은 형태의 Service가


// go-ethereum/node/node.go 
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       fileutil.Releaser // 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
}

func (n *Node) RegisterAPIs(apis []rpc.API) {
	n.lock.Lock()
	defer n.lock.Unlock()

	if n.state != initializingState {
		panic("can't register APIs on running/stopped node")
	}
	n.rpcAPIs = append(n.rpcAPIs, apis...)
}

Node.rpcAPIs에 추가되며, startInProc() 메서드로 api를 등록한다.


// go-ethereum/node/node.go 
func (n *Node) startInProc() error {
	for _, api := range n.rpcAPIs {
		if err := n.inprocHandler.RegisterName(api.Namespace, api.Service); err != nil {
			return err
		}
	}
	return nil
}

// go-ethereum/rpc/server.go
type Server struct {
	services serviceRegistry
	idgen    func() ID
	run      int32
	codecs   mapset.Set
}

func (s *Server) RegisterName(name string, receiver interface{}) error {
	return s.services.registerName(name, receiver)
}

// go-ethereum/rpc/service.go
func (r *serviceRegistry) registerName(name string, rcvr interface{}) error {
	rcvrVal := reflect.ValueOf(rcvr)
	if name == "" {
		return fmt.Errorf("no service name for type %s", rcvrVal.Type().String())
	}
	callbacks := suitableCallbacks(rcvrVal)
	if len(callbacks) == 0 {
		return fmt.Errorf("service %T doesn't have any suitable methods/subscriptions to expose", rcvr)
	}

	r.mu.Lock()
	defer r.mu.Unlock()
	if r.services == nil {
		r.services = make(map[string]service)
	}
	svc, ok := r.services[name]
	if !ok {
		svc = service{
			name:          name,
			callbacks:     make(map[string]*callback),
			subscriptions: make(map[string]*callback),
		}
		r.services[name] = svc
	}
	for name, cb := range callbacks {
		if cb.isSubscribe {
			svc.subscriptions[name] = cb
		} else {
			svc.callbacks[name] = cb
		}
	}
	return nil
}

func suitableCallbacks(receiver reflect.Value) map[string]*callback {
	typ := receiver.Type()
	callbacks := make(map[string]*callback)
	for m := 0; m < typ.NumMethod(); m++ {
		method := typ.Method(m)
		if method.PkgPath != "" {
			continue // method not exported
		}
		cb := newCallback(receiver, method.Func)
		if cb == nil {
			continue // function invalid
		}
		name := formatName(method.Name)
		callbacks[name] = cb
	}
	return callbacks
}

func formatName(name string) string {
	ret := []rune(name)
	if len(ret) > 0 {
		ret[0] = unicode.ToLower(ret[0])
	}
	return string(ret)
}
// go-ethereum/rpc/service.go
func newCallback(receiver, fn reflect.Value) *callback {
	fntype := fn.Type()
	c := &callback{fn: fn, rcvr: receiver, errPos: -1, isSubscribe: isPubSub(fntype)}
	// Determine parameter types. They must all be exported or builtin types.
	c.makeArgTypes()

	// Verify return types. The function must return at most one error
	// and/or one other non-error value.
	outs := make([]reflect.Type, fntype.NumOut())
	for i := 0; i < fntype.NumOut(); i++ {
		outs[i] = fntype.Out(i)
	}
	if len(outs) > 2 {
		return nil
	}
	// If an error is returned, it must be the last returned value.
	switch {
	case len(outs) == 1 && isErrorType(outs[0]):
		c.errPos = 0
	case len(outs) == 2:
		if isErrorType(outs[0]) || !isErrorType(outs[1]) {
			return nil
		}
		c.errPos = 1
	}
	return c
}

func (c *callback) makeArgTypes() {
	fntype := c.fn.Type()
	// Skip receiver and context.Context parameter (if present).
	firstArg := 0
	if c.rcvr.IsValid() {
		firstArg++
	}
	if fntype.NumIn() > firstArg && fntype.In(firstArg) == contextType {
		c.hasCtx = true
		firstArg++
	}
	// Add all remaining parameters.
	c.argTypes = make([]reflect.Type, fntype.NumIn()-firstArg)
	for i := firstArg; i < fntype.NumIn(); i++ {
		c.argTypes[i-firstArg] = fntype.In(i)
	}
}

예시로 PublicEthereumAPI 를 들어 보자면,
registerName("eth", *PublicEthereumAPI) 이며
suitableCallbacks를 메서드를 통하여 PublicEthereumAPI의 메서드를 재구성한다.
for문을 돌며 callback이라는 구조체와 메서드 name을 파싱한다.
예시로 PublicFilterAPI 의 NewFilter(crit FilterCriteria) (rpc.ID, error) 를 로직에 대입 시켜본다.

// PublicFilterAPI
// 아래 두개 메서드만 갖고 있다고 가정
func (api *PublicFilterAPI) NewFilter(crit FilterCriteria) (rpc.ID, error)
func (api *PublicFilterAPI) Logs(ctx context.Context, crit FilterCriteria) (*rpc.Subscription, error)

// name == "eth"
// rcvr == func Logs(ctx context.Context, crit FilterCriteria) (*rpc.Subscription, error)

// 메서드에서 reflect.Value 타입을 가져온다.
recvrVal := reflect.ValueOf(rcvr)
suitableCallbacks(rcvrVal)

>> suitableCallbacks
typ := rcvrVal.Type()

// for문을 돌며 메서드 순회
method := typ.Method(m)
/*
	type Method struct {
	  Name string

      // 해당 메서드가 Public 이면 비어있는 값
      PkgPath string

      Type  Type  // method type
      Func  Value // func with receiver as first argument
      Index int   // index for Type.Method
	}
    
    NewFilter
    {
    	Name : "Logs",
        PkgPath : "",
        Type : ?,
        Func : Value,
        Index : 0
    }
*/
cb := newCallback(rcvrVal, method.Func)

>> suitableCallbacks >> newCallback
fntype := method.Func.Type()
c := &callback{fn: fn, rcvr: rcvrVal, errPos: -1, isSubscribe: isPubSub(fnttype)}
c.makeArgTypes()

>> suitableCallbacks >> newCallback >> makeArgTypes
fnttype := c.fn.Type()
firstAgr := 0
// context.Context parameter를 제외하는 과정 ~

>> suitableCallbacks >> newCallback 
// callback안 fn의 파라미터에 context.Context가 사라진 상태
// Logs(ctx context.Context, crit FilterCriteria)
// -> Logs(crit FilterCriteria) (*rpc.Subscription, error)

outs := make([]reflect.Type, fntype.NumOut())
// fntype.NumOut() 이용하여 모든 Out 구한다.
// fntype.Out(n) 은 메서드 리턴값의 type을 반환한다.
// outs[0] = fntype.Out(0)  > > rpc.Subscription
// outs[1] = fntype.Out(1) > > error

// API는 리턴값이 두개 이하로 고정되어 있어 보인다.
if len(outs) > 2 {
	return nil
}

// 메서드 리턴값 검증
switch {
case len(outs) == 1 && isErrorType(outs[0]):
	c.errPos = 0
case len(outs) == 2:
	if isErrorType(outs[0]) || !isErrorType(outs[1]) {
		return nil
	}
	c.errPos = 1
}

return c

>> suitableCallbacks
cb := newCallback(receiver, method.Func) // 이 작업이 끝났다.
if cb == nil {
	continue
}
// Logs
name := formatName(method.Name)

>> suitableCallbacks >> formatName
ret := []rune(name)
if len(ret) > 0 {
	ret[0] = unicode.ToLower(ret[0])
}
return string(ret)

>> suitableCallbacks
결과값 name: logs, cb 를 map에 저장

요약해보자면, register한 API service의 모든 메서드를 순회하며 파라미터에 context.Context가 존재하면 없앤 후, 메서드 리턴값이 올바른지 확인한 뒤, 메서드 이름을 소문자로 바꾸고 수정된 함수를 맵 형태에 (methodName, fn) 저장한다.

map[string]*callback
("etherbase", func)
("coinbase", func)
("hashrate", func)

이와 같이 설정된다.
다시 이전 코드로 돌아오자.

//
type serviceRegistry struct {
	mu       sync.Mutex
	services map[string]service
}

func (r *serviceRegistry) registerName(name string, rcvr interface{}) error {
	rcvrVal := reflect.ValueOf(rcvr)
	if name == "" {
		return fmt.Errorf("no service name for type %s", rcvrVal.Type().String())
	}
	callbacks := suitableCallbacks(rcvrVal)
	if len(callbacks) == 0 {
		return fmt.Errorf("service %T doesn't have any suitable methods/subscriptions to expose", rcvr)
	}

	r.mu.Lock()
	defer r.mu.Unlock()
	if r.services == nil {
		r.services = make(map[string]service)
	}
	svc, ok := r.services[name]
	if !ok {
		svc = service{
			name:          name,
			callbacks:     make(map[string]*callback),
			subscriptions: make(map[string]*callback),
		}
		r.services[name] = svc
	}
	for name, cb := range callbacks {
		if cb.isSubscribe {
			svc.subscriptions[name] = cb
		} else {
			svc.callbacks[name] = cb
		}
	}
	return nil
}

callback := suitableCallbacks 리턴값은 map[string]*callback 타입이다.

(eth, debug, admin …)
기존의 serviceRegistry.services에 존재하는 name인지 확인 후, 존재하지 않는다면 메모리를 할당한다. 그 후, 위에서 얻은 callbacks로 채워준다.
callbacks가 아래와 같다면,

map["etherbase"], func)
map["coinbase"], func)
map["hashrate"], func)

r.services[name] = service{
	name:          "etherbase",
    callbacks:     make(map[string]*callback),
    subscriptions: make(map[string]*callback),
}

for name, cb := range callbacks {
	// name == "etherbase"
    // cb == *callback 타입 함수
	if cb.isSubscribe {
    	svc.subscriptions[name] = cb
    } else {
    	svc.callbacks[name] = cb
    }
}

결국 위의 과정은 node안, rpc.Server 속, serviceRegistry.callbacks 을 채워주기 위한 과정이다.

func (r *serviceRegistry) callback(method string) *callback {
	elem := strings.SplitN(method, serviceMethodSeparator, 2)
	if len(elem) != 2 {
		return nil
	}
	r.mu.Lock()
	defer r.mu.Unlock()
	return r.services[elem[0]].callbacks[elem[1]]
}

// call invokes the callback.
func (c *callback) call(ctx context.Context, method string, args []reflect.Value) (res interface{}, errRes error) {
	// Create the argument slice.
	fullargs := make([]reflect.Value, 0, 2+len(args))
	if c.rcvr.IsValid() {
		fullargs = append(fullargs, c.rcvr)
	}
	if c.hasCtx {
		fullargs = append(fullargs, reflect.ValueOf(ctx))
	}
	fullargs = append(fullargs, args...)

	// Catch panic while running the callback.
	defer func() {
		if err := recover(); err != nil {
			const size = 64 << 10
			buf := make([]byte, size)
			buf = buf[:runtime.Stack(buf, false)]
			log.Error("RPC method " + method + " crashed: " + fmt.Sprintf("%v\n%s", err, buf))
			errRes = errors.New("method handler crashed")
		}
	}()
	// Run the callback.
	results := c.fn.Call(fullargs)
	if len(results) == 0 {
		return nil, nil
	}
	if c.errPos >= 0 && !results[c.errPos].IsNil() {
		// Method has returned non-nil error value.
		err := results[c.errPos].Interface().(error)
		return reflect.Value{}, err
	}
	return results[0].Interface(), nil
}

위 serviceRegistry.callback 에서
elem := strings.SplitN(method, serviceMethodSeparator, 2)
예시로 "eth_etherbase" request가 들어오면,
"eth", "etherbase" 로 나뉘게 되고
r.services["eth"].callbacks["etherbase"]

이렇게 구한 callback에 대한 call 메서드를 수행하면 결과값이 나온다.

node에서는 실제 자주 사용하는 eth_getbalance나 eth_gettransactioncount가 등록되지는 않고, etherbase, coinbase 등의 노드 관련 서비스만 등록되었다.

위와 같은 API들은

// go-ethereum/eth/backend.go < 시작점
// go-ethereum/internal/ethapi/backend.go
// go-ethereum/internal/ethapi/api.go 

backend 에서 등록이 되며 전체적인 등록 과정은 위 node에서의 API 추가와 동일하다.


마지막 정리겸 예시 하나.
//PrivateAccountAPI 구조체의 메서드인 
signTransaction(ctx context.Context, args *TransactionArgs, passwd string) (*types.Transaction, error) 

위 메서드는 맵에 "eth" key로 저장되고, 그 value에는 ["signtransaction"] *callback 으로 저장되는데,
callback에는 signTransaction(args
TransactionArgs, passwd string) (*types.Transaction, error)
이와 같이 context를 파라미터에서 제거해서 들어가게 된다.

0개의 댓글