Skip to main content

Command Palette

Search for a command to run...

EP 4: Seal กับ On-Chain Access Control

จัดการสิทธิการเข้าถึงเนื้อหาแบบกระจายศูนย์ด้วย Seal

Updated
4 min read
EP 4: Seal กับ On-Chain Access Control

สวัสดีครับ เดินทางมาถึงตอนที่ 4 แล้วกับซี่รีย์ Walrus + Seal ถ้าหากใครยังไม่ได้อ่าน อ่านย้อนหลังได้ที่นี้นะครับ

สำหรับตอนนี้ เรามาเขียนในส่วนของ Move Smart Contract กันนะ เพราะจากตอนที่แล้ว เราได้เขียน Seal ไว้เรียบร้อยแล้ว แต่ขาดในส่วนของ Smart Contract ในการจัดการเรื่องสิทธิในการเข้าถึง

โดย Seal จะปกป้องข้อมูลของเราจากผู้ใช้งานทั่วไป ด้วยระบบป้องกันดังต่อไปนี้

  1. Client-Side Encryption: หลักการสำคัญของ Seal คือข้อมูลที่ละเอียดอ่อนจะถูกเข้ารหัส บนอุปกรณ์ของเราเอง ก่อนที่จะถูกส่งออกไปที่อื่นๆ โดยไม่มีเซิร์ฟเวอร์ แม้แต่ key servers ของ Seal เอง ก็จะไม่เห็นข้อมูล plaintext ของเราเลย

  2. Threshold Encryption: เพื่อหลีกเลี่ยงความเสี่ยงจากการจัดการกุญแจแบบรวมศูนย์ Seal ได้ใช้ Threshold Encryption แทนที่จะมีกุญแจถอดรหัสเพียงอันเดียว ซึ่งกุญแจนั้นจะถูกแบ่งออกเป็นหลาย shares และกระจายไปยังเครือข่าย Decentralized Key Servers ที่เป็นอิสระต่อกัน หากต้องการสร้างกุญแจ ต้นฉบับขึ้นมาใหม่เพื่อถอดรหัส จะต้องมี key servers จำนวนหนึ่ง (เช่น 2 ใน 3) ทำงานร่วมกัน ซึ่งขั้นตอนนี่จะช่วยขจัด single point of failure ออกไปได้เลยนั้นเอง

  3. On-Chain Access Control: นี่คือส่วนที่ล้ำสมัยที่สุดของสถาปัตยกรรม Seal! โดยตรรกะที่กำหนดว่าใครได้รับอนุญาตให้ถอดรหัสข้อมูลนั้นไม่ได้ถูก hard-coded อยู่ใน off-chain server แต่ถูกกำหนดไว้ใน Move Smart Contract นั้นเอง ซึ่งข้อมูลนี้จะถูกเปิดเผยต่อสาธารณะและตรวจสอบได้บน Sui Network! สิ่งนี้ทำให้เราสามารถสร้าง Access Policies ที่ละเอียดและสามารถ Programmable ได้ เช่น เช่น token-gating (อนุญาตเฉพาะผู้ถือ NFT บางประเภท), time-locks หรือ role-based permissions ที่ซับซ้อน

มาเริ่มกันเลย ต่อไปนี้เราจะลงในส่วนของ Technical แล้วนะ ในคือการเขียน Move Smart Contract หากใครยังไม่ได้เตรียมเครื่องไม้ เครื่องมือ ให้ย้อนกลับไปดูบทความด้านล่างนี้นะ

โดย Move Smart Contract ที่เราจะเขียนกันต่อไปนี้ จะมีหัวใจหลักของ Seal อยู่ที่ฟังก์ชั่น seal_approve ซึ่ง

จุดเชื่อมต่อที่สำคัญสำหรับนักพัฒนาคือ entry function ที่มีชื่อเฉพาะใน Move module ของเรา: seal_approve ฟังก์ชันนี้ทำหน้าที่เป็น On-Chain Gatekeeper สำหรับคำขอถอดรหัสทั้งหลาย!

ซึ่งเราจะทดสอบเขียน secret_message.move contract ของเรา กันมาเริ่มกันทีละ step กันเลยครับ

  1. สร้างโปรเจ็คขึ้นมาใหม่

     sui move new secret_message
    
  2. แก้ไข source code ในไฟล์ secret_message.move

     module secret_message::message {
         // Import เฉพาะตัวที่ต้องใช้จริง (module ส่วนใหญ่ Sui prelude มีให้แล้ว)
         use sui::object::UID;
         use sui::tx_context::TxContext;
         use std::string::String;
    
         /// Error ที่จะเกิดขึ้นเมื่อผู้ใช้ที่ไม่ได้รับอนุญาตพยายามเข้าถึงข้อความลับ
         const ENoAccess: u64 = 0;
    
         /// โครงสร้างของ SecretMessage ซึ่งเป็น Policy Object
         ///
         /// - Policy นี้ไม่เก็บข้อความจริงบน-chain
         /// - ตัวข้อความจะถูกเข้ารหัสและเก็บบน Walrus (off-chain)
         /// - On-chain จะเก็บเฉพาะ metadata และสิทธิ์การเข้าถึง
         ///
         /// ฟิลด์:
         /// - `sender`: address ของผู้สร้าง (ผู้ส่งข้อความลับ)
         /// - `recipient`: address ของผู้รับที่ได้รับอนุญาต
         /// - `walrus_blob_id`: ID ของ blob ที่เก็บ encrypted content บน Walrus
         public struct SecretMessage has key, store {
             id: UID,
             sender: address,
             recipient: address,
             walrus_blob_id: String,
         }
    
         /// ฟังก์ชันสำหรับสร้าง SecretMessage ใหม่
         ///
         /// ขั้นตอน:
         /// 1. ผู้ส่งเรียกฟังก์ชันนี้พร้อมระบุผู้รับ และ Walrus blob ID
         /// 2. ระบบสร้าง Policy Object ใหม่ (SecretMessage)
         /// 3. ส่ง Policy Object ไปยัง wallet ของผู้รับ
         ///
         /// หมายเหตุ:
         /// - ผู้รับเป็นคนเดียวที่ถือ object นี้ → สอดคล้องกับ access control
         public fun create_secret_message(
             recipient: address,
             walrus_blob_id: String,
             ctx: &mut TxContext
         ) {
             let message_policy = SecretMessage {
                 id: object::new(ctx),
                 sender: ctx.sender(),
                 recipient,
                 walrus_blob_id,
             };
    
             // โอน Policy Object ไปที่ผู้รับ → เพื่อให้เป็นเจ้าของสิทธิ์การถอดรหัส
             transfer::public_transfer(message_policy, recipient);
         }
    
         /// ฟังก์ชันหลักสำหรับทำงานร่วมกับ Seal (Decentralized Access Control Service)
         ///
         /// Concept:
         /// - Seal Key Servers จะจำลองการเรียกฟังก์ชันนี้ในขั้นตอน Verify Access
         /// - ถ้าฟังก์ชันนี้ "ผ่านเงื่อนไข" → Key Servers จะอนุญาตให้ user รับ key share
         /// - ถ้าไม่ผ่าน → จะไม่ปลดล็อก key share
         ///
         /// มันคือ "On-chain Access Control Policy"
         /// ใช้เงื่อนไขบน-chain ตรวจสอบว่าผู้ที่ขอถอดรหัสเป็น "recipient" ตัวจริงหรือไม่
         ///
         /// พารามิเตอร์:
         /// - `_`: ค่า vector<u8> ที่ Seal ต้องส่งมา (แต่ในที่นี้ไม่ใช้งาน)
         /// - `policy`: reference ไปยัง SecretMessage ของข้อความนั้น
         /// - `ctx`: ใช้ตรวจสอบว่า user ที่เรียกคือใคร
         public fun seal_approve(
             _: vector<u8>,
             policy: &SecretMessage,
             ctx: &TxContext
         ) {
             // อนุญาตเฉพาะผู้รับที่กำหนดไว้ใน policy เท่านั้น
             // ถ้า ctx.sender() ไม่ใช่ recipient → throw error
             assert!(ctx.sender() == policy.recipient, ENoAccess);
         }
     }
    
  3. Deploy Move Smart Contract

    เราเริ่มจากการทดสอบ Build กันก่อนนะ ว่าติดปัญหาอะไรไหม อย่าลืมสลับไปใช้งาน Testnet นะครับ

     sui move build
    

    หากทุกอย่างเรียบร้อยก็เตรียมไปสู่ขั้นตอนการ Deploy ไปยัง SUI Testnet ได้เลย

     sui client publish --gas-budget 100000000
    

    เราจะได้หน้าตาประมาณนี้นะ

     ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
     │ Object Changes                                                                                   │
     ├──────────────────────────────────────────────────────────────────────────────────────────────────┤
     │ Created Objects:                                                                                 │
     │  ┌──                                                                                             │
     │  │ ObjectID: 0x3e841ab9ffb9dd26ef128631030e2fed4ce19d40e2be1521e760fd4c2dbfe18b                  │
     │  │ Sender: 0xc1a83e6ed8a891cc891208e4288290013ed3f1ebf1576eede3f2c7bc4c56e75c                    │
     │  │ Owner: Account Address ( 0xc1a83e6ed8a891cc891208e4288290013ed3f1ebf1576eede3f2c7bc4c56e75c ) │
     │  │ ObjectType: 0x2::package::UpgradeCap                                                          │
     │  │ Version: 349180711                                                                            │
     │  │ Digest: 4zJPCm9Xab7VkY2bfsH347FwFcPQo74rYN75Qz9dddzk                                          │
     │  └──                                                                                             │
     │ Mutated Objects:                                                                                 │
     │  ┌──                                                                                             │
     │  │ ObjectID: 0x0e77d7f999871c0a82f098699b0128715ade05416531deee0902f8a52bee4f9e                  │
     │  │ Sender: 0xc1a83e6ed8a891cc891208e4288290013ed3f1ebf1576eede3f2c7bc4c56e75c                    │
     │  │ Owner: Account Address ( 0xc1a83e6ed8a891cc891208e4288290013ed3f1ebf1576eede3f2c7bc4c56e75c ) │
     │  │ ObjectType: 0x2::coin::Coin<0x2::sui::SUI>                                                    │
     │  │ Version: 349180711                                                                            │
     │  │ Digest: G8GFetS11dJDiDPYJvnvVaJ3kZnvy3ec31CPJfVSZTnu                                          │
     │  └──                                                                                             │
     │ Published Objects:                                                                               │
     │  ┌──                                                                                             │
     │  │ PackageID: 0x04c285b7f84f9db5cbef6e3c0a48a086463fa618a45ddc14154179fbde533c34                 │
     │  │ Version: 1                                                                                    │
     │  │ Digest: AFZExHfnygn3MqDCkfK1Hh9Ht5aaSxuGHu3bovjvbMhG                                          │
     │  │ Modules: message                                                                              │
     │  └──                                                                                             │
     ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
     ╭───────────────────────────────────────────────────────────────────────────────────────────────────╮
     │ Balance Changes                                                                                   │
     ├───────────────────────────────────────────────────────────────────────────────────────────────────┤
     │  ┌──                                                                                              │
     │  │ Owner: Account Address ( 0xc1a83e6ed8a891cc891208e4288290013ed3f1ebf1576eede3f2c7bc4c56e75c )  │
     │  │ CoinType: 0x2::sui::SUI                                                                        │
     │  │ Amount: -8488280                                                                               │
     │  └──                                                                                              │
     ╰───────────────────────────────────────────────────────────────────────────────────────────────────╯
    

    ซึ่งสิ่งที่สำคัญคือ หลังจากที่เราได้ทำการ Deploy สำเร็จเเล้ว เราจะเห็น PackageID เป็น String ยาวๆ 0x... ให้เก็บ PackageID นั้นเอาไว้ เพราะเราจะต้องนำไปใช้ใน EP 3 ซึ่งยังจำกันได้ใช่ไหมว่า ค่าของ PackageID เรายังเป็นค่าที่ยังไม่ได้กำหนด ซึ่งทำให้ตัวอย่างของเราทำงานไม่ได้

    ซึ่งนี้ที่ PackageID ของเราคือ

     0x04c285b7f84f9db5cbef6e3c0a48a086463fa618a45ddc14154179fbde533c34
    

เรียบร้อยครับ ตอนนี้เราได้ PackageID ที่พร้อมจะนำไปใช้งานแล้ว ก่อนที่เราจะไปกันต่อ ขอธิบายเพิ่มเกี่ยวกับการทำงานของ seal_approve กันสักนิดนึง โดยหลักการจะมีประมาณนี้นะ

  1. ผู้ใช้ต้องการถอดรหัส: Client application ของผู้ใช้ (เช่น เว็บไซต์ Only… ของเรา) ต้องการเข้าถึงคอนเทนต์ลับ

  2. จำลองธุรกรรม: แทนที่จะส่ง transaction ไปรันบน Sui Network ทันที Client application จะสร้าง Sui transaction ที่เรียกฟังก์ชัน seal_approve นี้ (แต่ยังไม่ execute จริง!) แล้วส่ง serialized bytes ของ transaction นี้ไปให้ Seal Key Servers

  3. Key Servers ตรวจสอบ: Key Servers หลายตัว จะทำการจำลองการรัน transaction นี้บน Sui แยกกัน

  4. ปล่อย Key Shares (ถ้าผ่าน): ถ้าการจำลองสำเร็จ (คือ assert! ในฟังก์ชัน seal_approve เป็นจริง เช่น ctx.sender() คือ recipient ที่ระบุไว้) Key Server นั้นๆ ก็จะปล่อย key share ของตนออกมา

  5. ปฏิเสธ (ถ้าไม่ผ่าน): แต่ถ้าการจำลองล้มเหลว (เช่น assert! เป็นเท็จ เพราะผู้ที่พยายามถอดรหัสไม่ใช่ recipient ที่ถูกต้อง) Key Server ก็จะปฏิเสธที่จะให้ key share

Sui ถูกใช้เป็น decentralized และ verifiable access-control oracle ได้อย่างชาญฉลาด! ข้อมูลที่เข้ารหัสสามารถเก็บไว้ที่ไหนก็ได้ — ไม่ว่าจะเป็น Walrus หรือเครือข่ายอื่น—เพราะฟังก์ชัน seal_approve ทำหน้าที่เป็น public, immutable, composable API สำหรับตรวจสอบสิทธิ์การเข้าถึงบน chain โดยตรง

พูดง่ายๆ คือ: ตัว blockchain กลายเป็น access-control engine ที่ทุกระบบเชื่อถือร่วมกันได้ เป็นดีไซน์ที่ทั้งเรียบง่ายและทรงพลังมากๆ เลยใช่ไหมล่ะ?

เราเตรียมทุกอย่างพร้อมแล้ว สัปดาห์เราจะมาประกอบร่างสุดท้ายกันแล้ว อย่าลืมไปทดสอบ Deploy กันเด้อ แล้วเจอกันใหม่นะ