Bitcoin

Xây dựng blockchain đơn giản với golang. P5 - Transaction + UTOX Set (Phần cuối)

Xin chào mọi người.

Đây là phần 5 trong bài viết của mình về xây dựng 1 blockchain đơn giản với ngôn ngữ Go.

Các bạn có thể có thể tham khảo 4 phần trước của mình ở đây :
Phần 1 : Cấu trúc cơ bản
Phần 2 : CLI + Network
Phần 3 : Persistent + Proof Of Work
Phần 4 : Wallet + Address

Ở phần 4 mình đã trình bày về việc xây dựng wallet với cơ chế tương tự bitcoin, phần này mình sẽ tiếp tục xây dựng nên 1 trong những phần thú vị nhất của bitcoin đó là Transaction. Thông qua việc xây dựng Transaction, chúng ta sẽ nắm được cách thức các giá trị được lưu chuyển thông qua các block như thế nào, và bản chất tính bảo mật của các giao dịch này chính là toán học.

Ở phần này mình sẽ trình bày theo hướng kết hợp cả lý thuyết và thực hiện

1. Transaction (Giao dịch)

Đầu tiên mình sẽ đi vào định nghĩa của 1 transaction trong bitcoin.

Trong thế giới thực, ta có 1 giao dịch được hiểu là 1 sự trao đổi giá trị giữa 2 bên, ví dụ :

  • A gửi 20000 của mình cho B.
  • B gửi bánh mỳ của mình cho A

Nếu hiểu theo nghĩa đó thì trong bitcoin, 1 transaction đó là phần xác nhận gửi tiền A gửi 20000 của mình cho B. Còn phần B gửi bánh mỳ của mình cho A diễn ra ở thế giới thực. (Mình trình bày điều này để tránh bạn đọc nhầm lẫn 1 giao dịch trong bitcoin là 1 cơ chế xác thực 2 chiều, đảm bảo 2 bên không lừa đảo, ta có thể nói đến điều đó với smart contract, nhưng transaction trong bitcoin chỉ là 1 cơ chế 1 chiều, đảm bảo rằng chính xác A chứ không phải A1 là người gửi tiền cho B)

Hoặc trong cơ sở dữ liệu, ta có 1 transaction được hiểu là 1 sự đảm bảo cho 1 chuỗi các query được thực hiện hết tất cả hoặc là không làm gì. Mặc dù trong bitcoin mình nghĩ cũng có cơ chế tương tự để giải quyết các conflict giữa các node, double spending khi được chạy song song ... nhưng ta sẽ không hiểu theo nghĩa đó khi nói về transaction.

Các bạn có thể tham khảo định nghĩa về transaction trong bitcoin tại đây

So với transaction trong bitcoin thì phần mình xây dựng có 1 điểm khác đó là mình sẽ xây dựng phần script bằng xử lý. (Trong bitcoin có 1 cơ chế gọi là script để thao tác với transaction, nhưng xây dựng nó khá tốn công, và với mục đích giả lập đơn giản nên mình sẽ đơn giản hoá nó bằng xử lý)

1.1. Cấu trúc transaction

Cấu trúc 1 transaction như sau :

type Transaction struct {
    ID     []byte
    TxIns  []TxInput
    TxOuts []TxOutput
}

Trong đó :

  • ID: chuỗi byte thể hiện định danh transaction đó
  • TxInds : mảng các TxInput
  • TxOuts : mảng các TxOutput

Và sự tồn tại của nó trong block như sau :

block.go

type Block struct {
    Transactions []Transaction
    ...
}

Trường Data trước đây được sử dụng sẽ được thay bằng mảng các transaction.

Tiếp theo mình sẽ trình bày cấu trúc của TxInput (Transaction input) và TxOutput (Transaction output)

1.2. TxOutput

Để hiểu TxInput dễ hơn thì mình sẽ nói về TxOutput trước.
Quay lại ví dụ về giao dịch của mình A gửi 20000 của mình cho B. ở mục trên, thì TxOutput ở đây được hiểu chính là 20000 đó. Và trong thực hiện, ta sẽ có cấu trúc của TxOutput như sau :

transaction_output.go

type TxOutput struct {
    Value      int
    PubKeyHash []byte
}

Với ví dụ trên thì Value = 20000PubKeyhash chính là public key của A được hash, rất thẳng. Value thể hiện lượng giá trị mà TxOutput đó mang, và PubKeyHash thể hiện quyền sở hữu của A với TxOutput này.

1 điểm chú ý là TxOutput không bị mất đi, mà nó chỉ chuyển từ trạng thái chưa được giao dịch sang được giao dịch. Với ví dụ ở đầu bài viết, khi giao dịch giữa A và B được thực hiện, TxOutput này dành cho A sẽ không bị mất đi, mà chỉ được đánh dấu là đã được sử dụng, và 1 TxOutput mới cho B sẽ được tạo ra.

1.3. TxInput

TxInput là sự đánh dấu sử dụng TxOutput ở trên. Ta sẽ có cấu trúc của TxInput trong thực hiện như sau :
transaction_input.go

type TxInput struct {
    Txid      []byte
    TxOutIdx  int
    Signature []byte
    PubKey    []byte
}
  • Txid : Transaction id của TxOut mà nó trỏ đến
  • TxOutIdx : Index của TxOut trong transaction mà nó trỏ đến.
  • Signature : Chữ ký thể hiện quyền sử dụng của TxOut mà nó trỏ đến
  • PubKey : Public key của người sở hữu TxOut mà nó trỏ đến, cùng với signature trở thành cặp dữ liệu để các node verify transaction đó là "chính chủ"

Với cấu trúc như trên, mình sẽ minh hoạ hoạt động khi 1 transaction được tạo ra như sau :

1.4. Tạo transaction

alt text

Ta sẽ tạo 1 giao dịch với ví dụ ban đầu là A đổi 20000 để đổi lấy bánh mỳ của B. B đã chuyển cho A bánh mỳ ở thế giới thực và đây là lúc A cần chuyển tiền.

Vậy trước đó ta giả sử A làm chủ 1 node, đào được 1 block mới, và phần thưởng cho đó 25000. A sẽ có 1 coinbase transaction là Transaction 1 ở Block n ở trên. Coinbase transaction là 1 transaction đặc biệt, transaction này không tham chiếu đến bất kỳ TxOutput nào trước đó (trường TxId, TxOutIdx, Signature đều rỗng) mà nó sẽ tạo ra 1 TxOutput mới, đó cũng chính là khi 1 lượng coin mới được sinh ra trong bitcoin. A đào được 1 block mới, do đó A có quyền tạo ra 1 coinbase transaction này và gán giá trị phần thưởng vào địa chỉ của mình.

transaction.go

func newCoinbaseTx(addrTo string) *Transaction {
    txIn := TxInput{[]byte{}, -1, nil, []byte{}}
    txOut := newTxOutput(subsidy, addrTo)
    tx := Transaction{nil, []TxInput{txIn}, []TxOutput{*txOut}}
    tx.ID = tx.hash()

    return &tx
}

newCoinbaseTx là hàm tạo 1 coinbase transaction mới. Như mình đã nói ở trên :

  • TxInput txIn có các trường hầu hết đều rỗng (TxOutIdx = -1 để đánh dấu nhưng mình nghĩ cũng không cần thiết).
  • TxOutput txOut có Value = subsidy (hiện tại là 25), PubKeyHash được trích xuất từ Address addTo gửi vào.

Vậy kết quả rằng A sẽ có 1 TxOutput trong Transaction 1 của block n với Value = 25000, PubKeyHash là hashed public key của A, thể hiện rằng A làm chủ 25000 này.

Tiếp theo A sẽ tạo 1 transaction gửi 20000 cho B, transaction này sẽ được tạo ra như sau :
(tham khảo hàm newTransaction(wallet *Wallet, to string, amount int)) blockchain.go)

  1. Tạo TxInput : Giá trị cần chuyển là 20000 < 25000, do đó ta chỉ cần tạo 1 TxInput trỏ đến TxOutput trên là đủ (Nếu giá trị cần chuyển > 25000, ta sẽ cần tạo thêm TxOutput trỏ đến các TxOutput khác của A sao cho tổng Output Value trỏ đến >= giá trị cần chuyển). TxInput được tạo sẽ có cấu trúc :
    • TxId : ID của transaction chứa coinbase TxOutput trên.
    • TxOutIdx: Index của TxOutput đó trong mảng TxOuts của transaction, ở đây là 1 (Mặc dù thực tế index bắt đầu từ 0 nhưng mình lấy 1 làm ví dụ để tránh nhầm lẫn)
    • Signature: rỗng, sẽ được tạo sau khi cả transaction được khởi tạo
    • PubKey: Public key của A, các node khác khi nhận transaction này sẽ dựa vào trường này đó để xác nhận quyền làm chủ TxOut trên là A chứ không phải ai khác.
  2. Tạo TxOutput : Ta sẽ cần tạo 2 TxOutput, 1 cái value = 20000 trỏ đến B (PubKeyHash của B) và 1 cái value = 5000 trỏ ngược lại A (PubKeyHash của A). Trong bitcoin, một khi đã dùng TxOutput thì phải dùng hết, do đó mặc dù về lý thuyết thì 1 TxOutput trỏ đến B là đủ, nhưng ta sẽ cần tạo thêm 1 TxOutput trỏ lại A. (Có lẽ là vì nếu không dùng hết, mỗi khi tính toán số dư tài khoản ta sẽ phải kiểm tra xem toàn blockchain đã dùng TxOutput này thế nào - rất tốn cost)
    • Trong thực tế có thể A sẽ cần phải trả thêm 1 khoản phí giao dịch (transaction fee) cho node để thực hiện việc chuyển khoản, khi đó 1 TxOutput khác trỏ đến ví của chủ node cũng sẽ được tạo ra. (Chương trình của mình chưa implement phần này.)
  3. Tạo transaction ID : Đến đây ta đã có 1 transaction với 1 TxInput và 2 TxOutput, chỉ còn trường ID trong transaction chưa được gán giá trị. ID đơn giản là giá trị hash nội dung hiện tại của transaction:
    • tx := Transaction{nil, inputs, outputs}
    • tx.ID = tx.hash()
  4. Tạo signature : ta sẽ sử dụng thuật toán chữ ký giống như bitcoin là ecdsa để tạo signature, dữ liệu đưa vào là toàn bộ phần còn lại của transaction ở trên.
    • r, s, err := ecdsa.Sign(rand.Reader, &privateKey, []byte(dataToSign))
    • cơ chế chữ ký này nói cách khác là bản chất toán học tạo nên tính bảo mật của blockchain. Do đó có thể bạn sẽ cần tìm hiểu về hoạt động của thuật toán chữ ký, sử dụng private key để tạo khoá và public key để mở khoá, để nắm được tại sao ý nghĩa của private key, public key trong blockchain nói chung và transaction nói riêng.

Vậy là A đã tạo xong transaction, A sẽ gửi transaction này đến 1 node trên mạng lưới blockchain yêu cầu nó chấp nhận và add vào block của mình. Tiếp theo mình sẽ trình bày về cách 1 node xác nhận transaction là hợp lệ hay không.

1.5. Xác nhận transaction

Xử lý xác nhận transaction mình viết tại đây :
blockchain.go

func (bc *Blockchain) verifyTransaction(tx *Transaction) bool {
    if tx.isCoinbase() {
        return true
    }

    UTXOSet := UTXOSet{bc}
    UTXOSet.Reindex()

    prevTxs := bc.findTransactionsByTx(tx)

    return tx.verifySignature() && UTXOSet.verifyTxInputs(tx.TxIns) && tx.verifyValues(prevTxs)
}
  1. Nếu transaction hợp cách cho 1 coinbase transaction thì trả về true. (Có tổng giá trị Vout = giá trị quy ước của mạng lưới, mặc dù trong implement mình đang làm thiếu)
  2. Nếu là transaction thường, ta sẽ xác nhận 3 điểm sau :
    • tx.verifySignature() : Kiểm tra xem signature được tạo trong TxInput có khớp với các TxOutput được trỏ đến - tức là đúng người chủ của TxOutput là A sử dụng không.
    • UTXOSet.verifyTxInputs(tx.TxIns): Kiểm tra xem các TxOutput được trỏ đến có thực sự chưa được dùng - nằm trong UTXOSet không.
    • tx.verifyValues(prevTxs): Kiểm tra tổng xem giá trị các TxOutput tạo ra có nhỏ hơn hoặc bằng các TxOutput mà nó tham chiếu đến không

Trong thực tế có lẽ implement trong bitcoin sẽ phức tạp hơn, nhưng chương trình của mình hiện tại xử lý verify 3 điểm như vậy.

Giả sử là node sẽ xác nhận transaction này của A là hợp lệ, transaction tiếp theo sẽ được chờ để thêm vào block mới được tạo. Sau 1 khoảng thời gian nhất định, A kiểm chứng đa số các node trong mạng đều đã tồn tại transaction này, vậy là việc xác nhận transaction trong mạng kết thúc, A đã tạo được 1 giao dịch 20000 đến B!
(Trong chương trình của mình, để đơn giản ngay khi nhận được transaction và xác nhận là hợp lệ block mới sẽ được tạo luôn)

Phần trình bày về transaction của mình đến đây là kết thúc. Cụ thể khi thực hiện thì xử lý code có thể phức tạp hơn 1 chút nhưng ý đồ vẫn vậy. Các bạn tham khảo thêm tại source code.

Tiếp theo mình sẽ trình bày về UTXO Set

2. UTXO Set

UTXO Set là viết tắt của Unspend Transaction Output Set, tức là tập hợp các TxOutput chưa được dùng.

Với cơ chế transaction ở phần trên, ta có thể nhận ra vấn đề là mỗi khi cần kiếm tra số dư của 1 tài khoản, ta sẽ cần kiểm tra toàn bộ số TxOutput và TxInput trên tất cả các blocks liên quan tới tài khoản đó, rồi lấy ra phần TxOutput chênh lệch, ta sẽ có được số dư tài khoản.

Ta có thể thấy là với 15Gb dữ liệu block của bitcoin ngày nay, duyệt toàn bộ block sẽ tốn thời gian đáng kể và không hiệu quả trong thực tế. UTXO giải pháp giải quyết vấn đề trên.

Ta để ý rằng ta chỉ cần tìm ra phần TxOutput chênh lệch không được dùng, và bỏ qua toàn bộ phần còn lại là đủ để tính được số dư tài khoản. Vậy thì ta sẽ duyệt toàn bộ blockchain 1 lần để lấy ra toàn bộ số TxOutput chưa được dùng. Sau đó - mỗi khi một block mới được thêm vào mạng lưới, ta cập nhật lại số TxOutput này dựa trên dữ liệu transaction của block đó, như vậy thì một cách đệ quy, chỉ cần đảm bảo quá trình cập nhật là chính xác, ta sẽ luôn có được UTXO đầy đủ.

utxo_set.go

type UTXOSet struct {
    Blockchain *Blockchain
}

boltDB chương trình sử dụng sẽ lưu dữ liệu ở hạng key-value, nên UTXOSet sẽ lưu dữ liệu như sau :

  • key : TransactionID chưa TxOutput trỏ đến
  • Value : map[int]TxOutput với key của mảng này là index của TxOutput trong transaction đó, TxOutput là dữ liệu trỏ đến.

Mình đặt lại type của map trên để tiện sử dụng :

type TxOutputMap map[int]TxOutput

Một utxoset sẽ trỏ đến 1 blockchain, và dựa trên blockchain này để khởi tạo nên mảng các TxOuput ban đầu.
Mình sẽ trình bày ý nghĩa 1 số hàm quan trọng của UTXOSet :

  • Reindex(): (Tái) khởi tạo set các TxOutput. Duyệt lại toàn bộ blockchain để lấy ra tập hợp mới nhất.
  • FindUTXO(pubKeyHash []byte) TxOutputMap: Tìm ra số các TxOutput ứng với một tài khoản (qua publicKeyHash)
  • FindSpendableOutputs(pubkeyHash []byte, amount int) : Cũng tương tự như FindUTXO, nhưng sẽ chỉ trả về số output cần thiết có value < amount
  • CountTransactions() int : Tính tổng các transaction được lưu trong UTXOSet
  • getAllAddressInfo() map[string]int : Tính tổng các value nắm giữ của tất cả tài khoản
  • getTotalValueOwnBy(publicKeyHash []byte) int: Tính số value nắm giữ của một tài khoản có publicKeyHash
  • verifyTxInputs(txIns []TxInput) bool: Xác thực số TxInput trỏ đến TxOutput tồn tại trong UTXOSet
  • Update(block *Block): Update lại UTXO mỗi khi block được tạo.

(Mình tham khảo từ source code gốc, có 1 số hàm được thêm mới, 1 số hàm thì chưa được áp dụng do không đủ thời gian)

Phần trình bày về UTXOSet kết thúc tại đây, tiếp theo mình sẽ trình bày về chạy chương trình.

3. Chạy chương trình

Các bạn có thể tham khảo demo video tại đây : https://youtu.be/X8G33BZS3WY
Source code : https://github.com/mytv1/blockchain_go/tree/part_5

4. Tổng kết

Mặc dù khi bắt đầu bài viết về blockchain này, mình dự định xây dựng thêm 1 số tính khác so với source code gốc như smart contract, consensus nữa, nhưng do mình không còn nhiều thời gian để tiếp tục tìm hiểu, và những nội dung mình trình bày có lẽ cũng đủ để ta lý giải được rất nhiều tính chất của blockchain, nên đây là phần cuối trong bài viết của mình về blockchain. Có thể mình sẽ tiếp tục bài viết về blockchain ở 1 thời điểm khác nếu có cơ hội.

Cảm ơn các bạn đã theo dõi bài viết!

Registration Login
Sign in with social account
or
Lost your Password?
Registration Login
Sign in with social account
or
A password will be send on your post
Registration Login
Registration