Crystal 프로그래밍 언어로 암호화폐 만들기
게시 됨: 2022-03-11이 게시물은 내부를 탐색하여 블록체인의 주요 측면을 이해하려는 시도입니다. 원래 비트코인 백서를 읽는 것으로 시작했지만 블록체인을 진정으로 이해하는 유일한 방법은 처음부터 새로운 암호 화폐를 구축하는 것이라고 느꼈습니다.
그래서 새로운 Crystal 프로그래밍 언어를 사용하여 암호화폐를 만들기로 결정하고 CrystalCoin 이라고 이름을 붙였습니다. 이 기사에서는 알고리즘 선택, 해시 난이도 또는 유사한 주제에 대해 논의하지 않습니다. 대신 블록체인의 강점과 한계에 대한 더 깊고 실질적인 이해를 제공해야 하는 구체적인 예를 자세히 설명하는 데 초점을 맞출 것입니다.
아직 읽지 않았다면 알고리즘과 해싱에 대한 배경 지식을 더 알고 싶다면 Demir Selmanovic의 기사 Cryptocurrency for Dummies: Bitcoin and Beyond를 살펴보는 것이 좋습니다.
내가 크리스탈 프로그래밍 언어를 선택한 이유
더 나은 시연을 위해 성능 저하 없이 Ruby와 같은 생산적인 언어를 사용하고 싶었습니다. 암호화폐에는 많은 시간이 소요되는 계산(즉, 마이닝 및 해싱 )이 있으므로 C++ 및 Java와 같은 컴파일된 언어가 "실제" 암호화폐를 구축하기 위해 선택되는 언어입니다. 즉, 더 깔끔한 구문을 가진 언어를 사용하여 개발을 재미있게 유지하고 더 나은 가독성을 제공하고 싶었습니다. 크리스탈 성능은 어쨌든 좋은 경향이 있습니다.
그렇다면 내가 Crystal 프로그래밍 언어를 사용하기로 결정한 이유는 무엇입니까? Crystal의 구문은 Ruby에서 크게 영감을 받았기 때문에 저에게는 읽기 쉽고 쓰기 쉽습니다. 특히 숙련된 Ruby 개발자에게 더 낮은 학습 곡선이라는 추가 이점이 있습니다.
이것은 Crystal lang 팀이 공식 웹 사이트에 게시하는 방법입니다.
C처럼 빠르고 Ruby처럼 매끄럽습니다.
그러나 해석 언어인 Ruby 또는 JavaScript와 달리 Crystal은 컴파일된 언어이므로 훨씬 빠르고 메모리 사용량이 적습니다. 내부적으로는 네이티브 코드로 컴파일하기 위해 LLVM을 사용합니다.
Crystal은 또한 정적으로 유형이 지정되므로 컴파일러가 컴파일 시간에 유형 오류를 잡는 데 도움이 됩니다.
Crystal 언어가 이 기사의 범위를 벗어나기 때문에 내가 왜 Crystal 언어를 훌륭하다고 생각하는지 설명하지 않겠습니다. 그러나 내 낙관론이 설득력이 없다고 생각되면 이 기사에서 Crystal의 잠재력에 대한 더 나은 개요를 자유롭게 확인하십시오.
참고: 이 기사는 이미 객체 지향 프로그래밍(OOP)에 대한 기본적인 이해가 있다고 가정합니다.
블록체인
그렇다면 블록체인이란 무엇일까요? 디지털 지문(암호화 해시라고도 함)으로 연결되고 보호되는 블록의 목록(체인)입니다.
가장 쉽게 생각할 수 있는 방법은 연결 목록 데이터 구조입니다. 즉, 연결 목록은 이전 요소에 대한 참조만 있으면 됩니다. 블록에는 이전 블록의 식별자에 따라 식별자가 있어야 합니다. 즉, 뒤에 오는 모든 단일 블록을 다시 계산하지 않고는 블록을 바꿀 수 없습니다.
지금은 블록체인을 체인과 연결된 일부 데이터가 있는 일련의 블록으로 생각하고 체인은 이전 블록의 해시입니다.
전체 블록체인은 상호 작용하려는 각 노드에 존재합니다. 즉, 네트워크의 각 노드에 복사됩니다. 단일 서버가 호스팅하지 않지만 모든 블록체인 개발 회사에서 사용하므로 분산화 됩니다.
예, 이것은 기존의 중앙 집중식 시스템에 비해 이상합니다. 각 노드에는 전체 블록체인의 복사본이 있습니다(2017년 12월까지 Bitcoin 블록체인에서 > 149Gb).
해싱 및 디지털 서명
그렇다면 이 해시 함수는 무엇입니까? 해시를 텍스트/객체를 제공할 때 고유한 지문을 반환하는 함수로 생각하십시오. 입력 개체의 가장 작은 변화조차도 지문을 극적으로 변화시킬 것입니다.
다양한 해싱 알고리즘이 있으며 이 기사에서는 Bitcoin
에서 사용되는 SHA256
해시 알고리즘을 사용할 것입니다.
SHA256
을 사용하면 입력이 256비트보다 작거나 256비트보다 훨씬 큰 경우에도 길이가 항상 64개의 16진수 문자(256비트)가 됩니다.
입력 | 해시된 결과 |
---|---|
T 매우 긴 텍스트 매우 긴 텍스트 매우 긴 텍스트 매우 긴 텍스트 매우 긴 텍스트 매우 긴 텍스트 매우 긴 텍스트 매우 긴 매우 긴 텍스트 매우 긴 텍스트 매우 긴 텍스트 매우 긴 텍스트 매우 긴 텍스트 매우 긴 텍스트 매우 긴 텍스트 매우 긴 텍스트 | cf49bbb21c8b7c078165919d7e57c145ccb7f398e7b58d9a3729de368d86294a |
토탈 | 2e4e500e20f1358224c08c7fb7d3e0e9a5e4ab7a013bfd6774dfa54d7684dd21 |
토탈. | 12075307ce09a6859601ce9d451d385053be80238ea127c5df6e6611eed7c6f0 |
마지막 예에서 .
(점)은 해시에 극적인 변화를 가져왔습니다.
따라서 블록체인에서 체인은 해시를 생성하는 해시 알고리즘에 블록 데이터를 전달하여 구축되며, 이 해시는 다음 블록에 연결되며, 이후에는 이전 블록의 해시와 연결된 일련의 블록을 형성합니다.
크리스탈에서 암호화폐 구축
이제 Crystal 프로젝트 생성을 시작하고 SHA256
암호화를 빌드해 보겠습니다.
Crystal 프로그래밍 언어가 설치되어 있다고 가정하고 Crystal의 내장 프로젝트 도구 crystal init app [name]
을 사용하여 CrystalCoin
코드베이스의 골격을 생성해 보겠습니다.
% crystal init app crystal_coin create crystal_coin/.gitignore create crystal_coin/.editorconfig create crystal_coin/LICENSE create crystal_coin/README.md create crystal_coin/.travis.yml create crystal_coin/shard.yml create crystal_coin/src/crystal_coin.cr create crystal_coin/src/crystal_coin/version.cr create crystal_coin/spec/spec_helper.cr create crystal_coin/spec/crystal_coin_spec.cr Initialized empty Git repository in /Users/eki/code/crystal_coin/.git/
이 명령은 이미 초기화된 git 저장소, 라이선스 및 추가 정보 파일과 함께 프로젝트의 기본 구조를 생성합니다. 또한 테스트를 위한 스텁과 프로젝트를 설명하고 샤드라고도 하는 종속성을 관리하기 위한 shard.yml
파일이 함께 제공됩니다.
SHA256
알고리즘을 빌드하는 데 필요한 openssl
샤드를 추가해 보겠습니다.
# shard.yml dependencies: openssl: github: datanoise/openssl.cr
일단 입력되면 터미널로 돌아가 crystal deps
를 실행하십시오. 이렇게 하면 우리가 사용할 openssl
과 그 종속성이 풀립니다.
이제 필요한 라이브러리가 코드에 설치되었습니다. 먼저 Block
클래스를 정의한 다음 해시 함수를 빌드해 보겠습니다.
# src/crystal_coin/block.cr require "openssl" module CrystalCoin class Block def initialize(data : String) @data = data end def hash hash = OpenSSL::Digest.new("SHA256") hash.update(@data) hash.hexdigest end end end puts CrystalCoin::Block.new("Hello, Cryptos!").hash
이제 터미널에서 crystal run crystal src/crystal_coin/block.cr
을 실행하여 애플리케이션을 테스트할 수 있습니다.
crystal_coin [master●] % crystal src/crystal_coin/block.cr 33eedea60b0662c66c289ceba71863a864cf84b00e10002ca1069bf58f9362d5
블록체인 설계
각 블록은 timestamp
와 선택적으로 index
와 함께 저장됩니다. CrystalCoin
에서는 둘 다 저장할 것입니다. 블록체인 전체의 무결성을 보장하기 위해 각 블록에는 자체 식별 해시 가 있습니다. 비트코인과 마찬가지로 각 블록의 해시는 블록의 암호화 해시( index
, timestamp
, data
및 이전 블록 해시 previous_hash
)가 됩니다. 데이터는 현재 원하는 모든 것이 될 수 있습니다.
module CrystalCoin class Block property current_hash : String def initialize(index = 0, data = "data", previous_hash = "hash") @data = data @index = index @timestamp = Time.now @previous_hash = previous_hash @current_hash = hash_block end private def hash_block hash = OpenSSL::Digest.new("SHA256") hash.update("#{@index}#{@timestamp}#{@data}#{@previous_hash}") hash.hexdigest end end end puts CrystalCoin::Block.new(data: "Same Data").current_hash
Crystal lang에서는 Ruby의 attr_accessor
, attr_getter
및 attr_setter
메서드를 새 키워드로 대체합니다.
루비 키워드 | 크리스탈 키워드 |
---|---|
attr_accessor | 재산 |
attr_reader | 얻는 사람 |
attr_writer | 세터 |
Crystal에서 눈치채셨을 수도 있는 또 다른 사항은 코드를 통해 컴파일러에게 특정 유형에 대한 힌트를 주기를 원한다는 것입니다. Crystal은 유형을 유추하지만 모호한 경우 유형을 명시적으로 선언할 수도 있습니다. 이것이 우리가 current_hash
에 대한 String
유형을 추가한 이유입니다.
이제 block.cr
을 두 번 실행하고 다른 timestamp
로 인해 동일한 데이터가 다른 해시를 생성한다는 점에 유의하십시오.
crystal_coin [master●] % crystal src/crystal_coin/block.cr 361d0df74e28d37b71f6c5f579ee182dd3d41f73f174dc88c9f2536172d3bb66 crystal_coin [master●] % crystal src/crystal_coin/block.cr b1fafd81ba13fc21598fb083d9429d1b8a7e9a7120dbdacc7e461791b96b9bf3
이제 블록 구조가 있지만 블록체인을 만들고 있습니다. 실제 체인을 형성하려면 블록을 추가해야 합니다. 앞서 언급했듯이 각 블록에는 이전 블록의 정보가 필요합니다. 그러나 블록체인의 첫 번째 블록은 어떻게 거기에 도달합니까? 음, 첫 번째 블록 또는 genesis
블록은 특수 블록(전임자가 없는 블록)입니다. 많은 경우 수동으로 추가되거나 추가할 수 있는 고유한 논리가 있습니다.
제네시스 블록을 반환하는 새 함수를 만들 것입니다. 이 블록은 index=0
이고 임의의 데이터 값과 previous_hash
매개변수에 임의의 값이 있습니다.
생성 블록을 생성하는 Block.first
메서드를 빌드하거나 클래스화해 보겠습니다.
module CrystalCoin class Block ... def self.first(data="Genesis Block") Block.new(data: data, previous_hash: "0") end ... end end
그리고 p CrystalCoin::Block.first
를 사용하여 테스트해 보겠습니다.
#<CrystalCoin::Block:0x10b33ac80 @current_hash="acb701a9b70cff5a0617d654e6b8a7155a8c712910d34df692db92455964d54e", @data="Genesis Block", @index=0, @timestamp=2018-05-13 17:54:02 +03:00, @previous_hash="0">
이제 제네시스 블록을 생성할 수 있으므로 블록체인에서 후속 블록을 생성하는 함수가 필요합니다.
이 함수는 체인의 이전 블록을 매개변수로 사용하여 생성할 블록에 대한 데이터를 생성하고 적절한 데이터와 함께 새 블록을 반환합니다. 새 블록이 이전 블록의 정보를 해시할 때 블록체인의 무결성은 새 블록마다 증가합니다.
중요한 결과는 모든 연속 블록의 해시를 변경하지 않고는 블록을 수정할 수 없다는 것입니다. 이것은 아래의 예에서 보여줍니다. 블록 44의 데이터가 LOOP
에서 EAST
로 변경되면 연속 블록의 모든 해시가 변경되어야 합니다. 이것은 블록의 해시가 (무엇보다도) previous_hash
_해시 값에 의존하기 때문입니다.
이렇게 하지 않으면 외부 당사자가 데이터를 변경하고 우리 체인을 완전히 새로운 자체 체인으로 교체하는 것이 더 쉬울 것입니다. 이 해시 체인은 암호화 증명 역할을 하며 블록이 블록체인에 추가되면 교체하거나 제거할 수 없도록 합니다. Block.next
클래스 메서드를 생성해 보겠습니다.
module CrystalCoin class Block ... def self.next(previous_node, data = "Transaction Data") Block.new( data: "Transaction data number (#{previous_node.index + 1})", index: previous_node.index + 1, previous_hash: previous_hash.hash ) end ... end end
이 모든 것을 함께 시도하기 위해 간단한 블록체인을 만들 것입니다. 목록의 첫 번째 요소는 제네시스 블록입니다. 물론 다음 블록을 추가해야 합니다. CrystalCoin
을 시연하기 위해 5개의 새로운 블록을 만들 것입니다.
blockchain = [ CrystalCoin::Block.first ] previous_block = blockchain[0] 5.times do new_block = CrystalCoin::Block.next(previous_block: previous_block) blockchain << new_block previous_block = new_block end p blockchain
[#<CrystalCoin::Block:0x108c57c80 @current_hash= "df7f9d47bee95c9158e3043ddd17204e97ccd6e8958e4e41dacc7f0c6c0df610", @index=0, @previous_hash="0", @timestamp=2018-06-04 12:13:21 +03:00, @data="Genesis Block>, #<CrystalCoin::Block:0x109c89740 @current_hash= "d188fcddd056c044c783d558fd6904ceeb9b2366339af491a293d369de4a81f6", @index=1, @previous_hash= "df7f9d47bee95c9158e3043ddd17204e97ccd6e8958e4e41dacc7f0c6c0df610", @timestamp=2018-06-04 12:13:21 +03:00, @data="Transaction data number (1)">, #<CrystalCoin::Block:0x109cc8500 @current_hash= "0b71b61118b9300b4fe8cdf4a7cbcc1dac4da7a8a3150aa97630994805354107", @index=2, @previous_hash= "d188fcddd056c044c783d558fd6904ceeb9b2366339af491a293d369de4a81f6", @timestamp=2018-06-04 12:13:21 +03:00, @transactions="Transaction data number (2)">, #<CrystalCoin::Block:0x109ccbe40 @current_hash= "9111406deea4f07f807891405078a3f8537416b31ab03d78bda3f86d9ae4c584", @index=3, @previous_hash= "0b71b61118b9300b4fe8cdf4a7cbcc1dac4da7a8a3150aa97630994805354107", @timestamp=2018-06-04 12:13:21 +03:00, @transactions="Transaction data number (3)">, #<CrystalCoin::Block:0x109cd0980 @current_hash= "0063bfc5695c0d49b291a8813c566b047323f1808a428e0eb1fca5c399547875", @index=4, @previous_hash= "9111406deea4f07f807891405078a3f8537416b31ab03d78bda3f86d9ae4c584", @timestamp=2018-06-04 12:13:21 +03:00, @transactions="Transaction data number (4)">, #<CrystalCoin::Block:0x109cd0100 @current_hash= "00a0c70e5412edd7389a0360b48c407ce4ddc8f14a0bcf16df277daf3c1a00c7", @index=5, @previous_hash= "0063bfc5695c0d49b291a8813c566b047323f1808a428e0eb1fca5c399547875", @timestamp=2018-06-04 12:13:21 +03:00, @transactions="Transaction data number (5)">
작업 증명
작업 증명 알고리즘(PoW)은 블록체인에서 새로운 블록을 생성하거나 채굴 하는 방법입니다. PoW의 목표는 문제를 해결하는 숫자를 찾는 것입니다. 숫자는 찾기 어렵지만 네트워크의 모든 사람이 계산적으로 쉽게 확인할 수 있어야 합니다. 이것이 작업 증명의 핵심 아이디어입니다.
모든 것이 명확한지 확인하기 위해 예를 들어 설명하겠습니다. 어떤 정수 x의 해시에 다른 y를 곱한 해시가 00
으로 시작해야 한다고 가정합니다. 그래서:
hash(x * y) = 00ac23dc...
이 단순화된 예에서는 x=5
를 수정하고 Crystal에서 구현해 보겠습니다.
x = 5 y = 0 while hash((x*y).to_s)[0..1] != "00" y += 1 end puts "The solution is y = #{y}" puts "Hash(#{x}*#{y}) = #{hash((x*y).to_s)}"
코드를 실행해 보겠습니다.
crystal_coin [master●●] % time crystal src/crystal_coin/pow.cr The solution is y = 530 Hash(5*530) = 00150bc11aeeaa3cdbdc1e27085b0f6c584c27e05f255e303898dcd12426f110 crystal src/crystal_coin/pow.cr 1.53s user 0.23s system 160% cpu 1.092 total
보시다시피 이 숫자 y=530
은 찾기 어려웠지만(무차별 대입) 해시 함수를 사용하여 쉽게 확인할 수 있습니다.
왜 이 PoW 알고리즘을 사용합니까? 블록당 하나의 해시를 생성하면 되지 않습니까? 해시는 유효 해야 합니다. 우리의 경우 해시의 처음 두 문자가 00
이면 해시가 유효합니다. 해시가 00......
으로 시작하면 유효한 것으로 간주됩니다. 이것을 어려움 이라고 합니다. 난이도가 높을수록 유효한 해시를 얻는 데 더 오래 걸립니다.
그러나 해시가 처음에 유효하지 않으면 우리가 사용하는 데이터에서 무언가가 변경되어야 합니다. 동일한 데이터를 계속해서 사용하면 동일한 해시를 계속해서 얻게 되며 해시는 절대 유효하지 않습니다. 우리는 해시에서 nonce
라는 것을 사용합니다(이전 예에서는 y
). 해시가 유효하지 않을 때마다 증가하는 숫자일 뿐입니다. 우리는 데이터(날짜, 메시지, 이전 해시, 인덱스)와 1의 nonce를 얻습니다. 우리가 얻은 해시가 유효하지 않으면 2의 nonce로 시도합니다. 그리고 유효한 해시를 얻을 때까지 nonce를 증가시킵니다. .
비트코인에서 작업 증명 알고리즘을 해시캐시라고 합니다. Block 클래스에 작업 증명을 추가하고 nonce를 찾기 위해 마이닝 을 시작합시다. 두 개의 선행 0 '00'의 하드 코딩된 난이도 를 사용합니다.
이제 이를 지원하도록 Block 클래스를 다시 설계해 보겠습니다. CrystalCoin
블록에는 다음 속성이 포함됩니다.
1) index: indicates the index of the block ex: 0,1 2) timestamp: timestamp in epoch, number of seconds since 1 Jan 1970 3) data: the actual data that needs to be stored on the blockchain. 4) previous_hash: the hash of the previous block, this is the chain/link between the blocks 5) nonce: this is the number that is to be mined/found. 6) current_hash: The hash value of the current block, this is generated by combining all the above attributes and passing it to a hashing algorithm
해싱을 수행하고 nonce
를 찾아 코드를 깨끗하고 모듈식으로 유지하기 위해 별도의 모듈을 만들겠습니다. 나는 그것을 proof_of_work.cr
이라고 부를 것이다:
require "openssl" module CrystalCoin module ProofOfWork private def proof_of_work(difficulty = "00") nonce = 0 loop do hash = calc_hash_with_nonce(nonce) if hash[0..1] == difficulty return nonce else nonce += 1 end end end private def calc_hash_with_nonce(nonce = 0) sha = OpenSSL::Digest.new("SHA256") sha.update("#{nonce}#{@index}#{@timestamp}#{@data}#{@previous_hash}") sha.hexdigest end end end
Block
클래스는 다음과 같습니다.
require "./proof_of_work" module CrystalCoin class Block include ProofOfWork property current_hash : String property index : Int32 property nonce : Int32 property previous_hash : String def initialize(index = 0, data = "data", previous_hash = "hash") @data = data @index = index @timestamp = Time.now @previous_hash = previous_hash @nonce = proof_of_work @current_hash = calc_hash_with_nonce(@nonce) end def self.first(data = "Genesis Block") Block.new(data: data, previous_hash: "0") end def self.next(previous_block, data = "Transaction Data") Block.new( data: "Transaction data number (#{previous_block.index + 1})", index: previous_block.index + 1, previous_hash: previous_block.current_hash ) end end end
일반적으로 Crystal 코드 및 Crystal 언어 예제에 대해 주의할 사항이 거의 없습니다. Crystal에서 메서드는 기본적으로 공개되어 있습니다. Crystal은 Ruby에서 오는 혼동을 줄 수 있는 private 키워드를 접두어로 하는 각 private 메소드를 요구합니다.
Crystal의 Integer 유형에는 Ruby의 Fixnum
과 비교하여 Int8
, Int16
, Int32
, Int64
, UInt8
, UInt16
, UInt32
또는 UInt64
가 있습니다. true
및 false
는 Ruby의 TrueClass
또는 FalseClass
클래스의 값이 아니라 Bool
클래스의 값입니다.
Crystal에는 핵심 언어 기능으로 선택적 및 명명된 메서드 인수가 있으며 인수를 처리하기 위해 특별한 코드를 작성할 필요가 없습니다. Block#initialize(index = 0, data = "data", previous_hash = "hash")
를 확인한 다음 Block.new(data: data, previous_hash: "0")
와 같이 호출합니다.
Crystal과 Ruby 프로그래밍 언어 간의 차이점에 대한 자세한 목록은 Crystal for Rubyists를 확인하십시오.
이제 다음을 사용하여 5개의 트랜잭션을 생성해 보겠습니다.
blockchain = [ CrystalCoin::Block.first ] puts blockchain.inspect previous_block = blockchain[0] 5.times do |i| new_block = CrystalCoin::Block.next(previous_block: previous_block) blockchain << new_block previous_block = new_block puts new_block.inspect end
[#<CrystalCoin::Block:0x108f8fea0 @current_hash="0088ca080a49334e1cb037ed4c42795d635515ef1742e6bcf439bf0f95711759", @index=0, @nonce=17, @timestamp=2018-05-14 17:20:46 +03:00, @data="Genesis Block", @previous_hash="0">] #<CrystalCoin::Block:0x108f8f660 @current_hash="001bc2b04d7ad8ef25ada30e2bde19d7bbbbb3ad42348017036b0d4974d0ccb0", @index=1, @nonce=24, @timestamp=2018-05-14 17:20:46 +03:00, @data="Transaction data number (1)", @previous_hash="0088ca080a49334e1cb037ed4c42795d635515ef1742e6bcf439bf0f95711759"> #<CrystalCoin::Block:0x109fc5ba0 @current_hash="0019256c998028111838b872a437cd8adced53f5e0f8f43388a1dc4654844fe5", @index=2, @nonce=61, @timestamp=2018-05-14 17:20:46 +03:00, @data="Transaction data number (2)", @previous_hash="001bc2b04d7ad8ef25ada30e2bde19d7bbbbb3ad42348017036b0d4974d0ccb0"> #<CrystalCoin::Block:0x109fdc300 @current_hash="0080a30d0da33426a1d4f36d870d9eb709eaefb0fca62cc68e497169c5368b97", @index=3, @nonce=149, @timestamp=2018-05-14 17:20:46 +03:00, @data="Transaction data number (3)", @previous_hash="0019256c998028111838b872a437cd8adced53f5e0f8f43388a1dc4654844fe5"> #<CrystalCoin::Block:0x109ff58a0 @current_hash="00074399d51c700940e556673580a366a37dec16671430141f6013f04283a484", @index=4, @nonce=570, @timestamp=2018-05-14 17:20:46 +03:00, @data="Transaction data number (4)", @previous_hash="0080a30d0da33426a1d4f36d870d9eb709eaefb0fca62cc68e497169c5368b97"> #<CrystalCoin::Block:0x109fde120 @current_hash="00720bb6e562a25c19ecd2b277925057626edab8981ff08eb13773f9bb1cb842", @index=5, @nonce=475, @timestamp=2018-05-14 17:20:46 +03:00, @data="Transaction data number (5)", @previous_hash="00074399d51c700940e556673580a366a37dec16671430141f6013f04283a484">
차이점이 보이시나요? 이제 모든 해시는 00
으로 시작합니다. 이것이 바로 작업 증명의 마법입니다. ProofOfWork
를 사용하여 nonce
를 찾았고 증명은 일치하는 어려움이 있는 해시, 즉 두 개의 선행 0 00
입니다.
우리가 만든 첫 번째 블록에서 일치하는 행운의 숫자를 찾을 때까지 17개의 nonce를 시도했습니다.
차단하다 | 루프/해시 계산 수 |
---|---|
#0 | 17 |
#1 | 24 |
#2 | 61 |
#삼 | 149 |
#4 | 570 |
#5 | 475 |
이제 4개의 선행 0의 difficulty="0000"
를 시도해 봅시다.
차단하다 | 루프/해시 계산 수 |
---|---|
#1 | 26 762 |
#2 | 68 419 |
#삼 | 23 416 |
#4 | 15 353 |
첫 번째 블록에서 일치하는 행운의 숫자를 찾을 때까지 26762개의 nonce(난이도 '00'으로 17개의 nonce와 비교)를 시도했습니다.
API로서의 블록체인
여태까지는 그런대로 잘됐다. 우리는 간단한 블록체인을 만들었으며 비교적 쉽게 만들 수 있었습니다. 그러나 여기서 문제는 CrystalCoin
이 하나의 단일 시스템에서만 실행할 수 있다는 것입니다(분산/분권되지 않음).
이제부터 CrystalCoin
에 JSON 데이터를 사용하기 시작합니다. 데이터는 트랜잭션이므로 각 블록의 데이터 필드는 트랜잭션 목록이 됩니다.
각 트랜잭션은 코인의 sender
, 코인의 receiver
및 전송되는 CrystalCoin의 amount
을 자세히 설명하는 JSON 객체가 됩니다.
{ "from": "71238uqirbfh894-random-public-key-a-alkjdflakjfewn204ij", "to": "93j4ivnqiopvh43-random-public-key-b-qjrgvnoeirbnferinfo", "amount": 3 }
새로운 transaction
형식(이전에는 data
라고 함)을 지원하기 위해 Block
클래스를 약간 수정했습니다. 따라서 혼란을 피하고 일관성을 유지하기 위해 이제부터 예제 응용 프로그램에 게시된 data
를 참조하는 데 transaction
이라는 용어를 사용합니다.
새로운 간단한 클래스 Transaction
을 소개합니다.
module CrystalCoin class Block class Transaction property from : String property to : String property amount : Int32 def initialize(@from, @to, @amount) end end end end
트랜잭션은 블록으로 포장되므로 블록은 하나 또는 여러 트랜잭션을 포함할 수 있습니다. 트랜잭션이 포함된 블록이 자주 생성되어 블록체인에 추가됩니다.

블록체인은 블록의 집합체라고 할 수 있습니다. 우리는 모든 블록을 Crystal 목록에 저장할 수 있으며 이것이 새로운 클래스 Blockchain
을 도입한 이유입니다.
Blockchain
체인에는 chain
및 uncommitted_transactions
배열이 있습니다. chain
은 블록체인에서 채굴된 모든 블록을 포함하고 uncommitted_transactions
에는 블록체인에 추가되지 않은(아직 채굴되지 않은) 모든 트랜잭션이 있습니다. Blockchain을 초기화하면 Blockchain
를 사용하여 제네시스 블록을 Block.first
하고 이를 chain
배열에 추가하고 빈 uncommitted_transactions
배열을 추가합니다.
uncommitted_transactions
배열에 트랜잭션을 추가하기 위해 Blockchain#add_transaction
메소드를 생성합니다.
새로운 Blockchain
클래스를 빌드해 보겠습니다.
require "./block" require "./transaction" module CrystalCoin class Blockchain getter chain getter uncommitted_transactions def initialize @chain = [ Block.first ] @uncommitted_transactions = [] of Block::Transaction end def add_transaction(transaction) @uncommitted_transactions << transaction end end end
Block
클래스에서는 data
대신 transactions
을 사용하기 시작할 것입니다.
module CrystalCoin class Block include ProofOfWork def initialize(index = 0, transactions = [] of Transaction, previous_hash = "hash") @transactions = transactions ... end .... def self.next(previous_block, transactions = [] of Transaction) Block.new( transactions: transactions, index: previous_block.index + 1, previous_hash: previous_block.current_hash ) end end end
이제 트랜잭션이 어떻게 생겼는지 알았으므로 node
라고 하는 블록체인 네트워크의 컴퓨터 중 하나에 트랜잭션을 추가하는 방법이 필요합니다. 그렇게 하기 위해 간단한 HTTP 서버를 만들 것입니다.
4개의 엔드포인트를 생성합니다.
- [POST]
/transactions/new
: 블록에 대한 새 트랜잭션 생성 - [GET]
/mine
: 서버에 새 블록을 채굴하도록 지시합니다. - [GET]
/chain
: 전체 블록체인을JSON
형식으로 반환합니다. - [GET]
/pending
: 보류 중인 트랜잭션을 반환합니다(uncommitted_transactions
).
Kemal 웹 프레임워크를 사용할 것입니다. 이는 끝점을 Crystal 기능에 쉽게 매핑할 수 있게 해주는 마이크로 프레임워크입니다. Kemal은 Rubyists용 Sinatra의 영향을 많이 받았고 매우 유사한 방식으로 작동합니다. Ruby on Rails에 상응하는 것을 찾고 있다면 Amber를 확인하십시오.
우리 서버는 블록체인 네트워크에서 단일 노드를 형성합니다. 먼저 Kemal
을 shard.yml
파일에 추가하고 종속성을 설치해 보겠습니다.
dependencies: kemal: github: kemalcr/kemal
이제 HTTP 서버의 골격을 구축해 보겠습니다.
# src/server.cr require "kemal" require "./crystal_coin" # Generate a globally unique address for this node node_identifier = UUID.random.to_s # Create our Blockchain blockchain = Blockchain.new get "/chain" do "Send the blockchain as json objects" end get "/mine" do "We'll mine a new Block" end get "/pending" do "Send pending transactions as json objects" end post "/transactions/new" do "We'll add a new transaction" end Kemal.run
그리고 서버를 실행합니다:
crystal_coin [master●●] % crystal run src/server.cr [development] Kemal is ready to lead at http://0.0.0.0:3000
서버가 제대로 작동하는지 확인합시다.
% curl http://0.0.0.0:3000/chain Send the blockchain as json objects%
여태까지는 그런대로 잘됐다. 이제 각 끝점 구현을 진행할 수 있습니다. /transactions/new
및 pending
엔드포인트를 구현하여 시작하겠습니다.
get "/pending" do { transactions: blockchain.uncommitted_transactions }.to_json end post "/transactions/new" do |env| transaction = CrystalCoin::Block::Transaction.new( from: env.params.json["from"].as(String), to: env.params.json["to"].as(String), amount: env.params.json["amount"].as(Int64) ) blockchain.add_transaction(transaction) "Transaction #{transaction} has been added to the node" end
간단한 구현. CrystalCoin::Block::Transaction
객체를 만들고 Blockchain#add_transaction
을 사용하여 uncommitted_transactions
배열에 트랜잭션을 추가하기만 하면 됩니다.
현재 트랜잭션은 처음에 uncommitted_transactions
풀에 저장됩니다. 확인되지 않은 트랜잭션을 블록에 넣고 작업 증명(PoW)을 계산하는 프로세스를 블록 마이닝 이라고 합니다. 제약 조건을 만족하는 nonce
가 파악되면 블록이 채굴되었고 새 블록이 블록체인에 추가되었다고 말할 수 있습니다.
CrystalCoin
에서는 이전에 만든 간단한 작업 증명 알고리즘을 사용합니다. 새 블록을 생성하려면 광부의 컴퓨터가 다음을 수행해야 합니다.
-
chain
의 마지막 블록을 찾습니다. - 보류 중인 트랜잭션을 찾습니다(
uncommitted_transactions
). -
Block.next
를 사용하여 새 블록을 만듭니다. - 채굴된 블록을
chain
배열에 추가합니다. -
uncommitted_transactions
배열을 정리합니다.
따라서 /mine
엔드포인트를 구현하려면 먼저 위의 단계를 Blockchain#mine
에서 구현해 보겠습니다.
module CrystalCoin class Blockchain include Consensus BLOCK_SIZE = 25 ... def mine raise "No transactions to be mined" if @uncommitted_transactions.empty? new_block = Block.next( previous_block: @chain.last, transactions: @uncommitted_transactions.shift(BLOCK_SIZE) ) @chain << new_block end end end
먼저 마이닝에 대해 보류 중인 트랜잭션이 있는지 확인합니다. 그런 다음 @chain.last
를 사용하여 마지막 블록과 채굴할 처음 25
트랜잭션을 가져옵니다 Array#shift(BLOCK_SIZE)
를 사용하여 처음 25개의 uncommitted_transactions
배열을 반환한 다음 인덱스 0에서 시작하는 요소를 제거합니다) .
이제 /mine
엔드포인트를 구현해 보겠습니다.
get "/mine" do blockchain.mine "Block with index=#{blockchain.chain.last.index} is mined." end
그리고 /chain
엔드포인트의 경우:
get "/chain" do { chain: blockchain.chain }.to_json end
블록체인과 상호작용
우리는 cURL
을 사용하여 네트워크를 통해 API와 상호 작용할 것입니다.
먼저 서버를 시작하겠습니다.
crystal_coin [master] % crystal run src/server.cr [development] Kemal is ready to lead at http://0.0.0.0:3000
그런 다음 트랜잭션 구조가 포함된 본문을 사용하여 http://localhost:3000/transactions/new
에 대한 POST
요청을 만들어 두 개의 새 트랜잭션을 생성해 보겠습니다.
crystal_coin [master●] % curl -X POST http://0.0.0.0:3000/transactions/new -H "Content-Type: application/json" -d '{"from": "eki", "to":"iron_man", "amount": 1000}' Transaction #<CrystalCoin::Block::Transaction:0x10c4159f0 @from="eki", @to="iron_man", @amount=1000_i64> has been added to the node% crystal_coin [master●] % curl -X POST http://0.0.0.0:3000/transactions/new -H "Content-Type: application/json" -d '{"from": "eki", "to":"hulk", "amount": 700}' Transaction #<CrystalCoin::Block::Transaction:0x10c415810 @from="eki", @to="hulk", @amount=700_i64> has been added to the node%
이제 보류 중인 트랜잭션(예: 아직 블록에 추가되지 않은 트랜잭션)을 나열해 보겠습니다.
crystal_coin [master●] % curl http://0.0.0.0:3000/pendings { "transactions":[ { "from":"ekis", "to":"huslks", "amount":7090 }, { "from":"ekis", "to":"huslks", "amount":70900 } ] }
보시다시피 이전에 생성한 두 개의 트랜잭션이 uncommitted_transactions
에 추가되었습니다.
이제 http://0.0.0.0:3000/mine
에 GET
요청을 하여 두 트랜잭션을 마이닝 해 보겠습니다.
crystal_coin [master●] % curl http://0.0.0.0:3000/mine Block with index=1 is mined.
첫 번째 블록을 성공적으로 채굴하여 chain
에 추가한 것 같습니다. chain
을 다시 확인하고 채굴된 블록이 포함되어 있는지 확인해 보겠습니다.
crystal_coin [master●] % curl http://0.0.0.0:3000/chain { "chain": [ { "index": 0, "current_hash": "00d469d383005b4303cfa7321c02478ce76182564af5d16e1a10d87e31e2cb30", "nonce": 363, "previous_hash": "0", "transactions": [ ], "timestamp": "2018-05-23T01:59:52+0300" }, { "index": 1, "current_hash": "003c05da32d3672670ba1e25ecb067b5bc407e1d5f8733b5e33d1039de1a9bf1", "nonce": 320, "previous_hash": "00d469d383005b4303cfa7321c02478ce76182564af5d16e1a10d87e31e2cb30", "transactions": [ { "from": "ekis", "to": "huslks", "amount": 7090 }, { "from": "ekis", "to": "huslks", "amount": 70900 } ], "timestamp": "2018-05-23T02:02:38+0300" } ] }
합의와 탈중앙화
이것은 멋지다. 우리는 거래를 수락하고 새로운 블록을 채굴할 수 있는 기본 블록체인을 확보했습니다. 그러나 우리가 지금까지 구현한 코드는 단일 컴퓨터에서 실행되는 반면 블록체인의 핵심은 분산되어야 한다는 것입니다. 그러나 그것들이 탈중앙화되어 있다면 그것들이 모두 동일한 사슬을 반영하도록 하려면 어떻게 해야 할까요?
이것이 Consensus
의 문제입니다.
네트워크에 둘 이상의 노드가 필요한 경우 합의 알고리즘을 구현해야 합니다.
새 노드 등록
합의 알고리즘을 구현하려면 네트워크의 이웃 노드에 대해 노드에 알리는 방법이 필요합니다. 우리 네트워크의 각 노드는 네트워크에 있는 다른 노드의 레지스트리를 유지해야 합니다. 따라서 더 많은 엔드포인트가 필요합니다.
- [POST]
/nodes/register
: URL 형식의 새 노드 목록을 수락합니다. - [GET]
/nodes/resolve
: 모든 충돌을 해결하는 합의 알고리즘을 구현하여 노드에 올바른 체인이 있는지 확인합니다.
블록체인의 생성자를 수정하고 노드를 등록하는 방법을 제공해야 합니다.
--- a/src/crystal_coin/blockchain.cr +++ b/src/crystal_coin/blockchain.cr @@ -7,10 +7,12 @@ module CrystalCoin getter chain getter uncommitted_transactions + getter nodes def initialize @chain = [ Block.first ] @uncommitted_transactions = [] of Block::Transaction + @nodes = Set(String).new [] of String end def add_transaction(transaction)
노드 목록을 보유하기 위해 String
유형의 Set
데이터 구조를 사용했다는 점에 유의하십시오. 이것은 새로운 노드의 추가가 멱등성이고 특정 노드를 몇 번이나 추가하더라도 정확히 한 번만 표시되도록 하는 저렴한 방법입니다.
이제 Consensus
에 새 모듈을 추가하고 첫 번째 메서드인 register_node(address)
를 구현해 보겠습니다.
require "uri" module CrystalCoin module Consensus def register_node(address : String) uri = URI.parse(address) node_address = "#{uri.scheme}:://#{uri.host}" node_address = "#{node_address}:#{uri.port}" unless uri.port.nil? @nodes.add(node_address) rescue raise "Invalid URL" end end end
register_node
함수는 노드의 URL을 구문 분석하고 형식을 지정합니다.
그리고 여기에 /nodes/register
끝점을 생성해 보겠습니다.
post "/nodes/register" do |env| nodes = env.params.json["nodes"].as(Array) raise "Empty array" if nodes.empty? nodes.each do |node| blockchain.register_node(node.to_s) end "New nodes have been added: #{blockchain.nodes}" end
이제 이 구현을 통해 여러 노드에서 문제에 직면할 수 있습니다. 몇몇 노드의 체인 사본은 다를 수 있습니다. 이 경우 전체 시스템의 무결성을 유지하기 위해 일부 버전의 체인에 동의해야 합니다. 합의를 이끌어내야 합니다.
이 문제를 해결하기 위해 가장 긴 유효한 체인이 사용되는 규칙을 만들 것입니다. 이 알고리즘을 사용하여 네트워크의 노드 간에 합의에 도달합니다. 이 접근 방식의 이면에 있는 이유는 가장 긴 체인이 가장 많은 작업을 수행한 것으로 추정하기 때문입니다.
module CrystalCoin module Consensus ... def resolve updated = false @nodes.each do |node| node_chain = parse_chain(node) return unless node_chain.size > @chain.size return unless valid_chain?(node_chain) @chain = node_chain updated = true rescue IO::Timeout puts "Timeout!" end updated end ... end end
resolve
는 모든 인접 노드를 순환하고 해당 체인을 다운로드하고 valid_chain?
방법. 길이가 우리보다 긴 유효한 체인이 발견되면 우리가 교체합니다.
이제 parse_chain()
및 valid_chain?()
개인 메서드를 구현해 보겠습니다.
module CrystalCoin module Consensus ... private def parse_chain(node : String) node_url = URI.parse("#{node}/chain") node_chain = HTTP::Client.get(node_url) node_chain = JSON.parse(node_chain.body)["chain"].to_json Array(CrystalCoin::Block).from_json(node_chain) end private def valid_chain?(node_chain) previous_hash = "0" node_chain.each do |block| current_block_hash = block.current_hash block.recalculate_hash return false if current_block_hash != block.current_hash return false if previous_hash != block.previous_hash return false if current_block_hash[0..1] != "00" previous_hash = block.current_hash end return true end end end
For parse_chain()
we:
- Issue a
GET
HTTP request usingHTTP::Client.get
to/chain
end-point. - Parse the
/chain
JSON response usingJSON.parse
. - Extract an array of
CrystalCoin::Block
objects from the JSON blob that was returned usingArray(CrystalCoin::Block).from_json(node_chain)
.
There is more than one way of parsing JSON in Crystal. The preferred method is to use Crystal's super-handy JSON.mapping(key_name: Type)
functionality that gives us the following:
- A way to create an instance of that class from a JSON string by running
Class.from_json
. - A way to serialize an instance of that class into a JSON string by running
instance.to_json
. - Getters and setters for keys defined in that class.
In our case, we had to define JSON.mapping
in CrystalCoin::Block
object, and we removed property
usage in the class, like so:
module CrystalCoin class Block JSON.mapping( index: Int32, current_hash: String, nonce: Int32, previous_hash: String, transactions: Array(Transaction), timestamp: Time ) ... end end
Now for Blockchain#valid_chain?
, we iterate through all of the blocks, and for each we:
- Recalculate the hash for the block using
Block#recalculate_hash
and check that the hash of the block is correct:
module CrystalCoin class Block ... def recalculate_hash @nonce = proof_of_work @current_hash = calc_hash_with_nonce(@nonce) end end end
- Check each of the blocks linked correctly with their previous hashes.
- Check the block's hash is valid for the number of zeros (
difficulty
in our case00
).
And finally we implement /nodes/resolve
end-point:
get "/nodes/resolve" do if blockchain.resolve "Successfully updated the chain" else "Current chain is up-to-date" end end
끝났다! You can find the final code on GitHub.
The structure of our project should look like this:
crystal_coin [master●] % tree src/ src/ ├── crystal_coin │ ├── block.cr │ ├── blockchain.cr │ ├── consensus.cr │ ├── proof_of_work.cr │ ├── transaction.cr │ └── version.cr ├── crystal_coin.cr └── server.cr
Let's Try it Out
- Grab a different machine, and run different nodes on your network. Or spin up processes using different ports on the same machine. In my case, I created two nodes on my machine, on a different port to have two nodes:
http://localhost:3000
andhttp://localhost:3001
. - Register the second node address to the first node using:
crystal_coin [master●●] % curl -X POST http://0.0.0.0:3000/nodes/register -H "Content-Type: application/json" -d '{"nodes": ["http://0.0.0.0:3001"]}' New nodes have been added: Set{"http://0.0.0.0:3001"}%
- Let's add a transaction to the second node:
crystal_coin [master●●] % curl -X POST http://0.0.0.0:3001/transactions/new -H "Content-Type: application/json" -d '{"from": "eqbal", "to":"spiderman", "amount": 100}' Transaction #<CrystalCoin::Block::Transaction:0x1039c29c0> has been added to the node%
- Let's mine transactions into a block at the second node:
crystal_coin [master●●] % curl http://0.0.0.0:3001/mine Block with index=1 is mined.%
- At this point, the first node has only one block (genesis block), and the second node has two nodes (genesis and the mined block):
crystal_coin [master●●] % curl http://0.0.0.0:3000/chain {"chain":[{"index":0,"current_hash":"00fe9b1014901e3a00f6d8adc6e9d9c1df03344dda84adaeddc8a1c2287fb062","nonce":157,"previous_hash":"0","transactions":[],"timestamp":"2018-05-24T00:21:45+0300"}]}%
crystal_coin [master●●] % curl http://0.0.0.0:3001/chain {"chain":[{"index":0,"current_hash":"007635d82950bc4b994a91f8b0b20afb73a3939e660097c9ea8416ad614faf8e","nonce":147,"previous_hash":"0","transactions":[],"timestamp":"2018-05-24T00:21:38+0300"},{"index":1,"current_hash":"00441a4d9a4dfbab0b07acd4c7639e53686944953fa3a6c64d2333a008627f7d","nonce":92,"previous_hash":"007635d82950bc4b994a91f8b0b20afb73a3939e660097c9ea8416ad614faf8e","transactions":[{"from":"eqbal","to":"spiderman","amount":100}],"timestamp":"2018-05-24T00:23:57+0300"}]}%
- Our goal is to update the chain in the first node to include the newly generated block at the second one. So let's resolve the first node:
crystal_coin [master●●] % curl http://0.0.0.0:3000/nodes/resolve Successfully updated the chain%
Let's check if the chain in the first node has updated:
crystal_coin [master●●] % curl http://0.0.0.0:3000/chain {"chain":[{"index":0,"current_hash":"007635d82950bc4b994a91f8b0b20afb73a3939e660097c9ea8416ad614faf8e","nonce":147,"previous_hash":"0","transactions":[],"timestamp":"2018-05-24T00:21:38+0300"},{"index":1,"current_hash":"00441a4d9a4dfbab0b07acd4c7639e53686944953fa3a6c64d2333a008627f7d","nonce":92,"previous_hash":"007635d82950bc4b994a91f8b0b20afb73a3939e660097c9ea8416ad614faf8e","transactions":[{"from":"eqbal","to":"spiderman","amount":100}],"timestamp":"2018-05-24T00:23:57+0300"}]}%
만세! Our Crystal language example works like a charm, and I hope you found this lengthy tutorial crystal clear, pardon the pun.
마무리
This Crystal language tutorial covered the fundamentals of a public blockchain. If you followed along, you implemented a blockchain from scratch and built a simple application allowing users to share information on the blockchain.
We've made a fairly sized blockchain at this point. Now, CrystalCoin
can be launched on multiple machines to create a network, and real CrystalCoins
can be mined.
I hope that this has inspired you to create something new, or at the very least take a closer look at Crystal programming.
Note: The code in this tutorial is not ready for real-world use. Please refer to this as a general guide only.