5. Wallet

Building a basic wallet module

간단한 지갑 모듈을 만들건데 블록체인과는 분리해서 만들고 다음 강좌에서 합치려고 한다.

현재는 지갑의 생성, 추가가 모두 간단하게 이루어지고 그 자체로는 의미가 없다.

지갑에서 public key, private key 의 생성, Address를 만드는 과정 등에 집중해서 살펴보자.

공개키 암호화

  • 앨리스는 개인키와 공개키를 생성하고 공개키를 밥에게 전달해야 한다.

  • 밥은 앨리스의 공개키로 메시지를 암호화 하고 앨리스에게 보낸다.

  • 암호화된 메시지를 받은 앨리스는 자신의 개인키로 메시지를 해독한다.

  • 공개키에서는 개인키를 만들 수 없으므로, 앨리스의 공개키로 암호화된 메시지를 읽을 수 있는 것은 대응되는 개인키를 가진 앨리스뿐이다.

  • ECDSA(Elliptic Curve Digital Signature Algorithm) 타원곡선암호는 비트코인, 이더리움 드에서 거래 시 정당한 소유주만이 자금을 쓸 수 있도록 하는 암호 알고리즘이다.

wallet.go

  • 위의 그림처럼 Private key로부터 Public key를 뽑아내고 그들로부터 Address가 나온다.

  • address는 위 3개의 그림과 같이 public key부터 많은 과정을 거쳐서 만들어진다.

  • 최종적으로는 version, checksum, public key hash 를 합쳐서 base 58로 인코딩해서 만들어진다.

  • 아래 코드는 지갑의 키페어 생성, 주소 생성 등의 메소드를 구현해놓았다.

package wallet

import (
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/sha256"
	"fmt"
	"log"

	"golang.org/x/crypto/ripemd160"
)

const (
	// checksum length in byte.
	// 4byte length
	checksumLength = 4
	// 0 byte를 16진법으로 표현.
	version = byte(0x00)
)

// Wallet : PrivateKey와 PublicKey를 가지고 있는 Wallet structure.
type Wallet struct {
	PrivateKey ecdsa.PrivateKey
	PublicKey  []byte
}

// Address : version, publicKeyHash, checksum 3가지를 concatenate한 후에 Base58로 인코딩해서 address를 만든다.
func (w Wallet) Address() []byte {
	pubHash := PublicKeyHash(w.PublicKey)

	versionedHash := append([]byte{version}, pubHash...)
	checksum := Checksum(versionedHash)

	fullHash := append(versionedHash, checksum...)
	address := Base58Encode(fullHash)

	fmt.Printf("pub key: %x\n", w.PublicKey)
	fmt.Printf("pub hash: %x\n", pubHash)
	fmt.Printf("address: %s\n", address)

	return address
}

// NewKeyPair : 새로운 키페어를 만든다.
// ecdsa라는 비대칭키 암호 알고리즘을 사용.
func NewKeyPair() (ecdsa.PrivateKey, []byte) {
	curve := elliptic.P256()

	private, err := ecdsa.GenerateKey(curve, rand.Reader)
	if err != nil {
		log.Panic(err)
	}

	// X, Y가 concatenate 되어 pub이 됨.
	pub := append(private.PublicKey.X.Bytes(), private.PublicKey.Y.Bytes()...)
	return *private, pub
}

// MakeWallet : NewKeyPair를 이용해서 Wallet을 만든다.
func MakeWallet() *Wallet {
	private, public := NewKeyPair()
	wallet := Wallet{private, public}

	return &wallet
}

// PublicKeyHash : sha256, ripemd160을 이용해 퍼블릭키를 해쉬로 변환.
// address 생성에 쓰임.
func PublicKeyHash(pubKey []byte) []byte {
	pubHash := sha256.Sum256(pubKey)

	hasher := ripemd160.New()
	_, err := hasher.Write(pubHash[:])
	if err != nil {
		log.Panic(err)
	}

	publicRipMD := hasher.Sum(nil)

	return publicRipMD
}

// Checksum : payload를 sha256을 이용해 해쉬로 변환하고 checksumLength만큼의 바이트만 사용한다.
func Checksum(payload []byte) []byte {
	firstHash := sha256.Sum256(payload)
	secondHash := sha256.Sum256(firstHash[:])

	return secondHash[:checksumLength]
}

wallets.go

  • 지갑을 map의 형태로 badgerDB에 저장한다. key : address, value : Wallet의 주소값

  • 지갑의 생성, 추가, 저장 등의 메소드를 구현했다.

package wallet

import (
	"bytes"
	"crypto/elliptic"
	"encoding/gob"
	"fmt"
	"io/ioutil"
	"log"
	"os"
)

const walletFile = "./tmp/wallets.data"

// Wallets : map형태로 badgerDB에 저장.
// key : address
// value : Wallet의 주소값
type Wallets struct {
	Wallets map[string]*Wallet
}

// CreateWallets : Wallets를 생성한다.
// Wallets 껍데기를 만들고 그 안에 기존 Wallets를 로드한다.
func CreateWallets() (*Wallets, error) {
	wallets := Wallets{}
	wallets.Wallets = make(map[string]*Wallet)

	err := wallets.LoadFile()

	return &wallets, err
}

// AddWallet : Wallet을 추가한다.
func (ws *Wallets) AddWallet() string {
	wallet := MakeWallet()
	// byte를 string으로 변환.
	address := fmt.Sprintf("%s", wallet.Address())

	ws.Wallets[address] = wallet

	return address
}

// GetAllAddresses : Wallet 안의 모든 주소들을 반환.
func (ws *Wallets) GetAllAddresses() []string {
	var addresses []string

	for address := range ws.Wallets {
		addresses = append(addresses, address)
	}

	return addresses
}

// GetWallet : map 이어서 접근하기 쉬움.
func (ws Wallets) GetWallet(address string) Wallet {
	return *ws.Wallets[address]
}

// LoadFile : 저장된 Wallets를 불러온다.
func (ws *Wallets) LoadFile() error {
	if _, err := os.Stat(walletFile); os.IsNotExist(err) {
		return err
	}

	var wallets Wallets

	fileContent, err := ioutil.ReadFile(walletFile)
	if err != nil {
		return err
	}

	// elliptic.P256 알고리즘으로 디코딩
	gob.Register(elliptic.P256())
	decoder := gob.NewDecoder(bytes.NewReader(fileContent))
	err = decoder.Decode(&wallets)
	if err != nil {
		return err
	}

	ws.Wallets = wallets.Wallets

	return nil
}

// SaveFile : elliptic.P256 알고리즘을 이용해서 walletFile에 저장한다.
func (ws *Wallets) SaveFile() {
	var content bytes.Buffer

	// elliptic.P256 알고리즘으로 인코딩
	gob.Register(elliptic.P256())

	encoder := gob.NewEncoder(&content)
	err := encoder.Encode(ws)
	if err != nil {
		log.Panic(err)
	}

	// 0644 : permission, 파일이 이미 존재하지 않으면 생성한다.
	err = ioutil.WriteFile(walletFile, content.Bytes(), 0644)
	if err != nil {
		log.Panic(err)
	}
}

utils.go

  • Address 만들 때 사용되는 Base58 인코딩, 디코딩 함수이다.

package wallet

import (
	"log"

	"github.com/mr-tron/base58"
)

// 0 O l I + /
// Base58 은 64에서 헷갈리는 6개의 단어를 빼고 58개로 이루어진 인코딩 기법이다.
// Wallet 주소를 헷갈려서 잘못 입력하면 원하는 곳으로 코인이나 데이터를 이동시키지 못 한다.

// Base58Encode : Base58로 인코딩
func Base58Encode(input []byte) []byte {
	encode := base58.Encode(input)

	return []byte(encode)
}

// Base58Decode : Base58로 디코딩
func Base58Decode(input []byte) []byte {
	decode, err := base58.Decode(string(input[:]))
	if err != nil {
		log.Panic(err)
	}

	return decode
}

Result

// pub key의 길이가 가장 길고 pub hash, address로 갈수록 길이가 짧아진다. 
// 인코딩을 거쳐 점점 길이가 짧아지는 것 (sha256 -> rmpemd160 -> base58)
// address는 버젼을 1로 설정했기 때문에 맨 앞 숫자가 1이다. 
pub key: a5b803c0fc1895a82e9a6a82e4a5a21f5deee034dec81ddd4ccc3f024b11c2a4f4e28b3aef57b6ccca878e70f9d5dfac2b647795d718b666414732b3019ca2b3
pub hash: 0a14615357ecba8bb69bbbffb63e1fca03480ab7
address: 1vJAxZMhEzno17nrUJhUfZqxD82XRVRic
  • 간단하게 지갑을 3개 생성하고 지갑들의 모든 주소를 출력하는 예제이다.

❯ go run main.go createwallet
pub key: 864d6ef68552034f420b6c001d5fa2b866f40f3e59e2c773bea40fddebe5b7036e13a4eee923be836cd3d96448163fe15b516c4ac66ad78426c1c848f5907afb
pub hash: 28b5bae6078d089e8cbefc1b6ef1da283493fe3f
address: 14iFjW13yc8pGhbEfkAEpEAgiB6mW4PH8Y
New address is: 14iFjW13yc8pGhbEfkAEpEAgiB6mW4PH8Y
❯ go run main.go listaddresses
14iFjW13yc8pGhbEfkAEpEAgiB6mW4PH8Y
❯ go run main.go createwallet
pub key: a707acaccc3ff6bab6007c6527b4cfd0f4aad6255dfa0035e1b839ed98bd450e2de0a3f1fc4fbb4f6df7c738513f0723dd92667c97466e472d0a767578bd2bea
pub hash: 02eebb0e91cdd3a7012f866b4c03171b6a7ac402
address: 1GWLgcBJ23bvcXHDrFn2eHHDbHsqaZTXj
New address is: 1GWLgcBJ23bvcXHDrFn2eHHDbHsqaZTXj
❯ go run main.go createwallet
pub key: 32e175364ef5fce20b3c882284ec9f3c37fc3c64476280f0e8ceccdd520dc6c3611c6ec817894c674e776964252d6699038094026ff1fd2294c6bd21653e943a
pub hash: 61d4514f939deca6414a2f72ce20db2aa9294cff
address: 19vGthH7Mu9d5UBLjiijrrPdqoh8QMhwGA
New address is: 19vGthH7Mu9d5UBLjiijrrPdqoh8QMhwGA
❯ go run main.go listaddresses
1GWLgcBJ23bvcXHDrFn2eHHDbHsqaZTXj
19vGthH7Mu9d5UBLjiijrrPdqoh8QMhwGA
14iFjW13yc8pGhbEfkAEpEAgiB6mW4PH8Y

Last updated