소스코드 출처:
https://github.com/NAKsir-melody/go-ethereum-korean
주의사항 : 현재의 Geth코드는 2023년 기준 업데이트가 많이 됐으므로 위 자료는 4년전 자료입니다.
해당 내용을 참고하시면서 보시면 됩니다.
Geth 한글주석 프로젝트를 통해 소스코드에서 각 함수와 변수, 구조체의 역할을 정리하고 소스코드를 공부하기 위해 포스팅했습니다. 전체적으로 database.go는 node에 대한 정보를 db화해서 연결한 node의 정보를 node db에 넣거나 또는 삭제할 node의 정보를 node db에서 삭제하는 등의 작업을 하며 각각 연결된 노드의 시간등을 업데이트하며 관리하는 코드로 이해했습니다.
다음과 같은 요소의 변수가 설정되어있다. 전체적으로 연결되는 노드를 업데이트하고 관리하기 위한 변수임을 알 수 있다.
null원소로 사용할 노드 아이디,
노드DB 유효시간,
유효시간을 초과해서 실행할 시간단위
var (
nodeDBNilNodeID = NodeID{} // Special node ID to use as a nil element.
// nil원소로서 사용할 특수한 노드 ID
nodeDBNodeExpiration = 24 * time.Hour // Time after which an unseen node should be dropped.
// 특정시간동안 보이지 않는 노드는 드롭되어야 한다
nodeDBCleanupCycle = time.Hour // Time period for running the expiration task.
// 초과 업무를 실행할 시간단위
)
노드DB란 geth가 알고있는 모든 노드를 저장하는 데이터베이스 역할을 하는 스트럭트 구조체다.
type nodeDB struct {
lvl *leveldb.DB // Interface to the database itself
// db 자체 인터페이스
self NodeID // Own node id to prevent adding it into the database
// DB에 추가하는 것을 막기위한 자기의 node ID
runner sync.Once // Ensures we can start at most one expirer
// 하나가 끝났을때만 실행가능한것을 보장함
quit chan struct{} // Channel to signal the expiring thread to stop
// 스레드를 종료시키기 위한 시그널을 전송할 채널
}
조건문을 보면 path가 비어있으면 newMemoryDB 함수 실행
return 값으로는 newPersistentNodeDB 함수를 return하고 있다.
func newNodeDB(path string, version int, self NodeID) (*nodeDB, error) {
if path == "" {
return newMemoryNodeDB(self)
}
return newPersistentNodeDB(path, version, self)
}
db 변수에 leveldb의 함수를 이용해서 스토리지에 저장한뒤
nodeDB 구조체에 데이터를 반환하는 것으로 보인다.
func newMemoryNodeDB(self NodeID) (*nodeDB, error) {
db, err := leveldb.Open(storage.NewMemStorage(), nil)
if err != nil {
return nil, err
}
return &nodeDB{
lvl: db,
self: self,
quit: make(chan struct{}),
}, nil
}
func newPersistentNodeDB(path string, version int, self NodeID) (*nodeDB, error) {
opts := &opt.Options{OpenFilesCacheCapacity: 5}
db, err := leveldb.OpenFile(path, opts)
if _, iscorrupted := err.(*errors.ErrCorrupted); iscorrupted {
db, err = leveldb.RecoverFile(path, nil)
}
if err != nil {
return nil, err
}
// The nodes contained in the cache correspond to a certain protocol version.
// Flush all nodes if the version doesn't match.
// 노드들은 특정 프로토콜 버전과 연관된 캐시에 적재된다
// 버전이 맞지 않을 경우 모든 노드를 플러시한다
currentVer := make([]byte, binary.MaxVarintLen64)
currentVer = currentVer[:binary.PutVarint(currentVer, int64(version))]
blob, err := db.Get(nodeDBVersionKey, nil)
switch err {
case leveldb.ErrNotFound:
// Version not found (i.e. empty cache), insert it
// 버전이 찾아지지 않음. 삽입
if err := db.Put(nodeDBVersionKey, currentVer, nil); err != nil {
db.Close()
return nil, err
}
case nil:
// Version present, flush if different
// 버전이 존재. 다를경우 플러시
if !bytes.Equal(blob, currentVer) {
db.Close()
if err = os.RemoveAll(path); err != nil {
return nil, err
}
return newPersistentNodeDB(path, version, self)
}
}
return &nodeDB{
lvl: db,
self: self,
quit: make(chan struct{}),
}, nil
}
func makeKey(id NodeID, field string) []byte {
if bytes.Equal(id[:], nodeDBNilNodeID[:]) {
return []byte(field)
}
return append(nodeDBItemPrefix, append(id[:], field...)...)
}
func splitKey(key []byte) (id NodeID, field string) {
// If the key is not of a node, return it plainly
// 키가 노드가 아닐경우 순수하게 리턴한다
if !bytes.HasPrefix(key, nodeDBItemPrefix) {
return NodeID{}, string(key)
}
// Otherwise split the id and field
// 아니라면 id와 필드를 분리한다
item := key[len(nodeDBItemPrefix):]
copy(id[:], item[:len(id)])
field = string(item[len(id):])
return id, field
}
func (db *nodeDB) fetchInt64(key []byte) int64 {
blob, err := db.lvl.Get(key, nil)
if err != nil {
return 0
}
val, read := binary.Varint(blob)
if read <= 0 {
return 0
}
return val
}
func (db *nodeDB) storeInt64(key []byte, n int64) error {
blob := make([]byte, binary.MaxVarintLen64)
blob = blob[:binary.PutVarint(blob, n)]
return db.lvl.Put(key, blob, nil)
}
func (db *nodeDB) node(id NodeID) *Node {
blob, err := db.lvl.Get(makeKey(id, nodeDBDiscoverRoot), nil)
if err != nil {
return nil
}
node := new(Node)
if err := rlp.DecodeBytes(blob, node); err != nil {
log.Error("Failed to decode node RLP", "err", err)
return nil
}
node.sha = crypto.Keccak256Hash(node.ID[:])
return node
}
DB에서 UPDATE쿼리와 유사한 것으로 보인다.
func (db *nodeDB) updateNode(node *Node) error {
blob, err := rlp.EncodeToBytes(node)
if err != nil {
return err
}
return db.lvl.Put(makeKey(node.ID, nodeDBDiscoverRoot), blob, nil)
}
DB에서 Delete쿼리와 유사한 것으로 보인다.
func (db *nodeDB) deleteNode(id NodeID) error {
deleter := db.lvl.NewIterator(util.BytesPrefix(makeKey(id, "")), nil)
for deleter.Next() {
if err := db.lvl.Delete(deleter.Key(), nil); err != nil {
return err
}
}
return nil
}
func (db *nodeDB) ensureExpirer() {
db.runner.Do(func() { go db.expirer() })
}
func (db *nodeDB) expirer() {
tick := time.NewTicker(nodeDBCleanupCycle)
defer tick.Stop()
for {
select {
case <-tick.C:
if err := db.expireNodes(); err != nil {
log.Error("Failed to expire nodedb items", "err", err)
}
case <-db.quit:
return
}
}
}
func (db *nodeDB) expireNodes() error {
threshold := time.Now().Add(-nodeDBNodeExpiration)
// Find discovered nodes that are older than the allowance
// 허용치보다 오래된 발견된 노드를 찾는다
it := db.lvl.NewIterator(nil, nil)
defer it.Release()
for it.Next() {
// Skip the item if not a discovery node
// 디스커버리 노드가 아니면 스킵
id, field := splitKey(it.Key())
if field != nodeDBDiscoverRoot {
continue
}
// Skip the node if not expired yet (and not self)
// 아직 시간초과 되지 않았다면 스킵
if !bytes.Equal(id[:], db.self[:]) {
if seen := db.bondTime(id); seen.After(threshold) {
continue
}
}
// Otherwise delete all associated information
// 아니라면 관련된 모든정보를 삭제한다
db.deleteNode(id)
}
return nil
}
func (db *nodeDB) lastPing(id NodeID) time.Time {
return time.Unix(db.fetchInt64(makeKey(id, nodeDBDiscoverPing)), 0)
}
func (db *nodeDB) updateLastPing(id NodeID, instance time.Time) error {
return db.storeInt64(makeKey(id, nodeDBDiscoverPing), instance.Unix())
}
func (db *nodeDB) bondTime(id NodeID) time.Time {
return time.Unix(db.fetchInt64(makeKey(id, nodeDBDiscoverPong)), 0)
}
func (db *nodeDB) hasBond(id NodeID) bool {
return time.Since(db.bondTime(id)) < nodeDBNodeExpiration
}
func (db *nodeDB) updateBondTime(id NodeID, instance time.Time) error {
return db.storeInt64(makeKey(id, nodeDBDiscoverPong), instance.Unix())
}
func (db *nodeDB) findFails(id NodeID) int {
return int(db.fetchInt64(makeKey(id, nodeDBDiscoverFindFails)))
}
func (db *nodeDB) updateFindFails(id NodeID, fails int) error {
return db.storeInt64(makeKey(id, nodeDBDiscoverFindFails), int64(fails))
}
func (db *nodeDB) querySeeds(n int, maxAge time.Duration) []*Node {
var (
now = time.Now()
nodes = make([]*Node, 0, n)
it = db.lvl.NewIterator(nil, nil)
id NodeID
)
defer it.Release()
seek:
for seeks := 0; len(nodes) < n && seeks < n*5; seeks++ {
// Seek to a random entry. The first byte is incremented by a
// random amount each time in order to increase the likelihood
// of hitting all existing nodes in very small databases.
// 랜덤 엔트리를 위한 검색
// 첫바이트는 작은 db에 존재하는 모든 노드를 히트할 확률을 높이기 위해
// 매시간 랜덤량으로 증가된다
ctr := id[0]
rand.Read(id[:])
id[0] = ctr + id[0]%16
it.Seek(makeKey(id, nodeDBDiscoverRoot))
n := nextNode(it)
if n == nil {
id[0] = 0
continue seek // iterator exhausted
// 반복자 종료
}
if n.ID == db.self {
continue seek
}
if now.Sub(db.bondTime(n.ID)) > maxAge {
continue seek
}
for i := range nodes {
if nodes[i].ID == n.ID {
continue seek // duplicate
// 중복
}
}
nodes = append(nodes, n)
}
return nodes
}
func nextNode(it iterator.Iterator) *Node {
for end := false; !end; end = !it.Next() {
id, field := splitKey(it.Key())
if field != nodeDBDiscoverRoot {
continue
}
var n Node
if err := rlp.DecodeBytes(it.Value(), &n); err != nil {
log.Warn("Failed to decode node RLP", "id", id, "err", err)
continue
}
return &n
}
return nil
}
func (db *nodeDB) close() {
close(db.quit)
db.lvl.Close()
}