PSBT คืออะไร และแก้ปัญหาอะไร
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 ออกตามมาตรฐาน PSBTExtractor (ดึง 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()));
โค้ดนี้ทำงานดังนี้:
สร้าง
Transaction
ที่มีอินพุตและเอาต์พุตตามต้องการ แล้วใช้Psbt::from_unsigned_tx
สร้าง PSBT จากธุรกรรมนั้นเติมข้อมูล input ได้แก่ UTXO เดิม (
witness_utxo
), สคริปต์ที่เกี่ยวข้อง (redeem_script
), และเส้นทาง BIP32 (bip32_derivation
)ใช้
.sign(&private_key, &secp)
เพื่อใส่ลายเซ็นลงใน PSBTสุดท้ายเรียก .finalize() (หรือลงมือเขียน witness เอง) และ .extract_tx() เพื่อดึง Transaction ที่มีลายเซ็นครบ สมบูรณ์พร้อมประกาศสู่เครือข่าย
อ้างอิง: เอกสาร BIP174 (PSBT) และบทความ Bitcoin Ops
รวมถึงตัวอย่างโค้ดจาก Rust Bitcoin Cookbook
Last updated