비트코인 네트워크 접속
C# : 비트코인 네트워크 접속하기
비트코인은 P2P 네트워크를 사용하기 때문에, 비트코인 클라이언트는 그 Peer 컴퓨터에 접속한다.
비트코인 클라이언트에서 P2P 호스트에 접속해서는 가장 먼저 자신의 버전 정보를 담은 version 메시지를 전달하고
상대편으로부터 version acknowledge (verack) 메시지를 전달받게 된다.
아래는 Peer 네트워크에 접속하는 일반적인 절차를 기술한 것이다.
-
먼저 접속한 Peer 호스트를 찾는다 (이를 peer discovery 라 부른다).
당연한 이야기지만 Peer에 연결하기 위해서는, 가장 먼저 Peer가 누구인지 알아야 한다.
비트코인은 Peer를 발견하기 위해 여러 방법을 사용한다.
먼저 만약 비트코인 클라이언트가 이전에 Peer에 접속했었다면, 이 정보를 캐시 혹은 DB에 저장해 두고 사용할 수 있다.
만약 처음으로 다른 Peer에 접속한다면, DNS Seeding 이라는 방법을 사용할 수 있다.
DNS Seed는 Peer IP 정보를 리턴하는 잘 알려진 호스트들을 일컫는다.
-
TCP를 사용하여 Peer 네트워크에 접속한다.
-
전달할 비트코인 메시지를 생성한다.
비트코인 네트워크 프로토콜 안의 모든 메시지는 동일한 컨테이너 포맷을 사용하는데, 필수적으로 필요한 메시지 헤더와
선택적 옵션인 메시지 바디 (Payload)로 구성되어 있다.
클라이언트가 처음 Peer 에 접속할 때, 클라이언트는 항상 컨트롤 메시지의 일종인 version 메시지를 보내게 되어 있다.
이 version 메시지는 비트코인 클라이언트의 정보를 수신 노드에 제공하게 된다.
양쪽 노드가 version 메시지를 교환하기 전까지는 다른 종류의 메시지는 받아들여지지 않는다.
-
Peer에 메시지를 전달한다.
-
Peer로부터 메시지를 전달 받는다.
테스트넷에 접속하는 방법
테스트를 위해 여기서 테스트넷(testnet3)에 접속해 보기로 한다.
복잡한 프로세스를 간략히 하고 Peer 접속과 초기 메시지 접속에 포커스하기 위해, 여기서는 Peer discovery 프로세스를
생략하고 잘 알려진 Peer 호스트를 하드코드해서 사용한다.
잘 알려진 테스트넷 Peer 호스트는 http://tpfaucet.appspot.com 사이트에서
[TestNet node] 섹션을 참고하면 된다.
Peer 호스트에 접속하기 위해, 여기서는 .NET Framework의 TcpClient 클래스를 사용한다.
아래 샘플 코드는 testnet3에 접속하는 방법을 예시하고 있다.
위에서 언급했듯이 이 코드에서 Peer 호스트로서 52.4.156.236를 하드코드해서 사용하였다.
string peerIP = "52.4.156.236"; // hardcoded for now
int peerPort = 18333; // mainnet: 8333, testnet3: 18333
// Create a version message payload
var payload = VersionMessage.CreatePayload(BitcoinNetwork.Testnet3);
// Create a message
byte[] message = Message.Create(BitcoinNetwork.Testnet3, "version", payload);
// TCP connection
TcpClient cli = new TcpClient();
cli.Connect(peerIP, peerPort);
// Write message to peer network
NetworkStream stream = cli.GetStream();
stream.Write(message, 0, message.Length);
// Receive message from peer network
var result = new byte[1024];
int readCount = stream.Read(result, 0, result.Length);
Debug.WriteLine(result);
cli.Close();
Peer 호스트에 연결한 후에 비트코인 클라이언트는 처음 version 메시지를 보내게 되어 있다.
version 메시지를 생성하기 위해서는 version 메시지 구조에 필요한 모든 필드들을 채워야 한다.
각 메시지 필드에 대한 자세한 설명은 https://bitcoin.org/en/developer-reference#version
웹사이트에 자세히 소개되어 있다.
아래 샘플 코드는 하나의 version 메시지를 생성하는 방법을 예시한 것이다.
이 샘플에서는 version 필드에 "70002" (타 버전 번호 사용가)
을 사용하였고, 송수신 노드에 대한 IPv6 주소로 로컬 루프백 주소인 "::ffff:127.0.0.1" 을 사용하였다.
public class VersionMessage
{
public static byte[] CreatePayload(BitcoinNetwork network)
{
int iPort = 0;
switch (network)
{
case BitcoinNetwork.Mainnet:
iPort = 8333;
break;
case BitcoinNetwork.Testnet3:
iPort = 18333;
break;
case BitcoinNetwork.Regnet:
iPort = 18444;
break;
}
byte[] port = BitConverter.GetBytes((UInt16)iPort).Reverse().ToArray();
Random rand = new Random(DateTime.Now.Millisecond);
IPAddress loopback = IPAddress.Parse("::ffff:127.0.0.1");
byte[] local = loopback.GetAddressBytes();
int version = 70002; // can vary
UInt64 services = 0;
Int64 timestamp = (int)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds;
UInt64 addr_recv_services = 0;
byte[] addr_recv_IP_address = local;
byte[] addr_recv_port = port;
UInt64 addr_trans_services = 0;
byte[] addr_trans_IP_address = local;
byte[] addr_trans_port = port;
UInt64 nonce = (ulong)rand.Next();
byte user_agent_bytes = 0;
string user_agent = null;
Int32 start_height = 4000; // can vary
bool relay = false;
MemoryStream mem = new MemoryStream();
using (BinaryWriter wr = new BinaryWriter(mem))
{
wr.Write(version);
wr.Write(services);
wr.Write(timestamp);
wr.Write(addr_recv_services);
wr.Write(addr_recv_IP_address, 0, 16);
wr.Write(addr_recv_port, 0, 2);
wr.Write(addr_trans_services);
wr.Write(addr_trans_IP_address, 0, 16);
wr.Write(addr_trans_port, 0, 2);
wr.Write(nonce);
wr.Write(user_agent_bytes);
if (user_agent_bytes > 0)
{
wr.Write(user_agent);
}
wr.Write(start_height);
wr.Write(relay);
}
byte[] msg = mem.ToArray();
mem.Close();
return msg;
}
}
일단 version 메시지 Payload가 생성하였으면, 이제 비트코인 메시지를 생성해야 하는데,
이는 메시지 헤더와 version 메시지 Payload 를 합쳐서 만들게 된다.
하나의 비트코인 메시지는 반드시 메시지 헤더가 있어야 하고, 메시지 바디 즉 Payload는 선택적 옵션으로 추가할 수 있다.
메시지 헤더는 크게 매직넘버 (Packet Prefix), 명령어 이름, Payload 크기, 체크섬 등 4 부분으로 구성되어 있다.
(메시지 헤더에 대한 자세한 사항은
여기를) 참고한다)
메시지 바디 즉 Payload는 메시지 헤더 바로 다음에 추가한다.
public class Message
{
public static byte[] Create(BitcoinNetwork network, string command, byte[] payload)
{
// magic number for Message
uint magic = 0;
switch(network)
{
case BitcoinNetwork.Mainnet:
magic = 0xD9B4BEF9;
break;
case BitcoinNetwork.Testnet3:
magic = 0x0709110B;
break;
case BitcoinNetwork.Regnet:
magic = 0xDAB5BFFA;
break;
}
// checksum: first 4 bytes of sha256(sha256(payload))
var sha = SHA256.Create();
var shaHash = sha.ComputeHash(sha.ComputeHash(payload));
byte[] checksum = shaHash.Take(4).ToArray();
// command is always 12 bytes long. Add null padding.
byte[] cmd = Enumerable.Repeat<byte>(0x00, 12).ToArray();
byte[] cmdBytes = Encoding.ASCII.GetBytes(command);
Array.Copy(cmdBytes, cmd, cmdBytes.Length);
MemoryStream mem = new MemoryStream();
using (var wr = new BinaryWriter(mem))
{
wr.Write(magic);
wr.Write(cmd, 0, 12);
wr.Write(payload.Length);
wr.Write(checksum, 0, 4);
wr.Write(payload, 0, payload.Length);
}
byte[] msg = mem.ToArray();
mem.Close();
return msg;
}
}
위 샘플 코드를 실행할 때, WireShark 같은 유틸러티를 동시에 실행하여 네크워크 패킷을 캡처하고 버전 메시지가 제대로 교환되는지 체크할 수 있다.
WireShark는 bitcoin 패킷을 해석할 수 있기 때문에, 네트워크 캡처후 bitcoin 을 필터링하면 쉽게 비트코인 교환 메시지를 살펴볼 수 있다.
아래 그림은 비트코인 초기 version 메시지의 응답으로 verack 메시지가 보내져 왔음을 보여준다.