이더리움 주소에 관한 지난 포스팅에서 이더리움 주소는 16진수이며, 공개키 Keccak-256 해시의 마지막 20바이트에서 파생한 식별자라고 했다. 개인키로 시작해서 타원 곡선 곱셈을 이용해 공개키를 만들고, keccak-256을 사용해 이 공개키의 해시를 계산해서 마지막 20바이트만을 취한것이 이더리움 주소였다.

  • 20바이트만 취한것
    676e80bC3E12924f220dC7064a89b04D570bd749
  • 최종 이더리움 주소
    0x676e80bC3E12924f220dC7064a89b04D570bd749

여기서는 이더리움 플랫폼의 데이터 계층에서의 어카운트에 대해 정리하려고 한다.
잘 알다시피 이더리움 플랫폼에서 어카운트(Account)는 모든 트랜잭션의 실행 주체이자 기본 단위로서 모든 것은 어카운트에서 시작한다. 이더리움은 EOA와 CA 두 가지의 어카운트 타입을 갖는데, 여기서는 어카운트 그 자체에 대해 정리할 것이다.

Account 주소

실제 어카운트 주소와 어카운트 정보는 다음의 Account 구조체에 저장된다.

//패키지 : accounts, 파일명 : accounts.go

// Account represents an Ethereum account located at a specific location defined
// by the optional URL field.
type Account struct {
	Address common.Address `json:"address"` // Ethereum account address derived from the key
	URL     URL            `json:"url"`     // Optional resource locator within a backend
}
//패키지 : Common, 파일명 : Types.go

// AddressLength is the expected length of the address
const AddressLength = 20

...

// Address represents the 20 byte address of an Ethereum account.
type Address [AddressLength]byte

Account 정보

어카운트 정보는 4개의 필드로 구성되어 있다.

  • 넌스(nonce) : 해당 어카운트가 보낸 트랜잭션의 수를 의미하며 0부터 시작.
    트랜잭션이 무제한 실행될 때는 의미가 없으나 트랜잭션을 한 번만 실행되게 할 때 사용할 수 있는 카운터. (CA라면 넌스틑 어카운트에 의해 생성된 컨트랙트의 수)
  • 잔액(balance) : 해당 어카운트의 이더 잔고(wei)
  • 루트(root) : 해당 어카운트가 저장될 머클 패트리시아 트리의 루트 노드. 실제 어카운트의 저장소는 머클 패트리시아 트리에 저장되는데, 이 트리의 루트 노드를 암호 해시화 한것이 바로 루트(root). 암호 해시는 keccak256을 사용
  • 코드해시(codeHash) : 해당 어카운트의 스마트 컨트랙트 바이트 코드의 해시. 코드 해시 값이 비어있으면 해당 어카운트는 일반 EOA이고, 컨트랙트 어카운트가 아니라는 의미

Account 생성

서명을 위해 개인키(private key)와 공개기(public key)의 쌍으로 정의되는 비대칭 암호화키를 생성하는데, 이더리움은 265비트 ECDSA 알고리즘을 사용한다. 정확히 말하면 이더리움은 C언어로 작성된 비트코인의 ECDSA 라이브러리인 secp256k1을 Go 언어로 래핑하여 사용한다. 이것은 ECDSA 서명 암호화를 통해 얻은 265비트 공개키를 다시 암호 해시 알고리즘 keccak256을 사용하여 암호화한 후 32바이트의 고정값을 생성해 내고 이 중 20바이트를 절삭하여 어카운트 주소값으로 사용하는 것이다.

"ECDSA는 비대칭 암호 키의 생성 알고리즘이고, Keccak256 임의의 값을 암호화한 후 고정 크기 값을 생성해 내는 해시 함수이다."
🔍 자세히 보기 : EOA 계정 생성 과정

Accounts 패키지 : Account 생성

Accounts/keyStore 패키지는 어카운트 키의 저장 디렉터리의 관리를 담당한다.
NewAccount()함수는 암호화를 하기 위한 키값을 변수 passphrase로 전달 받고, storeNewKey()함수를 호출한다.
newKey()함수를 호출하여 임의의 문자열로 키를 생성하고 이를 저장한다.

NewAccount() -> storeNewKey() -> newKey()

//패키지 : Accounts/Keystore, 파일명 : keystore.go

// KeyStore manages a key storage directory on disk.
type KeyStore struct {
	storage  keyStore                     // Storage backend, might be cleartext or encrypted
	cache    *accountCache                // In-memory account cache over the filesystem storage
	changes  chan struct{}                // Channel receiving change notifications from the cache
	unlocked map[common.Address]*unlocked // Currently unlocked account (decrypted private keys)

	wallets     []accounts.Wallet       // Wallet wrappers around the individual key files
	updateFeed  event.Feed              // Event feed to notify wallet additions/removals
	updateScope event.SubscriptionScope // Subscription scope tracking current live listeners
	updating    bool                    // Whether the event notification loop is running

	mu       sync.RWMutex
	importMu sync.Mutex // Import Mutex locks the import to prevent two insertions from racing
}

...

// NewAccount generates a new key and stores it into the key directory,
// encrypting it with the passphrase.
// 암호화를 위한 키값을 변수 passphrase로 전달 받아서 storeNewKey()를 호출
func (ks *KeyStore) NewAccount(passphrase string) (accounts.Account, error) {
	_, account, err := storeNewKey(ks.storage, crand.Reader, passphrase)
	if err != nil {
		return accounts.Account{}, err
	}
	// Add the account to the cache immediately rather
	// than waiting for file system notifications to pick it up.
	ks.cache.add(account)
	ks.refreshWallets()
	return account, nil
}
//패키지 : Accounts/Keystore, 파일명 : key.go

// newKey()를 호출하여 키 생성
func storeNewKey(ks keyStore, rand io.Reader, auth string) (*Key, accounts.Account, error) {
	key, err := newKey(rand)
	if err != nil {
		return nil, accounts.Account{}, err
	}
	a := accounts.Account{
		Address: key.Address,
		URL:     accounts.URL{Scheme: KeyStoreScheme, Path: ks.JoinPath(keyFileName(key.Address))},
	}
	if err := ks.StoreKey(a.URL.Path, key, auth); err != nil {
		zeroKey(key.PrivateKey)
		return nil, a, err
	}
	return key, a, err
}

newKey()함수로 내부에서 cypto 패키지의 S256()함수와 임의의 문자열을 매개변수로 ecdsa.GenerateKey()함수를 호출하여 임의의 256비트의 개인키를 생성한다. 그리고 이 개인키로 공개키를 생성하기 위해 newKeyFromECDSA()함수를 호출한다.

//패키지 : Accounts/Keystore, 파일명 : key.go

func newKey(rand io.Reader) (*Key, error) {
	privateKeyECDSA, err := ecdsa.GenerateKey(crypto.S256(), rand) // 개인키 생성
	if err != nil {
		return nil, err
	}
	return newKeyFromECDSA(privateKeyECDSA), nil // 개인키를 통해 공개키 생성
}
...

func newKeyFromECDSA(privateKeyECDSA *ecdsa.PrivateKey) *Key {
	id, err := uuid.NewRandom()
	if err != nil {
		panic(fmt.Sprintf("Could not create random uuid: %v", err))
	}
	key := &Key{
		Id:         id,
		Address:    crypto.PubkeyToAddress(privateKeyECDSA.PublicKey), //UUID생성 후 key 구조체 포인터 반환
		PrivateKey: privateKeyECDSA,
	}
	return key
}

newKeyFromECDSA()함수 내에서 PubkeyToAddress()함수를 호출하여 128비트 UUID를 생성한 후에 UUID와 바이트 타입의 Address와 PrivateKey로 구성된 Key 구조체의 포인터를 반환한다.
PubkeyToAddress()함수는 Pubkey를 받은 후 Keccak256 암호 해시한 후 BytesToAddress()함수를 통해 뒷부분 20 바이트만을 최종 어카운트로 잘라서 반환한다.

//패키지 : crypto, 파일명 : cryto.go

func PubkeyToAddress(p ecdsa.PublicKey) common.Address {
	pubBytes := FromECDSAPub(&p)
	return common.BytesToAddress(Keccak256(pubBytes[1:])[12:]) // Pubkey를 받은 후 해시화 한 것을 20바이트만 절삭
}
//패키지 : common, 파일명 : types.go

// Address represents the 20 byte address of an Ethereum account.
type Address [AddressLength]byte

// BytesToAddress returns Address with value b.
// If b is larger than len(h), b will be cropped from the left.
func BytesToAddress(b []byte) Address { 
	var a Address
	a.SetBytes(b) 
	return a
}
...

// SetBytes sets the address to the value of b.
// If b is larger than len(a), b will be cropped from the left.
func (a *Address) SetBytes(b []byte) { // b에 배열 a의 어카운트 값을 설정
	if len(b) > len(a) {
		b = b[len(b)-AddressLength:]
	}
	copy(a[AddressLength-len(b):], b) // 20바이트 주소를 어카운트에 복사
}

Account 상태

어카운트들이 모인 것을 이더리움에서는 상태(state)라고 하고 이를 stateObject 구조체로 표현한다. 어카운트에 접근하여 상태를 변경하려면 stateObject를 통해 접근한 후 상태를 변경할 수 있다. 변경된 어카운트는 CommitTrie()함수를 호출하여 변경된 Trie를 ethdb 패키지를 통해 레벨DB에 업데이트 한다.

//패키지 : core/state, 파일명 : state_object.go

// stateObject represents an Ethereum account which is being modified.
//
// The usage pattern is as follows:
// First you need to obtain a state object.
// Account values can be accessed and modified through the object.
// Finally, call commitTrie to write the modified storage trie into a database.
type stateObject struct {
	address  common.Address // 어드레스
	addrHash common.Hash // hash of ethereum address of the account 어카운트 주소의 keccak256해시
	data     types.StateAccount 이더리움 어카운트
	db       *StateDB //상태를 저장할 DBMS에 대한 포인터

	// Write caches. 상태값으로 필요한 데이터의 임시 저장 캐시
	trie Trie // storage trie, which becomes non-nil on first access. Trie 저장소
	code Code // contract bytecode, which gets set when code is loaded. 컨트랙트의 바이트 코드

	originStorage  Storage // Storage cache of original entries to dedup rewrites, reset for every transaction
	pendingStorage Storage // Storage entries that need to be flushed to disk, at the end of an entire block
	dirtyStorage   Storage // Storage entries that have been modified in the current transaction execution

	// Cache flags.
	// When an object is marked suicided it will be deleted from the trie
	// during the "update" phase of the state transition.
	dirtyCode bool // true if the code was updated
	suicided  bool
	deleted   bool
}
profile
내가 떠나기 전까지는 망하지 마라, 블록체인 개발자

0개의 댓글