Khi mới bắt đầu xây một sản phẩm trên Polkadot, rất dễ rơi vào một cái bẫy: thấy một phần trải nghiệm chưa tốt và lập tức nghĩ rằng mình phải tự xây nó.
Tôi cũng đã đi qua đúng con đường đó với TestSwap.
Ban đầu, vấn đề nhìn có vẻ khá đơn giản.
TestSwap đã hoạt động ổn trên desktop với các extension như Polkadot.js và SubWallet. Nhưng trên điện thoại, trải nghiệm lại khác. Người dùng thường phải mở dApp bên trong browser tích hợp của Nova Wallet hoặc SubWallet.
Tôi bắt đầu nghĩ:
Tại sao không làm một signer thật đơn giản cho mobile?
Không portfolio.
Không staking.
Không bảng giá token.
Không swap riêng.
Không dashboard DeFi.
Chỉ có account, browser và signing.
Một Cypress signer tối giản cho Android.
Ý tưởng khi đó rất rõ:
Desktop dùng extension. Mobile dùng Cypress signer.
Nhưng càng đi sâu, tôi càng nhận ra rằng vấn đề thực sự không nằm ở việc viết một signer.
Nó nằm ở câu hỏi:
Ai nên chịu trách nhiệm quản lý khóa và signing trong kiến trúc của một sản phẩm Polkadot?
Và câu hỏi đó đã khiến kế hoạch thay đổi nhiều hơn một lần.
Bước đầu tiên: tôi từng muốn đưa luôn Web Wallet vào TestSwap
Có một thời điểm tôi đã nghĩ rằng TestSwap có thể tự giải quyết gần như toàn bộ onboarding.
Nếu người dùng chưa có extension, website có thể cung cấp:
Create Wallet
hoặc:
Import Polkadot JSON → nhập mật khẩu → ký
File JSON đã mã hóa thậm chí có thể được giữ trong IndexedDB để lần sau người dùng chỉ cần nhập lại mật khẩu.
Về UX, cách này rất hấp dẫn.
Người dùng không phải rời khỏi website.
Không cần hiểu extension là gì.
Không cần cài thêm phần mềm.
Chỉ cần tạo tài khoản, nhớ mật khẩu và bắt đầu sử dụng sản phẩm.
Tôi đã viết một phần nền tảng theo hướng đó.
Nhưng chính lúc tìm hiểu kỹ hơn về signer và vòng đời của private key, tôi nhận ra mình đang kéo TestSwap sang một trust model hoàn toàn khác.
Vấn đề không nằm ở việc JSON có được mã hóa hay không
Giả sử private key được lưu dưới dạng JSON đã mã hóa.
Khi chưa có mật khẩu, việc lấy được file JSON không đồng nghĩa với việc lấy được private key.
Điều đó tốt.
Nhưng đến lúc người dùng nhập mật khẩu để ký giao dịch, website phải giải mã JSON.
Và đây là điểm quan trọng.
Nếu signer nằm ngay trong JavaScript của website, thì website và signer gần như cùng nằm trong một trust boundary.
Giả sử server của Cypress bị compromise và kẻ tấn công có thể thay bundle JavaScript mà trình duyệt tải xuống.
Code độc hại không nhất thiết phải phá mã hóa.
Nó chỉ cần chờ.
Người dùng tự làm phần khó nhất:
nhập mật khẩu → giải mã wallet → private key xuất hiện trong memory.
Ngay lúc đó code đang chạy trên website có thể tìm cách lấy secret.
Vì vậy, việc yêu cầu nhập mật khẩu cho mỗi giao dịch hay chỉ giữ wallet mở trong 15 phút chủ yếu giúp giảm thời gian secret tồn tại trong memory.
Nó không giải quyết vấn đề kiến trúc lớn hơn:
Nếu code mà server gửi xuống đã bị thay thế, web wallet đang chạy chính code của kẻ tấn công.
JavaScript cũng không phải môi trường lý tưởng để tuyên bố rằng secret đã biến mất hoàn toàn
Một vấn đề nhỏ hơn nhưng cũng đáng lưu ý là JavaScript chạy trong garbage-collected runtime.
Ta có thể:
- lock keypair;
- bỏ reference;
- giảm tối đa thời gian giữ secret;
- tránh lưu plaintext key;
- giới hạn phạm vi object chứa secret.
Nhưng không nên tuyên bố chắc chắn rằng ngay sau khi lock wallet, mọi byte từng chứa private key đã được zero khỏi RAM vật lý.
Đối với nhiều ứng dụng web thông thường, đây không phải vấn đề lớn.
Nhưng khi bắt đầu tự xây một wallet hoặc signer, tiêu chuẩn suy nghĩ phải khác.
Bạn không còn chỉ xây giao diện cho một blockchain.
Bạn đang xây một phần của hệ thống custody.
Đó là một trách nhiệm lớn hơn rất nhiều.
Tại sao extension tạo ra một ranh giới bảo mật dễ hiểu hơn
Điều này khiến tôi quay lại nhìn browser extension với một góc nhìn khác.
Giả sử TestSwap bị compromise nhưng extension vẫn an toàn.
Private key vẫn nằm trong extension.
Website có thể:
- gửi một signing request xấu;
- dựng một transaction khác với điều người dùng nghĩ;
- cố social-engineer người dùng ký;
- hiển thị thông tin gây hiểu nhầm.
Nhưng chỉ riêng việc chiếm được server của TestSwap chưa đồng nghĩa với việc attacker lấy được private key.
Extension tạo ra một trust boundary riêng.
Tất nhiên extension cũng không bất khả xâm phạm.
Nếu chính extension bị compromise, tài khoản developer bị chiếm hoặc kênh update bị tấn công, hậu quả vẫn có thể rất nghiêm trọng.
Nhưng về mặt kiến trúc, đó vẫn là một ranh giới dễ lý giải hơn việc website vừa là dApp vừa trực tiếp giữ signer.
Từ đó tôi thay đổi kế hoạch lần đầu:
Không đưa Web Wallet vào production TestSwap.
Trên desktop, extension đã giải quyết phần lớn vấn đề đủ tốt.
Cài một extension không thực sự là một trở ngại quá lớn.
Không đáng để Cypress tự gánh thêm một wallet layer chỉ để loại bỏ bước đó.
Mobile vẫn còn một khoảng trống
Desktop tương đối rõ.
Mobile thì chưa.
Browser extension không phải mô hình tự nhiên trên điện thoại.
Người dùng Polkadot hiện thường phải dùng browser tích hợp bên trong các wallet như Nova hoặc SubWallet để mở dApp và ký.
Vì vậy tôi chuyển sang ý tưởng thứ hai:
Không xây Web Wallet. Chỉ xây Cypress signer cho Android.
Signer đó sẽ cố tình rất nhỏ.
Website yêu cầu account hoặc signature.
Ứng dụng hiển thị yêu cầu.
Người dùng chọn Approve hoặc Reject.
Signing diễn ra phía ứng dụng.
Private key không bao giờ được đưa vào WebView.
Về mặt phạm vi sản phẩm, điều đó có vẻ hợp lý hơn rất nhiều.
Nhưng rồi Polkadot Products Devnet xuất hiện.
Và nó khiến tôi phải đặt lại câu hỏi một lần nữa.
Polkadot Products có thể đang xây chính lớp mà tôi định tự xây
Tôi muốn thử đưa Cypress lên Polkadot Products Devnet với một Product nhỏ mang tên dự kiến:
cypress.dot
Mục tiêu ban đầu không lớn.
Không chuyển toàn bộ Cypress sang Products.
Không thay thế TestSwap.
Không đưa toàn bộ website nghiên cứu lên một kiến trúc mới.
Chỉ làm một prototype đủ nhỏ để kiểm nghiệm vòng đời:
build → publish → cypress.dot → Product SDK → account/signing → update
Nhưng khi bắt đầu đọc về kiến trúc Products, điều tôi thấy hấp dẫn nhất không phải .dot.
Nó là khả năng wallet và signer trở thành hạ tầng của platform thay vì trách nhiệm của từng dApp.
Đây có thể là thay đổi rất lớn đối với builder
Một dApp Polkadot truyền thống thường phải xử lý hàng loạt việc liên quan đến wallet:
- tìm wallet;
- phát hiện extension;
- lấy danh sách account;
- xử lý nhiều implementation khác nhau;
- gửi signing request;
- hỗ trợ mobile wallet;
- xử lý compatibility;
- theo dõi thay đổi API giữa các wallet.
Nếu một Product chỉ cần nói với Host:
Cho tôi account này.
Hoặc:
Hãy ký payload này.
và Host chịu trách nhiệm đưa yêu cầu tới signer phù hợp, thì builder không còn phải tự quản private key hoặc xây một wallet integration layer quá lớn.
Về kiến trúc, đó là một hướng rất hấp dẫn.
TestSwap vẫn phải hiểu nghiệp vụ của chính nó.
Nó vẫn phải biết:
- người dùng muốn swap asset nào;
- amount bao nhiêu;
- fee dự kiến là gì;
- destination ở đâu;
- call nào cần được dựng;
- điều kiện nào cần được kiểm tra trước khi gửi;
- khi dùng XCM thì asset và destination có hợp lệ hay không.
Nhưng TestSwap không cần trở thành wallet.
Đó là sự phân chia trách nhiệm mà tôi muốn thấy ở một platform.
Từ dApp tự tích hợp wallet sang Product sử dụng Host
Một cách đơn giản để hình dung sự khác biệt là thế này.
Mô hình web hiện tại của TestSwap gần với:
TestSwap → extension/wallet → signer → chain
Nếu Products thực hiện đúng vai trò mà tôi kỳ vọng, mô hình có thể gần hơn với:
TestSwap Product → Product SDK → Host → signer → chain
Điểm khác biệt tưởng như nhỏ nhưng ý nghĩa kiến trúc lại khá lớn.
Ứng dụng tập trung vào việc:
Tôi muốn người dùng thực hiện hành động gì?
Platform tập trung vào:
Account nào đang hoạt động và yêu cầu này được ký an toàn bằng cách nào?
Nếu ranh giới đó hoạt động tốt, một lượng code đáng kể mà mỗi dApp đang tự viết có thể được loại bỏ.
Nhưng kiến trúc đẹp chưa chắc đã tạo ra UX tốt hơn
Đây là phần tôi chưa muốn kết luận sớm.
Giả sử trên desktop, Polkadot Web Host không trực tiếp giữ private key.
Mỗi lần ký, yêu cầu được chuyển sang Polkadot Mobile.
Về bảo mật, mô hình này rất dễ thích.
Nhưng hãy nhìn từ góc độ người dùng.
Người đang ngồi trước máy tính và đã có Polkadot.js hoặc SubWallet có thể hoàn thành:
Swap → extension popup → ký
trong vài giây.
Nếu thay bằng:
Swap → gửi yêu cầu sang điện thoại → lấy điện thoại → mở app → approve
thì kiến trúc có thể tốt hơn nhưng UX lại chậm hơn.
Đối với giao dịch hiếm hoặc giá trị lớn, điều này có thể hoàn toàn chấp nhận được.
Đối với một ứng dụng DeFi mà người dùng thực hiện nhiều thao tác liên tiếp, nó có thể trở thành friction đáng kể.
Vì vậy, nếu platform cho phép, tôi vẫn thích một mô hình có lựa chọn:
Browser extension
hoặc:
Polkadot Host / Mobile signer
Nhưng đây chính là kiểu câu hỏi mà prototype nên trả lời.
Không phải suy đoán trên giấy.
Và đó là lúc tôi nhận ra Cypress signer chưa chắc cần phải được xây
Kế hoạch của tôi đã đi qua ba trạng thái.
Ban đầu:
Xây Web Wallet để người dùng không cần extension.
Sau khi xem xét trust boundary:
Bỏ Web Wallet. Desktop dùng extension. Mobile xây Cypress signer.
Sau khi nhìn vào Polkadot Products:
Thử Products trước rồi mới quyết định có cần Cypress signer hay không.
Đây là thay đổi quan trọng nhất.
Một signer riêng không chỉ là vài màn hình UI.
Một khi nó giữ private key, Cypress phải chịu thêm trách nhiệm về:
- key storage;
- encryption;
- signing implementation;
- secure update;
- backup/recovery;
- Android security;
- dependency security;
- audit;
- distribution;
- maintenance dài hạn.
Nếu Polkadot đã cung cấp một account/signing layer đủ tốt ở cấp hệ sinh thái, việc Cypress tự xây lại lớp đó có thể chỉ tạo ra thêm một bề mặt tấn công và thêm phần mềm cần bảo trì.
Trong trường hợp đó, không xây signer mới là quyết định kỹ thuật tốt hơn.
Khi nào Cypress signer vẫn có lý do tồn tại?
Ngược lại, Products cũng không tự động làm ý tưởng signer trở nên vô nghĩa.
Có ít nhất hai trường hợp signer riêng vẫn có thể có giá trị.
Thứ nhất là mobile web truyền thống.
Nếu Products chỉ giải quyết tốt ứng dụng nằm bên trong môi trường Products nhưng người dùng vẫn cần signer thuận tiện cho các website Polkadot thông thường, thì khoảng trống ban đầu vẫn còn đó.
Thứ hai là interoperability với dApp ngoài Products.
Nếu một Cypress signer có thể hoạt động như một signer tối giản cho nhiều website mà không cố trở thành một super-wallet, nó vẫn có thể là một sản phẩm có lý do tồn tại.
Nhưng đó phải là kết luận sau khi thử platform.
Không nên là giả định trước khi thử.
TestSwap và cypress.dot không nhất thiết cạnh tranh với nhau
Một điều khác tôi nhận ra là không cần ép mình phải lựa chọn một trong hai kiến trúc.
Có thể cuối cùng Cypress vẫn có hai môi trường.
testswap.cypress.work
Một web application truyền thống.
Desktop sử dụng Polkadot.js, SubWallet hoặc extension tương thích.
Mobile sử dụng Nova, SubWallet hoặc signer phù hợp.
Kiến trúc quen thuộc với web hiện tại.
cypress.dot
Một Polkadot Product.
Sử dụng account và signing infrastructure của Polkadot Products.
Không tự quản private key.
Không cần Cypress xây lại wallet layer nếu Host đã cung cấp đầy đủ.
Hai frontend có thể phục vụ cùng một sản phẩm nhưng sử dụng hai integration architecture khác nhau.
Điều đó thậm chí còn tạo ra một phép so sánh rất hữu ích.
Ta có thể đo trực tiếp:
- code nào biến mất;
- UX nào đơn giản hơn;
- mobile hoạt động tốt hơn ở đâu;
- số bước onboarding thay đổi thế nào;
- maintenance burden giảm được bao nhiêu.
Giá trị của cypress.dot không nằm ở .dot
Đối với tôi, đây là điểm quan trọng nhất.
Nếu Products chỉ biến:
cypress.work
thành:
cypress.dot
thì điều đó không đủ hấp dẫn để thay đổi kiến trúc.
Domain đẹp không phải lý do để rebuild sản phẩm.
Câu hỏi đáng thử là:
Nếu account, wallet và signing trở thành hạ tầng chung của Polkadot, builder có thể tập trung gần như hoàn toàn vào sản phẩm hay không?
Nếu câu trả lời là có, tác động của Products đối với developer experience có thể đáng kể.
Một lượng code mà TestSwap hiện phải tự quản có thể trở thành trách nhiệm của platform.
Builder tập trung vào:
- business logic;
- transaction construction;
- simulation;
- fee;
- XCM;
- asset flow;
- UX của chính sản phẩm.
Thay vì mỗi team lại tự giải quyết wallet integration một lần nữa.
Nhưng nếu cái giá phải trả là desktop UX trở nên rườm rà, hoặc Product bị hạn chế khi cần tương tác sâu với các chain như Hydration, thì đó cũng là thông tin cực kỳ có giá trị.
Devnet tồn tại chính để phát hiện những giới hạn đó trước production.
Một bài học cho người mới xây sản phẩm trên Polkadot
Tôi nghĩ đây là bài học lớn nhất mà quá trình này mang lại cho tôi.
Khi mới học xây sản phẩm, rất dễ đánh đồng:
“Tôi có thể xây cái này”
với:
“Tôi nên xây cái này.”
Hai câu hoàn toàn khác nhau.
Tôi đã có thể tiếp tục Web Wallet.
Tôi cũng có thể bắt đầu viết Cypress signer.
Nhưng khả năng kỹ thuật không phải lý do đủ mạnh để thêm một subsystem mới.
Mỗi lớp phần mềm mới đều kéo theo:
- security assumptions mới;
- dependency mới;
- audit surface mới;
- UX mới;
- maintenance mới;
- trách nhiệm mới.
Vì vậy trước khi xây một phần hạ tầng lớn, nên hỏi:
Platform có đang giải quyết vấn đề này không?
Có tiêu chuẩn chung nào đang hình thành không?
Mình thực sự cần kiểm soát phần này, hay chỉ cần sử dụng nó?
Prototype nhỏ nhất nào có thể trả lời câu hỏi đó?
Bước tiếp theo: xây ít hơn để học nhiều hơn
Vì vậy hiện tại tôi không muốn bắt đầu bằng Cypress signer.
Bước hợp lý hơn là đưa một cypress.dot tối giản lên Products Devnet và thử thật.
Tôi muốn kiểm tra:
- account được expose cho Product như thế nào;
- signing request đi qua những lớp nào;
- desktop và mobile khác nhau ra sao;
- người dùng phải thực hiện bao nhiêu bước để ký;
- extension có còn vai trò trong Product hay không;
- Hydration có thể được tích hợp đến mức nào;
- transaction và XCM flow có bị giới hạn gì không;
- Product SDK loại bỏ được bao nhiêu wallet-specific code;
- update và publish Product thực tế hoạt động ra sao.
Sau đó mới quay lại câu hỏi:
Cypress có thực sự cần signer riêng không?
Nếu câu trả lời là có, lúc đó phạm vi của signer cũng sẽ rõ hơn rất nhiều.
Nếu câu trả lời là không, chúng tôi vừa tránh được việc xây và bảo trì một hệ thống custody không cần thiết.
Cả hai kết quả đều tốt.
Prototype không phải để chứng minh rằng ý tưởng ban đầu đúng
Tôi đã viết một phần Web Wallet rồi bỏ hướng đó.
Tôi đã lên kế hoạch xây Cypress Android signer rồi tạm dừng.
Nhìn từ bên ngoài, có thể trông giống như kế hoạch thay đổi liên tục.
Nhưng tôi nghĩ đây chính là cách một sản phẩm nên được hình thành khi mình chưa biết hết câu trả lời.
Prototype không phải để bảo vệ một ý tưởng.
Prototype là cách rẻ nhất để giết một giả định sai trước khi nó trở thành một hệ thống lớn.
Một nguyên tắc tôi muốn giữ lại sau quá trình này là:
Đừng xây thứ mà hạ tầng có thể sắp cung cấp sẵn.
Và có lẽ đó cũng là giá trị lớn nhất của một Devnet.
Không phải để chứng minh điều mình đã tin.
Mà để tìm ra thứ mình thực sự cần xây.
— TienCypress
