mirror of
https://github.com/logos-blockchain/lssa.git
synced 2026-01-02 13:23:10 +00:00
Merge branch 'main' into Pravdyvy/tree-ux-updates
This commit is contained in:
commit
d0064a4b0b
@ -46,6 +46,7 @@ hmac-sha512 = "1.1.7"
|
||||
chrono = "0.4.41"
|
||||
borsh = "1.5.7"
|
||||
base58 = "0.2.0"
|
||||
itertools = "0.14.0"
|
||||
|
||||
rocksdb = { version = "0.21.0", default-features = false, features = [
|
||||
"snappy",
|
||||
|
||||
204
README.md
204
README.md
@ -4,9 +4,9 @@ Nescience State Separation Architecture (NSSA) is a programmable blockchain syst
|
||||
|
||||
## Background
|
||||
|
||||
Typically, public blockchains maintain a fully transparent state, where the mapping from addresses to account values is entirely visible. In NSSA, we introduce a parallel *private state*, a new layer of accounts that coexists with the public one. The public and private states can be viewed as a partition of the address space: accounts with public addresses are openly visible, while private accounts are accessible only to holders of the corresponding viewing keys. Consistency across both states is enforced through zero-knowledge proofs (ZKPs).
|
||||
Typically, public blockchains maintain a fully transparent state, where the mapping from account IDs to account values is entirely visible. In NSSA, we introduce a parallel *private state*, a new layer of accounts that coexists with the public one. The public and private states can be viewed as a partition of the account ID space: accounts with public IDs are openly visible, while private accounts are accessible only to holders of the corresponding viewing keys. Consistency across both states is enforced through zero-knowledge proofs (ZKPs).
|
||||
|
||||
Public accounts are represented on-chain as a visible map from addresses to account states and are modified in-place when their values change. Private accounts, by contrast, are never stored in raw form on-chain. Each update creates a new commitment, which cryptographically binds the current value of the account while preserving privacy. Commitments of previous valid versions remain on-chain, but a nullifier set is maintained to mark old versions as spent, ensuring that only the most up-to-date version of each private account can be used in any execution.
|
||||
Public accounts are represented on-chain as a visible map from IDs to account states and are modified in-place when their values change. Private accounts, by contrast, are never stored in raw form on-chain. Each update creates a new commitment, which cryptographically binds the current value of the account while preserving privacy. Commitments of previous valid versions remain on-chain, but a nullifier set is maintained to mark old versions as spent, ensuring that only the most up-to-date version of each private account can be used in any execution.
|
||||
|
||||
### Programmability and selective privacy
|
||||
|
||||
@ -65,10 +65,20 @@ Both public and private executions of the same program are enforced to use the s
|
||||
|
||||
# Install dependencies
|
||||
Install build dependencies
|
||||
|
||||
- On Linux
|
||||
Ubuntu / Debian
|
||||
```sh
|
||||
apt install build-essential clang libssl-dev pkg-config
|
||||
```
|
||||
|
||||
Fedora
|
||||
```sh
|
||||
sudo dnf install clang openssl-devel pkgconf llvm
|
||||
```
|
||||
|
||||
> **Note for Fedora 41+ users:** GCC 14+ has stricter C++ standard library headers that cause build failures with the bundled RocksDB. You must set `CXXFLAGS="-include cstdint"` when running cargo commands. See the [Run tests](#run-tests) section for examples.
|
||||
|
||||
- On Mac
|
||||
```sh
|
||||
xcode-select --install
|
||||
@ -99,7 +109,10 @@ The NSSA repository includes both unit and integration test suites.
|
||||
|
||||
```bash
|
||||
# RISC0_DEV_MODE=1 is used to skip proof generation and reduce test runtime overhead
|
||||
RISC0_DEV_MODE=1 cargo test --release
|
||||
RISC0_DEV_MODE=1 cargo test --release
|
||||
|
||||
# On Fedora 41+ (GCC 14+), prefix with CXXFLAGS to fix RocksDB build:
|
||||
CXXFLAGS="-include cstdint" RISC0_DEV_MODE=1 cargo test --release
|
||||
```
|
||||
|
||||
### Integration tests
|
||||
@ -109,6 +122,9 @@ export NSSA_WALLET_HOME_DIR=$(pwd)/integration_tests/configs/debug/wallet/
|
||||
cd integration_tests
|
||||
# RISC0_DEV_MODE=1 skips proof generation; RUST_LOG=info enables runtime logs
|
||||
RUST_LOG=info RISC0_DEV_MODE=1 cargo run $(pwd)/configs/debug all
|
||||
|
||||
# On Fedora 41+ (GCC 14+), prefix with CXXFLAGS to fix RocksDB build:
|
||||
CXXFLAGS="-include cstdint" RUST_LOG=info RISC0_DEV_MODE=1 cargo run $(pwd)/configs/debug all
|
||||
```
|
||||
|
||||
# Run the sequencer
|
||||
@ -118,6 +134,9 @@ The sequencer can be run locally:
|
||||
```bash
|
||||
cd sequencer_runner
|
||||
RUST_LOG=info cargo run --release configs/debug
|
||||
|
||||
# On Fedora 41+ (GCC 14+), prefix with CXXFLAGS to fix RocksDB build:
|
||||
CXXFLAGS="-include cstdint" RUST_LOG=info cargo run --release configs/debug
|
||||
```
|
||||
|
||||
If everything went well you should see an output similar to this:
|
||||
@ -138,19 +157,32 @@ If everything went well you should see an output similar to this:
|
||||
# Try the Wallet CLI
|
||||
|
||||
## Install
|
||||
|
||||
This repository includes a CLI for interacting with the Nescience sequencer. To install it, run the following command from the root of the repository:
|
||||
|
||||
```bash
|
||||
cargo install --path wallet --force
|
||||
```
|
||||
Before using the CLI, set the environment variable `NSSA_WALLET_HOME_DIR` to the directory containing the wallet configuration file. A sample configuration is available at `integration_tests/configs/debug/wallet/`. To use it, run:
|
||||
|
||||
```bash
|
||||
export NSSA_WALLET_HOME_DIR=$(pwd)/integration_tests/configs/debug/wallet/
|
||||
# On Fedora 41+ (GCC 14+), prefix with CXXFLAGS to fix RocksDB build:
|
||||
CXXFLAGS="-include cstdint" cargo install --path wallet --force
|
||||
```
|
||||
|
||||
Run `wallet help` to check everything went well.
|
||||
|
||||
## Tutorial
|
||||
|
||||
This tutorial walks you through creating accounts and executing NSSA programs in both public and private contexts.
|
||||
|
||||
> [!NOTE]
|
||||
> The NSSA state is split into two separate but interconnected components: the public state and the private state.
|
||||
> The public state is an on-chain, publicly visible record of accounts indexed by their Account IDs
|
||||
> The private state mirrors this, but the actual account values are stored locally by each account owner. On-chain, only a hidden commitment to each private account state is recorded. This allows the chain to enforce freshness (i.e., prevent the reuse of stale private states) while preserving privacy and unlinkability across executions and private accounts.
|
||||
>
|
||||
> Every piece of state in NSSA is stored in an account (public or private). Accounts are either uninitialized or are owned by a program, and programs can only modify the accounts they own.
|
||||
>
|
||||
> In NSSA, accounts can only be modified through program execution. A program is the sole mechanism that can change an account’s value.
|
||||
> Programs run publicly when all involved accounts are public, and privately when at least one private account participates.
|
||||
|
||||
### Health-check
|
||||
|
||||
Verify that the node is running and that the wallet can connect to it:
|
||||
@ -177,7 +209,10 @@ Commands:
|
||||
|
||||
### Accounts
|
||||
|
||||
Every piece of state in NSSA is stored in an account. The CLI provides commands to manage accounts. Run `wallet account` to see the options available:
|
||||
> [!NOTE]
|
||||
> Accounts are the basic unit of state in NSSA. They essentially hold native tokens and arbitrary data managed by some program.
|
||||
|
||||
The CLI provides commands to manage accounts. Run `wallet account` to see the options available:
|
||||
```bash
|
||||
Commands:
|
||||
get Get account data
|
||||
@ -194,24 +229,32 @@ You can create both public and private accounts through the CLI. For example:
|
||||
wallet account new public
|
||||
|
||||
# Output:
|
||||
Generated new account with addr Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ
|
||||
Generated new account with account_id Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ
|
||||
```
|
||||
|
||||
This address is required when executing any program that interacts with the account.
|
||||
This id is required when executing any program that interacts with the account.
|
||||
|
||||
> [!NOTE]
|
||||
> Public accounts live on-chain and are identified by a 32-byte Account ID.
|
||||
> Running `wallet account new public` generates a fresh keypair for the signature scheme used in NSSA.
|
||||
> The account ID is derived from the public key. The private key is used to sign transactions and to authorize the account in program executions.
|
||||
|
||||
#### Account initialization
|
||||
|
||||
To query the account’s current status, run:
|
||||
|
||||
```bash
|
||||
# Replace the address with yours
|
||||
wallet account get --addr Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ
|
||||
# Replace the id with yours
|
||||
wallet account get --account-id Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ
|
||||
|
||||
# Output:
|
||||
Account is Uninitialized
|
||||
```
|
||||
|
||||
New accounts start as uninitialized, meaning no program owns them yet. Programs can claim uninitialized accounts; once claimed, the account becomes permanently owned by that program.
|
||||
> [!NOTE]
|
||||
> New accounts begin in an uninitialized state, meaning they are not yet owned by any program. A program may claim an uninitialized account; once claimed, the account becomes owned by that program.
|
||||
> Owned accounts can only be modified through executions of the owning program. The only exception is native-token credits: any program may credit native tokens to any account.
|
||||
> However, debiting native tokens from an account must always be performed by its owning program.
|
||||
|
||||
In this example, we will initialize the account for the Authenticated transfer program, which securely manages native token transfers by requiring authentication for debits.
|
||||
|
||||
@ -221,13 +264,13 @@ Initialize the account by running:
|
||||
# This command submits a public transaction executing the `init` function of the
|
||||
# Authenticated-transfer program. The wallet polls the sequencer until the
|
||||
# transaction is included in a block, which may take several seconds.
|
||||
wallet auth-transfer init --addr Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ
|
||||
wallet auth-transfer init --account-id Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ
|
||||
```
|
||||
|
||||
After it completes, check the updated account status:
|
||||
|
||||
```bash
|
||||
wallet account get --addr Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ
|
||||
wallet account get --account-id Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ
|
||||
|
||||
# Output:
|
||||
Account owned by authenticated transfer program
|
||||
@ -236,17 +279,17 @@ Account owned by authenticated transfer program
|
||||
|
||||
### Funding the account: executing the Piñata program
|
||||
|
||||
Now that we have a public account initialized by the authenticated transfer program, we need to fund it. For that, the testnet provides the Piñata program. See the [Pinata](#piñata-program) section for instructions on how to use it.
|
||||
Now that we have a public account initialized by the authenticated transfer program, we need to fund it. For that, the testnet provides the Piñata program.
|
||||
|
||||
```bash
|
||||
# Complete with your address and the correct solution for your case
|
||||
wallet pinata claim --to-addr Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ --solution 989106
|
||||
# Complete with your id
|
||||
wallet pinata claim --to Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ
|
||||
```
|
||||
|
||||
After the claim succeeds, the account will be funded with some tokens:
|
||||
|
||||
```bash
|
||||
wallet account get --addr Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ
|
||||
wallet account get --account-id Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ
|
||||
|
||||
# Output:
|
||||
Account owned by authenticated transfer program
|
||||
@ -270,10 +313,12 @@ Let's try it. For that we need to create another account for the recipient of th
|
||||
wallet account new public
|
||||
|
||||
# Output:
|
||||
Generated new account with addr Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS
|
||||
Generated new account with account_id Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS
|
||||
```
|
||||
|
||||
The new account is uninitialized. The authenticated transfers program will claim any uninitialized account used in a transfer. So we don't need to manually initialize the recipient account.
|
||||
|
||||
> [!NOTE]
|
||||
> The new account is uninitialized. The authenticated transfers program will claim any uninitialized account used in a transfer. So we don't need to manually initialize the recipient account.
|
||||
|
||||
Let's send 37 tokens to the new account.
|
||||
|
||||
@ -288,7 +333,7 @@ Once that succeeds we can check the states.
|
||||
|
||||
```bash
|
||||
# Sender account
|
||||
wallet account get --addr Public/HrA8TVjBS8UVf9akV7LRhyh6k4c7F6PS7PvqgtPmKAT8
|
||||
wallet account get --account-id Public/HrA8TVjBS8UVf9akV7LRhyh6k4c7F6PS7PvqgtPmKAT8
|
||||
|
||||
# Output:
|
||||
Account owned by authenticated transfer program
|
||||
@ -297,7 +342,7 @@ Account owned by authenticated transfer program
|
||||
|
||||
```bash
|
||||
# Recipient account
|
||||
wallet account get --addr Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS
|
||||
wallet account get --account-id Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS
|
||||
|
||||
# Output:
|
||||
Account owned by authenticated transfer program
|
||||
@ -306,24 +351,38 @@ Account owned by authenticated transfer program
|
||||
|
||||
#### Create a new private account
|
||||
|
||||
> [!NOTE]
|
||||
> Private accounts are structurally identical to public accounts; they differ only in how their state is stored off-chain and represented on-chain.
|
||||
> The raw values of a private account are never stored on-chain. Instead, the chain only holds a 32-byte commitment (a hash-like binding to the actual values). Transactions include encrypted versions of the private values so that users can recover them from the blockchain. The decryption keys are known only to the user and are never shared.
|
||||
> Private accounts are not managed through the usual signature mechanism used for public accounts. Instead, each private account is associated with two keypairs:
|
||||
> - *Nullifier keys*, for using the corresponding private account in privacy preserving executions.
|
||||
> - *Viewing keys*, used for encrypting and decrypting the values included in transactions.
|
||||
>
|
||||
> Private accounts also have a 32-byte identifier, derived from the nullifier public key.
|
||||
>
|
||||
> Just like public accounts, private accounts can only be initialized once. Any user can initialize them without knowing the owner's secret keys. However, modifying an initialized private account through an off-chain program execution requires knowledge of the owner’s secret keys.
|
||||
>
|
||||
> Transactions that modify the values of a private account include a commitment to the new values, which will be added to the on-chain commitment set. They also include a nullifier that marks the previous version as old.
|
||||
> The nullifier is constructed so that it cannot be linked to any prior commitment, ensuring that updates to the same private account cannot be correlated.
|
||||
|
||||
Now let’s switch to the private state and create a private account.
|
||||
|
||||
```bash
|
||||
wallet account new private
|
||||
|
||||
# Output:
|
||||
Generated new account with addr Private/HacPU3hakLYzWtSqUPw6TUr8fqoMieVWovsUR6sJf7cL
|
||||
Generated new account with account_id Private/HacPU3hakLYzWtSqUPw6TUr8fqoMieVWovsUR6sJf7cL
|
||||
With npk e6366f79d026c8bd64ae6b3d601f0506832ec682ab54897f205fffe64ec0d951
|
||||
With ipk 02ddc96d0eb56e00ce14994cfdaec5ae1f76244180a919545983156e3519940a17
|
||||
```
|
||||
|
||||
For now, focus only on the account address. Ignore the `npk` and `ipk` values. These are stored locally in the wallet and are used internally to build privacy-preserving transactions.
|
||||
Also, the account id for private accounts is derived from the `npk` and `ipk` values. But we won't need them now.
|
||||
For now, focus only on the account id. Ignore the `npk` and `ipk` values. These are the Nullifier public key and the Viewing public key. They are stored locally in the wallet and are used internally to build privacy-preserving transactions.
|
||||
Also, the account id for private accounts is derived from the `npk` value. But we won't need them now.
|
||||
|
||||
Just like public accounts, new private accounts start out uninitialized:
|
||||
|
||||
```bash
|
||||
wallet account get --addr Private/HacPU3hakLYzWtSqUPw6TUr8fqoMieVWovsUR6sJf7cL
|
||||
wallet account get --account-id Private/HacPU3hakLYzWtSqUPw6TUr8fqoMieVWovsUR6sJf7cL
|
||||
|
||||
# Output:
|
||||
Account is Uninitialized
|
||||
@ -337,7 +396,7 @@ This happens because program execution logic does not depend on whether the invo
|
||||
|
||||
Let’s send 17 tokens to the new private account.
|
||||
|
||||
The syntax is identical to the public-to-public transfer; just set the private address as the recipient.
|
||||
The syntax is identical to the public-to-public transfer; just set the private ID as the recipient.
|
||||
|
||||
This command will run the Authenticated-Transfer program locally, generate a proof, and submit it to the sequencer. Depending on your machine, this can take from 30 seconds to 4 minutes.
|
||||
|
||||
@ -352,7 +411,7 @@ After it succeeds, check both accounts:
|
||||
|
||||
```bash
|
||||
# Public sender account
|
||||
wallet account get --addr Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS
|
||||
wallet account get --account-id Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS
|
||||
|
||||
# Output:
|
||||
Account owned by authenticated transfer program
|
||||
@ -361,15 +420,16 @@ Account owned by authenticated transfer program
|
||||
|
||||
```bash
|
||||
# Private recipient account
|
||||
wallet account get --addr Private/HacPU3hakLYzWtSqUPw6TUr8fqoMieVWovsUR6sJf7cL
|
||||
wallet account get --account-id Private/HacPU3hakLYzWtSqUPw6TUr8fqoMieVWovsUR6sJf7cL
|
||||
|
||||
# Output:
|
||||
Account owned by authenticated transfer program
|
||||
{"balance":17}
|
||||
```
|
||||
|
||||
Note: the last command does not query the network.
|
||||
It works even offline because private account data lives only in your wallet storage. Other users cannot read your private balances.
|
||||
> [!NOTE]
|
||||
> The last command does not query the network.
|
||||
> It works even offline because private account data lives only in your wallet storage. Other users cannot read your private balances.
|
||||
|
||||
#### Digression: modifying private accounts
|
||||
|
||||
@ -388,12 +448,12 @@ Let's create a new (uninitialized) private account like before:
|
||||
wallet account new private
|
||||
|
||||
# Output:
|
||||
Generated new account with addr Private/AukXPRBmrYVqoqEW2HTs7N3hvTn3qdNFDcxDHVr5hMm5
|
||||
Generated new account with account_id Private/AukXPRBmrYVqoqEW2HTs7N3hvTn3qdNFDcxDHVr5hMm5
|
||||
With npk 0c95ebc4b3830f53da77bb0b80a276a776cdcf6410932acc718dcdb3f788a00e
|
||||
With ipk 039fd12a3674a880d3e917804129141e4170d419d1f9e28a3dcf979c1f2369cb72
|
||||
```
|
||||
|
||||
Now we'll ignore the private account address and focus on the `npk` and `ipk` values. We'll need this to send tokens to a foreign private account. Syntax is very similar.
|
||||
Now we'll ignore the private account ID and focus on the `npk` and `ipk` values. We'll need this to send tokens to a foreign private account. Syntax is very similar.
|
||||
|
||||
```bash
|
||||
wallet auth-transfer send \
|
||||
@ -415,6 +475,10 @@ We’ve shown how to use the authenticated-transfers program for transfers betwe
|
||||
### The token program
|
||||
|
||||
So far, we’ve made transfers using the authenticated-transfers program, which handles native token transfers. The Token program, on the other hand, is used for creating and managing custom tokens.
|
||||
|
||||
> [!NOTE]
|
||||
> The token program is a single program responsible for creating and managing all tokens. There is no need to deploy new programs to introduce new tokens. All token-related operations are performed by invoking the appropriate functions of the token program.
|
||||
|
||||
The CLI provides commands to execute the token program. To see the options available run `wallet token`:
|
||||
|
||||
```bash
|
||||
@ -424,9 +488,11 @@ Commands:
|
||||
help Print this message or the help of the given subcommand(s)
|
||||
```
|
||||
|
||||
The Token program manages its accounts in two categories. Meaning, all accounts owned by the Token program fall into one of these types.
|
||||
- Token definition accounts: these accounts store metadata about a token, such as its name, total supply, and other identifying properties. They act as the token’s unique identifier.
|
||||
- Token holding accounts: these accounts hold actual token balances. In addition to the balance, they also record which token definition they belong to.
|
||||
|
||||
> [!NOTE]
|
||||
> The Token program manages its accounts in two categories. Meaning, all accounts owned by the Token program fall into one of these types.
|
||||
> - Token definition accounts: these accounts store metadata about a token, such as its name, total supply, and other identifying properties. They act as the token’s unique identifier.
|
||||
> - Token holding accounts: these accounts hold actual token balances. In addition to the balance, they also record which token definition they belong to.
|
||||
|
||||
#### Creating a new token
|
||||
|
||||
@ -444,14 +510,14 @@ For example, let's create two new (uninitialized) public accounts and then use t
|
||||
wallet account new public
|
||||
|
||||
# Output:
|
||||
Generated new account with addr Public/4X9kAcnCZ1Ukkbm3nywW9xfCNPK8XaMWCk3zfs1sP4J7
|
||||
Generated new account with account_id Public/4X9kAcnCZ1Ukkbm3nywW9xfCNPK8XaMWCk3zfs1sP4J7
|
||||
```
|
||||
|
||||
```bash
|
||||
wallet account new public
|
||||
|
||||
# Output:
|
||||
Generated new account with addr Public/9RRSMm3w99uCD2Jp2Mqqf6dfc8me2tkFRE9HeU2DFftw
|
||||
Generated new account with account_id Public/9RRSMm3w99uCD2Jp2Mqqf6dfc8me2tkFRE9HeU2DFftw
|
||||
```
|
||||
|
||||
Now we use them to create a new token. Let's call it the "Token A"
|
||||
@ -460,14 +526,14 @@ Now we use them to create a new token. Let's call it the "Token A"
|
||||
wallet token new \
|
||||
--name TOKENA \
|
||||
--total-supply 1337 \
|
||||
--definition-addr Public/4X9kAcnCZ1Ukkbm3nywW9xfCNPK8XaMWCk3zfs1sP4J7 \
|
||||
--supply-addr Public/9RRSMm3w99uCD2Jp2Mqqf6dfc8me2tkFRE9HeU2DFftw
|
||||
--definition-account-id Public/4X9kAcnCZ1Ukkbm3nywW9xfCNPK8XaMWCk3zfs1sP4J7 \
|
||||
--supply-account-id Public/9RRSMm3w99uCD2Jp2Mqqf6dfc8me2tkFRE9HeU2DFftw
|
||||
```
|
||||
|
||||
After it succeeds, we can inspect the two accounts to see how they were initialized.
|
||||
|
||||
```bash
|
||||
wallet account get --addr Public/4X9kAcnCZ1Ukkbm3nywW9xfCNPK8XaMWCk3zfs1sP4J7
|
||||
wallet account get --account-id Public/4X9kAcnCZ1Ukkbm3nywW9xfCNPK8XaMWCk3zfs1sP4J7
|
||||
|
||||
# Output:
|
||||
Definition account owned by token program
|
||||
@ -475,7 +541,7 @@ Definition account owned by token program
|
||||
```
|
||||
|
||||
```bash
|
||||
wallet account get --addr Public/9RRSMm3w99uCD2Jp2Mqqf6dfc8me2tkFRE9HeU2DFftw
|
||||
wallet account get --account-id Public/9RRSMm3w99uCD2Jp2Mqqf6dfc8me2tkFRE9HeU2DFftw
|
||||
|
||||
# Output:
|
||||
Holding account owned by token program
|
||||
@ -492,7 +558,7 @@ Since we can’t reuse the accounts from the previous example, we need to create
|
||||
wallet account new public
|
||||
|
||||
# Output:
|
||||
Generated new account with addr Public/GQ3C8rbprTtQUCvkuVBRu3v9wvUvjafCMFqoSPvTEVii
|
||||
Generated new account with account_id Public/GQ3C8rbprTtQUCvkuVBRu3v9wvUvjafCMFqoSPvTEVii
|
||||
```
|
||||
|
||||
```bash
|
||||
@ -500,7 +566,7 @@ wallet account new private
|
||||
|
||||
|
||||
# Output:
|
||||
Generated new account with addr Private/HMRHZdPw4pbyPVZHNGrV6K5AA95wACFsHTRST84fr3CF
|
||||
Generated new account with account_id Private/HMRHZdPw4pbyPVZHNGrV6K5AA95wACFsHTRST84fr3CF
|
||||
With npk 6a2dfe433cf28e525aa0196d719be3c16146f7ee358ca39595323f94fde38f93
|
||||
With ipk 03d59abf4bee974cc12ddb44641c19f0b5441fef39191f047c988c29a77252a577
|
||||
```
|
||||
@ -513,15 +579,14 @@ Now we use them to create a new token. Let's call it "Token B".
|
||||
wallet token new \
|
||||
--name TOKENB \
|
||||
--total-supply 7331 \
|
||||
--definition-addr Public/GQ3C8rbprTtQUCvkuVBRu3v9wvUvjafCMFqoSPvTEVii \
|
||||
--supply-addr Private/HMRHZdPw4pbyPVZHNGrV6K5AA95wACFsHTRST84fr3CF
|
||||
--definition-account-id Public/GQ3C8rbprTtQUCvkuVBRu3v9wvUvjafCMFqoSPvTEVii \
|
||||
--supply-account-id Private/HMRHZdPw4pbyPVZHNGrV6K5AA95wACFsHTRST84fr3CF
|
||||
```
|
||||
|
||||
After it succeeds, we can check their values
|
||||
|
||||
|
||||
```bash
|
||||
wallet account get --addr Public/GQ3C8rbprTtQUCvkuVBRu3v9wvUvjafCMFqoSPvTEVii
|
||||
wallet account get --account-id Public/GQ3C8rbprTtQUCvkuVBRu3v9wvUvjafCMFqoSPvTEVii
|
||||
|
||||
# Output:
|
||||
Definition account owned by token program
|
||||
@ -529,7 +594,7 @@ Definition account owned by token program
|
||||
```
|
||||
|
||||
```bash
|
||||
wallet account get --addr Private/HMRHZdPw4pbyPVZHNGrV6K5AA95wACFsHTRST84fr3CF
|
||||
wallet account get --account-id Private/HMRHZdPw4pbyPVZHNGrV6K5AA95wACFsHTRST84fr3CF
|
||||
|
||||
# Output:
|
||||
Holding account owned by token program
|
||||
@ -550,7 +615,7 @@ Let's create a new public account for the recipient.
|
||||
wallet account new public
|
||||
|
||||
# Output:
|
||||
Generated new account with addr Public/88f2zeTgiv9LUthQwPJbrmufb9SiDfmpCs47B7vw6Gd6
|
||||
Generated new account with account_id Public/88f2zeTgiv9LUthQwPJbrmufb9SiDfmpCs47B7vw6Gd6
|
||||
```
|
||||
|
||||
Let's send 10 B tokens to this new account. We'll debit this from the supply account used in the creation of the token.
|
||||
@ -565,48 +630,13 @@ wallet token send \
|
||||
Let's inspect the public account:
|
||||
|
||||
```bash
|
||||
wallet account get --addr Public/88f2zeTgiv9LUthQwPJbrmufb9SiDfmpCs47B7vw6Gd6
|
||||
wallet account get --account-id Public/88f2zeTgiv9LUthQwPJbrmufb9SiDfmpCs47B7vw6Gd6
|
||||
|
||||
# Output:
|
||||
Holding account owned by token program
|
||||
{"account_type":"Token holding","definition_id":"GQ3C8rbprTtQUCvkuVBRu3v9wvUvjafCMFqoSPvTEVii","balance":10}
|
||||
```
|
||||
|
||||
### Piñata program
|
||||
|
||||
The testnet comes with a program that serves as a faucet for native tokens. We call it the Piñata. Use the command `wallet pinata claim` to get native tokens from it. This requires two parameters:
|
||||
- `--to-addr` is the address of the account that will receive the tokens. **Use only initialized accounts here.**
|
||||
- `--solution` a solution to the Pinata challenge. This will change every time the Pinata is successfully claimed.
|
||||
|
||||
To find the solution to the challenge, first query the Pinata account. This is always at the address: `Public/EfQhKQAkX2FJiwNii2WFQsGndjvF1Mzd7RuVe7QdPLw7`.
|
||||
|
||||
```bash
|
||||
wallet account get --addr Public/EfQhKQAkX2FJiwNii2WFQsGndjvF1Mzd7RuVe7QdPLw7
|
||||
|
||||
# Output:
|
||||
{"balance":750,"program_owner_b64":"/SQ9PX+NYQgXm7YMP7VMUBRwvU/Bq4pHTTZcCpTC5FM=","data_b64":"A939OBnG9OhvzOocqfCAJKSYvtcuV15OHDIVNg34MC8i","nonce":0}
|
||||
```
|
||||
|
||||
Copy the `data_b64` value and run the following python script:
|
||||
|
||||
```python
|
||||
import base64, hashlib
|
||||
|
||||
def find_16byte_prefix(data: str, max_attempts: int) -> bytes:
|
||||
data = base64.b64decode(data_b64)[1:]
|
||||
for attempt in range(max_attempts):
|
||||
suffix = attempt.to_bytes(16, 'little')
|
||||
h = hashlib.sha256(data + suffix).digest()
|
||||
if h[:3] == b"\x00\x00\x00":
|
||||
solution = int.from_bytes(suffix, 'little')
|
||||
return f"Solution: {solution}"
|
||||
raise RuntimeError(f"No suffix found in {max_attempts} attempts")
|
||||
|
||||
|
||||
data_b64 = "A939OBnG9OhvzOocqfCAJKSYvtcuV15OHDIVNg34MC8i" # <- Change with the value from the Piñata account
|
||||
print(find_16byte_prefix(data_b64, 50000000))
|
||||
```
|
||||
|
||||
### Chain information
|
||||
|
||||
The wallet provides some commands to query information about the chain. These are under the `wallet chain-info` command.
|
||||
|
||||
@ -15,6 +15,7 @@ log.workspace = true
|
||||
hex.workspace = true
|
||||
nssa-core = { path = "../nssa/core", features = ["host"] }
|
||||
borsh.workspace = true
|
||||
base64.workspace = true
|
||||
|
||||
[dependencies.nssa]
|
||||
path = "../nssa"
|
||||
|
||||
@ -62,6 +62,16 @@ pub struct Request {
|
||||
}
|
||||
|
||||
impl Request {
|
||||
pub fn from_payload_version_2_0(method: String, payload: serde_json::Value) -> Self {
|
||||
Self {
|
||||
jsonrpc: Version,
|
||||
method,
|
||||
params: payload,
|
||||
// ToDo: Correct checking of id
|
||||
id: 1.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Answer the request with a (positive) reply.
|
||||
///
|
||||
/// The ID is taken from the request.
|
||||
|
||||
@ -20,6 +20,7 @@ pub struct RegisterAccountRequest {
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct SendTxRequest {
|
||||
#[serde(with = "base64_deser")]
|
||||
pub transaction: Vec<u8>,
|
||||
}
|
||||
|
||||
@ -28,6 +29,13 @@ pub struct GetBlockDataRequest {
|
||||
pub block_id: u64,
|
||||
}
|
||||
|
||||
/// Get a range of blocks from `start_block_id` to `end_block_id` (inclusive)
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct GetBlockRangeDataRequest {
|
||||
pub start_block_id: u64,
|
||||
pub end_block_id: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct GetGenesisIdRequest {}
|
||||
|
||||
@ -69,6 +77,7 @@ parse_request!(HelloRequest);
|
||||
parse_request!(RegisterAccountRequest);
|
||||
parse_request!(SendTxRequest);
|
||||
parse_request!(GetBlockDataRequest);
|
||||
parse_request!(GetBlockRangeDataRequest);
|
||||
parse_request!(GetGenesisIdRequest);
|
||||
parse_request!(GetLastBlockRequest);
|
||||
parse_request!(GetInitialTestnetAccountsRequest);
|
||||
@ -97,9 +106,70 @@ pub struct SendTxResponse {
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct GetBlockDataResponse {
|
||||
#[serde(with = "base64_deser")]
|
||||
pub block: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct GetBlockRangeDataResponse {
|
||||
#[serde(with = "base64_deser::vec")]
|
||||
pub blocks: Vec<Vec<u8>>,
|
||||
}
|
||||
|
||||
mod base64_deser {
|
||||
use base64::{Engine as _, engine::general_purpose};
|
||||
use serde::{self, Deserialize, Deserializer, Serializer, ser::SerializeSeq as _};
|
||||
|
||||
pub fn serialize<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let base64_string = general_purpose::STANDARD.encode(bytes);
|
||||
serializer.serialize_str(&base64_string)
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let base64_string: String = Deserialize::deserialize(deserializer)?;
|
||||
general_purpose::STANDARD
|
||||
.decode(&base64_string)
|
||||
.map_err(serde::de::Error::custom)
|
||||
}
|
||||
|
||||
pub mod vec {
|
||||
use super::*;
|
||||
|
||||
pub fn serialize<S>(bytes_vec: &[Vec<u8>], serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut seq = serializer.serialize_seq(Some(bytes_vec.len()))?;
|
||||
for bytes in bytes_vec {
|
||||
let s = general_purpose::STANDARD.encode(bytes);
|
||||
seq.serialize_element(&s)?;
|
||||
}
|
||||
seq.end()
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Vec<u8>>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let base64_strings: Vec<String> = Deserialize::deserialize(deserializer)?;
|
||||
base64_strings
|
||||
.into_iter()
|
||||
.map(|s| {
|
||||
general_purpose::STANDARD
|
||||
.decode(&s)
|
||||
.map_err(serde::de::Error::custom)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct GetGenesisIdResponse {
|
||||
pub genesis_id: u64,
|
||||
@ -139,3 +209,10 @@ pub struct GetProofForCommitmentResponse {
|
||||
pub struct GetProgramIdsResponse {
|
||||
pub program_ids: HashMap<String, ProgramId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct GetInitialTestnetAccountsResponse {
|
||||
/// Hex encoded account id
|
||||
pub account_id: String,
|
||||
pub balance: u64,
|
||||
}
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
use std::collections::HashMap;
|
||||
use std::{collections::HashMap, ops::RangeInclusive};
|
||||
|
||||
use anyhow::Result;
|
||||
use json::{SendTxRequest, SendTxResponse, SequencerRpcRequest, SequencerRpcResponse};
|
||||
use nssa_core::program::ProgramId;
|
||||
use reqwest::Client;
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
|
||||
use super::rpc_primitives::requests::{
|
||||
@ -12,18 +12,20 @@ use super::rpc_primitives::requests::{
|
||||
};
|
||||
use crate::{
|
||||
error::{SequencerClientError, SequencerRpcError},
|
||||
rpc_primitives::requests::{
|
||||
GetAccountRequest, GetAccountResponse, GetAccountsNoncesRequest, GetAccountsNoncesResponse,
|
||||
GetLastBlockRequest, GetLastBlockResponse, GetProgramIdsRequest, GetProgramIdsResponse,
|
||||
GetProofForCommitmentRequest, GetProofForCommitmentResponse, GetTransactionByHashRequest,
|
||||
GetTransactionByHashResponse,
|
||||
rpc_primitives::{
|
||||
self,
|
||||
requests::{
|
||||
GetAccountRequest, GetAccountResponse, GetAccountsNoncesRequest,
|
||||
GetAccountsNoncesResponse, GetBlockRangeDataRequest, GetBlockRangeDataResponse,
|
||||
GetInitialTestnetAccountsResponse, GetLastBlockRequest, GetLastBlockResponse,
|
||||
GetProgramIdsRequest, GetProgramIdsResponse, GetProofForCommitmentRequest,
|
||||
GetProofForCommitmentResponse, GetTransactionByHashRequest,
|
||||
GetTransactionByHashResponse, SendTxRequest, SendTxResponse,
|
||||
},
|
||||
},
|
||||
sequencer_client::json::AccountInitialData,
|
||||
transaction::{EncodedTransaction, NSSATransaction},
|
||||
};
|
||||
|
||||
pub mod json;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SequencerClient {
|
||||
pub client: reqwest::Client,
|
||||
@ -46,7 +48,8 @@ impl SequencerClient {
|
||||
method: &str,
|
||||
payload: Value,
|
||||
) -> Result<Value, SequencerClientError> {
|
||||
let request = SequencerRpcRequest::from_payload_version_2_0(method.to_string(), payload);
|
||||
let request =
|
||||
rpc_primitives::message::Request::from_payload_version_2_0(method.to_string(), payload);
|
||||
|
||||
let call_builder = self.client.post(&self.sequencer_addr);
|
||||
|
||||
@ -54,6 +57,15 @@ impl SequencerClient {
|
||||
|
||||
let response_vall = call_res.json::<Value>().await?;
|
||||
|
||||
// TODO: Actually why we need separation of `result` and `error` in rpc response?
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct SequencerRpcResponse {
|
||||
pub jsonrpc: String,
|
||||
pub result: serde_json::Value,
|
||||
pub id: u64,
|
||||
}
|
||||
|
||||
if let Ok(response) = serde_json::from_value::<SequencerRpcResponse>(response_vall.clone())
|
||||
{
|
||||
Ok(response.result)
|
||||
@ -80,6 +92,26 @@ impl SequencerClient {
|
||||
Ok(resp_deser)
|
||||
}
|
||||
|
||||
pub async fn get_block_range(
|
||||
&self,
|
||||
range: RangeInclusive<u64>,
|
||||
) -> Result<GetBlockRangeDataResponse, SequencerClientError> {
|
||||
let block_req = GetBlockRangeDataRequest {
|
||||
start_block_id: *range.start(),
|
||||
end_block_id: *range.end(),
|
||||
};
|
||||
|
||||
let req = serde_json::to_value(block_req)?;
|
||||
|
||||
let resp = self
|
||||
.call_method_with_payload("get_block_range", req)
|
||||
.await?;
|
||||
|
||||
let resp_deser = serde_json::from_value(resp)?;
|
||||
|
||||
Ok(resp_deser)
|
||||
}
|
||||
|
||||
/// Get last known `blokc_id` from sequencer
|
||||
pub async fn get_last_block(&self) -> Result<GetLastBlockResponse, SequencerClientError> {
|
||||
let block_req = GetLastBlockRequest {};
|
||||
@ -223,7 +255,7 @@ impl SequencerClient {
|
||||
/// Get initial testnet accounts from sequencer
|
||||
pub async fn get_initial_testnet_accounts(
|
||||
&self,
|
||||
) -> Result<Vec<AccountInitialData>, SequencerClientError> {
|
||||
) -> Result<Vec<GetInitialTestnetAccountsResponse>, SequencerClientError> {
|
||||
let acc_req = GetInitialTestnetAccountsRequest {};
|
||||
|
||||
let req = serde_json::to_value(acc_req).unwrap();
|
||||
@ -1,53 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// Requests
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct SendTxRequest {
|
||||
pub transaction: Vec<u8>,
|
||||
}
|
||||
|
||||
// Responses
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct SendTxResponse {
|
||||
pub status: String,
|
||||
pub tx_hash: String,
|
||||
}
|
||||
|
||||
// General
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct SequencerRpcRequest {
|
||||
jsonrpc: String,
|
||||
pub method: String,
|
||||
pub params: serde_json::Value,
|
||||
pub id: u64,
|
||||
}
|
||||
|
||||
impl SequencerRpcRequest {
|
||||
pub fn from_payload_version_2_0(method: String, payload: serde_json::Value) -> Self {
|
||||
Self {
|
||||
jsonrpc: "2.0".to_string(),
|
||||
method,
|
||||
params: payload,
|
||||
// ToDo: Correct checking of id
|
||||
id: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct SequencerRpcResponse {
|
||||
pub jsonrpc: String,
|
||||
pub result: serde_json::Value,
|
||||
pub id: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
/// Helperstruct for account serialization
|
||||
pub struct AccountInitialData {
|
||||
/// Hex encoded account id
|
||||
pub account_id: String,
|
||||
pub balance: u64,
|
||||
}
|
||||
@ -2,9 +2,9 @@
|
||||
"override_rust_log": null,
|
||||
"sequencer_addr": "http://127.0.0.1:3040",
|
||||
"seq_poll_timeout_millis": 12000,
|
||||
"seq_poll_max_blocks": 5,
|
||||
"seq_tx_poll_max_blocks": 5,
|
||||
"seq_poll_max_retries": 5,
|
||||
"seq_poll_retry_delay_millis": 500,
|
||||
"seq_block_poll_max_amount": 100,
|
||||
"initial_accounts": [
|
||||
{
|
||||
"Public": {
|
||||
|
||||
Binary file not shown.
@ -1610,23 +1610,23 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
info!("########## test_modify_config_fields ##########");
|
||||
|
||||
let wallet_config = fetch_config().await.unwrap();
|
||||
let old_seq_poll_retry_delay_millis = wallet_config.seq_poll_retry_delay_millis;
|
||||
let old_seq_poll_timeout_millis = wallet_config.seq_poll_timeout_millis;
|
||||
|
||||
// Change config field
|
||||
let command = Command::Config(ConfigSubcommand::Set {
|
||||
key: "seq_poll_retry_delay_millis".to_string(),
|
||||
key: "seq_poll_timeout_millis".to_string(),
|
||||
value: "1000".to_string(),
|
||||
});
|
||||
wallet::cli::execute_subcommand(command).await.unwrap();
|
||||
|
||||
let wallet_config = fetch_config().await.unwrap();
|
||||
|
||||
assert_eq!(wallet_config.seq_poll_retry_delay_millis, 1000);
|
||||
assert_eq!(wallet_config.seq_poll_timeout_millis, 1000);
|
||||
|
||||
// Return how it was at the beginning
|
||||
let command = Command::Config(ConfigSubcommand::Set {
|
||||
key: "seq_poll_retry_delay_millis".to_string(),
|
||||
value: old_seq_poll_retry_delay_millis.to_string(),
|
||||
key: "seq_poll_timeout_millis".to_string(),
|
||||
value: old_seq_poll_timeout_millis.to_string(),
|
||||
});
|
||||
wallet::cli::execute_subcommand(command).await.unwrap();
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ chacha20 = { version = "0.9", default-features = false }
|
||||
k256 = { version = "0.13.3", optional = true }
|
||||
base58 = { version = "0.2.0", optional = true }
|
||||
anyhow = { version = "1.0.98", optional = true }
|
||||
borsh.workspace = true
|
||||
borsh = "1.5.7"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
use risc0_zkvm::{DeserializeOwned, guest::env, serde::Deserializer};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[cfg(feature = "host")]
|
||||
use crate::account::AccountId;
|
||||
use crate::account::{Account, AccountWithMetadata};
|
||||
|
||||
pub type ProgramId = [u32; 8];
|
||||
@ -12,19 +14,105 @@ pub struct ProgramInput<T> {
|
||||
pub instruction: T,
|
||||
}
|
||||
|
||||
/// A 32-byte seed used to compute a *Program-Derived AccountId* (PDA).
|
||||
///
|
||||
/// Each program can derive up to `2^256` unique account IDs by choosing different
|
||||
/// seeds. PDAs allow programs to control namespaced account identifiers without
|
||||
/// collisions between programs.
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))]
|
||||
pub struct PdaSeed([u8; 32]);
|
||||
|
||||
impl PdaSeed {
|
||||
pub fn new(value: [u8; 32]) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "host")]
|
||||
impl From<(&ProgramId, &PdaSeed)> for AccountId {
|
||||
fn from(value: (&ProgramId, &PdaSeed)) -> Self {
|
||||
use risc0_zkvm::sha::{Impl, Sha256};
|
||||
const PROGRAM_DERIVED_ACCOUNT_ID_PREFIX: &[u8; 32] =
|
||||
b"/NSSA/v0.2/AccountId/PDA/\x00\x00\x00\x00\x00\x00\x00";
|
||||
|
||||
let mut bytes = [0; 96];
|
||||
bytes[0..32].copy_from_slice(PROGRAM_DERIVED_ACCOUNT_ID_PREFIX);
|
||||
let program_id_bytes: &[u8] =
|
||||
bytemuck::try_cast_slice(value.0).expect("ProgramId should be castable to &[u8]");
|
||||
bytes[32..64].copy_from_slice(program_id_bytes);
|
||||
bytes[64..].copy_from_slice(&value.1.0);
|
||||
AccountId::new(
|
||||
Impl::hash_bytes(&bytes)
|
||||
.as_bytes()
|
||||
.try_into()
|
||||
.expect("Hash output must be exactly 32 bytes long"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))]
|
||||
pub struct ChainedCall {
|
||||
pub program_id: ProgramId,
|
||||
pub instruction_data: InstructionData,
|
||||
pub pre_states: Vec<AccountWithMetadata>,
|
||||
pub pda_seeds: Vec<PdaSeed>,
|
||||
}
|
||||
|
||||
/// Represents the final state of an `Account` after a program execution.
|
||||
/// A post state may optionally request that the executing program
|
||||
/// becomes the owner of the account (a “claim”). This is used to signal
|
||||
/// that the program intends to take ownership of the account.
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))]
|
||||
pub struct AccountPostState {
|
||||
account: Account,
|
||||
claim: bool,
|
||||
}
|
||||
|
||||
impl AccountPostState {
|
||||
/// Creates a post state without a claim request.
|
||||
/// The executing program is not requesting ownership of the account.
|
||||
pub fn new(account: Account) -> Self {
|
||||
Self {
|
||||
account,
|
||||
claim: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a post state that requests ownership of the account.
|
||||
/// This indicates that the executing program intends to claim the
|
||||
/// account as its own and is allowed to mutate it.
|
||||
pub fn new_claimed(account: Account) -> Self {
|
||||
Self {
|
||||
account,
|
||||
claim: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if this post state requests that the account
|
||||
/// be claimed (owned) by the executing program.
|
||||
pub fn requires_claim(&self) -> bool {
|
||||
self.claim
|
||||
}
|
||||
|
||||
/// Returns the underlying account
|
||||
pub fn account(&self) -> &Account {
|
||||
&self.account
|
||||
}
|
||||
|
||||
/// Returns the underlying account
|
||||
pub fn account_mut(&mut self) -> &mut Account {
|
||||
&mut self.account
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))]
|
||||
pub struct ProgramOutput {
|
||||
pub pre_states: Vec<AccountWithMetadata>,
|
||||
pub post_states: Vec<Account>,
|
||||
pub post_states: Vec<AccountPostState>,
|
||||
pub chained_calls: Vec<ChainedCall>,
|
||||
}
|
||||
|
||||
@ -38,7 +126,10 @@ pub fn read_nssa_inputs<T: DeserializeOwned>() -> ProgramInput<T> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_nssa_outputs(pre_states: Vec<AccountWithMetadata>, post_states: Vec<Account>) {
|
||||
pub fn write_nssa_outputs(
|
||||
pre_states: Vec<AccountWithMetadata>,
|
||||
post_states: Vec<AccountPostState>,
|
||||
) {
|
||||
let output = ProgramOutput {
|
||||
pre_states,
|
||||
post_states,
|
||||
@ -49,7 +140,7 @@ pub fn write_nssa_outputs(pre_states: Vec<AccountWithMetadata>, post_states: Vec
|
||||
|
||||
pub fn write_nssa_outputs_with_chained_call(
|
||||
pre_states: Vec<AccountWithMetadata>,
|
||||
post_states: Vec<Account>,
|
||||
post_states: Vec<AccountPostState>,
|
||||
chained_calls: Vec<ChainedCall>,
|
||||
) {
|
||||
let output = ProgramOutput {
|
||||
@ -68,7 +159,7 @@ pub fn write_nssa_outputs_with_chained_call(
|
||||
/// - `executing_program_id`: The identifier of the program that was executed.
|
||||
pub fn validate_execution(
|
||||
pre_states: &[AccountWithMetadata],
|
||||
post_states: &[Account],
|
||||
post_states: &[AccountPostState],
|
||||
executing_program_id: ProgramId,
|
||||
) -> bool {
|
||||
// 1. Lengths must match
|
||||
@ -78,25 +169,27 @@ pub fn validate_execution(
|
||||
|
||||
for (pre, post) in pre_states.iter().zip(post_states) {
|
||||
// 2. Nonce must remain unchanged
|
||||
if pre.account.nonce != post.nonce {
|
||||
if pre.account.nonce != post.account.nonce {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. Program ownership changes are not allowed
|
||||
if pre.account.program_owner != post.program_owner {
|
||||
if pre.account.program_owner != post.account.program_owner {
|
||||
return false;
|
||||
}
|
||||
|
||||
let account_program_owner = pre.account.program_owner;
|
||||
|
||||
// 4. Decreasing balance only allowed if owned by executing program
|
||||
if post.balance < pre.account.balance && account_program_owner != executing_program_id {
|
||||
if post.account.balance < pre.account.balance
|
||||
&& account_program_owner != executing_program_id
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 5. Data changes only allowed if owned by executing program or if account pre state has
|
||||
// default values
|
||||
if pre.account.data != post.data
|
||||
if pre.account.data != post.account.data
|
||||
&& pre.account != Account::default()
|
||||
&& account_program_owner != executing_program_id
|
||||
{
|
||||
@ -105,17 +198,67 @@ pub fn validate_execution(
|
||||
|
||||
// 6. If a post state has default program owner, the pre state must have been a default
|
||||
// account
|
||||
if post.program_owner == DEFAULT_PROGRAM_ID && pre.account != Account::default() {
|
||||
if post.account.program_owner == DEFAULT_PROGRAM_ID && pre.account != Account::default() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Total balance is preserved
|
||||
let total_balance_pre_states: u128 = pre_states.iter().map(|pre| pre.account.balance).sum();
|
||||
let total_balance_post_states: u128 = post_states.iter().map(|post| post.balance).sum();
|
||||
let total_balance_post_states: u128 = post_states.iter().map(|post| post.account.balance).sum();
|
||||
if total_balance_pre_states != total_balance_post_states {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_post_state_new_with_claim_constructor() {
|
||||
let account = Account {
|
||||
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
balance: 1337,
|
||||
data: vec![0xde, 0xad, 0xbe, 0xef],
|
||||
nonce: 10,
|
||||
};
|
||||
|
||||
let account_post_state = AccountPostState::new_claimed(account.clone());
|
||||
|
||||
assert_eq!(account, account_post_state.account);
|
||||
assert!(account_post_state.requires_claim());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_post_state_new_without_claim_constructor() {
|
||||
let account = Account {
|
||||
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
balance: 1337,
|
||||
data: vec![0xde, 0xad, 0xbe, 0xef],
|
||||
nonce: 10,
|
||||
};
|
||||
|
||||
let account_post_state = AccountPostState::new(account.clone());
|
||||
|
||||
assert_eq!(account, account_post_state.account);
|
||||
assert!(!account_post_state.requires_claim());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_post_state_account_getter() {
|
||||
let mut account = Account {
|
||||
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
balance: 1337,
|
||||
data: vec![0xde, 0xad, 0xbe, 0xef],
|
||||
nonce: 10,
|
||||
};
|
||||
|
||||
let mut account_post_state = AccountPostState::new(account.clone());
|
||||
|
||||
assert_eq!(account_post_state.account(), &account);
|
||||
assert_eq!(account_post_state.account_mut(), &mut account);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,16 +1,17 @@
|
||||
use nssa_core::{
|
||||
account::{Account, AccountWithMetadata},
|
||||
program::{ProgramInput, read_nssa_inputs, write_nssa_outputs},
|
||||
program::{
|
||||
AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, read_nssa_inputs, write_nssa_outputs,
|
||||
},
|
||||
};
|
||||
|
||||
/// Initializes a default account under the ownership of this program.
|
||||
/// This is achieved by a noop.
|
||||
fn initialize_account(pre_state: AccountWithMetadata) {
|
||||
let account_to_claim = pre_state.account.clone();
|
||||
let account_to_claim = AccountPostState::new_claimed(pre_state.account.clone());
|
||||
let is_authorized = pre_state.is_authorized;
|
||||
|
||||
// Continue only if the account to claim has default values
|
||||
if account_to_claim != Account::default() {
|
||||
if account_to_claim.account() != &Account::default() {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -36,10 +37,25 @@ fn transfer(sender: AccountWithMetadata, recipient: AccountWithMetadata, balance
|
||||
}
|
||||
|
||||
// Create accounts post states, with updated balances
|
||||
let mut sender_post = sender.account.clone();
|
||||
let mut recipient_post = recipient.account.clone();
|
||||
sender_post.balance -= balance_to_move;
|
||||
recipient_post.balance += balance_to_move;
|
||||
let sender_post = {
|
||||
// Modify sender's balance
|
||||
let mut sender_post_account = sender.account.clone();
|
||||
sender_post_account.balance -= balance_to_move;
|
||||
AccountPostState::new(sender_post_account)
|
||||
};
|
||||
|
||||
let recipient_post = {
|
||||
// Modify recipient's balance
|
||||
let mut recipient_post_account = recipient.account.clone();
|
||||
recipient_post_account.balance += balance_to_move;
|
||||
|
||||
// Claim recipient account if it has default program owner
|
||||
if recipient_post_account.program_owner == DEFAULT_PROGRAM_ID {
|
||||
AccountPostState::new_claimed(recipient_post_account)
|
||||
} else {
|
||||
AccountPostState::new(recipient_post_account)
|
||||
}
|
||||
};
|
||||
|
||||
write_nssa_outputs(vec![sender, recipient], vec![sender_post, recipient_post]);
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use nssa_core::program::{ProgramInput, read_nssa_inputs, write_nssa_outputs};
|
||||
use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs};
|
||||
use risc0_zkvm::sha::{Impl, Sha256};
|
||||
|
||||
const PRIZE: u128 = 150;
|
||||
@ -66,5 +66,11 @@ fn main() {
|
||||
pinata_post.data = data.next_data().to_vec();
|
||||
winner_post.balance += PRIZE;
|
||||
|
||||
write_nssa_outputs(vec![pinata, winner], vec![pinata_post, winner_post]);
|
||||
write_nssa_outputs(
|
||||
vec![pinata, winner],
|
||||
vec![
|
||||
AccountPostState::new(pinata_post),
|
||||
AccountPostState::new(winner_post),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
103
nssa/program_methods/guest/src/bin/pinata_token.rs
Normal file
103
nssa/program_methods/guest/src/bin/pinata_token.rs
Normal file
@ -0,0 +1,103 @@
|
||||
use nssa_core::program::{
|
||||
read_nssa_inputs, write_nssa_outputs_with_chained_call, AccountPostState, ChainedCall, PdaSeed, ProgramInput
|
||||
};
|
||||
use risc0_zkvm::serde::to_vec;
|
||||
use risc0_zkvm::sha::{Impl, Sha256};
|
||||
|
||||
const PRIZE: u128 = 150;
|
||||
|
||||
type Instruction = u128;
|
||||
|
||||
struct Challenge {
|
||||
difficulty: u8,
|
||||
seed: [u8; 32],
|
||||
}
|
||||
|
||||
impl Challenge {
|
||||
fn new(bytes: &[u8]) -> Self {
|
||||
assert_eq!(bytes.len(), 33);
|
||||
let difficulty = bytes[0];
|
||||
assert!(difficulty <= 32);
|
||||
|
||||
let mut seed = [0; 32];
|
||||
seed.copy_from_slice(&bytes[1..]);
|
||||
Self { difficulty, seed }
|
||||
}
|
||||
|
||||
// Checks if the leftmost `self.difficulty` number of bytes of SHA256(self.data || solution) are
|
||||
// zero.
|
||||
fn validate_solution(&self, solution: Instruction) -> bool {
|
||||
let mut bytes = [0; 32 + 16];
|
||||
bytes[..32].copy_from_slice(&self.seed);
|
||||
bytes[32..].copy_from_slice(&solution.to_le_bytes());
|
||||
let digest: [u8; 32] = Impl::hash_bytes(&bytes).as_bytes().try_into().unwrap();
|
||||
let difficulty = self.difficulty as usize;
|
||||
digest[..difficulty].iter().all(|&b| b == 0)
|
||||
}
|
||||
|
||||
fn next_data(self) -> [u8; 33] {
|
||||
let mut result = [0; 33];
|
||||
result[0] = self.difficulty;
|
||||
result[1..].copy_from_slice(Impl::hash_bytes(&self.seed).as_bytes());
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// A pinata program
|
||||
fn main() {
|
||||
// Read input accounts.
|
||||
// It is expected to receive three accounts: [pinata_definition, pinata_token_holding, winner_token_holding]
|
||||
let ProgramInput {
|
||||
pre_states,
|
||||
instruction: solution,
|
||||
} = read_nssa_inputs::<Instruction>();
|
||||
|
||||
let [
|
||||
pinata_definition,
|
||||
pinata_token_holding,
|
||||
winner_token_holding,
|
||||
] = match pre_states.try_into() {
|
||||
Ok(array) => array,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
let data = Challenge::new(&pinata_definition.account.data);
|
||||
|
||||
if !data.validate_solution(solution) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut pinata_definition_post = pinata_definition.account.clone();
|
||||
let pinata_token_holding_post = pinata_token_holding.account.clone();
|
||||
let winner_token_holding_post = winner_token_holding.account.clone();
|
||||
pinata_definition_post.data = data.next_data().to_vec();
|
||||
|
||||
let mut instruction_data: [u8; 23] = [0; 23];
|
||||
instruction_data[0] = 1;
|
||||
instruction_data[1..17].copy_from_slice(&PRIZE.to_le_bytes());
|
||||
|
||||
// Flip authorization to true for chained call
|
||||
let mut pinata_token_holding_for_chain_call = pinata_token_holding.clone();
|
||||
pinata_token_holding_for_chain_call.is_authorized = true;
|
||||
|
||||
let chained_calls = vec![ChainedCall {
|
||||
program_id: pinata_token_holding_post.program_owner,
|
||||
instruction_data: to_vec(&instruction_data).unwrap(),
|
||||
pre_states: vec![pinata_token_holding_for_chain_call, winner_token_holding.clone()],
|
||||
pda_seeds: vec![PdaSeed::new([0; 32])],
|
||||
}];
|
||||
|
||||
write_nssa_outputs_with_chained_call(
|
||||
vec![
|
||||
pinata_definition,
|
||||
pinata_token_holding,
|
||||
winner_token_holding,
|
||||
],
|
||||
vec![
|
||||
AccountPostState::new(pinata_definition_post),
|
||||
AccountPostState::new(pinata_token_holding_post),
|
||||
AccountPostState::new(winner_token_holding_post),
|
||||
],
|
||||
chained_calls,
|
||||
);
|
||||
}
|
||||
@ -70,7 +70,7 @@ fn main() {
|
||||
// Public account
|
||||
public_pre_states.push(pre_states[i].clone());
|
||||
|
||||
let mut post = post_states[i].clone();
|
||||
let mut post = post_states[i].account().clone();
|
||||
if pre_states[i].is_authorized {
|
||||
post.nonce += 1;
|
||||
}
|
||||
@ -126,7 +126,7 @@ fn main() {
|
||||
}
|
||||
|
||||
// Update post-state with new nonce
|
||||
let mut post_with_updated_values = post_states[i].clone();
|
||||
let mut post_with_updated_values = post_states[i].account().clone();
|
||||
post_with_updated_values.nonce = *new_nonce;
|
||||
|
||||
if post_with_updated_values.program_owner == DEFAULT_PROGRAM_ID {
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
use nssa_core::{
|
||||
account::{Account, AccountId, AccountWithMetadata, Data},
|
||||
program::{ProgramInput, read_nssa_inputs, write_nssa_outputs},
|
||||
program::{
|
||||
AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, read_nssa_inputs, write_nssa_outputs,
|
||||
},
|
||||
};
|
||||
|
||||
// The token program has three functions:
|
||||
@ -82,25 +84,25 @@ impl TokenHolding {
|
||||
|
||||
fn parse(data: &[u8]) -> Option<Self> {
|
||||
if data.len() != TOKEN_HOLDING_DATA_SIZE || data[0] != TOKEN_HOLDING_TYPE {
|
||||
None
|
||||
} else {
|
||||
let account_type = data[0];
|
||||
let definition_id = AccountId::new(
|
||||
data[1..33]
|
||||
.try_into()
|
||||
.expect("Defintion ID must be 32 bytes long"),
|
||||
);
|
||||
let balance = u128::from_le_bytes(
|
||||
data[33..]
|
||||
.try_into()
|
||||
.expect("balance must be 16 bytes little-endian"),
|
||||
);
|
||||
Some(Self {
|
||||
definition_id,
|
||||
balance,
|
||||
account_type,
|
||||
})
|
||||
return None;
|
||||
}
|
||||
|
||||
let account_type = data[0];
|
||||
let definition_id = AccountId::new(
|
||||
data[1..33]
|
||||
.try_into()
|
||||
.expect("Defintion ID must be 32 bytes long"),
|
||||
);
|
||||
let balance = u128::from_le_bytes(
|
||||
data[33..]
|
||||
.try_into()
|
||||
.expect("balance must be 16 bytes little-endian"),
|
||||
);
|
||||
Some(Self {
|
||||
definition_id,
|
||||
balance,
|
||||
account_type,
|
||||
})
|
||||
}
|
||||
|
||||
fn into_data(self) -> Data {
|
||||
@ -112,7 +114,7 @@ impl TokenHolding {
|
||||
}
|
||||
}
|
||||
|
||||
fn transfer(pre_states: &[AccountWithMetadata], balance_to_move: u128) -> Vec<Account> {
|
||||
fn transfer(pre_states: &[AccountWithMetadata], balance_to_move: u128) -> Vec<AccountPostState> {
|
||||
if pre_states.len() != 2 {
|
||||
panic!("Invalid number of input accounts");
|
||||
}
|
||||
@ -148,12 +150,19 @@ fn transfer(pre_states: &[AccountWithMetadata], balance_to_move: u128) -> Vec<Ac
|
||||
let sender_post = {
|
||||
let mut this = sender.account.clone();
|
||||
this.data = sender_holding.into_data();
|
||||
this
|
||||
AccountPostState::new(this)
|
||||
};
|
||||
|
||||
let recipient_post = {
|
||||
let mut this = recipient.account.clone();
|
||||
this.data = recipient_holding.into_data();
|
||||
this
|
||||
|
||||
// Claim the recipient account if it has default program owner
|
||||
if this.program_owner == DEFAULT_PROGRAM_ID {
|
||||
AccountPostState::new_claimed(this)
|
||||
} else {
|
||||
AccountPostState::new(this)
|
||||
}
|
||||
};
|
||||
|
||||
vec![sender_post, recipient_post]
|
||||
@ -163,7 +172,7 @@ fn new_definition(
|
||||
pre_states: &[AccountWithMetadata],
|
||||
name: [u8; 6],
|
||||
total_supply: u128,
|
||||
) -> Vec<Account> {
|
||||
) -> Vec<AccountPostState> {
|
||||
if pre_states.len() != 2 {
|
||||
panic!("Invalid number of input accounts");
|
||||
}
|
||||
@ -196,10 +205,13 @@ fn new_definition(
|
||||
let mut holding_target_account_post = holding_target_account.account.clone();
|
||||
holding_target_account_post.data = token_holding.into_data();
|
||||
|
||||
vec![definition_target_account_post, holding_target_account_post]
|
||||
vec![
|
||||
AccountPostState::new_claimed(definition_target_account_post),
|
||||
AccountPostState::new_claimed(holding_target_account_post),
|
||||
]
|
||||
}
|
||||
|
||||
fn initialize_account(pre_states: &[AccountWithMetadata]) -> Vec<Account> {
|
||||
fn initialize_account(pre_states: &[AccountWithMetadata]) -> Vec<AccountPostState> {
|
||||
if pre_states.len() != 2 {
|
||||
panic!("Invalid number of accounts");
|
||||
}
|
||||
@ -211,7 +223,7 @@ fn initialize_account(pre_states: &[AccountWithMetadata]) -> Vec<Account> {
|
||||
panic!("Only uninitialized accounts can be initialized");
|
||||
}
|
||||
|
||||
// TODO: We should check that this is an account owned by the token program.
|
||||
// TODO: #212 We should check that this is an account owned by the token program.
|
||||
// This check can't be done here since the ID of the program is known only after compiling it
|
||||
//
|
||||
// Check definition account is valid
|
||||
@ -220,10 +232,13 @@ fn initialize_account(pre_states: &[AccountWithMetadata]) -> Vec<Account> {
|
||||
let holding_values = TokenHolding::new(&definition.account_id);
|
||||
|
||||
let definition_post = definition.account.clone();
|
||||
let mut account_to_initialize_post = account_to_initialize.account.clone();
|
||||
account_to_initialize_post.data = holding_values.into_data();
|
||||
let mut account_to_initialize = account_to_initialize.account.clone();
|
||||
account_to_initialize.data = holding_values.into_data();
|
||||
|
||||
vec![definition_post, account_to_initialize_post]
|
||||
vec![
|
||||
AccountPostState::new(definition_post),
|
||||
AccountPostState::new_claimed(account_to_initialize),
|
||||
]
|
||||
}
|
||||
|
||||
type Instruction = [u8; 23];
|
||||
@ -234,7 +249,7 @@ fn main() {
|
||||
instruction,
|
||||
} = read_nssa_inputs::<Instruction>();
|
||||
|
||||
let (pre_states, post_states) = match instruction[0] {
|
||||
let post_states = match instruction[0] {
|
||||
0 => {
|
||||
// Parse instruction
|
||||
let total_supply = u128::from_le_bytes(
|
||||
@ -248,8 +263,7 @@ fn main() {
|
||||
assert_ne!(name, [0; 6]);
|
||||
|
||||
// Execute
|
||||
let post_states = new_definition(&pre_states, name, total_supply);
|
||||
(pre_states, post_states)
|
||||
new_definition(&pre_states, name, total_supply)
|
||||
}
|
||||
1 => {
|
||||
// Parse instruction
|
||||
@ -264,14 +278,14 @@ fn main() {
|
||||
assert_eq!(name, [0; 6]);
|
||||
|
||||
// Execute
|
||||
let post_states = transfer(&pre_states, balance_to_move);
|
||||
(pre_states, post_states)
|
||||
transfer(&pre_states, balance_to_move)
|
||||
}
|
||||
2 => {
|
||||
// Initialize account
|
||||
assert_eq!(instruction[1..], [0; 22]);
|
||||
let post_states = initialize_account(&pre_states);
|
||||
(pre_states, post_states)
|
||||
if instruction[1..] != [0; 22] {
|
||||
panic!("Invalid instruction for initialize account");
|
||||
}
|
||||
initialize_account(&pre_states)
|
||||
}
|
||||
_ => panic!("Invalid instruction"),
|
||||
};
|
||||
@ -387,14 +401,14 @@ mod tests {
|
||||
let post_states = new_definition(&pre_states, [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe], 10);
|
||||
let [definition_account, holding_account] = post_states.try_into().ok().unwrap();
|
||||
assert_eq!(
|
||||
definition_account.data,
|
||||
definition_account.account().data,
|
||||
vec![
|
||||
0, 0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
holding_account.data,
|
||||
holding_account.account().data,
|
||||
vec![
|
||||
1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,
|
||||
23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
@ -619,14 +633,14 @@ mod tests {
|
||||
let post_states = transfer(&pre_states, 11);
|
||||
let [sender_post, recipient_post] = post_states.try_into().ok().unwrap();
|
||||
assert_eq!(
|
||||
sender_post.data,
|
||||
sender_post.account().data,
|
||||
vec![
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 1, 26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
recipient_post.data,
|
||||
recipient_post.account().data,
|
||||
vec![
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 1, 10, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
|
||||
@ -640,7 +654,7 @@ mod tests {
|
||||
AccountWithMetadata {
|
||||
account: Account {
|
||||
// Definition ID with
|
||||
data: vec![0; TOKEN_DEFINITION_DATA_SIZE - 16]
|
||||
data: [0; TOKEN_DEFINITION_DATA_SIZE - 16]
|
||||
.into_iter()
|
||||
.chain(u128::to_le_bytes(1000))
|
||||
.collect(),
|
||||
@ -657,9 +671,9 @@ mod tests {
|
||||
];
|
||||
let post_states = initialize_account(&pre_states);
|
||||
let [definition, holding] = post_states.try_into().ok().unwrap();
|
||||
assert_eq!(definition.data, pre_states[0].account.data);
|
||||
assert_eq!(definition.account().data, pre_states[0].account.data);
|
||||
assert_eq!(
|
||||
holding.data,
|
||||
holding.account().data,
|
||||
vec![
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
|
||||
|
||||
@ -104,6 +104,11 @@ impl Program {
|
||||
// `program_methods`
|
||||
Self::new(PINATA_ELF.to_vec()).unwrap()
|
||||
}
|
||||
|
||||
pub fn pinata_token() -> Self {
|
||||
use crate::program_methods::PINATA_TOKEN_ELF;
|
||||
Self::new(PINATA_TOKEN_ELF.to_vec()).expect("Piñata program must be a valid R0BF file")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -207,6 +212,15 @@ mod tests {
|
||||
elf: CHAIN_CALLER_ELF.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn claimer() -> Self {
|
||||
use test_program_methods::{CLAIMER_ELF, CLAIMER_ID};
|
||||
|
||||
Program {
|
||||
id: CLAIMER_ID,
|
||||
elf: CLAIMER_ELF.to_vec(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -239,8 +253,8 @@ mod tests {
|
||||
|
||||
let [sender_post, recipient_post] = program_output.post_states.try_into().unwrap();
|
||||
|
||||
assert_eq!(sender_post, expected_sender_post);
|
||||
assert_eq!(recipient_post, expected_recipient_post);
|
||||
assert_eq!(sender_post.account(), &expected_sender_post);
|
||||
assert_eq!(recipient_post.account(), &expected_recipient_post);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use nssa_core::{
|
||||
account::{Account, AccountId, AccountWithMetadata},
|
||||
program::{ChainedCall, DEFAULT_PROGRAM_ID, validate_execution},
|
||||
program::{ChainedCall, DEFAULT_PROGRAM_ID, PdaSeed, ProgramId, validate_execution},
|
||||
};
|
||||
use sha2::{Digest, digest::FixedOutput};
|
||||
|
||||
@ -107,12 +107,13 @@ impl PublicTransaction {
|
||||
program_id: message.program_id,
|
||||
instruction_data: message.instruction_data.clone(),
|
||||
pre_states: input_pre_states,
|
||||
pda_seeds: vec![],
|
||||
};
|
||||
|
||||
let mut chained_calls = VecDeque::from_iter([initial_call]);
|
||||
let mut chained_calls = VecDeque::from_iter([(initial_call, None)]);
|
||||
let mut chain_calls_counter = 0;
|
||||
|
||||
while let Some(chained_call) = chained_calls.pop_front() {
|
||||
while let Some((chained_call, caller_program_id)) = chained_calls.pop_front() {
|
||||
if chain_calls_counter > MAX_NUMBER_CHAINED_CALLS {
|
||||
return Err(NssaError::MaxChainedCallsDepthExceeded);
|
||||
}
|
||||
@ -125,6 +126,9 @@ impl PublicTransaction {
|
||||
let mut program_output =
|
||||
program.execute(&chained_call.pre_states, &chained_call.instruction_data)?;
|
||||
|
||||
let authorized_pdas =
|
||||
self.compute_authorized_pdas(&caller_program_id, &chained_call.pda_seeds);
|
||||
|
||||
for pre in &program_output.pre_states {
|
||||
let account_id = pre.account_id;
|
||||
// Check that the program output pre_states coinicide with the values in the public
|
||||
@ -137,8 +141,11 @@ impl PublicTransaction {
|
||||
return Err(NssaError::InvalidProgramBehavior);
|
||||
}
|
||||
|
||||
// Check that authorization flags are consistent with the provided ones
|
||||
if pre.is_authorized && !signer_account_ids.contains(&account_id) {
|
||||
// Check that authorization flags are consistent with the provided ones or
|
||||
// authorized by program through the PDA mechanism
|
||||
let is_authorized = signer_account_ids.contains(&account_id)
|
||||
|| authorized_pdas.contains(&account_id);
|
||||
if pre.is_authorized != is_authorized {
|
||||
return Err(NssaError::InvalidProgramBehavior);
|
||||
}
|
||||
}
|
||||
@ -153,10 +160,16 @@ impl PublicTransaction {
|
||||
return Err(NssaError::InvalidProgramBehavior);
|
||||
}
|
||||
|
||||
// The invoked program claims the accounts with default program id.
|
||||
for post in program_output.post_states.iter_mut() {
|
||||
if post.program_owner == DEFAULT_PROGRAM_ID {
|
||||
post.program_owner = chained_call.program_id;
|
||||
for post in program_output
|
||||
.post_states
|
||||
.iter_mut()
|
||||
.filter(|post| post.requires_claim())
|
||||
{
|
||||
// The invoked program can only claim accounts with default program id.
|
||||
if post.account().program_owner == DEFAULT_PROGRAM_ID {
|
||||
post.account_mut().program_owner = chained_call.program_id;
|
||||
} else {
|
||||
return Err(NssaError::InvalidProgramBehavior);
|
||||
}
|
||||
}
|
||||
|
||||
@ -166,11 +179,11 @@ impl PublicTransaction {
|
||||
.iter()
|
||||
.zip(program_output.post_states.iter())
|
||||
{
|
||||
state_diff.insert(pre.account_id, post.clone());
|
||||
state_diff.insert(pre.account_id, post.account().clone());
|
||||
}
|
||||
|
||||
for new_call in program_output.chained_calls.into_iter().rev() {
|
||||
chained_calls.push_front(new_call);
|
||||
chained_calls.push_front((new_call, Some(chained_call.program_id)));
|
||||
}
|
||||
|
||||
chain_calls_counter += 1;
|
||||
@ -178,6 +191,21 @@ impl PublicTransaction {
|
||||
|
||||
Ok(state_diff)
|
||||
}
|
||||
|
||||
fn compute_authorized_pdas(
|
||||
&self,
|
||||
caller_program_id: &Option<ProgramId>,
|
||||
pda_seeds: &[PdaSeed],
|
||||
) -> HashSet<AccountId> {
|
||||
if let Some(caller_program_id) = caller_program_id {
|
||||
pda_seeds
|
||||
.iter()
|
||||
.map(|pda_seed| AccountId::from((caller_program_id, pda_seed)))
|
||||
.collect()
|
||||
} else {
|
||||
HashSet::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@ -239,6 +239,20 @@ impl V02State {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn add_pinata_token_program(&mut self, account_id: AccountId) {
|
||||
self.insert_program(Program::pinata_token());
|
||||
|
||||
self.public_state.insert(
|
||||
account_id,
|
||||
Account {
|
||||
program_owner: Program::pinata_token().id(),
|
||||
// Difficulty: 3
|
||||
data: vec![3; 33],
|
||||
..Account::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -250,7 +264,7 @@ pub mod tests {
|
||||
Commitment, Nullifier, NullifierPublicKey, NullifierSecretKey, SharedSecretKey,
|
||||
account::{Account, AccountId, AccountWithMetadata, Nonce},
|
||||
encryption::{EphemeralPublicKey, IncomingViewingPublicKey, Scalar},
|
||||
program::ProgramId,
|
||||
program::{PdaSeed, ProgramId},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@ -477,6 +491,7 @@ pub mod tests {
|
||||
self.insert_program(Program::minter());
|
||||
self.insert_program(Program::burner());
|
||||
self.insert_program(Program::chain_caller());
|
||||
self.insert_program(Program::claimer());
|
||||
self
|
||||
}
|
||||
|
||||
@ -2092,14 +2107,18 @@ pub mod tests {
|
||||
let key = PrivateKey::try_new([1; 32]).unwrap();
|
||||
let from = AccountId::from(&PublicKey::new_from_private_key(&key));
|
||||
let to = AccountId::new([2; 32]);
|
||||
let initial_balance = 100;
|
||||
let initial_balance = 1000;
|
||||
let initial_data = [(from, initial_balance), (to, 0)];
|
||||
let mut state =
|
||||
V02State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs();
|
||||
let from_key = key;
|
||||
let amount: u128 = 0;
|
||||
let instruction: (u128, ProgramId, u32) =
|
||||
(amount, Program::authenticated_transfer_program().id(), 2);
|
||||
let amount: u128 = 37;
|
||||
let instruction: (u128, ProgramId, u32, Option<PdaSeed>) = (
|
||||
amount,
|
||||
Program::authenticated_transfer_program().id(),
|
||||
2,
|
||||
None,
|
||||
);
|
||||
|
||||
let expected_to_post = Account {
|
||||
program_owner: Program::authenticated_transfer_program().id(),
|
||||
@ -2139,10 +2158,11 @@ pub mod tests {
|
||||
V02State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs();
|
||||
let from_key = key;
|
||||
let amount: u128 = 0;
|
||||
let instruction: (u128, ProgramId, u32) = (
|
||||
let instruction: (u128, ProgramId, u32, Option<PdaSeed>) = (
|
||||
amount,
|
||||
Program::authenticated_transfer_program().id(),
|
||||
MAX_NUMBER_CHAINED_CALLS as u32 + 1,
|
||||
None,
|
||||
);
|
||||
|
||||
let message = public_transaction::Message::try_new(
|
||||
@ -2162,4 +2182,208 @@ pub mod tests {
|
||||
Err(NssaError::MaxChainedCallsDepthExceeded)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_execution_that_requires_authentication_of_a_program_derived_account_id_succeeds() {
|
||||
let chain_caller = Program::chain_caller();
|
||||
let pda_seed = PdaSeed::new([37; 32]);
|
||||
let from = AccountId::from((&chain_caller.id(), &pda_seed));
|
||||
let to = AccountId::new([2; 32]);
|
||||
let initial_balance = 1000;
|
||||
let initial_data = [(from, initial_balance), (to, 0)];
|
||||
let mut state =
|
||||
V02State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs();
|
||||
let amount: u128 = 58;
|
||||
let instruction: (u128, ProgramId, u32, Option<PdaSeed>) = (
|
||||
amount,
|
||||
Program::authenticated_transfer_program().id(),
|
||||
1,
|
||||
Some(pda_seed),
|
||||
);
|
||||
|
||||
let expected_to_post = Account {
|
||||
program_owner: Program::authenticated_transfer_program().id(),
|
||||
balance: amount, // The `chain_caller` chains the program twice
|
||||
..Account::default()
|
||||
};
|
||||
let message = public_transaction::Message::try_new(
|
||||
chain_caller.id(),
|
||||
vec![to, from], // The chain_caller program permutes the account order in the chain
|
||||
// call
|
||||
vec![],
|
||||
instruction,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
state.transition_from_public_transaction(&tx).unwrap();
|
||||
|
||||
let from_post = state.get_account_by_id(&from);
|
||||
let to_post = state.get_account_by_id(&to);
|
||||
assert_eq!(from_post.balance, initial_balance - amount);
|
||||
assert_eq!(to_post, expected_to_post);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_claiming_mechanism_within_chain_call() {
|
||||
// This test calls the authenticated transfer program through the chain_caller program.
|
||||
// The transfer is made from an initialized sender to an uninitialized recipient. And
|
||||
// it is expected that the recipient account is claimed by the authenticated transfer
|
||||
// program and not the chained_caller program.
|
||||
let chain_caller = Program::chain_caller();
|
||||
let auth_transfer = Program::authenticated_transfer_program();
|
||||
let key = PrivateKey::try_new([1; 32]).unwrap();
|
||||
let account_id = AccountId::from(&PublicKey::new_from_private_key(&key));
|
||||
let initial_balance = 100;
|
||||
let initial_data = [(account_id, initial_balance)];
|
||||
let mut state =
|
||||
V02State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs();
|
||||
let from = account_id;
|
||||
let from_key = key;
|
||||
let to = AccountId::new([2; 32]);
|
||||
let amount: u128 = 37;
|
||||
|
||||
// Check the recipient is an uninitialized account
|
||||
assert_eq!(state.get_account_by_id(&to), Account::default());
|
||||
|
||||
let expected_to_post = Account {
|
||||
// The expected program owner is the authenticated transfer program
|
||||
program_owner: auth_transfer.id(),
|
||||
balance: amount,
|
||||
..Account::default()
|
||||
};
|
||||
|
||||
// The transaction executes the chain_caller program, which internally calls the
|
||||
// authenticated_transfer program
|
||||
let instruction: (u128, ProgramId, u32, Option<PdaSeed>) = (
|
||||
amount,
|
||||
Program::authenticated_transfer_program().id(),
|
||||
1,
|
||||
None,
|
||||
);
|
||||
let message = public_transaction::Message::try_new(
|
||||
chain_caller.id(),
|
||||
vec![to, from], // The chain_caller program permutes the account order in the chain
|
||||
// call
|
||||
vec![0],
|
||||
instruction,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
state.transition_from_public_transaction(&tx).unwrap();
|
||||
|
||||
let from_post = state.get_account_by_id(&from);
|
||||
let to_post = state.get_account_by_id(&to);
|
||||
assert_eq!(from_post.balance, initial_balance - amount);
|
||||
assert_eq!(to_post, expected_to_post);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pda_mechanism_with_pinata_token_program() {
|
||||
let pinata_token = Program::pinata_token();
|
||||
let token = Program::token();
|
||||
|
||||
let pinata_definition_id = AccountId::new([1; 32]);
|
||||
let pinata_token_definition_id = AccountId::new([2; 32]);
|
||||
// Total supply of pinata token will be in an account under a PDA.
|
||||
let pinata_token_holding_id = AccountId::from((&pinata_token.id(), &PdaSeed::new([0; 32])));
|
||||
let winner_token_holding_id = AccountId::new([3; 32]);
|
||||
|
||||
let mut expected_winner_account_data = [0; 49];
|
||||
expected_winner_account_data[0] = 1;
|
||||
expected_winner_account_data[1..33].copy_from_slice(pinata_token_definition_id.value());
|
||||
expected_winner_account_data[33..].copy_from_slice(&150u128.to_le_bytes());
|
||||
let expected_winner_token_holding_post = Account {
|
||||
program_owner: token.id(),
|
||||
data: expected_winner_account_data.to_vec(),
|
||||
..Account::default()
|
||||
};
|
||||
|
||||
let mut state = V02State::new_with_genesis_accounts(&[], &[]);
|
||||
state.add_pinata_token_program(pinata_definition_id);
|
||||
|
||||
// Execution of the token program to create new token for the pinata token
|
||||
// definition and supply accounts
|
||||
let total_supply: u128 = 10_000_000;
|
||||
// instruction: [0x00 || total_supply (little-endian 16 bytes) || name (6 bytes)]
|
||||
let mut instruction: [u8; 23] = [0; 23];
|
||||
instruction[1..17].copy_from_slice(&total_supply.to_le_bytes());
|
||||
instruction[17..].copy_from_slice(b"PINATA");
|
||||
let message = public_transaction::Message::try_new(
|
||||
token.id(),
|
||||
vec![pinata_token_definition_id, pinata_token_holding_id],
|
||||
vec![],
|
||||
instruction,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
state.transition_from_public_transaction(&tx).unwrap();
|
||||
|
||||
// Execution of the token program transfer just to initialize the winner token account
|
||||
let mut instruction: [u8; 23] = [0; 23];
|
||||
instruction[0] = 2;
|
||||
let message = public_transaction::Message::try_new(
|
||||
token.id(),
|
||||
vec![pinata_token_definition_id, winner_token_holding_id],
|
||||
vec![],
|
||||
instruction,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
state.transition_from_public_transaction(&tx).unwrap();
|
||||
|
||||
// Submit a solution to the pinata program to claim the prize
|
||||
let solution: u128 = 989106;
|
||||
let message = public_transaction::Message::try_new(
|
||||
pinata_token.id(),
|
||||
vec![
|
||||
pinata_definition_id,
|
||||
pinata_token_holding_id,
|
||||
winner_token_holding_id,
|
||||
],
|
||||
vec![],
|
||||
solution,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
state.transition_from_public_transaction(&tx).unwrap();
|
||||
|
||||
let winner_token_holding_post = state.get_account_by_id(&winner_token_holding_id);
|
||||
assert_eq!(
|
||||
winner_token_holding_post,
|
||||
expected_winner_token_holding_post
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_claiming_mechanism_cannot_claim_initialied_accounts() {
|
||||
let claimer = Program::claimer();
|
||||
let mut state = V02State::new_with_genesis_accounts(&[], &[]).with_test_programs();
|
||||
let account_id = AccountId::new([2; 32]);
|
||||
|
||||
// Insert an account with non-default program owner
|
||||
state.force_insert_account(
|
||||
account_id,
|
||||
Account {
|
||||
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
..Account::default()
|
||||
},
|
||||
);
|
||||
|
||||
let message =
|
||||
public_transaction::Message::try_new(claimer.id(), vec![account_id], vec![], ())
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx);
|
||||
|
||||
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)))
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput};
|
||||
use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs};
|
||||
|
||||
type Instruction = u128;
|
||||
|
||||
@ -17,5 +17,5 @@ fn main() {
|
||||
let mut account_post = account_pre.clone();
|
||||
account_post.balance -= balance_to_burn;
|
||||
|
||||
write_nssa_outputs(vec![pre], vec![account_post]);
|
||||
write_nssa_outputs(vec![pre], vec![AccountPostState::new(account_post)]);
|
||||
}
|
||||
|
||||
@ -1,43 +1,54 @@
|
||||
use nssa_core::program::{
|
||||
ChainedCall, ProgramId, ProgramInput, read_nssa_inputs, write_nssa_outputs_with_chained_call,
|
||||
AccountPostState, ChainedCall, PdaSeed, ProgramId, ProgramInput, read_nssa_inputs,
|
||||
write_nssa_outputs_with_chained_call,
|
||||
};
|
||||
use risc0_zkvm::serde::to_vec;
|
||||
|
||||
type Instruction = (u128, ProgramId, u32);
|
||||
type Instruction = (u128, ProgramId, u32, Option<PdaSeed>);
|
||||
|
||||
/// A program that calls another program `num_chain_calls` times.
|
||||
/// It permutes the order of the input accounts on the subsequent call
|
||||
/// The `ProgramId` in the instruction must be the program_id of the authenticated transfers program
|
||||
fn main() {
|
||||
let ProgramInput {
|
||||
pre_states,
|
||||
instruction: (balance, program_id, num_chain_calls),
|
||||
instruction: (balance, auth_transfer_id, num_chain_calls, pda_seed),
|
||||
} = read_nssa_inputs::<Instruction>();
|
||||
|
||||
let [sender_pre, receiver_pre] = match pre_states.try_into() {
|
||||
let [recipient_pre, sender_pre] = match pre_states.try_into() {
|
||||
Ok(array) => array,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
let instruction_data = to_vec(&balance).unwrap();
|
||||
|
||||
let mut chained_call = vec![
|
||||
ChainedCall {
|
||||
program_id,
|
||||
instruction_data: instruction_data.clone(),
|
||||
pre_states: vec![receiver_pre.clone(), sender_pre.clone()], // <- Account order permutation here
|
||||
};
|
||||
num_chain_calls as usize - 1
|
||||
];
|
||||
let mut running_recipient_pre = recipient_pre.clone();
|
||||
let mut running_sender_pre = sender_pre.clone();
|
||||
|
||||
chained_call.push(ChainedCall {
|
||||
program_id,
|
||||
instruction_data,
|
||||
pre_states: vec![receiver_pre.clone(), sender_pre.clone()], // <- Account order permutation here
|
||||
});
|
||||
if pda_seed.is_some() {
|
||||
running_sender_pre.is_authorized = true;
|
||||
}
|
||||
|
||||
let mut chained_calls = Vec::new();
|
||||
for _i in 0..num_chain_calls {
|
||||
let new_chained_call = ChainedCall {
|
||||
program_id: auth_transfer_id,
|
||||
instruction_data: instruction_data.clone(),
|
||||
pre_states: vec![running_sender_pre.clone(), running_recipient_pre.clone()], // <- Account order permutation here
|
||||
pda_seeds: pda_seed.iter().cloned().collect(),
|
||||
};
|
||||
chained_calls.push(new_chained_call);
|
||||
|
||||
running_sender_pre.account.balance -= balance;
|
||||
running_recipient_pre.account.balance += balance;
|
||||
}
|
||||
|
||||
write_nssa_outputs_with_chained_call(
|
||||
vec![sender_pre.clone(), receiver_pre.clone()],
|
||||
vec![sender_pre.account, receiver_pre.account],
|
||||
chained_call,
|
||||
vec![sender_pre.clone(), recipient_pre.clone()],
|
||||
vec![
|
||||
AccountPostState::new(sender_pre.account),
|
||||
AccountPostState::new(recipient_pre.account),
|
||||
],
|
||||
chained_calls,
|
||||
);
|
||||
}
|
||||
|
||||
19
nssa/test_program_methods/guest/src/bin/claimer.rs
Normal file
19
nssa/test_program_methods/guest/src/bin/claimer.rs
Normal file
@ -0,0 +1,19 @@
|
||||
use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs};
|
||||
|
||||
type Instruction = ();
|
||||
|
||||
fn main() {
|
||||
let ProgramInput {
|
||||
pre_states,
|
||||
instruction: _,
|
||||
} = read_nssa_inputs::<Instruction>();
|
||||
|
||||
let [pre] = match pre_states.try_into() {
|
||||
Ok(array) => array,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
let account_post = AccountPostState::new_claimed(pre.account.clone());
|
||||
|
||||
write_nssa_outputs(vec![pre], vec![account_post]);
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput};
|
||||
use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs};
|
||||
|
||||
type Instruction = ();
|
||||
|
||||
@ -14,5 +14,5 @@ fn main() {
|
||||
let mut account_post = account_pre.clone();
|
||||
account_post.data.push(0);
|
||||
|
||||
write_nssa_outputs(vec![pre], vec![account_post]);
|
||||
write_nssa_outputs(vec![pre], vec![AccountPostState::new_claimed(account_post)]);
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
use nssa_core::{
|
||||
account::Account,
|
||||
program::{read_nssa_inputs, write_nssa_outputs, ProgramInput},
|
||||
program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs},
|
||||
};
|
||||
|
||||
type Instruction = ();
|
||||
@ -15,5 +15,11 @@ fn main() {
|
||||
|
||||
let account_pre = pre.account.clone();
|
||||
|
||||
write_nssa_outputs(vec![pre], vec![account_pre, Account::default()]);
|
||||
write_nssa_outputs(
|
||||
vec![pre],
|
||||
vec![
|
||||
AccountPostState::new(account_pre),
|
||||
AccountPostState::new(Account::default()),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput};
|
||||
use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, AccountPostState, ProgramInput};
|
||||
|
||||
type Instruction = ();
|
||||
|
||||
@ -14,5 +14,5 @@ fn main() {
|
||||
let mut account_post = account_pre.clone();
|
||||
account_post.balance += 1;
|
||||
|
||||
write_nssa_outputs(vec![pre], vec![account_post]);
|
||||
write_nssa_outputs(vec![pre], vec![AccountPostState::new(account_post)]);
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput};
|
||||
use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs};
|
||||
|
||||
type Instruction = ();
|
||||
|
||||
@ -12,5 +12,5 @@ fn main() {
|
||||
|
||||
let account_pre1 = pre1.account.clone();
|
||||
|
||||
write_nssa_outputs(vec![pre1, pre2], vec![account_pre1]);
|
||||
write_nssa_outputs(vec![pre1, pre2], vec![AccountPostState::new(account_pre1)]);
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput};
|
||||
use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, AccountPostState, ProgramInput};
|
||||
|
||||
type Instruction = ();
|
||||
|
||||
@ -14,5 +14,5 @@ fn main() {
|
||||
let mut account_post = account_pre.clone();
|
||||
account_post.nonce += 1;
|
||||
|
||||
write_nssa_outputs(vec![pre], vec![account_post]);
|
||||
write_nssa_outputs(vec![pre], vec![AccountPostState::new(account_post)]);
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput};
|
||||
use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, AccountPostState, ProgramInput};
|
||||
|
||||
type Instruction = ();
|
||||
|
||||
@ -14,5 +14,5 @@ fn main() {
|
||||
let mut account_post = account_pre.clone();
|
||||
account_post.program_owner = [0, 1, 2, 3, 4, 5, 6, 7];
|
||||
|
||||
write_nssa_outputs(vec![pre], vec![account_post]);
|
||||
write_nssa_outputs(vec![pre], vec![AccountPostState::new(account_post)]);
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput};
|
||||
use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs};
|
||||
|
||||
type Instruction = u128;
|
||||
|
||||
@ -20,6 +20,9 @@ fn main() {
|
||||
|
||||
write_nssa_outputs(
|
||||
vec![sender_pre, receiver_pre],
|
||||
vec![sender_post, receiver_post],
|
||||
vec![
|
||||
AccountPostState::new(sender_post),
|
||||
AccountPostState::new(receiver_post),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ base58.workspace = true
|
||||
hex = "0.4.3"
|
||||
tempfile.workspace = true
|
||||
base64.workspace = true
|
||||
itertools.workspace = true
|
||||
|
||||
actix-web.workspace = true
|
||||
tokio.workspace = true
|
||||
|
||||
@ -13,7 +13,8 @@ use common::{
|
||||
requests::{
|
||||
GetAccountBalanceRequest, GetAccountBalanceResponse, GetAccountRequest,
|
||||
GetAccountResponse, GetAccountsNoncesRequest, GetAccountsNoncesResponse,
|
||||
GetBlockDataRequest, GetBlockDataResponse, GetGenesisIdRequest, GetGenesisIdResponse,
|
||||
GetBlockDataRequest, GetBlockDataResponse, GetBlockRangeDataRequest,
|
||||
GetBlockRangeDataResponse, GetGenesisIdRequest, GetGenesisIdResponse,
|
||||
GetInitialTestnetAccountsRequest, GetLastBlockRequest, GetLastBlockResponse,
|
||||
GetProgramIdsRequest, GetProgramIdsResponse, GetProofForCommitmentRequest,
|
||||
GetProofForCommitmentResponse, GetTransactionByHashRequest,
|
||||
@ -23,6 +24,7 @@ use common::{
|
||||
},
|
||||
transaction::{EncodedTransaction, NSSATransaction},
|
||||
};
|
||||
use itertools::Itertools as _;
|
||||
use log::warn;
|
||||
use nssa::{self, program::Program};
|
||||
use sequencer_core::{TransactionMalformationError, config::AccountInitialData};
|
||||
@ -33,6 +35,7 @@ use super::{JsonHandler, respond, types::err_rpc::RpcErr};
|
||||
pub const HELLO: &str = "hello";
|
||||
pub const SEND_TX: &str = "send_tx";
|
||||
pub const GET_BLOCK: &str = "get_block";
|
||||
pub const GET_BLOCK_RANGE: &str = "get_block_range";
|
||||
pub const GET_GENESIS: &str = "get_genesis";
|
||||
pub const GET_LAST_BLOCK: &str = "get_last_block";
|
||||
pub const GET_ACCOUNT_BALANCE: &str = "get_account_balance";
|
||||
@ -120,6 +123,25 @@ impl JsonHandler {
|
||||
respond(response)
|
||||
}
|
||||
|
||||
async fn process_get_block_range_data(&self, request: Request) -> Result<Value, RpcErr> {
|
||||
let get_block_req = GetBlockRangeDataRequest::parse(Some(request.params))?;
|
||||
|
||||
let blocks = {
|
||||
let state = self.sequencer_state.lock().await;
|
||||
(get_block_req.start_block_id..=get_block_req.end_block_id)
|
||||
.map(|block_id| state.block_store().get_block_at_id(block_id))
|
||||
.map_ok(|block| {
|
||||
borsh::to_vec(&HashableBlockData::from(block))
|
||||
.expect("derived BorshSerialize should never fail")
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
};
|
||||
|
||||
let response = GetBlockRangeDataResponse { blocks };
|
||||
|
||||
respond(response)
|
||||
}
|
||||
|
||||
async fn process_get_genesis(&self, request: Request) -> Result<Value, RpcErr> {
|
||||
let _get_genesis_req = GetGenesisIdRequest::parse(Some(request.params))?;
|
||||
|
||||
@ -297,6 +319,7 @@ impl JsonHandler {
|
||||
HELLO => self.process_temp_hello(request).await,
|
||||
SEND_TX => self.process_send_tx(request).await,
|
||||
GET_BLOCK => self.process_get_block_data(request).await,
|
||||
GET_BLOCK_RANGE => self.process_get_block_range_data(request).await,
|
||||
GET_GENESIS => self.process_get_genesis(request).await,
|
||||
GET_LAST_BLOCK => self.process_get_last_block(request).await,
|
||||
GET_INITIAL_TESTNET_ACCOUNTS => self.get_initial_testnet_accounts(request).await,
|
||||
|
||||
@ -19,8 +19,10 @@ borsh.workspace = true
|
||||
base58.workspace = true
|
||||
hex = "0.4.3"
|
||||
rand.workspace = true
|
||||
itertools = "0.14.0"
|
||||
itertools.workspace = true
|
||||
sha2.workspace = true
|
||||
futures.workspace = true
|
||||
async-stream = "0.3.6"
|
||||
|
||||
[dependencies.key_protocol]
|
||||
path = "../key_protocol"
|
||||
|
||||
@ -259,9 +259,9 @@ mod tests {
|
||||
override_rust_log: None,
|
||||
sequencer_addr: "http://127.0.0.1".to_string(),
|
||||
seq_poll_timeout_millis: 12000,
|
||||
seq_poll_max_blocks: 5,
|
||||
seq_tx_poll_max_blocks: 5,
|
||||
seq_poll_max_retries: 10,
|
||||
seq_poll_retry_delay_millis: 500,
|
||||
seq_block_poll_max_amount: 100,
|
||||
initial_accounts: create_initial_accounts(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,9 +9,7 @@ use serde::Serialize;
|
||||
use crate::{
|
||||
WalletCore,
|
||||
cli::{SubcommandReturnValue, WalletSubcommand},
|
||||
helperfunctions::{
|
||||
AccountPrivacyKind, HumanReadableAccount, parse_addr_with_privacy_prefix, parse_block_range,
|
||||
},
|
||||
helperfunctions::{AccountPrivacyKind, HumanReadableAccount, parse_addr_with_privacy_prefix},
|
||||
};
|
||||
|
||||
const TOKEN_DEFINITION_TYPE: u8 = 0;
|
||||
@ -180,7 +178,12 @@ impl From<TokenDefinition> for TokedDefinitionAccountView {
|
||||
fn from(value: TokenDefinition) -> Self {
|
||||
Self {
|
||||
account_type: "Token definition".to_string(),
|
||||
name: hex::encode(value.name),
|
||||
name: {
|
||||
// Assuming, that name does not have UTF-8 NULL and all zeroes are padding.
|
||||
let name_trimmed: Vec<_> =
|
||||
value.name.into_iter().take_while(|ch| *ch != 0).collect();
|
||||
String::from_utf8(name_trimmed).unwrap_or(hex::encode(value.name))
|
||||
},
|
||||
total_supply: value.total_supply,
|
||||
}
|
||||
}
|
||||
@ -280,7 +283,6 @@ impl WalletSubcommand for AccountSubcommand {
|
||||
new_subcommand.handle_subcommand(wallet_core).await
|
||||
}
|
||||
AccountSubcommand::SyncPrivate {} => {
|
||||
let last_synced_block = wallet_core.last_synced_block;
|
||||
let curr_last_block = wallet_core
|
||||
.sequencer_client
|
||||
.get_last_block()
|
||||
@ -300,13 +302,7 @@ impl WalletSubcommand for AccountSubcommand {
|
||||
|
||||
println!("Stored persistent data at {path:#?}");
|
||||
} else {
|
||||
parse_block_range(
|
||||
last_synced_block + 1,
|
||||
curr_last_block,
|
||||
wallet_core.sequencer_client.clone(),
|
||||
wallet_core,
|
||||
)
|
||||
.await?;
|
||||
wallet_core.sync_to_block(curr_last_block).await?;
|
||||
}
|
||||
|
||||
Ok(SubcommandReturnValue::SyncedToBlock(curr_last_block))
|
||||
@ -345,3 +341,47 @@ impl WalletSubcommand for AccountSubcommand {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::cli::account::{TokedDefinitionAccountView, TokenDefinition};
|
||||
|
||||
#[test]
|
||||
fn test_invalid_utf_8_name_of_token() {
|
||||
let token_def = TokenDefinition {
|
||||
account_type: 1,
|
||||
name: [137, 12, 14, 3, 5, 4],
|
||||
total_supply: 100,
|
||||
};
|
||||
|
||||
let token_def_view: TokedDefinitionAccountView = token_def.into();
|
||||
|
||||
assert_eq!(token_def_view.name, "890c0e030504");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_utf_8_name_of_token_all_bytes() {
|
||||
let token_def = TokenDefinition {
|
||||
account_type: 1,
|
||||
name: [240, 159, 146, 150, 66, 66],
|
||||
total_supply: 100,
|
||||
};
|
||||
|
||||
let token_def_view: TokedDefinitionAccountView = token_def.into();
|
||||
|
||||
assert_eq!(token_def_view.name, "💖BB");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_utf_8_name_of_token_less_bytes() {
|
||||
let token_def = TokenDefinition {
|
||||
account_type: 1,
|
||||
name: [78, 65, 77, 69, 0, 0],
|
||||
total_supply: 100,
|
||||
};
|
||||
|
||||
let token_def_view: TokedDefinitionAccountView = token_def.into();
|
||||
|
||||
assert_eq!(token_def_view.name, "NAME");
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,19 +46,19 @@ impl WalletSubcommand for ConfigSubcommand {
|
||||
wallet_core.storage.wallet_config.seq_poll_timeout_millis
|
||||
);
|
||||
}
|
||||
"seq_poll_max_blocks" => {
|
||||
println!("{}", wallet_core.storage.wallet_config.seq_poll_max_blocks);
|
||||
"seq_tx_poll_max_blocks" => {
|
||||
println!(
|
||||
"{}",
|
||||
wallet_core.storage.wallet_config.seq_tx_poll_max_blocks
|
||||
);
|
||||
}
|
||||
"seq_poll_max_retries" => {
|
||||
println!("{}", wallet_core.storage.wallet_config.seq_poll_max_retries);
|
||||
}
|
||||
"seq_poll_retry_delay_millis" => {
|
||||
"seq_block_poll_max_amount" => {
|
||||
println!(
|
||||
"{}",
|
||||
wallet_core
|
||||
.storage
|
||||
.wallet_config
|
||||
.seq_poll_retry_delay_millis
|
||||
wallet_core.storage.wallet_config.seq_block_poll_max_amount
|
||||
);
|
||||
}
|
||||
"initial_accounts" => {
|
||||
@ -80,17 +80,15 @@ impl WalletSubcommand for ConfigSubcommand {
|
||||
wallet_core.storage.wallet_config.seq_poll_timeout_millis =
|
||||
value.parse()?;
|
||||
}
|
||||
"seq_poll_max_blocks" => {
|
||||
wallet_core.storage.wallet_config.seq_poll_max_blocks = value.parse()?;
|
||||
"seq_tx_poll_max_blocks" => {
|
||||
wallet_core.storage.wallet_config.seq_tx_poll_max_blocks = value.parse()?;
|
||||
}
|
||||
"seq_poll_max_retries" => {
|
||||
wallet_core.storage.wallet_config.seq_poll_max_retries = value.parse()?;
|
||||
}
|
||||
"seq_poll_retry_delay_millis" => {
|
||||
wallet_core
|
||||
.storage
|
||||
.wallet_config
|
||||
.seq_poll_retry_delay_millis = value.parse()?;
|
||||
"seq_block_poll_max_amount" => {
|
||||
wallet_core.storage.wallet_config.seq_block_poll_max_amount =
|
||||
value.parse()?;
|
||||
}
|
||||
"initial_accounts" => {
|
||||
anyhow::bail!("Setting this field from wallet is not supported");
|
||||
@ -116,19 +114,19 @@ impl WalletSubcommand for ConfigSubcommand {
|
||||
"Sequencer client retry variable: how much time to wait between retries in milliseconds(can be zero)"
|
||||
);
|
||||
}
|
||||
"seq_poll_max_blocks" => {
|
||||
"seq_tx_poll_max_blocks" => {
|
||||
println!(
|
||||
"Sequencer client polling variable: max number of blocks to poll in parallel"
|
||||
"Sequencer client polling variable: max number of blocks to poll to find a transaction"
|
||||
);
|
||||
}
|
||||
"seq_poll_max_retries" => {
|
||||
println!(
|
||||
"Sequencer client retry variable: MAX number of retries before failing(can be zero)"
|
||||
"Sequencer client retry variable: max number of retries before failing(can be zero)"
|
||||
);
|
||||
}
|
||||
"seq_poll_retry_delay_millis" => {
|
||||
"seq_block_poll_max_amount" => {
|
||||
println!(
|
||||
"Sequencer client polling variable: how much time to wait in milliseconds between polling retries(can be zero)"
|
||||
"Sequencer client polling variable: max number of blocks to request in one polling call"
|
||||
);
|
||||
}
|
||||
"initial_accounts" => {
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
use std::{io::Write, sync::Arc};
|
||||
use std::io::Write;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use common::sequencer_client::SequencerClient;
|
||||
use nssa::program::Program;
|
||||
|
||||
use crate::{
|
||||
@ -16,7 +15,7 @@ use crate::{
|
||||
token::TokenProgramAgnosticSubcommand,
|
||||
},
|
||||
},
|
||||
helperfunctions::{fetch_config, fetch_persistent_storage, parse_block_range},
|
||||
helperfunctions::{fetch_config, fetch_persistent_storage},
|
||||
};
|
||||
|
||||
pub mod account;
|
||||
@ -171,29 +170,20 @@ pub async fn execute_subcommand(command: Command) -> Result<SubcommandReturnValu
|
||||
|
||||
pub async fn execute_continuous_run() -> Result<()> {
|
||||
let config = fetch_config().await?;
|
||||
let seq_client = Arc::new(SequencerClient::new(config.sequencer_addr.clone())?);
|
||||
let mut wallet_core = WalletCore::start_from_config_update_chain(config.clone()).await?;
|
||||
|
||||
let mut latest_block_num = seq_client.get_last_block().await?.last_block;
|
||||
let mut curr_last_block = latest_block_num;
|
||||
|
||||
loop {
|
||||
parse_block_range(
|
||||
curr_last_block,
|
||||
latest_block_num,
|
||||
seq_client.clone(),
|
||||
&mut wallet_core,
|
||||
)
|
||||
.await?;
|
||||
|
||||
curr_last_block = latest_block_num + 1;
|
||||
let latest_block_num = wallet_core
|
||||
.sequencer_client
|
||||
.get_last_block()
|
||||
.await?
|
||||
.last_block;
|
||||
wallet_core.sync_to_block(latest_block_num).await?;
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_millis(
|
||||
config.seq_poll_timeout_millis,
|
||||
))
|
||||
.await;
|
||||
|
||||
latest_block_num = seq_client.get_last_block().await?.last_block;
|
||||
}
|
||||
}
|
||||
|
||||
@ -254,13 +244,7 @@ pub async fn execute_keys_restoration(password: String, depth: u32) -> Result<()
|
||||
|
||||
println!("Last block is {last_block}");
|
||||
|
||||
parse_block_range(
|
||||
1,
|
||||
last_block,
|
||||
wallet_core.sequencer_client.clone(),
|
||||
&mut wallet_core,
|
||||
)
|
||||
.await?;
|
||||
wallet_core.sync_to_block(last_block).await?;
|
||||
|
||||
println!("Private tree clean up start");
|
||||
|
||||
|
||||
@ -135,12 +135,12 @@ pub struct WalletConfig {
|
||||
pub sequencer_addr: String,
|
||||
/// Sequencer polling duration for new blocks in milliseconds
|
||||
pub seq_poll_timeout_millis: u64,
|
||||
/// Sequencer polling max number of blocks
|
||||
pub seq_poll_max_blocks: usize,
|
||||
/// Sequencer polling max number of blocks to find transaction
|
||||
pub seq_tx_poll_max_blocks: usize,
|
||||
/// Sequencer polling max number error retries
|
||||
pub seq_poll_max_retries: u64,
|
||||
/// Sequencer polling error retry delay in milliseconds
|
||||
pub seq_poll_retry_delay_millis: u64,
|
||||
/// Max amount of blocks to poll in one request
|
||||
pub seq_block_poll_max_amount: u64,
|
||||
/// Initial accounts for wallet
|
||||
pub initial_accounts: Vec<InitialAccountData>,
|
||||
}
|
||||
@ -151,9 +151,9 @@ impl Default for WalletConfig {
|
||||
override_rust_log: None,
|
||||
sequencer_addr: "http://127.0.0.1:3040".to_string(),
|
||||
seq_poll_timeout_millis: 12000,
|
||||
seq_poll_max_blocks: 5,
|
||||
seq_tx_poll_max_blocks: 5,
|
||||
seq_poll_max_retries: 5,
|
||||
seq_poll_retry_delay_millis: 500,
|
||||
seq_block_poll_max_amount: 100,
|
||||
initial_accounts: {
|
||||
let init_acc_json = r#"
|
||||
[
|
||||
|
||||
@ -1,21 +1,16 @@
|
||||
use std::{path::PathBuf, str::FromStr, sync::Arc};
|
||||
use std::{path::PathBuf, str::FromStr};
|
||||
|
||||
use anyhow::Result;
|
||||
use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
|
||||
use common::{
|
||||
block::HashableBlockData, sequencer_client::SequencerClient, transaction::NSSATransaction,
|
||||
};
|
||||
use key_protocol::{
|
||||
key_management::key_tree::traits::KeyNode as _, key_protocol_core::NSSAUserData,
|
||||
};
|
||||
use nssa::{Account, privacy_preserving_transaction::message::EncryptedAccountData};
|
||||
use key_protocol::key_protocol_core::NSSAUserData;
|
||||
use nssa::Account;
|
||||
use nssa_core::account::Nonce;
|
||||
use rand::{RngCore, rngs::OsRng};
|
||||
use serde::Serialize;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
use crate::{
|
||||
HOME_DIR_ENV_VAR, WalletCore,
|
||||
HOME_DIR_ENV_VAR,
|
||||
config::{
|
||||
InitialAccountData, InitialAccountDataPrivate, InitialAccountDataPublic,
|
||||
PersistentAccountDataPrivate, PersistentAccountDataPublic, PersistentStorage, WalletConfig,
|
||||
@ -230,125 +225,6 @@ impl From<Account> for HumanReadableAccount {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn parse_block_range(
|
||||
start: u64,
|
||||
stop: u64,
|
||||
seq_client: Arc<SequencerClient>,
|
||||
wallet_core: &mut WalletCore,
|
||||
) -> Result<()> {
|
||||
for block_id in start..(stop + 1) {
|
||||
let block =
|
||||
borsh::from_slice::<HashableBlockData>(&seq_client.get_block(block_id).await?.block)?;
|
||||
|
||||
for tx in block.transactions {
|
||||
let nssa_tx = NSSATransaction::try_from(&tx)?;
|
||||
|
||||
if let NSSATransaction::PrivacyPreserving(tx) = nssa_tx {
|
||||
let mut affected_accounts = vec![];
|
||||
|
||||
for (acc_account_id, (key_chain, _)) in
|
||||
&wallet_core.storage.user_data.default_user_private_accounts
|
||||
{
|
||||
let view_tag = EncryptedAccountData::compute_view_tag(
|
||||
key_chain.nullifer_public_key.clone(),
|
||||
key_chain.incoming_viewing_public_key.clone(),
|
||||
);
|
||||
|
||||
for (ciph_id, encrypted_data) in tx
|
||||
.message()
|
||||
.encrypted_private_post_states
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
if encrypted_data.view_tag == view_tag {
|
||||
let ciphertext = &encrypted_data.ciphertext;
|
||||
let commitment = &tx.message.new_commitments[ciph_id];
|
||||
let shared_secret = key_chain
|
||||
.calculate_shared_secret_receiver(encrypted_data.epk.clone());
|
||||
|
||||
let res_acc = nssa_core::EncryptionScheme::decrypt(
|
||||
ciphertext,
|
||||
&shared_secret,
|
||||
commitment,
|
||||
ciph_id as u32,
|
||||
);
|
||||
|
||||
if let Some(res_acc) = res_acc {
|
||||
println!(
|
||||
"Received new account for account_id {acc_account_id:#?} with account object {res_acc:#?}"
|
||||
);
|
||||
|
||||
affected_accounts.push((*acc_account_id, res_acc));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for keys_node in wallet_core
|
||||
.storage
|
||||
.user_data
|
||||
.private_key_tree
|
||||
.key_map
|
||||
.values()
|
||||
{
|
||||
let acc_account_id = keys_node.account_id();
|
||||
let key_chain = &keys_node.value.0;
|
||||
|
||||
let view_tag = EncryptedAccountData::compute_view_tag(
|
||||
key_chain.nullifer_public_key.clone(),
|
||||
key_chain.incoming_viewing_public_key.clone(),
|
||||
);
|
||||
|
||||
for (ciph_id, encrypted_data) in tx
|
||||
.message()
|
||||
.encrypted_private_post_states
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
if encrypted_data.view_tag == view_tag {
|
||||
let ciphertext = &encrypted_data.ciphertext;
|
||||
let commitment = &tx.message.new_commitments[ciph_id];
|
||||
let shared_secret = key_chain
|
||||
.calculate_shared_secret_receiver(encrypted_data.epk.clone());
|
||||
|
||||
let res_acc = nssa_core::EncryptionScheme::decrypt(
|
||||
ciphertext,
|
||||
&shared_secret,
|
||||
commitment,
|
||||
ciph_id as u32,
|
||||
);
|
||||
|
||||
if let Some(res_acc) = res_acc {
|
||||
println!(
|
||||
"Received new account for account_id {acc_account_id:#?} with account object {res_acc:#?}"
|
||||
);
|
||||
|
||||
affected_accounts.push((acc_account_id, res_acc));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (affected_account_id, new_acc) in affected_accounts {
|
||||
wallet_core
|
||||
.storage
|
||||
.insert_private_account_data(affected_account_id, new_acc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wallet_core.last_synced_block = block_id;
|
||||
wallet_core.store_persistent_data().await?;
|
||||
|
||||
println!(
|
||||
"Block at id {block_id} with timestamp {} parsed",
|
||||
block.timestamp
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@ -5,13 +5,17 @@ use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
|
||||
use chain_storage::WalletChainStore;
|
||||
use common::{
|
||||
error::ExecutionFailureKind,
|
||||
sequencer_client::{SequencerClient, json::SendTxResponse},
|
||||
rpc_primitives::requests::SendTxResponse,
|
||||
sequencer_client::SequencerClient,
|
||||
transaction::{EncodedTransaction, NSSATransaction},
|
||||
};
|
||||
use config::WalletConfig;
|
||||
use key_protocol::key_management::key_tree::chain_index::ChainIndex;
|
||||
use key_protocol::key_management::key_tree::{chain_index::ChainIndex, traits::KeyNode as _};
|
||||
use log::info;
|
||||
use nssa::{Account, AccountId, PrivacyPreservingTransaction, program::Program};
|
||||
use nssa::{
|
||||
Account, AccountId, PrivacyPreservingTransaction,
|
||||
privacy_preserving_transaction::message::EncryptedAccountData, program::Program,
|
||||
};
|
||||
use nssa_core::{Commitment, MembershipProof, SharedSecretKey, program::InstructionData};
|
||||
pub use privacy_preserving_tx::PrivacyPreservingAccount;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
@ -299,4 +303,93 @@ impl WalletCore {
|
||||
shared_secrets,
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn sync_to_block(&mut self, block_id: u64) -> Result<()> {
|
||||
use futures::TryStreamExt as _;
|
||||
|
||||
if self.last_synced_block >= block_id {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let before_polling = std::time::Instant::now();
|
||||
|
||||
let poller = self.poller.clone();
|
||||
let mut blocks =
|
||||
std::pin::pin!(poller.poll_block_range(self.last_synced_block + 1..=block_id));
|
||||
|
||||
while let Some(block) = blocks.try_next().await? {
|
||||
for tx in block.transactions {
|
||||
let nssa_tx = NSSATransaction::try_from(&tx)?;
|
||||
self.sync_private_accounts_with_tx(nssa_tx);
|
||||
}
|
||||
|
||||
self.last_synced_block = block.block_id;
|
||||
self.store_persistent_data().await?;
|
||||
}
|
||||
|
||||
println!(
|
||||
"Synced to block {block_id} in {:?}",
|
||||
before_polling.elapsed()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sync_private_accounts_with_tx(&mut self, tx: NSSATransaction) {
|
||||
let NSSATransaction::PrivacyPreserving(tx) = tx else {
|
||||
return;
|
||||
};
|
||||
|
||||
let private_account_key_chains = self
|
||||
.storage
|
||||
.user_data
|
||||
.default_user_private_accounts
|
||||
.iter()
|
||||
.map(|(acc_account_id, (key_chain, _))| (*acc_account_id, key_chain))
|
||||
.chain(
|
||||
self.storage
|
||||
.user_data
|
||||
.private_key_tree
|
||||
.key_map
|
||||
.values()
|
||||
.map(|keys_node| (keys_node.account_id(), &keys_node.value.0)),
|
||||
);
|
||||
|
||||
let affected_accounts = private_account_key_chains
|
||||
.flat_map(|(acc_account_id, key_chain)| {
|
||||
let view_tag = EncryptedAccountData::compute_view_tag(
|
||||
key_chain.nullifer_public_key.clone(),
|
||||
key_chain.incoming_viewing_public_key.clone(),
|
||||
);
|
||||
|
||||
tx.message()
|
||||
.encrypted_private_post_states
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(move |(_, encrypted_data)| encrypted_data.view_tag == view_tag)
|
||||
.filter_map(|(ciph_id, encrypted_data)| {
|
||||
let ciphertext = &encrypted_data.ciphertext;
|
||||
let commitment = &tx.message.new_commitments[ciph_id];
|
||||
let shared_secret =
|
||||
key_chain.calculate_shared_secret_receiver(encrypted_data.epk.clone());
|
||||
|
||||
nssa_core::EncryptionScheme::decrypt(
|
||||
ciphertext,
|
||||
&shared_secret,
|
||||
commitment,
|
||||
ciph_id as u32,
|
||||
)
|
||||
})
|
||||
.map(move |res_acc| (acc_account_id, res_acc))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for (affected_account_id, new_acc) in affected_accounts {
|
||||
println!(
|
||||
"Received new account for account_id {affected_account_id:#?} with account object {new_acc:#?}"
|
||||
);
|
||||
self.storage
|
||||
.insert_private_account_data(affected_account_id, new_acc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use common::sequencer_client::SequencerClient;
|
||||
use common::{block::HashableBlockData, sequencer_client::SequencerClient};
|
||||
use log::{info, warn};
|
||||
|
||||
use crate::config::WalletConfig;
|
||||
@ -9,21 +9,21 @@ use crate::config::WalletConfig;
|
||||
#[derive(Clone)]
|
||||
/// Helperstruct to poll transactions
|
||||
pub struct TxPoller {
|
||||
pub polling_max_blocks_to_query: usize,
|
||||
pub polling_max_error_attempts: u64,
|
||||
polling_max_blocks_to_query: usize,
|
||||
polling_max_error_attempts: u64,
|
||||
// TODO: This should be Duration
|
||||
pub polling_error_delay_millis: u64,
|
||||
pub polling_delay_millis: u64,
|
||||
pub client: Arc<SequencerClient>,
|
||||
polling_delay_millis: u64,
|
||||
block_poll_max_amount: u64,
|
||||
client: Arc<SequencerClient>,
|
||||
}
|
||||
|
||||
impl TxPoller {
|
||||
pub fn new(config: WalletConfig, client: Arc<SequencerClient>) -> Self {
|
||||
Self {
|
||||
polling_delay_millis: config.seq_poll_timeout_millis,
|
||||
polling_max_blocks_to_query: config.seq_poll_max_blocks,
|
||||
polling_max_blocks_to_query: config.seq_tx_poll_max_blocks,
|
||||
polling_max_error_attempts: config.seq_poll_max_retries,
|
||||
polling_error_delay_millis: config.seq_poll_retry_delay_millis,
|
||||
block_poll_max_amount: config.seq_block_poll_max_amount,
|
||||
client: client.clone(),
|
||||
}
|
||||
}
|
||||
@ -66,4 +66,28 @@ impl TxPoller {
|
||||
|
||||
anyhow::bail!("Transaction not found in preconfigured amount of blocks");
|
||||
}
|
||||
|
||||
pub fn poll_block_range(
|
||||
&self,
|
||||
range: std::ops::RangeInclusive<u64>,
|
||||
) -> impl futures::Stream<Item = Result<HashableBlockData>> {
|
||||
async_stream::stream! {
|
||||
let mut chunk_start = *range.start();
|
||||
|
||||
loop {
|
||||
let chunk_end = std::cmp::min(chunk_start + self.block_poll_max_amount - 1, *range.end());
|
||||
|
||||
let blocks = self.client.get_block_range(chunk_start..=chunk_end).await?.blocks;
|
||||
for block in blocks {
|
||||
let block = borsh::from_slice::<HashableBlockData>(&block)?;
|
||||
yield Ok(block);
|
||||
}
|
||||
|
||||
chunk_start = chunk_end + 1;
|
||||
if chunk_start > *range.end() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use common::{error::ExecutionFailureKind, sequencer_client::json::SendTxResponse};
|
||||
use common::{error::ExecutionFailureKind, rpc_primitives::requests::SendTxResponse};
|
||||
use nssa::AccountId;
|
||||
|
||||
use super::{NativeTokenTransfer, auth_transfer_preparation};
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
use std::vec;
|
||||
|
||||
use common::{error::ExecutionFailureKind, sequencer_client::json::SendTxResponse};
|
||||
use common::{error::ExecutionFailureKind, rpc_primitives::requests::SendTxResponse};
|
||||
use nssa::{AccountId, program::Program};
|
||||
use nssa_core::{NullifierPublicKey, SharedSecretKey, encryption::IncomingViewingPublicKey};
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use common::{error::ExecutionFailureKind, sequencer_client::json::SendTxResponse};
|
||||
use common::{error::ExecutionFailureKind, rpc_primitives::requests::SendTxResponse};
|
||||
use nssa::{
|
||||
AccountId, PublicTransaction,
|
||||
program::Program,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use common::{error::ExecutionFailureKind, sequencer_client::json::SendTxResponse};
|
||||
use common::{error::ExecutionFailureKind, rpc_primitives::requests::SendTxResponse};
|
||||
use nssa::AccountId;
|
||||
use nssa_core::{NullifierPublicKey, SharedSecretKey, encryption::IncomingViewingPublicKey};
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use common::{error::ExecutionFailureKind, sequencer_client::json::SendTxResponse};
|
||||
use common::{error::ExecutionFailureKind, rpc_primitives::requests::SendTxResponse};
|
||||
use nssa::AccountId;
|
||||
use nssa_core::SharedSecretKey;
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use common::{error::ExecutionFailureKind, sequencer_client::json::SendTxResponse};
|
||||
use common::{error::ExecutionFailureKind, rpc_primitives::requests::SendTxResponse};
|
||||
use nssa::{AccountId, program::Program};
|
||||
use nssa_core::{
|
||||
NullifierPublicKey, SharedSecretKey, encryption::IncomingViewingPublicKey,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user