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를 파라미터에서 제거해서 들어가게 된다.
끝