PSBT คืออะไร และแก้ปัญหาอะไร

เนิื้อหาในคลิปมีการใช้ AI ในการสร้างเสียง และเนื้อหาไม่ครบถ้วนเท่าบทความด้านล่าง จัดทำเพื่อใครที่ต้องการทำความเข้าใจแบบพื้นฐานก่อนจะลงลึก

PSBT (Partially Signed Bitcoin Transaction) เป็นฟอร์แมตรูปแบบข้อมูลสำหรับธุรกรรมของบิตคอยน์ที่ออกแบบมาเพื่อให้โปรแกรมหรือกระเป๋าเงินหลายตัวสามารถแลกเปลี่ยนข้อมูลของธุรกรรมและลายเซ็นบางส่วนระหว่างกันได้ PSBT ได้ช่วยแก้ปัญหาการทำธุรกรรมที่ต้องมีหลายคนเซ็น (multi-signature) หรือกรณีที่ใช้กระเป๋าเงินหลายประเภทร่วมกัน โดยไม่ต้องเปิดเผยข้อมูลภายในกันโดยตรง นอกจากนี้ PSBT ยังอนุญาตให้อุปกรณ์ออฟไลน์หรือฮาร์ดแวร์วอลเล็ตลงชื่อได้โดยไม่ต้องเชื่อมต่อกับชุด UTXO ทั้งหมด ทำให้ปลอดภัยต่อการใช้งานบน cool wallet ในทางเทคนิค PSBT มีการกำหนดตาม BIP174 (เวอร์ชัน 0) และมี BIP370 สำหรับ PSBT เวอร์ชัน 2 ที่เพิ่มฟีเจอร์ใหม่ ๆ (เช่นข้อมูล Taproot)

PSBT จะเริ่มจากการเตรียมธุรกรรมเปล่า (unsigned transaction) ที่ระบุ UTXO ที่จะใช้จ่ายและ address ปลายทาง จากนั้นข้อมูลสำคัญของแต่ละ input เช่น สคริปต์และจำนวนเงินของ UTXO นั้น ๆ จะถูกเพิ่มเข้าไปใน PSBT และสามารถส่งต่อไปยังโปรแกรมหรืออุปกรณ์อื่นเพื่อใช้เซ็นลายเซ็นได้ โดยแต่ละรอบจะใส่ลายเซ็นบางส่วนเข้าไปใน PSBT หากเป็น wallet multisig หรือต้องใช้หลายคีย์ในการเซ็น ก็สามารถส่ง PSBT สำเนาให้แต่ละคนลงชื่อทีละคน และสุดท้ายรวม PSBT เหล่านั้นให้ได้ลายเซ็นครบถ้วน จากนั้นจึงแปลง PSBT ที่ลงชื่อครบเป็นธุรกรรมสมบูรณ์พร้อมประกาศสู่เครือข่ายได้

โครงสร้างและประเภทของ Input/Output ใน PSBT

PSBT ถูกจัดเก็บในรูปแบบ Key-Value Maps โดยแบ่งเป็นส่วน global map, input map และ output map

  • Global Map: ประกอบด้วยคีย์ข้อมูลทั่ว ๆ ไป เช่น ธุรกรรมบิทคอยน์ที่ยังไม่เซ็น (PSBT_GLOBAL_UNSIGNED_TX) ในรูปแบบไบนารีของ Transaction (scriptSig และ witness ของทุก input ) และสามารถมี public keys ตาม BIP32 (XPUB) เพื่อถ่ายทอดข้อมูลคีย์ต้นทางได้ (PSBT_GLOBAL_XPUB)

  • Input Map (แต่ละ input): มีข้อมูลเฉพาะของแต่ละ input โดยรองรับหลายประเภท ได้แก่:

    • Non-Witness UTXO (PSBT_IN_NON_WITNESS_UTXO): ถ้า input ใช้ UTXO แบบเดิม (non-segwit) จะเก็บทั้ง transaction เดิมไว้ทั้งหมด

    • Witness UTXO (PSBT_IN_WITNESS_UTXO) เก็บแค่ output เดิม (จำนวนเงินและ scriptPubKey) สำหรับ input แบบ SegWit

    • ลายเซ็นบางส่วน (PSBT_IN_PARTIAL_SIG): เก็บคู่ (Public Key, Signature) ที่เซ็นโดยผู้ร่วมเซ็นแต่ละคน

    • Sighash Type (PSBT_IN_SIGHASH_TYPE): ระบุวิธีการคำนวณแฮชสำหรับการเซ็น input นั้น (เช่น ALL, SINGLE ฯลฯ)

    • Redeem Script (PSBT_IN_REDEEM_SCRIPT), Witness Script (PSBT_IN_WITNESS_SCRIPT): กรณี output เดิมเป็น P2SH หรือ P2WSH จะเก็บสคริปต์เหล่านั้นไว้เพื่อใช้สร้างลายเซ็น

    • BIP32 Derivation (PSBT_IN_BIP32_DERIVATION): ข้อมูลเส้นทางการสืบทอดกุญแจ (master fingerprint + derivation path) ของ public key ใน transaction input เพื่อให้กระเป๋าเงินสามารถสืบหาคีย์ส่วนตัวได้

    • ScriptSig/ScriptWitness (PSBT_IN_FINAL_SCRIPTSIG, PSBT_IN_FINAL_SCRIPTWITNESS): เมื่อเซ็นเสร็จ สมบูรณ์ จะเก็บ scriptSig หรือ witness เต็มสำหรับ input เพื่อให้ตรวจสอบถูกต้องได้

  • Output Map (แต่ละ output): เก็บข้อมูลที่เกี่ยวข้องกับแต่ละ output ได้แก่

    • Redeem Script (PSBT_OUT_REDEEM_SCRIPT) และ Witness Script (PSBT_OUT_WITNESS_SCRIPT): หาก output เป็น P2SH/P2WSH ใด ๆ ก็เก็บสคริปต์ไว้สำหรับอ้างอิง

    • BIP32 Derivation (PSBT_OUT_BIP32_DERIVATION): กรณี output เป็นการจ่ายให้ address ที่เป็น HD wallet เก็บข้อมูลเส้นทางคีย์ (key origin) ของ public key ที่สร้าง output นี้

    • (PSBTv2) ใน PSBT เวอร์ชัน 2 จะมีฟิลด์ PSBT_OUT_AMOUNT และ PSBT_OUT_SCRIPT เก็บจำนวนเงินและ scriptPubKey ของแต่ละ output (ฟิลด์นี้ไม่มีใน PSBTv0)

    • (PSBTv2 Taproot) ถ้า output เป็น Taproot จะมีฟิลด์เก็บ X-only internal key และโครงสร้างต้นไม้ Taproot ต่าง ๆ เช่น PSBT_OUT_TAP_INTERNAL_KEY, PSBT_OUT_TAP_TREE, PSBT_OUT_TAP_BIP32_DERIVATION เพื่อรองรับ Taproot (ข้อนี้เป็นส่วนเสริมล่าสุดใน PSBTv2)

โดยสรุป PSBT ใช้โครงสร้าง map นี้เพื่อรองรับ input/output ทุกประเภทของ Bitcoin (P2PKH, P2SH, P2WPKH, P2WSH, P2TR ฯลฯ) โดยเก็บข้อมูลสคริปต์และคีย์ที่จำเป็นไว้ในรูปแบบยืดหยุ่น

ขั้นตอนการสร้างและเซ็น PSBT (Workflow)

กระบวนการทำงานของ PSBT แบ่งเป็นหลายบทบาทตามที่กำหนดใน BIP174, ได้แก่ Creator, Updater, Signer, Combiner, Finalizer และ Extractor สรุปขั้นตอนดังนี้:

  • Creator (สร้าง PSBT): เริ่มจากสร้าง Transaction เปล่า (มีรายการ input/output แต่ยังไม่มีลายเซ็นและ scriptSig/witness) แล้วสร้าง PSBT จากธุรกรรมนั้น (เช่นใน Rust ใช้ Psbt::from_unsigned_tx)

  • Updater (เติมข้อมูล): ใส่ข้อมูลเฉพาะของแต่ละ input เช่น UTXO เก่า (ใส่ใน non_witness_utxo หรือ witness_utxo), scriptPubKey/RedeemScript, ข้อมูล path ของ HD key, ชนิด sighash ฯลฯ ข้อมูลเหล่านี้ช่วยให้โปรแกรมเซ็นรู้ว่าต้องเซ็นอะไรและอย่างไร

  • Signer (ลงชื่อ): ผู้ถือคีย์ส่วนตัวจะทำการลงลายเซ็นใน PSBT สำหรับ input ที่เกี่ยวข้อง โดยการคำนวณ signature (เช่น ECDSA หรือ Schnorr) และใส่ลงในฟิลด์ PSBT_IN_PARTIAL_SIG ตามลำดับ หากมีหลายคนร่วมเซ็น สามารถส่ง PSBT สำเนาให้แต่ละคนเซ็นเพิ่มได้หลายรอบ

  • Combiner (รวม PSBT): หากได้มาหลาย PSBT จากคนละแหล่ง (เช่น multisig แต่ละคนเซ็นแยก) บทบาท Combiner จะนำ PSBT เหล่านั้นมารวมกันให้มีทุก signature อยู่ใน PSBT เดียว

  • Finalizer (สรุป): ตรวจสอบว่าแต่ละ input มี signature ครบตามที่ต้องการหรือไม่ จากนั้นสร้าง scriptSig หรือ scriptWitness เต็มรูป (final script) บน input แต่ละอัน แล้วลบข้อมูลชั่วคราวเช่น partial sig, sighash หรือ redeem/witness script ออกตามมาตรฐาน PSBT

  • Extractor (ดึง Transaction): เมื่อมี final script ครบ ก็สามารถแปลง PSBT เป็น Transaction ที่มีลายเซ็นครบถ้วน (Raw Transaction) พร้อมจะ broadcast ได้ (ใน rust-bitcoin ใช้ psbt.extract_tx() หรือ consensus::encode::serialize_hex)

ขั้นตอนเหล่านี้ช่วยให้การจัดการธุรกรรมที่ต้องมีหลายฝ่ายลงชื่อเป็นมาตรฐานและปลอดภัยขึ้น

ตัวอย่างการใช้งาน PSBT ด้วย Rust

ตัวอย่างโค้ดต่อไปนี้ใช้ไลบรารี rust-bitcoin ในการสร้าง PSBT, เติมข้อมูล input, และลงชื่อให้สมบูรณ์ (SegWit v0 example):

use bitcoin::{Transaction, TxIn, TxOut, OutPoint, Psbt, ScriptBuf};
use bitcoin::secp256k1::Secp256k1;

// สมมติเตรียม UTXO เดิมไว้
let prev_txid = ...; // TxID ของ utxo เก่า
let prev_out = 0;    // index ของ output เก่า
let utxo = TxOut { value: 50000, script_pubkey: prev_script.clone() };

// สร้าง transaction เปล่า (unsigned)
let unsigned_tx = Transaction {
    version: 2,
    lock_time: 0,
    input: vec![ TxIn {
        previous_output: OutPoint::new(prev_txid, prev_out),
        script_sig: ScriptBuf::new(),
        sequence: 0xFFFFFFFF,
        witness: Vec::new(),
    }],
    output: vec![ TxOut { value: 49000, script_pubkey: dest_script.clone() } ],
};

// สร้าง PSBT จาก transaction ที่ยัง unsigned
let mut psbt = Psbt::from_unsigned_tx(unsigned_tx).expect("Cannot create PSBT");

// เติมข้อมูลสำหรับ input ที่ PSBT
psbt.inputs[0].witness_utxo = Some(utxo.clone());                 // รายละเอียด UTXO เก่า
psbt.inputs[0].redeem_script = Some(prev_redeem_script.clone());  // ถ้ามี redeem script
psbt.inputs[0].bip32_derivation.insert(pubkey, (fingerprint, path)); // ข้อมูล HD key

// ลงชื่อ PSBT ด้วย private key
let secp = Secp256k1::new();
psbt.sign(&private_key, &secp).expect("Signing failed");

// สร้าง witness script สุดท้ายและดึง Transaction ที่เซ็นเสร็จ
psbt.finalize().expect("Finalizing failed");
let final_tx = psbt.extract_tx().expect("Extraction failed");
println!("Raw TX = {}", hex::encode(final_tx.serialize()));

โค้ดนี้ทำงานดังนี้:

  1. สร้าง Transaction ที่มีอินพุตและเอาต์พุตตามต้องการ แล้วใช้ Psbt::from_unsigned_tx สร้าง PSBT จากธุรกรรมนั้น

  2. เติมข้อมูล input ได้แก่ UTXO เดิม (witness_utxo), สคริปต์ที่เกี่ยวข้อง (redeem_script), และเส้นทาง BIP32 (bip32_derivation)

  3. ใช้ .sign(&private_key, &secp) เพื่อใส่ลายเซ็นลงใน PSBT

  4. สุดท้ายเรียก .finalize() (หรือลงมือเขียน witness เอง) และ .extract_tx() เพื่อดึง Transaction ที่มีลายเซ็นครบ สมบูรณ์พร้อมประกาศสู่เครือข่าย

อ้างอิง: เอกสาร BIP174 (PSBT) และบทความ Bitcoin Ops

รวมถึงตัวอย่างโค้ดจาก Rust Bitcoin Cookbook

Last updated