บทที่ 7: Authorization and Authentication
เมื่อคุณได้รับบิตคอยน์ คุณต้องตัดสินใจว่าใครจะได้รับสิทธิ์ในการใช้จ่ายเหรียญนั้น ซึ่งเรียกว่า authorization (การอนุญาต) นอกจากนี้ คุณต้องตัดสินใจด้วยว่า full node ควรแยกแยะผู้ที่ได้รับอนุญาตให้ใช้จ่ายออกจากคนอื่น ๆ อย่างไร ซึ่งเรียกว่า authentication (การยืนยันตัวตน) คำสั่งเกี่ยวกับการอนุญาตของคุณ และหลักฐานการยืนยันตัวตนจากผู้ใช้จ่าย จะถูกตรวจสอบโดย full node นับพัน ซึ่งต้องได้ข้อสรุปตรงกันว่าการใช้จ่ายนั้นได้รับอนุญาตและได้รับการยืนยันตัวตนอย่างถูกต้อง เพื่อให้ธุรกรรมดังกล่าวเป็นธุรกรรมที่ถูกต้อง
คำอธิบายดั้งเดิมของบิตคอยน์ใช้ public key ในการทำ authorization โดย Alice ส่งเหรียญให้ Bob ด้วยการใส่ public key ของเขาลงในเอาต์พุตของธุรกรรม ส่วนการทำ authentication มาจากการลงนาม (signature) โดย Bob ซึ่งผูกพันกับธุรกรรมการใช้จ่าย เช่น การที่ Bob ส่งต่อให้ Carol
ในเวอร์ชันแรกของบิตคอยน์ที่ถูกปล่อยออกมา ได้จัดเตรียมกลไกที่ยืดหยุ่นกว่าสำหรับทั้ง authorization และ authentication และการพัฒนาต่อมาทำให้กลไกเหล่านี้ยืดหยุ่นยิ่งขึ้นกว่าเดิม ในบทนี้ เราจะสำรวจฟีเจอร์เหล่านั้นและดูว่ามันถูกใช้งานอย่างแพร่หลายได้อย่างไร
Transaction Scripts and Script Language
เวอร์ชันแรก ๆ ของบิตคอยน์ได้แนะนำภาษาการเขียนโปรแกรมใหม่ชื่อว่า Script ซึ่งเป็นภาษาลักษณะคล้าย Forth ที่ทำงานบนสแตก ทั้งสคริปต์ที่ถูกวางไว้ในเอาต์พุต และสคริปต์อินพุตแบบ legacy ที่ใช้ในธุรกรรมการใช้จ่าย ล้วนเขียนด้วยภาษาสคริปต์นี้ทั้งสิ้น
Script เป็นภาษาที่เรียบง่ายมาก ต้องการการประมวลผลเพียงเล็กน้อย และไม่สามารถทำสิ่งซับซ้อนแบบที่ภาษาสมัยใหม่สามารถทำได้
เมื่อธุรกรรมแบบ legacy ยังเป็นรูปแบบที่ใช้กันมากที่สุด ธุรกรรมส่วนใหญ่ที่ประมวลผลบนเครือข่ายบิตคอยน์อยู่ในรูปแบบเดียวกันกับที่ alice จ่ายให้ bob ในบทก่อนหน้าและใช้สคริปต์ที่เรียกว่า pay to public key hash (P2PKH) แต่อย่างไรก็ตาม ธุรกรรมบิตคอยน์ไม่ได้จำกัดอยู่แค่สคริปต์ในรูปแบบนี้เท่านั้น จริง ๆ แล้วสคริปต์สามารถเขียนเพื่อกำหนดเงื่อนไขที่ซับซ้อนหลากหลายรูปแบบได้ แต่เพื่อที่จะเข้าใจสคริปต์ที่ซับซ้อนเหล่านั้น เราจำเป็นต้องเข้าใจพื้นฐานของสคริปต์ธุรกรรมและภาษาสคริปต์เสียก่อน
ในส่วนนี้ เราจะสาธิตองค์ประกอบพื้นฐานของภาษาสคริปต์สำหรับธุรกรรมบิตคอยน์ และแสดงให้เห็นว่าภาษานี้ใช้เพื่อกำหนดเงื่อนไขสำหรับการใช้จ่ายอย่างไร และจะสามารถทำให้เงื่อนไขนั้นสำเร็จได้อย่างไร
TIP: การตรวจสอบความถูกต้องของธุรกรรมบิตคอยน์ไม่ได้อาศัยรูปแบบตายตัว แต่เกิดจากการ รันภาษาสคริปต์ ซึ่งเปิดโอกาสให้สามารถกำหนดเงื่อนไขได้หลากหลายแทบไม่จำกัด
Turing Incompleteness
ภาษาสคริปต์ของธุรกรรมบิตคอยน์มีโอเปอเรเตอร์จำนวนมาก แต่ถูกจำกัดไว้อย่างจงใจในด้านสำคัญอย่างหนึ่ง คือ ไม่มีลูปหรือโครงสร้างควบคุมการไหลที่ซับซ้อน นอกเหนือจากการควบคุมแบบมีเงื่อนไขเท่านั้น สิ่งนี้ทำให้ภาษา ไม่เป็น Turing Complete หมายความว่าสคริปต์มีความซับซ้อนได้จำกัดและมีเวลาการประมวลผลที่คาดเดาได้ Script ไม่ใช่ภาษาสำหรับงานทั่วไป ข้อจำกัดเหล่านี้ช่วยป้องกันไม่ให้ภาษาใช้สร้างลูปไม่รู้จบหรือ “logic bomb” รูปแบบอื่นที่อาจแฝงอยู่ในธุรกรรมและถูกใช้โจมตีแบบปฏิเสธการให้บริการ (DoS) ต่อเครือข่ายบิตคอยน์ได้ โปรดจำไว้ว่า ทุกธุรกรรมจะถูกยืนยันโดย full node ทุกตัวในเครือข่ายบิตคอยน์ ภาษาแบบจำกัดช่วยป้องกันไม่ให้กลไกตรวจสอบธุรกรรมกลายเป็นช่องโหว่
Stateless Verification
ภาษาสคริปต์ของธุรกรรมบิตคอยน์เป็นแบบ stateless กล่าวคือไม่มีสถานะก่อนเริ่มการประมวลผลสคริปต์ และไม่มีการเก็บสถานะหลังจากสคริปต์รันเสร็จ ข้อมูลทั้งหมดที่จำเป็นสำหรับการประมวลผลสคริปต์อยู่ในสคริปต์และธุรกรรมที่กำลังรันสคริปต์เท่านั้น สคริปต์จะทำงานเหมือนกันทุกครั้งบนทุกระบบ หากระบบของคุณตรวจสอบสคริปต์ผ่าน คุณมั่นใจได้ว่าระบบอื่นในเครือข่ายบิตคอยน์ทั้งหมดก็จะตรวจสอบผ่านเช่นกัน หมายความว่า ธุรกรรมที่ถูกต้องจะถูกต้องสำหรับทุกคน และทุกคนรู้เช่นนั้น ความสามารถในการคาดเดาผลลัพธ์ได้แบบนี้เป็นประโยชน์สำคัญของระบบบิตคอยน์
Script Construction
กลไกการตรวจสอบธุรกรรมแบบดั้งเดิมของ Bitcoin อาศัยสองส่วนของสคริปต์ในการตรวจสอบธุรกรรม: output script และ input script
output script ระบุเงื่อนไขที่ต้องถูกทำให้สำเร็จเพื่อใช้จ่ายเอาต์พุตในอนาคต เช่น ใครมีสิทธิ์ใช้จ่ายเอาต์พุต และจะมีการตรวจสอบสิทธิ์อย่างไร
input script คือสคริปต์ที่ทำให้เงื่อนไขใน output script สำเร็จ และอนุญาตให้เอาต์พุตถูกใช้จ่ายได้ Input script เป็นส่วนหนึ่งของทุก ๆ อินพุตภายในธุรกรรม ส่วนใหญ่ในธุรกรรมแบบเดิมจะมีลายเซ็นดิจิทัลที่สร้างจาก private key ของผู้ใช้ แต่ input script ไม่จำเป็นต้องมีลายเซ็นเสมอไป
ทุกโหนดที่ตรวจสอบ Bitcoin จะตรวจสอบธุรกรรมโดยรัน output script และ input script ตามที่ได้กล่าวไว้ในบทที่ 4 อินพุตแต่ละตัวมี outpoint ที่อ้างถึงเอาต์พุตของธุรกรรมก่อนหน้า อินพุตยังมี input script อยู่ด้วย ซอฟต์แวร์ตรวจสอบจะคัดลอก input script ดึง UTXO ที่อินพุตอ้างถึง และคัดลอก output script จาก UTXO นั้น จากนั้น output script และ input script จะถูกประมวลผลรวมกัน อินพุตจะถูกพิจารณาว่าถูกต้องหาก input script ทำให้เงื่อนไขใน output script สำเร็จ (จะอธิบายในหัวข้อ Separate execution of output and input scripts) อินพุตทุกตัวจะถูกตรวจสอบแยกกันเป็นส่วนหนึ่งของการตรวจสอบธุรกรรมโดยรวม
โปรดสังเกตว่า ขั้นตอนข้างต้นเกี่ยวข้องกับการคัดลอกข้อมูลทั้งหมด ข้อมูลต้นฉบับในเอาต์พุตก่อนหน้าและในอินพุตปัจจุบันจะไม่ถูกเปลี่ยนแปลงเลย เอาต์พุตก่อนหน้าไม่ถูกเปลี่ยนแปลงและไม่ได้รับผลกระทบจากความพยายามใช้จ่ายที่ล้มเหลว มีเพียงธุรกรรมที่ถูกต้องซึ่งทำให้เงื่อนไขใน output script สำเร็จเท่านั้นที่จะทำให้เอาต์พุตถูกพิจารณาว่า “ถูกใช้จ่ายแล้ว”
การผสาน input script และ output script เพื่อประเมินสคริปต์ของธุรกรรม คือ ตัวอย่างของ output script และ input script ของธุรกรรม Bitcoin แบบดั้งเดิมที่พบมากที่สุด (การชำระเงินไปยัง public key hash) ซึ่งแสดงสคริปต์ที่รวมกันจากการนำทั้งสองสคริปต์มาต่อกันก่อนการตรวจสอบ

The script execution stack
ภาษาสคริปต์ของ Bitcoin ถูกเรียกว่า stack-based language เพราะใช้โครงสร้างข้อมูลที่เรียกว่า stack สแตกเป็นโครงสร้างข้อมูลที่เรียบง่ายมาก มองภาพได้เหมือนกองไพ่ สแตกมีสองการทำงานพื้นฐาน: push และ pop push คือการเพิ่มข้อมูลหนึ่งรายการไว้ด้านบนของสแตก ส่วน pop คือการนำรายการด้านบนสุดออกจากสแตก
ภาษาสคริปต์จะรันสคริปต์โดยประมวลผลรายการแต่ละรายการจากซ้ายไปขวา ตัวเลข (ค่าคงที่ของข้อมูล) จะถูก push ลงบนสแตก ตัวโอเปอเรเตอร์จะ push หรือ pop พารามิเตอร์หนึ่งตัวหรือมากกว่าจากสแตก นำไปประมวลผล และอาจ push ผลลัพธ์กลับลงสแตก ตัวอย่างเช่น OP_ADD จะ pop ข้อมูลสองรายการออกจากสแตก นำมาบวกกัน แล้ว push ผลรวมกลับลงบนสแตก
โอเปอเรเตอร์แบบมีเงื่อนไขจะประเมินเงื่อนไขและให้ผลลัพธ์เป็น boolean TRUE หรือ FALSE ตัวอย่างเช่น OP_EQUAL จะ pop ข้อมูลสองรายการจากสแตก และ push TRUE (TRUE แทนด้วยเลข 1) หากสองค่านั้นเท่ากัน หรือ push FALSE (แทนด้วยเลข 0) หากไม่เท่ากัน สคริปต์ของธุรกรรม Bitcoin มักมีโอเปอเรเตอร์แบบมีเงื่อนไขเพื่อสร้างผลลัพธ์ TRUE ที่แสดงว่าธุรกรรมนั้นถูกต้อง
A simple script (สคริปต์อย่างง่าย)
ตอนนี้เรามาลองประยุกต์สิ่งที่เราได้เรียนรู้เกี่ยวกับ scripts และ stacks กับตัวอย่างง่าย ๆ กัน
ในบทก่อนหน้าเราได้ยกสคริปต์อย่างง่ายมาตัวหนึ่งคือ
2 3 OP_ADD 5 OP_EQUAL
แสดงการทำงานของโอเปอเรเตอร์คณิตศาสตร์ OP_ADD ซึ่งทำการบวกตัวเลขสองตัวแล้ววางผลลัพธ์ไว้บนสแต็ก จากนั้นตามด้วยโอเปอเรเตอร์แบบเงื่อนไข OP_EQUAL ที่ตรวจสอบว่าผลรวมดังกล่าวเท่ากับ 5 หรือไม่ ในหนังสือเล่มนี้ เพื่อความกระชับ อาจมีการละคำนำหน้า OP_ ในบางตัวอย่าง หากต้องการรายละเอียดเพิ่มเติมเกี่ยวกับโอเปอเรเตอร์และฟังก์ชันทั้งหมดของสคริปต์ สามารถดูได้ที่หน้า Script ของ Bitcoin Wiki แม้ว่าผลลัพธ์สคริปต์แบบ Legacy ส่วนใหญ่จะอ้างอิง public key hash (ซึ่งโดยพื้นฐานคือ Bitcoin address แบบดั้งเดิม) เพื่อบังคับให้ต้องพิสูจน์ความเป็นเจ้าของก่อนจึงจะใช้เงินได้ แต่จริง ๆ แล้วสคริปต์ไม่จำเป็นต้องซับซ้อนขนาดนั้นก็ได้ สคริปต์ใด ๆ ที่ผสมกันระหว่าง output script และ input script แล้วให้ผลเป็น TRUE ถือว่า “ถูกต้อง”ดังนั้นสคริปต์คณิตศาสตร์ง่าย ๆ อย่างที่ใช้เป็นตัวอย่างในที่นี้ ก็ถือว่าเป็นสคริปต์ที่ถูกต้องเช่นกัน
ใช้ส่วนหนึ่งของสคริปต์คณิตศาสตร์เป็น output script
ให้ output script เป็น:
3 OP_ADD 5 OP_EQUAL
ซึ่งสามารถถูกทำให้สำเร็จ (satisfied) ด้วยธุรกรรมที่มี input script แบบนี้:
2
ซอฟต์แวร์ตรวจสอบความถูกต้อง (validation software) จะรวมสองสคริปต์เข้าด้วยกันกลายเป็น:
2 3 OP_ADD 5 OP_EQUAL
เมื่อสคริปต์นี้ถูกประมวลผล ตามที่เราเห็นในบทก่อนหน้า ผลลัพธ์สุดท้ายคือ OP_TRUE ทำให้ธุรกรรมนี้ “ถูกต้อง” แม้ว่านี่จะเป็น output script ที่ถูกต้อง แต่ให้สังเกตว่า UTXO ที่สร้างขึ้น สามารถถูกใช้จ่ายได้โดยใครก็ตาม ที่มีทักษะคณิตศาสตร์พอจะรู้ว่าเลข 2 จะทำให้สคริปต์นี้สำเร็จได้

TIP: ธุรกรรมจะถือว่า ถูกต้อง (valid) หากผลลัพธ์บนยอดสแต็กเป็นค่า TRUE ซึ่งหมายถึงค่าที่ไม่ใช่ศูนย์ใด ๆ ทั้งหมด ธุรกรรมจะถือว่า ไม่ถูกต้อง (invalid) หากผลบนยอดสแต็กเป็นค่า FALSE (ศูนย์ หรือสแต็กว่าง), หรือการประมวลผลสคริปต์ถูกหยุดโดยตรงด้วยโอเปอเรเตอร์บางตัว (เช่น VERIFY, OP_RETURN), หรือสคริปต์นั้นมีความผิดพลาดทางไวยากรณ์/ความหมาย (semantic) เช่น มีคำสั่ง OP_IF แต่ไม่มี OP_ENDIF ปิดท้าย ดูรายละเอียดเพิ่มเติมได้ที่หน้า Script ของ Bitcoin Wiki
สคริปต์ตัวอย่างถัดไปนี้จะมีความซับซ้อนขึ้นเล็กน้อย โดยมันคำนวณค่า 2 + 7 – 3 + 1 สังเกตว่าเมื่อสคริปต์มีโอเปอเรเตอร์หลายตัวต่อกัน สแต็กจะทำให้ผลลัพธ์ของโอเปอเรเตอร์ก่อนหน้า ถูกนำไปใช้ต่อโดยโอเปอเรเตอร์ตัวถัดไปได้
2 7 OP_ADD 3 OP_SUB 1 OP_ADD 7 OP_EQUAL
ลองตรวจสอบสคริปต์ก่อนหน้านี้ด้วยตัวเอง โดยใช้ดินสอและกระดาษ เมื่อการประมวลผลสคริปต์สิ้นสุดลง คุณควรจะเหลือค่าบนสแต็กเป็นค่า TRUE
Separate execution of output and input scripts
ในไคลเอนต์ Bitcoin เวอร์ชันแรก ๆ นั้น สคริปต์ของ output และ input ถูกต่อกันแล้วประมวลผลเป็นลำดับเดียว ด้วยเหตุผลด้านความปลอดภัย ซึ่งเกิดจากช่องโหว่ที่รู้จักกันในชื่อ 1 OP_RETURN bug ซึ่งวิธีนี้ถูกเปลี่ยนแปลงในปี 2010 เพื่อความปลอดภัย ในการใช้งานปัจจุบัน สคริปต์ทั้งสองจะถูกประมวลผลแยกกันโดยมีการคัดลอกสแต็กไปยังการประมวลผลครั้งถัดไป
อันดับแรก สคริปต์ input จะถูกประมวลผลโดยการดำเนินการของสแต็ก (stack execution engine) หากสคริปต์ input ถูกประมวลผลโดยไม่มีข้อผิดพลาดและไม่มีโอเปอเรชันเหลืออยู่ สแต็กจะถูกคัดลอกแล้วสคริปต์ output จะถูกประมวลผลต่อ หากผลลัพธ์ของการประมวลผลสคริปต์ output โดยใช้ข้อมูลสแต็กที่คัดลอกมาจาก input เป็น TRUE แสดงว่าสคริปต์ input สามารถแก้เงื่อนไขที่สคริปต์ output กำหนดได้ และด้วยเหตุนี้ input จึงเป็นการอนุญาตที่ถูกต้องสำหรับการใช้จ่าย UTXO นั้น แต่หากผลลัพธ์ใด ๆ นอกเหนือจาก TRUE ยังคงอยู่หลังการประมวลผลของสคริปต์รวมกัน แสดงว่า input นั้นไม่ถูกต้องเพราะล้มเหลวในการตอบสนองเงื่อนไขการใช้จ่ายที่วางไว้บน output
Pay to Public Key Hash
สคริปต์แบบ pay to public key hash (P2PKH) ใช้ output script ที่ภายในมีค่าแฮชซึ่งผูกพัน (commit) กับ public key หนึ่งค่า P2PKH เป็นที่รู้จักกันดีที่สุดในฐานะ Legacy Bitcoin address เอาต์พุตแบบ P2PKH สามารถถูกใช้จ่ายได้โดยการนำเสนอ public key ที่ตรงกับค่าแฮชที่ระบุไว้ และลายเซ็นดิจิทัลที่สร้างขึ้นด้วย private key ที่สอดคล้องกัน (ดูเรื่องนี้เพิ่มได้ในบทถัดไป) ส่วนตอนนี้มาดูตัวอย่างของ P2PKH output script กัน:
OP_DUP OP_HASH160 <Key Hash> OP_EQUALVERIFY OP_CHECKSIG
Key Hash คือข้อมูลที่เมื่อนำไปเข้ารหัส จะกลายเป็น Bitcoin address แบบ legacy ในรูปแบบ base58check แอปพลิเคชันส่วนใหญ่จะแสดงค่า public key hash ภายในสคริปต์ในรูปแบบเลขฐานสิบหก (hexadecimal) แทนที่จะเป็นรูปแบบ Bitcoin address แบบ base58check ที่ผู้ใช้คุ้นเคย ซึ่งจะขึ้นต้นด้วยตัวอักษร “1” output script ก่อนหน้านี้สามารถถูกทำให้สำเร็จ (satisfied) ได้ด้วย input script ในรูปแบบ:
<Signature> <Public Key>
เมื่อรวมสคริปต์ทั้งสองเข้าด้วยกัน จะได้สคริปต์ตรวจสอบความถูกต้อง (combined validation script) ดังนี้:
<Sig> <Pubkey> OP_DUP OP_HASH160 <Hash> OP_EQUALVERIFY OP_CHECKSIG
ผลลัพธ์จะเป็นค่า TRUE หาก input script มีลายเซ็นที่ถูกต้อง ซึ่งถูกสร้างขึ้นจาก private key ของของผู้ส่ง และสอดคล้องกับ public key hash ที่ถูกกำหนดไว้เป็นเงื่อนไขการใช้จ่ายนั้น

Scripted Multisignatures
สคริปต์แบบมัลติซิกเนเจอร์ (multisignature) กำหนดเงื่อนไขโดยบันทึกค่าของ public key จำนวน k ค่าไว้ในสคริปต์ และต้องมีลายเซ็นอย่างน้อย t ค่าจาก public key เหล่านั้นจึงจะสามารถใช้จ่ายเงินได้ เรียกรูปแบบนี้ว่า t-of-k ตัวอย่างเช่น 2-of-3 multisignature คือกรณีที่มีการระบุ public key ไว้ทั้งหมดสามค่าในฐานะผู้มีสิทธิ์ลงนาม และต้องใช้ลายเซ็นอย่างน้อยสองจากสามค่านั้น เพื่อสร้างธุรกรรมที่ถูกต้องสำหรับการใช้จ่ายเงิน
TIP: เอกสารของบิตคอยน์บางแหล่ง รวมถึงหนังสือเล่มนี้ในฉบับก่อนหน้า ใช้คำว่า “m-of-n” เพื่อเรียกมัลติซิกเนเจอร์แบบดั้งเดิม อย่างไรก็ตาม เมื่อพูดออกเสียงแล้ว ตัวอักษร m และ n แยกออกจากกันได้ยาก จึงมีการเลือกใช้คำว่า t-of-k แทน ทั้งสองคำนี้หมายถึงรูปแบบของระบบลายเซ็นแบบเดียวกัน
รูปแบบทั่วไปของ output script ที่กำหนดเงื่อนไขมัลติซิกเนเจอร์แบบ t-of-k มีดังนี้:
t <Public Key 1> <Public Key 2> ... <Public Key k> k OP_CHECKMULTISIG
โดยที่ k คือจำนวน public key ทั้งหมดที่ระบุไว้ และ t คือจำนวนลายเซ็นขั้นต่ำที่ต้องใช้เพื่อสามารถใช้จ่าย output นั้นได้ output script ที่กำหนดเงื่อนไขมัลติซิกเนเจอร์แบบ 2-of-3 จะมีลักษณะดังนี้:
2 <Public Key A> <Public Key B> <Public Key C> 3 OP_CHECKMULTISIG
output script ข้างต้นสามารถถูกทำให้สำเร็จได้ด้วย input script ที่มีลายเซ็น เช่น:
<Signature B> <Signature C>
หรือจะเป็นชุดลายเซ็นสองชุดใดก็ได้ ที่มาจาก private key ที่สอดคล้องกับ public key ทั้งสามค่าที่ถูกระบุไว้
เมื่อรวมสคริปต์ทั้งสองเข้าด้วยกัน จะได้สคริปต์ตรวจสอบความถูกต้องดังนี้:
<Sig B> <Sig C> 2 <Pubkey A> <Pubkey B> <Pubkey C> 3 OP_CHECKMULTISIG
เมื่อถูกประมวลผล สคริปต์ที่ถูกรวมกันนี้จะให้ผลลัพธ์เป็นค่า TRUE หาก input script มีลายเซ็นที่ถูกต้องจำนวนสองชุด ซึ่งมาจาก private key ที่สอดคล้องกับ public key สองค่าใดก็ได้ จากสามค่าที่ถูกกำหนดไว้เป็นเงื่อนไขการใช้จ่าย
ในปัจจุบัน นโยบายการส่งต่อธุรกรรม (transaction relay policy) ของ Bitcoin Core จำกัดจำนวน public key ใน multisignature output script ไว้สูงสุดที่สามค่า หมายความว่าสามารถสร้างมัลติซิกเนเจอร์ได้ตั้งแต่แบบ 1-of-1 ไปจนถึง 3-of-3 หรือรูปแบบใด ๆ ภายในช่วงนี้ คุณอาจต้องการตรวจสอบฟังก์ชัน IsStandard() เพื่อดูว่าขณะนี้เครือข่ายยอมรับรูปแบบใดบ้าง โปรดสังเกตว่า ข้อจำกัดที่สามคีย์นี้ใช้กับ multisignature script แบบมาตรฐานเท่านั้น (ซึ่งเรียกอีกชื่อหนึ่งว่า “bare” multisignature) ไม่ได้ใช้กับสคริปต์ที่ถูกห่อหุ้มด้วยโครงสร้างอื่น เช่น P2SH, P2WSH, หรือ P2TR สำหรับ multisignature script แบบ P2SH จะถูกจำกัดทั้งในระดับนโยบาย (policy) และฉันทามติ (consensus) ไว้ที่สูงสุด 15 คีย์ ทำให้สามารถสร้างมัลติซิกเนเจอร์ได้สูงสุดแบบ 15-of-15 เราจะเรียนรู้เกี่ยวกับ P2SH ในหัวข้อ Pay to Script Hash ส่วนสคริปต์รูปแบบอื่นทั้งหมด จะถูกจำกัดโดยฉันทามติไว้ที่ public key ได้ไม่เกิน 20 คีย์ ต่อคำสั่ง OP_CHECKMULTISIG หรือ OP_CHECKMULTISIGVERIFY หนึ่งคำสั่ง ทั้งนี้ สคริปต์หนึ่งรายการอาจมีคำสั่งเหล่านี้ได้มากกว่าหนึ่งครั้ง
An Oddity in CHECKMULTISIG Execution
มีความผิดปกติ (oddity) อย่างหนึ่งในการทำงานของ OP_CHECKMULTISIG ซึ่งทำให้ต้องมีวิธีแก้ไขเฉพาะหน้าเล็กน้อย เมื่อ OP_CHECKMULTISIG ถูกประมวลผล ตามหลักแล้วมันควรจะนำข้อมูลออกจากสแต็กมาใช้เป็นพารามิเตอร์จำนวน t + k + 2 ค่า อย่างไรก็ตาม เนื่องจากความผิดปกตินี้ OP_CHECKMULTISIG จะดึงค่าจากสแต็กออกมา มากกว่าที่คาดไว้หนึ่งค่า
เรามาดูรายละเอียดของเรื่องนี้ให้ชัดเจนขึ้น โดยใช้ตัวอย่างสคริปต์ตรวจสอบความถูกต้องจากก่อนหน้านี้:
<Sig B> <Sig C> 2 <Pubkey A> <Pubkey B> <Pubkey C> 3 OP_CHECKMULTISIG
ขั้นแรก OP_CHECKMULTISIG จะนำค่าแรกบนสุดของสแต็กออกมา ซึ่งก็คือ k (ในตัวอย่างนี้คือ “3”) จากนั้นจะนำข้อมูลออกมาอีก k ค่า ซึ่งเป็น public key ที่สามารถใช้ลงนามได้ ในตัวอย่างนี้คือ public key A, B และ C จากนั้นมันจะนำค่าออกมาอีกหนึ่งค่า ซึ่งก็คือ t หรือจำนวนลายเซ็นขั้นต่ำที่ต้องใช้ (quorum) โดยในกรณีนี้ t = 2 ตามปกติแล้ว ณ จุดนี้ OP_CHECKMULTISIG ควรจะนำค่าออกมาอีก t ค่า ซึ่งเป็นลายเซ็น เพื่อตรวจสอบว่าถูกต้องหรือไม่ อย่างไรก็ตาม เนื่องจากความผิดปกติในการนำไปใช้งานจริง OP_CHECKMULTISIG จะนำค่าจากสแต็กออกมา มากกว่าที่ควรหนึ่งค่า (รวมเป็น t + 1 ค่า) ค่าส่วนเกินนี้ถูกเรียกว่า dummy stack element และจะถูกละเว้นในระหว่างการตรวจสอบลายเซ็น ดังนั้นมันจึงไม่มีผลโดยตรงต่อการทำงานของ OP_CHECKMULTISIG เอง อย่างไรก็ตาม dummy element นี้ จำเป็นต้องมีอยู่ เพราะหากไม่มีค่าเหลืออยู่บนสแต็กในขณะที่ OP_CHECKMULTISIG พยายามดึงค่าออกมาเพิ่ม จะทำให้เกิด stack error และสคริปต์ล้มเหลว ส่งผลให้ธุรกรรมนั้นไม่ถูกต้อง เนื่องจาก dummy element ถูกละเว้น ค่าของมันจึงสามารถเป็นค่าอะไรก็ได้ แต่ในช่วงแรกได้มีธรรมเนียมใช้ค่า OP_0 สำหรับตำแหน่งนี้ ต่อมาสิ่งนี้ได้กลายเป็นกฎของ relay policy และในที่สุดก็กลายเป็นกฎระดับฉันทามติ (consensus rule) เมื่อมีการบังคับใช้ BIP147
เนื่องจากการนำ dummy element ออกจากสแต็กเป็นส่วนหนึ่งของกฎฉันทามติ (consensus rules) พฤติกรรมนี้จึงต้องถูกคงไว้และทำซ้ำตลอดไป ดังนั้นสคริปต์จึงควรมีหน้าตาเป็นดังนี้:
OP_0 <Sig B> <Sig C> 2 <Pubkey A> <Pubkey B> <Pubkey C> 3 OP_CHECKMULTISIG
ด้วยเหตุนี้ input script ที่ถูกใช้งานจริงใน multisignature จะ ไม่ใช่:
<Signature B> <Signature C>
แต่จะต้องเป็น:
OP_0 <Sig B> <Sig C>
บางคนเชื่อว่าความผิดปกตินี้เกิดจากบั๊กในโค้ดต้นฉบับของบิตคอยน์ แต่อย่างไรก็ตาม ยังมีคำอธิบายทางเลือกที่สมเหตุสมผลอยู่ การตรวจสอบลายเซ็นแบบ t-of-k อาจต้องใช้การตรวจสอบลายเซ็นมากกว่าทั้งค่า t หรือ k เพียงอย่างเดียว
ลองพิจารณาตัวอย่างง่าย ๆ แบบ 1-in-5 โดยมีสคริปต์ตรวจสอบความถูกต้องที่ถูกรวมกันดังนี้:
<dummy> <Sig4> 1 <key0> <key1> <key2> <key3> <key4> 5 OP_CHECKMULTISIG
ลายเซ็นจะถูกตรวจสอบกับ key0 ก่อน จากนั้นกับ key1 และต่อเนื่องไปยัง key อื่น ๆ จนกระทั่งในที่สุดจึงถูกนำไปเปรียบเทียบกับ public key ที่สอดคล้องกันจริง ๆ คือ key4 นั่นหมายความว่า จำเป็นต้องมีการตรวจสอบลายเซ็นถึง ห้าครั้ง ทั้งที่มีลายเซ็นเพียงหนึ่งรายการเท่านั้น แนวทางหนึ่งในการลดความซ้ำซ้อนนี้ คือให้ OP_CHECKMULTISIG รับข้อมูลในลักษณะเป็น map ที่ระบุว่าแต่ละลายเซ็นที่ส่งเข้ามาสัมพันธ์กับ public key ใด ซึ่งจะทำให้ OP_CHECKMULTISIG ต้องทำการตรวจสอบลายเซ็นเพียง t ครั้ง เท่านั้น จึงเป็นไปได้ว่า นักพัฒนาของบิตคอยน์ได้เพิ่มองค์ประกอบพิเศษนี้เข้าไปตั้งแต่เวอร์ชันแรก (ซึ่งปัจจุบันเราเรียกว่า dummy stack element) เพื่อเปิดทางให้สามารถเพิ่มความสามารถดังกล่าวผ่าน soft fork ในอนาคตได้ อย่างไรก็ตาม ฟีเจอร์ดังกล่าวไม่เคยถูกนำมาใช้งานจริง และการอัปเดตกฎฉันทามติด้วย BIP147 ในปี 2017 ก็ทำให้ไม่สามารถเพิ่มฟีเจอร์นี้ได้อีกต่อไปในอนาคต
มีเพียงนักพัฒนาของบิตคอยน์เท่านั้น ที่จะสามารถบอกได้ว่าการมีอยู่ของ dummy stack element นั้นเกิดจากบั๊ก หรือเป็นการวางแผนสำหรับการอัปเกรดในอนาคต สำหรับหนังสือเล่มนี้ เราเรียกสิ่งนี้เพียงว่าเป็น “ความผิดปกติ” (oddity)
นับจากนี้เป็นต้นไป หากคุณเห็นสคริปต์แบบ multisignature ให้คาดไว้เลยว่าจะต้องมี OP_0 เพิ่มเข้ามาในตอนต้น โดยจุดประสงค์เดียวของมันคือการเป็นวิธีแก้ปัญหาเฉพาะหน้า สำหรับความผิดปกติที่มีอยู่ในกฎฉันทามติ
Pay to Script Hash
Pay to Script Hash (P2SH) ถูกนำมาใช้ในปี 2012 ในฐานะรูปแบบการทำงานแบบใหม่ที่ทรงพลัง ซึ่งช่วยทำให้การใช้งานสคริปต์ที่ซับซ้อนเป็นเรื่องง่ายขึ้นอย่างมาก เพื่ออธิบายความจำเป็นของ P2SH เรามาดูตัวอย่างเชิงปฏิบัติกัน
โมฮัมเหม็ดเป็นผู้นำเข้าอุปกรณ์อิเล็กทรอนิกส์ซึ่งตั้งอยู่ในดูไบ บริษัทของโมฮัมเหม็ดใช้ฟีเจอร์มัลติซิกเนเจอร์ของบิตคอยน์อย่างแพร่หลายสำหรับบัญชีขององค์กร สคริปต์มัลติซิกเนเจอร์เป็นหนึ่งในการใช้งานความสามารถด้านสคริปต์ขั้นสูงของบิตคอยน์ที่พบได้บ่อยที่สุด และเป็นฟีเจอร์ที่ทรงพลังมาก บริษัทของโมฮัมเหม็ดใช้สคริปต์มัลติซิกเนเจอร์กับการชำระเงินจากลูกค้าทุกราย การชำระเงินของลูกค้าจะถูกล็อกไว้ในลักษณะที่ต้องใช้ลายเซ็นอย่างน้อยสองชุดจึงจะสามารถปลดล็อกได้ โมฮัมเหม็ด หุ้นส่วนอีกสามคนของเขา และทนายความของบริษัท ต่างสามารถให้ลายเซ็นได้คนละหนึ่งชุด รูปแบบมัลติซิกเนเจอร์เช่นนี้ช่วยสร้างกลไกกำกับดูแลกิจการ และป้องกันการโจรกรรม การยักยอก หรือการสูญหายของเงินได้
สคริปต์ที่ได้จากเงื่อนไขดังกล่าวจะค่อนข้างยาว และมีลักษณะดังนี้:
2 <Mohammed's Public Key> <Partner1 Public Key> <Partner2 Public Key>
<Partner3 Public Key> <Attorney Public Key> 5 OP_CHECKMULTISIG
แม้ว่าสคริปต์แบบมัลติซิกเนเจอร์จะเป็นฟีเจอร์ที่ทรงพลังมาก แต่ก็ใช้งานได้ค่อนข้างยุ่งยาก จากสคริปต์ตัวอย่างก่อนหน้า โมฮัมเหม็ดจะต้องสื่อสารสคริปต์นี้ให้ลูกค้าทุกรายทราบก่อนทำการชำระเงิน ลูกค้าแต่ละรายยังจำเป็นต้องใช้ซอฟต์แวร์กระเป๋าเงินบิตคอยน์แบบพิเศษ ที่สามารถสร้างสคริปต์ธุรกรรมแบบกำหนดเองได้ นอกจากนี้ ธุรกรรมที่ได้จะมีขนาดใหญ่กว่าธุรกรรมการชำระเงินแบบธรรมดาประมาณห้าเท่า เนื่องจากสคริปต์นี้มี public key ที่มีความยาวมาก ภาระของข้อมูลส่วนเกินนี้จะตกอยู่กับลูกค้าในรูปของค่าธรรมเนียมธุรกรรมที่สูงขึ้น สุดท้ายแล้ว สคริปต์ธุรกรรมขนาดใหญ่เช่นนี้จะถูกเก็บไว้ในชุดข้อมูล UTXO set ของทุก full node จนกว่าจะถูกใช้จ่าย ซึ่งปัญหาเหล่านี้ทั้งหมดทำให้การใช้งาน output script ที่ซับซ้อนเป็นเรื่องยากในทางปฏิบัติ
P2SH ถูกพัฒนาขึ้นมาเพื่อแก้ไขปัญหาเชิงปฏิบัติเหล่านี้ และเพื่อทำให้การใช้งานสคริปต์ที่ซับซ้อนง่ายพอ ๆ กับการชำระเงินไปยัง Bitcoin address แบบกุญแจเดียว ด้วยการชำระเงินแบบ P2SH สคริปต์ที่ซับซ้อนจะถูกแทนที่ด้วย “คำมั่น” (commitment) ซึ่งก็คือค่าแฮชเชิงคริปโตกราฟีของสคริปต์นั้น เมื่อมีการนำเสนอธุรกรรมเพื่อใช้จ่าย UTXO ในภายหลัง ธุรกรรมนั้นจะต้องมีทั้งสคริปต์ที่ตรงกับค่าแฮชที่ถูกผูกไว้ และข้อมูลที่ใช้ทำให้สคริปต์ดังกล่าวสำเร็จ กล่าวอย่างง่ายที่สุด P2SH หมายถึง “จ่ายเงินให้กับสคริปต์ที่ตรงกับค่าแฮชนี้ โดยสคริปต์นั้นจะถูกนำมาแสดงในภายหลังเมื่อ output นี้ถูกใช้จ่าย”
ในธุรกรรมแบบ P2SH สคริปต์ที่ถูกแทนที่ด้วยค่าแฮชจะถูกเรียกว่า redeem script เนื่องจากสคริปต์นี้จะถูกนำเสนอให้ระบบในช่วงเวลาที่มีการนำมาใช้จ่าย (redeem) แทนที่จะถูกใส่ไว้เป็น output script ตั้งแต่ต้นตารางด้านล่างจะ แสดงสคริปต์ในกรณีที่ไม่ใช้ P2SH และแสดงสคริปต์เดียวกันที่ถูกเข้ารหัสในรูปแบบ P2SH
Complex script without P2SH
| Output script | 2 PubKey1 PubKey2 PubKey3 PubKey4 PubKey5 5 OP_CHECKMULTISIG | | :---- | :---- | | Input script | Sig1 Sig2 |
Complex script as P2SH
| Redeem script | 2 PubKey1 PubKey2 PubKey3 PubKey4 PubKey5 5 OP_CHECKMULTISIG | | :---- | :---- | | Output script | OP_HASH160 <20-byte hash of redeem script> OP_EQUAL | | Input script | Sig1 Sig2 <redeem script> |
ดังที่เห็นได้จากตาราง เมื่อใช้ P2SH สคริปต์ที่ซับซ้อนซึ่งระบุเงื่อนไขในการใช้จ่าย output (redeem script) จะไม่ถูกนำมาใส่ไว้ใน output script อีกต่อไป แต่จะมีเพียงค่าแฮชของสคริปต์นั้นอยู่ใน output script เท่านั้น ส่วนตัว redeem script เองจะถูกนำมาแสดงภายหลังในฐานะส่วนหนึ่งของ input script เมื่อมีการใช้จ่าย output นั้นแนวทางนี้ทำให้ภาระในด้านค่าธรรมเนียมและความซับซ้อน ถูกย้ายจากฝั่งผู้รับเงิน ไปยังฝั่งผู้ใช้จ่ายเงินในภายหลังเรามาดูกรณีของบริษัทโมฮัมเหม็ด สคริปต์มัลติซิกเนเจอร์ที่ซับซ้อน และสคริปต์ P2SH ที่ได้จากมันกัน อันดับแรก คือสคริปต์มัลติซิกเนเจอร์ที่บริษัทของโมฮัมเหม็ดใช้สำหรับการรับชำระเงินจากลูกค้าทุกราย:
2 <Mohammed's Public Key> <Partner1 Public Key> <Partner2 Public Key>
<Partner3 Public Key> <Attorney Public Key> 5 OP_CHECKMULTISIG
สคริปต์ทั้งหมดนี้สามารถถูกแทนที่ด้วยค่าแฮชเชิงคริปโตกราฟีขนาด 20 ไบต์ ได้ โดยเริ่มจากการนำสคริปต์ไปผ่านอัลกอริทึมแฮช SHA256 ก่อน จากนั้นจึงนำผลลัพธ์ที่ได้ไปแฮชต่อด้วยอัลกอริทึม RIPEMD-160 ตัวอย่างเช่น เริ่มต้นด้วยค่าแฮชของ redeem script ของบริษัทโมฮัมเหม็ด:
54c557e07dde5bb6cb791c7a540e0a4796f5e97e
ธุรกรรมแบบ P2SH จะล็อก output ไว้กับค่าแฮชนี้ แทนที่จะใช้ redeem script ที่ยาวกว่า โดยใช้แม่แบบ output script แบบพิเศษดังนี้:
OP_HASH160 54c557e07dde5bb6cb791c7a540e0a4796f5e97e OP_EQUAL
ซึ่งอย่างที่เห็น จะสั้นกว่ามาก แทนที่จะเป็น “จ่ายไปยังสคริปต์มัลติซิกเนเจอร์ที่มี 5 คีย์นี้” ธุรกรรมแบบ P2SH จะกลายเป็น “จ่ายไปยังสคริปต์ที่มีค่าแฮชนี้” ลูกค้าที่ชำระเงินให้บริษัทของโมฮัมเหม็ด จำเป็นต้องใส่เพียง output script ที่สั้นกว่านี้ลงในธุรกรรมเท่านั้น
เมื่อโมฮัมเหม็ดและหุ้นส่วนต้องการใช้จ่าย UTXO นี้ พวกเขาจะต้องแสดง redeem script ต้นฉบับ (ซึ่งเป็นสคริปต์ที่ค่าแฮชของมันถูกใช้ล็อก UTXO ไว้) พร้อมกับลายเซ็นที่จำเป็นเพื่อปลดล็อกสคริปต์นั้น ดังนี้:
<Sig1> <Sig2> <2 PK1 PK2 PK3 PK4 PK5 5 OP_CHECKMULTISIG>
สคริปต์ทั้งสองจะถูกรวมกันเป็นสองขั้นตอน ขั้นแรก redeem script จะถูกตรวจสอบกับ output script เพื่อยืนยันว่าค่าแฮชตรงกัน:
<2 PK1 PK2 PK3 PK4 PK5 5 OP_CHECKMULTISIG> OP_HASH160 <script hash> OP_EQUAL
หากค่าแฮชของ redeem script ตรงกัน จากนั้น redeem script จะถูกนำมาประมวลผล:
<Sig1> <Sig2> 2 <PK1> <PK2> <PK3> <PK4> <PK5> 5 OP_CHECKMULTISIG
P2SH Addresses
อีกส่วนสำคัญของฟีเจอร์ P2SH คือความสามารถในการเข้ารหัสค่าแฮชของสคริปต์ให้อยู่ในรูปของ “address” ตามที่กำหนดไว้ใน BIP13 ที่อยู่แบบ P2SH คือการเข้ารหัสแบบ base58check ของค่าแฮชขนาด 20 ไบต์ของสคริปต์ เช่นเดียวกับที่ Bitcoin address ปกติคือการเข้ารหัสแบบ base58check ของค่าแฮชขนาด 20 ไบต์ของ public key ที่อยู่แบบ P2SH ใช้ version prefix เป็น “5” ซึ่งทำให้ที่อยู่ที่ถูกเข้ารหัสออกมาในรูป base58check เริ่มต้นด้วยตัวเลข “3”
ตัวอย่างเช่น สคริปต์ที่ซับซ้อนของบริษัทโมฮัมเหม็ด เมื่อนำไปแฮชและเข้ารหัสแบบ base58check เป็นที่อยู่ P2SH จะได้เป็น:
39RF6JqABiHdYHkfChV6USGMe6Nsr66Gzw
จากนี้ไป โมฮัมเหม็ดสามารถให้ “address” นี้กับลูกค้าได้ และลูกค้าก็สามารถใช้กระเป๋าเงินบิตคอยน์ทั่วไปแทบทุกแบบ เพื่อทำการชำระเงินอย่างง่าย ๆ ได้ เหมือนกับการจ่ายไปยัง Bitcoin address ปกติทั่วไป ตัวเลขนำหน้า 3 จะช่วยบอกใบ้ให้ทราบว่านี่คือที่อยู่แบบพิเศษ ซึ่งเชื่อมโยงกับสคริปต์ ไม่ใช่กับ public key โดยตรง แต่ในแง่ของการใช้งานแล้ว มันทำงานเหมือนกับการชำระเงินไปยัง Bitcoin address อื่น ๆ ทุกประการ ที่อยู่แบบ
P2SH ช่วยซ่อนความซับซ้อนทั้งหมดเอาไว้ ทำให้ผู้ที่ทำการชำระเงินไม่จำเป็นต้องเห็นหรือเข้าใจสคริปต์เลย
Benefits of P2SH
ฟีเจอร์ P2SH มีข้อดีเมื่อเทียบกับการใช้งานสคริปต์ที่ซับซ้อนโดยตรงใน output ดังนี้:
-
ความคล้ายคลึงกับ Bitcoin address แบบ legacy เดิม ทำให้ผู้ส่งและซอฟต์แวร์กระเป๋าเงินของผู้ส่ง ไม่จำเป็นต้องมีการออกแบบหรือพัฒนาเพิ่มเติมที่ซับซ้อนเพื่อรองรับ P2SH
-
P2SH ย้ายภาระในการจัดเก็บข้อมูลของสคริปต์ที่ยาว จากฝั่ง output (ซึ่งนอกจากจะถูกเก็บไว้บนบล็อกเชนแล้ว ยังอยู่ในชุดข้อมูล UTXO set ด้วย) ไปอยู่ที่ฝั่ง input (ซึ่งจะถูกเก็บไว้บนบล็อกเชนเท่านั้น)
-
P2SH ย้ายภาระในการจัดเก็บข้อมูลของสคริปต์ที่ยาว จากช่วงเวลาปัจจุบันขณะชำระเงิน ไปยังช่วงเวลาในอนาคตเมื่อมีการนำ output นั้นมาใช้จ่าย
-
P2SH ย้ายภาระค่าธรรมเนียมธุรกรรมที่เกิดจากสคริปต์ยาว จากผู้ส่ง ไปยังผู้รับ ซึ่งเป็นฝ่ายที่ต้องใส่ redeem script ที่ยาวลงไปเมื่อทำการใช้จ่าย
Redeem Script and Validation
คุณไม่สามารถใส่ P2SH ซ้อนอยู่ภายใน redeem script ของ P2SH ได้ เนื่องจากข้อกำหนดของ P2SH ไม่ได้รองรับการทำงานแบบเรียกซ้ำ (recursive) นอกจากนี้ แม้ในเชิงเทคนิคจะสามารถใส่ OP_RETURN (ดูหัวข้อ Data Recording Output (OP_RETURN)) ลงไปใน redeem script ได้ เพราะกฎไม่ได้ห้ามไว้ แต่ในทางปฏิบัติกลับไม่มีประโยชน์ใด ๆ เนื่องจากเมื่อมีการประมวลผล OP_RETURN ระหว่างขั้นตอนการตรวจสอบความถูกต้อง ธุรกรรมจะถูกตัดสินว่าไม่ถูกต้องทันที
โปรดสังเกตว่า เนื่องจาก redeem script จะไม่ถูกเผยแพร่ให้เครือข่ายทราบจนกว่าจะมีการพยายามใช้จ่าย P2SH output หากคุณสร้าง output ด้วยค่าแฮชของ redeem script ที่ไม่ถูกต้อง คุณจะไม่สามารถใช้จ่ายมันได้เลย ธุรกรรมที่พยายามใช้จ่าย ซึ่งต้องแนบ redeem script เข้ามาด้วย จะไม่ถูกยอมรับ เนื่องจากสคริปต์นั้นไม่ถูกต้อง สิ่งนี้ก่อให้เกิดความเสี่ยง เพราะเป็นไปได้ที่จะส่งบิตคอยน์ไปยัง P2SH address ที่ไม่สามารถถูกใช้จ่ายได้ในภายหลัง
คำเตือน: output script แบบ P2SH จะบรรจุเพียงค่าแฮชของ redeem script เท่านั้น ซึ่งไม่ได้ให้ข้อมูลใด ๆ เกี่ยวกับเนื้อหาของ redeem script เลย ดังนั้น P2SH output จะยังคงถูกถือว่าถูกต้องและถูกรับรอง แม้ว่า redeem script ที่ถูกอ้างถึงนั้นจะไม่ถูกต้องก็ตาม ด้วยเหตุนี้เองที่จะทำให้คุณอาจเผลอได้รับบิตคอยน์ในรูปแบบที่ไม่สามารถนำไปใช้จ่ายได้ในภายหลัง
Data Recording Output (OP_RETURN)
บิตคอยน์มีบล็อกเชนกระจายศูนย์ที่มีการประทับเวลา (timestamped blockchain) ซึ่งมีศักยภาพในการนำไปใช้มากกว่าการชำระเงิน หลายคนพยายามใช้ภาษาสคริปต์ของธุรกรรมเพื่อใช้ประโยชน์จากความปลอดภัยและความทนทานของระบบสำหรับแอปพลิเคชันอื่น เช่น บริการทนายความดิจิทัล (digital notary services) ความพยายามในช่วงแรก ๆ เพื่อใช้สคริปต์ของบิตคอยน์ในลักษณะนี้เกี่ยวข้องกับการสร้าง transaction output ที่บันทึกข้อมูลลงบนบล็อกเชน เช่น การบันทึก commitment ต่อไฟล์หนึ่งไฟล์ เพื่อให้ใครก็ตามสามารถตรวจสอบการมีอยู่จริงของไฟล์นั้น ณ วันที่หนึ่งได้ โดยอ้างอิงจากธุรกรรมนั้น
การใช้บล็อกเชนของบิตคอยน์เพื่อเก็บข้อมูลที่ไม่เกี่ยวข้องกับการชำระเงินเป็นประเด็นที่ถกเถียงกัน หลายคนมองว่าเป็นการใช้งานที่ไม่เหมาะสมและควรถูกขัดขวาง ขณะที่อีกกลุ่มมองว่าเป็นตัวอย่างของความสามารถทรงพลังของเทคโนโลยีบล็อกเชนและควรสนับสนุนการทดลองเช่นนี้ ผู้คัดค้านให้เหตุผลว่าการบรรจุข้อมูลเหล่านี้ทำให้ผู้ที่รันฟูลโหนดต้องแบกรับต้นทุนพื้นที่จัดเก็บข้อมูลที่ไม่ใช่สิ่งที่บล็อกเชนถูกออกแบบมาเพื่อรองรับ นอกจากนี้ ธุรกรรมลักษณะนี้ยังอาจสร้าง UTXO ที่ไม่สามารถใช้จ่ายได้ โดยใช้ legacy Bitcoin address เป็นพื้นที่ข้อมูลขนาด 20 ไบต์ เพราะแอดเดรสนั้นถูกใช้เป็นข้อมูลและไม่สอดคล้องกับกุญแจส่วนตัว UTXO ที่เกิดขึ้นจึงไม่มีวันถูกใช้จ่ายได้ และเป็น "ธุรกรรมปลอม" ซึ่งจะไม่ถูกลบออกจาก UTXO set ทำให้ฐานข้อมูล UTXO โตขึ้นเรื่อย ๆ หรือที่เรียกว่า "bloat"
โดยในที่สุดก็ได้มีการประนีประนอมโดยอนุญาตให้ output script ที่เริ่มต้นด้วย OP_RETURN สามารถเพิ่มข้อมูลที่ไม่เกี่ยวกับการชำระเงินลงใน transaction output ได้ อย่างไรก็ตาม แตกต่างจาก UTXO ปลอม OP_RETURN จะสร้างเอาต์พุตที่พิสูจน์ได้อย่างชัดเจนว่าไม่สามารถใช้จ่ายได้ ทำให้ไม่จำเป็นต้องถูกเก็บไว้ใน UTXO set เอาต์พุตที่มี OP_RETURN จะถูกบันทึกลงในบล็อกเชน ดังนั้นจึงใช้พื้นที่ดิสก์และเพิ่มขนาดบล็อกเชน แต่จะไม่ถูกเก็บใน UTXO set และจึงไม่ทำให้ Full node ต้องแบกรับต้นทุนฐานข้อมูลที่แพงขึ้น
OP_RETURN scripts จะมีรูปแบบประมาณนี้:
OP_RETURN <data>
ส่วนข้อมูล (data portion) มักใช้แทนค่าแฮช เช่น เอาต์พุตจากอัลกอริทึม SHA256 ซึ่งมีขนาด 32 ไบต์ แอปพลิเคชันบางประเภทจะใส่คำนำหน้า (prefix) ไว้หน้าข้อมูล เพื่อช่วยระบุว่าเป็นข้อมูลของแอปพลิเคชันใด ตัวอย่างเช่น บริการรับรองเอกสารดิจิทัล Proof of Existence ใช้คำนำหน้าขนาด 8 ไบต์คือคำว่า DOCPROOF ซึ่งเข้ารหัสแบบ ASCII และแสดงในรูปเลขฐานสิบหกเป็น 44 4f 43 50 52 4f 4f 46
ควรทำความเข้าใจว่าไม่มี input script ใดที่สามารถสอดคล้องกับ OP_RETURN เพื่อนำมา “ใช้จ่าย” (spend) เอาต์พุตแบบ OP_RETURN ได้ จุดประสงค์ทั้งหมดของ OP_RETURN คือการสร้างเอาต์พุตที่ไม่สามารถใช้จ่ายได้ตั้งแต่ต้น ดังนั้นจึงไม่จำเป็นต้องถูกเก็บไว้ใน UTXO set ในฐานะเอาต์พุตที่อาจถูกใช้จ่ายในอนาคต กล่าวได้ว่า OP_RETURN outputs เป็นเอาต์พุตที่พิสูจน์ได้ชัดเจนว่าไม่สามารถใช้จ่ายได้ (provably unspendable)
โดยทั่วไป OP_RETURN outputs จะกำหนดจำนวนบิตคอยน์เป็นศูนย์ เพราะบิตคอยน์ใด ๆ ที่ถูกกำหนดไว้ในเอาต์พุตลักษณะนี้ จะถือว่าสูญหายไปตลอดกาล หากมีการอ้างอิง OP_RETURN output มาเป็น input ของธุรกรรมใด ธุรกรรมนั้นจะถูกเครื่องยนต์ตรวจสอบสคริปต์ (script validation engine) ยุติการทำงานทันทีและถูกตัดสินว่าไม่ถูกต้อง การทำงานของ OP_RETURN จะทำให้สคริปต์ “คืนค่า” FALSE และหยุดการประมวลผล ดังนั้น หากอ้างอิง OP_RETURN output มาเป็น input โดยไม่ตั้งใจ ธุรกรรมนั้นจะเป็นธุรกรรมที่ไม่ถูกต้องทันที
ข้อจำกัดของ Transaction Lock Time
การใช้ค่า lock time ทำให้ผู้ใช้จ่ายสามารถกำหนดเงื่อนไขได้ว่าธุรกรรมจะยังไม่ถูกนำไปรวมอยู่ในบล็อกจนกว่าจะถึงความสูงของบล็อก (block height) หรือเวลาที่กำหนดไว้ แต่ ไม่ได้ ป้องกันไม่ให้เงินก้อนนั้นถูกนำไปใช้จ่ายผ่านธุรกรรมอื่นก่อนหน้านั้น อธิบายด้วยตัวอย่างต่อไปนี้
Alice ลงนามในธุรกรรมที่ใช้จ่ายเอาต์พุตหนึ่งของเธอไปยังที่อยู่ของ Bob และตั้งค่า transaction lock time ไว้ล่วงหน้า 3 เดือน จากนั้น Alice ส่งธุรกรรมนั้นให้ Bob เก็บไว้ เมื่อเป็นเช่นนี้ Alice และ Bob จะทราบว่า:
- Bob ไม่สามารถกระจาย (broadcast) ธุรกรรมนี้เพื่อรับเงินได้ จนกว่าจะครบกำหนด 3 เดือน
- Bob สามารถกระจายธุรกรรมนี้ได้หลังจากครบ 3 เดือนแล้ว
อย่างไรก็ตาม:
- Alice สามารถสร้างธุรกรรมอีกชุดที่ขัดแย้งกัน โดยใช้จ่ายอินพุตเดียวกันแต่ไม่ใส่ค่า lock time ซึ่งหมายความว่า Alice สามารถใช้จ่าย UTXO เดียวกันได้ก่อนครบ 3 เดือน
- Bob จึงไม่มีหลักประกันใด ๆ ว่า Alice จะไม่ทำเช่นนั้น
สิ่งสำคัญคือ ต้องเข้าใจข้อจำกัดของ transaction lock time ให้ชัดเจน เงื่อนไขเพียงอย่างเดียวที่รับประกันได้คือ Bob จะไม่สามารถนำธุรกรรมที่ลงนามล่วงหน้าไปใช้ก่อนครบ 3 เดือนได้ แต่ ไม่มีการรับประกันว่า Bob จะได้รับเงินก้อนนั้นจริง หากต้องการให้ Bob ได้รับเงินอย่างแน่นอน แต่ไม่สามารถใช้จ่ายได้จนกว่าจะครบ 3 เดือน ต้องกำหนดเงื่อนไข timelock ไว้บนตัว UTXO เองในสคริปต์ แทนที่จะกำหนดไว้ในธุรกรรม ซึ่งทำได้ด้วยรูปแบบของ timelock ถัดไปที่เรียกว่า Check Lock Time Verify (CLTV)
Check Lock Time Verify (OP_CLTV)
ในเดือนธันวาคม ค.ศ. 2015 ได้มีการเพิ่มรูปแบบของ timelock แบบใหม่เข้ามาในบิตคอยน์ผ่านการอัปเกรดแบบ soft fork โดยอ้างอิงตามข้อกำหนดใน BIP65 มีการเพิ่มคำสั่งสคริปต์ใหม่ที่ชื่อว่า OP_CHECKLOCKTIMEVERIFY (OP_CLTV) เข้าไปในภาษา scripting ของบิตคอยน์ OP_CLTV เป็น timelock ระดับเอาต์พุต (per-output timelock) แตกต่างจาก lock time แบบเดิมซึ่งเป็น timelock ระดับธุรกรรม (per-transaction timelock) ทำให้สามารถกำหนดเงื่อนไขด้านเวลาได้ยืดหยุ่นมากยิ่งขึ้น
กล่าวโดยสรุป เมื่อมีการใส่ opcode OP_CLTV ลงไปในเอาต์พุต เอาต์พุตนั้นจะถูกจำกัดไม่ให้สามารถใช้จ่ายได้จนกว่าจะถึงเวลาที่กำหนดไว้
OP_CLTV ไม่ได้มาแทนที่ lock time แต่ทำหน้าที่จำกัด UTXO เฉพาะรายการ ให้สามารถถูกใช้จ่ายได้ก็ต่อเมื่อธุรกรรมที่จะนำมาใช้จ่ายนั้นมีค่า lock time ที่ตั้งไว้มากกว่าหรือเท่ากับค่าที่กำหนดใน OP_CLTV
คำสั่ง OP_CLTV รับพารามิเตอร์หนึ่งค่าเป็นอินพุต ซึ่งแสดงเป็นตัวเลขในรูปแบบเดียวกับ lock time (อาจเป็นความสูงของบล็อก หรือเวลา Unix epoch) และตามที่คำว่า VERIFY ระบุไว้ OP_CLTV เป็น opcode ประเภทที่หากผลลัพธ์เป็น FALSE จะหยุดการทำงานของสคริปต์ทันที แต่ถ้าผลลัพธ์เป็น TRUE สคริปต์จะทำงานต่อไป
ในการใช้งาน OP_CLTV จะต้องใส่คำสั่งนี้ไว้ใน redeem script ของเอาต์พุตในธุรกรรมที่สร้างเอาต์พุตนั้นขึ้นมา ตัวอย่างเช่น หากอลิซต้องการจ่ายเงินให้บ็อบ ปกติบ็อบอาจยอมรับการจ่ายเงินผ่านสคริปต์ P2SH ลักษณะดังนี้
<Bob's public key> OP_CHECKSIG
เพื่อทำการล็อกเอาต์พุตไว้จนถึงเวลาที่กำหนด เช่น อีก 3 เดือนนับจากนี้ สคริปต์ P2SH ของบ็อบจะถูกเปลี่ยนเป็นแบบนี้แทน:
<Bob's pubkey> OP_CHECKSIGVERIFY <now + 3 months> OP_CHECKLOCKTIMEVERIFY
โดยที่ค่า <now {plus} 3 months> คือความสูงบล็อกหรือค่าเวลาที่ประเมินว่าจะตรงกับอีก 3 เดือนหลังจากที่ธุรกรรมถูกขุดลงบล็อก เช่น ความสูงบล็อกปัจจุบัน + 12,960 บล็อก หรือ Unix epoch time ปัจจุบัน + 7,760,000 วินาที
เมื่อบ็อบต้องการใช้จ่าย UTXO นี้ เขาจะสร้างธุรกรรมใหม่ที่อ้างอิง UTXO นั้นเป็นอินพุต จากนั้นใส่ลายเซ็นและกุญแจสาธารณะของเขาในสคริปต์อินพุต พร้อมตั้งค่า lock time ของธุรกรรมให้มากกว่าหรือเท่ากับค่าทิมล็อกที่ระบุไว้ใน OP_CHECKLOCKTIMEVERIFY ที่อลิซกำหนด แล้วจึงกระจายธุรกรรมนี้ไปยังเครือข่ายบิตคอยน์
ธุรกรรมของบ็อบจะถูกประเมินดังนี้: หากค่าพารามิเตอร์ของ OP_CHECKLOCKTIMEVERIFY ที่อลิซตั้งไว้นั้นน้อยกว่าหรือเท่ากับ lock time ของธุรกรรมที่ใช้จ่าย สคริปต์จะทำงานต่อไป (มีผลเหมือนกับไม่มีการทำงาน หรือเหมือน opcode ประเภท OP_NOP) แต่ถ้าค่าดังกล่าวมากกว่า lock time การทำงานของสคริปต์จะหยุดลงทันที และธุรกรรมจะถูกมองว่าไม่ถูกต้อง
กล่าวให้ชัดเจนยิ่งขึ้น BIP65 อธิบายว่า OP_CHECKLOCKTIMEVERIFY จะล้มเหลวและหยุดการทำงานของสคริปต์ทันที หากเกิดเงื่อนไขอย่างใดอย่างหนึ่งต่อไปนี้:
- สแตกว่างเปล่า
- ค่าบนสุดของสแตกมีค่าน้อยกว่า 0
- ประเภทของ lock time (ความสูงบล็อกเทียบกับ timestamp) ของค่าบนสุดในสแตกไม่ตรงกับประเภทของ lock time ในฟิลด์ lock time ของธุรกรรม
- ค่าบนสุดของสแตกมีค่ามากกว่าค่า lock time ของธุรกรรม
- ฟิลด์ sequence ของอินพุตมีค่าเป็น 0xffffffff
Timelock Conflicts
OP_CLTV และ locktime นั้นได้ใช้รูปแบบเดียวกันในการอธิบายระยะเวลาในการล๊อค ไม่ว่าจะเป็น block height หรือเมื่อเวลาผ่านไปในหน่วยของวินาทีตาม unix epoch ส่วนที่สำคัญคือ รูปแบบของ timelock จะต้องตรงกับรูปแบบของ OP_CLTV ใน output ทั้งคู่ต้องอ้างอิงแบบเดียวกัน คือจะเป็น block height หรือหน่วยวินาทีก็ตาม
สิ่งนี้หมายความว่าสคริปต์จะไม่มีทางเป็นสคริปต์ได้ ถ้ามีการเรียก OP_CLTV สองครั้งโดยใช้รูแปบบที่แตกต่างกัน คือครั้งนึงเป็น block height และ อีกครั้งหนึ่งเป็น timestamp ซึ่งส่วนนี้เองเป็นข้อผิดพลาดที่เกิดได้ง่ายและมักจะเกิดขึ้น เพราะฉะนั้นควรทดสอบสคริปต์อย่างละเอียดบน testnet หรือใช้เครื่องมือที่ออกแบบมาเพื่อป้องกันปัญหานี้ เช่น Miniscript compiler
ผลอีกอย่างที่สืบเนื่องกันมาจากข้อกำหนดที่ว่าหนึ่งธุรกรรมสามารถใช้ OP_CLTV ได้แบบเดียวเท่านั้น จึงทำให้ในธุรกรรมที่มีหลาย ๆ input โดยแต่ละ input ใช้รูปแบบของ OP_CLTV ที่แตกต่างกัน จะไม่สามารถนำมาสร้างธุรกรรมที่ใช้ input ทั้งสองพร้อมกันได้
โดยหลังจากมีการประมวลผล หากเงื่อนไขของ OP_CLTV เป็นจริง พารามิเตอร์ที่อยู่ก่อนหน้า OP_CLTV จะยังคงอยู่บนสุดของแสตก และอาจจำเป็นต้องเอาออกด้วย OP_DROP เพื่อให้การทำงานของ opcode อื่น ๆ ในสคริปต์ถัดไปได้เป็นไปอย่างถูกต้อง ด้วยเหตุนี้เราจึงมักจะเห็น OP_CHECKLOCKTIMEVERIFY จะถูกใช้ตามด้วย OP_DROP ในสคริปต์ต่าง ๆ เช่นเดียวกับ OP_CSV (จะอธิบายต่อในหัวข้อ Relative timelock) OP_CLTV มีพฤติกรรมที่แตกต่างจาก opcode ในกลุ่ม CHECKVERIFY ตัวอื่น ๆ ตรงที่ไม่ได้นำข้อมูลออกจากสแตก แต่ทั้งนี้ทั้งนั้นมันเป็นเพราะ ซอฟต์ฟอร์กที่เพิ่ม opcode เหล่านี้เข้าไป ได้อาศัยการนิยามใหม่ของ opcode ที่เคยเป็น no-operation (NOP) ซึ่งเดิมไม่ได้ลบข้อมูลออกจากสแตก และพฤติกรรมของ opcode NOP เดิมเหล่านั้นจำเป็นต้องถูกคงไว้เพื่อความเข้ากันได้
และด้วยการใช้ lock time ร่วมกันกับ OP_CLTV นั้นจะทำให้ในส่วนที่เราได้อธิบายไว้ในหัวข้อ ข้อจำกัดของ Transaction Lock Time เปลี่ยนแปลงไปเล็กน้อย เนื่องจากอลิสสามารถส่งบิตคอยน์ของเธอได้ทันที โดยกำหนดให้เงินถูกล๊อคไว้ที่กุญแจของบ๊อบตั้งแต่วินาทีนั้น อลิสจะไม่สามารถใช้เงินก้อนนี้ซ้ำได้อีก และในขณะเดียวกันบ๊อบก็ยังคงต้องรอ 3 เดือนก่อนตามที่กำหนดไว้
อยากตัวอย่างดังกล่าว ผู้อ่านทุกท่านคงจะได้เห็นแล้วว่าการนำ timelock เข้ามาไว้ในภาษา scripting ทำให้ OP_CLTV เปิดโอกาสให้เราสามารถพัฒนาสคริปต์ที่ซับซ้อนและน่าสนใจได้หลากหลายรูปแบบ
ซึ่งมาตรฐานดังกล่าวถูกกำหนดไว้ใน BIP65 (OP_CHECKLOCKTIMEVERIFY)
Relative Timelocks
Lock time และ OP_CLTV ทั้งคู่เป็น absolute timelocks กล่าวคือ เป็นการกำหนดเวลาที่ตายตัว ส่วนกลไก timelock อีกสองรูปแบบที่เราจะพิจรณาต่อไปนี้เป็น relative timelocks ซึ่งเป็นการกำหนดเงื่อนไขการใช้จ่าย output โดยอ้างอิงกับกับระยะเวลาตั้งแต่ธุกรรมก่อนหน้าได้รับการยืนยันและถูกบันทึกลงบนบล๊อคเชน
relative timelocks นั้นสามารถช่วยให้เราสามารถกำหนดข้อจำกัดในด้านของเวลาให้กำธุรกรรมนั้น ๆ โดยอ้างอิงจากเวลาที่ธุรกรรมก่อนหน้าถูกยืนยัน หรือกล่าวได้ว่าเวลาของธุรกรรมนั้นจะไม่ถูกนับจนกว่า UTXO ก่อนหน้าจะปรากฎบนบล๊อคเชน ฟังก์ชันนี้มีความสำคัญอย่างยิ่งกับการทำ bidirectional state channels และ Lightning Network (LN) ซึ่งเราจะได้อ่านเรื่องนี้เพิ่มเติมในหัวข้อ state channels
Relative timelocks นั้นได้มีการถูกนำมาใช้งานทั้งในระดับธุรกรรมและในระดับของสคริปต์ เช่นเดียวกันกับ absolute timelocks โดยในระดับธุรกรรมนั้นจะถูกใช้ในฟิลด์ของ sequence ซึ่งเป็นฟิลด์ที่ต้องอยู่ในส่วนของ input ในทุกธุรกรรมของบิตคอยน์ ส่วนในระดับของสคริปต์นั้นจะถูกเรียกใช้ผ่าน opcode ที่ชื่อ OP_CHECKSEQUENCEVERIFY (OP_CSV)
Relative timelocks ถูกนำมาใช้งานตามข้อกำหนดที่ระบุไว้ใน BIP68: Relative Lock-Time Using Consensus-Enforced Sequence Numbers และ BIP112: OP_CHECKSEQUENCEVERIFY
ทั้ง BIP68 และ BIP112 ถูกเปิดใช้งานในเดือนพฤษภาคม ปี 2016 ในรูปแบบของการอัปเกรดกฎ consensus แบบ soft fork
Relative Timelocks with OP_CSV
เช่นเดียวกันกับ OP_CLTV และ lock time ในระบบของบิตคอยน์มี opcode ในระดัสคริปต์สำหรับ relative timelock ที่อาศัยค่า sequence ภายในสคริปต์ด้วย opcode นั้นคือ OP_CHECKSEQUENCEVERIFY ซึ่งมักเรียกย่อว่า OP_CSV
เมื่อ opcode อย่าง OP_CSV ถูกประเมิณค่าในสคริปต์ของ UTXO จะอนุญาตให้ใช้จ่ายได้เฉพาะในธุรกรรมที่ค่า sequence ของ input มีค่ามากกว่าหรือเท่ากับพารามิเตอร์ของ OP_CSV เท่านั้น โดยสรุปแล้ว กลไกนี้จะจำกัดการใช้จ่าย UTXO จนกว่าจะมีจำนวนบล็อกหรือเวลาหน่วยวินาทีผ่านไปตามที่กำหนดไว้ นับจากเวลาที่ UTXO นั้นถูกขุดและบันทึกลงในบล็อกเชน
และเช่นเดียวกันกับ CLTV ค่าใน OP_CSV จะต้องมีรูปแบบที่สอดคล้องกับค่า sequence ที่ใช้คู่กัน โดยหาก OP_CSV ถูกกำหนดไว้เป็นจำนวนบล๊อก ค่า sequence ก็จำเป็นต้องอยู่ในรูปแบบของจำนวนบล๊อก และหาก OP_CSV ถูกกำหนดค่าไว้เป็นวินาที ค่า sequence ก็จำเป็นต้องเป็นค่าวินาทีเช่นกัน
คำเตือน: สคริปต์ที่มีการเรียกใช้ OP_CSV หลายครั้ง จะต้องใช้รูปแบบเดียวกันทั้งหมด คือจะเป็นรูปแบบของวินาที ก็ต้องเป็นวินาทีทั้งหมด ถ้าเป็นจำนวนบล๊อกก็ต้องเป็นจำนวนบล๊อกทั้งหมดเท่านั้น การผสมทั้งสองรูปแบบเข้าด้วยกันจะทำให้สคริปต์นั้นถูกมองว่าไม่ถูกต้อง และไม่สามารถถูกใช้จ่ายได้ตลอดไป ซึ่งเป็นปัญหาเดียวกันกับ OP_CLTV ในหัวข้อ Timelock Conflicts แต่อย่างไรก็ตาม OP_CSV อณุญาติให้มี input ที่ถูกต้องได้หลายแบบภายในธุรกรรมเดียวกัน ดังนั้นปัญหาของการปฎิสัมพันธ์ข้าม input ที่เกิดขึ้นกับ OP_CLTV จึงจะไม่เกิดขึ้นกับ OP_CSV
relative timelock ด้วย OP_CSV นั้นมีประโยชน์อย่างยิ่งในกรณีที่มีการสร้างและลงนามธุรกรรมหลายรายการที่เชื่อมต่อกันเป็นลูกโซ่ แต่ยังไม่ถูกเผยแพร่ไปยังเครือข่าย นั่นคือถูกเก็บไว้นอกบล็อกเชน ธุรกรรมลูกเหล่านั้นจะไม่สามารถถูกใช้งานได้จนกว่าธุรกรรมแม่จะถูกเผยแพร่ ถูกขุดรวมเข้าไปในบล็อกเชน และมีอายุผ่านไปตามระยะเวลาที่ระบุไว้ใน relative timelock การใช้งานลักษณะนี้แสดงตัวอย่างไว้ใน state channels และ lightning network
OP_CSV ถูกกำหนดรายละเอียดไว้ใน BIP112 และ CHECKSEQUENCEVERIFY
สคริปต์ที่มีการควบคุมลำดับการทำงาน (เงื่อนไขหลายเงื่อนไข)
หนึ่งในสิ่งที่น่าสนใจ และคุณสมบัติที่ทรงพลังมาก ๆ ของ Bitcoin script คือการควบคุมลำดับการทำงาน (Flow control) หรือที่เลือกว่าเงื่อนไขหลายเงื่อนไข คุณน่าจะคุ้นเคยกับแนวคิดของ flow control จากภาษาโปรแกรมต่าง ๆ ที่ใช้โครงสร้าง IF…THEN…ELSE เงื่อนไขใน Bitcoin Script แม้จะมีรูปแบบแตกต่างออกไปเล็กน้อย แต่โดยรวมแล้วเป็นโครงสร้างเดียวกัน
ในระดับพื้นฐาน opcode นั้นมีเงื่อนไขของบิตคอยน์ที่ช่วยให้เราสามารถสร้างสคริปต์ที่มีวิธีปลดล็อกได้สองแบบ ขึ้นอยู่กับผลลัพธ์ TRUE/FALSE จากการประเมินเงื่อนไขในทางตรรกะ ตัวอย่างเช่น หากค่า x เป็น TRUE เส้นทางของโค้ดที่ถูกประมวลผลคือ A และหากไม่เป็นเช่นนั้น (ELSE) เส้นทางของโค้ดคือเส้นทาง B
นอกจากนี้ นิพจน์ต่าง ๆ ภายในเงื่อนไขของบิตคอยน์ยังสามารถทับซ้อนกันได้อย่างไม่จำกัด หรือกล่าวได้ว่าภายในเงื่อนไขใด ๆ สามารถอีกอีกเงื่อนไขอยู่ด้านในได้ สคริปต์ของบิตคอยน์ที่มีการควบคุมลำดับการทำงาน จึงสามารถใช้สร้างสคริปต์ที่ซับซ้อนมาก โดยมีเส้นทางการประมวลผลที่เป็นไปได้หลายร้อยแบบ แม้จะไม่มีข้อจำกัดด้านระดับความลึกของการซ้อนเงื่อนไขแต่กฎฉันทามติ ก็ได้กำหนดข้อจำกัดไว้ที่ขนาดสูงสุดของสคริปต์ในหน่วยไบต์
บิตคอยน์ได้ใช้การควบคุมระดับการทำงานผ่าน opcode อย่าง OP_IF OP_ELSE OP_ENDIF และ OP_NOTIF นอกจากนี้นิพจน์เงื่อนไขยังสามารถมีตัวดำเนินการเชิงตระกะอย่าง OP_BOOLAND, OP_BOOLOR และ OP_NOT ได้ด้วย
ในภาษาโปรแกรมแบบดั้งเดิม (เชิงกระบวนการ) ส่วนใหญ่ การควบคุมลำดับการทำงานจะมีลักษณะดังนี้: pseudocode ของการควบคุมลำดับการทำงานในภาษาโปรแกรมส่วนใหญ่
if (condition):
code to run when condition is true
else:
code to run when condition is false
endif
code to run in either case
แต่ในภาษาที่อิงกับสแตก (stack-based) อย่าง Bitcoin Script เงื่อนไขเชิงตรรกะจะมาก่อนคำสั่ง IF ซึ่งทำให้โครงสร้างดูเหมือน “กลับด้าน”: การควบคุมลำดับการทำงานใน Bitcoin Script
condition
IF
code to run when condition is true
OP_ELSE
code to run when condition is false
OP_ENDIF
code to run in either case
เมื่ออ่าน Bitcoin Script ให้จำไว้ว่า เงื่อนไขที่ถูกนำมาประเมินค่าจะมาก่อน opcode IF เสมอ
เงื่อนไขแบบมี verify opcode
เงื่อนไขในอีกรูปแบบหนึ่งของบิตคอยน์สคริปต์ คือ opcode ใด ๆ ที่ลงท้ายด้วย VERIFY โดยคำต่อท้าย VERIFY หมายความว่า หากเงื่อนไขที่ถูกประเมินค่าไม่เป็น TRUE การทำงานของสคริปต์จะถูกยุติทันที และธุรกรรมจะถูกพิจารณาว่าไม่ถูกต้อง
ซึ่งแตกต่างจากการทำงานของ IF ซึ่งได้เปิดโอกาสให้มีเส้นทางการทำงานหลายรูปแบบ opcode ที่ลงท้ายด้วย VERIFY จะทำหน้าที่เป็น guard clause หรือก็คือตรรกะที่ต้องประเมินผลให้เป็นจริง เพื่อให้การดำเนินการของโปรแกรมสามารถดำเนินต่อไปได้
ตัวอย่างเช่น ในสคริปต์ต่อไปนี้ได้มีการกำหนดให้ต้องมีทั้งลายเซ็นของบ๊อบและค่า preimage ที่เมื่อแฮชแล้วต้องได้ค่าตามที่กำหนด ทั้งสองเงื่อนไขนี้เองต้องถูกทำให้เป็นจริงจึงจะสามารถปลดล๊อคได้:
สคริปต์ที่มี OP_EQUALVERIFY เป็น guard clause
OP_HASH160 <expected hash> OP_EQUALVERIFY <Bob's Pubkey> OP_CHECKSIG
เพื่อที่จะใช้จ่ายเงินก้อนนี้บ๊อบจำเป็นต้องแสดง preimage และลายเซ็นที่ถูกต้องเท่านั้น
<Bob's Sig> <hash pre-image>
หากไม่ได้นำเสนอ preimage มาก่อน Bob จะไม่สามารถไปถึงส่วนของสคริปต์ที่ตรวจสอบลายเซ็นของเขาได้
สคริปต์นี้สามารถเขียนใหม่โดยใช้ OP_IF แทนได้ดังนี้:
สคริปต์ที่มี IF ทำหน้าที่เป็น guard clause
OP_HASH160 <expected hash> OP_EQUAL
OP_IF
<Bob's Pubkey> OP_CHECKSIG
OP_ENDIF
ข้อมูลยืนยันตัวตนของ Bob ยังคงเหมือนเดิม:
การทำให้สคริปต์ข้างต้นเป็นจริง
<Bob's Sig> <hash pre-image>
สคริปต์ที่มีการใช้ OP_IF ให้ผลลัพธ์เหมือนกับการใช้ opcode ที่มีคำต่อท้าย VERIFY โดยทั้งสองแบบทำหน้าที่เป็น guard clause เหมือนกัน แต่อย่างไรก็ตาม โครงสร้างที่ใช้ VERIFY ใช้จะมีประสิทธิภาพมากกว่า เนื่องจากมีการใช้ opcode น้อยกว่าสองตัว
แล้วเมื่อไหร่ที่เราควรใช้ VERIFY แล้วเมื่อไหร่ที่เราควรใช้ OP_IF ? หากสิ่งที่เราต้องการทำมีเพียงการกำหนดเงื่อนไขเบื้องต้น (guard clause) VERIFY จะเป็นทางเลือกที่ดีกว่า แต่หากเราต้องการให้มีเส้นทางการทำงานมากกว่าหนึ่งแบบ (flow control) เราจำเป็นต้องใช้โครงสร้างควบคุมลำดับการทำงานแบบ OP_IF...OP_ELSE แทน
การใช้ Flow control ในสคริปต์
วิธีใช้การควบคุมลำดับการทำงานโดยทั่วไปภายในสคริปต์ของบิตคอยน์นั้นมักเป็นการสร้างสคริปต์ที่เปิดโอกาสให้สามารถสร้างเส้นทางการทำงานของสคริปต์ได้หลายแบบ โดยแต่ละเส้นทางก็มีวิธีการในการใช้จ่าย UTXO ที่แตกต่างกัน
ลองมาพิจรณาจากตัวยอ่างง่าย ๆ นี้กัน โดยเรามีผู้ที่สามารถลงนามในธุรกรรมนี้ได้สองคน นั้นคืออลิซและบ๊อบโดยขอแค่เป็นใครคนใดคนหนึ่งในสองคนนี้ก็สามารถใช้จ่ายได้ โดยจะเขียนออกมาเป็น 1-of-2 multisig แต่เราจะทำสิ่งเดียวกันนี้โดยใช้คำสั่ง OP_IF:
OP_IF
<Alice's Pubkey>
OP_ELSE
<Bob's Pubkey>
OP_ENDIF
OP_CHECKSIG
ถ้ามองสคริปต์ในตัวอย่างแล้วคุณอาจสงสัยว่า แล้วเงื่อนไขอยู่ตรงไหน? ทำไมไม่มีอะไรอยู่ก่อนคำสั่ง IF เลย
ส่วนเหตุผลนั้นก็เป็นเพราะว่าเงื่อนไขนั้นไม่ได้เป็นส่วนหนึ่งของสคริปต์ แต่จะถูกส่งมาในตอนที่มีการใช้จ่ายแทน ซึ่งทำให้ Alice และ Bob สามารถ “เลือก” เส้นทางการทำงานที่ต้องการได้:
<Alice's Sig> OP_TRUE
OP_TRUE ที่อยู่ท้ายสุดทำหน้าที่เป็นเงื่อนไข (TRUE) ซึ่งจะทำให้คำสั่ง OP_IF เลือกเส้นทางการใช้จ่ายแรก โดยในเงื่อนไขนี้จะทำให้ public key ที่อลิซมีลายเซ็นตรงกันถูกนำขึ้นสแตก Opcode OP_TRUE หรือที่เรียกว่า OP_1 จะใส่ค่าเลข 1 ลงบนสแตก
ส่วนสำหรับบ๊อบหากต้องการใช้จ่าย UTXO นี้ เขาจะต้องเลือกเส้นทางการทำงานที่สองของ OP_IF โดยให้ค่า FALSE แทน Opcode OP_FALSE หรือที่เรียกว่า OP_0 จะใส่ค่าเป็นอาร์เรย์ไบต์ว่างลงบนสแตก:
<Bob's Sig> OP_FALSE
input สคริปต์ของ Bob จะทำให้คำสั่ง OP_IF ไปสคริปต์ในส่วนที่สอง (OP_ELSE) ซึ่งต้องการลายเซ็นของบ๊อบแทน
เนื่องจากคำสั่ง OP_IF สามารถซ้อนกันได้ เราจึงสามารถสร้าง “เขาวงกต” ของเส้นทางการทำงานได้ โดย input script จะทำหน้าที่เสมือน “แผนที่” ที่ใช้เลือกว่าจริง ๆ แล้วเส้นทางการทำงานใดจะถูกนำมาใช้
OP_IF
subscript A
OP_ELSE
OP_IF
subscript B
OP_ELSE
subscript C
OP_ENDIF
OP_ENDIF
ในตัวอย่างนี้จะมีเส้นทางการทำงานทั้งหมดสามเส้นทางด้วยกัน (subscript A, subscript B, subscript C) โดย input script จะเป็นผู้กำหนดเส้นทางตามรูปแบบของลำดับค่า TRUE หรือ FALSE อย่างเช่น หากต้องการเลือกเส้นทาง subscript B input script จะต้องลงท้ายด้วย OP_1 OP_0 (TRUE, FALSE) ค่าเหล่านี้จะถูกนำขึ้นแสตกทำให้ค่า FALSE อยู่จุดบนสุดของแสตก ซึ่ง OP_IF ชั้นนอกจะทำการดึงค่า FALSE ออกมาและนำส่วน OP_ELSE แรก จากนั้นค่า TRUE จะเลื่อนขึ้นมาอยู่บนสุดของแสตก และจะถูกประเมิณโดย OP_IF ชั้นใน ซึ่งจะเลือกเส้นทางการทำงานแบบ B
ด้วยโครงสร้างของสคริปต์แบบนี้ เราจะสามารถสร้าง redeem script ที่มีเส้นทางการทำงานได้หลายสิบ หรือหลายร้อยเส้นทาง โดยแต่ละเส้นจะเสนอวิธีการใช้จ่าย UTXO ที่แตกต่างกันออกไป เมื่อเราต้องการใช้จ่าย เราจะสร้าง input script ที่นำทางไปตามเส้นทางการทำงานนั้น ๆ ด้วยการใส่ค่า TRUE และ FALSE ที่เหมาะสมลงบนสแตกในแต่ละจุดของการควบคุมลำดับการทำงาน
ตัวอย่างสคริปต์แบบซับซ้อน
ในส่วนนี้เราจะนำแนวคิดทั้งหมดที่ได้เรียนรู้จากบทนี้มารวมกันไว้ในตัวอย่างเดียว
สมมุติว่าโมฮัมเหม็ด ซึ่งเป็นเจ้าของบริษัทนำเข้าและส่งออกในดูไบ เขาต้องการสร้างบัญชีเงินทุนของบริษัทที่มีกฏเกณฑ์ที่ยืดหยุ่น และแผนของเขาคือการออกแบบมาเพื่อให้ใช้ระดับในการอณุญาติที่แตกต่างกันไปตามเงื่อนไขของ timelock และผู้เข้าร่วมโครงสร้าง multisig นี้จะประกอบไปด้วย โมฮัมเหม็ด หุ้นส่วนอย่าง ซาอีดและไซรา และสุดท้ายเป็นทนายความของบริษัท โดยหุ้นส่วนทั้งสามตัดสินใจโดยใช้หลักเสียงข้างมาก ดังนั้นจะต้องมีอย่างน้อยสองในสามคนเห็นชอบสำหรับกรณีต่าง ๆ แต่อย่างไรก็ตาม ในกรณีที่เกิดปัญหากับ key ของพวกเขา พวกเขาเพียงต้องการให้ทนายความสามารถกู้คืนเงินได้ โดยใช้ลายเซ็นของหุ้นส่วนคนใดคนหนึ่งจากสามคนร่วมด้วย สุดท้ายแล้วหากหุ้นส่วนทั้งหมดไม่สามารถติดต่อได้หรือไม่สามารถปฏิบัติน่าที่ได้เป็นระยะเวลาหนึ่ง พวกเขาต้องการให้ทนายความสามารถจัดการบัญชีได้โดยตรงหลังจากที่เขาได้รับสิทธ์เข้าถึงบันทึกธุรกรรมของบัญชีเงินทุนนี้ได้
สคริปต์ไถ่ถอนแบบ multisignature ที่เปลี่ยนแปลงได้ร่วมกันกับ timelock คือสิ่งที่โมฮัมเหม็ดต้องออกแบบขึ้นมาเพื่อให้บรรลุเป้าหมายนี้
ตัวอย่างที่ 1: การใช้ Multisignature ร่วมกับ timelock
01 OP_IF
02 OP_IF
03 2
04 OP_ELSE
05 <30 days> OP_CHECKSEQUENCEVERIFY OP_DROP
06 <Lawyer's Pubkey> OP_CHECKSIGVERIFY
07 1
08 OP_ENDIF
09 <Mohammed's Pubkey> <Saeed's Pubkey> <Zaira's Pubkey> 3 OP_CHECKMULTISIG
10 OP_ELSE
11 <90 days> OP_CHECKSEQUENCEVERIFY OP_DROP
12 <Lawyer's Pubkey> OP_CHECKSIG
13 OP_ENDIF
สคริปต์ของโมฮัมเหม็ดใช้โครงสร้างควบคุมลำดับการทำงานแบบ OP_IF…OP_ELSE ที่ซ้อนกันเพื่อสร้างเส้นทางการทำงานทั้งหมดสามเส้นทาง
ในเส้นทางการทำงานเส้นทางแรก สคริปต์นี้จะทำงานเหมือน multisig 2-of-3 ธรรมดา ๆ ระหว่างหุ้นส่วนทั้งสาม โดยเส้นทางการทำงานนี้ประกอบด้วยบรรทัดที่ 3 และบรรทัดที่ 9 โดยบรรทัดที่ 3 กำหนดโดย quorum ของ multisig เป็น 2 (2-of-3) เส้นทางการทำงานนี้สามารถถูกเลือกได้โดยการใส่ OP_TRUE OP_TRUE ต่อท้ายใน input script:
ข้อมูลการใช้จ่ายสำหรับเส้นทางการทำงานแรก (multisig แบบ 2-of-3)
OP_0 <Mohammed's Sig> <Zaira's Sig> OP_TRUE OP_TRUE
TIP: OP_0 ที่อยู่ตอนต้นของ input script นี้มีสาเหตุมาจากความผิดปกติของ OP_CHECKMULTISIG ซึ่งจะดึงค่าออกจากแสตกมากกว่าที่ควรหนึ่งค่า ค่าที่ถูกดึงออกมาเกินนี้จะถูกละเลยโดย OP_CHECKMULTISIG แต่มันจำเป็นต้องมีอยู่ มิฉะนั้นสคริปค์จะล้มเหลว การใส่อาร์เรย์ไบต์ว่างด้วย OP_0 จึงเป็นวิธีแก้ปัญหาชั่วคราวสำหรับความผิดปกตินี้ ตามที่อธิบายไว้ในหัวข้อ An Oddity in CHECKMULTISIG Execution
ในเส้นทางการทำงานเส้นทางที่สองสามารถถูกใช้งานได้ก็ต่อเมื่อผ่านไปแล้ว 30 วันหลังจากที่ UTXO ถูกสร้างขึ้น และเมื่อถึงเวลานั้น สคริปต์จะต้องการลายเซ็นของทนายความ และลายเซ็นจากหนึ่งในหุ้นส่วน (multisig 1 of 3) โดยเงื่อนไขนี้ถูกกำหนดโดยบรรทัดที่ 7 ซึ่งตั้งค่า quorum ของ multisigไว้ที่ 1 และเพื่อเลือกใช้เส้นทางการทำงานนี้ input script จะต้องลงท้ายด้วย OP_FALSE OP_TRUE:
ข้อมูลการใช้จ่ายสำหรับเส้นทางการทำงานที่สอง (ทนายความ + multisig แบบ 1-of-3)
OP_0 <Saeed's Sig> <Lawer's Sig> OP_FALSE OP_TRUE
TIP: OP_FALSE จะถูกใส่ลงบนสแตกก่อน จากนั้น OP_TRUE จะถูกใส่ลงไปอยู่ด้านบนของมัน ดังนั้นค่า TRUE จึงถูกดึง (pop) ออกมาก่อนโดย opcode OP_IF ตัวแรก
สุดท้าย เส้นทางการทำงานที่สามอนุญาตให้ทนายความสามารถใช้จ่ายเงินได้เพียงคนเดียว แต่จะทำได้ก็ต่อเมื่อผ่านไปแล้ว 90 วันเท่านั้น เพื่อเลือกเส้นทางการทำงานนี้ Input script จะต้องลงท้ายด้วย OP_FALSE:
ข้อมูลการใช้จ่ายสำหรับเส้นทางการทำงานที่สาม (ทนายความ)
<Lawyer's Sig> OP_FALSE
ลองจำลองการรันสคริปต์บนกระดาษเพื่อดูว่ามันทำงานอย่างไรบนสแตก
Pay to witness public key hash (P2WPKH)
เริ่มจากดูตัวอย่างของ output script แบบ P2PKH กันก่อน:
ตัวอย่าง output script P2PKH
OP_DUP OP_HASH160 ab68025513c3dbd2f7b92a94e0581f5d50f654e7
OP_EQUALVERIFY OP_CHECKSIG
เมื่อใช้ segregated witness อลิซจะสร้างสคริปต์แบบ P2WPKH หากสคริปต์นั้นผูกกับ public key เดียวกัน ก็จะมีหน้าตาประมาณนี้:
0 ab68025513c3dbd2f7b92a94e0581f5d50f654e7
อย่างที่เห็นว่า output script แบบ P2WPKH นั้นเรียบง่ายกว่าสคริปต์แบบ P2PKH เป็นอย่างมากถ้าเราเอามาเปรียบเทียบกัน มันประกอบด้วยค่าเพียงแค่สองค่าเท่านั้นที่จะถูกใส่ลงในแสตกสำหรับการประมวลผลสคริปต์ โดยสำหรับไคล์เอนต์บิตคอยน์รุ่นเก่า ๆ (ที่ยังไม่รองรับ segwit) การใส่สองค่านี้จะดูเหมือน output ที่ใครก็สามารถใช้จ่ายได้ แต่สำหรับไคลเอนต์รุ่นใหม่ที่รองรับ segwit ตัวเลขตัวแรก (0) จะถูกตีความเป็นหมายเลขเวอร์ชัน (witness version) และส่วนที่สอง (ขนาด 20 ไบต์) คือ witness program โดย witness program ขนาด 20 ไบต์นี้ก็คือแฮชของ public key เช่นเดียวกับในสคริปต์แบบ P2PKH
ตอนนี้เรามาดูธุรกรรมที่เกี่ยวข้องกันซึ่งบ๊อบใช้เพื่อจ่าย output นี้กัน โดยสำหรับสคริปต์แบบ lagacy นั้น ธุรกรรมที่ใช้จ่ายนั้นจะต้องใส่ลายเซ็นไว้ในส่วน input ของธุรกรรม:
[...]
"vin" : [
"txid": "abcdef12345...",
"vout": 0,
"scriptSig": “<Bob’s scriptSig>”,
]
[...]
แต่อย่างไรก็ตาม เพื่อใช้จ่าย output แบบ P2WPKH ธุรกรรมจะไม่มีลายเซ็นอยู่ในส่วนของ input เลย แต่แทนที่จะเป็นเช่นนั้น ธุรกรรมของบ๊อบจะมี input script ที่ว่างเปล่า และจะใส่ข้อมูลไว้ในส่วนของ witness แทน:
[...]
"vin" : [
"txid": "abcdef12345...",
"vout": 0,
"scriptSig": “”,
]
[...]
“witness”: “<Bob’s witness structure>”
[...]
การสร้างกระเป๋าเงินแบบ P2WPKH
สิ่งที่สำคัญที่สุดและควรจะทราบไว้คือ witness program แบบ P2WPKH ควรจะถูกสร้างขึ้นโดยผู้รับเงินเท่านั้น และไม่ควรแปลงมาจาก public key ของผู้ที่รู้จักอยู่แล้ว เพราะสคริปต์ของ P2PKH หรือแม้แต่จาก address ใด ๆ นั้นผู้ใช้จ่ายไม่มีทางรู้ได้เลยว่ากระเป๋าเงินของผู้รับนั้นสามารถสร้างธุรกรรมแบบ segwit และสามารถใช้จ่าย output แบบ P2WPKH ได้หรือไม่
นอกจากนี้ output แบบ P2WPKH นั้นจะต้องถูกสร้างจาก hash ของ compressed public key เท่านั้น เพราะว่า public key แบบ uncompressed นั้นถือว่าไม่อยู่ในมาตรฐานของ segwit และอาจจะถูกปิดการใช้งานอย่างชัดเจนใน soft fork ที่เกิดขึ้นภายหลัง หาก hash ที่ใช้ได้ใน P2WPKH มาจาก public key แบบ uncompressed จะทำให้ output ที่เกิดขึ้นนั้นไม่สามารถจ่ายออกได้อีก และจะทำให้คุณอาจที่จะสูญเสียเงินนั้นไปตลอดกาล นั่นจึงเป็นเหตุผลที่ว่าทำไม output แบบ P2WPKH นั้นถึงควรถูกสร้างโดยแอปกระเป๋าเงินของผู้รับผ่านการสร้าง compress public key จาก private key ของเขาเอง
Warning: P2WPKH ควรถูกสร้างโดยผู้รับ โดยการนำ public key แบบ compressed มาแปลงเป็น P2WPKH hash เท่านั้น ไม่ควรให้ผู้ใช้หรือบุคคลอื่นใด แปลง P2PKH script, Address, หรือ public key แบบ uncompressed ให้กลายเป็น P2WPKH witness script ซึ่งเป็นเรื่องปกติอยู่แล้วที่ผู้ใช้ควรส่งเงินให้ผู้รับ ในรูปแบบที่ผู้รับได้ระบุไว้เท่านั้น
Pay to witness script hash (P2WSH)
witness program ประเภทนี้จะสอดคล้องกันกับ P2SH ที่เราได้ผ่านกันมาก่อนหน้านี้ ในหัวข้อของ ในหัวข้อ Pay to Script Hash และในตัวอย่างที่บริษัทของนายโมฮัมเหม็ดได้ใช้สคริปต์แบบ P2SH เพื่อทำ multisig โดยสคริปต์ที่ถูกเข้ารหัสของเขาจะมีหน้าตาแบบนี้
OP_HASH160 54c557e07dde5bb6cb791c7a540e0a4796f5e97e OP_EQUAL
สคริปต์ P2SH นี้อ้างอิงไปยังแฮชของ redeem script ซึ่งได้กำหนดเงื่อนไขการใช้จ่ายแบบ multisig 2-of-3 เอาไว้เพื่อใช้ในการปลดล๊อคเงินทุน การจะใช้จ่าย output นี้ บริษัทของโมฮัมเหม็ดจะต้องแสดง redeem script ซึ่งมีค่าแฮชตรงกันกับ script hash ที่อยู่ภายใน P2SH output พร้อมกับลายเซ็นที่จำเป็นตามเงื่อนไขของ redeem script นั้น โดยใส่ทั้งหมดไว้ภายใน transaction input:
[...]
"vin" : [
"txid": "abcdef12345...",
"vout": 0,
"scriptSig": “<SigA> <SigB> <2 PubA PubB PubC PubD PubE 5 OP_CHECKMULTISIG>”,
]
ทีนี้เรามาดูกันดีกว่าว่าตัวอย่างที่เกริ่นมาข้างต้นนี้จะถูกอัปเกรดเป็น segwit v0 ได้อย่างไร หากลูกค้าของนายโมฮัมเหม็ดใช้กระเป๋าเงินที่รับรอง segwit พวกเขาจะทำการชำระเงินได้โดยการสร้าง P2WSH output ซึ่งมีลักษณะดังนี้:
0 a9b7b38d972cabc7961dbfbcb841ad4508d133c47ba87457b4a0e8aae86dbb89
เช่นเดียวกันกับตัวอย่างของ P2WPKH คุณจะเห็นได้ว่าสคริปต์แบบ segregated witness ที่ทำงานในแบบเดียวกันนั้นเรียบง่ายกว่า และนอกจากนี้ยังช่วยลด overhead ของเทมเพลตที่พบในสคริปต์ P2SH ลงไปได้มากอีกด้วย เนื่องจากสคริปต์ของ segreted witness นั้นประกอบด้วยค่าเพียง 2 ค่าที่ถูกใส่ลงในแสตก นั่นคือ witness version(0) และค่าแฮชของ SHA256 ขนาด 32 ไบต์ของ witness script (ในบางครั้งอาจถูกเรียกว่า witness program)
TIP: ในขณะที่ P2SH ใช้แฮชแบบ RIPEMD160(SHA256(script)) ขนาด 20 ไบต์ แต่ witness program ของ P2WSH จะใช้แฮชแบบ SHA256(script) ขนาด 32 ไบต์ ความแตกต่างในการเลือกอัลกอริทึมในการแฮชนี้ถูกออกแบบมาเพื่อเพิ่มความแข็งแกร่งในด้านความปลอดภัยให้กับ P2WSH แต่ในบางกรณีก็มีบ้างที่ความปลอดภัยระดับ 128 บิตใน P2WSH นั้นมีความปลอดภัยเท่ากับ 80 บิตใน P2SH ดูรายละเอียดเพิ่มเติมได้ที่ P2SH collision attack
หลังจากนั้นบริษัทของนายโมฮัมเหม็ดก็สามารถใช้จ่าย output แบบ P2WSH ได้โดยการนำเสนอ witness script ที่ถูกต้องพร้อมกับลายเซ็นในจำนวนที่เพียงพอเพื่อทำให้สคริปต์นั้นผ่านเงื่อนไขของ witness script และลายเซ็นเหล่านี้เองจะถูกรวมอยู่ในโครงสร้างของ witness โดยจะไม่มีข้อมูลใดถูกใส่ไว้ใน input script เนื่องจากนี่เป็น native witness program ซึ่งไม่ใช้ฟิลด์ input script เหมือนกับประเภท legacy
[...]
"vin" : [
"txid": "abcdef12345...",
"vout": 0,
"scriptSig": “”,
]
[...]
“witness”: “<SigA> <SigB> <2 PubA PubB PubC PubD PubE 5 OP_CHECKMULTISIG>”
[...]
ความแตกต่างระหว่าง P2WPKH และ P2WSH
ในสองหัวข้อก่อนหน้านี้เราได้อธิบายถึง witness program อยู่ 2 ประเภท ได้แก่ Pay to Witness Public Key Hash (P2WPKH) และ Pay to Witness Script Hash (P2WSH) โดย witness program ของทั้งสองประเภทนั้นมีหมายเลขเวอร์ชันเดียวกัน ตามด้วยการใส่ข้อมูลหนึ่งค่าลงในแสตกเหมือนกัน ทำให้รูปแบบของทั้งสองดูคล้ายคลึงกัน แต่การตีความของทั้งสองนั้นแตกต่างกันอย่างสิ้นเชิง โดยแบบหนึ่งจะถูกตีความเป็นแฮชของ public key ซึ่งปลดล๊อกได้ด้วยลายเซ็นในขณะที่อีกแบบหนึ่งนั้นจะถูกตีความเป็นค่าแฮชของสคริปต์ซึ่งปลดล๊อกได้ด้วย witness script
และความแตกต่างที่สำคัญที่สุดระหว่างทั้งสองคือความยาวของ witness program:
- witness program ของ P2WPKH มีขนาด 20 ไบต์
- witness program ของ P2WSH มีขนาด 32 ไบต์
ความแตกต่างเพียงจุดเดียวนี้เองทำให้โหนดของบิตคอยน์สามารถแยกได้ว่า witness program นั้นเป็นของสคริปต์รูปแบบใด โดยดูจากเพียงแค่ความยาวของแฮช
การอัปเกรดไปใช้ Segregated Witness
จากตัวอย่างก่อนหน้านี้ เราจะเห็นได้ว่าการอัปเกรดไปใช้ segregated witness นั้นเป็นกระบวนการที่มี 2 ขั้นตอนด้วยกัน ขั้นตอนแรก คือกระเป๋าเงินต้องสามารถสร้าง output แบบ segwit ได้ จากนั้น output เหล่านี้จึงสามารถถูกใช้ได้ด้วยกระเป๋าเงินที่รู้วิผะีสร้างธุรกรรมแบบ segregated witness โดยในตัวอย่างที่กล่าวมากระเป๋าเงินของอลิซสามารถสร้าง output จ่ายไปยังสคริปต์แบบ segregated witness ได้และกระเป๋าของบ๊อบก็รองรับ segwit และสามารถใช้จ่ายได้เช่นกัน
Segregated witness ถูกนำมาใช้ในลักษณะของการอัปเกรดที่ยังคงเข้ากันได้ย้อนหลัง (backward-compatible) ซึ่งทำให้ไคลเอนต์ทั้งแบบเก่าและแบบใหม่สามารถอยู่ร่วมกันได้ นักพัฒนากระเป๋าเงินได้อัปเกรดซอฟต์แวร์ของตนอย่างอิสระเพื่อเพิ่มความสามารถรองรับ segwit ส่วนรูปแบบเดิมอย่าง P2PKH และ P2SH ก็ยังคงใช้งานได้ตามปกติสำหรับกระเป๋าเงินที่ยังไม่ได้อัปเกรด
สิ่งนี้ทำให้เกิดสองสถานการณ์สำคัญ ซึ่งจะถูกอธิบายในหัวข้อถัดไป:
- ความสามารถของกระเป๋าเงินฝั่งผู้จ่ายที่ไม่รองรับ segwit ในการชำระเงินให้กับกระเป๋าเงินฝั่งผู้รับที่สามารถประมวลผลธุรกรรมแบบ segwit ได้
- ความสามารถของกระเป๋าเงินฝั่งผู้จ่ายที่รองรับ segwit ในการรับรู้และแยกแยะได้ว่าผู้รับรายใดรองรับ segwit และรายใดไม่รองรับ โดยพิจารณาจากรูปแบบของ address
การฝัง segregated witness ไว้ภายใน P2SH
สมมุติว่ากระเป๋าเงินของอลิซยังไม่ได้อัปเกรดเป็น segwit แต่กระเป๋าเงินของบ๊อบได้อัปเกรดแล้ว และสามารถจัดการธุรกรรมแบบ segwit ได้ ทั้งอลิซและบ๊อบยังสามารถใช้ output แบบดั้งเดิมที่ไม่ใช้ segwit ได้ตามปกติ อย่างไรก็ตามบ๊อบมักจะต้องการใช้ segwit เพื่อช่วยลดค่าธรรมเนียมธุรกรรมโดยอาศัยต้นทุนที่ต่ำกว่าสำหรับส่วน witness
ในกรณีนี้กระเป๋าเงินของบ๊อบสามารถสร้างที่อยู่แบบ P2SH ซึ่งภายในบรรจุสคริปต์ segwit อยู่ได้ กระเป๋าเงินของอลิซสามารถส่งเงินไปยัง address นั้นได้โดยไม่จำเป็นต้องรู้จักหรือเข้าใจ segwit แต่อย่างใด นอกจากนี้กระเป๋าเงินของบ๊อบก็สามารถใช้จ่าย output นั้นด้วยธุรกรรมแบบ segwit ได้ และทำให้ได้ประโยชน์จาก segwit และช่วยลดค่าธรรมเนียมธุรกรรมได้
สคริปต์ witness ทั้งสองรูปแบบ คือ P2WPKH และ P2WSH สามารถถูกฝังอยู่ภายใน address แบบ P2SH ได้ โดยจะเรียกว่า nested P2WPKH และ nested P2WSH ตามลำดับ
Nested pay to witness public key hash
รูปแบบแรกของ output script ที่เราจะพิจรณาคือ nested P2WPKH ซึ่งเป็น witness program แบบ pay to witness public key hash ที่ถูกฝังอยู่ภายในสคริปต์แบบ pay to script hash เพื่อให้กระเป๋าเงินที่ยังไม่รู้จัก segwit สามารถส่งเงินไปยัง output นี้ได้
กระเป๋าเงินของบ๊อบจะสร้าง P2WPKH witness program จาก public key ของ Bob จากนั้นนำ witness program นี้ไปแฮช และนำค่าแฮชที่ได้ไปเข้ารหัสเป็นสคริปต์แบบ P2SH โดยตัวสคริปต์นี้เองจะถูกแปลงเป็น address ของบิตคอยน์ ซึ่งจะเป็น address ที่ขึ้นต้นด้วย 3 เช่นเดียวกันกับ Pay to script hash
กระเป๋าเงินของบ๊อบเริ่มต้นด้วย witness version และ witness program แบบ P2WPKH ที่เราได้เห็นก่อนหน้านี้:
0 ab68025513c3dbd2f7b92a94e0581f5d50f654e7
ข้อมูลนี้จะประกอบด้วย witness version และค่าแฮชของ public key ของบ๊อบขนาด 20 ไบต์
จากนั้นกระเป๋าของเขาจะนำข้อมูลนี้ไปแฮช โดยเริ่มจาก SHA256 ก่อน แล้วตามด้วย RIPEMD 160 ทำให้ได้ค่าแฮชขนาด 20 ไบต์อีกค่าหนึ่ง ต่อมาแฮชของ redeem script นี้จะถูกแปลงเป็น address ของบิตคอยน์ในท้ายที่สุด จากนั้นกระเป๋าเงินของอลิซก็สามารถส่งเงินไปยังที่อยู่ 37Lx99uaGn5avKBxiW26HjedQE3LrDCZru ได้เช่นเดียวกันกับการส่งไปยัง address ทั่ว ๆ ไป
เพื่อจ่ายเงินให้บ๊อบ กระเป๋าเงินของอลิซจะล็อกเอาต์พุตด้วยสคริปต์แบบ P2SH ดังนี้:
OP_HASH160 3e0547268b3b19288b3adef9719ec8659f4b2b0b OP_EQUAL
แม้ว่ากระเป๋าเงินของอลิซจะไม่รองรับ segwit เลยก็ตาม แต่การชำระเงินที่เธอสร้างขึ้นนี้บ๊อบก็ยังสามารถนำไปใช้จ่ายต่อได้ด้วยธุรกรรมแบบ segwit
Nested pay to witness script hash
ในทำนองเดียวกันกับ witness program แบบ P2WSH สำหรับสคริปต์ multisig หรือสคริปต์ที่ซับซ้อนอื่น ๆ สามารถถูกฝังอยู่ภายใน address แบบ P2SH ได้ ทำให้กระเป๋าเงินใด ๆ ก็ตามสามารถชำระเงินได้ในลักษณะที่เข้ากันได้กับ segwit
ดังที่เราเห็นในหัวข้อของ Pay to witness script hash (P2WSH) บริษัทของโมฮัมเหม็ดใช้การชำระเงินแบบ segregated witness ไปยังสคริปต์ multisignature เพื่อให้ลูกค้าทุกรายสามารถชำระเงินให้บริษัทของเขาได้ ไม่ว่ากระเป๋าเงินของลูกค้าจะอัปเกรดรองรับ segwit แล้วหรือไม่ กระเป๋าเงินของโมฮัมเหม็ดก็สามารถผัง P2WSH witness program ไว้ภายในสคริปต์แบบ P2SH ได้
ขั้นแรกกระเป๋าเงินของโมฮัมเหม็ดจะนำ witness script ไปแฮชด้วย SHA256(ครั้งเดียว) และได้ค่าแฮชดังนี้:
9592d601848d04b172905e0ddb0adde59f1590f1e553ffc81ddc4b0ed927dd73
ถัดมา witness script ที่ถูกแฮชแล้วจะถูกแปลงให้เป็น P2WSH witness program ที่มีการใส่เวอร์ชันนำหน้า:
0 9592d601848d04b172905e0ddb0adde59f1590f1e553ffc81ddc4b0ed927dd73
จากนั้น witness program นี้จะถูกแฮชอีกครั้งด้วย SHA256 และ RIPEMD160 ตามลำดับ ทำให้ได้แฮชขนาด 20 ไบต์ใหม่ดังงนี้:
86762607e8fe87c0c37740cddee880988b9455b2
ต่อมากระเป๋าเงินจะสร้าง address บิตคอยน์ แบบ P2SH จากแฮชนี้:
3Dwz1MXhM6EfFoJChHCxh1jWHb8GQqRenG
โดยจากจุดนี้เอง ลูกค้าของโมฮัมเหม็ดก็สามารถชำระเงินมายัง address นี้ได้ แม้ว่ากระเป๋าเงินของพวกเขาจะไม่รับรอง segwit ก็ตามเพื่อส่งเงินให้โมฮัมเหม็ด กระเป๋าเงินของผู้ส่งจะล็อก output ด้วยสคริปต์ P2SH ดังต่อไปนี้:
OP_HASH160 86762607e8fe87c0c37740cddee880988b9455b2 OP_EQUAL
จากนั้นบริษัทของโมฮัมเหม็ดก็สามารถสร้างธุรกรรมแบบ segwit เพื่อใช้จ่ายเงินเหล่านี้ได้ โดยได้รับประโยชน์จากคุณสมบัติของ segwit รวมถึงค่าธรรมเนียมธุรกรรมที่ต่ำลง
Merklized Alternative Script Trees (MAST)
การใช้ OP_IF นั้นสามารถกำหนดเงื่อนไขสำหรับการใช้จ่ายได้หลายรูปแบบ แต่แนวทางนี้เองก็มีข้อเสียที่ไม่พึงประสงค์อยู่หลายประการ อาทิเช่น
น้ำหนัก(ต้นทุนในการทำธุรกรรม)
ทุกเงื่อนไขที่เพิ่มเข้าไปในสคริปต์นั้นจะทำให้สคริปต์มีขนาดใหญ่ขึ้น ซึ่งจะส่งผลให้ต้นทุนในการทำธุรกรรมเพิ่มขึ้นเมื่อต้องการใช้จ่ายบิตคอยน์ที่ถูกป้องกันไว้ด้วยสคริปต์นี้
ข้อจำกัดด้านขนาด
แม้ว่าคุณจะยอมจ่ายค่าธรรมเนียมเพิ่มเพื่อให้ใส่เงื่อนไขได้มากขึ้นก็ตาม แต่ก็ยังมีข้อจำกัดในด้านของเงื่อนไขสูงสุดที่สามารถใส่ได้ในสคริปต์อยู่ดี อย่างเช่น legacy script ถูกจำกัดขนาดไว้ที่ 10,000 ไบต์ ซึ่งทำให้ในทางปฏิบัตินั้นสามารถใส่ได้เพียงไม่กี่ร้อยเงื่อนไขเท่านั้น และแม้ว่าจะสามารถสร้างสคริปต์ที่มีขนาดใหญ่เท่ากับบล๊อคทั้งบล๊อคได้ ก็ยังจะมีได้เพียงประมาณ 20,000 เงื่อนไขเท่านั้นที่ใช้งานได้จริง ซึ่งถือว่ามากแล้วสำหรับการชำระเงินแบบง่าย ๆ แต่ก็เล็กมากเมื่อเทียบกับการใช้งานบางอย่างที่จินตนาการไว้สำหรับบิตคอยน์
การขาดแคลนความเป็นส่วนตัว
ทุกเงื่อนไขที่มีการเพิ่มเข้าไปในสคริปต์นั้นจะกลายเป๋นข้อมูลสาธารณะเมื่อคุณใช้จ่ายบิตคอยน์ที่ถูกปกป้องไว้ด้วยสคริปต์นั้น ๆ ตัวอย่างเช่นทนายความและหุ้นส่วนทางธุรกิจของนายโมฮัมเหม็ดก็จะสามารถเห็นสคริปต์ทั้งหมดไม่ว่าจะในตัว multi-signature หรือ timelock ก็ตาม และทุกครั้งที่มีการใช้จ่ายกับบัญชีนี้ จะทำให้ทนายความของพวกเขาติดตามธุรกรรมทั้งหมดได้แม้ว่าจะไม่ได้ร่วมลงนามก็ตาม
แต่อย่างไรก็ตาม บิตคอยน์มีการใช้โครงสร้างข้อมูลที่เรียกว่า merkle tree อยู่แล้ว ซึ่งมันช่วยให้สามารถตรวจสอบได้ว่าองค์ประกอบใด ๆ เป็นสมาชิกของข้อมูลหรือไม่โดยไม่จำเป็นต้องเปิดเผยหรือระบุสมาชิกอื่น ๆ ทั้งหมดของชุดนั้น
ซึ่งเราจะได้เรียนรู้เกี่ยวกับ merkle tree มากขึ้นในหัวข้อของ merkle trees แต่สาระสำคัญคือ สมาชิกแต่ละตัวของชุดข้อมูลที่เราต้องการ (เช่น เงื่อนไขการอณุญาตที่มีความยาวเท่าใดก็ได้) สามารถนำไปผ่านฟังก์ชันแฮชแล้วนำแฮชมารวมกันอีกครั้งเพื่อสร้างค่าผูกมัดของกิ่งทั้งสองกิ่ง ซึ่งเรียกว่า branch commitment
โดยค่าผูกมัดของกิ่งทั้งสองเองก็สามารถสร้างได้ด้วยวิธีเดียวกัน ขั้นตอนนี้จะถูกทำซ้ำเรื่อย ๆ จนเหลือตัวเดียวและเราจะเรียกตัวนั้นว่า merkle root
จากตัวอย่างสคริปต์ในหัวข้อ Variable multi-signature with timelock เราสามารถสร้าง Merkle tree สำหรับเงื่อนไขการอณุญาตทั้งสามแบบใน A MAST ที่มีสามสคริปต์ย่อยได้

ในขณะนี้เราสามารถสร้างหลักฐานการเป็นสมาชิกแบบกระทัดรัด (Compact membership proof) เพื่อพิสูจน์ได้ว่าเงื่อนไขการอนุญาตเงื่อนไขใดเงื่อนไขหนึ่งเป็นสมาชิกของ merkle tree โดยไม่ต้องเปิดเผยรายละเอียดใด ๆ ของสมาชิกอื่น ๆ ใน merkle tree สามาดูได้ในตัวอย่างของ A MAST membership proof for one of the subscripts และสังเกตว่าโหนดที่แรเงาไว้สามารถคำนวณได้จากข้อมูลอื่นที่ผู้ใช้ให้มา ดังนั้นจึงไม่จำเป็นต้องระบุโหนดเหล่านั้นในขณะทำการใช้จ่าย

ค่าแฮชที่ใช้ในการสร้างคอมมิจเมนต์แต่ละตัวมีขนาด 32 ไบต์ ดังนั้นการพิสูจน์ว่าการใช้จ่ายใน A MAST membership proof for one of the subscripts ได้รับอนุญาต (โดยใช้ merkle tree และเงื่อนไขที่เกี่ยวข้อง) และได้รับการพิสูจน์ตัวตน(ด้วยการใช้ลายเซ็น) จะใช้ข้อมูลทั้งหมด 383 ไบต์เมื่อเปรียบเทียบกันแล้ว การใช้จ่ายแบบเดียวกันโดยไม่ใช้ merkle tree (กล่าวคือ ต้องแสดงเงื่อนไขทั้งหมดที่เป็นไปได้) ที่จะใช้ข้อมูลถึง 412 ไบต์
การประหยัดได้ 29 ไบต์ (7%) ในตัวอย่างนี้ยังสะท้อนให้เห็นถึงศักยภาพในการประหยัดได้ไม่ครบถ้วน เนื่องจากลักษณะของ merkle tree ที่เป็นโครงสร้างแบบไบนารีทรีนั้น ทุกครั้งที่มีการเพิ่มจำนวนสมาชิกในเซตเพิ่มขึ้นเป็นสองเท่า เราจำเป็นต้องเพิ่มคอมมิตเมนต์ขนาด 32 ไบต์เพียงตัวเดียวเท่านั้น เช่นในกรณีที่มีทั้งหมด 3 เงื่อนไขเราจำเป็นต้องใช้คอมมิตเมนต์ 3 ตัว (ซึ่งหนึ่งในนั้นคือ merkle root ที่ต้องถูกรวมในการอณุญาตอยู่แล้ว) และเรายังสามารถมีคอมมิตเมนต์ได้ถึง 4 ตัวด้วยต้นทุนที่เท่าเดิม และหากมีคอมมิตเมนต์เพิ่มขึ้นอีกตัวหนึ่งจะทำให้เราสามารถรองรับเงื่อนไขได้สูงสุดถึงแปดเงื่อนไขด้วยกัน และในทำนองเดียวกันหากเรามี 16 คอมมิตเมนต์(ขนาดรวมเป็น 512 ไบต์) เราจะสามารถมีเงื่อนไขการอณุญาตได้มากกว่า 32,000 เงื่อนไข ซึ่งนั้นก็มากเกินกว่าที่จะนำไปใช้ได้จริงในบล็อก ๆ หนึ่งที่เต็มไปด้วยธุรกรรมที่มีคำสั่ง OP_IF ทั้งหมดเสียอีก และหากเราใช้ถึง 128 คอมมิตเมนต์ (4096 ไบต์) จำนวนเงื่อนไขที่เราจะสามารถสร้างได้ในเชิงทฤษฏีจะมีมากเกินกว่าเงื่อนไขทั้งหมดที่คอมพิวเตอร์ทุกเครื่องบนโลกจะสามารถสร้างขึ้นมาได้
โดยทั่วไปแล้ว เงื่อนไขการอณุญาตไม่ได้มีโอกาสถูกใช้งานเท่า ๆ กันทั้งหมด ในกรณีตัวอย่างของเรา เราคาดว่าโมฮัมเหม็ดและหุ้นส่วนของเขาจะใช้จ่ายเป็นประจำ ส่วนเงื่อนไขการหน่วงเวลามีไว้ใช้ในกรณีที่เกิดปัญหาเท่านั้น เราสามารถนำความรู้นี้มาใช้ในการปรับโครงสร้างของ tree ใหม่ทั้งหมดได้ ดังที่แสดงในรูปภาพข้างล่างนี้ โดยมีการจัดสคริปต์ที่คาดว่าจะมีการใช้งานมากที่สุดอยู่ในตำแหน่งที่เหมาะสมที่สุด

ในกรณีนี้ เราจำเป็นที่จะต้องให้คอมมิตเมนต์เพียงสองค่าเท่านั้นในกรณีที่ใช้จ่ายแบบที่คาดว่าจะบ่อยที่สุด (ประหยัดได้ 32 ไบต์) แม้ว่าการใช้จ่ายด้วยกรณีอื่น ๆ ยังคงจำเป็นต้องใช้ 3 คอมมิตเมนต์ก็ตาม ซึ่งแบบว่าหากคุณทราบหรือสามารถคาดเดาถึงความน่าจะเป็นของการใช้เงื่อนไขแต่ละแบบได้ คุณจะสามารถใช้อัลกอริทึมอย่าง Huffman เพื่อจัดวางเงื่อนไขเหล่านี้ลงไปในตำแหน่งที่ทำให้มีประสิทธิภาพสูงสุดได้ โดยสามารถดูรายละเอียดเพิ่มเติมได้ใน BIP341
นอกจากจะเพิ่มความซับซ้อนให้กับบิตคอยน์มาอีกเล็กน้อยแล้ว MAST ก็ยังแทบไม่มีข้อเสียที่สำคัญอะไรต่อบิตคอยน์เลย และก่อนที่จะพบแนวทางที่ดีกว่าในภายหลัง (เดี๋ยวเราจะได้เห็นในหัวข้อ taproot) ก็มีข้อเสนอที่แข็งแรงอยู่แล้วถึงสองอันสำหรับ MAST นั้นคือ BIP114 และ BIP116
MAST เทียบกับ MAST
แนวคิดแรกเริ่มของสิ่งที่ปัจจุบันเราเรียกว่า MAST ในบิตคอยน์ คือแนวคิดของ merklized abstract syntax trees (ต้นไม้โครงสร้างไวยากรณ์เชิงนามธรรมที่ถูกเมอร์เคิลไลซ์) ในต้นไม้โครงสร้างไวยากรณ์เชิงนามธรรม (AST) แต่ละเงื่อนไขในสคริปต์จะสร้างสาขาใหม่ ดังที่แสดงไว้ในรูปภาพข้างล่าง

AST นั้นได้ถูกใช้อย่างแพร่หลายในโปรแกรมที่ทำหน้าที่แยกวิเคราห์และปรับแต่งโค้ดของโปรแกรมอื่น ๆ เช่น คอมไพเลอร์ หากนำ AST มาใช้ในรูปแบบที่ถูกเมอร์เคิลไลซ์ (merklized AST) ก็จะสามารถผูกมัดได้กับทุกส่วนของโปรแกรม และเปิดให้ใช้คุณสมบัติต่าง ๆ ที่อธิบายไว้ใน merklized Alternative Script Trees (MAST) แต่อย่างไรก็ตามวิธีนี้จะต้องเปิดเผยค่าแฮชขนาด 32 ไบต์อย่างน้อยหนึ่งค่า สำหรับทุกส่วนย่อยของโปรแกรม ซึ่งทำให้ไม่ค่อยมีประสิธิภาพในด้านพื้นที่ของบล็อกเชนสำหรับโปรแกรมส่วนใหญ่
สิ่งที่ผู้คนส่วนใหญ่มักเรียกว่า MAST ในบริบทของบิตคอยน์ในปัจจุบันคือ merklized alternative script trees ซึ่งเป็น backronym ที่ถูกตั้งขึ้นโดยนักพัฒนา Anthony Towns ซึ่ง alternative script tree คือชุดของสคริปต์หลายสคริปต์ ซึ่งแต่ละสคริปต์นั้นมีความสมบูรณืในตัวเองและสามารถเลือกใช้งานได้เพียงหนึ่งสคริปต์เท่านั้น ทำให้สครืปต์เหบ่านี้เป็นทางเลือกแทนกัน ซึ่งแสดงไว้ในภาพด้านล่าง

ต้นไม้สคริปต์แบบทางเลือก (alternative script trees) ต้องการเพียงเปิดเผยไดเจสเพียง 32 ไบต์เท่านั้นสำหรับความลึกในแต่ละลำดับที่ผู้ใช้เลือกใช้และ merkle root สำหรับสคริปต์ส่วนใหญ่ นี่ถือเป็นการใช้พื้นที่ในบล๊อกเชนที่มีประสิธิภาพมากกว่าเป็นอย่างมาก
Pay to Contract (P2C)
ดังที่เราได้เห็นในบทก่อนหน้าในหัวข้อ public child key derivation คณิตศาสตร์ของการเข้ารหัสด้วยเส้นโค้งวงรี (Eliptic Curve Cryptography:ECC) อณุญาตให้อลิซใช้ private key เพื่อสร้าง public key ที่เธอมอบให้บ๊อบ สามารถเพิ่มค่าที่เป็นไปได้ค่าใดก็ได้เข้ามาใน public key นั้น เพื่อสร้าง public key ใหม่ขึ้นมา หากบ๊อบส่งค่าที่เขาเพิ่มนั้นกลับไปให้อลิซ เธอก็สามารถนำค่าเดียวกันไปบวกกับ private key ของเธอ เพื่อสร้าง private key ที่สอดคล้องกับ public key ที่ถูกอนุพันธ์ขึ้นมาได้
สรุปสั้น ๆ ก็คือบ๊อบสามารถสร้าง child public key ซึ่งมีเพียงอลิซเท่านั้นที่สามารถสร้าง private key ที่สอดคล้องกันขึ้นมาได้ สิ่งนี้เป็นประโยชน์สำหรับการกู้คืนกระเป๋าเงินแบบ Hierarchical Determinstic (HD) ตามมาตรฐานของ BIP32 แต่ก็ยังสามารถนำไปใช้ในวัตถุประสงค์อื่นได้อีกด้วย
ลองจินตนาการดูสิว่าบ๊อบต้องการซื้อของบางอย่างจากอลิซ แต่เขาก็ต้องการความสามารถในการพิสูจน์ภายหลังได้ว่าเขาจ่ายเงินไปเพื่ออะไร ในกรณีที่เกิดข้อพิพาทขึ้นอลิซและบ๊อบจึงตกลงกันเกี่ยวกับชื่อของสินค้าหรือบริการที่ขาย (เช่น “พอดแคสต์ของอลิซตอนที่#123”) แล้วแปลงคำอธิบายนั้นให้เป็นตัวเลข โดยการนำไปแฮชและตีความค่าแฮชที่ได้เป็นตัวเลข จากนั้นบ๊อบนำตัวเลขนั้นไปบวกกับ public key ของอลิซและจ่ายเงินให้ไป กระบวนการนี้เรียกว่า key tweaking และตัวเลขนั้นเรียกว่า tweak
อลิซสามารถใช้จ่ายเงินดังกล่าวได้โดยการปรับแต่ง(tweak) private key ของตัวเธอเองด้วยเลขเดียวกัน
และในภายหลังบ๊อบเองก็สามารถพิสูจน์กับใครก็ตามได้ว่าเขาได้จ่ายเงินให้อลิซเพื่ออะไร โดยการเปิดเผยถึงกุญแจตั้งต้น (underlying key) ของอลิซและคำอธิบายของสินค้าหรือบริการที่ทั้งสองใช้ร่วมกัน ทุกคนสามารถตรวจสอบได้ว่า public key ที่ถูกนำไปใช้ชำระเงินนั้นเท่ากับกุญแจตั้งต้นบวกกับค่าแฮช ของคำอธิบายนั้น และหากอลิซยอมรับว่ากุญแจดังกล่าวเป็นของเธอ ก็ย่อมแสดงว่าเธอยอมรับว่าเธอเป็นผู้รับเงินก้อนนั้น และหากอลิซเป็นผู้ใช้จ่ายเงินออกไป ก็ยิ่งเป็นหลักฐานยืนยันเพิ่มเติมว่าเธอรู้คำอธิบายนั้นตั้งแต่ตอนที่เธอลงนามในธุรกรรมการจ่ายเงิน เพราะเธอสามารถสร้างลายเซ็นที่ถูกต้องได้สำหรับ public key ที่ถูกปรับแต่ง ได้ก็ต่อเมื่อเธอรู้ว่าค่า tweak คืออะไรเท่านั้น
หากอลิซและบ๊อบไม่ได้ตัดสินใจเปิดเผยคำอธิบายที่พวกเขาใช้ต่อสาธารณะการชำระเงินระหว่างทั้งสองก็จะดูไม่ต่างจากการชำระเงินทั่วไปอื่น ๆ และจะไม่ก่อให้เกิดการสูญเสียความเป็นส่วนตัวแต่อย่างใด
เนื่องจาก P2C มีความเป็นส่วนตัวโดยปริยาย เราจึงไม่สามารถทราบได้ว่ามันถูกใช้งานตามวัตถุประสงค์ดั้งเดิมบ่อยเพียงใด ในทางทฤษฏีแล้ว การชำระเงินทุกครั้งอาจใช้วิธีนี้ก็ได้ แม้ว่าเราจะมองว่าสถานการณ์เช่นนั้นไม่น่าเกิดขึ้นก็ตาม แต่ในปัจจุบันนั้น P2C ถูกใช้อย่างแพร่หลายในรูปแบบที่แตกต่างกันออกไปเล็กน้อย ซึ่งเราจะได้เห็นกันใน taproot ที่จะอธิบายต่อภายหลัง
Scriptless Multisignatures and Threshold Signatures
ในการทำ Scripted Multisignatures คือการพิจรณาสคริปต์ที่กำหนดให้ต้องมีลายเซ็นจากหลายฝ่าย อย่างไรก็ตามยังมีอีกหนึ่งวิธีในการบังคับให้ต้องใช้กุญแจหลายดอกร่วมกัน ซึ่งก็น่าจะทำให้สับสนได้ไม่น้อย เพราะมันก็ถูกเรียกว่า multisignature เช่นกัน แต่เพื่อแยกความแตกต่างระหว่างสองแนวทางนี้ ในส่วนนี้เราจะเรียกเวอร์ชันที่ใช้ opcode แบบ OP_CHECKSIG ว่า script multisignatures และอีกเวอร์ชันที่จะพูดถึงต่อไปว่า scriptless multisignatures
Scriptless multisignature ทำงานโดยให้ผู้เข้าร่วมแต่ละคนสร้างความลับของตนเองขึ้นมาคล้ายกับวิธีสร้าง private key โดยเราจะเรียกความลับนี้ว่า partial private key โดยแม้ว่ามันจะมีความยาวเท่ากับ private key แบบปกติทุกประการจาก partial private key อันนี้ ผู้เข้าร่วมแต่ละคนจะอนุมาน partial public key โดยใช้อัลกอริทึมเดียวกันกับที่ใช้สร้าง public key ทั่วไป ตามที่อธิบายไว้ในหัวข้อ public key derivation จากนั้นผู้เข้าร่วมทุกคนจะแชร์ partial public key ของตนให้กับผู้เข้าร่วมอื่น ๆ และนำกุญแจทั้งหมดมารวมกันเพื่อสร้าง scriptless multisignature public key เพียงดอกเดียว
กุญแจสาธรณะที่ถูกรวมกันนี้จะมีลักษณะไม่ต่างจาก public key ทั่วไป บุคคลที่สามไม่สามารถแยกแยะได้ว่า public key นี้เกิดจากผู้ใช้หลายคนร่วมกัน หรือเป็น public key ธรรมดาที่สร้างขึ้นโดยผู้ใช้เพียงคนเดียว
สำหรับการใช้จ่ายบิตคอยน์ที่ถูกป้องกันด้วย scriptless multisignature public key ผู้เข้าร่วมแต่ละคนจะสร้างลายเซ็นย่อย (partial signature) ของตนเอง จากนั้นลายเซ็นย่อยเหล่านี้จะถูกรวมเข้าด้วยกันเพื่อสร้างเป็นลายเซ็นที่สมบูรณ์ (บางที่เรียกว่า Full signature) ซึ่งก็มีวิธีที่เป็นที่รู้จักกันหลายรูปแบบสำหรับการสร้างและรวมลายเซ็น ซึ่งเราจะกลับมาลงลึกในประเด็นนี้กันอีกครั้งในหัวข้อ Signature (บทที่ 8) และเช่นเดียวกันกับกรณีของ public key สำหรับ scriptless multisignatures ลายเซ็นที่ได้จากกระบวนการนี้จะมีหน้าตาเหมือนกับลายเซ็นทั่วไปทุกประการ บุคคลที่สามไม่สามารถตรวจสอบได้ว่าลายเซ็นนั้นถูกสร้างขึ้นโดยคนเพียงคนเดียวหรือเกิดจากความร่วมมือของผู้คนจำนวนมาก แม้ว่าจะเป็นการร่วมมือกันระดับล้านคนก็มองไม่ออก
Scriptless multisignatures มีทั้งขนาดเล็กกว่าและมีความเป็นส่วนตัวสูงกว่า scripted multisignatures อย่างชัดเจน สำหรับ scripted multisignatures จำนวนไบต์ที่ต้องบันทึกลงในธุรกรรมจะเพิ่มขึ้นตามจำนวนกุญแจและลายเซ็นที่เกี่ยวข้อง ยิ่งมีผู้ลงนามมาก ข้อมูลในธุรกรรมก็ยิ่งใหญ่ตามไปด้วย แต่ในทางตรงกันข้ามกัน สำหรับ scriptless signatures ขนาดของข้อมูลจะคงที่ ไม่ว่าจะมีผู้เข้าร่วมกี่คนก็ตาม แม้จะมีส่วนร่วมโดยคนเป็นล้านคนก็ตาม แต่ละคนสร้าง partial key และ partial signature ของตนเอง ธุรกรรมที่ได้จะมีขนาดข้อมูลเท่ากันทุกประการกับกรณีที่มีผู้ใช้เพียงคนเดียวใช้กุญแจดอกเดียวและลายเซ็นเพียงอันเดียว
ในแง่ของความเป็นส่วนตัวก็เป็นไปในทิศทางเดียวกัน เนื่องจาก scripted multisignatures ต้องเพิ่มข้อมูลทุกครั้งที่มีการเพิ่มกุญแจหรือลายเซ็น ธุรกรรมจึงเปิดเผยโดยนัยว่ามีการใช้กุญแจและลายเซ็นจำนวนเท่าใด ซึ่งอาจทำให้สามารถคาดเดาได้ว่าธุรกรรมนั้นจะสร้างขึ้นโดยกลุ่มคนใด แต่สำหรับ scriptless multisignatures ธุรกรรมทุกอันจะมีลักษณะเหมือนกันหมด และยังเหมือนกับธุรกรรมแบบลายเซ็นเดียวด้วย ทำให้ไม่มีข้อมูลรั่วไหลลออกมาทำให้ลดทอนความเป็นส่วนตัวของผู้ใช้งาน
ส่วนสำหรับข้อเสียของ scriptless multisignatures มีอยู่สองประการ อย่างแรกคืออัลกอริทึมที่ปลอดภัยทั้งหมดซึ่งเป็นที่รู้จักกันในปัจจุบันสำหรับการสร้าง scriptless multisignatures บนบิตคอยน์ จำเป็นต้องมีรอบของการโต้ตอบกันหรือไม่ก็ต้องมีการจัดการสถานะที่รอบคอบกว่าเมื่อเทียบกับ scripted signatures สิ่งนี้กลายเป็นความท้าทายในกรณีที่ลายเซ็นนั้นถูกสร้างโดยอุปกรณ์ลงนามแบบฮาร์ดแวร์ซึ่งแทบไม่มีการเช็คสถานะ และกุญแจกระจายอยู่ในสถานที่ทางกายภาพที่แตกต่างกัน ตัวอย่างเช่น หากคุณเก็บอุปกรณ์ลงนามแบบฮาร์ดแวร์ไว้ในตู้เซฟของธนาคาร สำหรับ scripted multisignature คุณอาจจะต้องไปที่ตู้เซฟนั้นเพียงครั้งเดียว แต่สำหรับ scriptless แล้วคุณอาจต้องไปสองถึงสามครั้งเพื่อทำให้กระบวนการลงนามเสร็จสมบูรณ์
ส่วนข้อเสียอีกอย่างหนึ่งคือการลงนามแบบ threshold signing ไม่เปิดเผยว่าใครเป็นผู้ลงนามจริงใน scripted threshold signing ตัวอย่างเช่น อลิซ บ๊อบ และคอลไลร์ ตกลงกันว่าเพียงมีลายเซ็นจากใครก็ได้สองในสามคน ก็เพียงพอที่จะใช้จ่ายเงินก้อนนี้ได้ หากอลิซและบ๊อบเป็นผู้ลงนามจะต้องใส่ลายเซ็นของทั้งสองคนลงในบล๊อกเชนซึ่งทำให้ใครก็ตามที่รู้จักกุญแจของพวกเขาก็สามารถพิสูจน์ได้ว่าอลิซและบ๊อบเป็นผู้ลงนามและคอไลร์ไม่ใช่ผู้ลงนาม แต่ใน scriptless threshold signing ลายเซ็นที่เกิดจากอลิซและบ๊อบจะแยกไม่ออกจากลายเซ็นที่เกิดขึ้นโดยอลิซกับคอไลร์หรือจากบ๊อบกับคอไลร์ ซึ่งมันเป็นข้อดีด้านความเป็นส่วนตัว เนื่องจากมันไม่เปิดเผยว่าใครร่วมลงนามบ้าง แต่อย่างไรก็ตามมันก็มีข้อเสียตามมา นั้นก็คือแม้ว่าคอไลร์จะอ้างว่าเธอไม่ได้ลงนาม แต่ก็ไม่มีอะไรเลยที่สามารถพิสูจน์ได้ว่าเธอไม่ได้ลงนามจริง ๆ ซึ่งอาจเป็นปัญหาในแง่ความรับผิดชอบและการตรวจสอบย้อนหลัง
สำหรับผู้ใช้ที่ต้องมีการย้ายเงินเข้าออกบ่อย ๆ ข้อดีด้านขนาดธุรกรรมที่เล็กลง และความเป็นส่วนตัวที่เพิ่มขึ้นของ multisignatures มีประโยชน์มากกว่าความยุ่งยากที่อาจเกิดขึ้นเป็นครั้งคราวในการสร้างลายเซ็นและการตรวจสอบย้อนหลังของลายเซ็นเหล่านั้น
Taproot
เหตุผลอย่างหนึ่งที่ผู้คนเลือกใช้บิตคอยน์คือสามารถสร้างสัญญาที่ให้ผลลัพธ์ได้อย่างคาดการณ์ได้สูงมาก สัญญาทางกฎหมายที่บังคับใช้โดยศาลขึ้นอยู่กับการตัดสินใจของผู้พิพากษาและคณะลูกขุน ในทางกลับกัน สัญญาของบิตคอยน์มักจะต้องการการกระทำจากผู้เข้าร่วมแต่จะถูกบังคับใช้โดยโหนดแบบเต็มนับพันที่รันโค้ดเหมือนกัน เมื่อได้รับสัญญาเดียวกันและข้อมูลชุดเดียวกัน โหนดแบบเต็มทุกตัวจะให้ผลลัพธ์เหมือนกันเสมอ การเบี่ยงเบนใด ๆ หมายความว่าบิตคอยน์ถูกทำลายไปแล้ว ผู้พิพากษาและคณะลูกขุนสามารถยืดหยุ่นได้มากกว่าโปรแกรม แต่เมื่อไม่ต้องการหรือไม่จำเป็นต้องมีความยืดหยุ่นนั้น ความสามารถในการคาดการณ์ของสัญญาบิตคอยน์ถือเป็นคุณสมบัติสำคัญ
หากผู้เข้าร่วมทั้งหมดเห็นว่าผลลัพธ์ของสัญญากลายเป็นเรื่องที่คาดการณ์ได้ทั้งหมด จริง ๆ แล้วก็ไม่มีความจำเป็นที่พวกเขาจะต้องใช้สัญญาต่อไป พวกเขาอาจทำในสิ่งที่สัญญาบังคับให้ทำแล้วยุติสัญญา ในสังคม นี่คือวิธีที่สัญญาส่วนใหญ่ยุติ: หากผู้ที่เกี่ยวข้องพอใจ พวกเขาจะไม่ยื่นสัญญาต่อศาล ในบิตคอยน์ หมายความว่าสัญญาที่ย่อมต้องใช้พื้นที่บล็อกจำนวนมากในการยุติ ควรมีข้อคลอสหนึ่งที่อนุญาตให้ยุติโดยความพึงพอใจร่วมกันแทน
ใน MAST และ scriptless multisignatures การออกแบบสัญญาแบบยินยอมร่วมกันเป็นเรื่องง่าย เพราะเราเพียงทำให้หนึ่งในใบไม้ชั้นบนสุดของต้นสคริปต์เป็น scriptless multisignature ระหว่างผู้เข้าร่วมทั้งหมด ซึ่งเราจะแสดงให้เห็นในรูปภาพที่ได้เห็นข้างบนเราสามารถทำให้มันมีประสิทธิภาพยิ่งขึ้นโดยเปลี่ยนจาก scripted multisignature เป็น scriptless multisignature
แนวทางนี้เองถือว่ามีประสิทธิภาพและความเป็นส่วนตัวพอสมควร หากมีการใช้สัญญาการยินยอมร่วมกัน เราจำเป็นต้องเปิดเผยเพียง merkle branch เดียว และสิ่งที่ถูกเปิดเผยก็มีแค่ว่าลายเซ็นใดที่ถูกใช้เท่านั้น ซึ่งอาจเป็นลายเซ็นจากคนเพียงคนเดียว หรือจากผู้เข้าร่วมหลายพันคนก็ไม่ได้มีใครแยกแยะได้
อย่างไรก็ตามในปี 2018 นักพัฒนาได้ตระหนักได้ว่าเราสามารถทำให้ดีกว่านี้ได้อีก หากนำแนวคิด pay to contract มาใช้ร่วมด้วย
ในคำอธิบายก่อนหน้านี้ของ Pay to contract ในหัวข้อ Pay to Contract (P2C) เราได้ tweak public key เพื่อผูกมัดกับข้อความของข้อตกลงระหว่างอลิซและบ๊อบ แทนที่จะเป็นเช่นนั้น เราสามารถผูกมัดกับโค้ดโปรแกรมของสัญญาได้โดยการผูกมัดกับ root ของ MAST tweak public key นั้นเป็น public key ปกติ ซึ่งหมายความว่ามันอาจต้องการลายเซ็นจากบุคคลเดียว หรืออาจเป็นจากหลายคน (หรืออาจถูกสร้างในลักษณะพิเศษให้เป็นไปไม่ได้ที่จะสร้างลายเซ็นสำหรับมัน) นั่นหมายความว่าเราสามารถทำให้สัญญาสำเร็จได้ด้วยลายเซ็นเดียวจากทุกฝ่ายที่เกี่ยวข้อง หรือโดยการเปิด branch ของ MAST ที่เราต้องการใช้ commitment tree ที่ประกอบด้วย public key และ MAST ดังกล่าวดังที่แสดงไว้ในภาพข้างล่าง

สิ่งนี้เองที่ทำให้สัญญาแบบยินยอมร่วมกันโดยใช้ multisignature มีประสิธิภาพสูงและมีความเป็นส่วนตัวมากยิ่งขึ้น เพราะธุรกรรมที่สร้างโดยใช้โดยคนเพียงคนเดียวซึ่งต้องการให้ยุติโดยลายเซ็นเดียว (หรือ multisignature ที่สร้างโดยหลายกระเป๋าที่เขาควบคุม) จะดูเหมือนกันกับการใช้จ่ายของสัญญาแบบยินยอมร่วมกัน ไม่มีความแตกต่างกันบนบล๊อกเชน ในกรณีนี้ระหว่างการใช้จ่ายโดยกลุ่มผู้ใช้เป็นล้านคนในสัญญาที่ซับซ้อน หรือจ่ายโดยคนเพียงคนเดียว
เมื่อการใช้จ่ายสามารถทำได้โดยใช้เพียงกุญแจ เช่นกรณีลายเซ็นเดี่ยวหรือ scriptless multisig จะเรียกการใช้จ่ายแบบนี้ว่า keypath spending เมื่อใช้ต้นไม้ของสคริปต์ จะเรียกการใช้จ่ายแบบนั้นว่า scriptpath spending สำหรับการใช้จ่ายแบบ keypath ข้อมูลที่จะถูกใส่บนบล๊อกเชนคือ public key (ที่อยู่ใน witness program) และลายเซ็น (ที่อยู่บน witness stack)
สำหรับการใช้จ่ายแบบ scriptpath spending ข้อมูลบนบล๊อกเชนจะรวมถึง public key ด้วยโดย public key นี้จะถูกใส่ไว้ใน witness program และในบริบทนี้เรียกว่า taproot output key โครงสร้างของ witness จะประกอบไปด้วยข้อมูลต่อไปนี้:
- หมายเลขเวอร์ชัน (Version number)
- กุญแจพื้นฐาน (Underlying key) คือกุญแจที่มีอยู่ก่อนที่จะถูกปรับแต่ง (tweak) ด้วย merkle root เพื่อสร้าง taproot output key กุญแจพื้นฐานนี้เรียกว่า taproot internal key
- สคริปต์ที่ถูกนำมาประมวลผลนี้เรียกว่า leaf script
- ค่าแฮชขนาด 32 ไบต์จำนวนหนึ่งค่าต่อหนึ่งจุดที่เชื่อมใน merkle tree ตามเส้นทางที่ leaf เชื่อมเข้ากับ Merkle root
- ข้อมูลใด ๆ ที่จำเป็นต่อการทำให้สคริปต์เป็นจริง (เช่น ลายเซ็น หรือ hash preimage)
ข้อเสียสำคัญเพียงอย่างเดียวของ taproot ที่มีการอธิบายไว้คือ กรณีของสัญญาที่ผู้เข้าร่วมต้องการใช้ MAST แต่ไม่ต้องการมีเงื่อนไขการยุติความพึงพอใจร่วมกัน สัญญาเหล่านั้นจำเป็นต้องบันทึก taproot internal key ได้บนบล๊อกเชน ซึ่งเพิ่ม overhead ประมาณ 33 ไบต์ แต่อย่างไรก็ตาม เนื่องจากคาดว่าสัญญาส่วนใหญ่จะได้รับประโยชน์จากเงื่อนไขหารยุติโดยใช้ความพึงพอใจร่วมกัน หรืออย่างน้อยก็มีเงื่อนไข multisignature อื่นที่ใช้ public key ที่อยู่ในระดับบนสุด และผู้ใช้ทุกคนยังได้รับประโยชน์จากการเพิ่ม anonymity set ที่ทำให้ output มีลักษณะคล้ายกันมากขึ้น overhead ที่เกิดขึ้นในกรณีที่พบไม่บ่อยนี้จึงไม่ถูกมองว่าสำคัญโดยผู้ใช้ส่วนใหญ่ที่มีส่วนร่วมในการเปิดใช้งาน taproot
การรองรับ taproot ถูกเพิ่มเข้ามาในบิตคอยน์ผ่านการทำ soft fork ซึ่งถูกเปิดใช้งานในเดือนพฤศจิกายนปี 2021
Tapscript
Taproot ทำให้สามารถใช้ MAST ได้ แต่ในภาษาสคริปต์ของบิตคอยน์ในเวอร์ชันที่แตกต่างจากเดิมเล็กน้อย โดยเวอร์ชันใหม่นี้เรียกว่า Tapscript โดยความแตกต่างสำคัญประกอบด้วย:
การเปลี่ยนแปลงของ scripted multisignature:
opcode เดิมอย่าง OP_CHECKMULTISIG และ OP_CHECKMULTISIGVERIFY ถูกนำออกไป เนื่องจาก opcode เหล่านี้ไม่สามารถทำงานร่วมกันได้ดีนักกับการเปลี่ยนแปลงอีกอย่างใน soft fork ของ taproot นั่นคือความสามารถในการใช้ลายเซ็นแบบ schnorr ร่วมกับการตรวจสอบแบบ batch แทนที่ด้วย opcode ใหม่คือ OP_CHECKSIGADD
เมื่อ OP_CHECKSIGADD ตรวจสอบลายเซ็นสำเร็จแล้ว จะเพิ่มค่าตัวนับขึ้นหนึ่ง ทำให้สามารถนับจำนวนลายเซ็นที่ผ่านการตรวจสอบได้อย่างสะดวก และนำไปเปรียบเทียบกับจำนวนลายเซ็นที่ต้องการ เพื่อสร้างพฤติกรรมเดียวกันกับที่ OP_CHECKMULTISIG เคยทำได้
การเปลี่ยนแปลงของลายเซ็นทั้งหมด:
การดำเนินการที่เกี่ยวข้องกับลายเซ็นทั้งหมดใน tapscript ใช้อัลกอริทึมลายเซ็นแบบ Schorr จามที่นิยามไว้ใน BIP340 เราจะอธิบายรายละเอียดของลายเซ็นแบบ schorr เพิ่มเติมในบทถัดไป นอกจากนี้ การดำเนินการตรวจสอบใด ๆ ที่คาดว่าจะไม่ผ่านการตรวจสอบ จะต้องได้รับค่า OP_FALSE (หรือที่เรียกว่า OP_0) แทนการส่งลายเซ็นจริงเข้าไป หากส่งค่าอื่นใดไปยังการตรวจสอบลายเซ็นที่ล้มเหลว สคริปต์ทั้งหมดจะล้มเหลวทันที กลไกนี้ยังช่วยสนับสนุนการตรวจสอบลายเซ็น Schnorr แบบ batch validation ได้อีกด้วย
opcode กลุ่ม OP_SUCCESSx:
opcode ที่ไม่สามารถใช้งานได้ใน script เวอร์ชันก่อนหน้าได้ถูกนิยามใหม่ให้เมื่อถูกเรียกใช้แล้วทำให้สคริปต์ทั้งหมดสำเร็จทันที (script succeeds) โดยกลไกนี้เปิดทางให้ soft fork ในอนาคตสามารถนิยามเงื่อนไขเพิ่มเติมได้ว่า opcode เหล่านี้จะไม่ทำให้สคริปต์สำเร็จในบางกรณี ซึ่งถือเป็นการ “จำกัดความสามารถลง” และจึงสามารถทำได้ผ่าน soft fork ในทางกลับกัน การเปลี่ยน opcode ที่เดิม ไม่ทำให้สคริปต์สำเร็จ ให้กลายเป็น opcode ที่ทำให้สคริปต์สำเร็จนั้น จะทำได้ผ่าน hard fork เท่านั้น ซึ่งเป็นแนวทางการอัปเกรดที่ยากและมีความเสี่ยงมากกว่า
แม้ว่าในบทนี้เราจะได้พิจรณาเรื่องการให้สิทธิ์และการยืนยันตัวตนอย่างละเอียดแล้ว แต่เรายังข้ามที่ส่วนสำคัญมากส่วนหนึ่งของวิธีที่บิตคอยน์ใช้ยืนยันผู้ใช้จ่ายไป นั่นคือลายเซ็น ซึ่งเราจะไปศึกษาในลำดับถัดไปในบทที่ 8 