Xây dựng blockchain đơn giản với golang. P2 - CLI + Network
Blockchain
23
White

Trần Mỹ viết ngày 15/01/2018

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

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

phần 1 mình đã trình bày về việc xây dựng 1 chương trình in ra 1 blockchain với cấu trúc rất đơn giản.

Ở phần này, mình sẽ phát triển thêm cấu trúc chương trình để giải quyết các vấn đề sau :

  1. CLI (Command line interface) : Ta sẽ xây dựng chương trình với hỗ trợ cli phong phú hơn ở phần 1 (Ở phần 1 ta chỉ cần chạy ./simplebc mà không cần thêm option nào).

    • Nếu bạn đã từng clone 1 repository tiền ảo nào đó ví dụ như ethereum hoặc từng chạy 1 chương trình mining, có lẽ bạn sẽ thấy chúng được cung cấp các command và option cli rất phong phú. CLI đa dạng sẽ rất tiện khi chương trình trở nên phức tạp hơn.
  2. Network : Trong thực tế ta có thể tưởng tượng blockchain không tồn tại độc lập, mà tồn tại trong 1 mạng lưới P2P. Khi đó ta sẽ có các node trong network có 1 số tính chất sau đây :

    • Synchonization (Đồng bộ) : Khi 1 node mới tham gia vào mạng lưới, nó có thể pull toàn bộ dữ liệu của blockchain từ các node khác về local của mình để xử lý.
    • Mining (Đào block) : 1 node có khả năng đào block mới và chia sẻ nó với các node khác trong mạng lưới, yêu cầu các node khác chấp nhận và thêm block này vào blockchain của họ
    • Consensus (Đồng thuận) : Các node trong mạng lưới cùng chấp nhận yêu cầu thêm block mới khi 1 block được tạo ra. Không chấp nhận có thể xảy ra ví dụ như giá trị hash của block mới không khớp với dữ liệu của nó, hoặc block này bao hàm mining reward quá cao, không khớp với thỏa thuận chung của cả mạng lưới...

Trong phần này mình chỉ xây dựng Network với khả năng Synchonization

Note : mặc dù mình cũng không chắc mình xây dựng "đúng cách", nhưng mình nghĩ xây dựng network là điều cần thiết để ta có thể giả lập các tính chất khác của blockchain.

Vậy tiếp theo mình sẽ trình bày về xây dựng chương trình.

1 số thông tin mình sẽ chỉ trình bày giản lược, cụ thể các bạn có thể tham khảo repository của mình tại đây

Xây dựng

1. Các struct mới

Trước khi vào phần chính, mình xin giới thiệu các struct mình định nghĩa mới. Các bạn có thể bỏ qua phần này và đọc lại khi cần.

Ở phần 1 mình đã định nghĩa 2 struct là BlockBlockchain, 2 struct vẫn sẽ giữ nguyên ở phần này. Các struct mới mình mới thêm vào ở phần này đó là :

1.1. Node, Network

network.go

type Node struct {
    Address string `json:"address"`
}

Node là cấu trúc biểu thị thông tin của phần tử trong mạng lưới, ở phần này ta chỉ có Address biểu thị địa chỉ của node đó. (Ví dụ localhost:3333)

type Network struct {
    LocalNode Node `json:"local_node"`
    NeighborNodes []Node `json:"neighbor_nodes"`
}

Network là cấu trúc biểu thị mạng lưới bao gồm các node. Ở đây mình xây dựng :

  • LocalNode biểu thị bản thân node đang chạy chương trình.
  • NeighborNodes biểu thị các node khác trong mạng lưới.

1.2. Message

message.go

type Message struct {
    Cmd    string `json:"Cmd"`
    Data   []byte `json:"Data"`
    Source Node   `json:"Source"`
}

Message là cấu trúc lưu trữ thông tin trao đổi giữa các node. Trong đó :

  • Cmd: Mã yêu cầu của message, có thể là yêu cầu thêm block, yêu cầu in màn hình...
  • Data: Nội dung chính của message. Ví dụ như dữ liệu block được serialized
  • Source: Thông tin node gửi message.

1.3. Config

config.go

type Config struct {
    Nw Network `json:"network"`
}

Config lưu trữ các thông tin cài đặt cần thiết cho chương trình khi chạy. Trong phần này mình chỉ lưu trữ thông tin mạng.

Dữ liệu Config được import từ config.json khi bắt đầu chạy chương trình.

Trên đây là tất cả các struct mới mình dùng trong phần này. Tiếp theo mình sẽ trình bày xây dựng tính năng đầu tiên là CLI.

2. CLI (Command line interface)

2.1. Lựa chọn package

Sau khi tìm hiểu thì mình có ít nhất 2 lựa chọn như sau để làm cli cho chương trình :

  • Sử dụng flag package : flag là 1 trong những standard package của golang. Bạn có thể tham khảo document tại https://golang.org/pkg/flag/ và example tại https://gobyexample.com/command-line-flags.
  • Sử dụng urfave/cli package : https://github.com/urfave/cli. Package này giống như 1 package mở rộng của flag ở trên, cung cấp nhiều chức năng giúp giảm thời gian phát triển cli trong thực tế so với flag, ví dụ như khả năng tự sinh ra command document.

Ở mức độ đơn giản của bài viết thì mình nghĩ cả 2 package trên đều thỏa mãn được yêu cầu tính năng, và cũng không nhiều sự khác biệt lắm.

Mình chọn dùng urfave/cli vì mình cảm thấy tiện và dễ nhìn (code được tổ chức thành từng block) hơn.
Nếu bạn muốn dùng flag package thì bạn có thể tham khảo 1 bài viết khác về blockchain tại đây

2.2. Import

Bạn có thể import thủ công với với go get github.com/urfave/cli.
Mình cũng đã đưa vào Makefile nên nếu dùng repo của mình bạn có thể chạy make deps để tự động cài tất cả các package cần thiết cho repo.

Makefile

deps:
    $(GOGET) github.com/urfave/cli

2.3. Cài đặt

Từ main.go mình thiết lập để gọi chạy cli.
Xử lý chính của chương trình mình sẽ viết trong cli.go

main.go

func main() {
    initLog(ioutil.Discard, os.Stdout, os.Stdout, os.Stderr)
    app := newCliApp()
    app.Run(os.Args)
}
  • initLog() : gọi hàm thiết lập cơ chế log của chương trình. Bạn có thể tham khảo chi tiết tại đây
  • app := newCliApp() : Tạo 1 cli app mới.
  • app.Run(os.Args) : Chạy cli app.

cli.go

func newCliApp() *cli.App {
    app := cli.NewApp()
    app.Name = "simple blockchain"
    app.Usage = "simple blockchain implemented by golang"

    initStartServerCLI(app)

    return app
}
  • newCliApp() : khởi tạo 1 cli application mới. Application này được đặt tên với app.Name và mô tả với app.Usage
  • initStartServerCLI(app) : thêm vào cli command và option tương ứng liên quan đến chạy server.
func initStartServerCLI(app *cli.App) {
    var configPath string
    app.Flags = []cli.Flag{
        cli.StringFlag{
            Name:        "config, c",
            Value:       defaultConfigPath,
            Usage:       "Load configuration form `FILE`",
            Destination: &configPath,
        },
    }

Ta thêm vào cli 1 global option là --config, khi chạy ta sẽ có thêm option là import file cấu hình với đường dẫn tùy ý. (Mặc định là import file config là ./config.json)

    app.Commands = []cli.Command{
        {
            Name:    "start",
            Aliases: []string{"s"},
            Usage:   "start server",
            Action: func(c *cli.Context) error {
                execStartCmd(c, configPath)
                return nil
            },
        },
    }
}

Ta thêm vào cli 1 command là start. Thay vì chạy với ./simplebc ở part 1, ta sẽ chạy với ./simplebc start thể hiện rằng ta đang chạy 1 node. Sau này ta có thể thêm các command khác như tạo wallet, phân biệt với việc chạy node.

func execStartCmd(c *cli.Context, configPath string) {
    initConfig(configPath)
    bc := getNeighborBc()
    if bc == nil || bc.isEmpty() {
        Info.Printf("Pull failed. Create new blockchain.")
        bc = InitBlockchain()
    }
    startServer(bc)
}

Khi command start được chỉ định khi chạy, execStartCmd() sẽ được gọi đến.

  • initConfig() : import config với đường dẫn là configPath. Trong phần này config chỉ bao gồm thông tin địa chỉ các node trong mạng lưới.
  • bc := getNeighborBc() : Hiện tại blockchain được lưu vào memory nên khi bắt đầu chạy blockchain sẽ chưa được khởi tạo. getNeighborBc sẽ pull blockchain từ các node khác trong mạng lưới. Nếu thất bại chương trình sẽ khởi tạo 1 blockchain mới.
  • startServer(bc) : Ta đã có 1 blockchain được khởi bc. startServer(bc) sẽ bật 1 tcp server, chương trình đóng vai là 1 node lắng nghe và phản hồi các message từ các node khác về thông tin liên quan đến bc. Cụ thể mình sẽ trình bày ở phần Network ở dưới.

Xong!

Khi build xong chương trình, ta có thể kiếm chứng option config và command start đã được thiết lập với help (khả năng tự động sinh command document của urface/cli )

alt text

Đến đây ta đã xây dựng được chức năng cli của chương trình. Tiếp theo mình sẽ trình bày về Network

3. Network

Mình sẽ trình bày một số điểm quan trọng trong chương trình. Cụ thể implement mình chưa trình bày được các bạn có thể tham khảo source code trên repository.

3.1. Vòng đời của 1 node.

Từ phần này ta sẽ chạy chương trình với vai trò là 1 node trong mạng lưới. Vòng đời của 1 node như sau :
alt text

Khi node bắt đầu chạy, blockchain trong node chưa được khởi tạo (vì hiện đang lưu in memory).

Lúc này node sẽ cố gắng kết nối tới các node khác trong mạng lưới để sync blockchain của node đó về của mình.

Nếu node là node đầu tiên được chạy trong mạng lưới, vì nó là đầu tiên nên sẽ không có blockchain trước đó để sync về, node sẽ khởi tạo 1 blockchain mới, với 1 genesis block.

Khởi tạo thành công, node khởi động 1 server tcp lắng nghe message từ khác node khác trong mạng lưới.

3.2. Các message

Các message dùng trong chương trình mình mô tả bằng biểu đồ dưới đây.
alt text

Các tên message trong mũi tên chính là thuộc tính Cmd mình định nghĩa trong struct Message

const (
    CmdSpreadHashList = "SPR_HL"

    CmdReqBestHeight = "REQ_BH"
    CmdReqBlock = "REQ_BL"
    CmdPrintBlockchain = "REQ_PRINT_BC"
    CmdReqAddBlock = "REQ_ADD_BL"

    CmdResBestHeight = "RES_BH"
    CmdResBlock = "RES_BL"
)

Để dễ hiểu, hoạt động của các message mình sẽ mô tả qua các kịch bản sau :
3.2.1. Đồng bộ :
Như mình đã trình bày ở trên, khi bắt đầu chương trình đầu tiên giả sử node A sẽ cố gắng pull dữ liệu blockchain từ các node B về của nó. Trình tự trao đổi message đến khi blockchain được tải về đầy đủ như sau :

  1. A gửi CmdReqBestHeight đến B, yêu cầu B cho A biết chiều cao blockchain của mình. (Giả sử blockchain có 2 block thì chiều cao là 2)
  2. B gửi CmdResBestHeight về A. A so sánh best height của B với của mình, giả sử của A là a nhỏ hơn b là 2 thì sẽ chuyển đến 3.
  3. A xác nhận block tiếp theo còn thiếu trong blockchain của mình là block số a + 1. A sẽ gửi CmdReqBlock đến B yêu cầu B gửi block số tiếp theo (a + 1) về mình.
  4. B nhận được CmdReqBlock với block index là 1. B gửi lại cho A dữ liệu block số 1 được serialized với message CmdResBlock
  5. A nhận được block số a + 1, tiếp tục quay lại 3 đến khi a = b
  6. a = b, chiều cao 2 blockchain bằng nhau, đồng bộ kết thúc.

Hiện tại cơ chế đồng bộ mình chỉ dựa vào chiều cao của các blockchain, thực tế có thể chúng ta sẽ cần phải kiểm tra xem các block dữ liệu có hợp lệ hay không, do đó sẽ cần cơ chế phức tạp hơn. Ở bài viết này mình sẽ chỉ tạm thời dừng ở mức độ này.

3.2.2. Phát tán yêu cầu thêm block mới
Trường hợp này có thể xảy ra khi 1 node A đào được 1 block mới. A sẽ phát tán yêu cầu đến các node khác trong mạng lưới của mình "Tôi đào được block mới rồi, các bạn gửi request CmdReqBestHeight về tôi để đồng bộ nó nhé".

A sẽ gửi CmdSpreadHashList message đến tất cả các node trong network để thông báo điều này. Các node khác nhận được sẽ gửi CmdReqBestHeight lại về A quay lại kịch bản ở trên để tiến hành đồng bộ.

Hiện tại chương trình của mình mới implement cho 2 kịch bản trên. Ngoài ra để tiện debug mình có thêm 2 message nữa là :

  • CmdPrintBlockchain: Khi 1 node nhận được message này, nó sẽ in ra trên màn hình terminal blockchain của mình.
  • CmdReqAddBlock : Node sẽ thêm 1 block với dữ liệu nằm trong message khi nhận được message này. Nhờ đó mình có thể thêm block bằng việc gửi message qua tcp đến node khi node đang chạy.

Với 2 kịch bản như trên, mình sẽ implement server như sau :

3.3. Server

server.go

func startServer(bc *Blockchain) {
    config = getConfig()
    l, err := net.Listen("tcp", config.Nw.LocalNode.Address)
    if err != nil {
        fmt.Println(err.Error())
        os.Exit(1)
    }

    defer l.Close()

    Info.Println("Node listening on " + config.Nw.LocalNode.Address)

    for {
        conn, err := l.Accept()
        if err != nil {
            Error.Println("Error accepting: ", err.Error())
            os.Exit(1)
        }

        go handleRequest(conn, bc)
    }
}

Bằng việc gọi hàm startServer, mình bật 1 server tcp lắng nghe các request đến.

3.3.1. Kết nối

l, err := net.Listen("tcp", config.Nw.LocalNode.Address)

Giao thức mình chọn cho server là tcp, với địa chỉ được lưu trong Nw.LocalNode.Address(ví dụ localhost:3333)

Các bạn có thể chọn giao thức là http mình nghĩ cũng rất ổn, tài liệu mình tham khảo khi xây dựng chương trình thì cũng có cái dùng tcp, cái thì dùng http.

Mình chọn tcp vì muốn ... dùng thử. Có thể sau này mình sẽ chuyển sang http khi chương trình trở nên phức tạp hơn.

3.3.2. Xử lý các request

Xử lý các request gửi đến server được viết trong hàm handleRequest()

server.go

func handleRequest(conn net.Conn, bc *Blockchain) {
    buf := make([]byte, 1024)
    length, err := conn.Read(buf)
    if err != nil {
        Error.Println("Error reading:", err.Error())
        return
    }

    m := deserializeMessage(buf[:length])

    Info.Printf("Handle command %s request from : %s\n", m.Cmd, conn.RemoteAddr())

    switch m.Cmd {
    case CmdReqBestHeight:
        handleReqBestHeight(conn, bc)
    case CmdReqBlock:
        handleReqBlock(conn, bc, m)
    case CmdPrintBlockchain:
        handlePrintBlockchain(bc)
    case CmdReqAddBlock:
        handleReqAddBlock(conn, bc, m)
    case CmdSpreadHashList:
        handleSpreadHashList(conn, bc, m)
    default:
        Info.Printf("Message command invalid\n")
    }

    conn.Close()
}

Khi server nhận được dữ liệu dưới dạng []byte, server sẽ convert nó sang Message với deserializeMessage()
Tùy thuộc vào kiểu Cmd của message mà ta sẽ dẫn đến các rẽ nhánh xử lý trong switch ở trên.

Chi tiết xử lý về mặt ý đồ mình đã trình bày ở phần 3.2. Các message các bạn có thể xem lại.

Mình xin kết thúc phần trình bày về Network ở đây, có lẽ cũng đã đủ để các bạn hiểu được ý tưởng mình thực hiện như thế nào.
Chi tiết implement các bạn có thể tham khảo source code.

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

So với phần 1 thì lần này ta chạy với tư cách là 1 node trong network sẽ cần config phức tạp hơn 1 chút.

Mình đã trình bày trong README trong repository, các bạn tham khảo tại : https://github.com/mytv1/blockchain_go/tree/part_2#running

Các bạn có thể bật 3 terminal với 2 node chạy, 1 terminal để gửi lệnh điều khiển. Khi đó giao diện sẽ trông như thế này :

alt text

Như trong hình ta có thể thấy blockchain ở node 1 và 2 khi in ra đều có cấu trúc giống hệt nhau, khả năng đồng bộ vậy là đã hoàn thành.

Tổng kết

Mình đã trình bày xong phần thực hiện 2 chức năng CLI và Network. Trong thực tế có lẽ Network là 1 trong những phần phức tạp nhất blockchain, có thể ta sẽ cần phải implement rất nhiều cơ chế khác để đảm bảo mạng phân cấp P2P ta dùng là có thể tin tưởng được.

Ở phần tiếp theo mình dự định sẽ xây dựng PersistentProof Of Work. Mình cũng rất vui nếu nhận được góp ý và PR từ mọi người.

Tham khảo

https://jeiwan.cc/posts/building-blockchain-in-go-part-7/
https://github.com/DNAProject/DNA (Mình recommend repo này nếu các bạn muốn tìm hiểu kỹ hơn về network)

TranMy 14-01-2018

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

Trần Mỹ

9 bài viết.
85 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
38 9
XIn chào mọi người. Thời gian gần đây mình có tìm hiểu về blockchain và golang. Mình viết bài viết này với mục đích chia sẻ và tổng hợp những kiến...
Trần Mỹ viết 6 tháng trước
38 9
White
28 5
Xin chào mọi người. Thời gian ngắn gần đây mình có tìm hiểu 1 chút về Bitcoin và Blockchain, và để củng cố kiến thức thu nạp được mình quyết định ...
Trần Mỹ viết 9 tháng trước
28 5
White
14 2
Xin chào mọi người. Đây là phần 3 trong bài viết của mình về xây dựng blockchain với ngôn ngữ Go. Các bạn có thể có thể tham khảo 2 phần trước củ...
Trần Mỹ viết 6 tháng trước
14 2
Bài viết liên quan
White
11 5
Tạm xóa
Giaosucan viết 5 tháng trước
11 5
White
10 2
Xin chào mọi người. Đây là phần 4 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 3 ph...
Trần Mỹ viết 5 tháng trước
10 2
White
38 9
XIn chào mọi người. Thời gian gần đây mình có tìm hiểu về blockchain và golang. Mình viết bài viết này với mục đích chia sẻ và tổng hợp những kiến...
Trần Mỹ viết 6 tháng trước
38 9
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
9 bài viết.
85 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!