랜덤 변수에서 도메인까지, 블록체인 키와 계정 주소

2024. 11. 14. 21:06Blockchain/Base

728x90
반응형

블록체인에서 사용자 별로 보유중인 코인/토큰 수량을 구분하기 위하여 마치 은행의 계좌번호처럼 주소의 개념을 사용한다. 블록체인 개발자들은 이 주소를 어떠한 데이터를 사용하여 어떻게 표현할 것인가에 대해 많은 고민을 하였다. 사용자가 조금이라도 더 쉽게 자신의 계정을 다루기 위해 고민한 흔적들을 확인해본다.

이 페이지에서는 UTXO를 사용하는 비트코인 등의 플랫폼에서, 잔액을 추적하기 위해 주소가 가변되어 고정되지 않는 경우는 제외하고 이더리움 같은 account 기반 블록체인 플랫폼을 중심으로 설명한다.

Mnemonic 코드

위 그림은 대표적인 블록체인 지갑앱인 메타마스크의 화면이다. 처음 메타마스크에 가입을 하고 자신의 주소를 생성할 때에 위와 같은 페이지를 발견했었을 것이다. '왜 이렇게 많은 문자를 기억하게 함으로써 지갑을 불편하게 생성하도록 하는걸까?'' 라는 물음이 있다면, 이 비밀복구구문은 사용자가 더 쉽게 자신의 주소와 잔액을 볼 수 있도록 하는 수단으로 생성된것이라 답변하고 싶다.

블록체인에서 사용되는 이 비밀복구구문은 Mnemonic code라고 한다. 이는 BIP39에서 블록체인 환경 표준화가 되었고, 대부분의 블록체인 플랫폼들이 키를 생성할 때 이 Mnemonic을 사용하여 키를 복구(recover)한다. 그림을 통해 이 Mnemonic이 어떻게 생성되는지, 이것으로 어떻게 복구를 하는지 확인해보자.

가장 먼저 랜덤하게 생성되는 변수가 존재한다. Entropy라고 불리우는 랜덤 변수를 생성하고 해시를 통한 결과값의 첫 4bit를 checksum으로 붙여 생성한다. 위의 예시는 entropy 데이터 128bits와 checksum 4bits를 합쳐 132bits가 된다. 이 데이터를 11bits씩 잘라서 변수를 만들고, 총 12개로 생성되는 비트값과 BIP39에 기록되어 있는 일반 단어 매핑 정보를 가져와서 총 12개의 mnemonic 코드를 생성한다.

여기서 132 / 11 = 12가 되어 12단어가 생성되어 위의 메타마스크 캡쳐화면에서도 총 12개의 영단어가 확인되는 것이다. 일반적으로 mnemonic 코드는 12~24개의 단어로 구성되는데, 24개의 mnemonic code는 256bits 크기의 entropy 데이터로 생성한 것이며, 그 checksum은 8개가 된다. 따라서 (256+8) / 11 = 24가 되어 총 24개의 단어가 생성되는 것이다.

위 그림에서 예시로 든 00001100000 = army를 실제 BIP39 단어 리스트 상에서 확인해본다.

...

00001100000는 십진수로 96(=32+64)이 된다. 위 그림에서 처럼 index의 시작을 0이 아닌 1로 보았을 때 97번째 단어가 00001100000을 나타내며 이는 "army"임을 확인할 수 있다.

Seed

이렇게 생성된 mnemonic을 사용하여 seed 구문을 생성한다. Mnenomic 코드와 passphrase를 사용한 salt값과 함께 key stretching 함수인 PBKDF2를 사용하여 seed 구문을 생성한다. Key stretching은 해시 함수의 결과값을 다시 해시 함수에 적용시켜 해싱을 반복하는 암호화 기법이다. 해시 round를 총 2048번 반복 적용하고, HMAC-SHA512를 사용하여 최종적으로 512bit의 seed 구문이 생성이 된다.

Master key

BIP32에서 처음 제안된 HD(Hierarchical Deterministic) wallet은 여러개의 비트코인 주소를 이용하여 입출금을 수행하면 더 안전할 것이다 라는 판단하에 제안되었으며, BIP44를 통해 비트코인만이 아닌 여러 플랫폼에서 복수적으로 사용가능하도록 발전했다.

앞서 생성된 seed로 부터 하나의 키에서 여러개의 키로 파생시킬 수 있는 Master key가 도출된다. 이 키를 기반으로 PKI 공개키 암호화에 사용되는 키 쌍을 생성 가능하다.

Private key

Master key에서 부터 private key들이 생성된다. 각각의 private key는 HD 트리 구조를 통해 다양하게 생성된다. HD 트리는 아래와 같다.

m / purpose' / coin_type' / account' / change / address_index
  • 레벨 1: purpose는 BIP44 표준을 나타낼 수 있도록 44로 설정한다.
  • 레벨 2: coin_typeSLIP44로 지정된 각 코인 별 type을 나타낸다. 예를 들어, 비트코인 BIT는 1, 이더리움 ETH는 60, 코스모스 ATOM은 118을 가진다.
  • 레벨 3: account는 각 계정을 이루는 키들의 root key로 볼 수 있다.
  • 레벨 4: change는 잔돈 계정 여부이다. BIP44는 원래 비트코인을 위해 제작되어서 이더리움 생태계와 관련 없는 '특이정'(quirk)이 포함되었다. 여기서 살펴볼 account 기반은 UTXO처럼 잔액 주소가 필요 없으므로 입금되는 경로만 사용하여 0으로 지정한다.
  • 레벨 5: address_index는 사용가능한 주소 인덱스로 만드는 것이다. 대부분의 지갑은 회원 가입 후 하나의 키를 발급할 때, address_index를 0으로 설정하고 생성한다.

이렇게 BIP44를 따라 HD 트리를 HD derivation path(HD 파생 경로)라고도 하며, 이 path로 각기 다른 private key가 생성된다. 이더리움을 위한 메타마스크 생성 시 일반적으로 HD path는 m/44'/60'/0'/0/0으로 생성된다.

위 그림은 여기의 캡처화면이며, HD key generator를 확인해주는 재미있는 사이트가 존재한다. 보면, BIP39 mnemonic을 입력하여 HD path를 넣으면 path에 따라 각기 다른 private/public key가 생성되는 것을 확인할 수 있다.

Public key

공개키(public key) 암호 방식은 이름 그대로 공개키를 이용한 암호 방식인데, 이는 비대칭 암호로써 private key와 public key가 한 쌍으로 존재한다. Public key는 모두가 알고 있는 키이고 private key는 해당키의 소유주만 알고 있는 키이다. 만약 평문을 public key로 암호화하면 해당 키의 소유주만 private key로 복호화 가능한 '공개키 암호' 방식이 되며, 평문을 소유주만 알고 있는 private key로 암호화 하면 모두가 알고 있는 public key로 복호화 가능하여, 암호화된 데이터를 만든 사람이 private key의 소유주임을 증명하는 '공개키 서명' 방식이 된다.

앞서 생성된 private key와 한 쌍으로 public key가 생성이 되며, 비트코인/이더리움 등 대다수의 블록체인이 ECDSA secp256k1 타원곡선 암호 알고리즘을 사용하여 private key와 public key를 생성한다.

Address

블록체인에서의 주소는 이 public key를 기반으로 생성된다. 이더리움은 공개키를 keccak256 해시함수에 넣은 결과값을 뒷자리 20bytes만 남기고 hex값으로 인코딩한 결과가 주소가 된다. 아래 Geth에서의 코드를 보자.

// github.com/ethereum/go-ethereum/crypto/crypto.go

func PubkeyToAddress(p ecdsa.PublicKey) common.Address {
    pubBytes := FromECDSAPub(&p)
    return common.BytesToAddress(Keccak256(pubBytes[1:])[12:])
}

ECDSA public key를 입력으로 받고 FromECDSAPub() 함수를 통해 public key를 검증한 뒤 이를 bytes 타입으로 변경한다. Keccak256() 해시 함수에 적용하면, 32bytes의 해시 결과값이 생성되고 앞은 잘라서 ([12:] - 12 index부터 31 index까지) 뒤의 20bytes만 남기고 hex 인코딩을 실시한다.

위는 메타마스크로 회원가입을 완료하면 확인되는 화면이다. 0x69EE...인 private key가 생성되고, 0xB383...인 public key를 기반으로 0xbeFD...인 주소가 최종적으로 만들어 진다.

Cosmos 같이 Bech32(BIP173)를 사용하는 경우는 주소 생성이 조금 달라진다. 비트코인에서 사용되는 Bech32 방식은 public key를 SHA-256 해시함수를 적용하고, 이 해시값를 input에 대해 160bit로 압축하는 해시함수인 RIPEMD160에 다시 해싱을 하여 도출된 결과를 주소의 payload로 설정한다.

Bech32에는 HRP(Human Redable Part)가 존재한다. 바로 주소 앞에 붙는 prefix인데, 비트코인은 bc, 코스모스는 cosmos로 prefix가 붙어있어 이 주소가 어떤 블록체인에 사용되는지 쉽게 확인할 수 있다. 이 HRP는 payload에 prefix로 붙고, HRP와 payload를 구분짓기 위하여 중간에 숫자 1을 넣어 seperate 시킨다. 최종적으로 이 데이터를 이용하여 6개의 checksum문자를 계산하여 postfix로 붙인다.

비트코인을 예로 들면, bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5 라는 주소가 있을 시, bc는 HRP, 1은 seperator, qw508d6qejxtdg4y5r3zarvary0c5xw7k는 payload, v8f3t5는 checksum이라 볼 수 있다.

Cosmos-SDK를 살펴보자.

// github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1/secp256k1.go

// Address returns a Bitcoin style addresses: RIPEMD160(SHA256(pubkey))
func (pubKey *PubKey) Address() crypto.Address {
    if len(pubKey.Key) != PubKeySize {
        panic("length of pubkey is incorrect")
    }

    sha := sha256.Sum256(pubKey.Key)
    hasherRIPEMD160 := ripemd160.New()
    hasherRIPEMD160.Write(sha[:]) // does not error
    return crypto.Address(hasherRIPEMD160.Sum(nil))
}

위 코드처럼 public key bytes를 SHA256 해시함수를 적용하고, 다시 그 결과값을 RIPEMD160을 적용하여 payload를 생성하는 것을 확인할 수 있다.

// 1. Generate Cosmos-SDK style address
addr := sdk.AccAddress(pubKey.Address())


// 2. Convert to 'string' type

// github.com/cosmos/cosmos-sdk/types/address.go
func (aa AccAddress) String() string {
    if aa.Empty() {
        return ""
    }

    key := conv.UnsafeBytesToStr(aa)

    if IsAddrCacheEnabled() {
        accAddrMu.Lock()
        defer accAddrMu.Unlock()

        addr, ok := accAddrCache.Get(key)
        if ok {
            return addr.(string)
        }
    }
    return cacheBech32Addr(GetConfig().GetBech32AccountAddrPrefix(), aa, accAddrCache, key)
}

// github.com/cosmos/cosmos-sdk/types/address.go
func cacheBech32Addr(prefix string, addr []byte, cache *simplelru.LRU, cacheKey string) string {
    bech32Addr, err := bech32.ConvertAndEncode(prefix, addr)
    if err != nil {
        panic(err)
    }
    if IsAddrCacheEnabled() {
        cache.Add(cacheKey, bech32Addr)
    }
    return bech32Addr
}

// github.com/cosmos/cosmos-sdk/types/bech32/bech32.go
func ConvertAndEncode(hrp string, data []byte) (string, error) {
    converted, err := bech32.ConvertBits(data, 8, 5, true)
    if err != nil {
        return "", fmt.Errorf("encoding bech32 failed: %w", err)
    }

    return bech32.Encode(hrp, converted)
}

pubKey.Address()를 통해 Cosmos-SDK의 address 타입인 AccAddress로 변환한 후에 AccAddress.String()을 실행하면 public key bytes가 Bech32 형태로 변환되는 것을 확인할 수 있다.

Name service

이렇게 생성된 hex 타입 또는 Bech32 타입 주소도 어렵다라는 의견이 존재하기 때문에 만들어진 서비스가 바로 name service이다. 마치 인터넷의 IP address가 외우기 어려워서 사람이 인지하기 쉬운 도메인으로 변경하는 DNS(Domain Name Service)가 생겨난 것과 유사하다. 이더리움에서는 대표적으로 ENS(Ethereum Name Service)가 존재한다. ENS는 이전에 게시한 자료에서 조금 더 상세히 확인가능하다.

위 그림은 이더리움의 창시자 Vitalik Vuterin의 ENS 주소이다. DNS와 마찬가지로 도메인의 형태를 가지며 ENS의 top 도메인은 .eth이다. 이 ENS는 각각이 NFT이며, expired 되기 때문에 계속 도메인을 유지하려면 만료되기 전에 연장을 진행해야 한다. vitalik.eth의 도메인의 하위 도메인(e.g. abc.vitalik.eth)들은 도메인 소유자가 임의로 사용할 수 있다.

이러한 ENS로 생성된 도메인은 위 그림 처럼 etherscan에서 지원하여 쉽게 자신의 주소를 알아볼 수 있거나, 메타마스크 등의 지갑서비스에서 to address를 지정할 때 기억하기 쉬운 도메인으로 사용성을 높일 수 있다.

Conclusion

이번 페이지에서 설명한 것들은 블록체인을 어떻게 하면 사용자가 더 쉽게 사용할 수 있을까 하는 개발자들의 고민이 담겼다고 볼 수 있을것이다. 외우기 어려운 seed 구문, private key 대신 mnemonic을 사용하여 연상하거나 복구하기 쉽고 잘못된 오류를 금방 찾아낼 수 있도록 하는것, 파생키를 통하여 한 사용자가 여러 계정을 편하게 다룰 수 있는것, 계정 주소를 쉽게 다룰 수 있게 하는것 등등이 바로 그것이다.

거래소 같이 일반인들이 많이 사용하는 블록체인 시스템은 확실히 UI/UX 적으로 좋아진것을 실감할 수 있다. 기존에 있던 주식앱과 큰 차이를 못느낄 정도이니까. 하지만 그외의 다른 서비스들은 아직까지 일반 사용자나 다른 분야의 개발자들이 접근하기에는 어려운 점이 존재한다. 성능 향상, 신기술 도입 등 기술적인 개선이 매우 필수적인 것은 분명하지만 서비스의 성패를 좌우하는것에는 일반 사용자의 접근성 향상이 큰 역할을 할 것으로 생각한다. 블록체인 서비스 개발 시에는 이러한 관점을 항상 기억해야 겠다는 생각이 든다.

참고
https://github.com/bitcoin/bips/tree/master/bip-0039
https://wiki1.kr/index.php/BIP44
https://en.bitcoin.it/wiki/Bech32

728x90
반응형

'Blockchain > Base' 카테고리의 다른 글

블록체인 기반 인증 서비스, DID  (1) 2023.12.18
Gossip protocol on Blockchain  (0) 2023.06.15
Blockchain에서의 Anchoring 기법  (0) 2023.06.15
2-way Peg concept  (0) 2023.06.14