Hashcash와 작업 증명

Hashcash는 이메일 스팸과 서비스 거부 공격의 비용을 높이기 위해 제안된 작업 증명 방식이다. 발신자는 조건을 만족하는 해시를 찾는 계산을 수행하고, 수신자는 결과를 비교적 적은 비용으로 검증한다. 계산 비용이 스팸을 불가능하게 만드는 것은 아니지만 대량 발송의 부담을 높일 수 있다.

비트코인은 Hashcash에서 이어지는 해시 기반 작업 증명 아이디어를 블록 생성에 사용한다. 다만 비트코인의 합의는 작업 증명뿐 아니라 블록과 트랜잭션 검증, 체인 선택 규칙도 포함한다. 아래 예제는 해시 탐색 원리를 보여주는 학습용 코드이며, 비트코인의 블록 헤더나 실제 Hashcash 토큰 형식을 구현한 것은 아니다.

실험

입력 문자열에 nonce를 붙여 SHA-256 해시를 계산하고, 문자열의 앞부분이 지정한 수의 0으로 시작할 때까지 반복한다. 난이도를 높이면 조건을 만족하는 결과를 찾기 위한 기대 시도 횟수가 증가한다. 한 번의 실행 시간은 시작 nonce와 입력에 따라 달라지므로, 난이도별 비교에는 반복 측정이 필요하다.

#include <iostream>
#include <string>
#include <openssl/sha.h>
#include <ctime>
#include <sstream>

std::string calculateHash(const std::string& input){
    unsigned char hash[SHA256_DIGEST_LENGTH];
    SHA256_CTX sha256;

    SHA256_Init(&sha256);
    SHA256_Update(&sha256, input.c_str(), input.length());
    SHA256_Final(hash, &sha256);

    std::stringstream ss;
    for(int i=0; i<SHA256_DIGEST_LENGTH; i++){
        ss << std::hex << (int)hash[i];

    }

    return ss.str();
}

std::string generateHashcash(const std::string& prefix, int difficulty) {
    std::string hash;
    int nonce = 213;
    while (true) {
        std::string candidate = prefix + std::to_string(nonce);
        hash = calculateHash(candidate);
        
        // Check if hash meets the required difficulty
        bool valid = true;
        for (int i = 0; i < difficulty; ++i) {
            if (hash[i] != '0') {
                valid = false;
                break;
            }
        }
        if (valid) {
            break;
        }
        ++nonce;
    }
    return hash;
}

int main() {
    clock_t start, finish;
    double duration;

    start = clock();

    std::string prefix = "example@domain.com:12345:";
    int difficulty = 3;
    std::string hashcash = generateHashcash(prefix, difficulty);
    std::cout << "Hashcash: " << hashcash << std::endl;

    finish = clock();
    duration = (double) (finish - start)/CLOCKS_PER_SEC;
    std::cout << duration << "s" << std::endl;
    return 0;
}//g++ -o pow ProofOfWork.cpp -lssl -lcrypto

이 예제의 prefix는 탐색에 사용하는 입력 문자열이고, difficulty는 출력 문자열에서 요구하는 선행 0의 개수다. 실제 프로토콜의 난이도나 비트 단위 목표값과는 구분해야 한다.

calculateHash: 입력된 문자열을 SHA-256 해시 함수를 사용하여 해싱하는데, 이 함수는 OpenSSL 라이브러리를 사용하여 구현되었다.

generateHashcash: 주어진 prefix와 difficulty를 바탕으로 해시값을 생성하고 이 함수에서는 nonce 값을 조정하여 원하는 difficulty를 만족하는 해시값을 찾는다.

main: 프로그램의 진입점이다. 여기서는 prefix와 difficulty를 설정하고 generateHashcash 함수를 호출해 해시캐시 값을 생성한다. 생성된 해시캐시 값을 출력한다.

실행 결과는 다음과 같다.

중간 결과를 출력하면

nonce를 바꾸며 탐색하는 과정을 확인할 수 있다.

예제에는 해시 바이트를 16진수로 출력할 때 두 자리로 채우지 않는 문제가 있다. 따라서 현재 코드의 문자열 조건을 정확한 선행 0비트 조건으로 해석하면 안 된다. 표준적인 난이도 실험을 위해서는 각 바이트를 두 자리로 출력하거나 해시 바이트를 직접 검사하도록 보완해야 한다. 위 실행 화면도 현재 예제에 대한 관찰로 한정한다.