บันทึกการเงิน
กติกาทุกข้อที่บทหลักเล่าไว้มีตัวตนจริงในรูปของโค้ดที่รันได้ และชุดแรกสุดยังอยู่ครบ หน้านี้คือการเอาสิ่งที่คุณเข้าใจแล้วไปทาบกับโค้ดจริงทีละชั้น
อ่านบทหลักก่อนจะได้ประโยชน์กว่ามาก
หน้านี้แยกออกมาจากบท บิตคอยน์ตั้งแต่ศูนย์ ซึ่งอธิบายกติกาทั้งหมดไว้แล้วโดยไม่ต้องเปิดโค้ดสักบรรทัด ตรงนี้ไม่ได้อธิบายกติกาซ้ำ แต่ชี้ว่ากติกาแต่ละข้อไปอยู่ตรงไหนในไฟล์จริง
ทุกอย่างที่เล่ามาทั้งบทเป็นกติกา และกติกาพวกนั้นมีตัวตนจริงในรูปของโค้ดที่รันได้ ที่สนุกคือชุดแรกสุดยังอยู่ครบและอ่านได้ทั้งหมด บทนี้เหลืออีกนิดเดียวแล้ว ผมเลยอยากพาเปิดฝาดูว่ากติกาแต่ละข้อหน้าตาเป็นยังไงในโค้ดจริง แล้วค่อย ๆ ต่อขึ้นมาทีละชั้นเหมือนที่เราต่อความเข้าใจกันมา
ชุดที่ปล่อยเมื่อวันที่ 9 มกราคม 2009 ชื่อ Bitcoin v0.1 เป็นภาษา C++ รันบนวินโดวส์อย่างเดียว ทั้งโปรเจกต์มี 19,820 บรรทัด ซึ่งน้อยกว่าที่คนส่วนใหญ่คาดไว้มาก แต่สิ่งที่ผมคิดว่าน่าสนใจกว่าตัวเลขรวม คือสัดส่วนว่าบรรทัดพวกนั้นไปกองอยู่ตรงไหน
| ไฟล์ | บรรทัด | ทำอะไร |
|---|---|---|
| ui.cpp, uibase.cpp และเฮดเดอร์ | 6,171 | หน้าต่างโปรแกรมบนวินโดวส์ ปุ่ม ช่องกรอก รายการธุรกรรม |
| main.cpp, main.h | 3,977 | กติกาทั้งหมด บล็อก ธุรกรรม การขุด การเลือกสายที่ยาวกว่า |
| serialize.h, uint256.h, bignum.h, base58.h | 2,600 | เครื่องมือพื้นฐาน เลข 256 บิต การแปลงเป็นไบต์ การเข้ารหัสที่อยู่ |
| net.cpp, net.h | 1,876 | ต่อซ็อกเก็ตกับเพื่อนบ้าน ส่งและรับข้อความ |
| script.cpp, script.h | 1,724 | ภาษาสคริปต์ที่ใช้ล็อกและปลดล็อกเงินแต่ละก้อน |
| db.cpp, db.h | 1,024 | เก็บสมุดลงดิสก์ด้วย Berkeley DB |
| util.cpp, util.h | 772 | ล็อก เวลา เธรด ฟังก์ชันจิปาถะ |
| sha.cpp, sha.h | 731 | ตัว SHA-256 ที่ปรับให้ขุดได้เร็วขึ้น |
| market.cpp, market.h | 446 | หน้าตลาดซื้อขายที่ทำค้างไว้ ไม่เคยเปิดใช้ |
| irc.cpp, irc.h | 272 | หาเพื่อนบ้านครั้งแรกด้วยการเข้าห้องแชต IRC |
| key.h, headers.h | 227 | กุญแจคู่ผ่าน OpenSSL และการรวมเฮดเดอร์ |
| รวม | 19,820 | ทั้งโปรเจกต์ |
สัดส่วนนี้เล่าอะไรบางอย่าง คนที่เขียนโปรแกรมนี้ไม่ได้ตั้งใจปล่อยเปเปอร์แล้วรอให้คนอื่นทำต่อ
เขานั่งประกอบปุ่มกับช่องกรอกเพื่อให้คนธรรมดาเปิดใช้ได้ตั้งแต่วันแรก
ในกองนั้นยังมีไฟล์ market.cpp ที่เป็นหน้าตลาดซื้อขายทำค้างไว้ 446 บรรทัด และไม่เคยถูกเปิดใช้เลย
ที่เหลือคือการไล่โค้ดจริงทีละชั้น ซึ่งอ่านหรือไม่อ่านก็ไม่กระทบเนื้อหาส่วนอื่นของบทนี้ ผมพับเก็บไว้เป็นชั้น ๆ ใครสนใจชั้นไหนกดดูชั้นนั้นได้เลย
ตัวเลข 80 ไบต์ที่บทหลักใช้ตอนเล่าเรื่องต้นไม้เมอร์เคิล มาจากตรงนี้
class CBlock
{
public:
// header
int nVersion;
uint256 hashPrevBlock;
uint256 hashMerkleRoot;
unsigned int nTime;
unsigned int nBits;
unsigned int nNonce;
// network and disk
vector<CTransaction> vtx;
ไล่ดูทีละช่องแล้วจะเจอทุกอย่างที่บทหลักเล่าไว้ hashPrevBlock คือตราที่ปั๊มคาบหน้ากระดาษ
hashMerkleRoot คือแฮชเดียวที่สรุปธุรกรรมทั้งบล็อก nBits คือความยากของรอบนั้น
และ nNonce คือช่องว่างที่นักขุดเติมเลขเดาลงไป
รวมกันได้ 4 บวก 32 บวก 32 บวก 4 บวก 4 บวก 4 เท่ากับ 80 ไบต์พอดี ไม่ว่าบล็อกนั้นจะมีธุรกรรมห้าพันรายการหรือรายการเดียว
ส่วน vtx ที่อยู่ข้างล่างคือรายการธุรกรรมจริง ซึ่งอยู่นอกหัวบล็อกและไม่ถูกแฮชตอนขุด
วงจรการขุดที่บทหลักวาดไว้ ในโค้ดจริงคือลูปนี้
uint256 hashTarget = CBigNum().SetCompact(pblock->nBits).getuint256();
uint256 hash;
loop
{
BlockSHA256(&tmp.block, nBlocks0, &tmp.hash1);
BlockSHA256(&tmp.hash1, nBlocks1, &hash);
if (hash <= hashTarget)
{
pblock->nNonce = tmp.block.nNonce;
assert(hash == pblock->GetHash());
// เจอแล้ว ประกาศบล็อกออกไป
break;
}
if ((++tmp.block.nNonce & 0x3ffff) == 0)
{
// ทุก ๆ 262144 รอบ แวะเช็คว่ามีใครเจอบล็อกก่อนเราหรือยัง
}
}
ตรงนี้มีเรื่องที่ผมเล่าไม่ครบไว้ตอนต้นบท และจุดนี้เป็นจังหวะที่ควรเฉลย
เงื่อนไขจริงไม่ใช่การนับว่าขึ้นต้นด้วยศูนย์กี่ตัว แต่คือ hash <= hashTarget
คือเอาแฮชมามองเป็นเลขจำนวนเต็มขนาด 256 บิต แล้วถามว่ามันน้อยกว่าเป้าหมายหรือเปล่า
สองอย่างนี้ให้ผลใกล้เคียงกัน เพราะเลขที่จะน้อยพอก็ต้องขึ้นต้นด้วยศูนย์อยู่ดี แต่แบบเป้าหมายปรับได้ละเอียดกว่ามาก การนับศูนย์ขยับได้ทีละสิบหกเท่า ส่วนการเลื่อนเป้าหมายขยับได้ทีละนิด ซึ่งจำเป็นสำหรับการรักษาจังหวะสิบนาทีให้แม่น
อีกบรรทัดที่น่าสังเกตคือ BlockSHA256 ถูกเรียกสองครั้งซ้อน แฮชของบล็อกในบิตคอยน์คือการแฮชสองรอบเสมอ
รอบแรกแฮชหัวบล็อก รอบสองแฮชผลลัพธ์ของรอบแรกอีกที
ตารางการออกเหรียญยาว 33 ยุคที่อยู่ข้างบน กับเพดาน 20,999,999.9769 เหรียญที่คนถกกันทั้งโลก ทั้งหมดมาจากฟังก์ชันนี้
int64 CBlock::GetBlockValue(int64 nFees) const
{
int64 nSubsidy = 50 * COIN;
// Subsidy is cut in half every 4 years
nSubsidy >>= (nBestHeight / 210000);
return nSubsidy + nFees;
}
บรรทัดที่ทำงานจริงมีบรรทัดเดียวคือ nSubsidy >>= (nBestHeight / 210000)
เครื่องหมาย >>= คือการเลื่อนบิตไปทางขวา ซึ่งมีค่าเท่ากับการหารด้วยสองแบบปัดลง
อยู่ในยุคที่ 5 ก็เลื่อนไป 4 ครั้ง เท่ากับหารสองสี่หน จาก 50 เหลือ 3.125 เหรียญ
และเพราะ nSubsidy เป็นจำนวนเต็มหน่วยซาโตชิ พอเลื่อนไปมากพอมันจะกลายเป็นศูนย์เอง
เพดาน 21 ล้านจึงไม่ได้ถูกเขียนไว้ที่ไหนในไฟล์นี้เลย มันเป็นผลลัพธ์ของการเลื่อนบิตบรรทัดนี้ล้วน ๆ
ตรงกับที่คำนวณให้ดูในตารางก่อนหน้า
unsigned int GetNextWorkRequired(const CBlockIndex* pindexLast)
{
const unsigned int nTargetTimespan = 14 * 24 * 60 * 60; // two weeks
const unsigned int nTargetSpacing = 10 * 60;
const unsigned int nInterval = nTargetTimespan / nTargetSpacing;
// Only change once per interval
if ((pindexLast->nHeight+1) % nInterval != 0)
return pindexLast->nBits;
// Go back by what we want to be 14 days worth of blocks
const CBlockIndex* pindexFirst = pindexLast;
for (int i = 0; pindexFirst && i < nInterval-1; i++)
pindexFirst = pindexFirst->pprev;
// Limit adjustment step
unsigned int nActualTimespan = pindexLast->nTime - pindexFirst->nTime;
if (nActualTimespan < nTargetTimespan/4)
nActualTimespan = nTargetTimespan/4;
if (nActualTimespan > nTargetTimespan*4)
nActualTimespan = nTargetTimespan*4;
// Retarget
CBigNum bnNew;
bnNew.SetCompact(pindexLast->nBits);
bnNew *= nActualTimespan;
bnNew /= nTargetTimespan; nInterval คำนวณจากสองสัปดาห์หารด้วยสิบนาที ได้ 2,016 ซึ่งเป็นที่มาของเลขที่ผมอ้างไว้ตอนพูดเรื่องกำลังขุด
ส่วนการหนีบไว้ที่หนึ่งในสี่และสี่เท่าคือกันไม่ให้ความยากกระโดดเกินสี่เท่าในรอบเดียว
แม้กำลังขุดจะหายไปทันทีทั้งโลกก็ตาม
บั๊กเล็ก ๆ ที่แก้ไม่ได้แล้ว
ลองอ่านบรรทัด for (int i = 0; pindexFirst && i < nInterval-1; i++) ดี ๆ
มันเดินถอยหลังไป 2,015 บล็อก ซึ่งกินช่วงเวลาแค่ 2,015 ช่วง
แล้วเอาเวลาที่วัดได้ไปเทียบกับเป้าหมายสองสัปดาห์ที่ตั้งไว้สำหรับ 2,016 ช่วง
ผลคือบิตคอยน์เล็งจังหวะเฉลี่ยไว้ที่ราว 10.005 นาทีต่อบล็อก แทนที่จะเป็น 10 นาทีเป๊ะ
คลาดไปราว 0.05% ซึ่งเล็กจนไม่มีใครเดือดร้อน
แต่แก้ไม่ได้แล้วเพราะการแก้จะเปลี่ยนกติกา
และทำให้โหนดที่ไม่ได้อัปเดตแยกสายออกไปทันที บั๊กบรรทัดนี้จึงยังรันอยู่ในทุกโหนดทั่วโลกจนถึงวันนี้
หัวข้อร้อยหน้ากระดาษบอกไปแล้วว่าบล็อกกำเนิดถูกเขียนค้างไว้ในโค้ด ตรงนี้คือหน้าตาของมันจริง ๆ
char* pszTimestamp = "The Times 03/Jan/2009 Chancellor on brink of second bailout for banks";
CTransaction txNew;
txNew.vin.resize(1);
txNew.vout.resize(1);
txNew.vin[0].scriptSig = CScript() << 486604799 << CBigNum(4) << vector<unsigned char>(...);
txNew.vout[0].nValue = 50 * COIN;
txNew.vout[0].scriptPubKey = CScript() << CBigNum("0x5F1DF16B2B704C8A...") << OP_CHECKSIG;
CBlock block;
block.vtx.push_back(txNew);
block.hashPrevBlock = 0;
block.hashMerkleRoot = block.BuildMerkleTree();
block.nVersion = 1;
block.nTime = 1231006505;
block.nBits = 0x1d00ffff;
block.nNonce = 2083236893;
assert(block.GetHash() == hashGenesisBlock);
บรรทัดบนสุดคือพาดหัวหนังสือพิมพ์ที่เล่าไปในหัวข้อประวัติ ในโค้ดมันถูกยัดเข้าไปใน scriptSig
ซึ่งเป็นช่องที่ปกติใช้ใส่ลายเซ็นของคนจ่าย บล็อกแรกไม่มีคนจ่าย ช่องนั้นเลยว่าง แล้วเขาใช้มันฝากข้อความแทน
block.hashPrevBlock = 0 คือคำตอบของปัญหาไก่กับไข่ ส่วน nTime = 1231006505
คือเวลานับเป็นวินาที ซึ่งแปลงออกมาได้เป็นวันที่ 3 มกราคม 2009 เวลา 18:15:05 ตามเวลามาตรฐานสากล
และ nNonce = 2083236893 คือเลขที่เขาต้องนั่งเดาจนเจอเหมือนนักขุดคนอื่นทุกประการ ไม่มีทางลัดให้ตัวเอง
เหรียญ 50 เหรียญแรกที่ไม่มีใครใช้ได้
รางวัล 50 เหรียญของบล็อกกำเนิดใช้จ่ายไม่ได้เลย เพราะวิธีที่โค้ดชุดนี้เขียนบล็อกกำเนิดลงฐานข้อมูล ทำให้ธุรกรรมใบนั้นไม่ถูกบันทึกเข้าไปในดัชนีธุรกรรมเหมือนบล็อกอื่น โหนดทุกตัวจึงมองไม่เห็นเงินก้อนนั้น ถึงจะมีกุญแจก็ใช้ไม่ได้ และตลอดสิบเจ็ดปีที่ผ่านมามีคนโอนบิตคอยน์เข้าไปที่อยู่นั้นเรื่อย ๆ เป็นเหมือนการวางดอกไม้ ซึ่งทุกเหรียญที่โอนเข้าไปก็หายไปตลอดกาลเหมือนกัน
เหลือคำถามข้อสุดท้าย โปรแกรมที่เพิ่งเปิดครั้งแรกในเครื่องคุณ ยังไม่รู้จักใครในโลกเลย แล้วมันไปหาโหนดตัวอื่นเจอได้ยังไง ในเมื่อไม่มีเซิร์ฟเวอร์กลางให้ถาม
คำตอบของชุดแรกนั้นง่ายจนน่ารัก มันเข้าห้องแชต
Send(hSocket, strprintf("NICK %s\r", strMyName.c_str()).c_str());
Send(hSocket, "JOIN #bitcoin\r");
Send(hSocket, "WHO #bitcoin\r");
โปรแกรมต่อเข้าเซิร์ฟเวอร์ IRC ซึ่งเป็นระบบห้องแชตยุคเก่า แล้วเข้าไปในห้องชื่อ #bitcoin
จากนั้นสั่ง WHO เพื่อขอรายชื่อคนในห้อง โหนดแต่ละตัวตั้งชื่อเล่นของตัวเองเป็นเลขที่อยู่ไอพีที่เข้ารหัสไว้
คนที่อยู่ในห้องเดียวกันก็คือโหนดตัวอื่น อ่านชื่อเล่นแล้วต่อหากันได้เลย
วิธีนี้ถูกเลิกใช้ไปนานแล้วเพราะพึ่งพาเซิร์ฟเวอร์แชตที่ใครก็ปิดได้ ทุกวันนี้โหนดใหม่ถามที่อยู่จากรายชื่อ ที่ฝังไว้ในโปรแกรมและจากระบบชื่อโดเมนแทน แต่ผมชอบชั้นนี้ที่สุดในทั้งโปรเจกต์ เพราะมันเป็นหลักฐานว่าของที่กลายเป็นเรื่องใหญ่ระดับโลก ตอนเริ่มมันคือของที่คนคนเดียวประกอบให้พอใช้งานได้ก่อน
คำว่า deploy ในความหมายที่เราคุ้นกัน คือเอาโค้ดขึ้นเซิร์ฟเวอร์ ต่อฐานข้อมูล เปิดโดเมน กับบิตคอยน์ไม่มีขั้นตอนพวกนั้นเลยสักข้อ เพราะไม่มีเซิร์ฟเวอร์ให้ขึ้น
ไม่มีสวิตช์ให้เปิดระบบ ไม่มีวันเปิดตัวอย่างเป็นทางการ ระบบเริ่มทำงานตั้งแต่เครื่องแรกเริ่มรัน และมันโตขึ้นทีละเครื่องตามจำนวนคนที่ตัดสินใจกดรันโปรแกรมนั้น จากหนึ่ง เป็นสอง จนวันนี้กลายเป็นเครือข่ายที่มีกำลังขุด 909.6 เอกซะแฮชต่อวินาที
โค้ดทุกชิ้นในหน้านี้ยกมาจากซอร์สโค้ด Bitcoin v0.1 ที่เผยแพร่เมื่อ 9 มกราคม 2009 ตัดเฉพาะส่วนที่เกี่ยวข้องและใส่จุดไข่ปลาไว้ตรงที่ตัดออก โดยไม่แก้ตรรกะเดิม
mini-chain.ts ผมเขียนเองและรันจริงด้วย Node.js ก่อนใส่ลงหน้านี้