[올바른 가상화폐 & 산업발전을 위한 프로젝트] 비트코인 코드 까보기 (1) - 포크, 지갑, 거래, 사인

in #kr7 years ago (edited)

올바른 가상화폐 문화와 산업발전을 위한 프로젝트


비트코인 코드 까보기 (1) - 포크, 지갑, 거래, 사인

들어가며..

안녕하세요. 길라임입니다.

STEEMIT은 베타버전이라 임시 저장도 안되고, 한국말로 되어있는 컨텐츠는 대부분 가상화폐에 한정되어 있는 편이지만, 아무래도 다른 가상화폐의 번지르르한 로드맵보다는 나름의 생태계와 구현물이 실제로 눈에 보여서 다행입니다.

기존의 코인 코드들에 대해 코드리뷰를 하고 검증을 하는 작업을 통해 일종의 신용평가를 해보고 싶어 STEEMIT을 시작하였습니다.

다만, 그 처음은 무조건 비트코인이 되어야합니다. 이 모든것을 시작한 것은 비트코인입니다.

바로 아래 단 9장의 페이퍼는 화폐의 역사를 바꿔버립니다.

Bitcoin: A Peer-to-Peer Electronic Cash System

저자인 사토시 나카모토는 처음부터 익명으로 시작한것으로 보아 자신이 미칠 파급력에 대한 짐작은 어느정도 있었을까요? 논문을 낼때는 자신의 이름을 알리고자 하는 목적이 큽니다. 제1저자, 제 2저자 등 굉장히 예민하고 욕심많은 이슈인데 굳이 익명으로 올렸을까요..

이 글에서는 비트코인의 알파버전을 까보며 그 소스를 이해하는 것으로 가상화폐의 이해를 시작하고자 합니다. 그리고 홈페이지만 그럴듯하게 만들어서 한탕 노리는 사기 코인들의 옥수수를 털어버리는데 일조하고자 합니다.

꽤 많은 부분은 아래의 책을 참고하였습니다.

  • A dissection of Bitcoin, Paul Huang
  • Mastering Bitcoin, Andreas antonopoulos

기술적으로 의미가 있는 책이 그렇게 많지는 않은데 위 책은 스펙만 화려한 경영 구루가 나와서 뜬구름 잡는것이 아닌, 실제 기술에 대한 내용이 실려있으니 구입하여 보셔도 될듯합니다. 이밖에 다른 기술서적은 시중에 깊이있는것들이 많이 없습니다.

비트코인 코드와 복사

비트코인의 코드는 github에서 바로 다운로드받으실수 있습니다. 모든 코드는 여기서 관리가 됩니다. 개발자는 git을 통해 코드를 관리하게 되는데 비트코인도 마찬가지고 거기서 파생된 모든 코인들은 대부분 제정신이라면 이곳을 통해 공개하고는 합니다.

image

비트코인의 star수와 fork수는 위와같습니다. 스캠코인이 아니라면 이렇게 star가 많습니다. 개발자들의 좋아요인데, 마치 페이스북처럼 원본은 라이크가 많습니다. 이를 그대로 복사 (포크) 해서 가져다 놓으면 어떻게 될까요?

image

길라임이 만든 가즈아코인입니다. 참 쉽죠? star는 한개도 없으나 fork는 원본을 따라갑니다. 이름짓기도 쉽습니다. 그냥 가져와서 테슬라움이니 아인XXX이니 커먼베이비슈퍼크립토커렌시앤벡터맨이니 이름붙여놓어놓으면 장땡입니다.

코드를 복사하기 이렇게 쉽다는건 대충 아시고 시간있으시면 라이트코인도 한번 확인해보세요 ^^;

알파버전 코드 다운로드 (ALPHA 0.1.5)

현재 비트코인 코드는 0.15 버전을 넘어가고 있는 상태입니다. 살짝 의아한 부분은 아직도 1버전이라는 딱지가 붙여지지 않았다는 것입니다. 관습적으로 정식 릴리즈버전이면 1.X로 시작하기 마련이지만, 비트코인자체는 하드포크가 수반되어야 하는 일이라 오리지널 비트코인의 버전 넘버가 높지 않은지도 모르겠습니다.

현재버전은 이것저것 소스가 추가되어 이해하기가 상당히 어렵습니다. 특히 스크립트 언어가아닌 C++기반의 소스는 바로 한줄한줄 이해하기가 어렵습니다.

따라서 우리는 암흑속으로 사라진 나가모토 사토시의 코드의 숨결이 살아있는 0.1대 버전을 살펴보면서 그 숨결의 냄새를 한번 맡아볼 예정입니다. 암내인지 꽃향기인지 살펴보러 갈까요.

$ git clone https://github.com/bitcoin/bitcoin.git
$ git checkout 4405b78d6059e536c36974088a8ed4d9f0f29898

비트코인의 소스를 가져오는것은 10초면 끝이 납니다. 다만 예전의 알파버전으로 돌아가기에는 알파버전 브랜치를 알아야 하는데, 위와같이 체크아웃을 하면 알파버전이 불러와 집니다.

1) 지갑 주소 만들기

비트코인의 키를 담을 지갑을 만드는게 맨 처음에 해야할 일인데, 이는 다음과 같은 코드로 되어있습니다.

    // ui.cpp 1857
    string strAddress = PubKeyToAddress(GenerateNewKey());
    SetAddressBookName(strAddress, strName);

빗섬등에서 지갑으로 전송하려면 QR코드나 아래와 같은 주소가 덩그러니 떠있는걸 알수있습니다. 위의 코드에서 나온 strAddress가 아래와 같을겁니다. (왜 ui.cpp인가 하면, 지갑주소만드는 ui를 만들어두었고, 여기서 편하게 지갑주소를 생성가능하도록 사토시가 해놓았네요.)

그렇다면 이 주소는 도대체 어디서 나올까요? 일단 저는 저렇게 제 지갑의 주소를 이 공간에 노출시켜버렸는데 위험하지는 않을까요? 노노, 이것은

  • 개인키로부터 -> 공개키
  • 공개키로부터 -> 해시
  • 해시를 읽기좋게 base58check이라는 방식을 통해 계산되어 나온 주소

입니다. 이 과정은 거꾸로 유추해낼수는 없고 하나하나 일일히 때려박는 방식이 아니면 불가능합니다. 하나하나 때려박기보다는 그 컴퓨팅파워로 차라리 비트코인을 파는게 더 나을겁니다.

이 과정은 다음의 코드부터 따라가보면 됩니다. 코드 안보실분은 이부분 패스하시면 됩니다.

// main.cpp 75
vector<unsigned char> GenerateNewKey()
{
    CKey key;
    key.MakeNewKey();
    if (!AddKey(key))
        throw runtime_error("GenerateNewKey() : AddKey failed\n");
    return key.GetPubKey();
}

공개키를 다시 해시에 넣는 과정은 또 아래코드에 대강 숨어있습니다. hash1, hash2와 같은 변수 네이밍은 뭔가 친숙합니다만, 뭐 다르게 이름짓기도 뭐하기는 합니다.

// util.h 392
inline uint160 Hash160(const vector<unsigned char>& vch)
{
    uint256 hash1;
    SHA256(&vch[0], vch.size(), (unsigned char*)&hash1);
    uint160 hash2;
    RIPEMD160((unsigned char*)&hash1, sizeof(hash1), (unsigned char*)&hash2);
    return hash2;
}

다시 읽기좋은 주소로 변환해주는 로직은 Hash160ToAddress() 여기 있네요.

// base58.h 198
inline string PubKeyToAddress(const vector<unsigned char>& vchPubKey)
{
    return Hash160ToAddress(Hash160(vchPubKey));
}

SHA256이나 RIPEMD160 은 단방향 함수로 사실 와이파이 비밀번호 설정할때 슬쩍 보신적이 있을겁니다. 사토시는 한개의 함수로는 불안했던지 위와같이 SHA256을 통과시키고 이를 다시 RIPEMD160함수에 통과시킵니다. 이래서 해시함수 덕후라는 이야기가 나오는듯 합니다. 그리고 이를통해 나온 주소가 대문자 i나 l등과 같은 부분에서 헷갈리지 않도록 세팅해놓은 base58check인코딩으로 다시한번 바꿔줍니다.

쉽게 말하면 이것입니다. 나만알고있는 비밀번호로부터 뭐 이상한 함수를 통과시켜 주소를 만들어냅니다. 이 주소는 나의 공개적인 주소로 쓰입니다. 누군가 나대신 이 주소에서 돈을 빼내려고 해도 내가 초기에 세팅한 개인키를 모르기때문에 말짱도루묵입니다.

그래서 최종적으로 튀어나오는게 아래와 같은 주소입니다. i나 l과 같은 헷갈리는 알파벳은 출현하지 않습니다.

image

돈이 남아도시는 분은 저에게 100 비트코인을 전송하시면 될것같구요.

2) 거래관련 코드 살펴보기

알파버전에서의 트랜잭션의 중요한 코드는 아래와 같은 이름으로 존재하고 있습니다. main.h파일을 들춰보면 대략 100라인부터 확인할수 있습니다. 아래의 구조는 트랜잭션에서 존재하는 여러 클래스들인데 이를 통해 트랜잭션이 일어날때 실제적으로 어떤 데이터들이 오고가는지 확인할 수 있습니다.

  • CInPoint
  • COutPoint
    • hash : uint256
    • n : uint
  • CTxIn
    • prevout: COutPoint
    • scriptSig : CScript
    • nSequence : uint
  • CTxOut
    • nValue : int64
    • scriptPubKey : CScript
  • CTransaction
    • nVersion : int
    • vin : vector[CTxIn]
    • vout : vector[CTxOut]
    • nLockTime : int

CTransaction이라는 객체 아래에 죄다 주렁주렁 트랜잭션에 관련한 데이터들이 달려있습니다.

실제로 거래를 날리는데의 로직을 살펴보는것은 ui.cpp에 숨겨져있는 onButtonSend()라는 로직을 참고해볼수 있습니다. 여기서는 중요한 로직 빼고는 죄다 날리거나 바꿔버리겠습니다. 관심있는 분들은 ui.cpp내부의 1368라인을 참고하시면 실제로 비트코인 소프트웨어를 만들때 날려야 하는 일련의 과정을 확일하실수 있습니다.

비트코인을 보내는 버튼 예제

void CSendDialog::OnButtonSend(wxCommandEvent& event)
{
    CWalletTx wtx;
    
    // 제주소를 임의로 박아넣었습니다.
    string strAddress = '15aNzLEMd53hYXWUpw3exnyHJuVX2y9MiC';

    // 잔액확인
    int64 nValue = 0;
    ParseMoney(strAddress->GetValue(), nValue)
    if (nValue + nTransactionFee > GetBalance()) return;
    
    // 비트코인 주소 가져오기
    uint160 hash160;
    bool fBitcoinAddress = AddressToHash160(strAddress, hash160);

    // 주소 형식이 맞으면
    if (fBitcoinAddress) SendMoney(scriptPubKey, nValue, wtx)

}

중요한 호출 메서드인 SendMoney() 는 main.cpp의 2657 라인에,
CreateTransaction() 은 2544라인에 존재합니다.

실제 돈을 보내고 이를 다른 노드에 전송


bool SendMoney(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew)
{
    //트랜잭션을 만들고
    CreateTransaction(scriptPubKey, nValue, wtxNew, nFeeRequired);

    //트랜잭션을 커밋합니다.
    CommitTransactionSpent(wtxNew);
    
    //트랜잭션이 받아들여진다면
    wtxNew.AcceptTransaction();

    //다른 피어들에게 이를 전송합니다.
    wtxNew.RelayWalletTransaction();

    return true;
}

트랜잭션을 실제 만드는 과정

이제 트랜잭션을 만들어보는 로직을 살펴봐야겠죠.

bool CreateTransaction(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew, int64& nFeeRequiredRet)
{   
    // 수수료를 추가합니다.
    int64 nValueOut = nValue;
    nValue += nFee;

    // 잔돈을 순환하면서 모아서 합계를 만들어 냅니다.
    set<CWalletTx*> setCoins;
    int64 nValueIn = 0;

    //이는 setCoins라는 set자료구조에 저장이 됩니다.
    SelectCoins(nValue, setCoins);
    foreach(CWalletTx* pcoin, setCoins) nValueIn += pcoin->GetCredit();

    // 목적지인 주소인 scriptPubKey에 금액을 넣어놓습니다.
    wtxNew.vout.push_back(CTxOut(nValueOut, scriptPubKey));

    // 투입하는 금액이 지불하는 금액보다 작다면 잔돈을 거슬러 받아야겠죠?
    if (nValueIn > nValue)
    {
        //루프(임의로 생략) 를 돌면서, 내 잔돈은 따로 챙겨둡니다. 
        if (txout.IsMine()) ExtractPubKey(txout.scriptPubKey, true, vchPubKey);           
        //그리고 보낼사람에게 줄 돈도 따로 챙겨둡니다.
        CScript scriptPubKey;
        scriptPubKey << vchPubKey << OP_CHECKSIG;
        wtxNew.vout.push_back(CTxOut(nValueIn - nValue, scriptPubKey));
    }

    // 다시 나에게 들어오는 트랜잭션을 세팅합니다.
    foreach(CWalletTx* pcoin, setCoins)
        for (int nOut = 0; nOut < pcoin->vout.size(); nOut++)
            if (pcoin->vout[nOut].IsMine())
                wtxNew.vin.push_back(CTxIn(pcoin->GetHash(), nOut));

    // 나에게 돌아오는 놈들은 사인합니다.
    int nIn = 0;
    foreach(CWalletTx* pcoin, setCoins)
        for (int nOut = 0; nOut < pcoin->vout.size(); nOut++)
            if (pcoin->vout[nOut].IsMine())
                SignSignature(*pcoin, wtxNew, nIn++);

    // 최소한의 수수료를 충족하는지 확인합니다.
    if (nFee < wtxNew.GetMinFee(true))
    {
        nFee = nFeeRequiredRet = wtxNew.GetMinFee(true);
        continue;
    }
}

최소 수수료는 짐작을 하는 과정이 필요한데, 왜냐면 모든 거래를 한번 돌면서 다 합치면서 계산 해보아야 최종적으로 최종 수수료를 계산할수 있기 때문입니다. 복잡한 루프문을 제가 임의로 숨겨버렸으니, 이부분은 직접 보시면 되지만 이것만 확인하면 됩니다.

  • 주소로 돈을 보냄
  • 남는돈은 내지갑으로 다시 보냄
  • 그리고 최소수수료를 계속 계산해본다.

이 최소수수료를 계산하는 부분이 뭔가 버전업이 되었을것같은 분위기는 나지만, 어쨌든 지금은 보낼돈과 잔돈, 그리고 수수료까지 계산하는 로직이 숨어있습니다.

3) 거래를 위한 사인 로직

거래를 하기 위해서는 분명히 본인의 개인키가 필요하고 이 없이 거래를 일으키는 행위는 비트코인의 존재이유를 없애버립니다. 이는 script.cpp 의 1090라인에 존재합니다.


bool SignSignature(const CTransaction& txFrom, CTransaction& txTo, unsigned int nIn, int nHashType, CScript scriptPrereq)
{
    // 사인된 해시를 받아서
    uint256 hash = SignatureHash(scriptPrereq + txout.scriptPubKey, txTo, nIn, nHashType);
    
    // 해시를 기본으로 하는 시그니처를 생성합니다.
    Solver(txout.scriptPubKey, hash, nHashType, txin.scriptSig);

    // 최종적으로 검사하는 로직입니다.
    EvalScript(txin.scriptSig + CScript(OP_CODESEPARATOR) + txout.scriptPubKey, txTo, nIn);
}


1편은 여기서 마무리합니다. 위에서 언급했듯이, Dissection of Bitcoin 에서 해당하는 코드에 대한 설명을 영문(!) 으로 정확하고 자세하게 확인하실 수 있으며 여기서 소개하는 로직은 실제로 동작하는 코드가 아니며 이해를 돕기위해 (혹은 제가 이해하기위해) 중요한 로직만 따왔습니다.

현재 0.15버전을 훑어보면 알파버전이 잡지 못했던 다양한 상황과 구조적인 모양을 훨씬 잡아놓은 모양새입니다. 사실 그러다보니 코드부터 들여다보면 이해하기가 힘듭니다.

많은 한국 스타트업과 대기업들에 계신분들이 코인 혹은 토큰을 이용해 국제적 자본을 끌어올수 있으면 참 좋겠다는 생각이 드는 크리스마스입니다.

그때까지 많은분들이 돈을 잃지 않고 시세가 무너지지 않길 빕니다. 공부를 좀더 하고 다시 계속 이어나가겠습니다.

Sort:  

쉬운 설명 감사합니다.

감사합니다~

자세한 설명 잘 읽었습니다. 다음 글도 기대가 됩니다.