diff --git a/.gitignore b/.gitignore index 51dac8258..826726f36 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,6 @@ perftool/plots/*.svg riscv-testdata/testdata/*.o riscv-testdata/testdata/*.s riscv-testdata/testdata/*.i + +# remove node modules +node_modules/ diff --git a/book/token-wallet-example/book.toml b/book/token-wallet-example/book.toml new file mode 100644 index 000000000..edf17714b --- /dev/null +++ b/book/token-wallet-example/book.toml @@ -0,0 +1,6 @@ +[book] +authors = ["Mozak team"] +language = "en" +multilingual = false +src = "src" +title = "wallet-token-example" diff --git a/book/token-wallet-example/src/CPC.md b/book/token-wallet-example/src/CPC.md new file mode 100644 index 000000000..73fd1a073 --- /dev/null +++ b/book/token-wallet-example/src/CPC.md @@ -0,0 +1,95 @@ +## Cross-Program Communication with `call_send` and `call_receive` +The SDK offers two functions, `call_send` and `call_receive`, that help us to emulate communication between token and wallet program. + +### Understanding `call_send` +`call_send` allows a program (the caller) to invoke a function from another program (the callee). +```rust +pub fn call_send( + caller_prog: ProgramIdentifier, + callee_prog: ProgramIdentifier, + call_args: A, + dispatch_native: impl Fn(A) -> R, +) -> R +where + A: CallArgument, + R: CallReturn, {} +``` +To the developer, it behaves in same way as the code line +```rust +let output = dispatch_native(call_args) +``` +While behind the scenes, during the "dry run", that is, the native execution, `call_send` would create a `CPCMessage` structure. This structure captures information about the call, including the caller and callee program identifiers, the function arguments, and the expected return value. + +```rust +pub struct CPCMessage { + pub caller_prog: ProgramIdentifier, + pub callee_prog: ProgramIdentifier, + pub call_args: RawMessage, + // return value of `dispatch_native(call_args)` + pub ret: RawMessage, +} +``` +This structure would be appended to `CallTape`, essentially recording the dialogue between the programs as defined by the script. + +During the zkvm execution, `call_send` essentially reads off from the correct `CPCMessage` from the `CallTape` and extracts the `ret` value. This corresponds to initiating the dialogue, and acknowledging that it has received the response. + +### Usage in requesting wallet for approval + +Consider a scenario where a `usdc_token` program needs permission from a `alice_wallet` program to transfer USDC tokens to bob_wallet. Here's how `call_send` comes into play: + +```rust + +// Arguments for `approve_transfer` function in `alice_wallet` +let approve_transfer_args = wallet::MethodArgs(token_object, remitter_wallet, remittee_wallet); + +// Function to call in `alice_wallet` +let approve_function = wallet::approve_transfer; + +// `usdc_token` calls `alice_wallet` to request approval +let approval = call_send( + usdc_token, + alice_wallet, + approve_transfer_args, + approve_function +); +``` +This example demonstrates how `usdc_token` uses `call_send` to invoke the `approve_transfer` function within `alice_wallet`, passing the necessary arguments and receiving the approval status. + +### Understanding `call_receive` + +The `call_receive` function complements `call_send` by enabling the callee program to receive and process the call initiated by the caller. + +```rust +pub fn call_receive() -> Option<(CPCMessage, usize)> +``` + +During native execution, `call_receive` acts as a placeholder, returning `None` since the corresponding `call_send` is assumed to have already recorded the dialougue. + +During ZKVM execution, the program provably reads the relevant `CPCMessage` from the `CallTape`, extracting the call details and arguments. Based on the extracted information, the program executes the designated function with the provided arguments. That is, provably showing that it is following the instructions mentioned in the dialouge. + +### Usage in receiving the request to approve + +Continuing from the token transfer scenario, here's how `alice_wallet` uses `call_receive` to handle the request from `usdc_token`: + + +```rust +// `alice_wallet` receives request for approval +// from `usdc_token_program` +let Some(message_from_token_program) = call_receive(); + +// the request is decoded +let CPCMessage { + caller_program: token_program, + callee_program: wallet_program, + approve_transfer_args, + approval, +} = message_from_token_program.0; + +// the arguments to `approve_transfer` call are +// prepared +let MethodArgs::ApproveTransfer(token_object, alice_wallet, bob_wallet) = approve_call_args; + +// the wallet calls its internal function +// to approve the transfer +approve_transfer(token_object, alice_wallet, bob_wallet); +``` diff --git a/book/token-wallet-example/src/SUMMARY.md b/book/token-wallet-example/src/SUMMARY.md new file mode 100644 index 000000000..6ee920801 --- /dev/null +++ b/book/token-wallet-example/src/SUMMARY.md @@ -0,0 +1,4 @@ +# Summary + +- [High level overview](./high_level_overview.md) +- [Cross Program Calls](./CPC.md) diff --git a/book/token-wallet-example/src/high_level_overview.md b/book/token-wallet-example/src/high_level_overview.md new file mode 100644 index 000000000..5ae76e967 --- /dev/null +++ b/book/token-wallet-example/src/high_level_overview.md @@ -0,0 +1,67 @@ +## High-Level Overview of a Transaction + + +**Token Wallet Example:** + +Imagine Alice owns USDC tokens in her wallet. She wants to transfer them to Bob's wallet. This transfer is done by the USDC token program, which can only modify USDC tokens after receiving approval from the owner's wallet program (Alice's in this case). + +**Program-Level Breakdown:** + +A USDC token is represented by a `StateObject` with a unique owner (`USDC token program`) and details like Alice's ownership and wallet program. The USDC program requires approval from Alice's wallet program to transfer ownership to Bob and his wallet program. + +```rust +let alice_wallet: ProgramIdentifier; +let bob_wallet: ProgramIdentifier; + +let usdc_token_data = USDCTokenData { + usdc_token_owner: ALICE_PUBLIC_KEY, + wallet_program: alice_wallet, + amount: 100, +}; + +let usdc_token_program: ProgramIdentifier; + +let usdc_token_object = StateObject { + address: address_of_object_in_state_tree, + constraint_owner: usdc_token_program, + data: usdc_token_data.to_bytes(), +}; +``` + +At a high level, the program performs the following: + +1. `usdc_token_program` verifies Alice owns `usdc_token_object`. +2. It requests `alice_wallet` to "call" a function approving the transfer of `usdc_token_object` to `bob_wallet`. +3. `alice_wallet` "receives the request" and calls its approval function. +4. The approval function proves Alice possesses the pre-image hash of the public key (essentially, her private key) for `usdc_token_object`. This proof is provided as private input to the wallet program. +5. `usdc_token_program` receives the approval. +6. It then "proposes a change" to update `usdc_token_object`'s owner and wallet to Bob's. + +Each program generates a zero-knowledge proof of its execution within zkVM. However, this isn't enough! The proof needs to capture the communication and interaction with the global state between programs. + +**Cross-Program Communication (CPC):** + +Programs don't actually "call" or "receive responses" in the traditional sense. Instead, they demonstrate following a predetermined script defining their interactions. This script, called `CallTape`, ensures both programs adhere to the agreed-upon communication protocol. Our proof system verifies the script's execution. + +For example, when the token program requests approval from Alice's wallet program, `CallTape` would record the following event: + +- `usdc_token_program` called `alice_wallet` to execute `approve_transfer` with arguments `(usdc_token_object, bob_wallet)`. +- `alice_wallet` approved the transfer, returning `true`. + +**Provable State Interaction:** + +Similar to `CallTape`, a `EventTape` stores all reads and writes a program performs on the global state. The program's proof also verifies the events on `EventTape` that it claims to have emitted. + +For instance, when the `usdc_token_program` wants to transfer ownership of `usdc_token_object` to Bob, `EventTape` would record: + +- `usdc_token_program` proposed an update to `usdc_token_object`'s `owner` field to `BOB_PUBLIC_KEY` and `wallet` field to `bob_wallet`. + +**Script Generation:** + +The script is essentially created by performing a "dry run" of all programs, mimicking their regular execution as Rust programs. This is called **Native Execution**. During this execution, the script is generated, and all cross-program calls and global state interactions are recorded in `CallTape` and `EventTape`, respectively. + +**Following the Script:** + +We have another execution environment called **ZKVM Execution** for generating proofs. After preparing the tapes, programs are run individually on zkVM. When a program needs to communicate with another, it reads from `CallTape` and provably accesses the agreed-upon response. It then continues its execution based on the response. + +**TODO:** Transaction Bundle