Cross program invocation in-depth. How to invoke any Program on solana (from any Program) ?
CPI nâng cao: làm sao để invoke giữa các chương trình khác nhau (native <> anchor) trên Solana. Tutorial và mã nguồn cho cách gọi CPI giữa các Program khác nhau dù là viết Native hay sử dụng Anchor
[You can find en version here]
Lời bàn
Khi search google về CPI, ta thường gặp các hướng dẫn CPI giữa các Program được viết & compile bằng Anchor. Tuy nhiên điều này không luôn xảy ra, nhất là khi ta muốn invoke một Program CHƯA-ĐƯỢC-ĐỊNH-NGHĨA-TẠI-COMPILE-TIME.
Trong bài viết dưới đây, bạn Trung sẽ hướng dẫn ta cách làm và có code demo. Các bạn có thể đọc bài viết gốc tại đây.
Problem ?
Thông thường, để thực hiện CPI từ program A đến program B trên solana, chúng ta cần source code của program B. Vậy liệu có cách nào để gọi đến program B mà không cần source code và sự khác nhau khi thực hiện CPI đến 1 program được tạo bằng anchor và native rust như thế nào ?
What we build ?
Các bạn có thể xem code demo ở đây:
Chương trình chính: https://github.com/hectagon-finance/pos_solana/tree/main/workflow
Chương trình được gọi: https://github.com/hectagon-finance/pos_solana/tree/main/logic1
Chương trình được gọi thứ 2: https://github.com/hectagon-finance/pos_solana/tree/main/logic2
Design:
(1) Khi client tương tác với hàm init_user( ) sẽ tạo ra một PDA (seed là [programID, Pubkey]) với max = 0
max: Số lượng workflow đã khởi tạo.
(2) Khi client tương tác với hàm create_workflow( ) sẽ tạo ra một PDA (seed là [programID, Pubkey, max]) với ping = 0 và max++ (max tăng thêm 1)
ping: Số lượng vote của một workflow.
Lưu ý: Trước khi chạy (1) và (2) Bạn cần thay đổi keypair ở 2 chỗ:
1. in test/workflow.ts
const keypair = web3.Keypair.fromSecretKey(
Uint8Array.from([64,33,98,76,216,30,65,85,81,32,240,30,164,197,23,225,253,179,10,197,190,174,155,56,130,224,202,128,189,201,48,37,20,123,160,201,77,149,50,29,89,209,232,173,89,87,250,249,192,221,235,132,195,237,147,165,80,165,155,92,70,100,203,86])
)
2. in terminal run `solana config get`
you will see: Keypair Path: /home/trung1701/.config/solana/id.json
please replace keypair in id.json
(3) Khi client tương tác với hàm vote(), program Workflow sẽ gọi tới program Logic1. Logic1 là program được viết bằng anchor framework. Trường ping sẽ tăng 1 trong PDA workflow (ping++).
(4) Khi client tương tác với hàm vote2(), program Workflow sẽ gọi tới program Logic2. Logic2 là program được viết bằng native rust. Trường ping cũng sẽ tăng 1 trong PDA workflow (ping++).
Program Logic1 và Logic2 được xây dựng hoàn toàn riêng biệt so với program Workflow. Program Workflow không có các tệp IDL của program Logic1 và Logic2. Vì vậy thứ duy nhất chúng ta chỉ biết là programID của program Logic1 và logic2 ở trên mạng solana để tương tác với chúng.
Trong Solana programming, Interface Definition Language (IDL) xác định public interface của một chương trình. Nó xác định cấu trúc account, instructions và error của Solana program. IDLs là các tệp .json được sử dụng để viết code client tương tác với một Solana program.
Vậy điểm khác nhau giữa việc thực hiện CPI giữa program được viết bằng anchor framework và native rust có gì khác nhau và cần lưu ý gì khi thực hiện CPI ?
CPI với anchor (Logic1 program)
Có thể bạn đã nghe đến discriminator trong anchor. Vậy nó thực sự là cái gì ?
Chính xác discriminator được sử dụng để xác định xem hàm nào sẽ được gọi đến trong một program được build bằng anchor. Discriminator được cắt ra từ 8 byte đầu của mã hash Sha256 của string "global:<NameOfFunction>"
Ví dụ, trong chương trình logic1 có hàm vote(). Làm sao để workflow program gọi tới hàm vote trong logic 1 program mà không xảy ra lỗi ? Để làm được điều này, ở trong trường data của instruction, ta phải khởi tạo một vector trong đó 8 bytes đầu được dùng để định nghĩa discriminator.
Step 1: using sha256 for string "global:vote" and you will get the result:
e36e9b17887eac197678a3a9928f2dfc8a1d553a698244524539ebb858a2b4d0
Step 2: Take the first 16 letters and convert hex to 8 bytes array
e36e9b17887eac19 => [227, 110, 155, 23, 136, 126, 172, 25]
Tuy nhiên đó mới chỉ là discriminator, bạn cần chú ý thêm định dạng bạn muốn gửi từ client. Trong ví dụ của tôi, client muốn gửi một struct{number1:1, number2:7}
. Chúng ta biến đổi struct trên thành một mảng byte. Mảng byte này có độ dài là 2. Bạn có thể tìm thấy đoạn code này ở trong folder test của workflow program, trong workflow.ts
file, nhìn vào comment //test invoke from anchor program to anchor program(logic1)
. Để hàm vote trong logic1 program có thể nhận data này, bạn sẽ cần thêm 4 bytes để khai báo thông tin độ dài mảng bytes của struct muốn gửi, sau đó mới tới mảng bytes của struct cần gửi. Bạn có thể tìm hiểu thêm về cách phân bổ space khi gửi các định dạng data khác tới một chương trình anchor tại đây: https://book.anchor-lang.com/anchor_references/space.html
Như vậy khi thực hiện CPI chúng ta có thể thấy instruction data chúng ta đã gửi trên solana explorer có dạng như sau:
Như bạn đã thấy để thực hiện một lệnh gọi tới bất kỳ chương trình build bởi anchor nào, chúng ta sẽ phải tuân theo các quy tắc khai báo cấu trúc dữ liệu của anchor. Nếu không, chúng ta sẽ ngay lập tức nhận được một lỗi.
CPI với native rust (Logic2 program)
Đối với native rust thì chúng ta có thể gọi tới một chương trình khác mà không cần bất kì lưu ý nào. Các bạn có thể xem hàm vote2() trong workflow program và logic2 program để so sánh. Tuy nhiên để có thể xử lý và điều hướng đến đúng fuction theo mục đích của instruction muốn gửi, chúng ta có thể thêm trường variant vào struct gửi đi từ client, sau đó program A sẽ gửi struct này tới program B. Khi đó bên program B sẽ cần xử lý variant để điều hướng tới function mà instruction mong muốn.
Summary
Tóm lại khi thực hiện CPI từ program A tới một function ở program B được built bằng anchor, chúng ta cần chú ý tới việc khai báo discriminator và format data theo các quy tắc về space mà anchor đưa ra.
Nếu bạn có bất cứ thắc mắc nào, xin vui lòng liên hệ @trung1712000 để được giải đáp nhé. Chúc các bạn nhanh chóng làm chủ công nghệ Solana!