이더리움 주소에 관한 지난 포스팅에서 이더리움 주소는 16진수이며, 공개키 Keccak-256 해시의 마지막 20바이트에서 파생한 식별자라고 했다. 개인키로 시작해서 타원 곡선 곱셈을 이용해 공개키를 만들고, keccak-256을 사용해 이 공개키의 해시를 계산해서 마지막 20바이트만을 취한것이 이더리움 주소였다.
여기서는 이더리움 플랫폼의 데이터 계층에서의 어카운트에 대해 정리하려고 한다.
잘 알다시피 이더리움 플랫폼에서 어카운트(Account)는 모든 트랜잭션의 실행 주체이자 기본 단위로서 모든 것은 어카운트에서 시작한다. 이더리움은 EOA와 CA 두 가지의 어카운트 타입을 갖는데, 여기서는 어카운트 그 자체에 대해 정리할 것이다.
실제 어카운트 주소와 어카운트 정보는 다음의 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
어카운트 정보는 4개의 필드로 구성되어 있다.
서명을 위해 개인키(private key)와 공개기(public key)의 쌍으로 정의되는 비대칭 암호화키를 생성하는데, 이더리움은 265비트 ECDSA 알고리즘을 사용한다. 정확히 말하면 이더리움은 C언어로 작성된 비트코인의 ECDSA 라이브러리인 secp256k1을 Go 언어로 래핑하여 사용한다. 이것은 ECDSA 서명 암호화를 통해 얻은 265비트 공개키를 다시 암호 해시 알고리즘 keccak256을 사용하여 암호화한 후 32바이트의 고정값을 생성해 내고 이 중 20바이트를 절삭하여 어카운트 주소값으로 사용하는 것이다.
"ECDSA는 비대칭 암호 키의 생성 알고리즘이고, Keccak256 임의의 값을 암호화한 후 고정 크기 값을 생성해 내는 해시 함수이다."
🔍 자세히 보기 : EOA 계정 생성 과정
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바이트 주소를 어카운트에 복사
}
어카운트들이 모인 것을 이더리움에서는 상태(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
}