Skip to main content

Command Palette

Search for a command to run...

EP 5: การรับส่งข้อมูลเข้ารหัสด้วย Walrus + Seal

รวมร่าง Walrus และ Seal สร้าง Privacy dApp ที่ใช้งานได้จริง

Updated
7 min read
EP 5:  การรับส่งข้อมูลเข้ารหัสด้วย Walrus + Seal

สวัสดีครับ เดินทางมาถึง EP สุดท้ายแล้ว สำหรับบทความเกี่ยวกับการใช้ Walrus และ Seal ในการสร้างเนื้อหาที่มีความ Privacy ก่อนอื่นเราย้อนไปดูกันสักนิดดีกว่า เราทำอะไรมาแล้วบ้าง

ลองไปอ่านดูย้อนหลังกันก่อนน๊ะ เพราะใน EP เราจะนำทุกอย่างมาประกอบร่างกันแล้วครับ


จากตัวอย่างโค๊ดล่าสุดนั้น เราเขียนละติดปัญหาเกี่ยวกับฟังก์ชั่นของ Walrus พอสมควร เเละพบว่าจริงๆเเล้ว เราสามารถเรียกใช้งาน API ผ่าน Provider endpoint เพื่อ ทำการรับเเละส่งข้อมูลไปยัง Walrus network ได้ เลยขอเปลี่ยนวิธีการรับส่งข้อมูลกันหน่อยนึงนะ

มาเริ่มกันเลย นี้คือตัวอย่างโค๊ดทั้งหมดที่นำมาประกอบร่างและ มีการทำให้โค๊ดอ่านง่ายขึ้นละ

  1. ทำการเเก้ไข .env กันก่อน

    SENDER_PRIVATE_KEY=suiprivatekey1xxx // Sender private key
    RECEIVER_PRIVATE_KEY=suiprivatekey1xxx //Receiver private key
    NETWORK=testnet
    PACKAGE_ID=0x0xxxx // Package Id ที่เราได้จาก EP.4
    
  2. สร้างไฟล์ config.ts

    import "dotenv/config";
    
    // --- Env Checks ---
    if (!process.env.SENDER_PRIVATE_KEY || !process.env.RECEIVER_PRIVATE_KEY) {
        throw new Error("Bro, you forgot the private keys in .env file.");
    }
    
    export const CONFIG = {
        // Keys
        SENDER_KEY: process.env.SENDER_PRIVATE_KEY,
        RECEIVER_KEY: process.env.RECEIVER_PRIVATE_KEY,
    
        // Network & Package
        SUI_NETWORK: process.env.SUI_NETWORK || "testnet",
        PACKAGE_ID: process.env.PACKAGE_ID
    
        // Seal Key Servers (Testnet)
        KEY_SERVERS: [
            "0x73d05d62c18d9374e3ea529e8e0ed6161da1a141a94d3f76ae3fe4e99356db75",
            "0xf5d14a81a982144ae441cd7d64b09027f116a468bd36e7eca494f750591623c8",
        ],
    
        // Dummy 32-byte hex for initial encryption setup
        PLACEHOLDER_POLICY_ID: "0x" + "0".repeat(64),
    };
    
    // --- Walrus Endpoints ---
    export const WALRUS_CONFIG = {
        EPOCHS: Number(process.env.WALRUS_EPOCHS ?? 1),
    
        PUBLISHERS: [
            "https://publisher.walrus-testnet.walrus.space",
            "https://wal-publisher-testnet.staketab.org",
            "https://walrus-testnet-publisher.redundex.com",
            "https://walrus-testnet-publisher.nodes.guru",
        ].filter(Boolean) as string[],
    
        AGGREGATORS: [
            "https://aggregator.walrus-testnet.walrus.space",
            "https://wal-aggregator-testnet.staketab.org",
            "https://walrus-testnet-aggregator.redundex.com",
            "https://walrus-testnet-aggregator.nodes.guru",
        ].filter(Boolean) as string[],
    };
    

    มีส่วนที่น่าสนใจเพิ่มขึ้นมาคือ WALRUS_CONFIG ซึ่งส่วนนี้จะมีทั้ง

    • WALRUS_EPOCH เป็นการระบุอายุการเก็บรักษาข้อมูลนั้นเอง

    • PUBLISHERS คือ endpoint สำหรับใช้ในการอัพโหลดข้อมูลไปยัง Walrus network

    • AGGREGATORS คือ endpoint สำหรับการอ่านข้อมูลจาก Walrus netwokr

  3. สร้างไฟล์ walrus.ts

    import { WALRUS_CONFIG } from "./config";
    
    /**
     * Uploads raw bytes to Walrus.
     * It tries multiple publisher nodes until one works.
     */
    export async function uploadBlob(data: Uint8Array): Promise<string> {
        let lastError: unknown = null;
        for (const baseUrl of WALRUS_CONFIG.PUBLISHERS) {
            const url = `\({baseUrl}/v1/blobs?epochs=\){WALRUS_CONFIG.EPOCHS}`;
            console.log(`☁️  Trying upload to: ${baseUrl}...`);
            try {
                const res = await fetch(url, {
                    method: "PUT",
                    body: data,
                });
    
                if (!res.ok) {
                    throw new Error(`HTTP \({res.status}: \){res.statusText}`);
                }
                const info = (await res.json()) as any;
                const blobId =
                    info.newlyCreated?.blobObject?.blobId ||
                    info.alreadyCertified?.blobId;
    
                if (!blobId) throw new Error("Invalid response format from Walrus");
    
                return blobId; // Success!
            } catch (e) {
                console.error(`-> Failed at ${baseUrl}, trying next...`);
                lastError = e;
            }
        }
        throw new Error(`All Walrus publishers failed. RIP. Error: ${lastError}`);
    }
    
    /**
     * Downloads raw bytes from Walrus.
     * It tries multiple aggregator nodes until one works.
     */
    export async function downloadBlob(blobId: string): Promise<Uint8Array> {
        let lastError: unknown = null;
        for (const baseUrl of WALRUS_CONFIG.AGGREGATORS) {
            const url = `\({baseUrl}/v1/blobs/\){blobId}`;
            console.log(`⬇️  Trying download from: ${baseUrl}...`);
            try {
                const res = await fetch(url);
                if (!res.ok) throw new Error(`HTTP ${res.status}`);
    
                const buffer = await res.arrayBuffer();
                return new Uint8Array(buffer); // Success!
            } catch (e) {
                console.error(`-> Node ${baseUrl} failed, trying next...`);
                lastError = e;
            }
        }
        throw new Error(`All Walrus aggregators failed. Blob might be gone.`);
    }
    

    ตรงไปตรงมาเลย ก่อนหน้านี้เราต้องเรียกผ่าน Walrus library ในการอ่านเเละเขียนข้อมูลใช่ไหม ซึ่งเท่าที่ลองดูเจอปัญหางอแงบ้าง เลยตัดจบด้วยการใช้วิธีเรียกผ่าน HTTP Protocol เลยทีเดียวจบ

  4. สร้างไฟล์ main.ts

    import { getFullnodeUrl, SuiClient } from "@mysten/sui/client";
    import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
    import { decodeSuiPrivateKey } from "@mysten/sui/cryptography";
    import { SealClient, SessionKey, EncryptedObject } from "@mysten/seal";
    import { Transaction } from "@mysten/sui/transactions";
    import { fromHex, toHex } from "@mysten/sui/utils";
    import { randomBytes } from "node:crypto";
    
    import { CONFIG } from "./config";
    import { uploadBlob, downloadBlob } from "./walrus";
    
    
    let suiClient: SuiClient;
    let sealClient: SealClient;
    
    async function main() {
        console.log("🚀 Starting the Walrus x Seal Privacy Demo...");
    
        // 1. Setup Network
        await initClients();
    
        // 2. Setup Users
        const sender = getKeypair(CONFIG.SENDER_KEY);
        const receiver = getKeypair(CONFIG.RECEIVER_KEY);
    
        console.log(`👤 Sender:   ${sender.getPublicKey().toSuiAddress()}`);
        console.log(`👤 Receiver: ${receiver.getPublicKey().toSuiAddress()}`);
    
        const secretMessage = "Yo fam, this is exclusive content! 📸 (Secured by Seal)";
    
        // 3. Sender Flow
        const policyObjectId = await runSenderFlow(secretMessage, receiver, sender);
    
        // 4. Simulate Network Delay
        console.log("\n⏳ Chilling for 5s to let Walrus propagate...");
        await sleep(5000);
    
        // 5. Receiver Flow
        await runReceiverFlow(policyObjectId, receiver);
    
        console.log("\n✨ Boom! Mission accomplished.");
    }
    
    // ============================================================================
    // WORKFLOWS
    // ============================================================================
    
    async function runSenderFlow(
        secret: string,
        recipient: Ed25519Keypair,
        sender: Ed25519Keypair
    ): Promise<string> {
        console.log(`\n--- [Step 1] Sender Turn ---`);
        console.log(`🔒 Encrypting payload...`);
    
        // Prepare a randomized ID for encryption
        // We add a nonce to ensure the ID is unique even if the policy ID is static
        const policyBytes = fromHex(CONFIG.PLACEHOLDER_POLICY_ID);
        const nonce = new Uint8Array(randomBytes(5));
        const idBytes = new Uint8Array(policyBytes.length + nonce.length);
        idBytes.set(policyBytes, 0);
        idBytes.set(nonce, policyBytes.length);
    
        // Encrypt locally
        const plainBytes = new TextEncoder().encode(secret);
        const { encryptedObject } = await sealClient.encrypt({
            threshold: 2,
            packageId: CONFIG.PACKAGE_ID,
            id: toHex(idBytes),
            data: plainBytes,
        });
    
        console.log(`-> Encrypted size: ${encryptedObject.byteLength} bytes`);
    
        // Upload to Walrus (Using our helper)
        const blobId = await uploadBlob(encryptedObject);
        console.log(`✅ Walrus Blob ID: ${blobId}`);
    
        // Mint the Policy Object on Sui
        console.log("⛓️ Minting SecretMessage object on-chain...");
        const tx = new Transaction();
        tx.moveCall({
            target: `${CONFIG.PACKAGE_ID}::message::create_secret_message`,
            arguments: [
                tx.pure.address(recipient.getPublicKey().toSuiAddress()),
                tx.pure.string(blobId)
            ],
        });
    
        const result = await suiClient.signAndExecuteTransaction({
            signer: sender,
            transaction: tx,
            options: { showObjectChanges: true },
        });
    
        // Find the new object ID
        const created = result.objectChanges?.find(
            (c: any) =>
                c.type === "created" &&
                "objectId" in c &&
                c.objectType.includes("::message::SecretMessage")
        );
    
        if (!created || !("objectId" in created)) {
            throw new Error("Tx failed. Could not find created object.");
        }
    
        const objectId = created.objectId as string;
        console.log(`📡 Policy Object created: ${objectId}`);
    
        return objectId;
    }
    
    async function runReceiverFlow(policyObjectId: string, recipient: Ed25519Keypair) {
        console.log(`\n--- [Step 2] Receiver Turn ---`);
        console.log(`📦 Fetching policy object: ${policyObjectId}`);
    
        // 1. Get Blob ID from Chain
        const obj = await suiClient.getObject({
            id: policyObjectId,
            options: { showContent: true },
        });
    
        const fields = (obj.data?.content as any)?.fields;
        if (!fields) throw new Error("Policy object is empty/invalid.");
    
        const blobId = fields.walrus_blob_id;
        console.log(`📦 Found Walrus ID: ${blobId}`);
    
        // 2. Download from Walrus (Using our helper)
        const encryptedBytes = await downloadBlob(blobId);
        console.log(`   -> Got ${encryptedBytes.byteLength} bytes`);
    
        // 3. Validation
        const info = EncryptedObject.parse(encryptedBytes);
        if (!info.id || !info.threshold) throw new Error("Data looks corrupted.");
    
        // 4. Session Key (Ephemeral security)
        console.log(`🔑 Generating temp session key...`);
        const recipientAddr = recipient.getPublicKey().toSuiAddress();
        const sessionKey = await SessionKey.create({
            address: recipientAddr,
            packageId: CONFIG.PACKAGE_ID,
            ttlMin: 10,
            signer: recipient,
            suiClient,
        });
    
        // 5. Build Proof (seal_approve)
        console.log("🛡️  Building auth proof...");
        const tx = new Transaction();
        tx.setSender(recipientAddr);
        tx.moveCall({
            target: `${CONFIG.PACKAGE_ID}::message::seal_approve`,
            arguments: [
                tx.pure.vector("u8", fromHex(info.id)),
                tx.object(policyObjectId),
            ],
        });
    
        const txBytes = await tx.build({ client: suiClient, onlyTransactionKind: true });
    
        // 6. Decrypt
        console.log("🔓 Asking Key Servers to decrypt...");
        const decryptedBytes = await sealClient.decrypt({
            data: encryptedBytes,
            sessionKey,
            txBytes,
        });
    
        const msg = new TextDecoder().decode(decryptedBytes);
        console.log(`\n🎉 SUCCESS! Message: "${msg}"`);
    }
    
    async function initClients() {
        suiClient = new SuiClient({ url: getFullnodeUrl(CONFIG.SUI_NETWORK) });
        sealClient = new SealClient({
            suiClient,
            serverConfigs: CONFIG.KEY_SERVERS.map((id) => ({
                objectId: id,
                weight: 1,
            })),
            verifyKeyServers: false, // skipping strict verification for testnet demo
        });
    }
    
    function getKeypair(secretKey: string) {
        return Ed25519Keypair.fromSecretKey(decodeSuiPrivateKey(secretKey).secretKey);
    }
    
    function sleep(ms: number) {
        return new Promise((r) => setTimeout(r, ms));
    }
    
    main().catch((e) => {
        console.error("FATAL ERROR:", e);
        process.exit(1);
    });
    

    มาถึงไฟล์สุดท้าย พระเอกของเรานั้นเอง มาไล่ทีละขั้นตอนกันนะ

    • เริ่มจากเป็นการ initClients() ก่อนส่วนนี้จะเห็นว่าไม่มีการเรียกใช้งาน Walrus แล้ว เพราะเราจะส่งข้อมูลผ่าน HTTP กัน

    • ส่วนของฟังก์ชั่น runSenderFlow นั้นจะเป็นนำข้อความของเราไปเข้ารหัสด้วย Seal หลังจากนั้นก็ทำการอัพโหลดข้อมูลไปยัง Walrus network นั้นเอง เเละเมื่อเราได้ blobId มาแล้วก็ทำการเรียกใช้งานฟังก์ชั่น Sui smart contract ที่เราได้เขียนไว้เเล้วเพื่อกำหนดผู้รับ

    • ส่วนของฟังก์ชั่น runReceiverFlow ทำตรงกันข้ามนั้นเองโดยนำ blobId ไปดาวโหลดข้อมูลจาก Walrus network ลงมาก่อนแล้วทำการถอดรหัสข้อมูลด้วย Seal หลังจากผู้รับต้องทำการเรียก Sui smart contract เพื่อยืนยันความเป็นเจ้าของนั้นเอง

เรียบร้อยแล้วง่ายไหม ง่ายมั๊ง ลองเอาไปรันดูนะครับ เเล้วเอาผลลัพธ์มาโชว์ด้วยนะ นี้คือตัวอย่างเล็กๆน้อยนะ ซึ่งสิ่งที่เราได้สร้างขึ้นมานั้นจะมี คุณสมบัติ

  • Client-Side Encryption - ข้อมูลเข้ารหัสบนอุปกรณ์ผู้ใช้

  • Decentralized Storage - เก็บบน Walrus แบบกระจายศูนย์

  • On-Chain Access Control - Smart Contract ควบคุมสิทธิ์การเข้าถึง

  • Threshold Cryptography - ไม่มี Single Point of Failure

  • Programmable Ownership - สิทธิ์เป็น Object ที่โอนย้ายได้

ซึ่งจากคุณสมบัติเหล่านี้เราสามารถนำไปต่อยอดเป็น use case อื่นได้อีกมายเลยนะ เช่น

  • Creator Platforms ทำพวก Subscription-based content ได้สบายๆ

  • Gaming Content ทำพวก Unlockable content ตามเงื่อนไข On-Chain หรือจะทำ Secret quests และ time-limited events ก็ได้นะ

  • AI & Data Markets สาย AI ขาย AI models และ datasets แบบ encrypted

  • NFT Utilities ทำ NFT ที่มีคอนเทนต์จริงซ่อนอยู่ :P

เอาละ ลองไปต่อยอดกันดูนะ ไว้เจอกันใหม่

ปิดท้ายด้วยเเหล่งข้อมูลที่สามารถศึกษาได้เพิ่ม