อธิบายการทำงานของ PyLibra 🐍 + แกะ 🐑 Libra Core

ก่อนอื่นเลยคงต้องขอขอบคุณ Sorawit Suriyakarn กับ Python library ที่สามารถทำให้เราใช้ภาษา python ในการสร้างกระเป๋าตังเก็บเหรียญ Libra ในเครื่องคอมพิวเตอร์ของตัวเองได้ไม่ยาก

วันนี้เราจะมาเล่าการสร้าง PyLibra กันว่าเราทำยังไงกันบ้าง ซึ่งในนี้จะมีการเข้าไปอ่าน code บางส่วนของ Libra Core ด้วยนอกจาก code ของ PyLibra เองแล้ว เพราะฉนั้นอาจจะยาวนิดนึง (มีภาพประกอบเยอะ)
ส่วนการใช้งาน PyLibra นั้นสามารถเข้าไปอ่านวิธีการใช้งาน(การสร้างกระเป๋าตัง,ส่งเงิน, ฯลฯ) ได้ที่ https://github.com/bandprotocol/pylibra

ในปัจจุบันการที่เราจะใช้งาน Libra นั้น เราต้องสั่งการผ่าน Libra CLI เช่นในการสร้างกระเป๋าตัง, ส่งเงิน แต่ถ้าหากเราต้องการที่จะ สร้าง transaction, เซ็น transaction, ส่ง transaction เรายังไม่สามารถทำได้ผ่านทาง Libra CLI ได้เพราะทางผู้พัฒนายังไม่ได้ทำออกมาให้เราใช้ ด้วยเหตุนี้ทีมเราเลยไปศึกษา Libra Core แล้วเขียนออกมาเป็น python lib เพื่อให้เป็นอีกทางเลือกนึงของ community ที่จะได้นำไปศึกษาและพัฒนาต่อได้ง่ายขึ้น

หมายเหตุในการอธิบายจะมีการอ้างอิงถึง code ภาษา rust ที่ใช้ใน https://github.com/libra/libra ปัจจุบันด้วย ซึ่ง code อาจจะมีการอัพเดทจากทาง Libra เองในอนาคต

Outline

  • การคุยกับ Node ของ Libra Testnet 💬
  • Libra Account 🏦
  • Libra Transaction 💸

เพื่อความเข้าใจที่มากขึ้นเราจะไล่ดู code กันไปขณะ go through

OK เริ่มกันเลย 🍾

การคุยกับ Node ของ Libra Testnet 💬

Libra เลือกใช้มาตราฐาน Protocol Buffers ในการคุยกันใน network

ไฟล์นามสกุล .proto ทั้งหมดใน repository ของ libra

เราก็เลือกมาเฉพาะ .proto ที่เราใช้เท่านั้น จากนั้นสั่ง generate_protobuf.sh เพื่อคอมไพล์ .proto เหล่านั้นเป็น .py

เราสร้าง generate_protobuf.sh เพื่อแปลง .proto ที่เราเอามาจาก Libra Core เป็น .py เพื่อให้ PyLibra ของเรารู้จัก Protobuf message ที่จำเป็น
ตัวอย่าง .py ที่ถูกสร้างขึ้นจาก .proto เมื่อ run generate_protobuf.sh

Libra Account 🏦

Account ของ Libra ประกอบไปด้วย 3 ส่วน

  1. private key เป็นเลข random ขนาด 256 bits
  2. public key เป็นเลขขนาด 256 bits เช่นกัน ที่ถูกสร้างมาจาก private key อีกทีนึงโดยในที่นี้ Libra เลือกใช้วิธีตามมาตราฐาน Curve25519
    ซึ่งใน PyLibra เราใช้ PyNaCl เป็น lib สำหรับสร้าง public key
  3. Address แบบที่ชาวคริปโตคุ้นเคยกัน ซึ่งก็คือ sha3 ของ public key
code ของ class Account ใน PyLibra

ซึ่ง Account หลายๆ Accounts ของเรานั้นจะถูกเก็บอยู่ใน wallet ซึ่ง wallet นี้จะเก็บ mnemonic ไว้ 1 ก้อน

mnemonic ก้อนนั้นก็เปรียบเสมือนบรรพบุรุษ private key ที่เราสามารถเรียกฟังก์ชัน get_account(เลขจำนวน 1,2,3,…) แล้ว wallet จะ return account ที่ 1,2,3,… ตามที่เราเรียกพร้อมกับลูกๆ private key, public key, address

จุดต่างของ wallet ที่สร้างจาก PyLibra กับ Libra Core คือ mnemonic ของ Core นั้นมี 24 คำแบบ fixed ไว้เลย ณ ตอนนี้ …
ส่วนของ PyLibra นั้นเราสามารถกำหนดความปลอดภัยได้ (จำนวน bits เยอะก็ปลอดภัยขึ้น แต่ก็ต้องจำคำมากขึ้น) ซึ่งตอนนี้ PyLibra นั้น support 128 bits, 160 bits, 192 bits, 224 bits, 256 bits (12คำ, 15คำ, 18คำ, 21คำ, 24คำ ตามลำดับ)
โดย Default ของ PyLibra คือ 128 bits 12 คำ

code ของ class LibraWallet ใน PyLibra

หลายคนอาจจะสงสัยว่า เอ๊ะ แล้วพวก balance, sequence number, … พวกนี้เก็บไว้ที่ไหนกัน
คำตอบคือ เราไม่เก็บ แต่ว่าเราจะไปถาม Libra Testnet ทุกครั้งที่ผู้ใช้งานเรียกฟังก์ชัน get_account_states

ฟังก์ชัน get_account_states ใน class LibraClient

ฟังก์ชัน get_account_states จะ return ก้อน AccountState กลับมาเป็น object ซึ่ง object AccountState นี้จะประกอบไปด้วย

  • authentication key
  • balance
  • received events count
  • sent events count
  • sequence number
code ของ class AccountState ใน PyLibra

Libra Transaction 💸

เราเริ่มจากการไปดู Libra Core ของ Libra ก่อนว่าส่ง transaction กันยังไง ซึ่ง code ส่วนใหญ่ต่อจากนี้จะเป็นภาษา rust

ใน repository ของ libra เราก็เข้าไปที่

client/src/client_proxy.rs

เราจะเจอฟังก์ชัน transfer_coins

ซึ่งฟังก์ชัน transfer_coins จะไปเรียกฟังก์ชัน transfer_coins_int

สังเกตุว่าฟังก์ชัน transfer_coins_int ก็จะไปเรียกฟังก์ชัน vm_genesis::encode_transfer_program(address ผู้รับเงิน, จำนวนเงินที่ส่ง)
ซึ่งฟังก์ชันนี้จะ return code ของ Program ส่งเงินที่ compiled แล้วออกมา โดย Program นี้จะเป็นส่วนหนึ่งของ Transaction เป็นตัวกำหนดว่า Transaction นี้จะทำอะไร
จากนั้นก็จะไปเรียกฟังก์ชัน create_submit_transaction_req อีกที แล้วใส่ Program ส่งเงิน ไปเป็นหนึ่งในพารามิเตอร์
แล้วฟังก์ชัน create_submit_transaction_req จะ return object SubmitTransactionRequest

ซึ่งถ้าหากเราเข้าไปดูในไฟล์ admission_control.proto ที่มี สเปคของก้อน SubmitTransactionRequest อยู่จะพบว่าประกอบไปด้วย signed_txn เราก็อาจพอเดาได้ว่ามันน่าจะหมายถึง transaction ที่ถูก signed แล้ว

message format ของ SubmitTransactionRequest

เราจึงต้องเรียกฟังก์ชัน create_signed_txn ก่อน
👉 👉 👉 เพื่อสร้างก้อน transaction ที่ถูก “เซ็น” แล้วขึ้นมา
แล้วค่อยสั่ง set_signed_txn

เพราะฉนั้นตอนนี้เราก็ไปดู create_signed_txn ต่อ
ซึ่งฟังก์ชัน create_signed_txn นั้นอยู่ในไฟล์

types/src/transaction_helpers.rs

ในฟังก์ชัน create_signed_txn นี้มีจุดน่าสนใจสองจุดคือ

  1. สร้าง ⚗️ transaction ยังไง 🤔
    - types/src/transaction_helpers.rs > RawTransaction::new(พารามิเตอร์ 6 ตัว แบบที่เห็นในภาพ)
  2. เซ็น ✍️ transaction ยังไง 🤔
    - let hash = RawTransactionBytes(&bytes).hash();
    - ใช้ RawTransactionHasher ในการ Hash

การสร้าง transaction ⚗️

หลังจากที่เราเจอฟังก์ชัน create_signed_txn ในไฟล์ types/src/transaction_helpers.rs เราพบว่ามีการฟังก์ชันนี้มีการสร้างก้อน
👉 RawTransaction ซึ่งเป็น object ที่มาจาก .proto

RawTransaction คือก้อน Transaction ที่ยังไม่ได้ ถูกเซ็น/signed

message format ของ RawTransaction

ภายใน object RawTransaction ของ Libra นั้นก็มีตัวแปรที่ blockchain dev คุ้นเคยกันดี เช่น
- sender_account (address คนส่ง)
- sequence_number (nonce ของคนส่ง ไว้ป้องกัน replay attack)
- oneof (อย่างใดอย่างหนึ่ง) ระหว่าง object Program กับ WriteSet
- max_gas_amount คล้ายๆ gas limit ใน Ethereum
- gas_unit_price ก็คล้ายๆ gas price ใน Ethereum
- expiration_time คิดว่าเป็นอายุของ transaction ว่าถ้าผ่านไปเวลาเท่านี้แล้วยังไม่ถูกขุดก็ให้เป็นโมฆะไป

ทีนี้เรามาดูที่ object Program ใน Libra เพราะว่าเราต้องส่งโปรแกรมไปกับ Transaction ด้วย เพื่อเป็นตัวกำหนดว่า Transaction นี้ให้ทำอะไร
แปลว่า ต้องมี code ของโปรแกรมแนบไปด้วยเวลาส่ง Transaction

แม้กระทั่งโอนเงิน 🙉

message format ของ Program

จาก Protobuf message เราพบว่าโปรแกรมประกอบไปด้วย 3 อย่าง
1. bytes code ซึ่งคือโปรแกรมภาษา move ที่ถูก compiled แล้ว
2. arguments หรือ input ของ โปรแกรม เช่น โปรแกรมส่งเงินมี input เป็น address ของผู้รับ และ จำนวนเงินที่ส่ง
3. modules คิดว่าเป็น code ของ module เวลาเรา deploy module บน Libra คล้ายๆเวลา deploy smart contract บน Ethereum (ยังไม่พบหลักฐานที่ชัดเจน)

ตัวอย่าง Transaction ที่อยู่ใน https://librabrowser.io สังเกตุว่ามี code Program ของการส่งเงินด้วย

ใน Core ของ Libra จะมี Program พื้นฐานที่ถูก compiled แล้วอยู่ เช่น Program ส่งเงิน, Program เสก/mint เงิน ฯลฯ
โดยเราสามารถเข้าไปดู Program พื้นฐานที่มีใน Libra ได้จาก

language/stdlib/src/transaction_scripts.rs
transaction_scripts.rs

ในไฟล์ transaction_scripts.rs มี hardcode ไว้หมดแล้ว โดยจะ link ไปยังไฟล์โปรแกรมภาษา move

ตัวอย่างโปรแกรมภาษา move ในที่นี้คือโปรแกรมส่งเงิน (peer_to_peer_transfer.mvir)

เมื่อเราเปิด Libra Core ขึ้นมาไฟล์ .mvir เหล่านี้ก็จะถูก compiled เป็น byte code เก็บไว้ในตัวแปรที่เป็น static

ตัวอย่าง byte code ของ Program ส่งเงิน“4c49425241564d0a010007014a00000004000000034e000000060000000c54000000050000000d5900000004000000055d0000002900000004860000002000000007a60000000d00000000000001000200010300020002040203020402063c53454c463e0c4c696272614163636f756e74046d61696e0f7061795f66726f6d5f73656e64657200000000000000000000000000000000000000000000000000000000000000000001020004000c000c01110102”
เปลี่ยน byte code ของ Program ในรูปเลขฐาน 16 ให้กลายเป็น bytes ด้วย python
byte code ที่เราเห็นใน https://librabrowser.io

จากองค์ความรู้ตรงนี้ที่เราได้จาก Libra Core เราก็นำมา implement ใน PyLibra ในแบบที่เป็น simplified version

ไฟล์ transfer.py ใน PyLibra ที่เรา hardcode โปรแกรมส่งเงินไว้ในตัวแปร TRANSFER_OPCODES

หลังจากที่เราได้ bytes ของ Program มา เราก็จะได้ก้อน object RawTransaction ที่พร้อมเอาไปให้เซ็น 🎉

การเซ็น ✍️ transaction

โอเค เราจะมาเซ็น RawTransaction กัน เราจะเริ่มจากการกลับไปดูฟังก์ชัน create_signed_txn ที่อยู่ในไฟล์

types/src/transaction_helpers.rs

ซึ่งเราพบว่า มีการเรียก signer.sign_txn(raw_txn) โดย signer นั้นคือ object TransactionSigner ซึ่งภายในก็มีฟังก์ชัน sign_txn

ส่วน raw_txn ในที่นี้ก็คือ object RawTransaction

TransactionSigner

object RawTransaction ถูกทำให้กลายเป็น bytes ด้วยการสั่ง
raw_txn.clone().into_proto().write_to_bytes()

จากนั้น bytes ของ RawTransaction ที่ได้มาจะถูกนำไป hash โดยใช้ฟังก์ชัน hash ของ RawTransactionBytes ในไฟล์

types/src/transaction.rs
RawTransactionBytes และฟังก์ชัน hash

เราเห็นว่าภายใน RawTransactionBytes มีฟังก์ชัน hash ที่มีตัวแปรชื่อ Hasher คอยทำหน้าที่ให้การ Hash อีกที

เริ่มจากการสร้างตัวแปรชื่อ state ซึ่งเป็นสิ่งที่ได้จากการเรียก Hasher::default()
จากนั้นสั่ง state.write(self.0) ซึ่ง self.0 ในที่นี้ก็คือ bytes ของ RawTransaction นั่นเอง

Hasher นั้นเป็น RawTransactionHasher object
จากการใช้ git grep เราก็พบว่า RawTransactionHasher มีต้นตออยู่ในไฟล์

crypto/legacy_crypto/src/hash.rs
struct ของ DefaultHasher

พอเราเข้ามา เราก็จะเจอกับ DefaultHasher ซึ่งเป็น struct และถูก implement ฟังก์ชัน Default ให้ return DefaultHasher ที่ภายในมี state ที่เป็น ก้อน object sha3_256 (256 ในที่นี้คือจำนวน bits)

หมายเหตุ Keccak::new_sha3_256() นี้มาจาก library ชื่อ tiny_keccak ซึ่งเป็นหนึ่งใน library สำหรับการ hash ในภาษา rust

เราเจอ DefaultHasher แล้ว … ว่าแต่ RawTransactionHasher ของเราหละ
โอเคลองเลื่อนลงมาอีกนิด เราก็จะเจอ macro ตัวนึงที่ชื่อ define_hasher

define_hasher คือ macro ที่รับพารามิเตอร์ 3 ตัว ได้แก่
- hasher_type (ประเภท hasher)
- hasher_name (ชื่อ hasher)
- salt (bytes ที่จะใส่เพิ่มลงไปในการ hash โดย hasher แต่ละตัวจะมี salt ที่แตกต่างกันไป)
เพื่อไปสร้างเป็น hasher ตัวอื่นๆต่อไปตาม code ด้านล่างที่เขียนว่า define_hasher!

RawTransactionHasher คือประเภทหนึ่งของบรรดา hasher

RawTransactionHasher คือ hasher ประเภทหนึ่งที่มี salt เป็น b”RawTransaction”
สำหรับ hasher ประเภทอื่นๆก็จะมี salt ที่แตกต่างกันไป

🌟🌟🌟 หมายเหตุการเขียน b นำหน้าเป็นการบอกว่าเป็น bytes นะจ๊ะ ไม่ได้เป็น string ปกติ 🌟🌟🌟

โอเคที่นี้กลับไปที่ define_hasher macro เราพบว่าเมื่อเราสั่ง new (สร้าง object hasher ขึ้นมา) ฟังก์ชัน new จะไปเรียก DefaultHasher::new_with_salt($salt)
$salt ในที่นี้ (ของ RawTransactionHasher) ก็คือ b”RawTransaction”
แสดงว่าเราต้องไปดูก่อนว่า DefaultHasher::new_with_salt นั้นทำอะไร

ในฟังก์ชัน new_with_salt นั้นนำ salt ของเรา (b”RawTransaction”) ไปต่อท้ายด้วย LIBRA_HASH_SUFFIX

salt.extend_from_slice(LIBRA_HASH_SUFFIX);

เราจึงไปดูต่อว่า LIBRA_HASH_SUFFIX คืออะไรกันแน่

ตัวแปร LIBRA_HASH_SUFFIX ที่แท้คือ bytes ที่ hardcode ไว้ซึ่งมีค่าเป็น b”@@$$LIBRA$$@@”

LIBRA_HASH_SUFFIX นั้นเป็น bytes ที่ hardcode ไว้เป็น b”@@$$LIBRA$$@@”

เพราะฉนั้นการบอกว่า salt.extend_from_slice(LIBRA_HASH_SUFFIX)
ก็คือการนำ bytes ของ LIBRA_HASH_SUFFIX ที่มีค่าเป็น b”@@$$LIBRA$$@@”
มาต่อด้านหลังของ salt ซึ่งในที่นี้คือ b”RawTransaction”
👉 ทำให้ตอนนี้ salt กลายเป็น b”RawTransaction@@$$LIBRA$$@@”

หลังจากได้ salt ที่เฉพาะเจาะจงกับ RawTransactionHasher มาแล้ว

ก็สั่ง state.update(HashValue::from_sha3(&salt[..]).as_ref());

HashValue::from_sha3(&salt[..]) คือการสั่งให้ hash ค่า salt (b”RawTransaction@@$$LIBRA$$@@”)
ซึ่ง hash (sha3_256) ของ b”RawTransaction@@$$LIBRA$$@@” จะได้

b”F\xf1t\xdfl\xa8\xdeZ\xd2\x97E\xf9\x15\x84\xbb\x91>}\xf8\xdd\x16.>\x92\x1a\\\x1d\x867\xc8\x8d\x16”

หรือเขียนในรูปเลขฐาน 16 (hex) ได้ว่า
46f174df6ca8de5ad29745f91584bb913e7df8dd162e3e921a5c1d8637c88d16

การลอง hash b”RawTransaction@@$$LIBRA$$@@” ใน python

ซึ่งฟังก์ชัน DefaultHasher::new_with_salt นั้นก็จะอัพเดท state (state ก็คล้ายๆ shazer ใน code python ตามภาพหนะแหละ) ด้วย hash ของ salt (ซึ่งก็คือ b”F\xf1t\xdfl\xa8\xdeZ\xd2\x97E\xf9\x15\x84\xbb\x91>}\xf8\xdd\x16.>\x92\x1a\\\x1d\x867\xc8\x8d\x16”) แล้วค่อย return object hasher ที่มี state เป็นดังที่กล่าวมา กลับออกไปให้กับฟังก์ชัน hash ที่อยู่ใน RawTransactionBytes ถ้าเรายังจำได้

ภาพซ้ำของ RawTransactionBytes

เมื่อเราสั่ง state.write(self.0) ก็คือการอัพเดท state (อัพเดท state คือการเอา bytes ไปต่อกับ bytes เดิมที่เคยอยู่ใน state หนะแหละ) ด้วย bytes ของ RawTransaction นั่นเอง

นั่นหมายความว่า state ของเราตอนนี้จะกลายเป็น b”F\xf1t\xdfl\xa8\xdeZ\xd2\x97E\xf9\x15\x84\xbb\x91>}\xf8\xdd\x16.>\x92\x1a\\\x1d\x867\xc8\x8d\x16” ต่อกับ b”bytes ของ RawTransaction

bytes ของ RawTransaction ก็คือ RawTransaction ที่ถูก serialized แล้วจึงกลายเป็น bytes

ถ้ายังจำได้ RawTransaction ประกอบไปด้วย sender_account, sequence_number, Program, max_gas_amount, gas_unit_price, expiration_time

จากนั้นพอเราสั่ง state.finish()
ก็คือทำการ hash b”F\xf1t\xdfl\xa8\xdeZ\xd2\x97E\xf9\x15\x84\xbb\x91>}\xf8\xdd\x16.>\x92\x1a\\\x1d\x867\xc8\x8d\x16” ต่อกับ b”bytes ของ RawTransaction
นั้นอีกรอบนึงด้วย sha3

โอเคเราจบกับ RawTransactionBytes ตรงนี้แล้ว
ได้เวลากลับมาที่ TransactionSigner

ภาพซ้ำของ TransactionSigner

ในบรรทัดที่ 57 เราได้ hash มาละ ซึ่ง sha3 ของ ((sha3 ของ b”RawTransaction@@$$LIBRA$$@@”) ต่อกับ bytes ของ RawTransaction)

ในบรรทัดที่ 58 เราก็ทำการ sign_message โดยเราจะ sign ค่า hash ที่เราพึ่งได้มาด้วย private key ของเรา

จากนั้นเราก็ทำการ git grep ไล่หาต้นตอของ sign_message ว่าเค้าใช้มาตราฐานอะไร เราก็พบว่า มันอยู่ในไฟล์

crypto/legacy_crypto/src/signing.rs
ฟังก์ชัน sign message ที่รับ hash ที่เราได้มาจาก RawTransactionBytes และ private key ของเราเพื่อใช้ในการ sign

โอเคเราพบแล้วว่า Libra Core ใช้มาตราฐานของ Curve25519 ในการ sign
พอเราได้จิ๊กซอว์ครบเราก็กลับมา implement เลียนแบบ Libra Core ใน python

ฟังก์ชันส่ง transaction ใน client.py ใน PyLibra

ในฟังก์ชันส่ง transaction ของเราใน PyLibra เราก็จะทำการ

  • init ค่าตัวแปรต่างๆสำหรับ RawTransaction
    ได้แก่ sender_account, sequence_number, Program (มากับตัวแปร transaction), max_gas_amount, gas_unit_price, expiration_time
  • จากนั้น serialize ตัว RawTransaction ให้กลายเป็น bytes
  • สร้าง shazer ที่เป็น sha3 แบบ 256 bits
  • อัพเดท shazer ด้วย 46f174df6ca8de5ad29745f91584bb913e7df8dd162e3e921a5c1d8637c88d16 ซึ่งก็คือ b”F\xf1t\xdfl\xa8\xdeZ\xd2\x97E\xf9\x15\x84\xbb\x91>}\xf8\xdd\x16.>\x92\x1a\\\x1d\x867\xc8\x8d\x16”
    ซึ่งก็คือ sha3 ของ b”RawTransaction@@$$LIBRA$$@@” (หลายร่างจริงๆ …)
  • อัพเดท shazer ด้วย bytes ของ RawTransaction ที่เราพึ่ง serialize มา
  • เราสั่ง shazer ให้ hash ทุกอย่างที่เราเคยอัพเดทให้มันไปด้วย shazer.digest()
  • เอา hash ที่ได้มา sign (เซ็น) ด้วยมาตราฐาน Curve25519 โดยใช้ library PyNaCl
  • เราเอาแค่ 64 bits แรกเพราะว่า PyNaCl จะ return signed message (64 bits แรก) ต่อกับ message ก่อนถูก sign (ในที่นี้คือ hash จาก shazer ของเรา)
  • จับทุกอย่างยัดลงใน SubmitTransactionRequest

ซึ่งเราจะส่งไปบอก Libra Core ด้วย Protobuf message SubmitTransactionRequest

โอเคจบแล้วครับสำหรับการสำรวจ Libra Core เพื่อมานำมาสร้าง PyLibra

ผมหวังว่าข้อมูลเหล่านี้จะเป็นประโยชน์ให้กับนักพัฒนาหลายๆท่านที่สนใจการทำงานภายในของ Libra Core

สิ่งที่นำเสนอนี้เป็นเพียงส่วนหนึ่งใน Libra Core เท่านั้น ยังมีอีกหลายส่วนที่น่าสนใจภายใน Libra Core เช่น ส่วน consensus, ส่วนของการเก็บ state, ส่วนที่เป็น compiler ของภาษา move ฯลฯ

ไว้วันหลังจะพยายามมานำเสนอส่วนอื่นภายใน Libra Core อีกสวัสดีครับ 🙏🏻

--

--