mirror of
https://github.com/logos-blockchain/lssa.git
synced 2026-01-02 13:23:10 +00:00
Merge branch 'main' into schouhy/implement-privacy-preserving-tail-calls
This commit is contained in:
commit
f54cdf4a4c
@ -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",
|
||||
|
||||
635
README.md
635
README.md
@ -1,15 +1,84 @@
|
||||
# nescience-testnet
|
||||
This repo serves for Nescience testnet
|
||||
# Nescience
|
||||
|
||||
Nescience State Separation Architecture (NSSA) is a programmable blockchain system that introduces a clean separation between public and private states, while keeping them fully interoperable. It lets developers build apps that can operate across both transparent and privacy-preserving accounts. Privacy is handled automatically by the protocol through zero-knowledge proofs (ZKPs). The result is a programmable blockchain where privacy comes built-in.
|
||||
|
||||
## Background
|
||||
|
||||
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 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
|
||||
|
||||
Our goal is to enable full programmability within this hybrid model, matching the flexibility and composability of public blockchains. Developers write and deploy programs in NSSA just as they would on any other blockchain. Privacy, along with the ability to execute programs involving any combination of public and private accounts, is handled entirely at the protocol level and available out of the box for all programs. From the program’s perspective, all accounts are indistinguishable. This abstraction allows developers to focus purely on business logic, while the system transparently enforces privacy and consistency guarantees.
|
||||
|
||||
To the best of our knowledge, this approach is unique to Nescience. Other programmable blockchains with a focus on privacy typically adopt a developer-driven model for private execution, meaning that dApp logic must explicitly handle private inputs correctly. In contrast, Nescience handles privacy at the protocol level, so developers do not need to modify their programs—private and public accounts are treated uniformly, and privacy-preserving execution is available out of the box.
|
||||
|
||||
### Example: creating and transferring tokens across states
|
||||
|
||||
1. Token creation (public execution):
|
||||
- Alice submits a transaction to execute the token program `New` function on-chain.
|
||||
- A new public token account is created, representing the token.
|
||||
- The minted tokens are recorded on-chain and fully visible on Alice's public account.
|
||||
2. Transfer from public to private (local / privacy-preserving execution)
|
||||
- Alice executes the token program `Transfer` function locally, specifying a Bob’s private account as recipient.
|
||||
- A ZKP of correct execution is generated.
|
||||
- The proof is submitted to the blockchain, and validator nodes verify it.
|
||||
- Alice's public account balance is modified accordingly.
|
||||
- Bob’s private account and balance remain hidden, while the transfer is provably valid.
|
||||
3. Transferring private to public (local / privacy-preserving execution)
|
||||
- Bob executes the token program `Transfer` function locally, specifying a Charlie’s public account as recipient.
|
||||
- A ZKP of correct execution is generated.
|
||||
- Bob’s private account and balance still remain hidden.
|
||||
- Charlie's public account is modified with the new tokens added.
|
||||
4. Transferring public to public (public execution):
|
||||
- Alice submits a transaction to execute the token program `Transfer` function on-chain, specifying Charlie's public account as recipient.
|
||||
- The execution is handled on-chain without ZKPs involved.
|
||||
- Alice's and Charlie's accounts are modified according to the transaction.
|
||||
|
||||
#### Key points:
|
||||
- The same token program is used in all executions.
|
||||
- The difference lies in execution mode: public executions update visible accounts on-chain, while private executions rely on ZKPs.
|
||||
- Validators only need to verify proofs for privacy-preserving transactions, keeping processing efficient.
|
||||
|
||||
### The account’s model
|
||||
|
||||
To achieve both state separation and full programmability, NSSA adopts a stateless program model. Programs do not hold internal state. Instead, all persistent data resides in accounts explicitly passed to the program during execution. This design enables fine-grained control over access and visibility while maintaining composability across public and private states.
|
||||
|
||||
### Execution types
|
||||
|
||||
Execution is divided into two fundamentally distinct types based on how they are processed: public execution, which is executed transparently on-chain, and private execution, which occurs off-chain. For private execution, the blockchain relies on ZKPs to verify the correctness of execution and ensure that all system invariants are preserved.
|
||||
|
||||
Both public and private executions of the same program are enforced to use the same Risc0 VM bytecode. For public transactions, programs are executed directly on-chain like any standard RISC-V VM execution, without generating or verifying proofs. For privacy-preserving transactions, users generate Risc0 ZKPs of correct execution, and validator nodes only verify these proofs rather than re-executing the program. This design ensures that from a validator’s perspective, public transactions are processed as quickly as any RISC-V–based VM, while verification of ZKPs keeps privacy-preserving transactions efficient as well. Additionally, the system naturally supports parallel execution similar to Solana, further increasing throughput. The main computational bottleneck for privacy-preserving transactions lies on the user side, in generating zk proofs.
|
||||
|
||||
### Resources
|
||||
- [IFT Research call](https://forum.vac.dev/t/ift-research-call-september-10th-2025-updates-on-the-development-of-nescience/566)
|
||||
- [NSSA v0.2 specs](https://www.notion.so/NSSA-v0-2-specifications-2848f96fb65c800c9818e6f66d9be8f2)
|
||||
- [Choice of VM/zkVM](https://www.notion.so/Conclusion-on-the-chosen-VM-and-zkVM-for-NSSA-2318f96fb65c806a810ed1300f56992d)
|
||||
- [NSSA vs other privacy projects](https://www.notion.so/Privacy-projects-comparison-2688f96fb65c8096b694ecf7e4deca30)
|
||||
- [NSSA state model](https://www.notion.so/Public-state-model-decision-2388f96fb65c80758b20c76de07b1fcc)
|
||||
- [NSSA sequencer specs](https://www.notion.so/Sequencer-specs-2428f96fb65c802da2bfea7b0b214ecb)
|
||||
- [NSSA sequencer code](https://www.notion.so/NSSA-sequencer-pseudocode-2508f96fb65c805e8859e047dffd6785)
|
||||
- [NSSA Token program desing](https://www.notion.so/Token-program-design-2538f96fb65c80a1b4bdc4fd9dd162d7)
|
||||
- [NSSA cross program calls](https://www.notion.so/NSSA-cross-program-calls-Tail-call-model-proposal-extended-version-2838f96fb65c8096b3a2d390444193b6)
|
||||
|
||||
For more details you can read [here](https://notes.status.im/Ya2wDpIyQquoiRiuEIM8hQ?view).
|
||||
|
||||
# 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
|
||||
@ -31,3 +100,561 @@ Then restart your shell and run
|
||||
```sh
|
||||
rzup install
|
||||
```
|
||||
|
||||
# Run tests
|
||||
|
||||
The NSSA repository includes both unit and integration test suites.
|
||||
|
||||
### Unit tests
|
||||
|
||||
```bash
|
||||
# RISC0_DEV_MODE=1 is used to skip proof generation and reduce test runtime overhead
|
||||
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
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
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:
|
||||
```bash
|
||||
[2025-11-13T19:50:29Z INFO sequencer_runner] Sequencer core set up
|
||||
[2025-11-13T19:50:29Z INFO network] Starting http server at 0.0.0.0:3040
|
||||
[2025-11-13T19:50:29Z INFO actix_server::builder] starting 8 workers
|
||||
[2025-11-13T19:50:29Z INFO sequencer_runner] HTTP server started
|
||||
[2025-11-13T19:50:29Z INFO sequencer_runner] Starting main sequencer loop
|
||||
[2025-11-13T19:50:29Z INFO actix_server::server] Tokio runtime found; starting in existing Tokio runtime
|
||||
[2025-11-13T19:50:29Z INFO actix_server::server] starting service: "actix-web-service-0.0.0.0:3040", workers: 8, listening on: 0.0.0.0:3040
|
||||
[2025-11-13T19:50:39Z INFO sequencer_runner] Collecting transactions from mempool, block creation
|
||||
[2025-11-13T19:50:39Z INFO sequencer_core] Created block with 0 transactions in 0 seconds
|
||||
[2025-11-13T19:50:39Z INFO sequencer_runner] Block with id 2 created
|
||||
[2025-11-13T19:50:39Z INFO sequencer_runner] Waiting for new transactions
|
||||
```
|
||||
|
||||
# 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
|
||||
|
||||
# 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:
|
||||
|
||||
```bash
|
||||
wallet check-health
|
||||
```
|
||||
|
||||
You should see `✅ All looks good!`.
|
||||
|
||||
### The commands
|
||||
|
||||
The wallet provides several commands to interact with the node and query state. To see the full list, run `wallet help`:
|
||||
|
||||
```bash
|
||||
Commands:
|
||||
auth-transfer Authenticated transfer subcommand
|
||||
chain-info Generic chain info subcommand
|
||||
account Account view and sync subcommand
|
||||
pinata Pinata program interaction subcommand
|
||||
token Token program interaction subcommand
|
||||
check-health Check the wallet can connect to the node and builtin local programs match the remote versions
|
||||
```
|
||||
|
||||
### Accounts
|
||||
|
||||
> [!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
|
||||
new Produce new public or private account
|
||||
sync-private Sync private accounts
|
||||
help Print this message or the help of the given subcommand(s)
|
||||
```
|
||||
|
||||
#### Create a new public account
|
||||
|
||||
You can create both public and private accounts through the CLI. For example:
|
||||
|
||||
```bash
|
||||
wallet account new public
|
||||
|
||||
# Output:
|
||||
Generated new account with account_id Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ
|
||||
```
|
||||
|
||||
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 id with yours
|
||||
wallet account get --account-id Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ
|
||||
|
||||
# Output:
|
||||
Account is Uninitialized
|
||||
```
|
||||
|
||||
> [!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.
|
||||
|
||||
Initialize the account by running:
|
||||
|
||||
```bash
|
||||
# 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 --account-id Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ
|
||||
```
|
||||
|
||||
After it completes, check the updated account status:
|
||||
|
||||
```bash
|
||||
wallet account get --account-id Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ
|
||||
|
||||
# Output:
|
||||
Account owned by authenticated transfer program
|
||||
{"balance":0}
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
```bash
|
||||
# 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 --account-id Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ
|
||||
|
||||
# Output:
|
||||
Account owned by authenticated transfer program
|
||||
{"balance":150}
|
||||
```
|
||||
|
||||
### Native token transfers: executing the Authenticated transfers program
|
||||
|
||||
NSSA comes with a program for managing and transferring native tokens. Run `wallet auth-transfer` to see the options available:
|
||||
```bash
|
||||
Commands:
|
||||
init Initialize account under authenticated transfer program
|
||||
send Send native tokens from one account to another with variable privacy
|
||||
help Print this message or the help of the given subcommand(s)
|
||||
```
|
||||
|
||||
We have already used the `init` command. The `send` command is used to execute the `Transfer` function of the authenticated program.
|
||||
Let's try it. For that we need to create another account for the recipient of the transfer.
|
||||
|
||||
```bash
|
||||
wallet account new public
|
||||
|
||||
# Output:
|
||||
Generated new account with account_id Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS
|
||||
```
|
||||
|
||||
|
||||
> [!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.
|
||||
|
||||
```bash
|
||||
wallet auth-transfer send \
|
||||
--from Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ \
|
||||
--to Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS \
|
||||
--amount 37
|
||||
```
|
||||
|
||||
Once that succeeds we can check the states.
|
||||
|
||||
```bash
|
||||
# Sender account
|
||||
wallet account get --account-id Public/HrA8TVjBS8UVf9akV7LRhyh6k4c7F6PS7PvqgtPmKAT8
|
||||
|
||||
# Output:
|
||||
Account owned by authenticated transfer program
|
||||
{"balance":113}
|
||||
```
|
||||
|
||||
```bash
|
||||
# Recipient account
|
||||
wallet account get --account-id Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS
|
||||
|
||||
# Output:
|
||||
Account owned by authenticated transfer program
|
||||
{"balance":37}
|
||||
```
|
||||
|
||||
#### 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 account_id Private/HacPU3hakLYzWtSqUPw6TUr8fqoMieVWovsUR6sJf7cL
|
||||
With npk e6366f79d026c8bd64ae6b3d601f0506832ec682ab54897f205fffe64ec0d951
|
||||
With ipk 02ddc96d0eb56e00ce14994cfdaec5ae1f76244180a919545983156e3519940a17
|
||||
```
|
||||
|
||||
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 --account-id Private/HacPU3hakLYzWtSqUPw6TUr8fqoMieVWovsUR6sJf7cL
|
||||
|
||||
# Output:
|
||||
Account is Uninitialized
|
||||
```
|
||||
Unlike public accounts, private accounts are never visible to the network. They exist only in your local wallet storage.
|
||||
|
||||
#### Sending tokens from the public account to the private account
|
||||
|
||||
Sending tokens to an uninitialized private account causes the Authenticated-Transfers program to claim it. Just like with public accounts.
|
||||
This happens because program execution logic does not depend on whether the involved accounts are public or private.
|
||||
|
||||
Let’s send 17 tokens to the new private account.
|
||||
|
||||
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.
|
||||
|
||||
```bash
|
||||
wallet auth-transfer send \
|
||||
--from Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS \
|
||||
--to Private/HacPU3hakLYzWtSqUPw6TUr8fqoMieVWovsUR6sJf7cL \
|
||||
--amount 17
|
||||
```
|
||||
|
||||
After it succeeds, check both accounts:
|
||||
|
||||
```bash
|
||||
# Public sender account
|
||||
wallet account get --account-id Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS
|
||||
|
||||
# Output:
|
||||
Account owned by authenticated transfer program
|
||||
{"balance":20}
|
||||
```
|
||||
|
||||
```bash
|
||||
# Private recipient account
|
||||
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.
|
||||
|
||||
#### Digression: modifying private accounts
|
||||
|
||||
As a general rule, private accounts can only be modified through a program execution performed by their owner. That is, the person who holds the private key for that account. There is one exception: an uninitialized private account may be initialized by any user, without requiring the private key. After initialization, only the owner can modify it.
|
||||
|
||||
This mechanism enables a common use case: transferring funds from any account (public or private) to a private account owned by someone else. For such transfers, the recipient’s private account must be uninitialized.
|
||||
|
||||
|
||||
#### Sending tokens from the public account to a private account owned by someone else
|
||||
|
||||
For this tutorial, we’ll simulate that scenario by creating a new private account that we own, but we’ll treat it as if it belonged to someone else.
|
||||
|
||||
Let's create a new (uninitialized) private account like before:
|
||||
|
||||
```bash
|
||||
wallet account new private
|
||||
|
||||
# Output:
|
||||
Generated new account with account_id Private/AukXPRBmrYVqoqEW2HTs7N3hvTn3qdNFDcxDHVr5hMm5
|
||||
With npk 0c95ebc4b3830f53da77bb0b80a276a776cdcf6410932acc718dcdb3f788a00e
|
||||
With ipk 039fd12a3674a880d3e917804129141e4170d419d1f9e28a3dcf979c1f2369cb72
|
||||
```
|
||||
|
||||
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 \
|
||||
--from Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS \
|
||||
--to-npk 0c95ebc4b3830f53da77bb0b80a276a776cdcf6410932acc718dcdb3f788a00e \
|
||||
--to-ipk 039fd12a3674a880d3e917804129141e4170d419d1f9e28a3dcf979c1f2369cb72 \
|
||||
--amount 3
|
||||
```
|
||||
|
||||
The command above produces a privacy-preserving transaction, which may take a few minutes to complete. The updated values of the private account are encrypted and included in the transaction.
|
||||
|
||||
Once the transaction is accepted, the recipient must run `wallet account sync-private`. This command scans the chain for encrypted values that belong to their private accounts and updates the local versions accordingly.
|
||||
|
||||
|
||||
#### Transfers in other combinations of public and private accounts
|
||||
|
||||
We’ve shown how to use the authenticated-transfers program for transfers between two public accounts, and for transfers from a public sender to a private recipient. Sending tokens from a private account (whether to a public account or to another private account) works in essentially the same way.
|
||||
|
||||
### 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
|
||||
Commands:
|
||||
new Produce a new token
|
||||
send Send tokens from one account to another with variable privacy
|
||||
help Print this message or the help of the given subcommand(s)
|
||||
```
|
||||
|
||||
|
||||
> [!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
|
||||
|
||||
To create a new token, simply run `wallet token new`. This will create a transaction to execute the `New` function of the token program.
|
||||
The command expects a name, the desired total supply, and two uninitialized accounts:
|
||||
- One that will be initialized as the token definition account for the new token.
|
||||
- Another that will be initialized as a token holding account and receive the token’s entire initial supply.
|
||||
|
||||
|
||||
##### New token with both definition and supply accounts set as public
|
||||
|
||||
For example, let's create two new (uninitialized) public accounts and then use them to create a new token.
|
||||
|
||||
```bash
|
||||
wallet account new public
|
||||
|
||||
# Output:
|
||||
Generated new account with account_id Public/4X9kAcnCZ1Ukkbm3nywW9xfCNPK8XaMWCk3zfs1sP4J7
|
||||
```
|
||||
|
||||
```bash
|
||||
wallet account new public
|
||||
|
||||
# Output:
|
||||
Generated new account with account_id Public/9RRSMm3w99uCD2Jp2Mqqf6dfc8me2tkFRE9HeU2DFftw
|
||||
```
|
||||
|
||||
Now we use them to create a new token. Let's call it the "Token A"
|
||||
|
||||
```bash
|
||||
wallet token new \
|
||||
--name TOKENA \
|
||||
--total-supply 1337 \
|
||||
--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 --account-id Public/4X9kAcnCZ1Ukkbm3nywW9xfCNPK8XaMWCk3zfs1sP4J7
|
||||
|
||||
# Output:
|
||||
Definition account owned by token program
|
||||
{"account_type":"Token definition","name":"TOKENA","total_supply":1337}
|
||||
```
|
||||
|
||||
```bash
|
||||
wallet account get --account-id Public/9RRSMm3w99uCD2Jp2Mqqf6dfc8me2tkFRE9HeU2DFftw
|
||||
|
||||
# Output:
|
||||
Holding account owned by token program
|
||||
{"account_type":"Token holding","definition_id":"4X9kAcnCZ1Ukkbm3nywW9xfCNPK8XaMWCk3zfs1sP4J7","balance":1337}
|
||||
```
|
||||
|
||||
##### New token with public account definition but private holding account for initial supply
|
||||
|
||||
Let’s create a new token, but this time using a public definition account and a private holding account to store the entire supply.
|
||||
|
||||
Since we can’t reuse the accounts from the previous example, we need to create fresh ones for this case.
|
||||
|
||||
```bash
|
||||
wallet account new public
|
||||
|
||||
# Output:
|
||||
Generated new account with account_id Public/GQ3C8rbprTtQUCvkuVBRu3v9wvUvjafCMFqoSPvTEVii
|
||||
```
|
||||
|
||||
```bash
|
||||
wallet account new private
|
||||
|
||||
|
||||
# Output:
|
||||
Generated new account with account_id Private/HMRHZdPw4pbyPVZHNGrV6K5AA95wACFsHTRST84fr3CF
|
||||
With npk 6a2dfe433cf28e525aa0196d719be3c16146f7ee358ca39595323f94fde38f93
|
||||
With ipk 03d59abf4bee974cc12ddb44641c19f0b5441fef39191f047c988c29a77252a577
|
||||
```
|
||||
|
||||
And we use them to create the token.
|
||||
|
||||
Now we use them to create a new token. Let's call it "Token B".
|
||||
|
||||
```bash
|
||||
wallet token new \
|
||||
--name TOKENB \
|
||||
--total-supply 7331 \
|
||||
--definition-account-id Public/GQ3C8rbprTtQUCvkuVBRu3v9wvUvjafCMFqoSPvTEVii \
|
||||
--supply-account-id Private/HMRHZdPw4pbyPVZHNGrV6K5AA95wACFsHTRST84fr3CF
|
||||
```
|
||||
|
||||
After it succeeds, we can check their values
|
||||
|
||||
```bash
|
||||
wallet account get --account-id Public/GQ3C8rbprTtQUCvkuVBRu3v9wvUvjafCMFqoSPvTEVii
|
||||
|
||||
# Output:
|
||||
Definition account owned by token program
|
||||
{"account_type":"Token definition","name":"TOKENB","total_supply":7331}
|
||||
```
|
||||
|
||||
```bash
|
||||
wallet account get --account-id Private/HMRHZdPw4pbyPVZHNGrV6K5AA95wACFsHTRST84fr3CF
|
||||
|
||||
# Output:
|
||||
Holding account owned by token program
|
||||
{"account_type":"Token holding","definition_id":"GQ3C8rbprTtQUCvkuVBRu3v9wvUvjafCMFqoSPvTEVii","balance":7331}
|
||||
```
|
||||
|
||||
Like any other private account owned by us, it cannot be seen by other users.
|
||||
|
||||
#### Custom token transfers
|
||||
|
||||
The Token program has a function to move funds from one token holding account to another one. If executed with an uninitialized account as the recipient, this will be automatically claimed by the token program.
|
||||
|
||||
The transfer function can be executed with the `wallet token send` command.
|
||||
|
||||
Let's create a new public account for the recipient.
|
||||
|
||||
```bash
|
||||
wallet account new public
|
||||
|
||||
# Output:
|
||||
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.
|
||||
|
||||
```bash
|
||||
wallet token send \
|
||||
--from Private/HMRHZdPw4pbyPVZHNGrV6K5AA95wACFsHTRST84fr3CF \
|
||||
--to Public/88f2zeTgiv9LUthQwPJbrmufb9SiDfmpCs47B7vw6Gd6 \
|
||||
--amount 10
|
||||
```
|
||||
|
||||
Let's inspect the public account:
|
||||
|
||||
```bash
|
||||
wallet account get --account-id Public/88f2zeTgiv9LUthQwPJbrmufb9SiDfmpCs47B7vw6Gd6
|
||||
|
||||
# Output:
|
||||
Holding account owned by token program
|
||||
{"account_type":"Token holding","definition_id":"GQ3C8rbprTtQUCvkuVBRu3v9wvUvjafCMFqoSPvTEVii","balance":10}
|
||||
```
|
||||
|
||||
### Chain information
|
||||
|
||||
The wallet provides some commands to query information about the chain. These are under the `wallet chain-info` command.
|
||||
|
||||
```bash
|
||||
Commands:
|
||||
current-block-id Get current block id from sequencer
|
||||
block Get block at id from sequencer
|
||||
transaction Get transaction at hash from sequencer
|
||||
```
|
||||
|
||||
For example, run this to find the current block id.
|
||||
|
||||
```bash
|
||||
wallet chain-info current-block-id
|
||||
|
||||
# Output:
|
||||
Last block id is 65537
|
||||
```
|
||||
|
||||
|
||||
|
||||
@ -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,
|
||||
}
|
||||
@ -36,9 +36,9 @@ path = "../wallet"
|
||||
[dependencies.common]
|
||||
path = "../common"
|
||||
|
||||
[dependencies.key_protocol]
|
||||
path = "../key_protocol"
|
||||
|
||||
[dependencies.nssa]
|
||||
path = "../nssa"
|
||||
features = ["no_docker"]
|
||||
|
||||
[dependencies.key_protocol]
|
||||
path = "../key_protocol"
|
||||
|
||||
@ -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.
@ -56,6 +56,8 @@ fn make_private_account_input_from_str(account_id: &str) -> String {
|
||||
pub async fn pre_test(
|
||||
home_dir: PathBuf,
|
||||
) -> Result<(ServerHandle, JoinHandle<Result<()>>, TempDir)> {
|
||||
wallet::cli::execute_setup("test_pass".to_owned()).await?;
|
||||
|
||||
let home_dir_sequencer = home_dir.join("sequencer");
|
||||
|
||||
let mut sequencer_config =
|
||||
|
||||
@ -8,6 +8,7 @@ use std::{
|
||||
use actix_web::dev::ServerHandle;
|
||||
use anyhow::Result;
|
||||
use common::{PINATA_BASE58, sequencer_client::SequencerClient};
|
||||
use key_protocol::key_management::key_tree::chain_index::ChainIndex;
|
||||
use log::info;
|
||||
use nssa::{AccountId, ProgramDeploymentTransaction, program::Program};
|
||||
use nssa_core::{NullifierPublicKey, encryption::shared_key_derivation::Secp256k1Point};
|
||||
@ -15,15 +16,17 @@ use sequencer_runner::startup_sequencer;
|
||||
use tempfile::TempDir;
|
||||
use tokio::task::JoinHandle;
|
||||
use wallet::{
|
||||
Command, SubcommandReturnValue, WalletCore,
|
||||
WalletCore,
|
||||
cli::{
|
||||
Command, SubcommandReturnValue,
|
||||
account::{AccountSubcommand, NewSubcommand},
|
||||
config::ConfigSubcommand,
|
||||
native_token_transfer_program::AuthTransferSubcommand,
|
||||
pinata_program::PinataProgramAgnosticSubcommand,
|
||||
token_program::TokenProgramAgnosticSubcommand,
|
||||
programs::{
|
||||
native_token_transfer::AuthTransferSubcommand, pinata::PinataProgramAgnosticSubcommand,
|
||||
token::TokenProgramAgnosticSubcommand,
|
||||
},
|
||||
},
|
||||
config::{PersistentAccountData, PersistentStorage},
|
||||
config::PersistentStorage,
|
||||
helperfunctions::{fetch_config, fetch_persistent_storage},
|
||||
};
|
||||
|
||||
@ -56,7 +59,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
|
||||
let seq_client = SequencerClient::new(wallet_config.sequencer_addr.clone()).unwrap();
|
||||
|
||||
wallet::execute_subcommand(command).await.unwrap();
|
||||
wallet::cli::execute_subcommand(command).await.unwrap();
|
||||
|
||||
info!("Waiting for next block creation");
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
@ -83,13 +86,15 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
#[nssa_integration_test]
|
||||
pub async fn test_success_move_to_another_account() {
|
||||
info!("########## test_success_move_to_another_account ##########");
|
||||
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public {}));
|
||||
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public {
|
||||
cci: ChainIndex::root(),
|
||||
}));
|
||||
|
||||
let wallet_config = fetch_config().await.unwrap();
|
||||
|
||||
let seq_client = SequencerClient::new(wallet_config.sequencer_addr.clone()).unwrap();
|
||||
|
||||
wallet::execute_subcommand(command).await.unwrap();
|
||||
wallet::cli::execute_subcommand(command).await.unwrap();
|
||||
|
||||
let PersistentStorage {
|
||||
accounts: persistent_accounts,
|
||||
@ -120,7 +125,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
amount: 100,
|
||||
});
|
||||
|
||||
wallet::execute_subcommand(command).await.unwrap();
|
||||
wallet::cli::execute_subcommand(command).await.unwrap();
|
||||
|
||||
info!("Waiting for next block creation");
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
@ -159,7 +164,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
|
||||
let seq_client = SequencerClient::new(wallet_config.sequencer_addr.clone()).unwrap();
|
||||
|
||||
let failed_send = wallet::execute_subcommand(command).await;
|
||||
let failed_send = wallet::cli::execute_subcommand(command).await;
|
||||
|
||||
assert!(failed_send.is_err());
|
||||
|
||||
@ -200,7 +205,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
|
||||
let seq_client = SequencerClient::new(wallet_config.sequencer_addr.clone()).unwrap();
|
||||
|
||||
wallet::execute_subcommand(command).await.unwrap();
|
||||
wallet::cli::execute_subcommand(command).await.unwrap();
|
||||
|
||||
info!("Waiting for next block creation");
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
@ -231,7 +236,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
amount: 100,
|
||||
});
|
||||
|
||||
wallet::execute_subcommand(command).await.unwrap();
|
||||
wallet::cli::execute_subcommand(command).await.unwrap();
|
||||
|
||||
info!("Waiting for next block creation");
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
@ -284,51 +289,44 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
let wallet_config = fetch_config().await.unwrap();
|
||||
|
||||
// Create new account for the token definition
|
||||
wallet::execute_subcommand(Command::Account(AccountSubcommand::New(
|
||||
NewSubcommand::Public {},
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: definition_account_id,
|
||||
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
|
||||
NewSubcommand::Public {
|
||||
cci: ChainIndex::root(),
|
||||
},
|
||||
)))
|
||||
.await
|
||||
.unwrap();
|
||||
.unwrap()
|
||||
else {
|
||||
panic!("invalid subcommand return value");
|
||||
};
|
||||
// Create new account for the token supply holder
|
||||
wallet::execute_subcommand(Command::Account(AccountSubcommand::New(
|
||||
NewSubcommand::Public {},
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: supply_account_id,
|
||||
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
|
||||
NewSubcommand::Public {
|
||||
cci: ChainIndex::root(),
|
||||
},
|
||||
)))
|
||||
.await
|
||||
.unwrap();
|
||||
.unwrap()
|
||||
else {
|
||||
panic!("invalid subcommand return value");
|
||||
};
|
||||
// Create new account for receiving a token transaction
|
||||
wallet::execute_subcommand(Command::Account(AccountSubcommand::New(
|
||||
NewSubcommand::Public {},
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: recipient_account_id,
|
||||
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
|
||||
NewSubcommand::Public {
|
||||
cci: ChainIndex::root(),
|
||||
},
|
||||
)))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let PersistentStorage {
|
||||
accounts: persistent_accounts,
|
||||
last_synced_block: _,
|
||||
} = fetch_persistent_storage().await.unwrap();
|
||||
|
||||
let mut new_persistent_accounts_account_id = Vec::new();
|
||||
|
||||
for per_acc in persistent_accounts {
|
||||
match per_acc {
|
||||
PersistentAccountData::Public(per_acc) => {
|
||||
if (per_acc.account_id.to_string() != ACC_RECEIVER)
|
||||
&& (per_acc.account_id.to_string() != ACC_SENDER)
|
||||
{
|
||||
new_persistent_accounts_account_id.push(per_acc.account_id);
|
||||
}
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
|
||||
let [
|
||||
definition_account_id,
|
||||
supply_account_id,
|
||||
recipient_account_id,
|
||||
] = new_persistent_accounts_account_id
|
||||
.try_into()
|
||||
.expect("Failed to produce new account, not present in persistent accounts");
|
||||
.unwrap()
|
||||
else {
|
||||
panic!("invalid subcommand return value");
|
||||
};
|
||||
|
||||
// Create new token
|
||||
let subcommand = TokenProgramAgnosticSubcommand::New {
|
||||
@ -339,7 +337,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
name: "A NAME".to_string(),
|
||||
total_supply: 37,
|
||||
};
|
||||
wallet::execute_subcommand(Command::Token(subcommand))
|
||||
wallet::cli::execute_subcommand(Command::Token(subcommand))
|
||||
.await
|
||||
.unwrap();
|
||||
info!("Waiting for next block creation");
|
||||
@ -398,7 +396,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
amount: 7,
|
||||
};
|
||||
|
||||
wallet::execute_subcommand(Command::Token(subcommand))
|
||||
wallet::cli::execute_subcommand(Command::Token(subcommand))
|
||||
.await
|
||||
.unwrap();
|
||||
info!("Waiting for next block creation");
|
||||
@ -453,8 +451,10 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
// Create new account for the token definition (public)
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: definition_account_id,
|
||||
} = wallet::execute_subcommand(Command::Account(AccountSubcommand::New(
|
||||
NewSubcommand::Public {},
|
||||
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
|
||||
NewSubcommand::Public {
|
||||
cci: ChainIndex::root(),
|
||||
},
|
||||
)))
|
||||
.await
|
||||
.unwrap()
|
||||
@ -464,8 +464,10 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
// Create new account for the token supply holder (private)
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: supply_account_id,
|
||||
} = wallet::execute_subcommand(Command::Account(AccountSubcommand::New(
|
||||
NewSubcommand::Private {},
|
||||
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
|
||||
NewSubcommand::Private {
|
||||
cci: ChainIndex::root(),
|
||||
},
|
||||
)))
|
||||
.await
|
||||
.unwrap()
|
||||
@ -475,8 +477,10 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
// Create new account for receiving a token transaction
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: recipient_account_id,
|
||||
} = wallet::execute_subcommand(Command::Account(AccountSubcommand::New(
|
||||
NewSubcommand::Private {},
|
||||
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
|
||||
NewSubcommand::Private {
|
||||
cci: ChainIndex::root(),
|
||||
},
|
||||
)))
|
||||
.await
|
||||
.unwrap()
|
||||
@ -494,7 +498,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
total_supply: 37,
|
||||
};
|
||||
|
||||
wallet::execute_subcommand(Command::Token(subcommand))
|
||||
wallet::cli::execute_subcommand(Command::Token(subcommand))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@ -541,7 +545,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
amount: 7,
|
||||
};
|
||||
|
||||
wallet::execute_subcommand(Command::Token(subcommand))
|
||||
wallet::cli::execute_subcommand(Command::Token(subcommand))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@ -575,7 +579,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
amount: 7,
|
||||
};
|
||||
|
||||
wallet::execute_subcommand(Command::Token(subcommand))
|
||||
wallet::cli::execute_subcommand(Command::Token(subcommand))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@ -608,8 +612,10 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
// Create new account for the token definition (public)
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: definition_account_id,
|
||||
} = wallet::execute_subcommand(Command::Account(AccountSubcommand::New(
|
||||
NewSubcommand::Public {},
|
||||
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
|
||||
NewSubcommand::Public {
|
||||
cci: ChainIndex::root(),
|
||||
},
|
||||
)))
|
||||
.await
|
||||
.unwrap()
|
||||
@ -619,8 +625,10 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
// Create new account for the token supply holder (private)
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: supply_account_id,
|
||||
} = wallet::execute_subcommand(Command::Account(AccountSubcommand::New(
|
||||
NewSubcommand::Private {},
|
||||
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
|
||||
NewSubcommand::Private {
|
||||
cci: ChainIndex::root(),
|
||||
},
|
||||
)))
|
||||
.await
|
||||
.unwrap()
|
||||
@ -630,8 +638,10 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
// Create new account for receiving a token transaction
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: recipient_account_id,
|
||||
} = wallet::execute_subcommand(Command::Account(AccountSubcommand::New(
|
||||
NewSubcommand::Private {},
|
||||
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
|
||||
NewSubcommand::Private {
|
||||
cci: ChainIndex::root(),
|
||||
},
|
||||
)))
|
||||
.await
|
||||
.unwrap()
|
||||
@ -649,7 +659,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
total_supply: 37,
|
||||
};
|
||||
|
||||
wallet::execute_subcommand(Command::Token(subcommand))
|
||||
wallet::cli::execute_subcommand(Command::Token(subcommand))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@ -703,7 +713,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
};
|
||||
|
||||
let SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash: _ } =
|
||||
wallet::execute_subcommand(Command::Token(subcommand))
|
||||
wallet::cli::execute_subcommand(Command::Token(subcommand))
|
||||
.await
|
||||
.unwrap()
|
||||
else {
|
||||
@ -715,7 +725,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
|
||||
let command = Command::Account(AccountSubcommand::SyncPrivate {});
|
||||
|
||||
wallet::execute_subcommand(command).await.unwrap();
|
||||
wallet::cli::execute_subcommand(command).await.unwrap();
|
||||
|
||||
let wallet_config = fetch_config().await.unwrap();
|
||||
let wallet_storage = WalletCore::start_from_config_update_chain(wallet_config)
|
||||
@ -744,8 +754,10 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
// Create new account for the token definition (public)
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: definition_account_id,
|
||||
} = wallet::execute_subcommand(Command::Account(AccountSubcommand::New(
|
||||
NewSubcommand::Public {},
|
||||
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
|
||||
NewSubcommand::Public {
|
||||
cci: ChainIndex::root(),
|
||||
},
|
||||
)))
|
||||
.await
|
||||
.unwrap()
|
||||
@ -755,8 +767,10 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
// Create new account for the token supply holder (public)
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: supply_account_id,
|
||||
} = wallet::execute_subcommand(Command::Account(AccountSubcommand::New(
|
||||
NewSubcommand::Public {},
|
||||
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
|
||||
NewSubcommand::Public {
|
||||
cci: ChainIndex::root(),
|
||||
},
|
||||
)))
|
||||
.await
|
||||
.unwrap()
|
||||
@ -766,8 +780,10 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
// Create new account for receiving a token transaction
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: recipient_account_id,
|
||||
} = wallet::execute_subcommand(Command::Account(AccountSubcommand::New(
|
||||
NewSubcommand::Private {},
|
||||
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
|
||||
NewSubcommand::Private {
|
||||
cci: ChainIndex::root(),
|
||||
},
|
||||
)))
|
||||
.await
|
||||
.unwrap()
|
||||
@ -785,7 +801,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
total_supply: 37,
|
||||
};
|
||||
|
||||
wallet::execute_subcommand(Command::Token(subcommand))
|
||||
wallet::cli::execute_subcommand(Command::Token(subcommand))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@ -822,7 +838,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
amount: 7,
|
||||
};
|
||||
|
||||
wallet::execute_subcommand(Command::Token(subcommand))
|
||||
wallet::cli::execute_subcommand(Command::Token(subcommand))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@ -851,7 +867,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
amount: 7,
|
||||
};
|
||||
|
||||
wallet::execute_subcommand(Command::Token(subcommand))
|
||||
wallet::cli::execute_subcommand(Command::Token(subcommand))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@ -880,8 +896,10 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
// Create new account for the token definition (public)
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: definition_account_id,
|
||||
} = wallet::execute_subcommand(Command::Account(AccountSubcommand::New(
|
||||
NewSubcommand::Public {},
|
||||
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
|
||||
NewSubcommand::Public {
|
||||
cci: ChainIndex::root(),
|
||||
},
|
||||
)))
|
||||
.await
|
||||
.unwrap()
|
||||
@ -891,8 +909,10 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
// Create new account for the token supply holder (private)
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: supply_account_id,
|
||||
} = wallet::execute_subcommand(Command::Account(AccountSubcommand::New(
|
||||
NewSubcommand::Private {},
|
||||
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
|
||||
NewSubcommand::Private {
|
||||
cci: ChainIndex::root(),
|
||||
},
|
||||
)))
|
||||
.await
|
||||
.unwrap()
|
||||
@ -902,8 +922,10 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
// Create new account for receiving a token transaction
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: recipient_account_id,
|
||||
} = wallet::execute_subcommand(Command::Account(AccountSubcommand::New(
|
||||
NewSubcommand::Public {},
|
||||
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
|
||||
NewSubcommand::Public {
|
||||
cci: ChainIndex::root(),
|
||||
},
|
||||
)))
|
||||
.await
|
||||
.unwrap()
|
||||
@ -921,7 +943,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
total_supply: 37,
|
||||
};
|
||||
|
||||
wallet::execute_subcommand(Command::Token(subcommand))
|
||||
wallet::cli::execute_subcommand(Command::Token(subcommand))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@ -968,7 +990,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
amount: 7,
|
||||
};
|
||||
|
||||
wallet::execute_subcommand(Command::Token(subcommand))
|
||||
wallet::cli::execute_subcommand(Command::Token(subcommand))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@ -997,7 +1019,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
amount: 7,
|
||||
};
|
||||
|
||||
wallet::execute_subcommand(Command::Token(subcommand))
|
||||
wallet::cli::execute_subcommand(Command::Token(subcommand))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@ -1029,7 +1051,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
amount: 100,
|
||||
});
|
||||
|
||||
wallet::execute_subcommand(command).await.unwrap();
|
||||
wallet::cli::execute_subcommand(command).await.unwrap();
|
||||
|
||||
info!("Waiting for next block creation");
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
@ -1068,7 +1090,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
});
|
||||
|
||||
let SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash } =
|
||||
wallet::execute_subcommand(command).await.unwrap()
|
||||
wallet::cli::execute_subcommand(command).await.unwrap()
|
||||
else {
|
||||
panic!("invalid subcommand return value");
|
||||
};
|
||||
@ -1104,9 +1126,11 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
);
|
||||
let from: AccountId = ACC_SENDER_PRIVATE.parse().unwrap();
|
||||
|
||||
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private {}));
|
||||
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private {
|
||||
cci: ChainIndex::root(),
|
||||
}));
|
||||
|
||||
let sub_ret = wallet::execute_subcommand(command).await.unwrap();
|
||||
let sub_ret = wallet::cli::execute_subcommand(command).await.unwrap();
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: to_account_id,
|
||||
} = sub_ret
|
||||
@ -1123,8 +1147,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
let (to_keys, _) = wallet_storage
|
||||
.storage
|
||||
.user_data
|
||||
.user_private_accounts
|
||||
.get(&to_account_id)
|
||||
.get_private_account(&to_account_id)
|
||||
.cloned()
|
||||
.unwrap();
|
||||
|
||||
@ -1136,7 +1159,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
amount: 100,
|
||||
});
|
||||
|
||||
let sub_ret = wallet::execute_subcommand(command).await.unwrap();
|
||||
let sub_ret = wallet::cli::execute_subcommand(command).await.unwrap();
|
||||
let SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash } = sub_ret else {
|
||||
panic!("FAILED TO SEND TX");
|
||||
};
|
||||
@ -1144,7 +1167,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
let tx = fetch_privacy_preserving_tx(&seq_client, tx_hash.clone()).await;
|
||||
|
||||
let command = Command::Account(AccountSubcommand::SyncPrivate {});
|
||||
wallet::execute_subcommand(command).await.unwrap();
|
||||
wallet::cli::execute_subcommand(command).await.unwrap();
|
||||
let wallet_storage = WalletCore::start_from_config_update_chain(wallet_config)
|
||||
.await
|
||||
.unwrap();
|
||||
@ -1171,13 +1194,13 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
// info!(
|
||||
// "########## test_success_private_transfer_to_another_owned_account_cont_run_path
|
||||
// ##########" );
|
||||
// let continious_run_handle = tokio::spawn(wallet::execute_continious_run());
|
||||
// let continious_run_handle = tokio::spawn(wallet::cli::execute_continious_run());
|
||||
|
||||
// let from: AccountId = ACC_SENDER_PRIVATE.parse().unwrap();
|
||||
|
||||
// let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private {}));
|
||||
|
||||
// let sub_ret = wallet::execute_subcommand(command).await.unwrap();
|
||||
// let sub_ret = wallet::cli::execute_subcommand(command).await.unwrap();
|
||||
// let SubcommandReturnValue::RegisterAccount {
|
||||
// account_id: to_account_id,
|
||||
// } = sub_ret
|
||||
@ -1207,13 +1230,12 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
// amount: 100,
|
||||
// });
|
||||
|
||||
// let sub_ret = wallet::execute_subcommand(command).await.unwrap();
|
||||
// let sub_ret = wallet::cli::execute_subcommand(command).await.unwrap();
|
||||
// let SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash } = sub_ret else {
|
||||
// panic!("FAILED TO SEND TX");
|
||||
// };
|
||||
|
||||
// let tx = fetch_privacy_preserving_tx(&seq_client, tx_hash.clone()).await;
|
||||
|
||||
// println!("Waiting for next blocks to check if continoius run fetch account");
|
||||
// tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
// tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
@ -1259,7 +1281,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
let from_acc = wallet_storage.get_account_private(&from).unwrap();
|
||||
assert_eq!(from_acc.balance, 10000);
|
||||
|
||||
wallet::execute_subcommand(command).await.unwrap();
|
||||
wallet::cli::execute_subcommand(command).await.unwrap();
|
||||
|
||||
info!("Waiting for next block creation");
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
@ -1302,7 +1324,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
let wallet_config = fetch_config().await.unwrap();
|
||||
let seq_client = SequencerClient::new(wallet_config.sequencer_addr.clone()).unwrap();
|
||||
|
||||
wallet::execute_subcommand(command).await.unwrap();
|
||||
wallet::cli::execute_subcommand(command).await.unwrap();
|
||||
|
||||
info!("Waiting for next block creation");
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
@ -1348,7 +1370,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
let seq_client = SequencerClient::new(wallet_config.sequencer_addr.clone()).unwrap();
|
||||
|
||||
let SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash } =
|
||||
wallet::execute_subcommand(command).await.unwrap()
|
||||
wallet::cli::execute_subcommand(command).await.unwrap()
|
||||
else {
|
||||
panic!("invalid subcommand return value");
|
||||
};
|
||||
@ -1376,11 +1398,10 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
pub async fn test_pinata() {
|
||||
info!("########## test_pinata ##########");
|
||||
let pinata_account_id = PINATA_BASE58;
|
||||
|
||||
let pinata_prize = 150;
|
||||
let solution = 989106;
|
||||
let command = Command::Pinata(PinataProgramAgnosticSubcommand::Claim {
|
||||
to_account_id: make_public_account_input_from_str(ACC_SENDER),
|
||||
solution,
|
||||
to: make_public_account_input_from_str(ACC_SENDER),
|
||||
});
|
||||
|
||||
let wallet_config = fetch_config().await.unwrap();
|
||||
@ -1393,7 +1414,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
.unwrap()
|
||||
.balance;
|
||||
|
||||
wallet::execute_subcommand(command).await.unwrap();
|
||||
wallet::cli::execute_subcommand(command).await.unwrap();
|
||||
|
||||
info!("Waiting for next block creation");
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
@ -1468,9 +1489,11 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
#[nssa_integration_test]
|
||||
pub async fn test_authenticated_transfer_initialize_function() {
|
||||
info!("########## test initialize account for authenticated transfer ##########");
|
||||
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public {}));
|
||||
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public {
|
||||
cci: ChainIndex::root(),
|
||||
}));
|
||||
let SubcommandReturnValue::RegisterAccount { account_id } =
|
||||
wallet::execute_subcommand(command).await.unwrap()
|
||||
wallet::cli::execute_subcommand(command).await.unwrap()
|
||||
else {
|
||||
panic!("Error creating account");
|
||||
};
|
||||
@ -1478,7 +1501,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
let command = Command::AuthTransfer(AuthTransferSubcommand::Init {
|
||||
account_id: make_public_account_input_from_str(&account_id.to_string()),
|
||||
});
|
||||
wallet::execute_subcommand(command).await.unwrap();
|
||||
wallet::cli::execute_subcommand(command).await.unwrap();
|
||||
|
||||
info!("Checking correct execution");
|
||||
let wallet_config = fetch_config().await.unwrap();
|
||||
@ -1506,11 +1529,9 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
info!("########## test_pinata_private_receiver ##########");
|
||||
let pinata_account_id = PINATA_BASE58;
|
||||
let pinata_prize = 150;
|
||||
let solution = 989106;
|
||||
|
||||
let command = Command::Pinata(PinataProgramAgnosticSubcommand::Claim {
|
||||
to_account_id: make_private_account_input_from_str(ACC_SENDER_PRIVATE),
|
||||
solution,
|
||||
to: make_private_account_input_from_str(ACC_SENDER_PRIVATE),
|
||||
});
|
||||
|
||||
let wallet_config = fetch_config().await.unwrap();
|
||||
@ -1524,7 +1545,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
.balance;
|
||||
|
||||
let SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash: _ } =
|
||||
wallet::execute_subcommand(command).await.unwrap()
|
||||
wallet::cli::execute_subcommand(command).await.unwrap()
|
||||
else {
|
||||
panic!("invalid subcommand return value");
|
||||
};
|
||||
@ -1540,7 +1561,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
.balance;
|
||||
|
||||
let command = Command::Account(AccountSubcommand::SyncPrivate {});
|
||||
wallet::execute_subcommand(command).await.unwrap();
|
||||
wallet::cli::execute_subcommand(command).await.unwrap();
|
||||
|
||||
let wallet_config = fetch_config().await.unwrap();
|
||||
let seq_client = SequencerClient::new(wallet_config.sequencer_addr.clone()).unwrap();
|
||||
@ -1560,16 +1581,17 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
|
||||
#[nssa_integration_test]
|
||||
pub async fn test_pinata_private_receiver_new_account() {
|
||||
info!("########## test_pinata_private_receiver ##########");
|
||||
info!("########## test_pinata_private_receiver_new_account ##########");
|
||||
let pinata_account_id = PINATA_BASE58;
|
||||
let pinata_prize = 150;
|
||||
let solution = 989106;
|
||||
|
||||
// Create new account for the token supply holder (private)
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: winner_account_id,
|
||||
} = wallet::execute_subcommand(Command::Account(AccountSubcommand::New(
|
||||
NewSubcommand::Private {},
|
||||
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
|
||||
NewSubcommand::Private {
|
||||
cci: ChainIndex::root(),
|
||||
},
|
||||
)))
|
||||
.await
|
||||
.unwrap()
|
||||
@ -1578,8 +1600,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
};
|
||||
|
||||
let command = Command::Pinata(PinataProgramAgnosticSubcommand::Claim {
|
||||
to_account_id: make_private_account_input_from_str(&winner_account_id.to_string()),
|
||||
solution,
|
||||
to: make_private_account_input_from_str(&winner_account_id.to_string()),
|
||||
});
|
||||
|
||||
let wallet_config = fetch_config().await.unwrap();
|
||||
@ -1592,7 +1613,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
|
||||
.unwrap()
|
||||
.balance;
|
||||
|
||||
wallet::execute_subcommand(command).await.unwrap();
|
||||
wallet::cli::execute_subcommand(command).await.unwrap();
|
||||
|
||||
info!("Waiting for next block creation");
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
@ -1625,25 +1646,25 @@ 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::execute_subcommand(command).await.unwrap();
|
||||
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::execute_subcommand(command).await.unwrap();
|
||||
wallet::cli::execute_subcommand(command).await.unwrap();
|
||||
|
||||
info!("Success!");
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ hex = "0.4.3"
|
||||
aes-gcm.workspace = true
|
||||
bip39.workspace = true
|
||||
hmac-sha512.workspace = true
|
||||
thiserror.workspace = true
|
||||
nssa-core = { path = "../nssa/core", features = ["host"] }
|
||||
|
||||
[dependencies.common]
|
||||
|
||||
148
key_protocol/src/key_management/key_tree/chain_index.rs
Normal file
148
key_protocol/src/key_management/key_tree/chain_index.rs
Normal file
@ -0,0 +1,148 @@
|
||||
use std::{fmt::Display, str::FromStr};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Serialize, Deserialize)]
|
||||
pub struct ChainIndex(Vec<u32>);
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ChainIndexError {
|
||||
#[error("No root found")]
|
||||
NoRootFound,
|
||||
#[error("Failed to parse segment into a number")]
|
||||
ParseIntError(#[from] std::num::ParseIntError),
|
||||
}
|
||||
|
||||
impl FromStr for ChainIndex {
|
||||
type Err = ChainIndexError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if !s.starts_with('/') {
|
||||
return Err(ChainIndexError::NoRootFound);
|
||||
}
|
||||
|
||||
if s == "/" {
|
||||
return Ok(ChainIndex(vec![]));
|
||||
}
|
||||
|
||||
let uprooted_substring = s.strip_prefix("/").unwrap();
|
||||
|
||||
let splitted_chain: Vec<&str> = uprooted_substring.split("/").collect();
|
||||
let mut res = vec![];
|
||||
|
||||
for split_ch in splitted_chain {
|
||||
let cci = split_ch.parse()?;
|
||||
res.push(cci);
|
||||
}
|
||||
|
||||
Ok(Self(res))
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ChainIndex {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "/")?;
|
||||
for cci in &self.0[..(self.0.len().saturating_sub(1))] {
|
||||
write!(f, "{cci}/")?;
|
||||
}
|
||||
if let Some(last) = self.0.last() {
|
||||
write!(f, "{}", last)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ChainIndex {
|
||||
fn default() -> Self {
|
||||
ChainIndex::from_str("/").expect("Root parsing failure")
|
||||
}
|
||||
}
|
||||
|
||||
impl ChainIndex {
|
||||
pub fn root() -> Self {
|
||||
ChainIndex::default()
|
||||
}
|
||||
|
||||
pub fn chain(&self) -> &[u32] {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn next_in_line(&self) -> ChainIndex {
|
||||
let mut chain = self.0.clone();
|
||||
// ToDo: Add overflow check
|
||||
if let Some(last_p) = chain.last_mut() {
|
||||
*last_p += 1
|
||||
}
|
||||
|
||||
ChainIndex(chain)
|
||||
}
|
||||
|
||||
pub fn nth_child(&self, child_id: u32) -> ChainIndex {
|
||||
let mut chain = self.0.clone();
|
||||
chain.push(child_id);
|
||||
|
||||
ChainIndex(chain)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_chain_id_root_correct() {
|
||||
let chain_id = ChainIndex::root();
|
||||
let chain_id_2 = ChainIndex::from_str("/").unwrap();
|
||||
|
||||
assert_eq!(chain_id, chain_id_2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chain_id_deser_correct() {
|
||||
let chain_id = ChainIndex::from_str("/257").unwrap();
|
||||
|
||||
assert_eq!(chain_id.chain(), &[257]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chain_id_deser_failure_no_root() {
|
||||
let chain_index_error = ChainIndex::from_str("257").err().unwrap();
|
||||
|
||||
assert!(matches!(chain_index_error, ChainIndexError::NoRootFound));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chain_id_deser_failure_int_parsing_failure() {
|
||||
let chain_index_error = ChainIndex::from_str("/hello").err().unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
chain_index_error,
|
||||
ChainIndexError::ParseIntError(_)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chain_id_next_in_line_correct() {
|
||||
let chain_id = ChainIndex::from_str("/257").unwrap();
|
||||
let next_in_line = chain_id.next_in_line();
|
||||
|
||||
assert_eq!(next_in_line, ChainIndex::from_str("/258").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chain_id_child_correct() {
|
||||
let chain_id = ChainIndex::from_str("/257").unwrap();
|
||||
let child = chain_id.nth_child(3);
|
||||
|
||||
assert_eq!(child, ChainIndex::from_str("/257/3").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_correct_display() {
|
||||
let chainid = ChainIndex(vec![5, 7, 8]);
|
||||
|
||||
let string_index = format!("{chainid}");
|
||||
|
||||
assert_eq!(string_index, "/5/7/8".to_string());
|
||||
}
|
||||
}
|
||||
263
key_protocol/src/key_management/key_tree/keys_private.rs
Normal file
263
key_protocol/src/key_management/key_tree/keys_private.rs
Normal file
@ -0,0 +1,263 @@
|
||||
use k256::{Scalar, elliptic_curve::PrimeField};
|
||||
use nssa_core::encryption::IncomingViewingPublicKey;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::key_management::{
|
||||
KeyChain,
|
||||
key_tree::traits::KeyNode,
|
||||
secret_holders::{PrivateKeyHolder, SecretSpendingKey},
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct ChildKeysPrivate {
|
||||
pub value: (KeyChain, nssa::Account),
|
||||
pub ccc: [u8; 32],
|
||||
/// Can be [`None`] if root
|
||||
pub cci: Option<u32>,
|
||||
}
|
||||
|
||||
impl KeyNode for ChildKeysPrivate {
|
||||
fn root(seed: [u8; 64]) -> Self {
|
||||
let hash_value = hmac_sha512::HMAC::mac(seed, "NSSA_master_priv");
|
||||
|
||||
let ssk = SecretSpendingKey(
|
||||
*hash_value
|
||||
.first_chunk::<32>()
|
||||
.expect("hash_value is 64 bytes, must be safe to get first 32"),
|
||||
);
|
||||
let ccc = *hash_value
|
||||
.last_chunk::<32>()
|
||||
.expect("hash_value is 64 bytes, must be safe to get last 32");
|
||||
|
||||
let nsk = ssk.generate_nullifier_secret_key();
|
||||
let isk = ssk.generate_incoming_viewing_secret_key();
|
||||
let ovk = ssk.generate_outgoing_viewing_secret_key();
|
||||
|
||||
let npk = (&nsk).into();
|
||||
let ipk = IncomingViewingPublicKey::from_scalar(isk);
|
||||
|
||||
Self {
|
||||
value: (
|
||||
KeyChain {
|
||||
secret_spending_key: ssk,
|
||||
nullifer_public_key: npk,
|
||||
incoming_viewing_public_key: ipk,
|
||||
private_key_holder: PrivateKeyHolder {
|
||||
nullifier_secret_key: nsk,
|
||||
incoming_viewing_secret_key: isk,
|
||||
outgoing_viewing_secret_key: ovk,
|
||||
},
|
||||
},
|
||||
nssa::Account::default(),
|
||||
),
|
||||
ccc,
|
||||
cci: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn nth_child(&self, cci: u32) -> Self {
|
||||
let parent_pt = Scalar::from_repr(
|
||||
self.value
|
||||
.0
|
||||
.private_key_holder
|
||||
.outgoing_viewing_secret_key
|
||||
.into(),
|
||||
)
|
||||
.expect("Key generated as scalar, must be valid representation")
|
||||
+ Scalar::from_repr(self.value.0.private_key_holder.nullifier_secret_key.into())
|
||||
.expect("Key generated as scalar, must be valid representation")
|
||||
* Scalar::from_repr(
|
||||
self.value
|
||||
.0
|
||||
.private_key_holder
|
||||
.incoming_viewing_secret_key
|
||||
.into(),
|
||||
)
|
||||
.expect("Key generated as scalar, must be valid representation");
|
||||
let mut input = vec![];
|
||||
|
||||
input.extend_from_slice(b"NSSA_seed_priv");
|
||||
input.extend_from_slice(&parent_pt.to_bytes());
|
||||
input.extend_from_slice(&cci.to_le_bytes());
|
||||
|
||||
let hash_value = hmac_sha512::HMAC::mac(input, self.ccc);
|
||||
|
||||
let ssk = SecretSpendingKey(
|
||||
*hash_value
|
||||
.first_chunk::<32>()
|
||||
.expect("hash_value is 64 bytes, must be safe to get first 32"),
|
||||
);
|
||||
let ccc = *hash_value
|
||||
.last_chunk::<32>()
|
||||
.expect("hash_value is 64 bytes, must be safe to get last 32");
|
||||
|
||||
let nsk = ssk.generate_nullifier_secret_key();
|
||||
let isk = ssk.generate_incoming_viewing_secret_key();
|
||||
let ovk = ssk.generate_outgoing_viewing_secret_key();
|
||||
|
||||
let npk = (&nsk).into();
|
||||
let ipk = IncomingViewingPublicKey::from_scalar(isk);
|
||||
|
||||
Self {
|
||||
value: (
|
||||
KeyChain {
|
||||
secret_spending_key: ssk,
|
||||
nullifer_public_key: npk,
|
||||
incoming_viewing_public_key: ipk,
|
||||
private_key_holder: PrivateKeyHolder {
|
||||
nullifier_secret_key: nsk,
|
||||
incoming_viewing_secret_key: isk,
|
||||
outgoing_viewing_secret_key: ovk,
|
||||
},
|
||||
},
|
||||
nssa::Account::default(),
|
||||
),
|
||||
ccc,
|
||||
cci: Some(cci),
|
||||
}
|
||||
}
|
||||
|
||||
fn chain_code(&self) -> &[u8; 32] {
|
||||
&self.ccc
|
||||
}
|
||||
|
||||
fn child_index(&self) -> Option<u32> {
|
||||
self.cci
|
||||
}
|
||||
|
||||
fn account_id(&self) -> nssa::AccountId {
|
||||
nssa::AccountId::from(&self.value.0.nullifer_public_key)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ChildKeysPrivate> for &'a (KeyChain, nssa::Account) {
|
||||
fn from(value: &'a ChildKeysPrivate) -> Self {
|
||||
&value.value
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a mut ChildKeysPrivate> for &'a mut (KeyChain, nssa::Account) {
|
||||
fn from(value: &'a mut ChildKeysPrivate) -> Self {
|
||||
&mut value.value
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_keys_deterministic_generation() {
|
||||
let root_keys = ChildKeysPrivate::root([42; 64]);
|
||||
let child_keys = root_keys.nth_child(5);
|
||||
|
||||
assert_eq!(root_keys.cci, None);
|
||||
assert_eq!(child_keys.cci, Some(5));
|
||||
|
||||
assert_eq!(
|
||||
root_keys.value.0.secret_spending_key.0,
|
||||
[
|
||||
249, 83, 253, 32, 174, 204, 185, 44, 253, 167, 61, 92, 128, 5, 152, 4, 220, 21, 88,
|
||||
84, 167, 180, 154, 249, 44, 77, 33, 136, 59, 131, 203, 152
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
child_keys.value.0.secret_spending_key.0,
|
||||
[
|
||||
16, 242, 229, 242, 252, 158, 153, 210, 234, 120, 70, 85, 83, 196, 5, 53, 28, 26,
|
||||
187, 230, 22, 193, 146, 232, 237, 3, 166, 184, 122, 1, 233, 93
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
root_keys.value.0.private_key_holder.nullifier_secret_key,
|
||||
[
|
||||
38, 195, 52, 182, 16, 66, 167, 156, 9, 14, 65, 100, 17, 93, 166, 71, 27, 148, 93,
|
||||
85, 116, 109, 130, 8, 195, 222, 159, 214, 141, 41, 124, 57
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
child_keys.value.0.private_key_holder.nullifier_secret_key,
|
||||
[
|
||||
215, 46, 2, 151, 174, 60, 86, 154, 5, 3, 175, 245, 12, 176, 220, 58, 250, 118, 236,
|
||||
49, 254, 221, 229, 58, 40, 1, 170, 145, 175, 108, 23, 170
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
root_keys
|
||||
.value
|
||||
.0
|
||||
.private_key_holder
|
||||
.incoming_viewing_secret_key,
|
||||
[
|
||||
153, 161, 15, 34, 96, 184, 165, 165, 27, 244, 155, 40, 70, 5, 241, 133, 78, 40, 61,
|
||||
118, 48, 148, 226, 5, 97, 18, 201, 128, 82, 248, 163, 72
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
child_keys
|
||||
.value
|
||||
.0
|
||||
.private_key_holder
|
||||
.incoming_viewing_secret_key,
|
||||
[
|
||||
192, 155, 55, 43, 164, 115, 71, 145, 227, 225, 21, 57, 55, 12, 226, 44, 10, 103,
|
||||
39, 73, 230, 173, 60, 69, 69, 122, 110, 241, 164, 3, 192, 57
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
root_keys
|
||||
.value
|
||||
.0
|
||||
.private_key_holder
|
||||
.outgoing_viewing_secret_key,
|
||||
[
|
||||
205, 87, 71, 129, 90, 242, 217, 200, 140, 252, 124, 46, 207, 7, 33, 156, 83, 166,
|
||||
150, 81, 98, 131, 182, 156, 110, 92, 78, 140, 125, 218, 152, 154
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
child_keys
|
||||
.value
|
||||
.0
|
||||
.private_key_holder
|
||||
.outgoing_viewing_secret_key,
|
||||
[
|
||||
131, 202, 219, 172, 219, 29, 48, 120, 226, 209, 209, 10, 216, 173, 48, 167, 233,
|
||||
17, 35, 155, 30, 217, 176, 120, 72, 146, 250, 226, 165, 178, 255, 90
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
root_keys.value.0.nullifer_public_key.0,
|
||||
[
|
||||
65, 176, 149, 243, 192, 45, 216, 177, 169, 56, 229, 7, 28, 66, 204, 87, 109, 83,
|
||||
152, 64, 14, 188, 179, 210, 147, 60, 22, 251, 203, 70, 89, 215
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
child_keys.value.0.nullifer_public_key.0,
|
||||
[
|
||||
69, 104, 130, 115, 48, 134, 19, 188, 67, 148, 163, 54, 155, 237, 57, 27, 136, 228,
|
||||
111, 233, 205, 158, 149, 31, 84, 11, 241, 176, 243, 12, 138, 249
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
root_keys.value.0.incoming_viewing_public_key.0,
|
||||
&[
|
||||
3, 174, 56, 136, 244, 179, 18, 122, 38, 220, 36, 50, 200, 41, 104, 167, 70, 18, 60,
|
||||
202, 93, 193, 29, 16, 125, 252, 96, 51, 199, 152, 47, 233, 178
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
child_keys.value.0.incoming_viewing_public_key.0,
|
||||
&[
|
||||
3, 18, 202, 246, 79, 141, 169, 51, 55, 202, 120, 169, 244, 201, 156, 162, 216, 115,
|
||||
126, 53, 46, 94, 235, 125, 114, 178, 215, 81, 171, 93, 93, 88, 117
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
132
key_protocol/src/key_management/key_tree/keys_public.rs
Normal file
132
key_protocol/src/key_management/key_tree/keys_public.rs
Normal file
@ -0,0 +1,132 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::key_management::key_tree::traits::KeyNode;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct ChildKeysPublic {
|
||||
pub csk: nssa::PrivateKey,
|
||||
pub cpk: nssa::PublicKey,
|
||||
pub ccc: [u8; 32],
|
||||
/// Can be [`None`] if root
|
||||
pub cci: Option<u32>,
|
||||
}
|
||||
|
||||
impl KeyNode for ChildKeysPublic {
|
||||
fn root(seed: [u8; 64]) -> Self {
|
||||
let hash_value = hmac_sha512::HMAC::mac(seed, "NSSA_master_pub");
|
||||
|
||||
let csk = nssa::PrivateKey::try_new(*hash_value.first_chunk::<32>().unwrap()).unwrap();
|
||||
let ccc = *hash_value.last_chunk::<32>().unwrap();
|
||||
let cpk = nssa::PublicKey::new_from_private_key(&csk);
|
||||
|
||||
Self {
|
||||
csk,
|
||||
cpk,
|
||||
ccc,
|
||||
cci: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn nth_child(&self, cci: u32) -> Self {
|
||||
let mut hash_input = vec![];
|
||||
hash_input.extend_from_slice(self.csk.value());
|
||||
hash_input.extend_from_slice(&cci.to_le_bytes());
|
||||
|
||||
let hash_value = hmac_sha512::HMAC::mac(&hash_input, self.ccc);
|
||||
|
||||
let csk = nssa::PrivateKey::try_new(
|
||||
*hash_value
|
||||
.first_chunk::<32>()
|
||||
.expect("hash_value is 64 bytes, must be safe to get first 32"),
|
||||
)
|
||||
.unwrap();
|
||||
let ccc = *hash_value
|
||||
.last_chunk::<32>()
|
||||
.expect("hash_value is 64 bytes, must be safe to get last 32");
|
||||
let cpk = nssa::PublicKey::new_from_private_key(&csk);
|
||||
|
||||
Self {
|
||||
csk,
|
||||
cpk,
|
||||
ccc,
|
||||
cci: Some(cci),
|
||||
}
|
||||
}
|
||||
|
||||
fn chain_code(&self) -> &[u8; 32] {
|
||||
&self.ccc
|
||||
}
|
||||
|
||||
fn child_index(&self) -> Option<u32> {
|
||||
self.cci
|
||||
}
|
||||
|
||||
fn account_id(&self) -> nssa::AccountId {
|
||||
nssa::AccountId::from(&self.cpk)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ChildKeysPublic> for &'a nssa::PrivateKey {
|
||||
fn from(value: &'a ChildKeysPublic) -> Self {
|
||||
&value.csk
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_keys_deterministic_generation() {
|
||||
let root_keys = ChildKeysPublic::root([42; 64]);
|
||||
let child_keys = root_keys.nth_child(5);
|
||||
|
||||
assert_eq!(root_keys.cci, None);
|
||||
assert_eq!(child_keys.cci, Some(5));
|
||||
|
||||
assert_eq!(
|
||||
root_keys.ccc,
|
||||
[
|
||||
61, 30, 91, 26, 133, 91, 236, 192, 231, 53, 186, 139, 11, 221, 202, 11, 178, 215,
|
||||
254, 103, 191, 60, 117, 112, 1, 226, 31, 156, 83, 104, 150, 224
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
child_keys.ccc,
|
||||
[
|
||||
67, 26, 102, 68, 189, 155, 102, 80, 199, 188, 112, 142, 207, 157, 36, 210, 48, 224,
|
||||
35, 6, 112, 180, 11, 190, 135, 218, 9, 14, 84, 231, 58, 98
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
root_keys.csk.value(),
|
||||
&[
|
||||
241, 82, 246, 237, 62, 130, 116, 47, 189, 112, 99, 67, 178, 40, 115, 245, 141, 193,
|
||||
77, 164, 243, 76, 222, 64, 50, 146, 23, 145, 91, 164, 92, 116
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
child_keys.csk.value(),
|
||||
&[
|
||||
11, 151, 27, 212, 167, 26, 77, 234, 103, 145, 53, 191, 184, 25, 240, 191, 156, 25,
|
||||
60, 144, 65, 22, 193, 163, 246, 227, 212, 81, 49, 170, 33, 158
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
root_keys.cpk.value(),
|
||||
&[
|
||||
220, 170, 95, 177, 121, 37, 86, 166, 56, 238, 232, 72, 21, 106, 107, 217, 158, 74,
|
||||
133, 91, 143, 244, 155, 15, 2, 230, 223, 169, 13, 20, 163, 138
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
child_keys.cpk.value(),
|
||||
&[
|
||||
152, 249, 236, 111, 132, 96, 184, 122, 21, 179, 240, 15, 234, 155, 164, 144, 108,
|
||||
110, 120, 74, 176, 147, 196, 168, 243, 186, 203, 79, 97, 17, 194, 52
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
324
key_protocol/src/key_management/key_tree/mod.rs
Normal file
324
key_protocol/src/key_management/key_tree/mod.rs
Normal file
@ -0,0 +1,324 @@
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::key_management::{
|
||||
key_tree::{
|
||||
chain_index::ChainIndex, keys_private::ChildKeysPrivate, keys_public::ChildKeysPublic,
|
||||
traits::KeyNode,
|
||||
},
|
||||
secret_holders::SeedHolder,
|
||||
};
|
||||
|
||||
pub mod chain_index;
|
||||
pub mod keys_private;
|
||||
pub mod keys_public;
|
||||
pub mod traits;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct KeyTree<N: KeyNode> {
|
||||
pub key_map: BTreeMap<ChainIndex, N>,
|
||||
pub account_id_map: HashMap<nssa::AccountId, ChainIndex>,
|
||||
}
|
||||
|
||||
pub type KeyTreePublic = KeyTree<ChildKeysPublic>;
|
||||
pub type KeyTreePrivate = KeyTree<ChildKeysPrivate>;
|
||||
|
||||
impl<N: KeyNode> KeyTree<N> {
|
||||
pub fn new(seed: &SeedHolder) -> Self {
|
||||
let seed_fit: [u8; 64] = seed
|
||||
.seed
|
||||
.clone()
|
||||
.try_into()
|
||||
.expect("SeedHolder seed is 64 bytes long");
|
||||
|
||||
let root_keys = N::root(seed_fit);
|
||||
let account_id = root_keys.account_id();
|
||||
|
||||
let key_map = BTreeMap::from_iter([(ChainIndex::root(), root_keys)]);
|
||||
let account_id_map = HashMap::from_iter([(account_id, ChainIndex::root())]);
|
||||
|
||||
Self {
|
||||
key_map,
|
||||
account_id_map,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_from_root(root: N) -> Self {
|
||||
let account_id_map = HashMap::from_iter([(root.account_id(), ChainIndex::root())]);
|
||||
let key_map = BTreeMap::from_iter([(ChainIndex::root(), root)]);
|
||||
|
||||
Self {
|
||||
key_map,
|
||||
account_id_map,
|
||||
}
|
||||
}
|
||||
|
||||
// ToDo: Add function to create a tree from list of nodes with consistency check.
|
||||
|
||||
pub fn find_next_last_child_of_id(&self, parent_id: &ChainIndex) -> Option<u32> {
|
||||
if !self.key_map.contains_key(parent_id) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let leftmost_child = parent_id.nth_child(u32::MIN);
|
||||
|
||||
if !self.key_map.contains_key(&leftmost_child) {
|
||||
return Some(0);
|
||||
}
|
||||
|
||||
let mut right = u32::MAX - 1;
|
||||
let mut left_border = u32::MIN;
|
||||
let mut right_border = u32::MAX;
|
||||
|
||||
loop {
|
||||
let rightmost_child = parent_id.nth_child(right);
|
||||
|
||||
let rightmost_ref = self.key_map.get(&rightmost_child);
|
||||
let rightmost_ref_next = self.key_map.get(&rightmost_child.next_in_line());
|
||||
|
||||
match (&rightmost_ref, &rightmost_ref_next) {
|
||||
(Some(_), Some(_)) => {
|
||||
left_border = right;
|
||||
right = (right + right_border) / 2;
|
||||
}
|
||||
(Some(_), None) => {
|
||||
break Some(right + 1);
|
||||
}
|
||||
(None, None) => {
|
||||
right_border = right;
|
||||
right = (left_border + right) / 2;
|
||||
}
|
||||
(None, Some(_)) => {
|
||||
unreachable!();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_new_node(&mut self, parent_cci: ChainIndex) -> Option<nssa::AccountId> {
|
||||
let father_keys = self.key_map.get(&parent_cci)?;
|
||||
let next_child_id = self
|
||||
.find_next_last_child_of_id(&parent_cci)
|
||||
.expect("Can be None only if parent is not present");
|
||||
let next_cci = parent_cci.nth_child(next_child_id);
|
||||
|
||||
let child_keys = father_keys.nth_child(next_child_id);
|
||||
|
||||
let account_id = child_keys.account_id();
|
||||
|
||||
self.key_map.insert(next_cci.clone(), child_keys);
|
||||
self.account_id_map.insert(account_id, next_cci);
|
||||
|
||||
Some(account_id)
|
||||
}
|
||||
|
||||
pub fn get_node(&self, account_id: nssa::AccountId) -> Option<&N> {
|
||||
self.account_id_map
|
||||
.get(&account_id)
|
||||
.and_then(|chain_id| self.key_map.get(chain_id))
|
||||
}
|
||||
|
||||
pub fn get_node_mut(&mut self, account_id: nssa::AccountId) -> Option<&mut N> {
|
||||
self.account_id_map
|
||||
.get(&account_id)
|
||||
.and_then(|chain_id| self.key_map.get_mut(chain_id))
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, account_id: nssa::AccountId, chain_index: ChainIndex, node: N) {
|
||||
self.account_id_map.insert(account_id, chain_index.clone());
|
||||
self.key_map.insert(chain_index, node);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::str::FromStr;
|
||||
|
||||
use nssa::AccountId;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn seed_holder_for_tests() -> SeedHolder {
|
||||
SeedHolder {
|
||||
seed: [42; 64].to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple_key_tree() {
|
||||
let seed_holder = seed_holder_for_tests();
|
||||
|
||||
let tree = KeyTreePublic::new(&seed_holder);
|
||||
|
||||
assert!(tree.key_map.contains_key(&ChainIndex::root()));
|
||||
assert!(tree.account_id_map.contains_key(&AccountId::new([
|
||||
46, 223, 229, 177, 59, 18, 189, 219, 153, 31, 249, 90, 112, 230, 180, 164, 80, 25, 106,
|
||||
159, 14, 238, 1, 192, 91, 8, 210, 165, 199, 41, 60, 104,
|
||||
])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_small_key_tree() {
|
||||
let seed_holder = seed_holder_for_tests();
|
||||
|
||||
let mut tree = KeyTreePublic::new(&seed_holder);
|
||||
|
||||
let next_last_child_for_parent_id = tree
|
||||
.find_next_last_child_of_id(&ChainIndex::root())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(next_last_child_for_parent_id, 0);
|
||||
|
||||
tree.generate_new_node(ChainIndex::root()).unwrap();
|
||||
|
||||
assert!(
|
||||
tree.key_map
|
||||
.contains_key(&ChainIndex::from_str("/0").unwrap())
|
||||
);
|
||||
|
||||
let next_last_child_for_parent_id = tree
|
||||
.find_next_last_child_of_id(&ChainIndex::root())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(next_last_child_for_parent_id, 1);
|
||||
|
||||
tree.generate_new_node(ChainIndex::root()).unwrap();
|
||||
tree.generate_new_node(ChainIndex::root()).unwrap();
|
||||
tree.generate_new_node(ChainIndex::root()).unwrap();
|
||||
tree.generate_new_node(ChainIndex::root()).unwrap();
|
||||
tree.generate_new_node(ChainIndex::root()).unwrap();
|
||||
tree.generate_new_node(ChainIndex::root()).unwrap();
|
||||
|
||||
let next_last_child_for_parent_id = tree
|
||||
.find_next_last_child_of_id(&ChainIndex::root())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(next_last_child_for_parent_id, 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_key_tree_can_not_make_child_keys() {
|
||||
let seed_holder = seed_holder_for_tests();
|
||||
|
||||
let mut tree = KeyTreePublic::new(&seed_holder);
|
||||
|
||||
let next_last_child_for_parent_id = tree
|
||||
.find_next_last_child_of_id(&ChainIndex::root())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(next_last_child_for_parent_id, 0);
|
||||
|
||||
tree.generate_new_node(ChainIndex::root()).unwrap();
|
||||
|
||||
assert!(
|
||||
tree.key_map
|
||||
.contains_key(&ChainIndex::from_str("/0").unwrap())
|
||||
);
|
||||
|
||||
let next_last_child_for_parent_id = tree
|
||||
.find_next_last_child_of_id(&ChainIndex::root())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(next_last_child_for_parent_id, 1);
|
||||
|
||||
let key_opt = tree.generate_new_node(ChainIndex::from_str("/3").unwrap());
|
||||
|
||||
assert_eq!(key_opt, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_key_tree_complex_structure() {
|
||||
let seed_holder = seed_holder_for_tests();
|
||||
|
||||
let mut tree = KeyTreePublic::new(&seed_holder);
|
||||
|
||||
let next_last_child_for_parent_id = tree
|
||||
.find_next_last_child_of_id(&ChainIndex::root())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(next_last_child_for_parent_id, 0);
|
||||
|
||||
tree.generate_new_node(ChainIndex::root()).unwrap();
|
||||
|
||||
assert!(
|
||||
tree.key_map
|
||||
.contains_key(&ChainIndex::from_str("/0").unwrap())
|
||||
);
|
||||
|
||||
let next_last_child_for_parent_id = tree
|
||||
.find_next_last_child_of_id(&ChainIndex::root())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(next_last_child_for_parent_id, 1);
|
||||
|
||||
tree.generate_new_node(ChainIndex::root()).unwrap();
|
||||
|
||||
assert!(
|
||||
tree.key_map
|
||||
.contains_key(&ChainIndex::from_str("/1").unwrap())
|
||||
);
|
||||
|
||||
let next_last_child_for_parent_id = tree
|
||||
.find_next_last_child_of_id(&ChainIndex::root())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(next_last_child_for_parent_id, 2);
|
||||
|
||||
tree.generate_new_node(ChainIndex::from_str("/0").unwrap())
|
||||
.unwrap();
|
||||
|
||||
let next_last_child_for_parent_id = tree
|
||||
.find_next_last_child_of_id(&ChainIndex::from_str("/0").unwrap())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(next_last_child_for_parent_id, 1);
|
||||
|
||||
assert!(
|
||||
tree.key_map
|
||||
.contains_key(&ChainIndex::from_str("/0/0").unwrap())
|
||||
);
|
||||
|
||||
tree.generate_new_node(ChainIndex::from_str("/0").unwrap())
|
||||
.unwrap();
|
||||
|
||||
let next_last_child_for_parent_id = tree
|
||||
.find_next_last_child_of_id(&ChainIndex::from_str("/0").unwrap())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(next_last_child_for_parent_id, 2);
|
||||
|
||||
assert!(
|
||||
tree.key_map
|
||||
.contains_key(&ChainIndex::from_str("/0/1").unwrap())
|
||||
);
|
||||
|
||||
tree.generate_new_node(ChainIndex::from_str("/0").unwrap())
|
||||
.unwrap();
|
||||
|
||||
let next_last_child_for_parent_id = tree
|
||||
.find_next_last_child_of_id(&ChainIndex::from_str("/0").unwrap())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(next_last_child_for_parent_id, 3);
|
||||
|
||||
assert!(
|
||||
tree.key_map
|
||||
.contains_key(&ChainIndex::from_str("/0/2").unwrap())
|
||||
);
|
||||
|
||||
tree.generate_new_node(ChainIndex::from_str("/0/1").unwrap())
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
tree.key_map
|
||||
.contains_key(&ChainIndex::from_str("/0/1/0").unwrap())
|
||||
);
|
||||
|
||||
let next_last_child_for_parent_id = tree
|
||||
.find_next_last_child_of_id(&ChainIndex::from_str("/0/1").unwrap())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(next_last_child_for_parent_id, 1);
|
||||
}
|
||||
}
|
||||
14
key_protocol/src/key_management/key_tree/traits.rs
Normal file
14
key_protocol/src/key_management/key_tree/traits.rs
Normal file
@ -0,0 +1,14 @@
|
||||
/// Trait, that reperesents a Node in hierarchical key tree
|
||||
pub trait KeyNode {
|
||||
/// Tree root node
|
||||
fn root(seed: [u8; 64]) -> Self;
|
||||
|
||||
/// `cci`'s child of node
|
||||
fn nth_child(&self, cci: u32) -> Self;
|
||||
|
||||
fn chain_code(&self) -> &[u8; 32];
|
||||
|
||||
fn child_index(&self) -> Option<u32>;
|
||||
|
||||
fn account_id(&self) -> nssa::AccountId;
|
||||
}
|
||||
@ -8,12 +8,13 @@ use serde::{Deserialize, Serialize};
|
||||
pub type PublicAccountSigningKey = [u8; 32];
|
||||
|
||||
pub mod ephemeral_key_holder;
|
||||
pub mod key_tree;
|
||||
pub mod secret_holders;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
/// Entrypoint to key management
|
||||
pub struct KeyChain {
|
||||
secret_spending_key: SecretSpendingKey,
|
||||
pub secret_spending_key: SecretSpendingKey,
|
||||
pub private_key_holder: PrivateKeyHolder,
|
||||
pub nullifer_public_key: NullifierPublicKey,
|
||||
pub incoming_viewing_public_key: IncomingViewingPublicKey,
|
||||
@ -39,6 +40,25 @@ impl KeyChain {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_mnemonic(passphrase: String) -> Self {
|
||||
// Currently dropping SeedHolder at the end of initialization.
|
||||
// Not entirely sure if we need it in the future.
|
||||
let seed_holder = SeedHolder::new_mnemonic(passphrase);
|
||||
let secret_spending_key = seed_holder.produce_top_secret_key_holder();
|
||||
|
||||
let private_key_holder = secret_spending_key.produce_private_key_holder();
|
||||
|
||||
let nullifer_public_key = private_key_holder.generate_nullifier_public_key();
|
||||
let incoming_viewing_public_key = private_key_holder.generate_incoming_viewing_public_key();
|
||||
|
||||
Self {
|
||||
secret_spending_key,
|
||||
private_key_holder,
|
||||
nullifer_public_key,
|
||||
incoming_viewing_public_key,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn calculate_shared_secret_receiver(
|
||||
&self,
|
||||
ephemeral_public_key_sender: EphemeralPublicKey,
|
||||
|
||||
@ -8,6 +8,8 @@ use rand::{RngCore, rngs::OsRng};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, digest::FixedOutput};
|
||||
|
||||
const NSSA_ENTROPY_BYTES: [u8; 32] = [0; 32];
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Seed holder. Non-clonable to ensure that different holders use different seeds.
|
||||
/// Produces `TopSecretKeyHolder` objects.
|
||||
@ -37,7 +39,8 @@ impl SeedHolder {
|
||||
let mut enthopy_bytes: [u8; 32] = [0; 32];
|
||||
OsRng.fill_bytes(&mut enthopy_bytes);
|
||||
|
||||
let mnemonic = Mnemonic::from_entropy(&enthopy_bytes).unwrap();
|
||||
let mnemonic = Mnemonic::from_entropy(&enthopy_bytes)
|
||||
.expect("Enthropy must be a multiple of 32 bytes");
|
||||
let seed_wide = mnemonic.to_seed("mnemonic");
|
||||
|
||||
Self {
|
||||
@ -45,6 +48,16 @@ impl SeedHolder {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_mnemonic(passphrase: String) -> Self {
|
||||
let mnemonic = Mnemonic::from_entropy(&NSSA_ENTROPY_BYTES)
|
||||
.expect("Enthropy must be a multiple of 32 bytes");
|
||||
let seed_wide = mnemonic.to_seed(passphrase);
|
||||
|
||||
Self {
|
||||
seed: seed_wide.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_secret_spending_key_hash(&self) -> HashType {
|
||||
let mut hash = hmac_sha512::HMAC::mac(&self.seed, "NSSA_seed");
|
||||
|
||||
@ -155,4 +168,14 @@ mod tests {
|
||||
|
||||
let _ = top_secret_key_holder.generate_outgoing_viewing_secret_key();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_seeds_generated_same_from_same_mnemonic() {
|
||||
let mnemonic = "test_pass";
|
||||
|
||||
let seed_holder1 = SeedHolder::new_mnemonic(mnemonic.to_string());
|
||||
let seed_holder2 = SeedHolder::new_mnemonic(mnemonic.to_string());
|
||||
|
||||
assert_eq!(seed_holder1.seed, seed_holder2.seed);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,16 +4,25 @@ use anyhow::Result;
|
||||
use k256::AffinePoint;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::key_management::KeyChain;
|
||||
use crate::key_management::{
|
||||
KeyChain,
|
||||
key_tree::{KeyTreePrivate, KeyTreePublic, chain_index::ChainIndex},
|
||||
secret_holders::SeedHolder,
|
||||
};
|
||||
|
||||
pub type PublicKey = AffinePoint;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct NSSAUserData {
|
||||
/// Map for all user public accounts
|
||||
pub pub_account_signing_keys: HashMap<nssa::AccountId, nssa::PrivateKey>,
|
||||
/// Map for all user private accounts
|
||||
pub user_private_accounts: HashMap<nssa::AccountId, (KeyChain, nssa_core::account::Account)>,
|
||||
/// Default public accounts
|
||||
pub default_pub_account_signing_keys: HashMap<nssa::AccountId, nssa::PrivateKey>,
|
||||
/// Default private accounts
|
||||
pub default_user_private_accounts:
|
||||
HashMap<nssa::AccountId, (KeyChain, nssa_core::account::Account)>,
|
||||
/// Tree of public keys
|
||||
pub public_key_tree: KeyTreePublic,
|
||||
/// Tree of private keys
|
||||
pub private_key_tree: KeyTreePrivate,
|
||||
}
|
||||
|
||||
impl NSSAUserData {
|
||||
@ -47,39 +56,42 @@ impl NSSAUserData {
|
||||
}
|
||||
|
||||
pub fn new_with_accounts(
|
||||
accounts_keys: HashMap<nssa::AccountId, nssa::PrivateKey>,
|
||||
accounts_key_chains: HashMap<nssa::AccountId, (KeyChain, nssa_core::account::Account)>,
|
||||
default_accounts_keys: HashMap<nssa::AccountId, nssa::PrivateKey>,
|
||||
default_accounts_key_chains: HashMap<
|
||||
nssa::AccountId,
|
||||
(KeyChain, nssa_core::account::Account),
|
||||
>,
|
||||
public_key_tree: KeyTreePublic,
|
||||
private_key_tree: KeyTreePrivate,
|
||||
) -> Result<Self> {
|
||||
if !Self::valid_public_key_transaction_pairing_check(&accounts_keys) {
|
||||
if !Self::valid_public_key_transaction_pairing_check(&default_accounts_keys) {
|
||||
anyhow::bail!(
|
||||
"Key transaction pairing check not satisfied, there is account_ids, which is not derived from keys"
|
||||
);
|
||||
}
|
||||
|
||||
if !Self::valid_private_key_transaction_pairing_check(&accounts_key_chains) {
|
||||
if !Self::valid_private_key_transaction_pairing_check(&default_accounts_key_chains) {
|
||||
anyhow::bail!(
|
||||
"Key transaction pairing check not satisfied, there is account_ids, which is not derived from keys"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
pub_account_signing_keys: accounts_keys,
|
||||
user_private_accounts: accounts_key_chains,
|
||||
default_pub_account_signing_keys: default_accounts_keys,
|
||||
default_user_private_accounts: default_accounts_key_chains,
|
||||
public_key_tree,
|
||||
private_key_tree,
|
||||
})
|
||||
}
|
||||
|
||||
/// Generated new private key for public transaction signatures
|
||||
///
|
||||
/// Returns the account_id of new account
|
||||
pub fn generate_new_public_transaction_private_key(&mut self) -> nssa::AccountId {
|
||||
let private_key = nssa::PrivateKey::new_os_random();
|
||||
let account_id =
|
||||
nssa::AccountId::from(&nssa::PublicKey::new_from_private_key(&private_key));
|
||||
|
||||
self.pub_account_signing_keys
|
||||
.insert(account_id, private_key);
|
||||
|
||||
account_id
|
||||
pub fn generate_new_public_transaction_private_key(
|
||||
&mut self,
|
||||
parent_cci: ChainIndex,
|
||||
) -> nssa::AccountId {
|
||||
self.public_key_tree.generate_new_node(parent_cci).unwrap()
|
||||
}
|
||||
|
||||
/// Returns the signing key for public transaction signatures
|
||||
@ -87,22 +99,23 @@ impl NSSAUserData {
|
||||
&self,
|
||||
account_id: &nssa::AccountId,
|
||||
) -> Option<&nssa::PrivateKey> {
|
||||
self.pub_account_signing_keys.get(account_id)
|
||||
// First seek in defaults
|
||||
if let Some(key) = self.default_pub_account_signing_keys.get(account_id) {
|
||||
Some(key)
|
||||
// Then seek in tree
|
||||
} else {
|
||||
self.public_key_tree.get_node(*account_id).map(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generated new private key for privacy preserving transactions
|
||||
///
|
||||
/// Returns the account_id of new account
|
||||
pub fn generate_new_privacy_preserving_transaction_key_chain(&mut self) -> nssa::AccountId {
|
||||
let key_chain = KeyChain::new_os_random();
|
||||
let account_id = nssa::AccountId::from(&key_chain.nullifer_public_key);
|
||||
|
||||
self.user_private_accounts.insert(
|
||||
account_id,
|
||||
(key_chain, nssa_core::account::Account::default()),
|
||||
);
|
||||
|
||||
account_id
|
||||
pub fn generate_new_privacy_preserving_transaction_key_chain(
|
||||
&mut self,
|
||||
parent_cci: ChainIndex,
|
||||
) -> nssa::AccountId {
|
||||
self.private_key_tree.generate_new_node(parent_cci).unwrap()
|
||||
}
|
||||
|
||||
/// Returns the signing key for public transaction signatures
|
||||
@ -110,7 +123,13 @@ impl NSSAUserData {
|
||||
&self,
|
||||
account_id: &nssa::AccountId,
|
||||
) -> Option<&(KeyChain, nssa_core::account::Account)> {
|
||||
self.user_private_accounts.get(account_id)
|
||||
// First seek in defaults
|
||||
if let Some(key) = self.default_user_private_accounts.get(account_id) {
|
||||
Some(key)
|
||||
// Then seek in tree
|
||||
} else {
|
||||
self.private_key_tree.get_node(*account_id).map(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the signing key for public transaction signatures
|
||||
@ -118,14 +137,27 @@ impl NSSAUserData {
|
||||
&mut self,
|
||||
account_id: &nssa::AccountId,
|
||||
) -> Option<&mut (KeyChain, nssa_core::account::Account)> {
|
||||
self.user_private_accounts.get_mut(account_id)
|
||||
// First seek in defaults
|
||||
if let Some(key) = self.default_user_private_accounts.get_mut(account_id) {
|
||||
Some(key)
|
||||
// Then seek in tree
|
||||
} else {
|
||||
self.private_key_tree
|
||||
.get_node_mut(*account_id)
|
||||
.map(Into::into)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for NSSAUserData {
|
||||
fn default() -> Self {
|
||||
// Safe unwrap as maps are empty
|
||||
Self::new_with_accounts(HashMap::default(), HashMap::default()).unwrap()
|
||||
Self::new_with_accounts(
|
||||
HashMap::new(),
|
||||
HashMap::new(),
|
||||
KeyTreePublic::new(&SeedHolder::new_mnemonic("default".to_string())),
|
||||
KeyTreePrivate::new(&SeedHolder::new_mnemonic("default".to_string())),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
@ -137,20 +169,27 @@ mod tests {
|
||||
fn test_new_account() {
|
||||
let mut user_data = NSSAUserData::default();
|
||||
|
||||
let addr_pub = user_data.generate_new_public_transaction_private_key();
|
||||
let addr_private = user_data.generate_new_privacy_preserving_transaction_key_chain();
|
||||
let account_id_pub =
|
||||
user_data.generate_new_public_transaction_private_key(ChainIndex::root());
|
||||
let account_id_private =
|
||||
user_data.generate_new_privacy_preserving_transaction_key_chain(ChainIndex::root());
|
||||
|
||||
let is_private_key_generated = user_data.get_pub_account_signing_key(&addr_pub).is_some();
|
||||
let is_private_key_generated = user_data
|
||||
.get_pub_account_signing_key(&account_id_pub)
|
||||
.is_some();
|
||||
|
||||
assert!(is_private_key_generated);
|
||||
|
||||
let is_key_chain_generated = user_data.get_private_account(&addr_private).is_some();
|
||||
let is_key_chain_generated = user_data.get_private_account(&account_id_private).is_some();
|
||||
|
||||
assert!(is_key_chain_generated);
|
||||
|
||||
let addr_private_str = addr_private.to_string();
|
||||
println!("{addr_private_str:#?}");
|
||||
let key_chain = &user_data.get_private_account(&addr_private).unwrap().0;
|
||||
let account_id_private_str = account_id_private.to_string();
|
||||
println!("{account_id_private_str:#?}");
|
||||
let key_chain = &user_data
|
||||
.get_private_account(&account_id_private)
|
||||
.unwrap()
|
||||
.0;
|
||||
println!("{key_chain:#?}");
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,6 +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 = "1.5.7"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
@ -3,6 +3,7 @@ use std::{fmt::Display, str::FromStr};
|
||||
|
||||
#[cfg(feature = "host")]
|
||||
use base58::{FromBase58, ToBase58};
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::program::ProgramId;
|
||||
@ -11,7 +12,9 @@ pub type Nonce = u128;
|
||||
pub type Data = Vec<u8>;
|
||||
|
||||
/// Account to be used both in public and private contexts
|
||||
#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Clone, Default, PartialEq, Eq, BorshSerialize, BorshDeserialize,
|
||||
)]
|
||||
#[cfg_attr(any(feature = "host", test), derive(Debug))]
|
||||
pub struct Account {
|
||||
pub program_owner: ProgramId,
|
||||
@ -39,7 +42,7 @@ impl AccountWithMetadata {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Hash, BorshSerialize, BorshDeserialize)]
|
||||
#[cfg_attr(
|
||||
any(feature = "host", test),
|
||||
derive(Debug, Copy, PartialOrd, Ord, Default)
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use risc0_zkvm::sha::{Impl, Sha256};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{NullifierPublicKey, account::Account};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
|
||||
#[cfg_attr(any(feature = "host", test), derive(Debug, Clone, PartialEq, Eq, Hash))]
|
||||
pub struct Commitment(pub(super) [u8; 32]);
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use chacha20::{
|
||||
ChaCha20,
|
||||
cipher::{KeyIvInit, StreamCipher},
|
||||
@ -20,7 +21,7 @@ pub struct SharedSecretKey(pub [u8; 32]);
|
||||
|
||||
pub struct EncryptionScheme;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
|
||||
#[cfg_attr(any(feature = "host", test), derive(Debug, Clone, PartialEq, Eq))]
|
||||
pub struct Ciphertext(pub(crate) Vec<u8>);
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use k256::{
|
||||
AffinePoint, EncodedPoint, FieldBytes, ProjectivePoint,
|
||||
elliptic_curve::{
|
||||
@ -9,7 +10,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{SharedSecretKey, encryption::Scalar};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct Secp256k1Point(pub Vec<u8>);
|
||||
|
||||
impl Secp256k1Point {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use risc0_zkvm::sha::{Impl, Sha256};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@ -40,7 +41,7 @@ impl From<&NullifierSecretKey> for NullifierPublicKey {
|
||||
|
||||
pub type NullifierSecretKey = [u8; 32];
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
|
||||
#[cfg_attr(any(feature = "host", test), derive(Debug, Clone, PartialEq, Eq, Hash))]
|
||||
pub struct Nullifier(pub(super) [u8; 32]);
|
||||
|
||||
|
||||
@ -3,6 +3,8 @@ use std::collections::HashSet;
|
||||
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];
|
||||
@ -15,6 +17,43 @@ 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 {
|
||||
@ -22,7 +61,56 @@ pub struct ChainedCall {
|
||||
pub program_id: ProgramId,
|
||||
/// The instruction data to pass
|
||||
pub instruction_data: InstructionData,
|
||||
pub account_indices: Vec<usize>,
|
||||
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)]
|
||||
@ -32,10 +120,8 @@ pub struct ProgramOutput {
|
||||
pub instruction_data: InstructionData,
|
||||
/// The account pre states the program received to produce this output
|
||||
pub pre_states: Vec<AccountWithMetadata>,
|
||||
/// The account post states produced with the given pre states and instruction data
|
||||
pub post_states: Vec<Account>,
|
||||
/// The optional next call of a program
|
||||
pub chained_call: Option<ChainedCall>,
|
||||
pub post_states: Vec<AccountPostState>,
|
||||
pub chained_calls: Vec<ChainedCall>,
|
||||
}
|
||||
|
||||
pub fn read_nssa_inputs<T: DeserializeOwned>() -> (ProgramInput<T>, InstructionData) {
|
||||
@ -54,13 +140,13 @@ pub fn read_nssa_inputs<T: DeserializeOwned>() -> (ProgramInput<T>, InstructionD
|
||||
pub fn write_nssa_outputs(
|
||||
instruction_data: InstructionData,
|
||||
pre_states: Vec<AccountWithMetadata>,
|
||||
post_states: Vec<Account>,
|
||||
post_states: Vec<AccountPostState>,
|
||||
) {
|
||||
let output = ProgramOutput {
|
||||
instruction_data,
|
||||
pre_states,
|
||||
post_states,
|
||||
chained_call: None,
|
||||
chained_calls: Vec::new(),
|
||||
};
|
||||
env::commit(&output);
|
||||
}
|
||||
@ -68,14 +154,14 @@ pub fn write_nssa_outputs(
|
||||
pub fn write_nssa_outputs_with_chained_call(
|
||||
instruction_data: InstructionData,
|
||||
pre_states: Vec<AccountWithMetadata>,
|
||||
post_states: Vec<Account>,
|
||||
chained_call: Option<ChainedCall>,
|
||||
post_states: Vec<AccountPostState>,
|
||||
chained_calls: Vec<ChainedCall>,
|
||||
) {
|
||||
let output = ProgramOutput {
|
||||
instruction_data,
|
||||
pre_states,
|
||||
post_states,
|
||||
chained_call,
|
||||
chained_calls,
|
||||
};
|
||||
env::commit(&output);
|
||||
}
|
||||
@ -88,7 +174,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. Check account ids are all different
|
||||
@ -103,25 +189,27 @@ pub fn validate_execution(
|
||||
|
||||
for (pre, post) in pre_states.iter().zip(post_states) {
|
||||
// 3. Nonce must remain unchanged
|
||||
if pre.account.nonce != post.nonce {
|
||||
if pre.account.nonce != post.account.nonce {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4. 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;
|
||||
|
||||
// 5. 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;
|
||||
}
|
||||
|
||||
// 6. 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
|
||||
{
|
||||
@ -130,14 +218,14 @@ pub fn validate_execution(
|
||||
|
||||
// 7. 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;
|
||||
}
|
||||
}
|
||||
|
||||
// 8. 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;
|
||||
}
|
||||
@ -155,3 +243,53 @@ fn validate_uniqueness_of_account_ids(pre_states: &[AccountWithMetadata]) -> boo
|
||||
|
||||
number_of_accounts == number_of_account_ids
|
||||
}
|
||||
|
||||
#[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
nssa/program_methods/guest/Cargo.lock
generated
1
nssa/program_methods/guest/Cargo.lock
generated
@ -1574,6 +1574,7 @@ checksum = "a5b0c77c1b780822bc749a33e39aeb2c07584ab93332303babeabb645298a76e"
|
||||
name = "nssa-core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"borsh",
|
||||
"chacha20",
|
||||
"risc0-zkvm",
|
||||
"serde",
|
||||
|
||||
@ -1,17 +1,18 @@
|
||||
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) -> (AccountWithMetadata, Account) {
|
||||
let account_to_claim = pre_state.account.clone();
|
||||
fn initialize_account(pre_state: AccountWithMetadata) -> AccountPostState {
|
||||
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() {
|
||||
panic!("Invalid input");
|
||||
if account_to_claim.account() != &Account::default() {
|
||||
panic!("Account must be uninitialized");
|
||||
}
|
||||
|
||||
// Continue only if the owner authorized this operation
|
||||
@ -19,8 +20,7 @@ fn initialize_account(pre_state: AccountWithMetadata) -> (AccountWithMetadata, A
|
||||
panic!("Invalid input");
|
||||
}
|
||||
|
||||
// Noop will result in account being claimed for this program
|
||||
(pre_state, account_to_claim)
|
||||
account_to_claim
|
||||
}
|
||||
|
||||
/// Transfers `balance_to_move` native balance from `sender` to `recipient`.
|
||||
@ -28,7 +28,7 @@ fn transfer(
|
||||
sender: AccountWithMetadata,
|
||||
recipient: AccountWithMetadata,
|
||||
balance_to_move: u128,
|
||||
) -> (Vec<AccountWithMetadata>, Vec<Account>) {
|
||||
) -> Vec<AccountPostState> {
|
||||
// Continue only if the sender has authorized this operation
|
||||
if !sender.is_authorized {
|
||||
panic!("Invalid input");
|
||||
@ -40,11 +40,27 @@ fn transfer(
|
||||
}
|
||||
|
||||
// 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;
|
||||
(vec![sender, recipient], vec![sender_post, recipient_post])
|
||||
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)
|
||||
}
|
||||
};
|
||||
|
||||
vec![sender_post, recipient_post]
|
||||
}
|
||||
|
||||
/// A transfer of balance program.
|
||||
@ -59,10 +75,10 @@ fn main() {
|
||||
instruction_words,
|
||||
) = read_nssa_inputs();
|
||||
|
||||
let (pre_states, post_states) = match (pre_states.as_slice(), balance_to_move) {
|
||||
let post_states = match (pre_states.as_slice(), balance_to_move) {
|
||||
([account_to_claim], 0) => {
|
||||
let (pre, post) = initialize_account(account_to_claim.clone());
|
||||
(vec![pre], vec![post])
|
||||
let post = initialize_account(account_to_claim.clone());
|
||||
vec![post]
|
||||
}
|
||||
([sender, recipient], balance_to_move) => {
|
||||
transfer(sender.clone(), recipient.clone(), balance_to_move)
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
use nssa_core::program::{ProgramInput, read_nssa_inputs, write_nssa_outputs};
|
||||
use risc0_zkvm::{
|
||||
sha::{Impl, Sha256},
|
||||
};
|
||||
use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs};
|
||||
use risc0_zkvm::sha::{Impl, Sha256};
|
||||
|
||||
const PRIZE: u128 = 150;
|
||||
|
||||
@ -74,6 +72,9 @@ fn main() {
|
||||
write_nssa_outputs(
|
||||
instruction_words,
|
||||
vec![pinata, winner],
|
||||
vec![pinata_post, winner_post],
|
||||
vec![
|
||||
AccountPostState::new(pinata_post),
|
||||
AccountPostState::new(winner_post),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
111
nssa/program_methods/guest/src/bin/pinata_token.rs
Normal file
111
nssa/program_methods/guest/src/bin/pinata_token.rs
Normal file
@ -0,0 +1,111 @@
|
||||
use nssa_core::program::{
|
||||
AccountPostState, ChainedCall, PdaSeed, ProgramInput, read_nssa_inputs,
|
||||
write_nssa_outputs_with_chained_call,
|
||||
};
|
||||
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,
|
||||
},
|
||||
instruction_words,
|
||||
) = 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(
|
||||
instruction_words,
|
||||
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,
|
||||
);
|
||||
}
|
||||
@ -29,11 +29,11 @@ fn main() {
|
||||
panic!("Max chained calls depth is exceeded");
|
||||
}
|
||||
|
||||
if program_outputs
|
||||
.last()
|
||||
.and_then(|last| last.chained_call.as_ref())
|
||||
.is_some()
|
||||
{
|
||||
let Some(last_program_call) = program_outputs.last() else {
|
||||
panic!("Program outputs is empty")
|
||||
};
|
||||
|
||||
if !last_program_call.chained_calls.is_empty() {
|
||||
panic!("Call stack is incomplete");
|
||||
}
|
||||
|
||||
@ -41,7 +41,12 @@ fn main() {
|
||||
let caller = &window[0];
|
||||
let callee = &window[1];
|
||||
|
||||
let Some(chained_call) = &caller.chained_call else {
|
||||
if caller.chained_calls.len() > 1 {
|
||||
panic!("Privacy Multi-chained calls are not supported yet");
|
||||
}
|
||||
|
||||
// TODO: Modify when multi-chain calls are supported in the circuit
|
||||
let Some(chained_call) = &caller.chained_calls.first() else {
|
||||
panic!("Expected chained call");
|
||||
};
|
||||
|
||||
@ -71,9 +76,16 @@ fn main() {
|
||||
}
|
||||
|
||||
// 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 = 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 = program_id;
|
||||
} else {
|
||||
panic!("Cannot claim an initialized account")
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,10 +101,11 @@ fn main() {
|
||||
} else {
|
||||
pre_states.push(pre.clone());
|
||||
}
|
||||
state_diff.insert(pre.account_id.clone(), post.clone());
|
||||
state_diff.insert(pre.account_id.clone(), post.account().clone());
|
||||
}
|
||||
|
||||
if let Some(next_chained_call) = &program_output.chained_call {
|
||||
// TODO: Modify when multi-chain calls are supported in the circuit
|
||||
if let Some(next_chained_call) = &program_output.chained_calls.first() {
|
||||
program_id = next_chained_call.program_id;
|
||||
} else if i != program_outputs.len() - 1 {
|
||||
panic!("Inner call without a chained call found")
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
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 two functions:
|
||||
// The token program has three functions:
|
||||
// 1. New token definition.
|
||||
// Arguments to this function are:
|
||||
// * Two **default** accounts: [definition_account, holding_account].
|
||||
@ -18,6 +20,11 @@ use nssa_core::{
|
||||
// * Two accounts: [sender_account, recipient_account].
|
||||
// * An instruction data byte string of length 23, indicating the total supply with the following layout
|
||||
// [0x01 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 || 0x00 || 0x00 || 0x00].
|
||||
// 3. Initialize account with zero balance
|
||||
// Arguments to this function are:
|
||||
// * Two accounts: [definition_account, account_to_initialize].
|
||||
// * An dummy byte string of length 23, with the following layout
|
||||
// [0x02 || 0x00 || 0x00 || 0x00 || ... || 0x00 || 0x00].
|
||||
|
||||
const TOKEN_DEFINITION_TYPE: u8 = 0;
|
||||
const TOKEN_DEFINITION_DATA_SIZE: usize = 23;
|
||||
@ -45,6 +52,25 @@ impl TokenDefinition {
|
||||
bytes[7..].copy_from_slice(&self.total_supply.to_le_bytes());
|
||||
bytes.into()
|
||||
}
|
||||
|
||||
fn parse(data: &[u8]) -> Option<Self> {
|
||||
if data.len() != TOKEN_DEFINITION_DATA_SIZE || data[0] != TOKEN_DEFINITION_TYPE {
|
||||
None
|
||||
} else {
|
||||
let account_type = data[0];
|
||||
let name = data[1..7].try_into().unwrap();
|
||||
let total_supply = u128::from_le_bytes(
|
||||
data[7..]
|
||||
.try_into()
|
||||
.expect("Total supply must be 16 bytes little-endian"),
|
||||
);
|
||||
Some(Self {
|
||||
account_type,
|
||||
name,
|
||||
total_supply,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TokenHolding {
|
||||
@ -58,17 +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().unwrap());
|
||||
let balance = u128::from_le_bytes(data[33..].try_into().unwrap());
|
||||
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 {
|
||||
@ -80,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");
|
||||
}
|
||||
@ -116,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]
|
||||
@ -131,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");
|
||||
}
|
||||
@ -164,7 +205,40 @@ 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<AccountPostState> {
|
||||
if pre_states.len() != 2 {
|
||||
panic!("Invalid number of accounts");
|
||||
}
|
||||
|
||||
let definition = &pre_states[0];
|
||||
let account_to_initialize = &pre_states[1];
|
||||
|
||||
if account_to_initialize.account != Account::default() {
|
||||
panic!("Only uninitialized accounts can be initialized");
|
||||
}
|
||||
|
||||
// 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
|
||||
let _definition_values =
|
||||
TokenDefinition::parse(&definition.account.data).expect("Definition account must be valid");
|
||||
let holding_values = TokenHolding::new(&definition.account_id);
|
||||
|
||||
let definition_post = definition.account.clone();
|
||||
let mut account_to_initialize = account_to_initialize.account.clone();
|
||||
account_to_initialize.data = holding_values.into_data();
|
||||
|
||||
vec![
|
||||
AccountPostState::new(definition_post),
|
||||
AccountPostState::new_claimed(account_to_initialize),
|
||||
]
|
||||
}
|
||||
|
||||
type Instruction = [u8; 23];
|
||||
@ -178,36 +252,58 @@ fn main() {
|
||||
instruction_words,
|
||||
) = read_nssa_inputs::<Instruction>();
|
||||
|
||||
match instruction[0] {
|
||||
let post_states = match instruction[0] {
|
||||
0 => {
|
||||
// Parse instruction
|
||||
let total_supply = u128::from_le_bytes(instruction[1..17].try_into().unwrap());
|
||||
let name: [u8; 6] = instruction[17..].try_into().unwrap();
|
||||
let total_supply = u128::from_le_bytes(
|
||||
instruction[1..17]
|
||||
.try_into()
|
||||
.expect("Total supply must be 16 bytes little-endian"),
|
||||
);
|
||||
let name: [u8; 6] = instruction[17..]
|
||||
.try_into()
|
||||
.expect("Name must be 6 bytes long");
|
||||
assert_ne!(name, [0; 6]);
|
||||
|
||||
// Execute
|
||||
let post_states = new_definition(&pre_states, name, total_supply);
|
||||
write_nssa_outputs(instruction_words, pre_states, post_states);
|
||||
new_definition(&pre_states, name, total_supply)
|
||||
}
|
||||
1 => {
|
||||
// Parse instruction
|
||||
let balance_to_move = u128::from_le_bytes(instruction[1..17].try_into().unwrap());
|
||||
let name: [u8; 6] = instruction[17..].try_into().unwrap();
|
||||
let balance_to_move = u128::from_le_bytes(
|
||||
instruction[1..17]
|
||||
.try_into()
|
||||
.expect("Balance to move must be 16 bytes little-endian"),
|
||||
);
|
||||
let name: [u8; 6] = instruction[17..]
|
||||
.try_into()
|
||||
.expect("Name must be 6 bytes long");
|
||||
assert_eq!(name, [0; 6]);
|
||||
|
||||
// Execute
|
||||
let post_states = transfer(&pre_states, balance_to_move);
|
||||
write_nssa_outputs(instruction_words, pre_states, post_states);
|
||||
transfer(&pre_states, balance_to_move)
|
||||
}
|
||||
2 => {
|
||||
// Initialize account
|
||||
if instruction[1..] != [0; 22] {
|
||||
panic!("Invalid instruction for initialize account");
|
||||
}
|
||||
initialize_account(&pre_states)
|
||||
}
|
||||
_ => panic!("Invalid instruction"),
|
||||
};
|
||||
|
||||
write_nssa_outputs(instruction_words, pre_states, post_states);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use nssa_core::account::{Account, AccountId, AccountWithMetadata};
|
||||
|
||||
use crate::{TOKEN_HOLDING_DATA_SIZE, TOKEN_HOLDING_TYPE, new_definition, transfer};
|
||||
use crate::{
|
||||
TOKEN_DEFINITION_DATA_SIZE, TOKEN_HOLDING_DATA_SIZE, TOKEN_HOLDING_TYPE,
|
||||
initialize_account, new_definition, transfer,
|
||||
};
|
||||
|
||||
#[should_panic(expected = "Invalid number of input accounts")]
|
||||
#[test]
|
||||
@ -308,14 +404,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,
|
||||
@ -540,18 +636,51 @@ 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
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_token_initialize_account_succeeds() {
|
||||
let pre_states = vec![
|
||||
AccountWithMetadata {
|
||||
account: Account {
|
||||
// Definition ID with
|
||||
data: [0; TOKEN_DEFINITION_DATA_SIZE - 16]
|
||||
.into_iter()
|
||||
.chain(u128::to_le_bytes(1000))
|
||||
.collect(),
|
||||
..Account::default()
|
||||
},
|
||||
is_authorized: false,
|
||||
account_id: AccountId::new([1; 32]),
|
||||
},
|
||||
AccountWithMetadata {
|
||||
account: Account::default(),
|
||||
is_authorized: false,
|
||||
account_id: AccountId::new([2; 32]),
|
||||
},
|
||||
];
|
||||
let post_states = initialize_account(&pre_states);
|
||||
let [definition, holding] = post_states.try_into().ok().unwrap();
|
||||
assert_eq!(definition.account().data, pre_states[0].account.data);
|
||||
assert_eq!(
|
||||
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
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,258 +1,24 @@
|
||||
use std::io::{Cursor, Read};
|
||||
|
||||
use nssa_core::{
|
||||
Commitment, Nullifier,
|
||||
account::Account,
|
||||
encryption::{Ciphertext, EphemeralPublicKey},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
AccountId, PrivacyPreservingTransaction, PublicKey, Signature,
|
||||
error::NssaError,
|
||||
privacy_preserving_transaction::{
|
||||
circuit::Proof,
|
||||
message::{EncryptedAccountData, Message},
|
||||
witness_set::WitnessSet,
|
||||
},
|
||||
PrivacyPreservingTransaction, error::NssaError,
|
||||
privacy_preserving_transaction::message::Message,
|
||||
};
|
||||
|
||||
const MESSAGE_ENCODING_PREFIX_LEN: usize = 32;
|
||||
const MESSAGE_ENCODING_PREFIX: &[u8; MESSAGE_ENCODING_PREFIX_LEN] =
|
||||
b"/NSSA/v0.2/TxMessage/Private/\x00\x00\x00";
|
||||
|
||||
impl EncryptedAccountData {
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut bytes = self.ciphertext.to_bytes();
|
||||
bytes.extend_from_slice(&self.epk.to_bytes());
|
||||
bytes.push(self.view_tag);
|
||||
bytes
|
||||
}
|
||||
|
||||
pub fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result<Self, NssaError> {
|
||||
let ciphertext = Ciphertext::from_cursor(cursor)?;
|
||||
let epk = EphemeralPublicKey::from_cursor(cursor)?;
|
||||
|
||||
let mut tag_bytes = [0; 1];
|
||||
cursor.read_exact(&mut tag_bytes)?;
|
||||
let view_tag = tag_bytes[0];
|
||||
|
||||
Ok(Self {
|
||||
ciphertext,
|
||||
epk,
|
||||
view_tag,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Message {
|
||||
pub(crate) fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut bytes = MESSAGE_ENCODING_PREFIX.to_vec();
|
||||
|
||||
// Public account_ids
|
||||
let public_account_ids_len: u32 = self.public_account_ids.len() as u32;
|
||||
bytes.extend_from_slice(&public_account_ids_len.to_le_bytes());
|
||||
for account_id in &self.public_account_ids {
|
||||
bytes.extend_from_slice(account_id.value());
|
||||
}
|
||||
// Nonces
|
||||
let nonces_len = self.nonces.len() as u32;
|
||||
bytes.extend(&nonces_len.to_le_bytes());
|
||||
for nonce in &self.nonces {
|
||||
bytes.extend(&nonce.to_le_bytes());
|
||||
}
|
||||
// Public post states
|
||||
let public_post_states_len: u32 = self.public_post_states.len() as u32;
|
||||
bytes.extend_from_slice(&public_post_states_len.to_le_bytes());
|
||||
for account in &self.public_post_states {
|
||||
bytes.extend_from_slice(&account.to_bytes());
|
||||
}
|
||||
|
||||
// Encrypted post states
|
||||
let encrypted_accounts_post_states_len: u32 =
|
||||
self.encrypted_private_post_states.len() as u32;
|
||||
bytes.extend_from_slice(&encrypted_accounts_post_states_len.to_le_bytes());
|
||||
for encrypted_account in &self.encrypted_private_post_states {
|
||||
bytes.extend_from_slice(&encrypted_account.to_bytes());
|
||||
}
|
||||
|
||||
// New commitments
|
||||
let new_commitments_len: u32 = self.new_commitments.len() as u32;
|
||||
bytes.extend_from_slice(&new_commitments_len.to_le_bytes());
|
||||
for commitment in &self.new_commitments {
|
||||
bytes.extend_from_slice(&commitment.to_byte_array());
|
||||
}
|
||||
|
||||
// New nullifiers
|
||||
let new_nullifiers_len: u32 = self.new_nullifiers.len() as u32;
|
||||
bytes.extend_from_slice(&new_nullifiers_len.to_le_bytes());
|
||||
for (nullifier, commitment_set_digest) in &self.new_nullifiers {
|
||||
bytes.extend_from_slice(&nullifier.to_byte_array());
|
||||
bytes.extend_from_slice(commitment_set_digest);
|
||||
}
|
||||
|
||||
bytes
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
borsh::to_vec(&self).expect("Autoderived borsh serialization failure")
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result<Self, NssaError> {
|
||||
let prefix = {
|
||||
let mut this = [0u8; MESSAGE_ENCODING_PREFIX_LEN];
|
||||
cursor.read_exact(&mut this)?;
|
||||
this
|
||||
};
|
||||
if &prefix != MESSAGE_ENCODING_PREFIX {
|
||||
return Err(NssaError::TransactionDeserializationError(
|
||||
"Invalid privacy preserving message prefix".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut len_bytes = [0u8; 4];
|
||||
|
||||
// Public account_ids
|
||||
cursor.read_exact(&mut len_bytes)?;
|
||||
let public_account_ids_len = u32::from_le_bytes(len_bytes) as usize;
|
||||
let mut public_account_ids = Vec::with_capacity(public_account_ids_len);
|
||||
for _ in 0..public_account_ids_len {
|
||||
let mut value = [0u8; 32];
|
||||
cursor.read_exact(&mut value)?;
|
||||
public_account_ids.push(AccountId::new(value))
|
||||
}
|
||||
|
||||
// Nonces
|
||||
cursor.read_exact(&mut len_bytes)?;
|
||||
let nonces_len = u32::from_le_bytes(len_bytes) as usize;
|
||||
let mut nonces = Vec::with_capacity(nonces_len);
|
||||
for _ in 0..nonces_len {
|
||||
let mut buf = [0u8; 16];
|
||||
cursor.read_exact(&mut buf)?;
|
||||
nonces.push(u128::from_le_bytes(buf))
|
||||
}
|
||||
|
||||
// Public post states
|
||||
cursor.read_exact(&mut len_bytes)?;
|
||||
let public_post_states_len = u32::from_le_bytes(len_bytes) as usize;
|
||||
let mut public_post_states = Vec::with_capacity(public_post_states_len);
|
||||
for _ in 0..public_post_states_len {
|
||||
public_post_states.push(Account::from_cursor(cursor)?);
|
||||
}
|
||||
|
||||
// Encrypted private post states
|
||||
cursor.read_exact(&mut len_bytes)?;
|
||||
let encrypted_len = u32::from_le_bytes(len_bytes) as usize;
|
||||
let mut encrypted_private_post_states = Vec::with_capacity(encrypted_len);
|
||||
for _ in 0..encrypted_len {
|
||||
encrypted_private_post_states.push(EncryptedAccountData::from_cursor(cursor)?);
|
||||
}
|
||||
|
||||
// New commitments
|
||||
cursor.read_exact(&mut len_bytes)?;
|
||||
let new_commitments_len = u32::from_le_bytes(len_bytes) as usize;
|
||||
let mut new_commitments = Vec::with_capacity(new_commitments_len);
|
||||
for _ in 0..new_commitments_len {
|
||||
new_commitments.push(Commitment::from_cursor(cursor)?);
|
||||
}
|
||||
|
||||
// New nullifiers
|
||||
cursor.read_exact(&mut len_bytes)?;
|
||||
let new_nullifiers_len = u32::from_le_bytes(len_bytes) as usize;
|
||||
let mut new_nullifiers = Vec::with_capacity(new_nullifiers_len);
|
||||
for _ in 0..new_nullifiers_len {
|
||||
let nullifier = Nullifier::from_cursor(cursor)?;
|
||||
let mut commitment_set_digest = [0; 32];
|
||||
cursor.read_exact(&mut commitment_set_digest)?;
|
||||
new_nullifiers.push((nullifier, commitment_set_digest));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
public_account_ids,
|
||||
nonces,
|
||||
public_post_states,
|
||||
encrypted_private_post_states,
|
||||
new_commitments,
|
||||
new_nullifiers,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl WitnessSet {
|
||||
pub(crate) fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut bytes = Vec::new();
|
||||
let size = self.signatures_and_public_keys().len() as u32;
|
||||
bytes.extend_from_slice(&size.to_le_bytes());
|
||||
for (signature, public_key) in self.signatures_and_public_keys() {
|
||||
bytes.extend_from_slice(signature.to_bytes());
|
||||
bytes.extend_from_slice(public_key.to_bytes());
|
||||
}
|
||||
bytes.extend_from_slice(&self.proof.to_bytes());
|
||||
bytes
|
||||
}
|
||||
|
||||
pub(crate) fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result<Self, NssaError> {
|
||||
let num_signatures: u32 = {
|
||||
let mut buf = [0u8; 4];
|
||||
cursor.read_exact(&mut buf)?;
|
||||
u32::from_le_bytes(buf)
|
||||
};
|
||||
let mut signatures_and_public_keys = Vec::with_capacity(num_signatures as usize);
|
||||
for _i in 0..num_signatures {
|
||||
let signature = Signature::from_cursor(cursor)?;
|
||||
let public_key = PublicKey::from_cursor(cursor)?;
|
||||
signatures_and_public_keys.push((signature, public_key))
|
||||
}
|
||||
let proof = Proof::from_cursor(cursor)?;
|
||||
Ok(Self {
|
||||
signatures_and_public_keys,
|
||||
proof,
|
||||
})
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Self, NssaError> {
|
||||
Ok(borsh::from_slice(bytes)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl PrivacyPreservingTransaction {
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut bytes = self.message().to_bytes();
|
||||
bytes.extend_from_slice(&self.witness_set().to_bytes());
|
||||
bytes
|
||||
borsh::to_vec(&self).expect("Autoderived borsh serialization failure")
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Self, NssaError> {
|
||||
let mut cursor = Cursor::new(bytes);
|
||||
Self::from_cursor(&mut cursor)
|
||||
}
|
||||
|
||||
pub fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result<Self, NssaError> {
|
||||
let message = Message::from_cursor(cursor)?;
|
||||
let witness_set = WitnessSet::from_cursor(cursor)?;
|
||||
Ok(PrivacyPreservingTransaction::new(message, witness_set))
|
||||
Ok(borsh::from_slice(bytes)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Proof {
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut bytes = Vec::new();
|
||||
let proof_len = self.0.len() as u32;
|
||||
bytes.extend_from_slice(&proof_len.to_le_bytes());
|
||||
bytes.extend_from_slice(&self.0);
|
||||
bytes
|
||||
}
|
||||
|
||||
pub fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result<Self, NssaError> {
|
||||
let proof_len = u32_from_cursor(cursor) as usize;
|
||||
let mut proof = Vec::with_capacity(proof_len);
|
||||
|
||||
for _ in 0..proof_len {
|
||||
let mut one_byte_buf = [0u8];
|
||||
|
||||
cursor.read_exact(&mut one_byte_buf)?;
|
||||
|
||||
proof.push(one_byte_buf[0]);
|
||||
}
|
||||
Ok(Self(proof))
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Improve error handling. Remove unwraps.
|
||||
pub fn u32_from_cursor(cursor: &mut Cursor<&[u8]>) -> u32 {
|
||||
let mut word_buf = [0u8; 4];
|
||||
cursor.read_exact(&mut word_buf).unwrap();
|
||||
u32::from_le_bytes(word_buf)
|
||||
}
|
||||
|
||||
@ -1,77 +1,17 @@
|
||||
// TODO: Consider switching to deriving Borsh
|
||||
|
||||
use std::io::{Cursor, Read};
|
||||
|
||||
use crate::{
|
||||
ProgramDeploymentTransaction, error::NssaError, program_deployment_transaction::Message,
|
||||
};
|
||||
|
||||
const MESSAGE_ENCODING_PREFIX_LEN: usize = 32;
|
||||
const MESSAGE_ENCODING_PREFIX: &[u8; MESSAGE_ENCODING_PREFIX_LEN] =
|
||||
b"/NSSA/v0.2/TxMessage/Program/\x00\x00\x00";
|
||||
|
||||
impl Message {
|
||||
/// Serializes a `Message` into bytes in the following layout:
|
||||
/// PREFIX || bytecode_len (4 bytes LE) || <bytecode>
|
||||
/// Integers are encoded in little-endian byte order, and fields appear in the above order.
|
||||
pub(crate) fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut bytes = MESSAGE_ENCODING_PREFIX.to_vec();
|
||||
let bytecode_len = self.bytecode.len() as u32;
|
||||
bytes.extend(&bytecode_len.to_le_bytes());
|
||||
bytes.extend(&self.bytecode);
|
||||
bytes
|
||||
}
|
||||
|
||||
pub(crate) fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result<Self, NssaError> {
|
||||
let prefix = {
|
||||
let mut this = [0u8; MESSAGE_ENCODING_PREFIX_LEN];
|
||||
cursor.read_exact(&mut this)?;
|
||||
this
|
||||
};
|
||||
if &prefix != MESSAGE_ENCODING_PREFIX {
|
||||
return Err(NssaError::TransactionDeserializationError(
|
||||
"Invalid public message prefix".to_string(),
|
||||
));
|
||||
}
|
||||
let bytecode_len = u32_from_cursor(cursor)?;
|
||||
let mut bytecode = vec![0; bytecode_len as usize];
|
||||
let num_bytes = cursor.read(&mut bytecode)?;
|
||||
if num_bytes != bytecode_len as usize {
|
||||
println!("num bytes: {}", num_bytes);
|
||||
return Err(NssaError::TransactionDeserializationError(
|
||||
"Invalid number of bytes".to_string(),
|
||||
));
|
||||
}
|
||||
Ok(Self { bytecode })
|
||||
}
|
||||
}
|
||||
use crate::{ProgramDeploymentTransaction, error::NssaError};
|
||||
|
||||
impl ProgramDeploymentTransaction {
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
self.message.to_bytes()
|
||||
borsh::to_vec(&self).expect("Autoderived borsh serialization failure")
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Self, NssaError> {
|
||||
let mut cursor = Cursor::new(bytes);
|
||||
Self::from_cursor(&mut cursor)
|
||||
Ok(borsh::from_slice(bytes)?)
|
||||
}
|
||||
|
||||
pub fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result<Self, NssaError> {
|
||||
let message = Message::from_cursor(cursor)?;
|
||||
Ok(Self::new(message))
|
||||
}
|
||||
}
|
||||
|
||||
fn u32_from_cursor(cursor: &mut Cursor<&[u8]>) -> Result<u32, NssaError> {
|
||||
let mut word_buf = [0u8; 4];
|
||||
cursor.read_exact(&mut word_buf)?;
|
||||
Ok(u32::from_le_bytes(word_buf))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::io::Cursor;
|
||||
|
||||
use crate::{ProgramDeploymentTransaction, program_deployment_transaction::Message};
|
||||
|
||||
#[test]
|
||||
@ -79,8 +19,7 @@ mod tests {
|
||||
let message = Message::new(vec![0xca, 0xfe, 0xca, 0xfe, 0x01, 0x02, 0x03]);
|
||||
let tx = ProgramDeploymentTransaction::new(message);
|
||||
let bytes = tx.to_bytes();
|
||||
let mut cursor = Cursor::new(bytes.as_ref());
|
||||
let tx_from_cursor = ProgramDeploymentTransaction::from_cursor(&mut cursor).unwrap();
|
||||
assert_eq!(tx, tx_from_cursor);
|
||||
let tx_from_bytes = ProgramDeploymentTransaction::from_bytes(&bytes).unwrap();
|
||||
assert_eq!(tx, tx_from_bytes);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,155 +1,17 @@
|
||||
// TODO: Consider switching to deriving Borsh
|
||||
|
||||
use std::io::{Cursor, Read};
|
||||
|
||||
use nssa_core::program::ProgramId;
|
||||
|
||||
use crate::{
|
||||
AccountId, PublicKey, PublicTransaction, Signature,
|
||||
error::NssaError,
|
||||
public_transaction::{Message, WitnessSet},
|
||||
};
|
||||
|
||||
const MESSAGE_ENCODING_PREFIX_LEN: usize = 32;
|
||||
const MESSAGE_ENCODING_PREFIX: &[u8; MESSAGE_ENCODING_PREFIX_LEN] =
|
||||
b"/NSSA/v0.2/TxMessage/Public/\x00\x00\x00\x00";
|
||||
use crate::{PublicTransaction, error::NssaError, public_transaction::Message};
|
||||
|
||||
impl Message {
|
||||
/// Serializes a `Message` into bytes in the following layout:
|
||||
/// PREFIX || <program_id> (4 bytes LE) * 8 || account_ids_len (4 bytes LE) || account_ids (32
|
||||
/// bytes * N) || nonces_len (4 bytes LE) || nonces (16 bytes LE * M) || instruction_data_len ||
|
||||
/// instruction_data (4 bytes LE * K) Integers and words are encoded in little-endian byte
|
||||
/// order, and fields appear in the above order.
|
||||
pub(crate) fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut bytes = MESSAGE_ENCODING_PREFIX.to_vec();
|
||||
// program_id: [u32; 8]
|
||||
for word in &self.program_id {
|
||||
bytes.extend_from_slice(&word.to_le_bytes());
|
||||
}
|
||||
// account_ids: Vec<[u8;32]>
|
||||
// serialize length as u32 little endian, then all account_ids concatenated
|
||||
let account_ids_len = self.account_ids.len() as u32;
|
||||
bytes.extend(&account_ids_len.to_le_bytes());
|
||||
for account_id in &self.account_ids {
|
||||
bytes.extend_from_slice(account_id.value());
|
||||
}
|
||||
// nonces: Vec<u128>
|
||||
// serialize length as u32 little endian, then all nonces concatenated in LE
|
||||
let nonces_len = self.nonces.len() as u32;
|
||||
bytes.extend(&nonces_len.to_le_bytes());
|
||||
for nonce in &self.nonces {
|
||||
bytes.extend(&nonce.to_le_bytes());
|
||||
}
|
||||
// instruction_data: Vec<u32>
|
||||
// serialize length as u32 little endian, then all account_ids concatenated
|
||||
let instr_len = self.instruction_data.len() as u32;
|
||||
bytes.extend(&instr_len.to_le_bytes());
|
||||
for word in &self.instruction_data {
|
||||
bytes.extend(&word.to_le_bytes());
|
||||
}
|
||||
|
||||
bytes
|
||||
}
|
||||
|
||||
pub(crate) fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result<Self, NssaError> {
|
||||
let prefix = {
|
||||
let mut this = [0u8; MESSAGE_ENCODING_PREFIX_LEN];
|
||||
cursor.read_exact(&mut this)?;
|
||||
this
|
||||
};
|
||||
if &prefix != MESSAGE_ENCODING_PREFIX {
|
||||
return Err(NssaError::TransactionDeserializationError(
|
||||
"Invalid public message prefix".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let program_id: ProgramId = {
|
||||
let mut this = [0u32; 8];
|
||||
for item in &mut this {
|
||||
*item = u32_from_cursor(cursor)?;
|
||||
}
|
||||
this
|
||||
};
|
||||
let account_ids_len = u32_from_cursor(cursor)?;
|
||||
let mut account_ids = Vec::with_capacity(account_ids_len as usize);
|
||||
for _ in 0..account_ids_len {
|
||||
let mut value = [0u8; 32];
|
||||
cursor.read_exact(&mut value)?;
|
||||
account_ids.push(AccountId::new(value))
|
||||
}
|
||||
let nonces_len = u32_from_cursor(cursor)?;
|
||||
let mut nonces = Vec::with_capacity(nonces_len as usize);
|
||||
for _ in 0..nonces_len {
|
||||
let mut buf = [0u8; 16];
|
||||
cursor.read_exact(&mut buf)?;
|
||||
nonces.push(u128::from_le_bytes(buf))
|
||||
}
|
||||
let instruction_data_len = u32_from_cursor(cursor)?;
|
||||
let mut instruction_data = Vec::with_capacity(instruction_data_len as usize);
|
||||
for _ in 0..instruction_data_len {
|
||||
let word = u32_from_cursor(cursor)?;
|
||||
instruction_data.push(word)
|
||||
}
|
||||
Ok(Self {
|
||||
program_id,
|
||||
account_ids,
|
||||
nonces,
|
||||
instruction_data,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl WitnessSet {
|
||||
pub(crate) fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut bytes = Vec::new();
|
||||
let size = self.signatures_and_public_keys().len() as u32;
|
||||
bytes.extend_from_slice(&size.to_le_bytes());
|
||||
for (signature, public_key) in self.signatures_and_public_keys() {
|
||||
bytes.extend_from_slice(signature.to_bytes());
|
||||
bytes.extend_from_slice(public_key.to_bytes());
|
||||
}
|
||||
bytes
|
||||
}
|
||||
|
||||
pub(crate) fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result<Self, NssaError> {
|
||||
let num_signatures: u32 = {
|
||||
let mut buf = [0u8; 4];
|
||||
cursor.read_exact(&mut buf)?;
|
||||
u32::from_le_bytes(buf)
|
||||
};
|
||||
let mut signatures_and_public_keys = Vec::with_capacity(num_signatures as usize);
|
||||
for _i in 0..num_signatures {
|
||||
let signature = Signature::from_cursor(cursor)?;
|
||||
let public_key = PublicKey::from_cursor(cursor)?;
|
||||
signatures_and_public_keys.push((signature, public_key))
|
||||
}
|
||||
Ok(Self {
|
||||
signatures_and_public_keys,
|
||||
})
|
||||
borsh::to_vec(&self).expect("Autoderived borsh serialization failure")
|
||||
}
|
||||
}
|
||||
|
||||
impl PublicTransaction {
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut bytes = self.message().to_bytes();
|
||||
bytes.extend_from_slice(&self.witness_set().to_bytes());
|
||||
bytes
|
||||
borsh::to_vec(&self).expect("Autoderived borsh serialization failure")
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Self, NssaError> {
|
||||
let mut cursor = Cursor::new(bytes);
|
||||
Self::from_cursor(&mut cursor)
|
||||
}
|
||||
|
||||
pub fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result<Self, NssaError> {
|
||||
let message = Message::from_cursor(cursor)?;
|
||||
let witness_set = WitnessSet::from_cursor(cursor)?;
|
||||
Ok(PublicTransaction::new(message, witness_set))
|
||||
Ok(borsh::from_slice(bytes)?)
|
||||
}
|
||||
}
|
||||
|
||||
fn u32_from_cursor(cursor: &mut Cursor<&[u8]>) -> Result<u32, NssaError> {
|
||||
let mut word_buf = [0u8; 4];
|
||||
cursor.read_exact(&mut word_buf)?;
|
||||
Ok(u32::from_le_bytes(word_buf))
|
||||
}
|
||||
|
||||
@ -54,4 +54,7 @@ pub enum NssaError {
|
||||
|
||||
#[error("Program already exists")]
|
||||
ProgramAlreadyExists,
|
||||
|
||||
#[error("Chain of calls is too long")]
|
||||
MaxChainedCallsDepthExceeded,
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use nssa_core::{
|
||||
MembershipProof, NullifierPublicKey, NullifierSecretKey, PrivacyPreservingCircuitInput,
|
||||
PrivacyPreservingCircuitOutput, SharedSecretKey,
|
||||
@ -16,9 +17,10 @@ use crate::{
|
||||
};
|
||||
|
||||
/// Proof of the privacy preserving execution circuit
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct Proof(pub(crate) Vec<u8>);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ProgramWithDependencies {
|
||||
pub program: Program,
|
||||
// TODO: avoid having a copy of the bytecode of each dependency.
|
||||
@ -72,7 +74,10 @@ pub fn execute_and_prove(
|
||||
// Prove circuit.
|
||||
env_builder.add_assumption(inner_receipt);
|
||||
|
||||
if let Some(next_call) = program_output.chained_call {
|
||||
// TODO: Remove when multi-chain calls are supported in the circuit
|
||||
assert!(program_output.chained_calls.len() <= 1);
|
||||
// TODO: Modify when multi-chain calls are supported in the circuit
|
||||
if let Some(next_call) = program_output.chained_calls.first() {
|
||||
program = dependencies
|
||||
.get(&next_call.program_id)
|
||||
.ok_or(NssaError::InvalidProgramBehavior)?;
|
||||
@ -85,20 +90,11 @@ pub fn execute_and_prove(
|
||||
.zip(program_output.post_states)
|
||||
{
|
||||
let mut post_with_metadata = pre.clone();
|
||||
post_with_metadata.account = post.clone();
|
||||
post_with_metadata.account = post.account().clone();
|
||||
post_states_with_metadata.push(post_with_metadata);
|
||||
}
|
||||
|
||||
pre_states = next_call
|
||||
.account_indices
|
||||
.iter()
|
||||
.map(|&i| {
|
||||
post_states_with_metadata
|
||||
.get(i)
|
||||
.ok_or_else(|| NssaError::InvalidInput("Invalid account indices".into()))
|
||||
.cloned()
|
||||
})
|
||||
.collect::<Result<Vec<_>, NssaError>>()?;
|
||||
pre_states = next_call.pre_states.clone();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use nssa_core::{
|
||||
Commitment, CommitmentSetDigest, Nullifier, NullifierPublicKey, PrivacyPreservingCircuitOutput,
|
||||
account::{Account, Nonce},
|
||||
@ -9,7 +10,7 @@ use crate::{AccountId, error::NssaError};
|
||||
|
||||
pub type ViewTag = u8;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct EncryptedAccountData {
|
||||
pub ciphertext: Ciphertext,
|
||||
pub epk: EphemeralPublicKey,
|
||||
@ -42,7 +43,7 @@ impl EncryptedAccountData {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct Message {
|
||||
pub(crate) public_account_ids: Vec<AccountId>,
|
||||
pub(crate) nonces: Vec<Nonce>,
|
||||
@ -90,8 +91,6 @@ impl Message {
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use std::io::Cursor;
|
||||
|
||||
use nssa_core::{
|
||||
Commitment, EncryptionScheme, Nullifier, NullifierPublicKey, SharedSecretKey,
|
||||
account::Account,
|
||||
@ -140,17 +139,6 @@ pub mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_message_serialization_roundtrip() {
|
||||
let message = message_for_tests();
|
||||
|
||||
let bytes = message.to_bytes();
|
||||
let mut cursor = Cursor::new(bytes.as_ref());
|
||||
let message_from_cursor = Message::from_cursor(&mut cursor).unwrap();
|
||||
|
||||
assert_eq!(message, message_from_cursor);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypted_account_data_constructor() {
|
||||
let npk = NullifierPublicKey::from(&[1; 32]);
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use nssa_core::{
|
||||
Commitment, CommitmentSetDigest, Nullifier, PrivacyPreservingCircuitOutput,
|
||||
account::{Account, AccountWithMetadata},
|
||||
@ -12,7 +13,7 @@ use crate::{
|
||||
privacy_preserving_transaction::{circuit::Proof, message::EncryptedAccountData},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct PrivacyPreservingTransaction {
|
||||
pub message: Message,
|
||||
witness_set: WitnessSet,
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
|
||||
use crate::{
|
||||
PrivateKey, PublicKey, Signature,
|
||||
privacy_preserving_transaction::{circuit::Proof, message::Message},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct WitnessSet {
|
||||
pub(crate) signatures_and_public_keys: Vec<(Signature, PublicKey)>,
|
||||
pub(crate) proof: Proof,
|
||||
|
||||
@ -14,7 +14,7 @@ use crate::{
|
||||
/// TODO: Make this variable when fees are implemented
|
||||
const MAX_NUM_CYCLES_PUBLIC_EXECUTION: u64 = 1024 * 1024 * 32; // 32M cycles
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Program {
|
||||
id: ProgramId,
|
||||
elf: Vec<u8>,
|
||||
@ -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]
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct Message {
|
||||
pub(crate) bytecode: Vec<u8>,
|
||||
}
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
|
||||
use crate::{
|
||||
V02State, error::NssaError, program::Program, program_deployment_transaction::message::Message,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct ProgramDeploymentTransaction {
|
||||
pub(crate) message: Message,
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use nssa_core::{
|
||||
account::Nonce,
|
||||
program::{InstructionData, ProgramId},
|
||||
@ -6,7 +7,7 @@ use serde::Serialize;
|
||||
|
||||
use crate::{AccountId, error::NssaError, program::Program};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct Message {
|
||||
pub(crate) program_id: ProgramId,
|
||||
pub(crate) account_ids: Vec<AccountId>,
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use nssa_core::{
|
||||
account::{Account, AccountId, AccountWithMetadata},
|
||||
program::{DEFAULT_PROGRAM_ID, MAX_NUMBER_CHAINED_CALLS, validate_execution},
|
||||
program::{ChainedCall, DEFAULT_PROGRAM_ID, PdaSeed, ProgramId, validate_execution},
|
||||
};
|
||||
use sha2::{Digest, digest::FixedOutput};
|
||||
|
||||
@ -10,9 +11,10 @@ use crate::{
|
||||
V02State,
|
||||
error::NssaError,
|
||||
public_transaction::{Message, WitnessSet},
|
||||
state::MAX_NUMBER_CHAINED_CALLS,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct PublicTransaction {
|
||||
message: Message,
|
||||
witness_set: WitnessSet,
|
||||
@ -87,7 +89,7 @@ impl PublicTransaction {
|
||||
}
|
||||
|
||||
// Build pre_states for execution
|
||||
let mut input_pre_states: Vec<_> = message
|
||||
let input_pre_states: Vec<_> = message
|
||||
.account_ids
|
||||
.iter()
|
||||
.map(|account_id| {
|
||||
@ -101,22 +103,51 @@ impl PublicTransaction {
|
||||
|
||||
let mut state_diff: HashMap<AccountId, Account> = HashMap::new();
|
||||
|
||||
let mut program_id = message.program_id;
|
||||
let mut instruction_data = message.instruction_data.clone();
|
||||
let initial_call = ChainedCall {
|
||||
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, None)]);
|
||||
let mut chain_calls_counter = 0;
|
||||
|
||||
while let Some((chained_call, caller_program_id)) = chained_calls.pop_front() {
|
||||
if chain_calls_counter > MAX_NUMBER_CHAINED_CALLS {
|
||||
return Err(NssaError::MaxChainedCallsDepthExceeded);
|
||||
}
|
||||
|
||||
for _i in 0..MAX_NUMBER_CHAINED_CALLS {
|
||||
// Check the `program_id` corresponds to a deployed program
|
||||
let Some(program) = state.programs().get(&program_id) else {
|
||||
let Some(program) = state.programs().get(&chained_call.program_id) else {
|
||||
return Err(NssaError::InvalidInput("Unknown program".into()));
|
||||
};
|
||||
|
||||
let mut program_output = program.execute(&input_pre_states, &instruction_data)?;
|
||||
let mut program_output =
|
||||
program.execute(&chained_call.pre_states, &chained_call.instruction_data)?;
|
||||
|
||||
// This check is equivalent to checking that the program output pre_states coinicide
|
||||
// with the values in the public state or with any modifications to those values
|
||||
// during the chain of calls.
|
||||
if input_pre_states != program_output.pre_states {
|
||||
return Err(NssaError::InvalidProgramBehavior);
|
||||
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
|
||||
// state or with any modifications to those values during the chain of calls.
|
||||
let expected_pre = state_diff
|
||||
.get(&account_id)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| state.get_account_by_id(&account_id));
|
||||
if pre.account != expected_pre {
|
||||
return Err(NssaError::InvalidProgramBehavior);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify execution corresponds to a well-behaved program.
|
||||
@ -124,15 +155,21 @@ impl PublicTransaction {
|
||||
if !validate_execution(
|
||||
&program_output.pre_states,
|
||||
&program_output.post_states,
|
||||
program_id,
|
||||
chained_call.program_id,
|
||||
) {
|
||||
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 = 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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -142,44 +179,33 @@ 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());
|
||||
}
|
||||
|
||||
if let Some(next_chained_call) = program_output.chained_call {
|
||||
program_id = next_chained_call.program_id;
|
||||
instruction_data = next_chained_call.instruction_data;
|
||||
for new_call in program_output.chained_calls.into_iter().rev() {
|
||||
chained_calls.push_front((new_call, Some(chained_call.program_id)));
|
||||
}
|
||||
|
||||
// Build post states with metadata for next call
|
||||
let mut post_states_with_metadata = Vec::new();
|
||||
for (pre, post) in program_output
|
||||
.pre_states
|
||||
.iter()
|
||||
.zip(program_output.post_states)
|
||||
{
|
||||
let mut post_with_metadata = pre.clone();
|
||||
post_with_metadata.account = post.clone();
|
||||
post_states_with_metadata.push(post_with_metadata);
|
||||
}
|
||||
|
||||
input_pre_states = next_chained_call
|
||||
.account_indices
|
||||
.iter()
|
||||
.map(|&i| {
|
||||
post_states_with_metadata
|
||||
.get(i)
|
||||
.ok_or_else(|| {
|
||||
NssaError::InvalidInput("Invalid account indices".into())
|
||||
})
|
||||
.cloned()
|
||||
})
|
||||
.collect::<Result<Vec<_>, NssaError>>()?;
|
||||
} else {
|
||||
break;
|
||||
};
|
||||
chain_calls_counter += 1;
|
||||
}
|
||||
|
||||
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)]
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
|
||||
use crate::{PrivateKey, PublicKey, Signature, public_transaction::Message};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct WitnessSet {
|
||||
pub(crate) signatures_and_public_keys: Vec<(Signature, PublicKey)>,
|
||||
}
|
||||
|
||||
@ -1,27 +0,0 @@
|
||||
use std::io::{Cursor, Read};
|
||||
|
||||
use crate::{PublicKey, Signature, error::NssaError};
|
||||
|
||||
impl PublicKey {
|
||||
pub(crate) fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result<Self, NssaError> {
|
||||
let mut value = [0u8; 32];
|
||||
cursor.read_exact(&mut value)?;
|
||||
Self::try_new(value)
|
||||
}
|
||||
|
||||
pub(crate) fn to_bytes(&self) -> &[u8] {
|
||||
self.value()
|
||||
}
|
||||
}
|
||||
|
||||
impl Signature {
|
||||
pub(crate) fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result<Self, NssaError> {
|
||||
let mut value = [0u8; 64];
|
||||
cursor.read_exact(&mut value)?;
|
||||
Ok(Self { value })
|
||||
}
|
||||
|
||||
pub(crate) fn to_bytes(&self) -> &[u8] {
|
||||
&self.value
|
||||
}
|
||||
}
|
||||
@ -1,12 +1,12 @@
|
||||
mod encoding;
|
||||
mod private_key;
|
||||
mod public_key;
|
||||
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
pub use private_key::PrivateKey;
|
||||
pub use public_key::PublicKey;
|
||||
use rand::{RngCore, rngs::OsRng};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct Signature {
|
||||
value: [u8; 64],
|
||||
}
|
||||
|
||||
@ -1,11 +1,27 @@
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use nssa_core::account::AccountId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::{PrivateKey, error::NssaError};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, Serialize, Deserialize)]
|
||||
pub struct PublicKey([u8; 32]);
|
||||
|
||||
impl BorshDeserialize for PublicKey {
|
||||
fn deserialize_reader<R: std::io::Read>(reader: &mut R) -> std::io::Result<Self> {
|
||||
let mut buf = [0u8; 32];
|
||||
reader.read_exact(&mut buf)?;
|
||||
|
||||
Self::try_new(buf).map_err(|_| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"Invalid public key: not a valid point",
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl PublicKey {
|
||||
pub fn new_from_private_key(key: &PrivateKey) -> Self {
|
||||
let value = {
|
||||
@ -18,7 +34,7 @@ impl PublicKey {
|
||||
Self(value)
|
||||
}
|
||||
|
||||
pub(super) fn try_new(value: [u8; 32]) -> Result<Self, NssaError> {
|
||||
pub fn try_new(value: [u8; 32]) -> Result<Self, NssaError> {
|
||||
// Check point is valid
|
||||
let _ = secp256k1::XOnlyPublicKey::from_byte_array(value)
|
||||
.map_err(|_| NssaError::InvalidPublicKey)?;
|
||||
@ -92,4 +108,14 @@ mod test {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_correct_ser_deser_roundtrip() {
|
||||
let pub_key = PublicKey::try_new([42; 32]).unwrap();
|
||||
|
||||
let pub_key_borsh_ser = borsh::to_vec(&pub_key).unwrap();
|
||||
let pub_key_new: PublicKey = borsh::from_slice(&pub_key_borsh_ser).unwrap();
|
||||
|
||||
assert_eq!(pub_key, pub_key_new);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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::{
|
||||
@ -266,6 +280,7 @@ pub mod tests {
|
||||
program::Program,
|
||||
public_transaction,
|
||||
signature::PrivateKey,
|
||||
state::MAX_NUMBER_CHAINED_CALLS,
|
||||
};
|
||||
|
||||
fn transfer_transaction(
|
||||
@ -479,6 +494,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,6 +2108,135 @@ pub mod tests {
|
||||
fn test_public_chained_call() {
|
||||
let program = Program::chain_caller();
|
||||
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 = 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 = 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(),
|
||||
balance: amount * 2, // The `chain_caller` chains the program twice
|
||||
..Account::default()
|
||||
};
|
||||
|
||||
let message = public_transaction::Message::try_new(
|
||||
program.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);
|
||||
// The `chain_caller` program calls the program twice
|
||||
assert_eq!(from_post.balance, initial_balance - 2 * amount);
|
||||
assert_eq!(to_post, expected_to_post);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_execution_fails_if_chained_calls_exceeds_depth() {
|
||||
let program = Program::chain_caller();
|
||||
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_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, Option<PdaSeed>) = (
|
||||
amount,
|
||||
Program::authenticated_transfer_program().id(),
|
||||
MAX_NUMBER_CHAINED_CALLS as u32 + 1,
|
||||
None,
|
||||
);
|
||||
|
||||
let message = public_transaction::Message::try_new(
|
||||
program.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);
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx);
|
||||
assert!(matches!(
|
||||
result,
|
||||
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)];
|
||||
@ -2101,18 +2246,29 @@ pub mod tests {
|
||||
let from_key = key;
|
||||
let to = AccountId::new([2; 32]);
|
||||
let amount: u128 = 37;
|
||||
let instruction: (u128, ProgramId) =
|
||||
(amount, Program::authenticated_transfer_program().id());
|
||||
|
||||
// Check the recipient is an uninitialized account
|
||||
assert_eq!(state.get_account_by_id(&to), Account::default());
|
||||
|
||||
let expected_to_post = Account {
|
||||
program_owner: Program::chain_caller().id(),
|
||||
// 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(
|
||||
program.id(),
|
||||
vec![to, from], // The chain_caller program permutes the account order in the call
|
||||
chain_caller.id(),
|
||||
vec![to, from], // The chain_caller program permutes the account order in the chain
|
||||
// call
|
||||
vec![0],
|
||||
instruction,
|
||||
)
|
||||
@ -2162,8 +2318,12 @@ pub mod tests {
|
||||
)
|
||||
.with_test_programs();
|
||||
let amount: u128 = 37;
|
||||
let instruction: (u128, ProgramId) =
|
||||
(amount, Program::authenticated_transfer_program().id());
|
||||
let instruction: (u128, ProgramId, u32, Option<PdaSeed>) = (
|
||||
amount,
|
||||
Program::authenticated_transfer_program().id(),
|
||||
1,
|
||||
None,
|
||||
);
|
||||
|
||||
let from_esk = [3; 32];
|
||||
let from_ss = SharedSecretKey::new(&from_esk, &from_keys.ivk());
|
||||
@ -2179,7 +2339,7 @@ pub mod tests {
|
||||
let program_with_deps = ProgramWithDependencies::new(chain_caller, dependencies);
|
||||
|
||||
let from_new_nonce = 0xdeadbeef1;
|
||||
let to_new_nonce = 0xdeadbeef1;
|
||||
let to_new_nonce = 0xdeadbeef2;
|
||||
|
||||
let from_expected_post = Account {
|
||||
balance: initial_balance - amount,
|
||||
@ -2200,17 +2360,17 @@ pub mod tests {
|
||||
&[to_account, from_account],
|
||||
&Program::serialize_instruction(instruction).unwrap(),
|
||||
&[1, 1],
|
||||
&[to_new_nonce, from_new_nonce],
|
||||
&[(to_keys.npk(), from_ss), (from_keys.npk(), to_ss)],
|
||||
&[from_new_nonce, to_new_nonce],
|
||||
&[(from_keys.npk(), to_ss), (to_keys.npk(), from_ss)],
|
||||
&[
|
||||
(
|
||||
to_keys.nsk,
|
||||
state.get_proof_for_commitment(&to_commitment).unwrap(),
|
||||
),
|
||||
(
|
||||
from_keys.nsk,
|
||||
state.get_proof_for_commitment(&from_commitment).unwrap(),
|
||||
),
|
||||
(
|
||||
to_keys.nsk,
|
||||
state.get_proof_for_commitment(&to_commitment).unwrap(),
|
||||
),
|
||||
],
|
||||
&program_with_deps,
|
||||
)
|
||||
@ -2245,4 +2405,110 @@ pub mod tests {
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
|
||||
#[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
nssa/test_program_methods/guest/Cargo.lock
generated
1
nssa/test_program_methods/guest/Cargo.lock
generated
@ -1579,6 +1579,7 @@ checksum = "a5b0c77c1b780822bc749a33e39aeb2c07584ab93332303babeabb645298a76e"
|
||||
name = "nssa-core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"borsh",
|
||||
"chacha20",
|
||||
"risc0-zkvm",
|
||||
"serde",
|
||||
|
||||
@ -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};
|
||||
|
||||
type Instruction = u128;
|
||||
|
||||
@ -20,5 +20,5 @@ fn main() {
|
||||
let mut account_post = account_pre.clone();
|
||||
account_post.balance -= balance_to_burn;
|
||||
|
||||
write_nssa_outputs(instruction_words, vec![pre], vec![account_post]);
|
||||
write_nssa_outputs(instruction_words, vec![pre], vec![AccountPostState::new(account_post)]);
|
||||
}
|
||||
|
||||
@ -1,38 +1,58 @@
|
||||
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);
|
||||
type Instruction = (u128, ProgramId, u32, Option<PdaSeed>);
|
||||
|
||||
/// A program that calls another program.
|
||||
/// 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),
|
||||
instruction: (balance, auth_transfer_id, num_chain_calls, pda_seed),
|
||||
},
|
||||
instruction_words,
|
||||
instruction_words
|
||||
) = 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 chained_call = Some(ChainedCall {
|
||||
program_id,
|
||||
instruction_data,
|
||||
account_indices: vec![1, 0], // <- Account order permutation here
|
||||
});
|
||||
let mut running_recipient_pre = recipient_pre.clone();
|
||||
let mut running_sender_pre = sender_pre.clone();
|
||||
|
||||
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(
|
||||
instruction_words,
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
22
nssa/test_program_methods/guest/src/bin/claimer.rs
Normal file
22
nssa/test_program_methods/guest/src/bin/claimer.rs
Normal file
@ -0,0 +1,22 @@
|
||||
use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs};
|
||||
|
||||
type Instruction = ();
|
||||
|
||||
fn main() {
|
||||
let (
|
||||
ProgramInput {
|
||||
pre_states,
|
||||
instruction: _,
|
||||
},
|
||||
instruction_words,
|
||||
) = 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(instruction_words, vec![pre], vec![account_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};
|
||||
|
||||
type Instruction = ();
|
||||
|
||||
@ -14,5 +14,9 @@ fn main() {
|
||||
let mut account_post = account_pre.clone();
|
||||
account_post.data.push(0);
|
||||
|
||||
write_nssa_outputs(instruction_words, vec![pre], vec![account_post]);
|
||||
write_nssa_outputs(
|
||||
instruction_words,
|
||||
vec![pre],
|
||||
vec![AccountPostState::new_claimed(account_post)],
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
use nssa_core::{
|
||||
account::Account,
|
||||
program::{ProgramInput, read_nssa_inputs, write_nssa_outputs},
|
||||
program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs},
|
||||
};
|
||||
|
||||
type Instruction = ();
|
||||
@ -18,6 +18,9 @@ fn main() {
|
||||
write_nssa_outputs(
|
||||
instruction_words,
|
||||
vec![pre],
|
||||
vec![account_pre, Account::default()],
|
||||
vec![
|
||||
AccountPostState::new(account_pre),
|
||||
AccountPostState::new(Account::default()),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use nssa_core::program::{ProgramInput, read_nssa_inputs, write_nssa_outputs};
|
||||
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(instruction_words, vec![pre], vec![account_post]);
|
||||
write_nssa_outputs(instruction_words, vec![pre], vec![AccountPostState::new(account_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};
|
||||
|
||||
type Instruction = ();
|
||||
|
||||
@ -12,5 +12,9 @@ fn main() {
|
||||
|
||||
let account_pre1 = pre1.account.clone();
|
||||
|
||||
write_nssa_outputs(instruction_words, vec![pre1, pre2], vec![account_pre1]);
|
||||
write_nssa_outputs(
|
||||
instruction_words,
|
||||
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(instruction_words, vec![pre], vec![account_post]);
|
||||
write_nssa_outputs(instruction_words ,vec![pre], vec![AccountPostState::new(account_post)]);
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use nssa_core::program::{ProgramInput, read_nssa_inputs, write_nssa_outputs};
|
||||
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(instruction_words, vec![pre], vec![account_post]);
|
||||
write_nssa_outputs(instruction_words, vec![pre], vec![AccountPostState::new(account_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};
|
||||
|
||||
type Instruction = u128;
|
||||
|
||||
@ -24,6 +24,9 @@ fn main() {
|
||||
write_nssa_outputs(
|
||||
instruction_words,
|
||||
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,6 +19,10 @@ borsh.workspace = true
|
||||
base58.workspace = true
|
||||
hex = "0.4.3"
|
||||
rand.workspace = true
|
||||
itertools.workspace = true
|
||||
sha2.workspace = true
|
||||
futures.workspace = true
|
||||
async-stream = "0.3.6"
|
||||
|
||||
[dependencies.key_protocol]
|
||||
path = "../key_protocol"
|
||||
|
||||
294
wallet/src/chain_storage.rs
Normal file
294
wallet/src/chain_storage.rs
Normal file
@ -0,0 +1,294 @@
|
||||
use std::collections::{HashMap, hash_map::Entry};
|
||||
|
||||
use anyhow::Result;
|
||||
use key_protocol::{
|
||||
key_management::{
|
||||
key_tree::{KeyTreePrivate, KeyTreePublic, chain_index::ChainIndex},
|
||||
secret_holders::SeedHolder,
|
||||
},
|
||||
key_protocol_core::NSSAUserData,
|
||||
};
|
||||
use nssa::program::Program;
|
||||
|
||||
use crate::config::{InitialAccountData, PersistentAccountData, WalletConfig};
|
||||
|
||||
pub struct WalletChainStore {
|
||||
pub user_data: NSSAUserData,
|
||||
pub wallet_config: WalletConfig,
|
||||
}
|
||||
|
||||
impl WalletChainStore {
|
||||
pub fn new(
|
||||
config: WalletConfig,
|
||||
persistent_accounts: Vec<PersistentAccountData>,
|
||||
) -> Result<Self> {
|
||||
if persistent_accounts.is_empty() {
|
||||
anyhow::bail!("Roots not found; please run setup beforehand");
|
||||
}
|
||||
|
||||
let mut public_init_acc_map = HashMap::new();
|
||||
let mut private_init_acc_map = HashMap::new();
|
||||
|
||||
let public_root = persistent_accounts
|
||||
.iter()
|
||||
.find(|data| match data {
|
||||
&PersistentAccountData::Public(data) => data.chain_index == ChainIndex::root(),
|
||||
_ => false,
|
||||
})
|
||||
.cloned()
|
||||
.expect("Malformed persistent account data, must have public root");
|
||||
|
||||
let private_root = persistent_accounts
|
||||
.iter()
|
||||
.find(|data| match data {
|
||||
&PersistentAccountData::Private(data) => data.chain_index == ChainIndex::root(),
|
||||
_ => false,
|
||||
})
|
||||
.cloned()
|
||||
.expect("Malformed persistent account data, must have private root");
|
||||
|
||||
let mut public_tree = KeyTreePublic::new_from_root(match public_root {
|
||||
PersistentAccountData::Public(data) => data.data,
|
||||
_ => unreachable!(),
|
||||
});
|
||||
let mut private_tree = KeyTreePrivate::new_from_root(match private_root {
|
||||
PersistentAccountData::Private(data) => data.data,
|
||||
_ => unreachable!(),
|
||||
});
|
||||
|
||||
for pers_acc_data in persistent_accounts {
|
||||
match pers_acc_data {
|
||||
PersistentAccountData::Public(data) => {
|
||||
public_tree.insert(data.account_id, data.chain_index, data.data);
|
||||
}
|
||||
PersistentAccountData::Private(data) => {
|
||||
private_tree.insert(data.account_id, data.chain_index, data.data);
|
||||
}
|
||||
PersistentAccountData::Preconfigured(acc_data) => match acc_data {
|
||||
InitialAccountData::Public(data) => {
|
||||
public_init_acc_map.insert(data.account_id.parse()?, data.pub_sign_key);
|
||||
}
|
||||
InitialAccountData::Private(data) => {
|
||||
private_init_acc_map
|
||||
.insert(data.account_id.parse()?, (data.key_chain, data.account));
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
user_data: NSSAUserData::new_with_accounts(
|
||||
public_init_acc_map,
|
||||
private_init_acc_map,
|
||||
public_tree,
|
||||
private_tree,
|
||||
)?,
|
||||
wallet_config: config,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_storage(config: WalletConfig, password: String) -> Result<Self> {
|
||||
let mut public_init_acc_map = HashMap::new();
|
||||
let mut private_init_acc_map = HashMap::new();
|
||||
|
||||
for init_acc_data in config.initial_accounts.clone() {
|
||||
match init_acc_data {
|
||||
InitialAccountData::Public(data) => {
|
||||
public_init_acc_map.insert(data.account_id.parse()?, data.pub_sign_key);
|
||||
}
|
||||
InitialAccountData::Private(data) => {
|
||||
let mut account = data.account;
|
||||
// TODO: Program owner is only known after code is compiled and can't be set in
|
||||
// the config. Therefore we overwrite it here on startup. Fix this when program
|
||||
// id can be fetched from the node and queried from the wallet.
|
||||
account.program_owner = Program::authenticated_transfer_program().id();
|
||||
private_init_acc_map
|
||||
.insert(data.account_id.parse()?, (data.key_chain, account));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let public_tree = KeyTreePublic::new(&SeedHolder::new_mnemonic(password.clone()));
|
||||
let private_tree = KeyTreePrivate::new(&SeedHolder::new_mnemonic(password));
|
||||
|
||||
Ok(Self {
|
||||
user_data: NSSAUserData::new_with_accounts(
|
||||
public_init_acc_map,
|
||||
private_init_acc_map,
|
||||
public_tree,
|
||||
private_tree,
|
||||
)?,
|
||||
wallet_config: config,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn insert_private_account_data(
|
||||
&mut self,
|
||||
account_id: nssa::AccountId,
|
||||
account: nssa_core::account::Account,
|
||||
) {
|
||||
println!("inserting at address {account_id}, this account {account:?}");
|
||||
|
||||
let entry = self
|
||||
.user_data
|
||||
.default_user_private_accounts
|
||||
.entry(account_id)
|
||||
.and_modify(|data| data.1 = account.clone());
|
||||
|
||||
if matches!(entry, Entry::Vacant(_)) {
|
||||
self.user_data
|
||||
.private_key_tree
|
||||
.account_id_map
|
||||
.get(&account_id)
|
||||
.map(|chain_index| {
|
||||
self.user_data
|
||||
.private_key_tree
|
||||
.key_map
|
||||
.entry(chain_index.clone())
|
||||
.and_modify(|data| data.value.1 = account)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use key_protocol::key_management::key_tree::{
|
||||
keys_private::ChildKeysPrivate, keys_public::ChildKeysPublic, traits::KeyNode,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use crate::config::{
|
||||
InitialAccountData, PersistentAccountDataPrivate, PersistentAccountDataPublic,
|
||||
};
|
||||
|
||||
fn create_initial_accounts() -> Vec<InitialAccountData> {
|
||||
let initial_acc1 = serde_json::from_str(
|
||||
r#"{
|
||||
"Public": {
|
||||
"account_id": "BLgCRDXYdQPMMWVHYRFGQZbgeHx9frkipa8GtpG2Syqy",
|
||||
"pub_sign_key": [
|
||||
16,
|
||||
162,
|
||||
106,
|
||||
154,
|
||||
236,
|
||||
125,
|
||||
52,
|
||||
184,
|
||||
35,
|
||||
100,
|
||||
238,
|
||||
174,
|
||||
69,
|
||||
197,
|
||||
41,
|
||||
77,
|
||||
187,
|
||||
10,
|
||||
118,
|
||||
75,
|
||||
0,
|
||||
11,
|
||||
148,
|
||||
238,
|
||||
185,
|
||||
181,
|
||||
133,
|
||||
17,
|
||||
220,
|
||||
72,
|
||||
124,
|
||||
77
|
||||
]
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let initial_acc2 = serde_json::from_str(
|
||||
r#"{
|
||||
"Public": {
|
||||
"account_id": "Gj1mJy5W7J5pfmLRujmQaLfLMWidNxQ6uwnhb666ZwHw",
|
||||
"pub_sign_key": [
|
||||
113,
|
||||
121,
|
||||
64,
|
||||
177,
|
||||
204,
|
||||
85,
|
||||
229,
|
||||
214,
|
||||
178,
|
||||
6,
|
||||
109,
|
||||
191,
|
||||
29,
|
||||
154,
|
||||
63,
|
||||
38,
|
||||
242,
|
||||
18,
|
||||
244,
|
||||
219,
|
||||
8,
|
||||
208,
|
||||
35,
|
||||
136,
|
||||
23,
|
||||
127,
|
||||
207,
|
||||
237,
|
||||
216,
|
||||
169,
|
||||
190,
|
||||
27
|
||||
]
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let initial_accounts = vec![initial_acc1, initial_acc2];
|
||||
|
||||
initial_accounts
|
||||
}
|
||||
|
||||
fn create_sample_wallet_config() -> WalletConfig {
|
||||
WalletConfig {
|
||||
override_rust_log: None,
|
||||
sequencer_addr: "http://127.0.0.1".to_string(),
|
||||
seq_poll_timeout_millis: 12000,
|
||||
seq_tx_poll_max_blocks: 5,
|
||||
seq_poll_max_retries: 10,
|
||||
seq_block_poll_max_amount: 100,
|
||||
initial_accounts: create_initial_accounts(),
|
||||
}
|
||||
}
|
||||
|
||||
fn create_sample_persistent_accounts() -> Vec<PersistentAccountData> {
|
||||
let public_data = ChildKeysPublic::root([42; 64]);
|
||||
let private_data = ChildKeysPrivate::root([47; 64]);
|
||||
|
||||
vec![
|
||||
PersistentAccountData::Public(PersistentAccountDataPublic {
|
||||
account_id: public_data.account_id(),
|
||||
chain_index: ChainIndex::root(),
|
||||
data: public_data,
|
||||
}),
|
||||
PersistentAccountData::Private(PersistentAccountDataPrivate {
|
||||
account_id: private_data.account_id(),
|
||||
chain_index: ChainIndex::root(),
|
||||
data: private_data,
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_initializes_correctly() {
|
||||
let config = create_sample_wallet_config();
|
||||
let accs = create_sample_persistent_accounts();
|
||||
|
||||
let _ = WalletChainStore::new(config.clone(), accs).unwrap();
|
||||
}
|
||||
}
|
||||
@ -1,188 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Result;
|
||||
use key_protocol::key_protocol_core::NSSAUserData;
|
||||
use nssa::program::Program;
|
||||
|
||||
use crate::config::{InitialAccountData, PersistentAccountData, WalletConfig};
|
||||
|
||||
pub struct WalletChainStore {
|
||||
pub user_data: NSSAUserData,
|
||||
pub wallet_config: WalletConfig,
|
||||
}
|
||||
|
||||
impl WalletChainStore {
|
||||
pub fn new(config: WalletConfig) -> Result<Self> {
|
||||
let mut public_init_acc_map = HashMap::new();
|
||||
let mut private_init_acc_map = HashMap::new();
|
||||
|
||||
for init_acc_data in config.initial_accounts.clone() {
|
||||
match init_acc_data {
|
||||
InitialAccountData::Public(data) => {
|
||||
public_init_acc_map.insert(data.account_id.parse()?, data.pub_sign_key);
|
||||
}
|
||||
InitialAccountData::Private(data) => {
|
||||
let mut account = data.account;
|
||||
// TODO: Program owner is only known after code is compiled and can't be set in
|
||||
// the config. Therefore we overwrite it here on startup. Fix this when program
|
||||
// id can be fetched from the node and queried from the wallet.
|
||||
account.program_owner = Program::authenticated_transfer_program().id();
|
||||
private_init_acc_map
|
||||
.insert(data.account_id.parse()?, (data.key_chain, account));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
user_data: NSSAUserData::new_with_accounts(public_init_acc_map, private_init_acc_map)?,
|
||||
wallet_config: config,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn insert_private_account_data(
|
||||
&mut self,
|
||||
account_id: nssa::AccountId,
|
||||
account: nssa_core::account::Account,
|
||||
) {
|
||||
println!(
|
||||
"inserting at addres {}, this account {:?}",
|
||||
account_id, account
|
||||
);
|
||||
self.user_data
|
||||
.user_private_accounts
|
||||
.entry(account_id)
|
||||
.and_modify(|(_, acc)| *acc = account);
|
||||
}
|
||||
|
||||
pub(crate) fn insert_account_data(&mut self, acc_data: PersistentAccountData) {
|
||||
match acc_data {
|
||||
PersistentAccountData::Public(acc_data) => {
|
||||
self.user_data
|
||||
.pub_account_signing_keys
|
||||
.insert(acc_data.account_id, acc_data.pub_sign_key);
|
||||
}
|
||||
PersistentAccountData::Private(acc_data) => {
|
||||
self.user_data
|
||||
.user_private_accounts
|
||||
.insert(acc_data.account_id, (acc_data.key_chain, acc_data.account));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::InitialAccountData;
|
||||
|
||||
fn create_initial_accounts() -> Vec<InitialAccountData> {
|
||||
let initial_acc1 = serde_json::from_str(
|
||||
r#"{
|
||||
"Public": {
|
||||
"account_id": "BLgCRDXYdQPMMWVHYRFGQZbgeHx9frkipa8GtpG2Syqy",
|
||||
"pub_sign_key": [
|
||||
16,
|
||||
162,
|
||||
106,
|
||||
154,
|
||||
236,
|
||||
125,
|
||||
52,
|
||||
184,
|
||||
35,
|
||||
100,
|
||||
238,
|
||||
174,
|
||||
69,
|
||||
197,
|
||||
41,
|
||||
77,
|
||||
187,
|
||||
10,
|
||||
118,
|
||||
75,
|
||||
0,
|
||||
11,
|
||||
148,
|
||||
238,
|
||||
185,
|
||||
181,
|
||||
133,
|
||||
17,
|
||||
220,
|
||||
72,
|
||||
124,
|
||||
77
|
||||
]
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let initial_acc2 = serde_json::from_str(
|
||||
r#"{
|
||||
"Public": {
|
||||
"account_id": "Gj1mJy5W7J5pfmLRujmQaLfLMWidNxQ6uwnhb666ZwHw",
|
||||
"pub_sign_key": [
|
||||
113,
|
||||
121,
|
||||
64,
|
||||
177,
|
||||
204,
|
||||
85,
|
||||
229,
|
||||
214,
|
||||
178,
|
||||
6,
|
||||
109,
|
||||
191,
|
||||
29,
|
||||
154,
|
||||
63,
|
||||
38,
|
||||
242,
|
||||
18,
|
||||
244,
|
||||
219,
|
||||
8,
|
||||
208,
|
||||
35,
|
||||
136,
|
||||
23,
|
||||
127,
|
||||
207,
|
||||
237,
|
||||
216,
|
||||
169,
|
||||
190,
|
||||
27
|
||||
]
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let initial_accounts = vec![initial_acc1, initial_acc2];
|
||||
|
||||
initial_accounts
|
||||
}
|
||||
|
||||
fn create_sample_wallet_config() -> WalletConfig {
|
||||
WalletConfig {
|
||||
override_rust_log: None,
|
||||
sequencer_addr: "http://127.0.0.1".to_string(),
|
||||
seq_poll_timeout_millis: 12000,
|
||||
seq_poll_max_blocks: 5,
|
||||
seq_poll_max_retries: 10,
|
||||
seq_poll_retry_delay_millis: 500,
|
||||
initial_accounts: create_initial_accounts(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_initializes_correctly() {
|
||||
let config = create_sample_wallet_config();
|
||||
|
||||
let _ = WalletChainStore::new(config.clone()).unwrap();
|
||||
}
|
||||
}
|
||||
@ -1,14 +1,15 @@
|
||||
use anyhow::Result;
|
||||
use base58::ToBase58;
|
||||
use clap::Subcommand;
|
||||
use itertools::Itertools as _;
|
||||
use key_protocol::key_management::key_tree::chain_index::ChainIndex;
|
||||
use nssa::{Account, AccountId, program::Program};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{
|
||||
SubcommandReturnValue, WalletCore,
|
||||
cli::WalletSubcommand,
|
||||
WalletCore,
|
||||
cli::{SubcommandReturnValue, WalletSubcommand},
|
||||
helperfunctions::{AccountPrivacyKind, HumanReadableAccount, parse_addr_with_privacy_prefix},
|
||||
parse_block_range,
|
||||
};
|
||||
|
||||
const TOKEN_DEFINITION_TYPE: u8 = 0;
|
||||
@ -83,15 +84,26 @@ pub enum AccountSubcommand {
|
||||
New(NewSubcommand),
|
||||
/// Sync private accounts
|
||||
SyncPrivate {},
|
||||
/// List all accounts owned by the wallet
|
||||
#[command(visible_alias = "ls")]
|
||||
List {},
|
||||
}
|
||||
|
||||
/// Represents generic register CLI subcommand
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
pub enum NewSubcommand {
|
||||
/// Register new public account
|
||||
Public {},
|
||||
Public {
|
||||
#[arg(long)]
|
||||
/// Chain index of a parent node
|
||||
cci: ChainIndex,
|
||||
},
|
||||
/// Register new private account
|
||||
Private {},
|
||||
Private {
|
||||
#[arg(long)]
|
||||
/// Chain index of a parent node
|
||||
cci: ChainIndex,
|
||||
},
|
||||
}
|
||||
|
||||
impl WalletSubcommand for NewSubcommand {
|
||||
@ -100,8 +112,8 @@ impl WalletSubcommand for NewSubcommand {
|
||||
wallet_core: &mut WalletCore,
|
||||
) -> Result<SubcommandReturnValue> {
|
||||
match self {
|
||||
NewSubcommand::Public {} => {
|
||||
let account_id = wallet_core.create_new_account_public();
|
||||
NewSubcommand::Public { cci } => {
|
||||
let account_id = wallet_core.create_new_account_public(cci);
|
||||
|
||||
println!("Generated new account with account_id Public/{account_id}");
|
||||
|
||||
@ -111,8 +123,8 @@ impl WalletSubcommand for NewSubcommand {
|
||||
|
||||
Ok(SubcommandReturnValue::RegisterAccount { account_id })
|
||||
}
|
||||
NewSubcommand::Private {} => {
|
||||
let account_id = wallet_core.create_new_account_private();
|
||||
NewSubcommand::Private { cci } => {
|
||||
let account_id = wallet_core.create_new_account_private(cci);
|
||||
|
||||
let (key, _) = wallet_core
|
||||
.storage
|
||||
@ -164,7 +176,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,
|
||||
}
|
||||
}
|
||||
@ -264,36 +281,105 @@ 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()
|
||||
.await?
|
||||
.last_block;
|
||||
|
||||
if !wallet_core
|
||||
if wallet_core
|
||||
.storage
|
||||
.user_data
|
||||
.user_private_accounts
|
||||
.private_key_tree
|
||||
.account_id_map
|
||||
.is_empty()
|
||||
{
|
||||
parse_block_range(
|
||||
last_synced_block + 1,
|
||||
curr_last_block,
|
||||
wallet_core.sequencer_client.clone(),
|
||||
wallet_core,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
wallet_core.last_synced_block = curr_last_block;
|
||||
|
||||
let path = wallet_core.store_persistent_data().await?;
|
||||
|
||||
println!("Stored persistent data at {path:#?}");
|
||||
} else {
|
||||
wallet_core.sync_to_block(curr_last_block).await?;
|
||||
}
|
||||
|
||||
Ok(SubcommandReturnValue::SyncedToBlock(curr_last_block))
|
||||
}
|
||||
AccountSubcommand::List {} => {
|
||||
let user_data = &wallet_core.storage.user_data;
|
||||
let accounts = user_data
|
||||
.default_pub_account_signing_keys
|
||||
.keys()
|
||||
.map(|id| format!("Preconfigured Public/{id}"))
|
||||
.chain(
|
||||
user_data
|
||||
.default_user_private_accounts
|
||||
.keys()
|
||||
.map(|id| format!("Preconfigured Private/{id}")),
|
||||
)
|
||||
.chain(
|
||||
user_data
|
||||
.public_key_tree
|
||||
.account_id_map
|
||||
.iter()
|
||||
.map(|(id, chain_index)| format!("{chain_index} Public/{id}")),
|
||||
)
|
||||
.chain(
|
||||
user_data
|
||||
.private_key_tree
|
||||
.account_id_map
|
||||
.iter()
|
||||
.map(|(id, chain_index)| format!("{chain_index} Private/{id}")),
|
||||
)
|
||||
.format(",\n");
|
||||
|
||||
println!("{accounts}");
|
||||
Ok(SubcommandReturnValue::Empty)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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");
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
|
||||
use crate::{SubcommandReturnValue, WalletCore, cli::WalletSubcommand};
|
||||
use crate::{
|
||||
WalletCore,
|
||||
cli::{SubcommandReturnValue, WalletSubcommand},
|
||||
};
|
||||
|
||||
/// Represents generic chain CLI subcommand
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
|
||||
use crate::{SubcommandReturnValue, WalletCore, cli::WalletSubcommand};
|
||||
use crate::{
|
||||
WalletCore,
|
||||
cli::{SubcommandReturnValue, WalletSubcommand},
|
||||
};
|
||||
|
||||
/// Represents generic config CLI subcommand
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
@ -52,19 +55,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" => {
|
||||
@ -86,17 +89,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");
|
||||
@ -122,19 +123,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,15 +1,188 @@
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use nssa::program::Program;
|
||||
|
||||
use crate::{SubcommandReturnValue, WalletCore};
|
||||
use crate::{
|
||||
WalletCore,
|
||||
cli::{
|
||||
account::AccountSubcommand,
|
||||
chain::ChainSubcommand,
|
||||
config::ConfigSubcommand,
|
||||
programs::{
|
||||
native_token_transfer::AuthTransferSubcommand, pinata::PinataProgramAgnosticSubcommand,
|
||||
token::TokenProgramAgnosticSubcommand,
|
||||
},
|
||||
},
|
||||
helperfunctions::fetch_config,
|
||||
};
|
||||
|
||||
pub mod account;
|
||||
pub mod chain;
|
||||
pub mod config;
|
||||
pub mod native_token_transfer_program;
|
||||
pub mod pinata_program;
|
||||
pub mod token_program;
|
||||
pub mod programs;
|
||||
|
||||
pub(crate) trait WalletSubcommand {
|
||||
async fn handle_subcommand(self, wallet_core: &mut WalletCore)
|
||||
-> Result<SubcommandReturnValue>;
|
||||
}
|
||||
|
||||
/// Represents CLI command for a wallet
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
#[clap(about)]
|
||||
pub enum Command {
|
||||
/// Authenticated transfer subcommand
|
||||
#[command(subcommand)]
|
||||
AuthTransfer(AuthTransferSubcommand),
|
||||
/// Generic chain info subcommand
|
||||
#[command(subcommand)]
|
||||
ChainInfo(ChainSubcommand),
|
||||
/// Account view and sync subcommand
|
||||
#[command(subcommand)]
|
||||
Account(AccountSubcommand),
|
||||
/// Pinata program interaction subcommand
|
||||
#[command(subcommand)]
|
||||
Pinata(PinataProgramAgnosticSubcommand),
|
||||
/// Token program interaction subcommand
|
||||
#[command(subcommand)]
|
||||
Token(TokenProgramAgnosticSubcommand),
|
||||
/// Check the wallet can connect to the node and builtin local programs
|
||||
/// match the remote versions
|
||||
CheckHealth {},
|
||||
/// Command to setup config, get and set config fields
|
||||
#[command(subcommand)]
|
||||
Config(ConfigSubcommand),
|
||||
}
|
||||
|
||||
/// Represents overarching CLI command for a wallet with setup included
|
||||
#[derive(Debug, Subcommand, Clone)]
|
||||
#[clap(about)]
|
||||
pub enum OverCommand {
|
||||
/// Represents CLI command for a wallet
|
||||
#[command(subcommand)]
|
||||
Command(Command),
|
||||
/// Setup of a storage. Initializes rots for public and private trees from `password`.
|
||||
Setup {
|
||||
#[arg(short, long)]
|
||||
password: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// To execute commands, env var NSSA_WALLET_HOME_DIR must be set into directory with config
|
||||
///
|
||||
/// All account adresses must be valid 32 byte base58 strings.
|
||||
///
|
||||
/// All account account_ids must be provided as {privacy_prefix}/{account_id},
|
||||
/// where valid options for `privacy_prefix` is `Public` and `Private`
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(version, about)]
|
||||
pub struct Args {
|
||||
/// Continious run flag
|
||||
#[arg(short, long)]
|
||||
pub continuous_run: bool,
|
||||
/// Wallet command
|
||||
#[command(subcommand)]
|
||||
pub command: Option<OverCommand>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SubcommandReturnValue {
|
||||
PrivacyPreservingTransfer { tx_hash: String },
|
||||
RegisterAccount { account_id: nssa::AccountId },
|
||||
Account(nssa::Account),
|
||||
Empty,
|
||||
SyncedToBlock(u64),
|
||||
}
|
||||
|
||||
pub async fn execute_subcommand(command: Command) -> Result<SubcommandReturnValue> {
|
||||
let wallet_config = fetch_config().await?;
|
||||
let mut wallet_core = WalletCore::start_from_config_update_chain(wallet_config).await?;
|
||||
|
||||
let subcommand_ret = match command {
|
||||
Command::AuthTransfer(transfer_subcommand) => {
|
||||
transfer_subcommand
|
||||
.handle_subcommand(&mut wallet_core)
|
||||
.await?
|
||||
}
|
||||
Command::ChainInfo(chain_subcommand) => {
|
||||
chain_subcommand.handle_subcommand(&mut wallet_core).await?
|
||||
}
|
||||
Command::Account(account_subcommand) => {
|
||||
account_subcommand
|
||||
.handle_subcommand(&mut wallet_core)
|
||||
.await?
|
||||
}
|
||||
Command::Pinata(pinata_subcommand) => {
|
||||
pinata_subcommand
|
||||
.handle_subcommand(&mut wallet_core)
|
||||
.await?
|
||||
}
|
||||
Command::CheckHealth {} => {
|
||||
let remote_program_ids = wallet_core
|
||||
.sequencer_client
|
||||
.get_program_ids()
|
||||
.await
|
||||
.expect("Error fetching program ids");
|
||||
let Some(authenticated_transfer_id) = remote_program_ids.get("authenticated_transfer")
|
||||
else {
|
||||
panic!("Missing authenticated transfer ID from remote");
|
||||
};
|
||||
if authenticated_transfer_id != &Program::authenticated_transfer_program().id() {
|
||||
panic!("Local ID for authenticated transfer program is different from remote");
|
||||
}
|
||||
let Some(token_id) = remote_program_ids.get("token") else {
|
||||
panic!("Missing token program ID from remote");
|
||||
};
|
||||
if token_id != &Program::token().id() {
|
||||
panic!("Local ID for token program is different from remote");
|
||||
}
|
||||
let Some(circuit_id) = remote_program_ids.get("privacy_preserving_circuit") else {
|
||||
panic!("Missing privacy preserving circuit ID from remote");
|
||||
};
|
||||
if circuit_id != &nssa::PRIVACY_PRESERVING_CIRCUIT_ID {
|
||||
panic!("Local ID for privacy preserving circuit is different from remote");
|
||||
}
|
||||
|
||||
println!("✅All looks good!");
|
||||
|
||||
SubcommandReturnValue::Empty
|
||||
}
|
||||
Command::Token(token_subcommand) => {
|
||||
token_subcommand.handle_subcommand(&mut wallet_core).await?
|
||||
}
|
||||
Command::Config(config_subcommand) => {
|
||||
config_subcommand
|
||||
.handle_subcommand(&mut wallet_core)
|
||||
.await?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(subcommand_ret)
|
||||
}
|
||||
|
||||
pub async fn execute_continuous_run() -> Result<()> {
|
||||
let config = fetch_config().await?;
|
||||
let mut wallet_core = WalletCore::start_from_config_update_chain(config.clone()).await?;
|
||||
|
||||
loop {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn execute_setup(password: String) -> Result<()> {
|
||||
let config = fetch_config().await?;
|
||||
let wallet_core = WalletCore::start_from_config_new_storage(config.clone(), password).await?;
|
||||
|
||||
wallet_core.store_persistent_data().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
3
wallet/src/cli/programs/mod.rs
Normal file
3
wallet/src/cli/programs/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod native_token_transfer;
|
||||
pub mod pinata;
|
||||
pub mod token;
|
||||
@ -4,9 +4,10 @@ use common::transaction::NSSATransaction;
|
||||
use nssa::AccountId;
|
||||
|
||||
use crate::{
|
||||
SubcommandReturnValue, WalletCore,
|
||||
cli::WalletSubcommand,
|
||||
WalletCore,
|
||||
cli::{SubcommandReturnValue, WalletSubcommand},
|
||||
helperfunctions::{AccountPrivacyKind, parse_addr_with_privacy_prefix},
|
||||
program_facades::native_token_transfer::NativeTokenTransfer,
|
||||
};
|
||||
|
||||
/// Represents generic CLI subcommand for a wallet working with native token transfer program
|
||||
@ -56,11 +57,11 @@ impl WalletSubcommand for AuthTransferSubcommand {
|
||||
AccountPrivacyKind::Public => {
|
||||
let account_id = account_id.parse()?;
|
||||
|
||||
let res = wallet_core
|
||||
.register_account_under_authenticated_transfers_programs(account_id)
|
||||
let res = NativeTokenTransfer(wallet_core)
|
||||
.register_account(account_id)
|
||||
.await?;
|
||||
|
||||
println!("Results of tx send is {res:#?}");
|
||||
println!("Results of tx send are {res:#?}");
|
||||
|
||||
let transfer_tx =
|
||||
wallet_core.poll_native_token_transfer(res.tx_hash).await?;
|
||||
@ -74,13 +75,11 @@ impl WalletSubcommand for AuthTransferSubcommand {
|
||||
AccountPrivacyKind::Private => {
|
||||
let account_id = account_id.parse()?;
|
||||
|
||||
let (res, [secret]) = wallet_core
|
||||
.register_account_under_authenticated_transfers_programs_private(
|
||||
account_id,
|
||||
)
|
||||
let (res, secret) = NativeTokenTransfer(wallet_core)
|
||||
.register_account_private(account_id)
|
||||
.await?;
|
||||
|
||||
println!("Results of tx send is {res:#?}");
|
||||
println!("Results of tx send are {res:#?}");
|
||||
|
||||
let tx_hash = res.tx_hash;
|
||||
let transfer_tx = wallet_core
|
||||
@ -317,23 +316,11 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandPrivate {
|
||||
let from: AccountId = from.parse().unwrap();
|
||||
let to: AccountId = to.parse().unwrap();
|
||||
|
||||
let to_initialization = wallet_core.check_private_account_initialized(&to).await?;
|
||||
let (res, [secret_from, secret_to]) = NativeTokenTransfer(wallet_core)
|
||||
.send_private_transfer_to_owned_account(from, to, amount)
|
||||
.await?;
|
||||
|
||||
let (res, [secret_from, secret_to]) = if let Some(to_proof) = to_initialization {
|
||||
wallet_core
|
||||
.send_private_native_token_transfer_owned_account_already_initialized(
|
||||
from, to, amount, to_proof,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
wallet_core
|
||||
.send_private_native_token_transfer_owned_account_not_initialized(
|
||||
from, to, amount,
|
||||
)
|
||||
.await?
|
||||
};
|
||||
|
||||
println!("Results of tx send is {res:#?}");
|
||||
println!("Results of tx send are {res:#?}");
|
||||
|
||||
let tx_hash = res.tx_hash;
|
||||
let transfer_tx = wallet_core
|
||||
@ -373,11 +360,11 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandPrivate {
|
||||
let to_ipk =
|
||||
nssa_core::encryption::shared_key_derivation::Secp256k1Point(to_ipk.to_vec());
|
||||
|
||||
let (res, [secret_from, _]) = wallet_core
|
||||
.send_private_native_token_transfer_outer_account(from, to_npk, to_ipk, amount)
|
||||
let (res, [secret_from, _]) = NativeTokenTransfer(wallet_core)
|
||||
.send_private_transfer_to_outer_account(from, to_npk, to_ipk, amount)
|
||||
.await?;
|
||||
|
||||
println!("Results of tx send is {res:#?}");
|
||||
println!("Results of tx send are {res:#?}");
|
||||
|
||||
let tx_hash = res.tx_hash;
|
||||
let transfer_tx = wallet_core
|
||||
@ -413,21 +400,11 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandShielded {
|
||||
let from: AccountId = from.parse().unwrap();
|
||||
let to: AccountId = to.parse().unwrap();
|
||||
|
||||
let to_initialization = wallet_core.check_private_account_initialized(&to).await?;
|
||||
let (res, secret) = NativeTokenTransfer(wallet_core)
|
||||
.send_shielded_transfer(from, to, amount)
|
||||
.await?;
|
||||
|
||||
let (res, [secret]) = if let Some(to_proof) = to_initialization {
|
||||
wallet_core
|
||||
.send_shielded_native_token_transfer_already_initialized(
|
||||
from, to, amount, to_proof,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
wallet_core
|
||||
.send_shielded_native_token_transfer_not_initialized(from, to, amount)
|
||||
.await?
|
||||
};
|
||||
|
||||
println!("Results of tx send is {res:#?}");
|
||||
println!("Results of tx send are {res:#?}");
|
||||
|
||||
let tx_hash = res.tx_hash;
|
||||
let transfer_tx = wallet_core
|
||||
@ -468,11 +445,11 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandShielded {
|
||||
let to_ipk =
|
||||
nssa_core::encryption::shared_key_derivation::Secp256k1Point(to_ipk.to_vec());
|
||||
|
||||
let res = wallet_core
|
||||
.send_shielded_native_token_transfer_outer_account(from, to_npk, to_ipk, amount)
|
||||
let (res, _) = NativeTokenTransfer(wallet_core)
|
||||
.send_shielded_transfer_to_outer_account(from, to_npk, to_ipk, amount)
|
||||
.await?;
|
||||
|
||||
println!("Results of tx send is {res:#?}");
|
||||
println!("Results of tx send are {res:#?}");
|
||||
|
||||
let tx_hash = res.tx_hash;
|
||||
|
||||
@ -502,11 +479,11 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommand {
|
||||
let from: AccountId = from.parse().unwrap();
|
||||
let to: AccountId = to.parse().unwrap();
|
||||
|
||||
let (res, [secret]) = wallet_core
|
||||
.send_deshielded_native_token_transfer(from, to, amount)
|
||||
let (res, secret) = NativeTokenTransfer(wallet_core)
|
||||
.send_deshielded_transfer(from, to, amount)
|
||||
.await?;
|
||||
|
||||
println!("Results of tx send is {res:#?}");
|
||||
println!("Results of tx send are {res:#?}");
|
||||
|
||||
let tx_hash = res.tx_hash;
|
||||
let transfer_tx = wallet_core
|
||||
@ -532,11 +509,11 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommand {
|
||||
let from: AccountId = from.parse().unwrap();
|
||||
let to: AccountId = to.parse().unwrap();
|
||||
|
||||
let res = wallet_core
|
||||
.send_public_native_token_transfer(from, to, amount)
|
||||
let res = NativeTokenTransfer(wallet_core)
|
||||
.send_public_transfer(from, to, amount)
|
||||
.await?;
|
||||
|
||||
println!("Results of tx send is {res:#?}");
|
||||
println!("Results of tx send are {res:#?}");
|
||||
|
||||
let transfer_tx = wallet_core.poll_native_token_transfer(res.tx_hash).await?;
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context, Result};
|
||||
use clap::Subcommand;
|
||||
use common::{PINATA_BASE58, transaction::NSSATransaction};
|
||||
use log::info;
|
||||
|
||||
use crate::{
|
||||
SubcommandReturnValue, WalletCore,
|
||||
cli::WalletSubcommand,
|
||||
WalletCore,
|
||||
cli::{SubcommandReturnValue, WalletSubcommand},
|
||||
helperfunctions::{AccountPrivacyKind, parse_addr_with_privacy_prefix},
|
||||
program_facades::pinata::Pinata,
|
||||
};
|
||||
|
||||
/// Represents generic CLI subcommand for a wallet working with pinata program
|
||||
@ -14,12 +14,9 @@ use crate::{
|
||||
pub enum PinataProgramAgnosticSubcommand {
|
||||
/// Claim pinata
|
||||
Claim {
|
||||
/// to_account_id - valid 32 byte base58 string with privacy prefix
|
||||
/// to - valid 32 byte base58 string with privacy prefix
|
||||
#[arg(long)]
|
||||
to_account_id: String,
|
||||
/// solution - solution to pinata challenge
|
||||
#[arg(long)]
|
||||
solution: u128,
|
||||
to: String,
|
||||
},
|
||||
}
|
||||
|
||||
@ -29,26 +26,20 @@ impl WalletSubcommand for PinataProgramAgnosticSubcommand {
|
||||
wallet_core: &mut WalletCore,
|
||||
) -> Result<SubcommandReturnValue> {
|
||||
let underlying_subcommand = match self {
|
||||
PinataProgramAgnosticSubcommand::Claim {
|
||||
to_account_id,
|
||||
solution,
|
||||
} => {
|
||||
let (to_account_id, to_addr_privacy) =
|
||||
parse_addr_with_privacy_prefix(&to_account_id)?;
|
||||
PinataProgramAgnosticSubcommand::Claim { to } => {
|
||||
let (to, to_addr_privacy) = parse_addr_with_privacy_prefix(&to)?;
|
||||
|
||||
match to_addr_privacy {
|
||||
AccountPrivacyKind::Public => {
|
||||
PinataProgramSubcommand::Public(PinataProgramSubcommandPublic::Claim {
|
||||
pinata_account_id: PINATA_BASE58.to_string(),
|
||||
winner_account_id: to_account_id,
|
||||
solution,
|
||||
winner_account_id: to,
|
||||
})
|
||||
}
|
||||
AccountPrivacyKind::Private => PinataProgramSubcommand::Private(
|
||||
PinataProgramSubcommandPrivate::ClaimPrivateOwned {
|
||||
pinata_account_id: PINATA_BASE58.to_string(),
|
||||
winner_account_id: to_account_id,
|
||||
solution,
|
||||
winner_account_id: to,
|
||||
},
|
||||
),
|
||||
}
|
||||
@ -82,9 +73,6 @@ pub enum PinataProgramSubcommandPublic {
|
||||
/// winner_account_id - valid 32 byte hex string
|
||||
#[arg(long)]
|
||||
winner_account_id: String,
|
||||
/// solution - solution to pinata challenge
|
||||
#[arg(long)]
|
||||
solution: u128,
|
||||
},
|
||||
}
|
||||
|
||||
@ -100,9 +88,6 @@ pub enum PinataProgramSubcommandPrivate {
|
||||
/// winner_account_id - valid 32 byte hex string
|
||||
#[arg(long)]
|
||||
winner_account_id: String,
|
||||
/// solution - solution to pinata challenge
|
||||
#[arg(long)]
|
||||
solution: u128,
|
||||
},
|
||||
}
|
||||
|
||||
@ -115,16 +100,28 @@ impl WalletSubcommand for PinataProgramSubcommandPublic {
|
||||
PinataProgramSubcommandPublic::Claim {
|
||||
pinata_account_id,
|
||||
winner_account_id,
|
||||
solution,
|
||||
} => {
|
||||
let res = wallet_core
|
||||
.claim_pinata(
|
||||
pinata_account_id.parse().unwrap(),
|
||||
let pinata_account_id = pinata_account_id.parse().unwrap();
|
||||
let solution = find_solution(wallet_core, pinata_account_id)
|
||||
.await
|
||||
.context("failed to compute solution")?;
|
||||
|
||||
let res = Pinata(wallet_core)
|
||||
.claim(
|
||||
pinata_account_id,
|
||||
winner_account_id.parse().unwrap(),
|
||||
solution,
|
||||
)
|
||||
.await?;
|
||||
info!("Results of tx send is {res:#?}");
|
||||
|
||||
println!("Results of tx send are {res:#?}");
|
||||
|
||||
let tx_hash = res.tx_hash;
|
||||
let transfer_tx = wallet_core
|
||||
.poll_native_token_transfer(tx_hash.clone())
|
||||
.await?;
|
||||
|
||||
println!("Transaction data is {transfer_tx:?}");
|
||||
|
||||
Ok(SubcommandReturnValue::Empty)
|
||||
}
|
||||
@ -141,41 +138,26 @@ impl WalletSubcommand for PinataProgramSubcommandPrivate {
|
||||
PinataProgramSubcommandPrivate::ClaimPrivateOwned {
|
||||
pinata_account_id,
|
||||
winner_account_id,
|
||||
solution,
|
||||
} => {
|
||||
let pinata_account_id = pinata_account_id.parse().unwrap();
|
||||
let winner_account_id = winner_account_id.parse().unwrap();
|
||||
let solution = find_solution(wallet_core, pinata_account_id)
|
||||
.await
|
||||
.context("failed to compute solution")?;
|
||||
|
||||
let winner_initialization = wallet_core
|
||||
.check_private_account_initialized(&winner_account_id)
|
||||
let (res, secret_winner) = Pinata(wallet_core)
|
||||
.claim_private_owned_account(pinata_account_id, winner_account_id, solution)
|
||||
.await?;
|
||||
|
||||
let (res, [secret_winner]) = if let Some(winner_proof) = winner_initialization {
|
||||
wallet_core
|
||||
.claim_pinata_private_owned_account_already_initialized(
|
||||
pinata_account_id,
|
||||
winner_account_id,
|
||||
solution,
|
||||
winner_proof,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
wallet_core
|
||||
.claim_pinata_private_owned_account_not_initialized(
|
||||
pinata_account_id,
|
||||
winner_account_id,
|
||||
solution,
|
||||
)
|
||||
.await?
|
||||
};
|
||||
|
||||
info!("Results of tx send is {res:#?}");
|
||||
println!("Results of tx send are {res:#?}");
|
||||
|
||||
let tx_hash = res.tx_hash;
|
||||
let transfer_tx = wallet_core
|
||||
.poll_native_token_transfer(tx_hash.clone())
|
||||
.await?;
|
||||
|
||||
println!("Transaction data is {transfer_tx:?}");
|
||||
|
||||
if let NSSATransaction::PrivacyPreserving(tx) = transfer_tx {
|
||||
let acc_decode_data = vec![(secret_winner, winner_account_id)];
|
||||
|
||||
@ -210,3 +192,46 @@ impl WalletSubcommand for PinataProgramSubcommand {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn find_solution(wallet: &WalletCore, pinata_account_id: nssa::AccountId) -> Result<u128> {
|
||||
let account = wallet.get_account_public(pinata_account_id).await?;
|
||||
let data: [u8; 33] = account
|
||||
.data
|
||||
.try_into()
|
||||
.map_err(|_| anyhow::Error::msg("invalid pinata account data"))?;
|
||||
|
||||
println!("Computing solution for pinata...");
|
||||
let now = std::time::Instant::now();
|
||||
|
||||
let solution = compute_solution(data);
|
||||
|
||||
println!("Found solution {solution} in {:?}", now.elapsed());
|
||||
Ok(solution)
|
||||
}
|
||||
|
||||
fn compute_solution(data: [u8; 33]) -> u128 {
|
||||
let difficulty = data[0];
|
||||
let seed = &data[1..];
|
||||
|
||||
let mut solution = 0u128;
|
||||
while !validate_solution(difficulty, seed, solution) {
|
||||
solution = solution.checked_add(1).expect("solution overflowed u128");
|
||||
}
|
||||
|
||||
solution
|
||||
}
|
||||
|
||||
fn validate_solution(difficulty: u8, seed: &[u8], solution: u128) -> bool {
|
||||
use sha2::{Digest as _, digest::FixedOutput as _};
|
||||
|
||||
let mut bytes = [0; 32 + 16];
|
||||
bytes[..32].copy_from_slice(seed);
|
||||
bytes[32..].copy_from_slice(&solution.to_le_bytes());
|
||||
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
hasher.update(bytes);
|
||||
let digest: [u8; 32] = hasher.finalize_fixed().into();
|
||||
|
||||
let difficulty = difficulty as usize;
|
||||
digest[..difficulty].iter().all(|&b| b == 0)
|
||||
}
|
||||
@ -4,9 +4,10 @@ use common::transaction::NSSATransaction;
|
||||
use nssa::AccountId;
|
||||
|
||||
use crate::{
|
||||
SubcommandReturnValue, WalletCore,
|
||||
cli::WalletSubcommand,
|
||||
WalletCore,
|
||||
cli::{SubcommandReturnValue, WalletSubcommand},
|
||||
helperfunctions::{AccountPrivacyKind, parse_addr_with_privacy_prefix},
|
||||
program_facades::token::Token,
|
||||
};
|
||||
|
||||
/// Represents generic CLI subcommand for a wallet working with token program
|
||||
@ -338,8 +339,8 @@ impl WalletSubcommand for TokenProgramSubcommandPublic {
|
||||
}
|
||||
let mut name_bytes = [0; 6];
|
||||
name_bytes[..name.len()].copy_from_slice(name);
|
||||
wallet_core
|
||||
.send_new_token_definition(
|
||||
Token(wallet_core)
|
||||
.send_new_definition(
|
||||
definition_account_id.parse().unwrap(),
|
||||
supply_account_id.parse().unwrap(),
|
||||
name_bytes,
|
||||
@ -353,8 +354,8 @@ impl WalletSubcommand for TokenProgramSubcommandPublic {
|
||||
recipient_account_id,
|
||||
balance_to_move,
|
||||
} => {
|
||||
wallet_core
|
||||
.send_transfer_token_transaction(
|
||||
Token(wallet_core)
|
||||
.send_transfer_transaction(
|
||||
sender_account_id.parse().unwrap(),
|
||||
recipient_account_id.parse().unwrap(),
|
||||
balance_to_move,
|
||||
@ -389,8 +390,8 @@ impl WalletSubcommand for TokenProgramSubcommandPrivate {
|
||||
let definition_account_id: AccountId = definition_account_id.parse().unwrap();
|
||||
let supply_account_id: AccountId = supply_account_id.parse().unwrap();
|
||||
|
||||
let (res, [secret_supply]) = wallet_core
|
||||
.send_new_token_definition_private_owned(
|
||||
let (res, secret_supply) = Token(wallet_core)
|
||||
.send_new_definition_private_owned(
|
||||
definition_account_id,
|
||||
supply_account_id,
|
||||
name_bytes,
|
||||
@ -398,7 +399,7 @@ impl WalletSubcommand for TokenProgramSubcommandPrivate {
|
||||
)
|
||||
.await?;
|
||||
|
||||
println!("Results of tx send is {res:#?}");
|
||||
println!("Results of tx send are {res:#?}");
|
||||
|
||||
let tx_hash = res.tx_hash;
|
||||
let transfer_tx = wallet_core
|
||||
@ -428,31 +429,15 @@ impl WalletSubcommand for TokenProgramSubcommandPrivate {
|
||||
let sender_account_id: AccountId = sender_account_id.parse().unwrap();
|
||||
let recipient_account_id: AccountId = recipient_account_id.parse().unwrap();
|
||||
|
||||
let recipient_initialization = wallet_core
|
||||
.check_private_account_initialized(&recipient_account_id)
|
||||
let (res, [secret_sender, secret_recipient]) = Token(wallet_core)
|
||||
.send_transfer_transaction_private_owned_account(
|
||||
sender_account_id,
|
||||
recipient_account_id,
|
||||
balance_to_move,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (res, [secret_sender, secret_recipient]) =
|
||||
if let Some(recipient_proof) = recipient_initialization {
|
||||
wallet_core
|
||||
.send_transfer_token_transaction_private_owned_account_already_initialized(
|
||||
sender_account_id,
|
||||
recipient_account_id,
|
||||
balance_to_move,
|
||||
recipient_proof,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
wallet_core
|
||||
.send_transfer_token_transaction_private_owned_account_not_initialized(
|
||||
sender_account_id,
|
||||
recipient_account_id,
|
||||
balance_to_move,
|
||||
)
|
||||
.await?
|
||||
};
|
||||
|
||||
println!("Results of tx send is {res:#?}");
|
||||
println!("Results of tx send are {res:#?}");
|
||||
|
||||
let tx_hash = res.tx_hash;
|
||||
let transfer_tx = wallet_core
|
||||
@ -496,8 +481,8 @@ impl WalletSubcommand for TokenProgramSubcommandPrivate {
|
||||
recipient_ipk.to_vec(),
|
||||
);
|
||||
|
||||
let (res, [secret_sender, _]) = wallet_core
|
||||
.send_transfer_token_transaction_private_foreign_account(
|
||||
let (res, [secret_sender, _]) = Token(wallet_core)
|
||||
.send_transfer_transaction_private_foreign_account(
|
||||
sender_account_id,
|
||||
recipient_npk,
|
||||
recipient_ipk,
|
||||
@ -505,7 +490,7 @@ impl WalletSubcommand for TokenProgramSubcommandPrivate {
|
||||
)
|
||||
.await?;
|
||||
|
||||
println!("Results of tx send is {res:#?}");
|
||||
println!("Results of tx send are {res:#?}");
|
||||
|
||||
let tx_hash = res.tx_hash;
|
||||
let transfer_tx = wallet_core
|
||||
@ -545,15 +530,15 @@ impl WalletSubcommand for TokenProgramSubcommandDeshielded {
|
||||
let sender_account_id: AccountId = sender_account_id.parse().unwrap();
|
||||
let recipient_account_id: AccountId = recipient_account_id.parse().unwrap();
|
||||
|
||||
let (res, [secret_sender]) = wallet_core
|
||||
.send_transfer_token_transaction_deshielded(
|
||||
let (res, secret_sender) = Token(wallet_core)
|
||||
.send_transfer_transaction_deshielded(
|
||||
sender_account_id,
|
||||
recipient_account_id,
|
||||
balance_to_move,
|
||||
)
|
||||
.await?;
|
||||
|
||||
println!("Results of tx send is {res:#?}");
|
||||
println!("Results of tx send are {res:#?}");
|
||||
|
||||
let tx_hash = res.tx_hash;
|
||||
let transfer_tx = wallet_core
|
||||
@ -604,8 +589,8 @@ impl WalletSubcommand for TokenProgramSubcommandShielded {
|
||||
recipient_ipk.to_vec(),
|
||||
);
|
||||
|
||||
let res = wallet_core
|
||||
.send_transfer_token_transaction_shielded_foreign_account(
|
||||
let (res, _) = Token(wallet_core)
|
||||
.send_transfer_transaction_shielded_foreign_account(
|
||||
sender_account_id,
|
||||
recipient_npk,
|
||||
recipient_ipk,
|
||||
@ -613,7 +598,7 @@ impl WalletSubcommand for TokenProgramSubcommandShielded {
|
||||
)
|
||||
.await?;
|
||||
|
||||
println!("Results of tx send is {res:#?}");
|
||||
println!("Results of tx send are {res:#?}");
|
||||
|
||||
let tx_hash = res.tx_hash;
|
||||
let transfer_tx = wallet_core
|
||||
@ -638,31 +623,15 @@ impl WalletSubcommand for TokenProgramSubcommandShielded {
|
||||
let sender_account_id: AccountId = sender_account_id.parse().unwrap();
|
||||
let recipient_account_id: AccountId = recipient_account_id.parse().unwrap();
|
||||
|
||||
let recipient_initialization = wallet_core
|
||||
.check_private_account_initialized(&recipient_account_id)
|
||||
let (res, secret_recipient) = Token(wallet_core)
|
||||
.send_transfer_transaction_shielded_owned_account(
|
||||
sender_account_id,
|
||||
recipient_account_id,
|
||||
balance_to_move,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (res, [secret_recipient]) =
|
||||
if let Some(recipient_proof) = recipient_initialization {
|
||||
wallet_core
|
||||
.send_transfer_token_transaction_shielded_owned_account_already_initialized(
|
||||
sender_account_id,
|
||||
recipient_account_id,
|
||||
balance_to_move,
|
||||
recipient_proof,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
wallet_core
|
||||
.send_transfer_token_transaction_shielded_owned_account_not_initialized(
|
||||
sender_account_id,
|
||||
recipient_account_id,
|
||||
balance_to_move,
|
||||
)
|
||||
.await?
|
||||
};
|
||||
|
||||
println!("Results of tx send is {res:#?}");
|
||||
println!("Results of tx send are {res:#?}");
|
||||
|
||||
let tx_hash = res.tx_hash;
|
||||
let transfer_tx = wallet_core
|
||||
@ -1,4 +1,9 @@
|
||||
use key_protocol::key_management::KeyChain;
|
||||
use key_protocol::key_management::{
|
||||
KeyChain,
|
||||
key_tree::{
|
||||
chain_index::ChainIndex, keys_private::ChildKeysPrivate, keys_public::ChildKeysPublic,
|
||||
},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@ -10,7 +15,8 @@ pub struct InitialAccountDataPublic {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PersistentAccountDataPublic {
|
||||
pub account_id: nssa::AccountId,
|
||||
pub pub_sign_key: nssa::PrivateKey,
|
||||
pub chain_index: ChainIndex,
|
||||
pub data: ChildKeysPublic,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@ -23,8 +29,8 @@ pub struct InitialAccountDataPrivate {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PersistentAccountDataPrivate {
|
||||
pub account_id: nssa::AccountId,
|
||||
pub account: nssa_core::account::Account,
|
||||
pub key_chain: KeyChain,
|
||||
pub chain_index: ChainIndex,
|
||||
pub data: ChildKeysPrivate,
|
||||
}
|
||||
|
||||
// Big difference in enum variants sizes
|
||||
@ -45,6 +51,7 @@ pub enum InitialAccountData {
|
||||
pub enum PersistentAccountData {
|
||||
Public(PersistentAccountDataPublic),
|
||||
Private(PersistentAccountDataPrivate),
|
||||
Preconfigured(InitialAccountData),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@ -67,6 +74,7 @@ impl PersistentAccountData {
|
||||
match &self {
|
||||
Self::Public(acc) => acc.account_id,
|
||||
Self::Private(acc) => acc.account_id,
|
||||
Self::Preconfigured(acc) => acc.account_id(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -95,6 +103,12 @@ impl From<PersistentAccountDataPrivate> for PersistentAccountData {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<InitialAccountData> for PersistentAccountData {
|
||||
fn from(value: InitialAccountData) -> Self {
|
||||
Self::Preconfigured(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GasConfig {
|
||||
/// Gas spent per deploying one byte of data
|
||||
@ -121,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>,
|
||||
}
|
||||
@ -137,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#"
|
||||
[
|
||||
|
||||
@ -12,6 +12,7 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use crate::{
|
||||
HOME_DIR_ENV_VAR,
|
||||
config::{
|
||||
InitialAccountData, InitialAccountDataPrivate, InitialAccountDataPublic,
|
||||
PersistentAccountDataPrivate, PersistentAccountDataPublic, PersistentStorage, WalletConfig,
|
||||
},
|
||||
};
|
||||
@ -90,7 +91,7 @@ pub async fn fetch_config() -> Result<WalletConfig> {
|
||||
|
||||
/// Fetch data stored at home
|
||||
///
|
||||
/// If file not present, it is considered as empty list of persistent accounts
|
||||
/// File must be created through setup beforehand.
|
||||
pub async fn fetch_persistent_storage() -> Result<PersistentStorage> {
|
||||
let home = get_home()?;
|
||||
let accs_path = home.join("storage.json");
|
||||
@ -102,10 +103,9 @@ pub async fn fetch_persistent_storage() -> Result<PersistentStorage> {
|
||||
Ok(serde_json::from_slice(&storage_content)?)
|
||||
}
|
||||
Err(err) => match err.kind() {
|
||||
std::io::ErrorKind::NotFound => Ok(PersistentStorage {
|
||||
accounts: vec![],
|
||||
last_synced_block: 0,
|
||||
}),
|
||||
std::io::ErrorKind::NotFound => {
|
||||
anyhow::bail!("Not found, please setup roots from config command beforehand");
|
||||
}
|
||||
_ => {
|
||||
anyhow::bail!("IO error {err:#?}");
|
||||
}
|
||||
@ -120,25 +120,51 @@ pub fn produce_data_for_storage(
|
||||
) -> PersistentStorage {
|
||||
let mut vec_for_storage = vec![];
|
||||
|
||||
for (account_id, key) in &user_data.pub_account_signing_keys {
|
||||
vec_for_storage.push(
|
||||
PersistentAccountDataPublic {
|
||||
account_id: *account_id,
|
||||
pub_sign_key: key.clone(),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
for (account_id, key) in &user_data.public_key_tree.account_id_map {
|
||||
if let Some(data) = user_data.public_key_tree.key_map.get(key) {
|
||||
vec_for_storage.push(
|
||||
PersistentAccountDataPublic {
|
||||
account_id: *account_id,
|
||||
chain_index: key.clone(),
|
||||
data: data.clone(),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (account_id, (key, acc)) in &user_data.user_private_accounts {
|
||||
for (account_id, key) in &user_data.private_key_tree.account_id_map {
|
||||
if let Some(data) = user_data.private_key_tree.key_map.get(key) {
|
||||
vec_for_storage.push(
|
||||
PersistentAccountDataPrivate {
|
||||
account_id: *account_id,
|
||||
chain_index: key.clone(),
|
||||
data: data.clone(),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (account_id, key) in &user_data.default_pub_account_signing_keys {
|
||||
vec_for_storage.push(
|
||||
PersistentAccountDataPrivate {
|
||||
account_id: *account_id,
|
||||
account: acc.clone(),
|
||||
key_chain: key.clone(),
|
||||
}
|
||||
InitialAccountData::Public(InitialAccountDataPublic {
|
||||
account_id: account_id.to_string(),
|
||||
pub_sign_key: key.clone(),
|
||||
})
|
||||
.into(),
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
for (account_id, (key_chain, account)) in &user_data.default_user_private_accounts {
|
||||
vec_for_storage.push(
|
||||
InitialAccountData::Private(InitialAccountDataPrivate {
|
||||
account_id: account_id.to_string(),
|
||||
account: account.clone(),
|
||||
key_chain: key_chain.clone(),
|
||||
})
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
|
||||
PersistentStorage {
|
||||
|
||||
@ -3,30 +3,28 @@ use std::{path::PathBuf, sync::Arc};
|
||||
use anyhow::Result;
|
||||
use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
|
||||
use chain_storage::WalletChainStore;
|
||||
use clap::{Parser, Subcommand};
|
||||
use common::{
|
||||
block::HashableBlockData,
|
||||
error::ExecutionFailureKind,
|
||||
rpc_primitives::requests::SendTxResponse,
|
||||
sequencer_client::SequencerClient,
|
||||
transaction::{EncodedTransaction, NSSATransaction},
|
||||
};
|
||||
use config::WalletConfig;
|
||||
use key_protocol::key_management::key_tree::{chain_index::ChainIndex, traits::KeyNode as _};
|
||||
use log::info;
|
||||
use nssa::{
|
||||
Account, AccountId, privacy_preserving_transaction::message::EncryptedAccountData,
|
||||
program::Program,
|
||||
Account, AccountId, PrivacyPreservingTransaction,
|
||||
privacy_preserving_transaction::message::EncryptedAccountData, program::Program,
|
||||
};
|
||||
use nssa_core::{Commitment, MembershipProof};
|
||||
use nssa_core::{Commitment, MembershipProof, SharedSecretKey, program::InstructionData};
|
||||
pub use privacy_preserving_tx::PrivacyPreservingAccount;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
use crate::{
|
||||
cli::{
|
||||
WalletSubcommand, account::AccountSubcommand, chain::ChainSubcommand,
|
||||
config::ConfigSubcommand, native_token_transfer_program::AuthTransferSubcommand,
|
||||
pinata_program::PinataProgramAgnosticSubcommand,
|
||||
token_program::TokenProgramAgnosticSubcommand,
|
||||
},
|
||||
config::PersistentStorage,
|
||||
helperfunctions::{fetch_config, fetch_persistent_storage, get_home, produce_data_for_storage},
|
||||
helperfunctions::{
|
||||
fetch_persistent_storage, get_home, produce_data_for_storage, produce_random_nonces,
|
||||
},
|
||||
poller::TxPoller,
|
||||
};
|
||||
|
||||
@ -36,11 +34,9 @@ pub mod chain_storage;
|
||||
pub mod cli;
|
||||
pub mod config;
|
||||
pub mod helperfunctions;
|
||||
pub mod pinata_interactions;
|
||||
pub mod poller;
|
||||
pub mod token_program_interactions;
|
||||
pub mod token_transfers;
|
||||
pub mod transaction_utils;
|
||||
mod privacy_preserving_tx;
|
||||
pub mod program_facades;
|
||||
|
||||
pub struct WalletCore {
|
||||
pub storage: WalletChainStore,
|
||||
@ -54,15 +50,12 @@ impl WalletCore {
|
||||
let client = Arc::new(SequencerClient::new(config.sequencer_addr.clone())?);
|
||||
let tx_poller = TxPoller::new(config.clone(), client.clone());
|
||||
|
||||
let mut storage = WalletChainStore::new(config)?;
|
||||
|
||||
let PersistentStorage {
|
||||
accounts: persistent_accounts,
|
||||
last_synced_block,
|
||||
} = fetch_persistent_storage().await?;
|
||||
for pers_acc_data in persistent_accounts {
|
||||
storage.insert_account_data(pers_acc_data);
|
||||
}
|
||||
|
||||
let storage = WalletChainStore::new(config, persistent_accounts)?;
|
||||
|
||||
Ok(Self {
|
||||
storage,
|
||||
@ -72,6 +65,23 @@ impl WalletCore {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn start_from_config_new_storage(
|
||||
config: WalletConfig,
|
||||
password: String,
|
||||
) -> Result<Self> {
|
||||
let client = Arc::new(SequencerClient::new(config.sequencer_addr.clone())?);
|
||||
let tx_poller = TxPoller::new(config.clone(), client.clone());
|
||||
|
||||
let storage = WalletChainStore::new_storage(config, password)?;
|
||||
|
||||
Ok(Self {
|
||||
storage,
|
||||
poller: tx_poller,
|
||||
sequencer_client: client.clone(),
|
||||
last_synced_block: 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Store persistent data at home
|
||||
pub async fn store_persistent_data(&self) -> Result<PathBuf> {
|
||||
let home = get_home()?;
|
||||
@ -102,16 +112,16 @@ impl WalletCore {
|
||||
Ok(config_path)
|
||||
}
|
||||
|
||||
pub fn create_new_account_public(&mut self) -> AccountId {
|
||||
pub fn create_new_account_public(&mut self, chain_index: ChainIndex) -> AccountId {
|
||||
self.storage
|
||||
.user_data
|
||||
.generate_new_public_transaction_private_key()
|
||||
.generate_new_public_transaction_private_key(chain_index)
|
||||
}
|
||||
|
||||
pub fn create_new_account_private(&mut self) -> AccountId {
|
||||
pub fn create_new_account_private(&mut self, chain_index: ChainIndex) -> AccountId {
|
||||
self.storage
|
||||
.user_data
|
||||
.generate_new_privacy_preserving_transaction_key_chain()
|
||||
.generate_new_privacy_preserving_transaction_key_chain(chain_index)
|
||||
}
|
||||
|
||||
/// Get account balance
|
||||
@ -141,20 +151,24 @@ impl WalletCore {
|
||||
Ok(response.account)
|
||||
}
|
||||
|
||||
pub fn get_account_public_signing_key(
|
||||
&self,
|
||||
account_id: &AccountId,
|
||||
) -> Option<&nssa::PrivateKey> {
|
||||
self.storage
|
||||
.user_data
|
||||
.get_pub_account_signing_key(account_id)
|
||||
}
|
||||
|
||||
pub fn get_account_private(&self, account_id: &AccountId) -> Option<Account> {
|
||||
self.storage
|
||||
.user_data
|
||||
.user_private_accounts
|
||||
.get(account_id)
|
||||
.get_private_account(account_id)
|
||||
.map(|value| value.1.clone())
|
||||
}
|
||||
|
||||
pub fn get_private_account_commitment(&self, account_id: &AccountId) -> Option<Commitment> {
|
||||
let (keys, account) = self
|
||||
.storage
|
||||
.user_data
|
||||
.user_private_accounts
|
||||
.get(account_id)?;
|
||||
let (keys, account) = self.storage.user_data.get_private_account(account_id)?;
|
||||
Some(Commitment::new(&keys.nullifer_public_key, account))
|
||||
}
|
||||
|
||||
@ -208,225 +222,168 @@ impl WalletCore {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents CLI command for a wallet
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
#[clap(about)]
|
||||
pub enum Command {
|
||||
/// Authenticated transfer subcommand
|
||||
#[command(subcommand)]
|
||||
AuthTransfer(AuthTransferSubcommand),
|
||||
/// Generic chain info subcommand
|
||||
#[command(subcommand)]
|
||||
ChainInfo(ChainSubcommand),
|
||||
/// Account view and sync subcommand
|
||||
#[command(subcommand)]
|
||||
Account(AccountSubcommand),
|
||||
/// Pinata program interaction subcommand
|
||||
#[command(subcommand)]
|
||||
Pinata(PinataProgramAgnosticSubcommand),
|
||||
/// Token program interaction subcommand
|
||||
#[command(subcommand)]
|
||||
Token(TokenProgramAgnosticSubcommand),
|
||||
/// Check the wallet can connect to the node and builtin local programs
|
||||
/// match the remote versions
|
||||
CheckHealth {},
|
||||
/// Command to setup config, get and set config fields
|
||||
#[command(subcommand)]
|
||||
Config(ConfigSubcommand),
|
||||
}
|
||||
pub async fn send_privacy_preserving_tx(
|
||||
&self,
|
||||
accounts: Vec<PrivacyPreservingAccount>,
|
||||
instruction_data: &InstructionData,
|
||||
program: &Program,
|
||||
) -> Result<(SendTxResponse, Vec<SharedSecretKey>), ExecutionFailureKind> {
|
||||
self.send_privacy_preserving_tx_with_pre_check(accounts, instruction_data, program, |_| {
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// To execute commands, env var NSSA_WALLET_HOME_DIR must be set into directory with config
|
||||
///
|
||||
/// All account adresses must be valid 32 byte base58 strings.
|
||||
///
|
||||
/// All account account_ids must be provided as {privacy_prefix}/{account_id},
|
||||
/// where valid options for `privacy_prefix` is `Public` and `Private`
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(version, about)]
|
||||
pub struct Args {
|
||||
/// Continious run flag
|
||||
#[arg(short, long)]
|
||||
pub continious_run: bool,
|
||||
/// Wallet command
|
||||
#[command(subcommand)]
|
||||
pub command: Option<Command>,
|
||||
}
|
||||
pub async fn send_privacy_preserving_tx_with_pre_check(
|
||||
&self,
|
||||
accounts: Vec<PrivacyPreservingAccount>,
|
||||
instruction_data: &InstructionData,
|
||||
program: &Program,
|
||||
tx_pre_check: impl FnOnce(&[&Account]) -> Result<(), ExecutionFailureKind>,
|
||||
) -> Result<(SendTxResponse, Vec<SharedSecretKey>), ExecutionFailureKind> {
|
||||
let acc_manager = privacy_preserving_tx::AccountManager::new(self, accounts).await?;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SubcommandReturnValue {
|
||||
PrivacyPreservingTransfer { tx_hash: String },
|
||||
RegisterAccount { account_id: nssa::AccountId },
|
||||
Account(nssa::Account),
|
||||
Empty,
|
||||
SyncedToBlock(u64),
|
||||
}
|
||||
let pre_states = acc_manager.pre_states();
|
||||
tx_pre_check(
|
||||
&pre_states
|
||||
.iter()
|
||||
.map(|pre| &pre.account)
|
||||
.collect::<Vec<_>>(),
|
||||
)?;
|
||||
|
||||
pub async fn execute_subcommand(command: Command) -> Result<SubcommandReturnValue> {
|
||||
let wallet_config = fetch_config().await?;
|
||||
let mut wallet_core = WalletCore::start_from_config_update_chain(wallet_config).await?;
|
||||
let private_account_keys = acc_manager.private_account_keys();
|
||||
let (output, proof) = nssa::privacy_preserving_transaction::circuit::execute_and_prove(
|
||||
&pre_states,
|
||||
instruction_data,
|
||||
acc_manager.visibility_mask(),
|
||||
&produce_random_nonces(private_account_keys.len()),
|
||||
&private_account_keys
|
||||
.iter()
|
||||
.map(|keys| (keys.npk.clone(), keys.ssk.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
&acc_manager.private_account_auth(),
|
||||
&program.to_owned().into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let subcommand_ret = match command {
|
||||
Command::AuthTransfer(transfer_subcommand) => {
|
||||
transfer_subcommand
|
||||
.handle_subcommand(&mut wallet_core)
|
||||
.await?
|
||||
let message =
|
||||
nssa::privacy_preserving_transaction::message::Message::try_from_circuit_output(
|
||||
acc_manager.public_account_ids(),
|
||||
Vec::from_iter(acc_manager.public_account_nonces()),
|
||||
private_account_keys
|
||||
.iter()
|
||||
.map(|keys| (keys.npk.clone(), keys.ipk.clone(), keys.epk.clone()))
|
||||
.collect(),
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let witness_set =
|
||||
nssa::privacy_preserving_transaction::witness_set::WitnessSet::for_message(
|
||||
&message,
|
||||
proof,
|
||||
&acc_manager.witness_signing_keys(),
|
||||
);
|
||||
let tx = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
|
||||
let shared_secrets = private_account_keys
|
||||
.into_iter()
|
||||
.map(|keys| keys.ssk)
|
||||
.collect();
|
||||
|
||||
Ok((
|
||||
self.sequencer_client.send_tx_private(tx).await?,
|
||||
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(());
|
||||
}
|
||||
Command::ChainInfo(chain_subcommand) => {
|
||||
chain_subcommand.handle_subcommand(&mut wallet_core).await?
|
||||
}
|
||||
Command::Account(account_subcommand) => {
|
||||
account_subcommand
|
||||
.handle_subcommand(&mut wallet_core)
|
||||
.await?
|
||||
}
|
||||
Command::Pinata(pinata_subcommand) => {
|
||||
pinata_subcommand
|
||||
.handle_subcommand(&mut wallet_core)
|
||||
.await?
|
||||
}
|
||||
Command::CheckHealth {} => {
|
||||
let remote_program_ids = wallet_core
|
||||
.sequencer_client
|
||||
.get_program_ids()
|
||||
.await
|
||||
.expect("Error fetching program ids");
|
||||
let Some(authenticated_transfer_id) = remote_program_ids.get("authenticated_transfer")
|
||||
else {
|
||||
panic!("Missing authenticated transfer ID from remote");
|
||||
};
|
||||
if authenticated_transfer_id != &Program::authenticated_transfer_program().id() {
|
||||
panic!("Local ID for authenticated transfer program is different from remote");
|
||||
}
|
||||
let Some(token_id) = remote_program_ids.get("token") else {
|
||||
panic!("Missing token program ID from remote");
|
||||
};
|
||||
if token_id != &Program::token().id() {
|
||||
panic!("Local ID for token program is different from remote");
|
||||
}
|
||||
let Some(circuit_id) = remote_program_ids.get("privacy_preserving_circuit") else {
|
||||
panic!("Missing privacy preserving circuit ID from remote");
|
||||
};
|
||||
if circuit_id != &nssa::PRIVACY_PRESERVING_CIRCUIT_ID {
|
||||
panic!("Local ID for privacy preserving circuit is different from remote");
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
println!("✅All looks good!");
|
||||
|
||||
SubcommandReturnValue::Empty
|
||||
self.last_synced_block = block.block_id;
|
||||
self.store_persistent_data().await?;
|
||||
}
|
||||
Command::Token(token_subcommand) => {
|
||||
token_subcommand.handle_subcommand(&mut wallet_core).await?
|
||||
}
|
||||
Command::Config(config_subcommand) => {
|
||||
config_subcommand
|
||||
.handle_subcommand(&mut wallet_core)
|
||||
.await?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(subcommand_ret)
|
||||
}
|
||||
|
||||
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.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 (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
|
||||
"Synced to block {block_id} in {:?}",
|
||||
before_polling.elapsed()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
fn sync_private_accounts_with_tx(&mut self, tx: NSSATransaction) {
|
||||
let NSSATransaction::PrivacyPreserving(tx) = tx else {
|
||||
return;
|
||||
};
|
||||
|
||||
pub async fn execute_continious_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 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 mut latest_block_num = seq_client.get_last_block().await?.last_block;
|
||||
let mut curr_last_block = latest_block_num;
|
||||
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(),
|
||||
);
|
||||
|
||||
loop {
|
||||
parse_block_range(
|
||||
curr_last_block,
|
||||
latest_block_num,
|
||||
seq_client.clone(),
|
||||
&mut wallet_core,
|
||||
)
|
||||
.await?;
|
||||
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());
|
||||
|
||||
curr_last_block = latest_block_num + 1;
|
||||
nssa_core::EncryptionScheme::decrypt(
|
||||
ciphertext,
|
||||
&shared_secret,
|
||||
commitment,
|
||||
ciph_id as u32,
|
||||
)
|
||||
})
|
||||
.map(move |res_acc| (acc_account_id, res_acc))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
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;
|
||||
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,14 +1,16 @@
|
||||
use anyhow::Result;
|
||||
use clap::{CommandFactory, Parser};
|
||||
use clap::{CommandFactory as _, Parser as _};
|
||||
use tokio::runtime::Builder;
|
||||
use wallet::{Args, execute_continious_run, execute_subcommand};
|
||||
use wallet::cli::{Args, OverCommand, execute_continuous_run, execute_setup, execute_subcommand};
|
||||
|
||||
pub const NUM_THREADS: usize = 2;
|
||||
|
||||
// TODO #169: We have sample configs for sequencer, but not for wallet
|
||||
// TODO #168: Why it requires config as a directory? Maybe better to deduce directory from config
|
||||
// file path? TODO #172: Why it requires config as env var while sequencer_runner accepts as
|
||||
// argument? TODO #171: Running pinata doesn't give output about transaction hash and etc.
|
||||
// file path?
|
||||
// TODO #172: Why it requires config as env var while sequencer_runner accepts as
|
||||
// argument?
|
||||
// TODO #171: Running pinata doesn't give output about transaction hash and etc.
|
||||
fn main() -> Result<()> {
|
||||
let runtime = Builder::new_multi_thread()
|
||||
.worker_threads(NUM_THREADS)
|
||||
@ -21,16 +23,20 @@ fn main() -> Result<()> {
|
||||
env_logger::init();
|
||||
|
||||
runtime.block_on(async move {
|
||||
if let Some(command) = args.command {
|
||||
// TODO: It should return error, not panic
|
||||
execute_subcommand(command).await.unwrap();
|
||||
} else if args.continious_run {
|
||||
execute_continious_run().await.unwrap();
|
||||
if let Some(over_command) = args.command {
|
||||
match over_command {
|
||||
OverCommand::Command(command) => {
|
||||
let _output = execute_subcommand(command).await?;
|
||||
Ok(())
|
||||
}
|
||||
OverCommand::Setup { password } => execute_setup(password).await,
|
||||
}
|
||||
} else if args.continuous_run {
|
||||
execute_continuous_run().await
|
||||
} else {
|
||||
let help = Args::command().render_long_help();
|
||||
println!("{help}");
|
||||
Ok(())
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
212
wallet/src/privacy_preserving_tx.rs
Normal file
212
wallet/src/privacy_preserving_tx.rs
Normal file
@ -0,0 +1,212 @@
|
||||
use common::error::ExecutionFailureKind;
|
||||
use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder;
|
||||
use nssa::{AccountId, PrivateKey};
|
||||
use nssa_core::{
|
||||
MembershipProof, NullifierPublicKey, NullifierSecretKey, SharedSecretKey,
|
||||
account::{AccountWithMetadata, Nonce},
|
||||
encryption::{EphemeralPublicKey, IncomingViewingPublicKey},
|
||||
};
|
||||
|
||||
use crate::WalletCore;
|
||||
|
||||
pub enum PrivacyPreservingAccount {
|
||||
Public(AccountId),
|
||||
PrivateOwned(AccountId),
|
||||
PrivateForeign {
|
||||
npk: NullifierPublicKey,
|
||||
ipk: IncomingViewingPublicKey,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct PrivateAccountKeys {
|
||||
pub npk: NullifierPublicKey,
|
||||
pub ssk: SharedSecretKey,
|
||||
pub ipk: IncomingViewingPublicKey,
|
||||
pub epk: EphemeralPublicKey,
|
||||
}
|
||||
|
||||
enum State {
|
||||
Public {
|
||||
account: AccountWithMetadata,
|
||||
sk: Option<PrivateKey>,
|
||||
},
|
||||
Private(AccountPreparedData),
|
||||
}
|
||||
|
||||
pub struct AccountManager {
|
||||
states: Vec<State>,
|
||||
visibility_mask: Vec<u8>,
|
||||
}
|
||||
|
||||
impl AccountManager {
|
||||
pub async fn new(
|
||||
wallet: &WalletCore,
|
||||
accounts: Vec<PrivacyPreservingAccount>,
|
||||
) -> Result<Self, ExecutionFailureKind> {
|
||||
let mut pre_states = Vec::with_capacity(accounts.len());
|
||||
let mut visibility_mask = Vec::with_capacity(accounts.len());
|
||||
|
||||
for account in accounts {
|
||||
let (state, mask) = match account {
|
||||
PrivacyPreservingAccount::Public(account_id) => {
|
||||
let acc = wallet
|
||||
.get_account_public(account_id)
|
||||
.await
|
||||
.map_err(|_| ExecutionFailureKind::KeyNotFoundError)?;
|
||||
|
||||
let sk = wallet.get_account_public_signing_key(&account_id).cloned();
|
||||
let account = AccountWithMetadata::new(acc.clone(), sk.is_some(), account_id);
|
||||
|
||||
(State::Public { account, sk }, 0)
|
||||
}
|
||||
PrivacyPreservingAccount::PrivateOwned(account_id) => {
|
||||
let pre = private_acc_preparation(wallet, account_id).await?;
|
||||
let mask = if pre.auth_acc.is_authorized { 1 } else { 2 };
|
||||
|
||||
(State::Private(pre), mask)
|
||||
}
|
||||
PrivacyPreservingAccount::PrivateForeign { npk, ipk } => {
|
||||
let acc = nssa_core::account::Account::default();
|
||||
let auth_acc = AccountWithMetadata::new(acc, false, &npk);
|
||||
let pre = AccountPreparedData {
|
||||
nsk: None,
|
||||
npk,
|
||||
ipk,
|
||||
auth_acc,
|
||||
proof: None,
|
||||
};
|
||||
|
||||
(State::Private(pre), 2)
|
||||
}
|
||||
};
|
||||
|
||||
pre_states.push(state);
|
||||
visibility_mask.push(mask);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
states: pre_states,
|
||||
visibility_mask,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn pre_states(&self) -> Vec<AccountWithMetadata> {
|
||||
self.states
|
||||
.iter()
|
||||
.map(|state| match state {
|
||||
State::Public { account, .. } => account.clone(),
|
||||
State::Private(pre) => pre.auth_acc.clone(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn visibility_mask(&self) -> &[u8] {
|
||||
&self.visibility_mask
|
||||
}
|
||||
|
||||
pub fn public_account_nonces(&self) -> Vec<Nonce> {
|
||||
self.states
|
||||
.iter()
|
||||
.filter_map(|state| match state {
|
||||
State::Public { account, sk } => sk.as_ref().map(|_| account.account.nonce),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn private_account_keys(&self) -> Vec<PrivateAccountKeys> {
|
||||
self.states
|
||||
.iter()
|
||||
.filter_map(|state| match state {
|
||||
State::Private(pre) => {
|
||||
let eph_holder = EphemeralKeyHolder::new(&pre.npk);
|
||||
|
||||
Some(PrivateAccountKeys {
|
||||
npk: pre.npk.clone(),
|
||||
ssk: eph_holder.calculate_shared_secret_sender(&pre.ipk),
|
||||
ipk: pre.ipk.clone(),
|
||||
epk: eph_holder.generate_ephemeral_public_key(),
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn private_account_auth(&self) -> Vec<(NullifierSecretKey, MembershipProof)> {
|
||||
self.states
|
||||
.iter()
|
||||
.filter_map(|state| match state {
|
||||
State::Private(pre) => Some((pre.nsk?, pre.proof.clone()?)),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn public_account_ids(&self) -> Vec<AccountId> {
|
||||
self.states
|
||||
.iter()
|
||||
.filter_map(|state| match state {
|
||||
State::Public { account, .. } => Some(account.account_id),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn witness_signing_keys(&self) -> Vec<&PrivateKey> {
|
||||
self.states
|
||||
.iter()
|
||||
.filter_map(|state| match state {
|
||||
State::Public { sk, .. } => sk.as_ref(),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
struct AccountPreparedData {
|
||||
nsk: Option<NullifierSecretKey>,
|
||||
npk: NullifierPublicKey,
|
||||
ipk: IncomingViewingPublicKey,
|
||||
auth_acc: AccountWithMetadata,
|
||||
proof: Option<MembershipProof>,
|
||||
}
|
||||
|
||||
async fn private_acc_preparation(
|
||||
wallet: &WalletCore,
|
||||
account_id: AccountId,
|
||||
) -> Result<AccountPreparedData, ExecutionFailureKind> {
|
||||
let Some((from_keys, from_acc)) = wallet
|
||||
.storage
|
||||
.user_data
|
||||
.get_private_account(&account_id)
|
||||
.cloned()
|
||||
else {
|
||||
return Err(ExecutionFailureKind::KeyNotFoundError);
|
||||
};
|
||||
|
||||
let mut nsk = Some(from_keys.private_key_holder.nullifier_secret_key);
|
||||
|
||||
let from_npk = from_keys.nullifer_public_key;
|
||||
let from_ipk = from_keys.incoming_viewing_public_key;
|
||||
|
||||
// TODO: Remove this unwrap, error types must be compatible
|
||||
let proof = wallet
|
||||
.check_private_account_initialized(&account_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if proof.is_none() {
|
||||
nsk = None;
|
||||
}
|
||||
|
||||
let sender_pre = AccountWithMetadata::new(from_acc.clone(), proof.is_some(), &from_npk);
|
||||
|
||||
Ok(AccountPreparedData {
|
||||
nsk,
|
||||
npk: from_npk,
|
||||
ipk: from_ipk,
|
||||
auth_acc: sender_pre,
|
||||
proof,
|
||||
})
|
||||
}
|
||||
6
wallet/src/program_facades/mod.rs
Normal file
6
wallet/src/program_facades/mod.rs
Normal file
@ -0,0 +1,6 @@
|
||||
//! This module contains [`WalletCore`](crate::WalletCore) facades for interacting with various
|
||||
//! on-chain programs.
|
||||
|
||||
pub mod native_token_transfer;
|
||||
pub mod pinata;
|
||||
pub mod token;
|
||||
@ -0,0 +1,35 @@
|
||||
use common::{error::ExecutionFailureKind, rpc_primitives::requests::SendTxResponse};
|
||||
use nssa::AccountId;
|
||||
|
||||
use super::{NativeTokenTransfer, auth_transfer_preparation};
|
||||
use crate::PrivacyPreservingAccount;
|
||||
|
||||
impl NativeTokenTransfer<'_> {
|
||||
pub async fn send_deshielded_transfer(
|
||||
&self,
|
||||
from: AccountId,
|
||||
to: AccountId,
|
||||
balance_to_move: u128,
|
||||
) -> Result<(SendTxResponse, nssa_core::SharedSecretKey), ExecutionFailureKind> {
|
||||
let (instruction_data, program, tx_pre_check) = auth_transfer_preparation(balance_to_move);
|
||||
|
||||
self.0
|
||||
.send_privacy_preserving_tx_with_pre_check(
|
||||
vec![
|
||||
PrivacyPreservingAccount::PrivateOwned(from),
|
||||
PrivacyPreservingAccount::Public(to),
|
||||
],
|
||||
&instruction_data,
|
||||
&program,
|
||||
tx_pre_check,
|
||||
)
|
||||
.await
|
||||
.map(|(resp, secrets)| {
|
||||
let first = secrets
|
||||
.into_iter()
|
||||
.next()
|
||||
.expect("expected sender's secret");
|
||||
(resp, first)
|
||||
})
|
||||
}
|
||||
}
|
||||
33
wallet/src/program_facades/native_token_transfer/mod.rs
Normal file
33
wallet/src/program_facades/native_token_transfer/mod.rs
Normal file
@ -0,0 +1,33 @@
|
||||
use common::error::ExecutionFailureKind;
|
||||
use nssa::{Account, program::Program};
|
||||
use nssa_core::program::InstructionData;
|
||||
|
||||
use crate::WalletCore;
|
||||
|
||||
pub mod deshielded;
|
||||
pub mod private;
|
||||
pub mod public;
|
||||
pub mod shielded;
|
||||
|
||||
pub struct NativeTokenTransfer<'w>(pub &'w WalletCore);
|
||||
|
||||
fn auth_transfer_preparation(
|
||||
balance_to_move: u128,
|
||||
) -> (
|
||||
InstructionData,
|
||||
Program,
|
||||
impl FnOnce(&[&Account]) -> Result<(), ExecutionFailureKind>,
|
||||
) {
|
||||
let instruction_data = Program::serialize_instruction(balance_to_move).unwrap();
|
||||
let program = Program::authenticated_transfer_program();
|
||||
let tx_pre_check = move |accounts: &[&Account]| {
|
||||
let from = accounts[0];
|
||||
if from.balance >= balance_to_move {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ExecutionFailureKind::InsufficientFundsError)
|
||||
}
|
||||
};
|
||||
|
||||
(instruction_data, program, tx_pre_check)
|
||||
}
|
||||
89
wallet/src/program_facades/native_token_transfer/private.rs
Normal file
89
wallet/src/program_facades/native_token_transfer/private.rs
Normal file
@ -0,0 +1,89 @@
|
||||
use std::vec;
|
||||
|
||||
use common::{error::ExecutionFailureKind, rpc_primitives::requests::SendTxResponse};
|
||||
use nssa::{AccountId, program::Program};
|
||||
use nssa_core::{NullifierPublicKey, SharedSecretKey, encryption::IncomingViewingPublicKey};
|
||||
|
||||
use super::{NativeTokenTransfer, auth_transfer_preparation};
|
||||
use crate::PrivacyPreservingAccount;
|
||||
|
||||
impl NativeTokenTransfer<'_> {
|
||||
pub async fn register_account_private(
|
||||
&self,
|
||||
from: AccountId,
|
||||
) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> {
|
||||
let instruction: u128 = 0;
|
||||
|
||||
self.0
|
||||
.send_privacy_preserving_tx_with_pre_check(
|
||||
vec![PrivacyPreservingAccount::PrivateOwned(from)],
|
||||
&Program::serialize_instruction(instruction).unwrap(),
|
||||
&Program::authenticated_transfer_program(),
|
||||
|_| Ok(()),
|
||||
)
|
||||
.await
|
||||
.map(|(resp, secrets)| {
|
||||
let mut secrets_iter = secrets.into_iter();
|
||||
let first = secrets_iter.next().expect("expected sender's secret");
|
||||
(resp, first)
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn send_private_transfer_to_outer_account(
|
||||
&self,
|
||||
from: AccountId,
|
||||
to_npk: NullifierPublicKey,
|
||||
to_ipk: IncomingViewingPublicKey,
|
||||
balance_to_move: u128,
|
||||
) -> Result<(SendTxResponse, [SharedSecretKey; 2]), ExecutionFailureKind> {
|
||||
let (instruction_data, program, tx_pre_check) = auth_transfer_preparation(balance_to_move);
|
||||
|
||||
self.0
|
||||
.send_privacy_preserving_tx_with_pre_check(
|
||||
vec![
|
||||
PrivacyPreservingAccount::PrivateOwned(from),
|
||||
PrivacyPreservingAccount::PrivateForeign {
|
||||
npk: to_npk,
|
||||
ipk: to_ipk,
|
||||
},
|
||||
],
|
||||
&instruction_data,
|
||||
&program,
|
||||
tx_pre_check,
|
||||
)
|
||||
.await
|
||||
.map(|(resp, secrets)| {
|
||||
let mut secrets_iter = secrets.into_iter();
|
||||
let first = secrets_iter.next().expect("expected sender's secret");
|
||||
let second = secrets_iter.next().expect("expected receiver's secret");
|
||||
(resp, [first, second])
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn send_private_transfer_to_owned_account(
|
||||
&self,
|
||||
from: AccountId,
|
||||
to: AccountId,
|
||||
balance_to_move: u128,
|
||||
) -> Result<(SendTxResponse, [SharedSecretKey; 2]), ExecutionFailureKind> {
|
||||
let (instruction_data, program, tx_pre_check) = auth_transfer_preparation(balance_to_move);
|
||||
|
||||
self.0
|
||||
.send_privacy_preserving_tx_with_pre_check(
|
||||
vec![
|
||||
PrivacyPreservingAccount::PrivateOwned(from),
|
||||
PrivacyPreservingAccount::PrivateOwned(to),
|
||||
],
|
||||
&instruction_data,
|
||||
&program,
|
||||
tx_pre_check,
|
||||
)
|
||||
.await
|
||||
.map(|(resp, secrets)| {
|
||||
let mut secrets_iter = secrets.into_iter();
|
||||
let first = secrets_iter.next().expect("expected sender's secret");
|
||||
let second = secrets_iter.next().expect("expected receiver's secret");
|
||||
(resp, [first, second])
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1,25 +1,25 @@
|
||||
use common::{error::ExecutionFailureKind, sequencer_client::json::SendTxResponse};
|
||||
use common::{error::ExecutionFailureKind, rpc_primitives::requests::SendTxResponse};
|
||||
use nssa::{
|
||||
AccountId, PublicTransaction,
|
||||
program::Program,
|
||||
public_transaction::{Message, WitnessSet},
|
||||
};
|
||||
|
||||
use crate::WalletCore;
|
||||
use super::NativeTokenTransfer;
|
||||
|
||||
impl WalletCore {
|
||||
pub async fn send_public_native_token_transfer(
|
||||
impl NativeTokenTransfer<'_> {
|
||||
pub async fn send_public_transfer(
|
||||
&self,
|
||||
from: AccountId,
|
||||
to: AccountId,
|
||||
balance_to_move: u128,
|
||||
) -> Result<SendTxResponse, ExecutionFailureKind> {
|
||||
let Ok(balance) = self.get_account_balance(from).await else {
|
||||
let Ok(balance) = self.0.get_account_balance(from).await else {
|
||||
return Err(ExecutionFailureKind::SequencerError);
|
||||
};
|
||||
|
||||
if balance >= balance_to_move {
|
||||
let Ok(nonces) = self.get_accounts_nonces(vec![from]).await else {
|
||||
let Ok(nonces) = self.0.get_accounts_nonces(vec![from]).await else {
|
||||
return Err(ExecutionFailureKind::SequencerError);
|
||||
};
|
||||
|
||||
@ -28,7 +28,7 @@ impl WalletCore {
|
||||
let message =
|
||||
Message::try_new(program_id, account_ids, nonces, balance_to_move).unwrap();
|
||||
|
||||
let signing_key = self.storage.user_data.get_pub_account_signing_key(&from);
|
||||
let signing_key = self.0.storage.user_data.get_pub_account_signing_key(&from);
|
||||
|
||||
let Some(signing_key) = signing_key else {
|
||||
return Err(ExecutionFailureKind::KeyNotFoundError);
|
||||
@ -38,17 +38,17 @@ impl WalletCore {
|
||||
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
Ok(self.sequencer_client.send_tx_public(tx).await?)
|
||||
Ok(self.0.sequencer_client.send_tx_public(tx).await?)
|
||||
} else {
|
||||
Err(ExecutionFailureKind::InsufficientFundsError)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn register_account_under_authenticated_transfers_programs(
|
||||
pub async fn register_account(
|
||||
&self,
|
||||
from: AccountId,
|
||||
) -> Result<SendTxResponse, ExecutionFailureKind> {
|
||||
let Ok(nonces) = self.get_accounts_nonces(vec![from]).await else {
|
||||
let Ok(nonces) = self.0.get_accounts_nonces(vec![from]).await else {
|
||||
return Err(ExecutionFailureKind::SequencerError);
|
||||
};
|
||||
|
||||
@ -57,7 +57,7 @@ impl WalletCore {
|
||||
let program_id = Program::authenticated_transfer_program().id();
|
||||
let message = Message::try_new(program_id, account_ids, nonces, instruction).unwrap();
|
||||
|
||||
let signing_key = self.storage.user_data.get_pub_account_signing_key(&from);
|
||||
let signing_key = self.0.storage.user_data.get_pub_account_signing_key(&from);
|
||||
|
||||
let Some(signing_key) = signing_key else {
|
||||
return Err(ExecutionFailureKind::KeyNotFoundError);
|
||||
@ -67,6 +67,6 @@ impl WalletCore {
|
||||
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
Ok(self.sequencer_client.send_tx_public(tx).await?)
|
||||
Ok(self.0.sequencer_client.send_tx_public(tx).await?)
|
||||
}
|
||||
}
|
||||
68
wallet/src/program_facades/native_token_transfer/shielded.rs
Normal file
68
wallet/src/program_facades/native_token_transfer/shielded.rs
Normal file
@ -0,0 +1,68 @@
|
||||
use common::{error::ExecutionFailureKind, rpc_primitives::requests::SendTxResponse};
|
||||
use nssa::AccountId;
|
||||
use nssa_core::{NullifierPublicKey, SharedSecretKey, encryption::IncomingViewingPublicKey};
|
||||
|
||||
use super::{NativeTokenTransfer, auth_transfer_preparation};
|
||||
use crate::PrivacyPreservingAccount;
|
||||
|
||||
impl NativeTokenTransfer<'_> {
|
||||
pub async fn send_shielded_transfer(
|
||||
&self,
|
||||
from: AccountId,
|
||||
to: AccountId,
|
||||
balance_to_move: u128,
|
||||
) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> {
|
||||
let (instruction_data, program, tx_pre_check) = auth_transfer_preparation(balance_to_move);
|
||||
|
||||
self.0
|
||||
.send_privacy_preserving_tx_with_pre_check(
|
||||
vec![
|
||||
PrivacyPreservingAccount::Public(from),
|
||||
PrivacyPreservingAccount::PrivateOwned(to),
|
||||
],
|
||||
&instruction_data,
|
||||
&program,
|
||||
tx_pre_check,
|
||||
)
|
||||
.await
|
||||
.map(|(resp, secrets)| {
|
||||
let first = secrets
|
||||
.into_iter()
|
||||
.next()
|
||||
.expect("expected sender's secret");
|
||||
(resp, first)
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn send_shielded_transfer_to_outer_account(
|
||||
&self,
|
||||
from: AccountId,
|
||||
to_npk: NullifierPublicKey,
|
||||
to_ipk: IncomingViewingPublicKey,
|
||||
balance_to_move: u128,
|
||||
) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> {
|
||||
let (instruction_data, program, tx_pre_check) = auth_transfer_preparation(balance_to_move);
|
||||
|
||||
self.0
|
||||
.send_privacy_preserving_tx_with_pre_check(
|
||||
vec![
|
||||
PrivacyPreservingAccount::Public(from),
|
||||
PrivacyPreservingAccount::PrivateForeign {
|
||||
npk: to_npk,
|
||||
ipk: to_ipk,
|
||||
},
|
||||
],
|
||||
&instruction_data,
|
||||
&program,
|
||||
tx_pre_check,
|
||||
)
|
||||
.await
|
||||
.map(|(resp, secrets)| {
|
||||
let first = secrets
|
||||
.into_iter()
|
||||
.next()
|
||||
.expect("expected sender's secret");
|
||||
(resp, first)
|
||||
})
|
||||
}
|
||||
}
|
||||
52
wallet/src/program_facades/pinata.rs
Normal file
52
wallet/src/program_facades/pinata.rs
Normal file
@ -0,0 +1,52 @@
|
||||
use common::{error::ExecutionFailureKind, rpc_primitives::requests::SendTxResponse};
|
||||
use nssa::AccountId;
|
||||
use nssa_core::SharedSecretKey;
|
||||
|
||||
use crate::{PrivacyPreservingAccount, WalletCore};
|
||||
|
||||
pub struct Pinata<'w>(pub &'w WalletCore);
|
||||
|
||||
impl Pinata<'_> {
|
||||
pub async fn claim(
|
||||
&self,
|
||||
pinata_account_id: AccountId,
|
||||
winner_account_id: AccountId,
|
||||
solution: u128,
|
||||
) -> Result<SendTxResponse, ExecutionFailureKind> {
|
||||
let account_ids = vec![pinata_account_id, winner_account_id];
|
||||
let program_id = nssa::program::Program::pinata().id();
|
||||
let message =
|
||||
nssa::public_transaction::Message::try_new(program_id, account_ids, vec![], solution)
|
||||
.unwrap();
|
||||
|
||||
let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = nssa::PublicTransaction::new(message, witness_set);
|
||||
|
||||
Ok(self.0.sequencer_client.send_tx_public(tx).await?)
|
||||
}
|
||||
|
||||
pub async fn claim_private_owned_account(
|
||||
&self,
|
||||
pinata_account_id: AccountId,
|
||||
winner_account_id: AccountId,
|
||||
solution: u128,
|
||||
) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> {
|
||||
self.0
|
||||
.send_privacy_preserving_tx(
|
||||
vec![
|
||||
PrivacyPreservingAccount::Public(pinata_account_id),
|
||||
PrivacyPreservingAccount::PrivateOwned(winner_account_id),
|
||||
],
|
||||
&nssa::program::Program::serialize_instruction(solution).unwrap(),
|
||||
&nssa::program::Program::pinata(),
|
||||
)
|
||||
.await
|
||||
.map(|(resp, secrets)| {
|
||||
let first = secrets
|
||||
.into_iter()
|
||||
.next()
|
||||
.expect("expected recipient's secret");
|
||||
(resp, first)
|
||||
})
|
||||
}
|
||||
}
|
||||
275
wallet/src/program_facades/token.rs
Normal file
275
wallet/src/program_facades/token.rs
Normal file
@ -0,0 +1,275 @@
|
||||
use common::{error::ExecutionFailureKind, rpc_primitives::requests::SendTxResponse};
|
||||
use nssa::{AccountId, program::Program};
|
||||
use nssa_core::{
|
||||
NullifierPublicKey, SharedSecretKey, encryption::IncomingViewingPublicKey,
|
||||
program::InstructionData,
|
||||
};
|
||||
|
||||
use crate::{PrivacyPreservingAccount, WalletCore};
|
||||
|
||||
pub struct Token<'w>(pub &'w WalletCore);
|
||||
|
||||
impl Token<'_> {
|
||||
pub async fn send_new_definition(
|
||||
&self,
|
||||
definition_account_id: AccountId,
|
||||
supply_account_id: AccountId,
|
||||
name: [u8; 6],
|
||||
total_supply: u128,
|
||||
) -> Result<SendTxResponse, ExecutionFailureKind> {
|
||||
let account_ids = vec![definition_account_id, supply_account_id];
|
||||
let program_id = nssa::program::Program::token().id();
|
||||
// Instruction must be: [0x00 || total_supply (little-endian 16 bytes) || name (6 bytes)]
|
||||
let mut instruction = [0; 23];
|
||||
instruction[1..17].copy_from_slice(&total_supply.to_le_bytes());
|
||||
instruction[17..].copy_from_slice(&name);
|
||||
let message = nssa::public_transaction::Message::try_new(
|
||||
program_id,
|
||||
account_ids,
|
||||
vec![],
|
||||
instruction,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
|
||||
let tx = nssa::PublicTransaction::new(message, witness_set);
|
||||
|
||||
Ok(self.0.sequencer_client.send_tx_public(tx).await?)
|
||||
}
|
||||
|
||||
pub async fn send_new_definition_private_owned(
|
||||
&self,
|
||||
definition_account_id: AccountId,
|
||||
supply_account_id: AccountId,
|
||||
name: [u8; 6],
|
||||
total_supply: u128,
|
||||
) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> {
|
||||
let (instruction_data, program) = token_program_preparation_definition(name, total_supply);
|
||||
|
||||
self.0
|
||||
.send_privacy_preserving_tx(
|
||||
vec![
|
||||
PrivacyPreservingAccount::Public(definition_account_id),
|
||||
PrivacyPreservingAccount::PrivateOwned(supply_account_id),
|
||||
],
|
||||
&instruction_data,
|
||||
&program,
|
||||
)
|
||||
.await
|
||||
.map(|(resp, secrets)| {
|
||||
let first = secrets
|
||||
.into_iter()
|
||||
.next()
|
||||
.expect("expected recipient's secret");
|
||||
(resp, first)
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn send_transfer_transaction(
|
||||
&self,
|
||||
sender_account_id: AccountId,
|
||||
recipient_account_id: AccountId,
|
||||
amount: u128,
|
||||
) -> Result<SendTxResponse, ExecutionFailureKind> {
|
||||
let account_ids = vec![sender_account_id, recipient_account_id];
|
||||
let program_id = nssa::program::Program::token().id();
|
||||
// Instruction must be: [0x01 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 ||
|
||||
// 0x00 || 0x00 || 0x00].
|
||||
let mut instruction = [0; 23];
|
||||
instruction[0] = 0x01;
|
||||
instruction[1..17].copy_from_slice(&amount.to_le_bytes());
|
||||
let Ok(nonces) = self.0.get_accounts_nonces(vec![sender_account_id]).await else {
|
||||
return Err(ExecutionFailureKind::SequencerError);
|
||||
};
|
||||
let message = nssa::public_transaction::Message::try_new(
|
||||
program_id,
|
||||
account_ids,
|
||||
nonces,
|
||||
instruction,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let Some(signing_key) = self
|
||||
.0
|
||||
.storage
|
||||
.user_data
|
||||
.get_pub_account_signing_key(&sender_account_id)
|
||||
else {
|
||||
return Err(ExecutionFailureKind::KeyNotFoundError);
|
||||
};
|
||||
let witness_set =
|
||||
nssa::public_transaction::WitnessSet::for_message(&message, &[signing_key]);
|
||||
|
||||
let tx = nssa::PublicTransaction::new(message, witness_set);
|
||||
|
||||
Ok(self.0.sequencer_client.send_tx_public(tx).await?)
|
||||
}
|
||||
|
||||
pub async fn send_transfer_transaction_private_owned_account(
|
||||
&self,
|
||||
sender_account_id: AccountId,
|
||||
recipient_account_id: AccountId,
|
||||
amount: u128,
|
||||
) -> Result<(SendTxResponse, [SharedSecretKey; 2]), ExecutionFailureKind> {
|
||||
let (instruction_data, program) = token_program_preparation_transfer(amount);
|
||||
|
||||
self.0
|
||||
.send_privacy_preserving_tx(
|
||||
vec![
|
||||
PrivacyPreservingAccount::PrivateOwned(sender_account_id),
|
||||
PrivacyPreservingAccount::PrivateOwned(recipient_account_id),
|
||||
],
|
||||
&instruction_data,
|
||||
&program,
|
||||
)
|
||||
.await
|
||||
.map(|(resp, secrets)| {
|
||||
let mut iter = secrets.into_iter();
|
||||
let first = iter.next().expect("expected sender's secret");
|
||||
let second = iter.next().expect("expected recipient's secret");
|
||||
(resp, [first, second])
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn send_transfer_transaction_private_foreign_account(
|
||||
&self,
|
||||
sender_account_id: AccountId,
|
||||
recipient_npk: NullifierPublicKey,
|
||||
recipient_ipk: IncomingViewingPublicKey,
|
||||
amount: u128,
|
||||
) -> Result<(SendTxResponse, [SharedSecretKey; 2]), ExecutionFailureKind> {
|
||||
let (instruction_data, program) = token_program_preparation_transfer(amount);
|
||||
|
||||
self.0
|
||||
.send_privacy_preserving_tx(
|
||||
vec![
|
||||
PrivacyPreservingAccount::PrivateOwned(sender_account_id),
|
||||
PrivacyPreservingAccount::PrivateForeign {
|
||||
npk: recipient_npk,
|
||||
ipk: recipient_ipk,
|
||||
},
|
||||
],
|
||||
&instruction_data,
|
||||
&program,
|
||||
)
|
||||
.await
|
||||
.map(|(resp, secrets)| {
|
||||
let mut iter = secrets.into_iter();
|
||||
let first = iter.next().expect("expected sender's secret");
|
||||
let second = iter.next().expect("expected recipient's secret");
|
||||
(resp, [first, second])
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn send_transfer_transaction_deshielded(
|
||||
&self,
|
||||
sender_account_id: AccountId,
|
||||
recipient_account_id: AccountId,
|
||||
amount: u128,
|
||||
) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> {
|
||||
let (instruction_data, program) = token_program_preparation_transfer(amount);
|
||||
|
||||
self.0
|
||||
.send_privacy_preserving_tx(
|
||||
vec![
|
||||
PrivacyPreservingAccount::PrivateOwned(sender_account_id),
|
||||
PrivacyPreservingAccount::Public(recipient_account_id),
|
||||
],
|
||||
&instruction_data,
|
||||
&program,
|
||||
)
|
||||
.await
|
||||
.map(|(resp, secrets)| {
|
||||
let first = secrets
|
||||
.into_iter()
|
||||
.next()
|
||||
.expect("expected sender's secret");
|
||||
(resp, first)
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn send_transfer_transaction_shielded_owned_account(
|
||||
&self,
|
||||
sender_account_id: AccountId,
|
||||
recipient_account_id: AccountId,
|
||||
amount: u128,
|
||||
) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> {
|
||||
let (instruction_data, program) = token_program_preparation_transfer(amount);
|
||||
|
||||
self.0
|
||||
.send_privacy_preserving_tx(
|
||||
vec![
|
||||
PrivacyPreservingAccount::Public(sender_account_id),
|
||||
PrivacyPreservingAccount::PrivateOwned(recipient_account_id),
|
||||
],
|
||||
&instruction_data,
|
||||
&program,
|
||||
)
|
||||
.await
|
||||
.map(|(resp, secrets)| {
|
||||
let first = secrets
|
||||
.into_iter()
|
||||
.next()
|
||||
.expect("expected recipient's secret");
|
||||
(resp, first)
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn send_transfer_transaction_shielded_foreign_account(
|
||||
&self,
|
||||
sender_account_id: AccountId,
|
||||
recipient_npk: NullifierPublicKey,
|
||||
recipient_ipk: IncomingViewingPublicKey,
|
||||
amount: u128,
|
||||
) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> {
|
||||
let (instruction_data, program) = token_program_preparation_transfer(amount);
|
||||
|
||||
self.0
|
||||
.send_privacy_preserving_tx(
|
||||
vec![
|
||||
PrivacyPreservingAccount::Public(sender_account_id),
|
||||
PrivacyPreservingAccount::PrivateForeign {
|
||||
npk: recipient_npk,
|
||||
ipk: recipient_ipk,
|
||||
},
|
||||
],
|
||||
&instruction_data,
|
||||
&program,
|
||||
)
|
||||
.await
|
||||
.map(|(resp, secrets)| {
|
||||
let first = secrets
|
||||
.into_iter()
|
||||
.next()
|
||||
.expect("expected recipient's secret");
|
||||
(resp, first)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn token_program_preparation_transfer(amount: u128) -> (InstructionData, Program) {
|
||||
// Instruction must be: [0x01 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 ||
|
||||
// 0x00 || 0x00 || 0x00].
|
||||
let mut instruction = [0; 23];
|
||||
instruction[0] = 0x01;
|
||||
instruction[1..17].copy_from_slice(&amount.to_le_bytes());
|
||||
let instruction_data = Program::serialize_instruction(instruction).unwrap();
|
||||
let program = Program::token();
|
||||
|
||||
(instruction_data, program)
|
||||
}
|
||||
|
||||
fn token_program_preparation_definition(
|
||||
name: [u8; 6],
|
||||
total_supply: u128,
|
||||
) -> (InstructionData, Program) {
|
||||
// Instruction must be: [0x00 || total_supply (little-endian 16 bytes) || name (6 bytes)]
|
||||
let mut instruction = [0; 23];
|
||||
instruction[1..17].copy_from_slice(&total_supply.to_le_bytes());
|
||||
instruction[17..].copy_from_slice(&name);
|
||||
let instruction_data = Program::serialize_instruction(instruction).unwrap();
|
||||
let program = Program::token();
|
||||
|
||||
(instruction_data, program)
|
||||
}
|
||||
@ -1,278 +0,0 @@
|
||||
use common::{error::ExecutionFailureKind, sequencer_client::json::SendTxResponse};
|
||||
use nssa::{Account, AccountId, program::Program};
|
||||
use nssa_core::{
|
||||
MembershipProof, NullifierPublicKey, SharedSecretKey, encryption::IncomingViewingPublicKey,
|
||||
program::InstructionData,
|
||||
};
|
||||
|
||||
use crate::WalletCore;
|
||||
|
||||
impl WalletCore {
|
||||
pub fn token_program_preparation_transfer(
|
||||
amount: u128,
|
||||
) -> (
|
||||
InstructionData,
|
||||
Program,
|
||||
impl FnOnce(&Account, &Account) -> Result<(), ExecutionFailureKind>,
|
||||
) {
|
||||
// Instruction must be: [0x01 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 ||
|
||||
// 0x00 || 0x00 || 0x00].
|
||||
let mut instruction = [0; 23];
|
||||
instruction[0] = 0x01;
|
||||
instruction[1..17].copy_from_slice(&amount.to_le_bytes());
|
||||
let instruction_data = Program::serialize_instruction(instruction).unwrap();
|
||||
let program = Program::token();
|
||||
let tx_pre_check = |_: &Account, _: &Account| Ok(());
|
||||
|
||||
(instruction_data, program, tx_pre_check)
|
||||
}
|
||||
|
||||
pub fn token_program_preparation_definition(
|
||||
name: [u8; 6],
|
||||
total_supply: u128,
|
||||
) -> (
|
||||
InstructionData,
|
||||
Program,
|
||||
impl FnOnce(&Account, &Account) -> Result<(), ExecutionFailureKind>,
|
||||
) {
|
||||
// Instruction must be: [0x00 || total_supply (little-endian 16 bytes) || name (6 bytes)]
|
||||
let mut instruction = [0; 23];
|
||||
instruction[1..17].copy_from_slice(&total_supply.to_le_bytes());
|
||||
instruction[17..].copy_from_slice(&name);
|
||||
let instruction_data = Program::serialize_instruction(instruction).unwrap();
|
||||
let program = Program::token();
|
||||
let tx_pre_check = |_: &Account, _: &Account| Ok(());
|
||||
|
||||
(instruction_data, program, tx_pre_check)
|
||||
}
|
||||
|
||||
pub async fn send_new_token_definition(
|
||||
&self,
|
||||
definition_account_id: AccountId,
|
||||
supply_account_id: AccountId,
|
||||
name: [u8; 6],
|
||||
total_supply: u128,
|
||||
) -> Result<SendTxResponse, ExecutionFailureKind> {
|
||||
let account_ids = vec![definition_account_id, supply_account_id];
|
||||
let program_id = nssa::program::Program::token().id();
|
||||
// Instruction must be: [0x00 || total_supply (little-endian 16 bytes) || name (6 bytes)]
|
||||
let mut instruction = [0; 23];
|
||||
instruction[1..17].copy_from_slice(&total_supply.to_le_bytes());
|
||||
instruction[17..].copy_from_slice(&name);
|
||||
let message = nssa::public_transaction::Message::try_new(
|
||||
program_id,
|
||||
account_ids,
|
||||
vec![],
|
||||
instruction,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
|
||||
let tx = nssa::PublicTransaction::new(message, witness_set);
|
||||
|
||||
Ok(self.sequencer_client.send_tx_public(tx).await?)
|
||||
}
|
||||
|
||||
pub async fn send_new_token_definition_private_owned(
|
||||
&self,
|
||||
definition_account_id: AccountId,
|
||||
supply_account_id: AccountId,
|
||||
name: [u8; 6],
|
||||
total_supply: u128,
|
||||
) -> Result<(SendTxResponse, [SharedSecretKey; 1]), ExecutionFailureKind> {
|
||||
let (instruction_data, program, tx_pre_check) =
|
||||
WalletCore::token_program_preparation_definition(name, total_supply);
|
||||
|
||||
// Kind of non-obvious naming
|
||||
// Basically this funtion is called because authentication mask is [0, 2]
|
||||
self.shielded_two_accs_receiver_uninit(
|
||||
definition_account_id,
|
||||
supply_account_id,
|
||||
instruction_data,
|
||||
tx_pre_check,
|
||||
program,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn send_transfer_token_transaction(
|
||||
&self,
|
||||
sender_account_id: AccountId,
|
||||
recipient_account_id: AccountId,
|
||||
amount: u128,
|
||||
) -> Result<SendTxResponse, ExecutionFailureKind> {
|
||||
let account_ids = vec![sender_account_id, recipient_account_id];
|
||||
let program_id = nssa::program::Program::token().id();
|
||||
// Instruction must be: [0x01 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 ||
|
||||
// 0x00 || 0x00 || 0x00].
|
||||
let mut instruction = [0; 23];
|
||||
instruction[0] = 0x01;
|
||||
instruction[1..17].copy_from_slice(&amount.to_le_bytes());
|
||||
let Ok(nonces) = self.get_accounts_nonces(vec![sender_account_id]).await else {
|
||||
return Err(ExecutionFailureKind::SequencerError);
|
||||
};
|
||||
let message = nssa::public_transaction::Message::try_new(
|
||||
program_id,
|
||||
account_ids,
|
||||
nonces,
|
||||
instruction,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let Some(signing_key) = self
|
||||
.storage
|
||||
.user_data
|
||||
.get_pub_account_signing_key(&sender_account_id)
|
||||
else {
|
||||
return Err(ExecutionFailureKind::KeyNotFoundError);
|
||||
};
|
||||
let witness_set =
|
||||
nssa::public_transaction::WitnessSet::for_message(&message, &[signing_key]);
|
||||
|
||||
let tx = nssa::PublicTransaction::new(message, witness_set);
|
||||
|
||||
Ok(self.sequencer_client.send_tx_public(tx).await?)
|
||||
}
|
||||
|
||||
pub async fn send_transfer_token_transaction_private_owned_account_already_initialized(
|
||||
&self,
|
||||
sender_account_id: AccountId,
|
||||
recipient_account_id: AccountId,
|
||||
amount: u128,
|
||||
recipient_proof: MembershipProof,
|
||||
) -> Result<(SendTxResponse, [SharedSecretKey; 2]), ExecutionFailureKind> {
|
||||
let (instruction_data, program, tx_pre_check) =
|
||||
WalletCore::token_program_preparation_transfer(amount);
|
||||
|
||||
self.private_tx_two_accs_all_init(
|
||||
sender_account_id,
|
||||
recipient_account_id,
|
||||
instruction_data,
|
||||
tx_pre_check,
|
||||
program,
|
||||
recipient_proof,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn send_transfer_token_transaction_private_owned_account_not_initialized(
|
||||
&self,
|
||||
sender_account_id: AccountId,
|
||||
recipient_account_id: AccountId,
|
||||
amount: u128,
|
||||
) -> Result<(SendTxResponse, [SharedSecretKey; 2]), ExecutionFailureKind> {
|
||||
let (instruction_data, program, tx_pre_check) =
|
||||
WalletCore::token_program_preparation_transfer(amount);
|
||||
|
||||
self.private_tx_two_accs_receiver_uninit(
|
||||
sender_account_id,
|
||||
recipient_account_id,
|
||||
instruction_data,
|
||||
tx_pre_check,
|
||||
program,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn send_transfer_token_transaction_private_foreign_account(
|
||||
&self,
|
||||
sender_account_id: AccountId,
|
||||
recipient_npk: NullifierPublicKey,
|
||||
recipient_ipk: IncomingViewingPublicKey,
|
||||
amount: u128,
|
||||
) -> Result<(SendTxResponse, [SharedSecretKey; 2]), ExecutionFailureKind> {
|
||||
let (instruction_data, program, tx_pre_check) =
|
||||
WalletCore::token_program_preparation_transfer(amount);
|
||||
|
||||
self.private_tx_two_accs_receiver_outer(
|
||||
sender_account_id,
|
||||
recipient_npk,
|
||||
recipient_ipk,
|
||||
instruction_data,
|
||||
tx_pre_check,
|
||||
program,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn send_transfer_token_transaction_deshielded(
|
||||
&self,
|
||||
sender_account_id: AccountId,
|
||||
recipient_account_id: AccountId,
|
||||
amount: u128,
|
||||
) -> Result<(SendTxResponse, [SharedSecretKey; 1]), ExecutionFailureKind> {
|
||||
let (instruction_data, program, tx_pre_check) =
|
||||
WalletCore::token_program_preparation_transfer(amount);
|
||||
|
||||
self.deshielded_tx_two_accs(
|
||||
sender_account_id,
|
||||
recipient_account_id,
|
||||
instruction_data,
|
||||
tx_pre_check,
|
||||
program,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn send_transfer_token_transaction_shielded_owned_account_already_initialized(
|
||||
&self,
|
||||
sender_account_id: AccountId,
|
||||
recipient_account_id: AccountId,
|
||||
amount: u128,
|
||||
recipient_proof: MembershipProof,
|
||||
) -> Result<(SendTxResponse, [SharedSecretKey; 1]), ExecutionFailureKind> {
|
||||
let (instruction_data, program, tx_pre_check) =
|
||||
WalletCore::token_program_preparation_transfer(amount);
|
||||
|
||||
self.shielded_two_accs_all_init(
|
||||
sender_account_id,
|
||||
recipient_account_id,
|
||||
instruction_data,
|
||||
tx_pre_check,
|
||||
program,
|
||||
recipient_proof,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn send_transfer_token_transaction_shielded_owned_account_not_initialized(
|
||||
&self,
|
||||
sender_account_id: AccountId,
|
||||
recipient_account_id: AccountId,
|
||||
amount: u128,
|
||||
) -> Result<(SendTxResponse, [SharedSecretKey; 1]), ExecutionFailureKind> {
|
||||
let (instruction_data, program, tx_pre_check) =
|
||||
WalletCore::token_program_preparation_transfer(amount);
|
||||
|
||||
self.shielded_two_accs_receiver_uninit(
|
||||
sender_account_id,
|
||||
recipient_account_id,
|
||||
instruction_data,
|
||||
tx_pre_check,
|
||||
program,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn send_transfer_token_transaction_shielded_foreign_account(
|
||||
&self,
|
||||
sender_account_id: AccountId,
|
||||
recipient_npk: NullifierPublicKey,
|
||||
recipient_ipk: IncomingViewingPublicKey,
|
||||
amount: u128,
|
||||
) -> Result<SendTxResponse, ExecutionFailureKind> {
|
||||
let (instruction_data, program, tx_pre_check) =
|
||||
WalletCore::token_program_preparation_transfer(amount);
|
||||
|
||||
self.shielded_two_accs_receiver_outer(
|
||||
sender_account_id,
|
||||
recipient_npk,
|
||||
recipient_ipk,
|
||||
instruction_data,
|
||||
tx_pre_check,
|
||||
program,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
use common::{error::ExecutionFailureKind, sequencer_client::json::SendTxResponse};
|
||||
use nssa::AccountId;
|
||||
|
||||
use crate::WalletCore;
|
||||
|
||||
impl WalletCore {
|
||||
pub async fn send_deshielded_native_token_transfer(
|
||||
&self,
|
||||
from: AccountId,
|
||||
to: AccountId,
|
||||
balance_to_move: u128,
|
||||
) -> Result<(SendTxResponse, [nssa_core::SharedSecretKey; 1]), ExecutionFailureKind> {
|
||||
let (instruction_data, program, tx_pre_check) =
|
||||
WalletCore::auth_transfer_preparation(balance_to_move);
|
||||
|
||||
self.deshielded_tx_two_accs(from, to, instruction_data, tx_pre_check, program)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
use common::error::ExecutionFailureKind;
|
||||
use nssa::{Account, program::Program};
|
||||
use nssa_core::program::InstructionData;
|
||||
|
||||
use crate::WalletCore;
|
||||
|
||||
pub mod deshielded;
|
||||
pub mod private;
|
||||
pub mod public;
|
||||
pub mod shielded;
|
||||
|
||||
impl WalletCore {
|
||||
pub fn auth_transfer_preparation(
|
||||
balance_to_move: u128,
|
||||
) -> (
|
||||
InstructionData,
|
||||
Program,
|
||||
impl FnOnce(&Account, &Account) -> Result<(), ExecutionFailureKind>,
|
||||
) {
|
||||
let instruction_data = Program::serialize_instruction(balance_to_move).unwrap();
|
||||
let program = Program::authenticated_transfer_program();
|
||||
let tx_pre_check = move |from: &Account, _: &Account| {
|
||||
if from.balance >= balance_to_move {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ExecutionFailureKind::InsufficientFundsError)
|
||||
}
|
||||
};
|
||||
|
||||
(instruction_data, program, tx_pre_check)
|
||||
}
|
||||
}
|
||||
@ -1,64 +0,0 @@
|
||||
use common::{error::ExecutionFailureKind, sequencer_client::json::SendTxResponse};
|
||||
use nssa::AccountId;
|
||||
use nssa_core::{
|
||||
MembershipProof, NullifierPublicKey, SharedSecretKey, encryption::IncomingViewingPublicKey,
|
||||
};
|
||||
|
||||
use crate::WalletCore;
|
||||
|
||||
impl WalletCore {
|
||||
pub async fn send_private_native_token_transfer_outer_account(
|
||||
&self,
|
||||
from: AccountId,
|
||||
to_npk: NullifierPublicKey,
|
||||
to_ipk: IncomingViewingPublicKey,
|
||||
balance_to_move: u128,
|
||||
) -> Result<(SendTxResponse, [SharedSecretKey; 2]), ExecutionFailureKind> {
|
||||
let (instruction_data, program, tx_pre_check) =
|
||||
WalletCore::auth_transfer_preparation(balance_to_move);
|
||||
|
||||
self.private_tx_two_accs_receiver_outer(
|
||||
from,
|
||||
to_npk,
|
||||
to_ipk,
|
||||
instruction_data,
|
||||
tx_pre_check,
|
||||
program,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn send_private_native_token_transfer_owned_account_not_initialized(
|
||||
&self,
|
||||
from: AccountId,
|
||||
to: AccountId,
|
||||
balance_to_move: u128,
|
||||
) -> Result<(SendTxResponse, [SharedSecretKey; 2]), ExecutionFailureKind> {
|
||||
let (instruction_data, program, tx_pre_check) =
|
||||
WalletCore::auth_transfer_preparation(balance_to_move);
|
||||
|
||||
self.private_tx_two_accs_receiver_uninit(from, to, instruction_data, tx_pre_check, program)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn send_private_native_token_transfer_owned_account_already_initialized(
|
||||
&self,
|
||||
from: AccountId,
|
||||
to: AccountId,
|
||||
balance_to_move: u128,
|
||||
to_proof: MembershipProof,
|
||||
) -> Result<(SendTxResponse, [SharedSecretKey; 2]), ExecutionFailureKind> {
|
||||
let (instruction_data, program, tx_pre_check) =
|
||||
WalletCore::auth_transfer_preparation(balance_to_move);
|
||||
|
||||
self.private_tx_two_accs_all_init(
|
||||
from,
|
||||
to,
|
||||
instruction_data,
|
||||
tx_pre_check,
|
||||
program,
|
||||
to_proof,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@ -1,57 +0,0 @@
|
||||
use common::{error::ExecutionFailureKind, sequencer_client::json::SendTxResponse};
|
||||
use nssa::AccountId;
|
||||
use nssa_core::{
|
||||
MembershipProof, NullifierPublicKey, SharedSecretKey, encryption::IncomingViewingPublicKey,
|
||||
};
|
||||
|
||||
use crate::WalletCore;
|
||||
|
||||
impl WalletCore {
|
||||
pub async fn send_shielded_native_token_transfer_already_initialized(
|
||||
&self,
|
||||
from: AccountId,
|
||||
to: AccountId,
|
||||
balance_to_move: u128,
|
||||
to_proof: MembershipProof,
|
||||
) -> Result<(SendTxResponse, [SharedSecretKey; 1]), ExecutionFailureKind> {
|
||||
let (instruction_data, program, tx_pre_check) =
|
||||
WalletCore::auth_transfer_preparation(balance_to_move);
|
||||
|
||||
self.shielded_two_accs_all_init(from, to, instruction_data, tx_pre_check, program, to_proof)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn send_shielded_native_token_transfer_not_initialized(
|
||||
&self,
|
||||
from: AccountId,
|
||||
to: AccountId,
|
||||
balance_to_move: u128,
|
||||
) -> Result<(SendTxResponse, [SharedSecretKey; 1]), ExecutionFailureKind> {
|
||||
let (instruction_data, program, tx_pre_check) =
|
||||
WalletCore::auth_transfer_preparation(balance_to_move);
|
||||
|
||||
self.shielded_two_accs_receiver_uninit(from, to, instruction_data, tx_pre_check, program)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn send_shielded_native_token_transfer_outer_account(
|
||||
&self,
|
||||
from: AccountId,
|
||||
to_npk: NullifierPublicKey,
|
||||
to_ipk: IncomingViewingPublicKey,
|
||||
balance_to_move: u128,
|
||||
) -> Result<SendTxResponse, ExecutionFailureKind> {
|
||||
let (instruction_data, program, tx_pre_check) =
|
||||
WalletCore::auth_transfer_preparation(balance_to_move);
|
||||
|
||||
self.shielded_two_accs_receiver_outer(
|
||||
from,
|
||||
to_npk,
|
||||
to_ipk,
|
||||
instruction_data,
|
||||
tx_pre_check,
|
||||
program,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user