mirror of
https://github.com/logos-blockchain/lssa.git
synced 2026-05-13 05:09:27 +00:00
Merge branch 'main' into schouhy/diversify-private-pdas-by-identifier
This commit is contained in:
commit
927c24de68
7
Cargo.lock
generated
7
Cargo.lock
generated
@ -3797,10 +3797,16 @@ dependencies = [
|
||||
name = "indexer_ffi"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cbindgen",
|
||||
"indexer_service",
|
||||
"indexer_service_protocol",
|
||||
"indexer_service_rpc",
|
||||
"jsonrpsee",
|
||||
"log",
|
||||
"nssa",
|
||||
"tokio",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3916,6 +3922,7 @@ dependencies = [
|
||||
"hex",
|
||||
"indexer_ffi",
|
||||
"indexer_service",
|
||||
"indexer_service_protocol",
|
||||
"indexer_service_rpc",
|
||||
"jsonrpsee",
|
||||
"key_protocol",
|
||||
|
||||
@ -37,7 +37,7 @@ members = [
|
||||
"examples/program_deployment/methods",
|
||||
"examples/program_deployment/methods/guest",
|
||||
"testnet_initial_state",
|
||||
"indexer_ffi",
|
||||
"indexer/ffi",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
@ -57,7 +57,7 @@ indexer_service_protocol = { path = "indexer/service/protocol" }
|
||||
indexer_service_rpc = { path = "indexer/service/rpc" }
|
||||
wallet = { path = "wallet" }
|
||||
wallet-ffi = { path = "wallet-ffi", default-features = false }
|
||||
indexer_ffi = { path = "indexer_ffi" }
|
||||
indexer_ffi = { path = "indexer/ffi" }
|
||||
clock_core = { path = "programs/clock/core" }
|
||||
token_core = { path = "programs/token/core" }
|
||||
token_program = { path = "programs/token" }
|
||||
|
||||
BIN
artifacts/test_program_methods/auth_transfer_pda_proxy.bin
Normal file
BIN
artifacts/test_program_methods/auth_transfer_pda_proxy.bin
Normal file
Binary file not shown.
BIN
artifacts/test_program_methods/auth_transfer_proxy.bin
Normal file
BIN
artifacts/test_program_methods/auth_transfer_proxy.bin
Normal file
Binary file not shown.
@ -5,9 +5,16 @@ name = "indexer_ffi"
|
||||
version = "0.1.0"
|
||||
|
||||
[dependencies]
|
||||
nssa.workspace = true
|
||||
indexer_service.workspace = true
|
||||
indexer_service_rpc = { workspace = true, features = ["client"] }
|
||||
indexer_service_protocol.workspace = true
|
||||
|
||||
url.workspace = true
|
||||
log = { workspace = true }
|
||||
tokio = { features = ["rt-multi-thread"], workspace = true }
|
||||
jsonrpsee.workspace = true
|
||||
anyhow.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
cbindgen = "0.29"
|
||||
685
indexer/ffi/indexer_ffi.h
Normal file
685
indexer/ffi/indexer_ffi.h
Normal file
@ -0,0 +1,685 @@
|
||||
#include <stdarg.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
typedef enum OperationStatus {
|
||||
Ok = 0,
|
||||
NullPointer = 1,
|
||||
InitializationError = 2,
|
||||
ClientError = 3,
|
||||
} OperationStatus;
|
||||
|
||||
typedef enum FfiTransactionKind {
|
||||
Public = 0,
|
||||
Private,
|
||||
ProgramDeploy,
|
||||
} FfiTransactionKind;
|
||||
|
||||
typedef enum FfiBedrockStatus {
|
||||
Pending = 0,
|
||||
Safe,
|
||||
Finalized,
|
||||
} FfiBedrockStatus;
|
||||
|
||||
typedef struct IndexerServiceFFI {
|
||||
void *indexer_handle;
|
||||
void *runtime;
|
||||
void *indexer_client;
|
||||
} IndexerServiceFFI;
|
||||
|
||||
/**
|
||||
* Simple wrapper around a pointer to a value or an error.
|
||||
*
|
||||
* Pointer is not guaranteed. You should check the error field before
|
||||
* dereferencing the pointer.
|
||||
*/
|
||||
typedef struct PointerResult_IndexerServiceFFI__OperationStatus {
|
||||
struct IndexerServiceFFI *value;
|
||||
enum OperationStatus error;
|
||||
} PointerResult_IndexerServiceFFI__OperationStatus;
|
||||
|
||||
typedef struct PointerResult_IndexerServiceFFI__OperationStatus InitializedIndexerServiceFFIResult;
|
||||
|
||||
/**
|
||||
* Simple wrapper around a pointer to a value or an error.
|
||||
*
|
||||
* Pointer is not guaranteed. You should check the error field before
|
||||
* dereferencing the pointer.
|
||||
*/
|
||||
typedef struct PointerResult_u64__OperationStatus {
|
||||
uint64_t *value;
|
||||
enum OperationStatus error;
|
||||
} PointerResult_u64__OperationStatus;
|
||||
|
||||
typedef uint64_t FfiBlockId;
|
||||
|
||||
/**
|
||||
* 32-byte array type for `AccountId`, keys, hashes, etc.
|
||||
*/
|
||||
typedef struct FfiBytes32 {
|
||||
uint8_t data[32];
|
||||
} FfiBytes32;
|
||||
|
||||
typedef struct FfiBytes32 FfiHashType;
|
||||
|
||||
typedef uint64_t FfiTimestamp;
|
||||
|
||||
/**
|
||||
* 64-byte array type for signatures, etc.
|
||||
*/
|
||||
typedef struct FfiBytes64 {
|
||||
uint8_t data[64];
|
||||
} FfiBytes64;
|
||||
|
||||
typedef struct FfiBytes64 FfiSignature;
|
||||
|
||||
typedef struct FfiBlockHeader {
|
||||
FfiBlockId block_id;
|
||||
FfiHashType prev_block_hash;
|
||||
FfiHashType hash;
|
||||
FfiTimestamp timestamp;
|
||||
FfiSignature signature;
|
||||
} FfiBlockHeader;
|
||||
|
||||
/**
|
||||
* Program ID - 8 u32 values (32 bytes total).
|
||||
*/
|
||||
typedef struct FfiProgramId {
|
||||
uint32_t data[8];
|
||||
} FfiProgramId;
|
||||
|
||||
typedef struct FfiBytes32 FfiAccountId;
|
||||
|
||||
typedef struct FfiVec_FfiAccountId {
|
||||
FfiAccountId *entries;
|
||||
uintptr_t len;
|
||||
uintptr_t capacity;
|
||||
} FfiVec_FfiAccountId;
|
||||
|
||||
typedef struct FfiVec_FfiAccountId FfiAccountIdList;
|
||||
|
||||
/**
|
||||
* U128 - 16 bytes little endian.
|
||||
*/
|
||||
typedef struct FfiU128 {
|
||||
uint8_t data[16];
|
||||
} FfiU128;
|
||||
|
||||
typedef struct FfiU128 FfiNonce;
|
||||
|
||||
typedef struct FfiVec_FfiNonce {
|
||||
FfiNonce *entries;
|
||||
uintptr_t len;
|
||||
uintptr_t capacity;
|
||||
} FfiVec_FfiNonce;
|
||||
|
||||
typedef struct FfiVec_FfiNonce FfiNonceList;
|
||||
|
||||
typedef struct FfiVec_u32 {
|
||||
uint32_t *entries;
|
||||
uintptr_t len;
|
||||
uintptr_t capacity;
|
||||
} FfiVec_u32;
|
||||
|
||||
typedef struct FfiVec_u32 FfiInstructionDataList;
|
||||
|
||||
typedef struct FfiPublicMessage {
|
||||
struct FfiProgramId program_id;
|
||||
FfiAccountIdList account_ids;
|
||||
FfiNonceList nonces;
|
||||
FfiInstructionDataList instruction_data;
|
||||
} FfiPublicMessage;
|
||||
|
||||
typedef struct FfiBytes32 FfiPublicKey;
|
||||
|
||||
typedef struct FfiSignaturePubKeyEntry {
|
||||
FfiSignature signature;
|
||||
FfiPublicKey public_key;
|
||||
} FfiSignaturePubKeyEntry;
|
||||
|
||||
typedef struct FfiVec_FfiSignaturePubKeyEntry {
|
||||
struct FfiSignaturePubKeyEntry *entries;
|
||||
uintptr_t len;
|
||||
uintptr_t capacity;
|
||||
} FfiVec_FfiSignaturePubKeyEntry;
|
||||
|
||||
typedef struct FfiVec_FfiSignaturePubKeyEntry FfiSignaturePubKeyList;
|
||||
|
||||
typedef struct FfiPublicTransactionBody {
|
||||
FfiHashType hash;
|
||||
struct FfiPublicMessage message;
|
||||
FfiSignaturePubKeyList witness_set;
|
||||
} FfiPublicTransactionBody;
|
||||
|
||||
/**
|
||||
* Account data structure - C-compatible version of nssa Account.
|
||||
*
|
||||
* Note: `balance` and `nonce` are u128 values represented as little-endian
|
||||
* byte arrays since C doesn't have native u128 support.
|
||||
*/
|
||||
typedef struct FfiAccount {
|
||||
struct FfiProgramId program_owner;
|
||||
/**
|
||||
* Balance as little-endian [u8; 16].
|
||||
*/
|
||||
struct FfiU128 balance;
|
||||
/**
|
||||
* Pointer to account data bytes.
|
||||
*/
|
||||
uint8_t *data;
|
||||
/**
|
||||
* Length of account data.
|
||||
*/
|
||||
uintptr_t data_len;
|
||||
/**
|
||||
* Capacity of account data.
|
||||
*/
|
||||
uintptr_t data_cap;
|
||||
/**
|
||||
* Nonce as little-endian [u8; 16].
|
||||
*/
|
||||
struct FfiU128 nonce;
|
||||
} FfiAccount;
|
||||
|
||||
typedef struct FfiVec_FfiAccount {
|
||||
struct FfiAccount *entries;
|
||||
uintptr_t len;
|
||||
uintptr_t capacity;
|
||||
} FfiVec_FfiAccount;
|
||||
|
||||
typedef struct FfiVec_FfiAccount FfiAccountList;
|
||||
|
||||
typedef struct FfiVec_u8 {
|
||||
uint8_t *entries;
|
||||
uintptr_t len;
|
||||
uintptr_t capacity;
|
||||
} FfiVec_u8;
|
||||
|
||||
typedef struct FfiVec_u8 FfiVecU8;
|
||||
|
||||
typedef struct FfiEncryptedAccountData {
|
||||
FfiVecU8 ciphertext;
|
||||
FfiVecU8 epk;
|
||||
uint8_t view_tag;
|
||||
} FfiEncryptedAccountData;
|
||||
|
||||
typedef struct FfiVec_FfiEncryptedAccountData {
|
||||
struct FfiEncryptedAccountData *entries;
|
||||
uintptr_t len;
|
||||
uintptr_t capacity;
|
||||
} FfiVec_FfiEncryptedAccountData;
|
||||
|
||||
typedef struct FfiVec_FfiEncryptedAccountData FfiEncryptedAccountDataList;
|
||||
|
||||
typedef struct FfiVec_FfiBytes32 {
|
||||
struct FfiBytes32 *entries;
|
||||
uintptr_t len;
|
||||
uintptr_t capacity;
|
||||
} FfiVec_FfiBytes32;
|
||||
|
||||
typedef struct FfiVec_FfiBytes32 FfiVecBytes32;
|
||||
|
||||
typedef struct FfiNullifierCommitmentSet {
|
||||
struct FfiBytes32 nullifier;
|
||||
struct FfiBytes32 commitment_set_digest;
|
||||
} FfiNullifierCommitmentSet;
|
||||
|
||||
typedef struct FfiVec_FfiNullifierCommitmentSet {
|
||||
struct FfiNullifierCommitmentSet *entries;
|
||||
uintptr_t len;
|
||||
uintptr_t capacity;
|
||||
} FfiVec_FfiNullifierCommitmentSet;
|
||||
|
||||
typedef struct FfiVec_FfiNullifierCommitmentSet FfiNullifierCommitmentSetList;
|
||||
|
||||
typedef struct FfiPrivacyPreservingMessage {
|
||||
FfiAccountIdList public_account_ids;
|
||||
FfiNonceList nonces;
|
||||
FfiAccountList public_post_states;
|
||||
FfiEncryptedAccountDataList encrypted_private_post_states;
|
||||
FfiVecBytes32 new_commitments;
|
||||
FfiNullifierCommitmentSetList new_nullifiers;
|
||||
uint64_t block_validity_window[2];
|
||||
uint64_t timestamp_validity_window[2];
|
||||
} FfiPrivacyPreservingMessage;
|
||||
|
||||
typedef FfiVecU8 FfiProof;
|
||||
|
||||
typedef struct FfiPrivateTransactionBody {
|
||||
FfiHashType hash;
|
||||
struct FfiPrivacyPreservingMessage message;
|
||||
FfiSignaturePubKeyList witness_set;
|
||||
FfiProof proof;
|
||||
} FfiPrivateTransactionBody;
|
||||
|
||||
typedef FfiVecU8 FfiProgramDeploymentMessage;
|
||||
|
||||
typedef struct FfiProgramDeploymentTransactionBody {
|
||||
FfiHashType hash;
|
||||
FfiProgramDeploymentMessage message;
|
||||
} FfiProgramDeploymentTransactionBody;
|
||||
|
||||
typedef struct FfiTransactionBody {
|
||||
struct FfiPublicTransactionBody *public_body;
|
||||
struct FfiPrivateTransactionBody *private_body;
|
||||
struct FfiProgramDeploymentTransactionBody *program_deployment_body;
|
||||
} FfiTransactionBody;
|
||||
|
||||
typedef struct FfiTransaction {
|
||||
struct FfiTransactionBody body;
|
||||
enum FfiTransactionKind kind;
|
||||
} FfiTransaction;
|
||||
|
||||
typedef struct FfiVec_FfiTransaction {
|
||||
struct FfiTransaction *entries;
|
||||
uintptr_t len;
|
||||
uintptr_t capacity;
|
||||
} FfiVec_FfiTransaction;
|
||||
|
||||
typedef struct FfiVec_FfiTransaction FfiBlockBody;
|
||||
|
||||
typedef struct FfiBytes32 FfiMsgId;
|
||||
|
||||
typedef struct FfiBlock {
|
||||
struct FfiBlockHeader header;
|
||||
FfiBlockBody body;
|
||||
enum FfiBedrockStatus bedrock_status;
|
||||
FfiMsgId bedrock_parent_id;
|
||||
} FfiBlock;
|
||||
|
||||
typedef struct FfiOption_FfiBlock {
|
||||
struct FfiBlock *value;
|
||||
bool is_some;
|
||||
} FfiOption_FfiBlock;
|
||||
|
||||
typedef struct FfiOption_FfiBlock FfiBlockOpt;
|
||||
|
||||
/**
|
||||
* Simple wrapper around a pointer to a value or an error.
|
||||
*
|
||||
* Pointer is not guaranteed. You should check the error field before
|
||||
* dereferencing the pointer.
|
||||
*/
|
||||
typedef struct PointerResult_FfiBlockOpt__OperationStatus {
|
||||
FfiBlockOpt *value;
|
||||
enum OperationStatus error;
|
||||
} PointerResult_FfiBlockOpt__OperationStatus;
|
||||
|
||||
/**
|
||||
* Simple wrapper around a pointer to a value or an error.
|
||||
*
|
||||
* Pointer is not guaranteed. You should check the error field before
|
||||
* dereferencing the pointer.
|
||||
*/
|
||||
typedef struct PointerResult_FfiAccount__OperationStatus {
|
||||
struct FfiAccount *value;
|
||||
enum OperationStatus error;
|
||||
} PointerResult_FfiAccount__OperationStatus;
|
||||
|
||||
typedef struct FfiOption_FfiTransaction {
|
||||
struct FfiTransaction *value;
|
||||
bool is_some;
|
||||
} FfiOption_FfiTransaction;
|
||||
|
||||
/**
|
||||
* Simple wrapper around a pointer to a value or an error.
|
||||
*
|
||||
* Pointer is not guaranteed. You should check the error field before
|
||||
* dereferencing the pointer.
|
||||
*/
|
||||
typedef struct PointerResult_FfiOption_FfiTransaction_____OperationStatus {
|
||||
struct FfiOption_FfiTransaction *value;
|
||||
enum OperationStatus error;
|
||||
} PointerResult_FfiOption_FfiTransaction_____OperationStatus;
|
||||
|
||||
typedef struct FfiVec_FfiBlock {
|
||||
struct FfiBlock *entries;
|
||||
uintptr_t len;
|
||||
uintptr_t capacity;
|
||||
} FfiVec_FfiBlock;
|
||||
|
||||
/**
|
||||
* Simple wrapper around a pointer to a value or an error.
|
||||
*
|
||||
* Pointer is not guaranteed. You should check the error field before
|
||||
* dereferencing the pointer.
|
||||
*/
|
||||
typedef struct PointerResult_FfiVec_FfiBlock_____OperationStatus {
|
||||
struct FfiVec_FfiBlock *value;
|
||||
enum OperationStatus error;
|
||||
} PointerResult_FfiVec_FfiBlock_____OperationStatus;
|
||||
|
||||
typedef struct FfiOption_u64 {
|
||||
uint64_t *value;
|
||||
bool is_some;
|
||||
} FfiOption_u64;
|
||||
|
||||
/**
|
||||
* Simple wrapper around a pointer to a value or an error.
|
||||
*
|
||||
* Pointer is not guaranteed. You should check the error field before
|
||||
* dereferencing the pointer.
|
||||
*/
|
||||
typedef struct PointerResult_FfiVec_FfiTransaction_____OperationStatus {
|
||||
struct FfiVec_FfiTransaction *value;
|
||||
enum OperationStatus error;
|
||||
} PointerResult_FfiVec_FfiTransaction_____OperationStatus;
|
||||
|
||||
/**
|
||||
* Creates and starts an indexer based on the provided
|
||||
* configuration file path.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* - `config_path`: A pointer to a string representing the path to the configuration file.
|
||||
* - `port`: Number representing a port, on which indexers RPC will start.
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* An `InitializedIndexerServiceFFIResult` containing either a pointer to the
|
||||
* initialized `IndexerServiceFFI` or an error code.
|
||||
*/
|
||||
InitializedIndexerServiceFFIResult start_indexer(const char *config_path, uint16_t port);
|
||||
|
||||
/**
|
||||
* Stops and frees the resources associated with the given indexer service.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* - `indexer`: A pointer to the `IndexerServiceFFI` instance to be stopped.
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* An `OperationStatus` indicating success or failure.
|
||||
*
|
||||
* # Safety
|
||||
*
|
||||
* The caller must ensure that:
|
||||
* - `indexer` is a valid pointer to a `IndexerServiceFFI` instance
|
||||
* - The `IndexerServiceFFI` instance was created by this library
|
||||
* - The pointer will not be used after this function returns
|
||||
*/
|
||||
enum OperationStatus stop_indexer(struct IndexerServiceFFI *indexer);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* It's up to the caller to pass a proper pointer, if somehow from c/c++ side
|
||||
* this is called with a type which doesn't come from a returned `CString` it
|
||||
* will cause a segfault.
|
||||
*/
|
||||
void free_cstring(char *block);
|
||||
|
||||
/**
|
||||
* Query the last block id from indexer.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried.
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* A `PointerResult<u64, OperationStatus>` indicating success or failure.
|
||||
*
|
||||
* # Safety
|
||||
*
|
||||
* The caller must ensure that:
|
||||
* - `indexer` is a valid pointer to a `IndexerServiceFFI` instance
|
||||
*/
|
||||
struct PointerResult_u64__OperationStatus query_last_block(const struct IndexerServiceFFI *indexer);
|
||||
|
||||
/**
|
||||
* Query the block by id from indexer.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried.
|
||||
* - `block_id`: `u64` number of block id
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* A `PointerResult<FfiBlockOpt, OperationStatus>` indicating success or failure.
|
||||
*
|
||||
* # Safety
|
||||
*
|
||||
* The caller must ensure that:
|
||||
* - `indexer` is a valid pointer to a `IndexerServiceFFI` instance
|
||||
*/
|
||||
struct PointerResult_FfiBlockOpt__OperationStatus query_block(const struct IndexerServiceFFI *indexer,
|
||||
FfiBlockId block_id);
|
||||
|
||||
/**
|
||||
* Query the block by id from indexer.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried.
|
||||
* - `hash`: `FfiHashType` - hash of block
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* A `PointerResult<FfiBlockOpt, OperationStatus>` indicating success or failure.
|
||||
*
|
||||
* # Safety
|
||||
*
|
||||
* The caller must ensure that:
|
||||
* - `indexer` is a valid pointer to a `IndexerServiceFFI` instance
|
||||
*/
|
||||
struct PointerResult_FfiBlockOpt__OperationStatus query_block_by_hash(const struct IndexerServiceFFI *indexer,
|
||||
FfiHashType hash);
|
||||
|
||||
/**
|
||||
* Query the account by id from indexer.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried.
|
||||
* - `account_id`: `FfiAccountId` - id of queried account
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* A `PointerResult<FfiAccount, OperationStatus>` indicating success or failure.
|
||||
*
|
||||
* # Safety
|
||||
*
|
||||
* The caller must ensure that:
|
||||
* - `indexer` is a valid pointer to a `IndexerServiceFFI` instance
|
||||
*/
|
||||
struct PointerResult_FfiAccount__OperationStatus query_account(const struct IndexerServiceFFI *indexer,
|
||||
FfiAccountId account_id);
|
||||
|
||||
/**
|
||||
* Query the trasnaction by hash from indexer.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried.
|
||||
* - `hash`: `FfiHashType` - hash of transaction
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* A `PointerResult<FfiOption<FfiTransaction>, OperationStatus>` indicating success or failure.
|
||||
*
|
||||
* # Safety
|
||||
*
|
||||
* The caller must ensure that:
|
||||
* - `indexer` is a valid pointer to a `IndexerServiceFFI` instance
|
||||
*/
|
||||
struct PointerResult_FfiOption_FfiTransaction_____OperationStatus query_transaction(const struct IndexerServiceFFI *indexer,
|
||||
FfiHashType hash);
|
||||
|
||||
/**
|
||||
* Query the blocks by block range from indexer.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried.
|
||||
* - `before`: `FfiOption<u64>` - end block of query
|
||||
* - `limit`: `u64` - number of blocks to query before `before`
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* A `PointerResult<FfiVec<FfiBlock>, OperationStatus>` indicating success or failure.
|
||||
*
|
||||
* # Safety
|
||||
*
|
||||
* The caller must ensure that:
|
||||
* - `indexer` is a valid pointer to a `IndexerServiceFFI` instance
|
||||
*/
|
||||
struct PointerResult_FfiVec_FfiBlock_____OperationStatus query_block_vec(const struct IndexerServiceFFI *indexer,
|
||||
struct FfiOption_u64 before,
|
||||
uint64_t limit);
|
||||
|
||||
/**
|
||||
* Query the transactions range by account id from indexer.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried.
|
||||
* - `account_id`: `FfiAccountId` - id of queried account
|
||||
* - `offset`: `u64` - first tx id of query
|
||||
* - `limit`: `u64` - number of tx ids to query after `offset`
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* A `PointerResult<FfiVec<FfiBlock>, OperationStatus>` indicating success or failure.
|
||||
*
|
||||
* # Safety
|
||||
*
|
||||
* The caller must ensure that:
|
||||
* - `indexer` is a valid pointer to a `IndexerServiceFFI` instance
|
||||
*/
|
||||
struct PointerResult_FfiVec_FfiTransaction_____OperationStatus query_transactions_by_account(const struct IndexerServiceFFI *indexer,
|
||||
FfiAccountId account_id,
|
||||
uint64_t offset,
|
||||
uint64_t limit);
|
||||
|
||||
/**
|
||||
* Frees the resources associated with the given ffi account.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* - `val`: An instance of `FfiAccount`.
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* void.
|
||||
*
|
||||
* # Safety
|
||||
*
|
||||
* The caller must ensure that:
|
||||
* - `val` is a valid instance of `FfiAccount`.
|
||||
*/
|
||||
void free_ffi_account(struct FfiAccount val);
|
||||
|
||||
/**
|
||||
* Frees the resources associated with the given ffi block.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* - `val`: An instance of `FfiBlock`.
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* void.
|
||||
*
|
||||
* # Safety
|
||||
*
|
||||
* The caller must ensure that:
|
||||
* - `val` is a valid instance of `FfiBlock`.
|
||||
*/
|
||||
void free_ffi_block(struct FfiBlock val);
|
||||
|
||||
/**
|
||||
* Frees the resources associated with the given ffi block option.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* - `val`: An instance of `FfiBlockOpt`.
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* void.
|
||||
*
|
||||
* # Safety
|
||||
*
|
||||
* The caller must ensure that:
|
||||
* - `val` is a valid instance of `FfiBlockOpt`.
|
||||
*/
|
||||
void free_ffi_block_opt(FfiBlockOpt val);
|
||||
|
||||
/**
|
||||
* Frees the resources associated with the given ffi block vector.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* - `val`: An instance of `FfiVec<FfiBlock>`.
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* void.
|
||||
*
|
||||
* # Safety
|
||||
*
|
||||
* The caller must ensure that:
|
||||
* - `val` is a valid instance of `FfiVec<FfiBlock>`.
|
||||
*/
|
||||
void free_ffi_block_vec(struct FfiVec_FfiBlock val);
|
||||
|
||||
/**
|
||||
* Frees the resources associated with the given ffi transaction.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* - `val`: An instance of `FfiTransaction`.
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* void.
|
||||
*
|
||||
* # Safety
|
||||
*
|
||||
* The caller must ensure that:
|
||||
* - `val` is a valid instance of `FfiTransaction`.
|
||||
*/
|
||||
void free_ffi_transaction(struct FfiTransaction val);
|
||||
|
||||
/**
|
||||
* Frees the resources associated with the given ffi transaction option.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* - `val`: An instance of `FfiOption<FfiTransaction>`.
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* void.
|
||||
*
|
||||
* # Safety
|
||||
*
|
||||
* The caller must ensure that:
|
||||
* - `val` is a valid instance of `FfiOption<FfiTransaction>`.
|
||||
*/
|
||||
void free_ffi_transaction_opt(struct FfiOption_FfiTransaction val);
|
||||
|
||||
/**
|
||||
* Frees the resources associated with the given vector of ffi transactions.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* - `val`: An instance of `FfiVec<FfiTransaction>`.
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* void.
|
||||
*
|
||||
* # Safety
|
||||
*
|
||||
* The caller must ensure that:
|
||||
* - `val` is a valid instance of `FfiVec<FfiTransaction>`.
|
||||
*/
|
||||
void free_ffi_transaction_vec(struct FfiVec_FfiTransaction val);
|
||||
|
||||
bool is_ok(const enum OperationStatus *self);
|
||||
|
||||
bool is_error(const enum OperationStatus *self);
|
||||
36
indexer/ffi/src/api/client.rs
Normal file
36
indexer/ffi/src/api/client.rs
Normal file
@ -0,0 +1,36 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use url::Url;
|
||||
|
||||
use crate::OperationStatus;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum UrlProtocol {
|
||||
Http,
|
||||
Ws,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for UrlProtocol {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Http => write!(f, "http"),
|
||||
Self::Ws => write!(f, "ws"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn addr_to_url(protocol: UrlProtocol, addr: SocketAddr) -> Result<Url, OperationStatus> {
|
||||
// Convert 0.0.0.0 to 127.0.0.1 for client connections
|
||||
// When binding to port 0, the server binds to 0.0.0.0:<random_port>
|
||||
// but clients need to connect to 127.0.0.1:<port> to work reliably
|
||||
let url_string = if addr.ip().is_unspecified() {
|
||||
format!("{protocol}://127.0.0.1:{}", addr.port())
|
||||
} else {
|
||||
format!("{protocol}://{addr}")
|
||||
};
|
||||
|
||||
url_string.parse().map_err(|e| {
|
||||
log::error!("Could not parse indexer url: {e}");
|
||||
OperationStatus::InitializationError
|
||||
})
|
||||
}
|
||||
@ -2,7 +2,15 @@ use std::{ffi::c_char, path::PathBuf};
|
||||
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
use crate::{IndexerServiceFFI, api::PointerResult, errors::OperationStatus};
|
||||
use crate::{
|
||||
IndexerServiceFFI,
|
||||
api::{
|
||||
PointerResult,
|
||||
client::{UrlProtocol, addr_to_url},
|
||||
},
|
||||
client::{IndexerClient, IndexerClientTrait as _},
|
||||
errors::OperationStatus,
|
||||
};
|
||||
|
||||
pub type InitializedIndexerServiceFFIResult = PointerResult<IndexerServiceFFI, OperationStatus>;
|
||||
|
||||
@ -67,7 +75,13 @@ fn setup_indexer(
|
||||
OperationStatus::InitializationError
|
||||
})?;
|
||||
|
||||
Ok(IndexerServiceFFI::new(indexer_handle, rt))
|
||||
let indexer_url = addr_to_url(UrlProtocol::Ws, indexer_handle.addr())?;
|
||||
let indexer_client = rt.block_on(IndexerClient::new(&indexer_url)).map_err(|e| {
|
||||
log::error!("Could not start indexer client: {e}");
|
||||
OperationStatus::InitializationError
|
||||
})?;
|
||||
|
||||
Ok(IndexerServiceFFI::new(indexer_handle, rt, indexer_client))
|
||||
}
|
||||
|
||||
/// Stops and frees the resources associated with the given indexer service.
|
||||
@ -1,5 +1,8 @@
|
||||
pub use result::PointerResult;
|
||||
|
||||
pub mod client;
|
||||
pub mod lifecycle;
|
||||
pub mod memory;
|
||||
pub mod query;
|
||||
pub mod result;
|
||||
pub mod types;
|
||||
334
indexer/ffi/src/api/query.rs
Normal file
334
indexer/ffi/src/api/query.rs
Normal file
@ -0,0 +1,334 @@
|
||||
use indexer_service_protocol::{AccountId, HashType};
|
||||
use indexer_service_rpc::RpcClient as _;
|
||||
|
||||
use crate::{
|
||||
IndexerServiceFFI,
|
||||
api::{
|
||||
PointerResult,
|
||||
types::{
|
||||
FfiAccountId, FfiBlockId, FfiHashType, FfiOption, FfiVec,
|
||||
account::FfiAccount,
|
||||
block::{FfiBlock, FfiBlockOpt},
|
||||
transaction::FfiTransaction,
|
||||
},
|
||||
},
|
||||
errors::OperationStatus,
|
||||
};
|
||||
|
||||
/// Query the last block id from indexer.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A `PointerResult<u64, OperationStatus>` indicating success or failure.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller must ensure that:
|
||||
/// - `indexer` is a valid pointer to a `IndexerServiceFFI` instance
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn query_last_block(
|
||||
indexer: *const IndexerServiceFFI,
|
||||
) -> PointerResult<u64, OperationStatus> {
|
||||
if indexer.is_null() {
|
||||
log::error!("Attempted to query a null indexer pointer. This is a bug. Aborting.");
|
||||
return PointerResult::from_error(OperationStatus::NullPointer);
|
||||
}
|
||||
|
||||
let indexer = unsafe { &*indexer };
|
||||
|
||||
let client = unsafe { indexer.client() };
|
||||
let runtime = unsafe { indexer.runtime() };
|
||||
|
||||
runtime
|
||||
.block_on(client.get_last_finalized_block_id())
|
||||
.map_or_else(
|
||||
|_| PointerResult::from_error(OperationStatus::ClientError),
|
||||
PointerResult::from_value,
|
||||
)
|
||||
}
|
||||
|
||||
/// Query the block by id from indexer.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried.
|
||||
/// - `block_id`: `u64` number of block id
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A `PointerResult<FfiBlockOpt, OperationStatus>` indicating success or failure.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller must ensure that:
|
||||
/// - `indexer` is a valid pointer to a `IndexerServiceFFI` instance
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn query_block(
|
||||
indexer: *const IndexerServiceFFI,
|
||||
block_id: FfiBlockId,
|
||||
) -> PointerResult<FfiBlockOpt, OperationStatus> {
|
||||
if indexer.is_null() {
|
||||
log::error!("Attempted to query a null indexer pointer. This is a bug. Aborting.");
|
||||
return PointerResult::from_error(OperationStatus::NullPointer);
|
||||
}
|
||||
|
||||
let indexer = unsafe { &*indexer };
|
||||
|
||||
let client = unsafe { indexer.client() };
|
||||
let runtime = unsafe { indexer.runtime() };
|
||||
|
||||
runtime
|
||||
.block_on(client.get_block_by_id(block_id))
|
||||
.map_or_else(
|
||||
|_| PointerResult::from_error(OperationStatus::ClientError),
|
||||
|block_opt| {
|
||||
let block_ffi = block_opt.map_or_else(FfiBlockOpt::from_none, |block| {
|
||||
FfiBlockOpt::from_value(block.into())
|
||||
});
|
||||
|
||||
PointerResult::from_value(block_ffi)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Query the block by id from indexer.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried.
|
||||
/// - `hash`: `FfiHashType` - hash of block
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A `PointerResult<FfiBlockOpt, OperationStatus>` indicating success or failure.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller must ensure that:
|
||||
/// - `indexer` is a valid pointer to a `IndexerServiceFFI` instance
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn query_block_by_hash(
|
||||
indexer: *const IndexerServiceFFI,
|
||||
hash: FfiHashType,
|
||||
) -> PointerResult<FfiBlockOpt, OperationStatus> {
|
||||
if indexer.is_null() {
|
||||
log::error!("Attempted to query a null indexer pointer. This is a bug. Aborting.");
|
||||
return PointerResult::from_error(OperationStatus::NullPointer);
|
||||
}
|
||||
|
||||
let indexer = unsafe { &*indexer };
|
||||
|
||||
let client = unsafe { indexer.client() };
|
||||
let runtime = unsafe { indexer.runtime() };
|
||||
|
||||
runtime
|
||||
.block_on(client.get_block_by_hash(HashType(hash.data)))
|
||||
.map_or_else(
|
||||
|_| PointerResult::from_error(OperationStatus::ClientError),
|
||||
|block_opt| {
|
||||
let block_ffi = block_opt.map_or_else(FfiBlockOpt::from_none, |block| {
|
||||
FfiBlockOpt::from_value(block.into())
|
||||
});
|
||||
|
||||
PointerResult::from_value(block_ffi)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Query the account by id from indexer.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried.
|
||||
/// - `account_id`: `FfiAccountId` - id of queried account
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A `PointerResult<FfiAccount, OperationStatus>` indicating success or failure.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller must ensure that:
|
||||
/// - `indexer` is a valid pointer to a `IndexerServiceFFI` instance
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn query_account(
|
||||
indexer: *const IndexerServiceFFI,
|
||||
account_id: FfiAccountId,
|
||||
) -> PointerResult<FfiAccount, OperationStatus> {
|
||||
if indexer.is_null() {
|
||||
log::error!("Attempted to query a null indexer pointer. This is a bug. Aborting.");
|
||||
return PointerResult::from_error(OperationStatus::NullPointer);
|
||||
}
|
||||
|
||||
let indexer = unsafe { &*indexer };
|
||||
|
||||
let client = unsafe { indexer.client() };
|
||||
let runtime = unsafe { indexer.runtime() };
|
||||
|
||||
runtime
|
||||
.block_on(client.get_account(AccountId {
|
||||
value: account_id.data,
|
||||
}))
|
||||
.map_or_else(
|
||||
|_| PointerResult::from_error(OperationStatus::ClientError),
|
||||
|acc| {
|
||||
let acc_nssa: nssa::Account =
|
||||
acc.try_into().expect("Source is in blocks, must fit");
|
||||
PointerResult::from_value(acc_nssa.into())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Query the trasnaction by hash from indexer.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried.
|
||||
/// - `hash`: `FfiHashType` - hash of transaction
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A `PointerResult<FfiOption<FfiTransaction>, OperationStatus>` indicating success or failure.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller must ensure that:
|
||||
/// - `indexer` is a valid pointer to a `IndexerServiceFFI` instance
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn query_transaction(
|
||||
indexer: *const IndexerServiceFFI,
|
||||
hash: FfiHashType,
|
||||
) -> PointerResult<FfiOption<FfiTransaction>, OperationStatus> {
|
||||
if indexer.is_null() {
|
||||
log::error!("Attempted to query a null indexer pointer. This is a bug. Aborting.");
|
||||
return PointerResult::from_error(OperationStatus::NullPointer);
|
||||
}
|
||||
|
||||
let indexer = unsafe { &*indexer };
|
||||
|
||||
let client = unsafe { indexer.client() };
|
||||
let runtime = unsafe { indexer.runtime() };
|
||||
|
||||
runtime
|
||||
.block_on(client.get_transaction(HashType(hash.data)))
|
||||
.map_or_else(
|
||||
|_| PointerResult::from_error(OperationStatus::ClientError),
|
||||
|tx_opt| {
|
||||
let tx_ffi = tx_opt.map_or_else(FfiOption::<FfiTransaction>::from_none, |tx| {
|
||||
FfiOption::<FfiTransaction>::from_value(tx.into())
|
||||
});
|
||||
|
||||
PointerResult::from_value(tx_ffi)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Query the blocks by block range from indexer.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried.
|
||||
/// - `before`: `FfiOption<u64>` - end block of query
|
||||
/// - `limit`: `u64` - number of blocks to query before `before`
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A `PointerResult<FfiVec<FfiBlock>, OperationStatus>` indicating success or failure.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller must ensure that:
|
||||
/// - `indexer` is a valid pointer to a `IndexerServiceFFI` instance
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn query_block_vec(
|
||||
indexer: *const IndexerServiceFFI,
|
||||
before: FfiOption<u64>,
|
||||
limit: u64,
|
||||
) -> PointerResult<FfiVec<FfiBlock>, OperationStatus> {
|
||||
if indexer.is_null() {
|
||||
log::error!("Attempted to query a null indexer pointer. This is a bug. Aborting.");
|
||||
return PointerResult::from_error(OperationStatus::NullPointer);
|
||||
}
|
||||
|
||||
let indexer = unsafe { &*indexer };
|
||||
|
||||
let client = unsafe { indexer.client() };
|
||||
let runtime = unsafe { indexer.runtime() };
|
||||
|
||||
let before_std = before.is_some.then(|| unsafe { *before.value });
|
||||
|
||||
runtime
|
||||
.block_on(client.get_blocks(before_std, limit))
|
||||
.map_or_else(
|
||||
|_| PointerResult::from_error(OperationStatus::ClientError),
|
||||
|block_vec| {
|
||||
PointerResult::from_value(
|
||||
block_vec
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Query the transactions range by account id from indexer.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried.
|
||||
/// - `account_id`: `FfiAccountId` - id of queried account
|
||||
/// - `offset`: `u64` - first tx id of query
|
||||
/// - `limit`: `u64` - number of tx ids to query after `offset`
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A `PointerResult<FfiVec<FfiBlock>, OperationStatus>` indicating success or failure.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller must ensure that:
|
||||
/// - `indexer` is a valid pointer to a `IndexerServiceFFI` instance
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn query_transactions_by_account(
|
||||
indexer: *const IndexerServiceFFI,
|
||||
account_id: FfiAccountId,
|
||||
offset: u64,
|
||||
limit: u64,
|
||||
) -> PointerResult<FfiVec<FfiTransaction>, OperationStatus> {
|
||||
if indexer.is_null() {
|
||||
log::error!("Attempted to query a null indexer pointer. This is a bug. Aborting.");
|
||||
return PointerResult::from_error(OperationStatus::NullPointer);
|
||||
}
|
||||
|
||||
let indexer = unsafe { &*indexer };
|
||||
|
||||
let client = unsafe { indexer.client() };
|
||||
let runtime = unsafe { indexer.runtime() };
|
||||
|
||||
runtime
|
||||
.block_on(client.get_transactions_by_account(
|
||||
AccountId {
|
||||
value: account_id.data,
|
||||
},
|
||||
offset,
|
||||
limit,
|
||||
))
|
||||
.map_or_else(
|
||||
|_| PointerResult::from_error(OperationStatus::ClientError),
|
||||
|tx_vec| {
|
||||
PointerResult::from_value(
|
||||
tx_vec
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
119
indexer/ffi/src/api/types/account.rs
Normal file
119
indexer/ffi/src/api/types/account.rs
Normal file
@ -0,0 +1,119 @@
|
||||
use indexer_service_protocol::ProgramId;
|
||||
|
||||
use crate::api::types::{FfiBytes32, FfiProgramId, FfiU128};
|
||||
|
||||
/// Account data structure - C-compatible version of nssa Account.
|
||||
///
|
||||
/// Note: `balance` and `nonce` are u128 values represented as little-endian
|
||||
/// byte arrays since C doesn't have native u128 support.
|
||||
#[repr(C)]
|
||||
pub struct FfiAccount {
|
||||
pub program_owner: FfiProgramId,
|
||||
/// Balance as little-endian [u8; 16].
|
||||
pub balance: FfiU128,
|
||||
/// Pointer to account data bytes.
|
||||
pub data: *mut u8,
|
||||
/// Length of account data.
|
||||
pub data_len: usize,
|
||||
/// Capacity of account data.
|
||||
pub data_cap: usize,
|
||||
/// Nonce as little-endian [u8; 16].
|
||||
pub nonce: FfiU128,
|
||||
}
|
||||
|
||||
// Helper functions to convert between Rust and FFI types
|
||||
|
||||
impl From<&nssa::AccountId> for FfiBytes32 {
|
||||
fn from(id: &nssa::AccountId) -> Self {
|
||||
Self::from_account_id(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<nssa::Account> for FfiAccount {
|
||||
fn from(value: nssa::Account) -> Self {
|
||||
let nssa::Account {
|
||||
program_owner,
|
||||
balance,
|
||||
data,
|
||||
nonce,
|
||||
} = value;
|
||||
|
||||
let (data, data_len, data_cap) = data.into_inner().into_raw_parts();
|
||||
|
||||
let program_owner = FfiProgramId {
|
||||
data: program_owner,
|
||||
};
|
||||
Self {
|
||||
program_owner,
|
||||
balance: balance.into(),
|
||||
data,
|
||||
data_len,
|
||||
data_cap,
|
||||
nonce: nonce.0.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FfiAccount> for indexer_service_protocol::Account {
|
||||
fn from(value: FfiAccount) -> Self {
|
||||
let FfiAccount {
|
||||
program_owner,
|
||||
balance,
|
||||
data,
|
||||
data_cap,
|
||||
data_len,
|
||||
nonce,
|
||||
} = value;
|
||||
|
||||
Self {
|
||||
program_owner: ProgramId(program_owner.data),
|
||||
balance: balance.into(),
|
||||
data: indexer_service_protocol::Data(unsafe {
|
||||
Vec::from_raw_parts(data, data_len, data_cap)
|
||||
}),
|
||||
nonce: nonce.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&FfiAccount> for indexer_service_protocol::Account {
|
||||
fn from(value: &FfiAccount) -> Self {
|
||||
let &FfiAccount {
|
||||
program_owner,
|
||||
balance,
|
||||
data,
|
||||
data_cap,
|
||||
data_len,
|
||||
nonce,
|
||||
} = value;
|
||||
|
||||
Self {
|
||||
program_owner: ProgramId(program_owner.data),
|
||||
balance: balance.into(),
|
||||
data: indexer_service_protocol::Data(unsafe {
|
||||
Vec::from_raw_parts(data, data_len, data_cap)
|
||||
}),
|
||||
nonce: nonce.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Frees the resources associated with the given ffi account.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `val`: An instance of `FfiAccount`.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// void.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller must ensure that:
|
||||
/// - `val` is a valid instance of `FfiAccount`.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn free_ffi_account(val: FfiAccount) {
|
||||
let orig_val: indexer_service_protocol::Account = val.into();
|
||||
drop(orig_val);
|
||||
}
|
||||
199
indexer/ffi/src/api/types/block.rs
Normal file
199
indexer/ffi/src/api/types/block.rs
Normal file
@ -0,0 +1,199 @@
|
||||
use indexer_service_protocol::{
|
||||
BedrockStatus, Block, BlockHeader, HashType, MantleMsgId, Signature,
|
||||
};
|
||||
|
||||
use crate::api::types::{
|
||||
FfiBlockId, FfiHashType, FfiMsgId, FfiOption, FfiSignature, FfiTimestamp, FfiVec,
|
||||
transaction::free_ffi_transaction_vec, vectors::FfiBlockBody,
|
||||
};
|
||||
|
||||
#[repr(C)]
|
||||
pub struct FfiBlock {
|
||||
pub header: FfiBlockHeader,
|
||||
pub body: FfiBlockBody,
|
||||
pub bedrock_status: FfiBedrockStatus,
|
||||
pub bedrock_parent_id: FfiMsgId,
|
||||
}
|
||||
|
||||
impl From<Block> for FfiBlock {
|
||||
fn from(value: Block) -> Self {
|
||||
let Block {
|
||||
header,
|
||||
body,
|
||||
bedrock_status,
|
||||
bedrock_parent_id,
|
||||
} = value;
|
||||
|
||||
Self {
|
||||
header: header.into(),
|
||||
body: body
|
||||
.transactions
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
bedrock_status: bedrock_status.into(),
|
||||
bedrock_parent_id: bedrock_parent_id.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type FfiBlockOpt = FfiOption<FfiBlock>;
|
||||
|
||||
#[repr(C)]
|
||||
pub struct FfiBlockHeader {
|
||||
pub block_id: FfiBlockId,
|
||||
pub prev_block_hash: FfiHashType,
|
||||
pub hash: FfiHashType,
|
||||
pub timestamp: FfiTimestamp,
|
||||
pub signature: FfiSignature,
|
||||
}
|
||||
|
||||
impl From<BlockHeader> for FfiBlockHeader {
|
||||
fn from(value: BlockHeader) -> Self {
|
||||
let BlockHeader {
|
||||
block_id,
|
||||
prev_block_hash,
|
||||
hash,
|
||||
timestamp,
|
||||
signature,
|
||||
} = value;
|
||||
|
||||
Self {
|
||||
block_id,
|
||||
prev_block_hash: prev_block_hash.into(),
|
||||
hash: hash.into(),
|
||||
timestamp,
|
||||
signature: signature.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub enum FfiBedrockStatus {
|
||||
Pending = 0x0,
|
||||
Safe,
|
||||
Finalized,
|
||||
}
|
||||
|
||||
impl From<BedrockStatus> for FfiBedrockStatus {
|
||||
fn from(value: BedrockStatus) -> Self {
|
||||
match value {
|
||||
BedrockStatus::Finalized => Self::Finalized,
|
||||
BedrockStatus::Pending => Self::Pending,
|
||||
BedrockStatus::Safe => Self::Safe,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FfiBedrockStatus> for BedrockStatus {
|
||||
fn from(value: FfiBedrockStatus) -> Self {
|
||||
match value {
|
||||
FfiBedrockStatus::Finalized => Self::Finalized,
|
||||
FfiBedrockStatus::Pending => Self::Pending,
|
||||
FfiBedrockStatus::Safe => Self::Safe,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Frees the resources associated with the given ffi block.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `val`: An instance of `FfiBlock`.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// void.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller must ensure that:
|
||||
/// - `val` is a valid instance of `FfiBlock`.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn free_ffi_block(val: FfiBlock) {
|
||||
// We don't really need all the casts, but just in case
|
||||
// All except `ffi_tx_ffi_vec` is Copy types, so no need for Drop
|
||||
let _ = BlockHeader {
|
||||
block_id: val.header.block_id,
|
||||
prev_block_hash: HashType(val.header.prev_block_hash.data),
|
||||
hash: HashType(val.header.hash.data),
|
||||
timestamp: val.header.timestamp,
|
||||
signature: Signature(val.header.signature.data),
|
||||
};
|
||||
let ffi_tx_ffi_vec = val.body;
|
||||
|
||||
#[expect(clippy::let_underscore_must_use, reason = "No use for this Copy type")]
|
||||
let _: BedrockStatus = val.bedrock_status.into();
|
||||
|
||||
let _ = MantleMsgId(val.bedrock_parent_id.data);
|
||||
|
||||
unsafe {
|
||||
free_ffi_transaction_vec(ffi_tx_ffi_vec);
|
||||
};
|
||||
}
|
||||
|
||||
/// Frees the resources associated with the given ffi block option.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `val`: An instance of `FfiBlockOpt`.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// void.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller must ensure that:
|
||||
/// - `val` is a valid instance of `FfiBlockOpt`.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn free_ffi_block_opt(val: FfiBlockOpt) {
|
||||
if val.is_some {
|
||||
let value = unsafe { Box::from_raw(val.value) };
|
||||
|
||||
// We don't really need all the casts, but just in case
|
||||
// All except `ffi_tx_ffi_vec` is Copy types, so no need for Drop
|
||||
let _ = BlockHeader {
|
||||
block_id: value.header.block_id,
|
||||
prev_block_hash: HashType(value.header.prev_block_hash.data),
|
||||
hash: HashType(value.header.hash.data),
|
||||
timestamp: value.header.timestamp,
|
||||
signature: Signature(value.header.signature.data),
|
||||
};
|
||||
let ffi_tx_ffi_vec = value.body;
|
||||
|
||||
#[expect(clippy::let_underscore_must_use, reason = "No use for this Copy type")]
|
||||
let _: BedrockStatus = value.bedrock_status.into();
|
||||
|
||||
let _ = MantleMsgId(value.bedrock_parent_id.data);
|
||||
|
||||
unsafe {
|
||||
free_ffi_transaction_vec(ffi_tx_ffi_vec);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Frees the resources associated with the given ffi block vector.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `val`: An instance of `FfiVec<FfiBlock>`.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// void.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller must ensure that:
|
||||
/// - `val` is a valid instance of `FfiVec<FfiBlock>`.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn free_ffi_block_vec(val: FfiVec<FfiBlock>) {
|
||||
let ffi_block_std_vec: Vec<_> = val.into();
|
||||
for block in ffi_block_std_vec {
|
||||
unsafe {
|
||||
free_ffi_block(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
165
indexer/ffi/src/api/types/mod.rs
Normal file
165
indexer/ffi/src/api/types/mod.rs
Normal file
@ -0,0 +1,165 @@
|
||||
use indexer_service_protocol::{AccountId, HashType, MantleMsgId, ProgramId, PublicKey, Signature};
|
||||
|
||||
pub mod account;
|
||||
pub mod block;
|
||||
pub mod transaction;
|
||||
pub mod vectors;
|
||||
|
||||
/// 32-byte array type for `AccountId`, keys, hashes, etc.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct FfiBytes32 {
|
||||
pub data: [u8; 32],
|
||||
}
|
||||
|
||||
/// 64-byte array type for signatures, etc.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct FfiBytes64 {
|
||||
pub data: [u8; 64],
|
||||
}
|
||||
|
||||
/// Program ID - 8 u32 values (32 bytes total).
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct FfiProgramId {
|
||||
pub data: [u32; 8],
|
||||
}
|
||||
|
||||
impl From<ProgramId> for FfiProgramId {
|
||||
fn from(value: ProgramId) -> Self {
|
||||
Self { data: value.0 }
|
||||
}
|
||||
}
|
||||
|
||||
/// U128 - 16 bytes little endian.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct FfiU128 {
|
||||
pub data: [u8; 16],
|
||||
}
|
||||
|
||||
impl FfiBytes32 {
|
||||
/// Create from a 32-byte array.
|
||||
#[must_use]
|
||||
pub const fn from_bytes(bytes: [u8; 32]) -> Self {
|
||||
Self { data: bytes }
|
||||
}
|
||||
|
||||
/// Create from an `AccountId`.
|
||||
#[must_use]
|
||||
pub const fn from_account_id(id: &nssa::AccountId) -> Self {
|
||||
Self { data: *id.value() }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u128> for FfiU128 {
|
||||
fn from(value: u128) -> Self {
|
||||
Self {
|
||||
data: value.to_le_bytes(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FfiU128> for u128 {
|
||||
fn from(value: FfiU128) -> Self {
|
||||
Self::from_le_bytes(value.data)
|
||||
}
|
||||
}
|
||||
|
||||
pub type FfiHashType = FfiBytes32;
|
||||
pub type FfiMsgId = FfiBytes32;
|
||||
pub type FfiBlockId = u64;
|
||||
pub type FfiTimestamp = u64;
|
||||
pub type FfiSignature = FfiBytes64;
|
||||
pub type FfiAccountId = FfiBytes32;
|
||||
pub type FfiNonce = FfiU128;
|
||||
pub type FfiPublicKey = FfiBytes32;
|
||||
|
||||
impl From<HashType> for FfiHashType {
|
||||
fn from(value: HashType) -> Self {
|
||||
Self { data: value.0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MantleMsgId> for FfiMsgId {
|
||||
fn from(value: MantleMsgId) -> Self {
|
||||
Self { data: value.0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Signature> for FfiSignature {
|
||||
fn from(value: Signature) -> Self {
|
||||
Self { data: value.0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AccountId> for FfiAccountId {
|
||||
fn from(value: AccountId) -> Self {
|
||||
Self { data: value.value }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PublicKey> for FfiPublicKey {
|
||||
fn from(value: PublicKey) -> Self {
|
||||
Self { data: value.0 }
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct FfiVec<T> {
|
||||
pub entries: *mut T,
|
||||
pub len: usize,
|
||||
pub capacity: usize,
|
||||
}
|
||||
|
||||
impl<T> From<Vec<T>> for FfiVec<T> {
|
||||
fn from(value: Vec<T>) -> Self {
|
||||
let (entries, len, capacity) = value.into_raw_parts();
|
||||
Self {
|
||||
entries,
|
||||
len,
|
||||
capacity,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<FfiVec<T>> for Vec<T> {
|
||||
fn from(value: FfiVec<T>) -> Self {
|
||||
unsafe { Self::from_raw_parts(value.entries, value.len, value.capacity) }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> FfiVec<T> {
|
||||
/// # Safety
|
||||
///
|
||||
/// `index` must be lesser than `self.len`.
|
||||
#[must_use]
|
||||
pub unsafe fn get(&self, index: usize) -> &T {
|
||||
let ptr = unsafe { self.entries.add(index) };
|
||||
unsafe { &*ptr }
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct FfiOption<T> {
|
||||
pub value: *mut T,
|
||||
pub is_some: bool,
|
||||
}
|
||||
|
||||
impl<T> FfiOption<T> {
|
||||
pub fn from_value(val: T) -> Self {
|
||||
Self {
|
||||
value: Box::into_raw(Box::new(val)),
|
||||
is_some: true,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn from_none() -> Self {
|
||||
Self {
|
||||
value: std::ptr::null_mut(),
|
||||
is_some: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
548
indexer/ffi/src/api/types/transaction.rs
Normal file
548
indexer/ffi/src/api/types/transaction.rs
Normal file
@ -0,0 +1,548 @@
|
||||
use indexer_service_protocol::{
|
||||
AccountId, Ciphertext, Commitment, CommitmentSetDigest, EncryptedAccountData,
|
||||
EphemeralPublicKey, HashType, Nullifier, PrivacyPreservingMessage,
|
||||
PrivacyPreservingTransaction, ProgramDeploymentMessage, ProgramDeploymentTransaction,
|
||||
ProgramId, Proof, PublicKey, PublicMessage, PublicTransaction, Signature, Transaction,
|
||||
ValidityWindow, WitnessSet,
|
||||
};
|
||||
|
||||
use crate::api::types::{
|
||||
FfiBytes32, FfiHashType, FfiOption, FfiProgramId, FfiPublicKey, FfiSignature, FfiVec,
|
||||
vectors::{
|
||||
FfiAccountIdList, FfiAccountList, FfiEncryptedAccountDataList, FfiInstructionDataList,
|
||||
FfiNonceList, FfiNullifierCommitmentSetList, FfiProgramDeploymentMessage, FfiProof,
|
||||
FfiSignaturePubKeyList, FfiVecBytes32, FfiVecU8,
|
||||
},
|
||||
};
|
||||
|
||||
#[repr(C)]
|
||||
pub struct FfiPublicTransactionBody {
|
||||
pub hash: FfiHashType,
|
||||
pub message: FfiPublicMessage,
|
||||
pub witness_set: FfiSignaturePubKeyList,
|
||||
}
|
||||
|
||||
impl From<PublicTransaction> for FfiPublicTransactionBody {
|
||||
fn from(value: PublicTransaction) -> Self {
|
||||
let PublicTransaction {
|
||||
hash,
|
||||
message,
|
||||
witness_set,
|
||||
} = value;
|
||||
|
||||
Self {
|
||||
hash: hash.into(),
|
||||
message: message.into(),
|
||||
witness_set: witness_set
|
||||
.signatures_and_public_keys
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Box<FfiPublicTransactionBody>> for PublicTransaction {
|
||||
fn from(value: Box<FfiPublicTransactionBody>) -> Self {
|
||||
Self {
|
||||
hash: HashType(value.hash.data),
|
||||
message: PublicMessage {
|
||||
program_id: ProgramId(value.message.program_id.data),
|
||||
account_ids: {
|
||||
let std_vec: Vec<_> = value.message.account_ids.into();
|
||||
std_vec
|
||||
.into_iter()
|
||||
.map(|ffi_val| AccountId {
|
||||
value: ffi_val.data,
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
nonces: {
|
||||
let std_vec: Vec<_> = value.message.nonces.into();
|
||||
std_vec.into_iter().map(Into::into).collect()
|
||||
},
|
||||
instruction_data: value.message.instruction_data.into(),
|
||||
},
|
||||
witness_set: WitnessSet {
|
||||
signatures_and_public_keys: {
|
||||
let std_vec: Vec<_> = value.witness_set.into();
|
||||
std_vec
|
||||
.into_iter()
|
||||
.map(|ffi_val| {
|
||||
(
|
||||
Signature(ffi_val.signature.data),
|
||||
PublicKey(ffi_val.public_key.data),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
proof: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct FfiPublicMessage {
|
||||
pub program_id: FfiProgramId,
|
||||
pub account_ids: FfiAccountIdList,
|
||||
pub nonces: FfiNonceList,
|
||||
pub instruction_data: FfiInstructionDataList,
|
||||
}
|
||||
|
||||
impl From<PublicMessage> for FfiPublicMessage {
|
||||
fn from(value: PublicMessage) -> Self {
|
||||
let PublicMessage {
|
||||
program_id,
|
||||
account_ids,
|
||||
nonces,
|
||||
instruction_data,
|
||||
} = value;
|
||||
|
||||
Self {
|
||||
program_id: program_id.into(),
|
||||
account_ids: account_ids
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
nonces: nonces
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
instruction_data: instruction_data.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct FfiPrivateTransactionBody {
|
||||
pub hash: FfiHashType,
|
||||
pub message: FfiPrivacyPreservingMessage,
|
||||
pub witness_set: FfiSignaturePubKeyList,
|
||||
pub proof: FfiProof,
|
||||
}
|
||||
|
||||
impl From<PrivacyPreservingTransaction> for FfiPrivateTransactionBody {
|
||||
fn from(value: PrivacyPreservingTransaction) -> Self {
|
||||
let PrivacyPreservingTransaction {
|
||||
hash,
|
||||
message,
|
||||
witness_set,
|
||||
} = value;
|
||||
|
||||
Self {
|
||||
hash: hash.into(),
|
||||
message: message.into(),
|
||||
witness_set: witness_set
|
||||
.signatures_and_public_keys
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
proof: witness_set
|
||||
.proof
|
||||
.expect("Private execution: proof must be present")
|
||||
.0
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Box<FfiPrivateTransactionBody>> for PrivacyPreservingTransaction {
|
||||
fn from(value: Box<FfiPrivateTransactionBody>) -> Self {
|
||||
Self {
|
||||
hash: HashType(value.hash.data),
|
||||
message: PrivacyPreservingMessage {
|
||||
public_account_ids: {
|
||||
let std_vec: Vec<_> = value.message.public_account_ids.into();
|
||||
std_vec
|
||||
.into_iter()
|
||||
.map(|ffi_val| AccountId {
|
||||
value: ffi_val.data,
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
nonces: {
|
||||
let std_vec: Vec<_> = value.message.nonces.into();
|
||||
std_vec.into_iter().map(Into::into).collect()
|
||||
},
|
||||
public_post_states: {
|
||||
let std_vec: Vec<_> = value.message.public_post_states.into();
|
||||
std_vec.into_iter().map(Into::into).collect()
|
||||
},
|
||||
encrypted_private_post_states: {
|
||||
let std_vec: Vec<_> = value.message.encrypted_private_post_states.into();
|
||||
std_vec
|
||||
.into_iter()
|
||||
.map(|ffi_val| EncryptedAccountData {
|
||||
ciphertext: Ciphertext(ffi_val.ciphertext.into()),
|
||||
epk: EphemeralPublicKey(ffi_val.epk.into()),
|
||||
view_tag: ffi_val.view_tag,
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
new_commitments: {
|
||||
let std_vec: Vec<_> = value.message.new_commitments.into();
|
||||
std_vec
|
||||
.into_iter()
|
||||
.map(|ffi_val| Commitment(ffi_val.data))
|
||||
.collect()
|
||||
},
|
||||
new_nullifiers: {
|
||||
let std_vec: Vec<_> = value.message.new_nullifiers.into();
|
||||
std_vec
|
||||
.into_iter()
|
||||
.map(|ffi_val| {
|
||||
(
|
||||
Nullifier(ffi_val.nullifier.data),
|
||||
CommitmentSetDigest(ffi_val.commitment_set_digest.data),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
block_validity_window: cast_ffi_validity_window(
|
||||
value.message.block_validity_window,
|
||||
),
|
||||
timestamp_validity_window: cast_ffi_validity_window(
|
||||
value.message.timestamp_validity_window,
|
||||
),
|
||||
},
|
||||
witness_set: WitnessSet {
|
||||
signatures_and_public_keys: {
|
||||
let std_vec: Vec<_> = value.witness_set.into();
|
||||
std_vec
|
||||
.into_iter()
|
||||
.map(|ffi_val| {
|
||||
(
|
||||
Signature(ffi_val.signature.data),
|
||||
PublicKey(ffi_val.public_key.data),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
proof: Some(Proof(value.proof.into())),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct FfiPrivacyPreservingMessage {
|
||||
pub public_account_ids: FfiAccountIdList,
|
||||
pub nonces: FfiNonceList,
|
||||
pub public_post_states: FfiAccountList,
|
||||
pub encrypted_private_post_states: FfiEncryptedAccountDataList,
|
||||
pub new_commitments: FfiVecBytes32,
|
||||
pub new_nullifiers: FfiNullifierCommitmentSetList,
|
||||
pub block_validity_window: [u64; 2],
|
||||
pub timestamp_validity_window: [u64; 2],
|
||||
}
|
||||
|
||||
impl From<PrivacyPreservingMessage> for FfiPrivacyPreservingMessage {
|
||||
fn from(value: PrivacyPreservingMessage) -> Self {
|
||||
let PrivacyPreservingMessage {
|
||||
public_account_ids,
|
||||
nonces,
|
||||
public_post_states,
|
||||
encrypted_private_post_states,
|
||||
new_commitments,
|
||||
new_nullifiers,
|
||||
block_validity_window,
|
||||
timestamp_validity_window,
|
||||
} = value;
|
||||
|
||||
Self {
|
||||
public_account_ids: public_account_ids
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
nonces: nonces
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
public_post_states: public_post_states
|
||||
.into_iter()
|
||||
.map(|acc_ind| -> nssa::Account {
|
||||
acc_ind.try_into().expect("Source is in blocks, must fit")
|
||||
})
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
encrypted_private_post_states: encrypted_private_post_states
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
new_commitments: new_commitments
|
||||
.into_iter()
|
||||
.map(|comm| FfiBytes32 { data: comm.0 })
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
new_nullifiers: new_nullifiers
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
block_validity_window: cast_validity_window(block_validity_window),
|
||||
timestamp_validity_window: cast_validity_window(timestamp_validity_window),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct FfiNullifierCommitmentSet {
|
||||
pub nullifier: FfiBytes32,
|
||||
pub commitment_set_digest: FfiBytes32,
|
||||
}
|
||||
|
||||
impl From<(Nullifier, CommitmentSetDigest)> for FfiNullifierCommitmentSet {
|
||||
fn from(value: (Nullifier, CommitmentSetDigest)) -> Self {
|
||||
Self {
|
||||
nullifier: FfiBytes32 { data: value.0.0 },
|
||||
commitment_set_digest: FfiBytes32 { data: value.1.0 },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct FfiEncryptedAccountData {
|
||||
pub ciphertext: FfiVecU8,
|
||||
pub epk: FfiVecU8,
|
||||
pub view_tag: u8,
|
||||
}
|
||||
|
||||
impl From<EncryptedAccountData> for FfiEncryptedAccountData {
|
||||
fn from(value: EncryptedAccountData) -> Self {
|
||||
let EncryptedAccountData {
|
||||
ciphertext,
|
||||
epk,
|
||||
view_tag,
|
||||
} = value;
|
||||
|
||||
Self {
|
||||
ciphertext: ciphertext.0.into(),
|
||||
epk: epk.0.into(),
|
||||
view_tag,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct FfiSignaturePubKeyEntry {
|
||||
pub signature: FfiSignature,
|
||||
pub public_key: FfiPublicKey,
|
||||
}
|
||||
|
||||
impl From<(Signature, PublicKey)> for FfiSignaturePubKeyEntry {
|
||||
fn from(value: (Signature, PublicKey)) -> Self {
|
||||
Self {
|
||||
signature: value.0.into(),
|
||||
public_key: value.1.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct FfiProgramDeploymentTransactionBody {
|
||||
pub hash: FfiHashType,
|
||||
pub message: FfiProgramDeploymentMessage,
|
||||
}
|
||||
|
||||
impl From<Box<FfiProgramDeploymentTransactionBody>> for ProgramDeploymentTransaction {
|
||||
fn from(value: Box<FfiProgramDeploymentTransactionBody>) -> Self {
|
||||
Self {
|
||||
hash: HashType(value.hash.data),
|
||||
message: ProgramDeploymentMessage {
|
||||
bytecode: value.message.into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ProgramDeploymentTransaction> for FfiProgramDeploymentTransactionBody {
|
||||
fn from(value: ProgramDeploymentTransaction) -> Self {
|
||||
let ProgramDeploymentTransaction { hash, message } = value;
|
||||
|
||||
Self {
|
||||
hash: hash.into(),
|
||||
message: message.bytecode.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct FfiTransactionBody {
|
||||
pub public_body: *mut FfiPublicTransactionBody,
|
||||
pub private_body: *mut FfiPrivateTransactionBody,
|
||||
pub program_deployment_body: *mut FfiProgramDeploymentTransactionBody,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct FfiTransaction {
|
||||
pub body: FfiTransactionBody,
|
||||
pub kind: FfiTransactionKind,
|
||||
}
|
||||
|
||||
impl From<Transaction> for FfiTransaction {
|
||||
fn from(value: Transaction) -> Self {
|
||||
match value {
|
||||
Transaction::Public(pub_tx) => Self {
|
||||
body: FfiTransactionBody {
|
||||
public_body: Box::into_raw(Box::new(pub_tx.into())),
|
||||
private_body: std::ptr::null_mut(),
|
||||
program_deployment_body: std::ptr::null_mut(),
|
||||
},
|
||||
kind: FfiTransactionKind::Public,
|
||||
},
|
||||
Transaction::PrivacyPreserving(priv_tx) => Self {
|
||||
body: FfiTransactionBody {
|
||||
public_body: std::ptr::null_mut(),
|
||||
private_body: Box::into_raw(Box::new(priv_tx.into())),
|
||||
program_deployment_body: std::ptr::null_mut(),
|
||||
},
|
||||
kind: FfiTransactionKind::Private,
|
||||
},
|
||||
Transaction::ProgramDeployment(pr_dep_tx) => Self {
|
||||
body: FfiTransactionBody {
|
||||
public_body: std::ptr::null_mut(),
|
||||
private_body: std::ptr::null_mut(),
|
||||
program_deployment_body: Box::into_raw(Box::new(pr_dep_tx.into())),
|
||||
},
|
||||
kind: FfiTransactionKind::ProgramDeploy,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub enum FfiTransactionKind {
|
||||
Public = 0x0,
|
||||
Private,
|
||||
ProgramDeploy,
|
||||
}
|
||||
|
||||
/// Frees the resources associated with the given ffi transaction.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `val`: An instance of `FfiTransaction`.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// void.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller must ensure that:
|
||||
/// - `val` is a valid instance of `FfiTransaction`.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn free_ffi_transaction(val: FfiTransaction) {
|
||||
match val.kind {
|
||||
FfiTransactionKind::Public => {
|
||||
let body = unsafe { Box::from_raw(val.body.public_body) };
|
||||
let std_body: PublicTransaction = body.into();
|
||||
drop(std_body);
|
||||
}
|
||||
FfiTransactionKind::Private => {
|
||||
let body = unsafe { Box::from_raw(val.body.private_body) };
|
||||
let std_body: PrivacyPreservingTransaction = body.into();
|
||||
drop(std_body);
|
||||
}
|
||||
FfiTransactionKind::ProgramDeploy => {
|
||||
let body = unsafe { Box::from_raw(val.body.program_deployment_body) };
|
||||
let std_body: ProgramDeploymentTransaction = body.into();
|
||||
drop(std_body);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Frees the resources associated with the given ffi transaction option.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `val`: An instance of `FfiOption<FfiTransaction>`.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// void.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller must ensure that:
|
||||
/// - `val` is a valid instance of `FfiOption<FfiTransaction>`.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn free_ffi_transaction_opt(val: FfiOption<FfiTransaction>) {
|
||||
if val.is_some {
|
||||
let value = unsafe { Box::from_raw(val.value) };
|
||||
|
||||
match value.kind {
|
||||
FfiTransactionKind::Public => {
|
||||
let body = unsafe { Box::from_raw(value.body.public_body) };
|
||||
let std_body: PublicTransaction = body.into();
|
||||
drop(std_body);
|
||||
}
|
||||
FfiTransactionKind::Private => {
|
||||
let body = unsafe { Box::from_raw(value.body.private_body) };
|
||||
let std_body: PrivacyPreservingTransaction = body.into();
|
||||
drop(std_body);
|
||||
}
|
||||
FfiTransactionKind::ProgramDeploy => {
|
||||
let body = unsafe { Box::from_raw(value.body.program_deployment_body) };
|
||||
let std_body: ProgramDeploymentTransaction = body.into();
|
||||
drop(std_body);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Frees the resources associated with the given vector of ffi transactions.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `val`: An instance of `FfiVec<FfiTransaction>`.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// void.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller must ensure that:
|
||||
/// - `val` is a valid instance of `FfiVec<FfiTransaction>`.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn free_ffi_transaction_vec(val: FfiVec<FfiTransaction>) {
|
||||
let ffi_tx_std_vec: Vec<_> = val.into();
|
||||
for tx in ffi_tx_std_vec {
|
||||
unsafe {
|
||||
free_ffi_transaction(tx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cast_validity_window(window: ValidityWindow) -> [u64; 2] {
|
||||
[
|
||||
window.0.0.unwrap_or_default(),
|
||||
window.0.1.unwrap_or(u64::MAX),
|
||||
]
|
||||
}
|
||||
|
||||
const fn cast_ffi_validity_window(ffi_window: [u64; 2]) -> ValidityWindow {
|
||||
let left = if ffi_window[0] == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(ffi_window[0])
|
||||
};
|
||||
|
||||
let right = if ffi_window[1] == u64::MAX {
|
||||
None
|
||||
} else {
|
||||
Some(ffi_window[1])
|
||||
};
|
||||
|
||||
ValidityWindow((left, right))
|
||||
}
|
||||
31
indexer/ffi/src/api/types/vectors.rs
Normal file
31
indexer/ffi/src/api/types/vectors.rs
Normal file
@ -0,0 +1,31 @@
|
||||
use crate::api::types::{
|
||||
FfiAccountId, FfiBytes32, FfiNonce, FfiVec,
|
||||
account::FfiAccount,
|
||||
transaction::{
|
||||
FfiEncryptedAccountData, FfiNullifierCommitmentSet, FfiSignaturePubKeyEntry, FfiTransaction,
|
||||
},
|
||||
};
|
||||
|
||||
pub type FfiVecU8 = FfiVec<u8>;
|
||||
|
||||
pub type FfiAccountList = FfiVec<FfiAccount>;
|
||||
|
||||
pub type FfiAccountIdList = FfiVec<FfiAccountId>;
|
||||
|
||||
pub type FfiVecBytes32 = FfiVec<FfiBytes32>;
|
||||
|
||||
pub type FfiBlockBody = FfiVec<FfiTransaction>;
|
||||
|
||||
pub type FfiNonceList = FfiVec<FfiNonce>;
|
||||
|
||||
pub type FfiInstructionDataList = FfiVec<u32>;
|
||||
|
||||
pub type FfiSignaturePubKeyList = FfiVec<FfiSignaturePubKeyEntry>;
|
||||
|
||||
pub type FfiProof = FfiVecU8;
|
||||
|
||||
pub type FfiProgramDeploymentMessage = FfiVecU8;
|
||||
|
||||
pub type FfiEncryptedAccountDataList = FfiVec<FfiEncryptedAccountData>;
|
||||
|
||||
pub type FfiNullifierCommitmentSetList = FfiVec<FfiNullifierCommitmentSet>;
|
||||
33
indexer/ffi/src/client.rs
Normal file
33
indexer/ffi/src/client.rs
Normal file
@ -0,0 +1,33 @@
|
||||
use std::{ops::Deref, sync::Arc};
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use log::info;
|
||||
pub use url::Url;
|
||||
|
||||
pub trait IndexerClientTrait: Clone {
|
||||
async fn new(indexer_url: &Url) -> Result<Self>;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct IndexerClient(Arc<jsonrpsee::ws_client::WsClient>);
|
||||
|
||||
impl IndexerClientTrait for IndexerClient {
|
||||
async fn new(indexer_url: &Url) -> Result<Self> {
|
||||
info!("Connecting to Indexer at {indexer_url}");
|
||||
|
||||
let client = jsonrpsee::ws_client::WsClientBuilder::default()
|
||||
.build(indexer_url)
|
||||
.await
|
||||
.context("Failed to create websocket client")?;
|
||||
|
||||
Ok(Self(Arc::new(client)))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for IndexerClient {
|
||||
type Target = jsonrpsee::ws_client::WsClient;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,7 @@ pub enum OperationStatus {
|
||||
Ok = 0x0,
|
||||
NullPointer = 0x1,
|
||||
InitializationError = 0x2,
|
||||
ClientError = 0x3,
|
||||
}
|
||||
|
||||
impl OperationStatus {
|
||||
@ -3,18 +3,26 @@ use std::{ffi::c_void, net::SocketAddr};
|
||||
use indexer_service::IndexerHandle;
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
use crate::client::IndexerClient;
|
||||
|
||||
#[repr(C)]
|
||||
pub struct IndexerServiceFFI {
|
||||
indexer_handle: *mut c_void,
|
||||
runtime: *mut c_void,
|
||||
indexer_client: *mut c_void,
|
||||
}
|
||||
|
||||
impl IndexerServiceFFI {
|
||||
pub fn new(indexer_handle: indexer_service::IndexerHandle, runtime: Runtime) -> Self {
|
||||
pub fn new(
|
||||
indexer_handle: indexer_service::IndexerHandle,
|
||||
runtime: Runtime,
|
||||
indexer_client: IndexerClient,
|
||||
) -> Self {
|
||||
Self {
|
||||
// Box the complex types and convert to opaque pointers
|
||||
indexer_handle: Box::into_raw(Box::new(indexer_handle)).cast::<c_void>(),
|
||||
runtime: Box::into_raw(Box::new(runtime)).cast::<c_void>(),
|
||||
indexer_client: Box::into_raw(Box::new(indexer_client)).cast::<c_void>(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,10 +33,11 @@ impl IndexerServiceFFI {
|
||||
/// The caller must ensure that:
|
||||
/// - `self` is a valid object(contains valid pointers in all fields)
|
||||
#[must_use]
|
||||
pub unsafe fn into_parts(self) -> (Box<IndexerHandle>, Box<Runtime>) {
|
||||
pub unsafe fn into_parts(self) -> (Box<IndexerHandle>, Box<Runtime>, Box<IndexerClient>) {
|
||||
let indexer_handle = unsafe { Box::from_raw(self.indexer_handle.cast::<IndexerHandle>()) };
|
||||
let runtime = unsafe { Box::from_raw(self.runtime.cast::<Runtime>()) };
|
||||
(indexer_handle, runtime)
|
||||
let indexer_client = unsafe { Box::from_raw(self.indexer_client.cast::<IndexerClient>()) };
|
||||
(indexer_handle, runtime, indexer_client)
|
||||
}
|
||||
|
||||
/// Helper to get indexer handle addr.
|
||||
@ -49,7 +58,7 @@ impl IndexerServiceFFI {
|
||||
indexer_handle.addr()
|
||||
}
|
||||
|
||||
/// Helper to get indexer handle addr.
|
||||
/// Helper to get indexer handle ref.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
@ -64,6 +73,38 @@ impl IndexerServiceFFI {
|
||||
.expect("Indexer Handle must be non-null pointer")
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to get indexer client ref.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller must ensure that:
|
||||
/// - `self` is a valid object(contains valid pointers in all fields)
|
||||
#[must_use]
|
||||
pub const unsafe fn client(&self) -> &IndexerClient {
|
||||
unsafe {
|
||||
self.indexer_client
|
||||
.cast::<IndexerClient>()
|
||||
.as_ref()
|
||||
.expect("Indexer Client must be non-null pointer")
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to get indexer runtime ref.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller must ensure that:
|
||||
/// - `self` is a valid object(contains valid pointers in all fields)
|
||||
#[must_use]
|
||||
pub const unsafe fn runtime(&self) -> &Runtime {
|
||||
unsafe {
|
||||
self.runtime
|
||||
.cast::<Runtime>()
|
||||
.as_ref()
|
||||
.expect("Indexer Runtime must be non-null pointer")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Implement Drop to prevent memory leaks
|
||||
@ -72,6 +113,7 @@ impl Drop for IndexerServiceFFI {
|
||||
let Self {
|
||||
indexer_handle,
|
||||
runtime,
|
||||
indexer_client,
|
||||
} = self;
|
||||
|
||||
if indexer_handle.is_null() {
|
||||
@ -80,7 +122,11 @@ impl Drop for IndexerServiceFFI {
|
||||
if runtime.is_null() {
|
||||
log::error!("Attempted to drop a null tokio runtime pointer. This is a bug");
|
||||
}
|
||||
if indexer_client.is_null() {
|
||||
log::error!("Attempted to drop a null client pointer. This is a bug");
|
||||
}
|
||||
drop(unsafe { Box::from_raw(indexer_handle.cast::<IndexerHandle>()) });
|
||||
drop(unsafe { Box::from_raw(runtime.cast::<Runtime>()) });
|
||||
drop(unsafe { Box::from_raw(indexer_client.cast::<IndexerClient>()) });
|
||||
}
|
||||
}
|
||||
@ -4,5 +4,6 @@ pub use errors::OperationStatus;
|
||||
pub use indexer::IndexerServiceFFI;
|
||||
|
||||
pub mod api;
|
||||
mod client;
|
||||
mod errors;
|
||||
mod indexer;
|
||||
@ -1,76 +0,0 @@
|
||||
#include <stdarg.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
typedef enum OperationStatus {
|
||||
Ok = 0,
|
||||
NullPointer = 1,
|
||||
InitializationError = 2,
|
||||
} OperationStatus;
|
||||
|
||||
typedef struct IndexerServiceFFI {
|
||||
void *indexer_handle;
|
||||
void *runtime;
|
||||
} IndexerServiceFFI;
|
||||
|
||||
/**
|
||||
* Simple wrapper around a pointer to a value or an error.
|
||||
*
|
||||
* Pointer is not guaranteed. You should check the error field before
|
||||
* dereferencing the pointer.
|
||||
*/
|
||||
typedef struct PointerResult_IndexerServiceFFI__OperationStatus {
|
||||
struct IndexerServiceFFI *value;
|
||||
enum OperationStatus error;
|
||||
} PointerResult_IndexerServiceFFI__OperationStatus;
|
||||
|
||||
typedef struct PointerResult_IndexerServiceFFI__OperationStatus InitializedIndexerServiceFFIResult;
|
||||
|
||||
/**
|
||||
* Creates and starts an indexer based on the provided
|
||||
* configuration file path.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* - `config_path`: A pointer to a string representing the path to the configuration file.
|
||||
* - `port`: Number representing a port, on which indexers RPC will start.
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* An `InitializedIndexerServiceFFIResult` containing either a pointer to the
|
||||
* initialized `IndexerServiceFFI` or an error code.
|
||||
*/
|
||||
InitializedIndexerServiceFFIResult start_indexer(const char *config_path, uint16_t port);
|
||||
|
||||
/**
|
||||
* Stops and frees the resources associated with the given indexer service.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* - `indexer`: A pointer to the `IndexerServiceFFI` instance to be stopped.
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* An `OperationStatus` indicating success or failure.
|
||||
*
|
||||
* # Safety
|
||||
*
|
||||
* The caller must ensure that:
|
||||
* - `indexer` is a valid pointer to a `IndexerServiceFFI` instance
|
||||
* - The `IndexerServiceFFI` instance was created by this library
|
||||
* - The pointer will not be used after this function returns
|
||||
*/
|
||||
enum OperationStatus stop_indexer(struct IndexerServiceFFI *indexer);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
* It's up to the caller to pass a proper pointer, if somehow from c/c++ side
|
||||
* this is called with a type which doesn't come from a returned `CString` it
|
||||
* will cause a segfault.
|
||||
*/
|
||||
void free_cstring(char *block);
|
||||
|
||||
bool is_ok(const enum OperationStatus *self);
|
||||
|
||||
bool is_error(const enum OperationStatus *self);
|
||||
@ -25,6 +25,7 @@ jsonrpsee = { workspace = true, features = ["ws-client"] }
|
||||
wallet-ffi.workspace = true
|
||||
indexer_ffi.workspace = true
|
||||
testnet_initial_state.workspace = true
|
||||
indexer_service_protocol.workspace = true
|
||||
|
||||
url.workspace = true
|
||||
|
||||
|
||||
@ -266,6 +266,11 @@ impl BlockingTestContextFFI {
|
||||
pub fn runtime_clone(&self) -> Arc<tokio::runtime::Runtime> {
|
||||
Arc::<tokio::runtime::Runtime>::clone(&self.runtime)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn indexer_ffi(&self) -> *const IndexerServiceFFI {
|
||||
&raw const (self.indexer_ffi)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for BlockingTestContextFFI {
|
||||
|
||||
@ -1,11 +1,18 @@
|
||||
#![expect(
|
||||
clippy::shadow_unrelated,
|
||||
clippy::tests_outside_test_module,
|
||||
clippy::undocumented_unsafe_blocks,
|
||||
reason = "We don't care about these in tests"
|
||||
)]
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use indexer_service_rpc::RpcClient as _;
|
||||
use indexer_ffi::{
|
||||
IndexerServiceFFI, OperationStatus,
|
||||
api::{
|
||||
PointerResult,
|
||||
types::{FfiAccountId, FfiOption, FfiVec, account::FfiAccount, block::FfiBlock},
|
||||
},
|
||||
};
|
||||
use integration_tests::{
|
||||
TIME_TO_WAIT_FOR_BLOCK_SECONDS, format_private_account_id, format_public_account_id,
|
||||
test_context_ffi::BlockingTestContextFFI, verify_commitment_is_in_state,
|
||||
@ -17,6 +24,23 @@ use wallet::cli::{Command, programs::native_token_transfer::AuthTransferSubcomma
|
||||
/// Maximum time to wait for the indexer to catch up to the sequencer.
|
||||
const L2_TO_L1_TIMEOUT_MILLIS: u64 = 180_000;
|
||||
|
||||
unsafe extern "C" {
|
||||
unsafe fn query_last_block(
|
||||
indexer: *const IndexerServiceFFI,
|
||||
) -> PointerResult<u64, OperationStatus>;
|
||||
|
||||
unsafe fn query_block_vec(
|
||||
indexer: *const IndexerServiceFFI,
|
||||
before: FfiOption<u64>,
|
||||
limit: u64,
|
||||
) -> PointerResult<FfiVec<FfiBlock>, OperationStatus>;
|
||||
|
||||
unsafe fn query_account(
|
||||
indexer: *const IndexerServiceFFI,
|
||||
account_id: FfiAccountId,
|
||||
) -> PointerResult<FfiAccount, OperationStatus>;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn indexer_test_run_ffi() -> Result<()> {
|
||||
let blocking_ctx = BlockingTestContextFFI::new()?;
|
||||
@ -28,10 +52,19 @@ fn indexer_test_run_ffi() -> Result<()> {
|
||||
});
|
||||
|
||||
let last_block_indexer = blocking_ctx.ctx().get_last_block_indexer(runtime_wrapped)?;
|
||||
let last_block_indexer_ffi_res = unsafe { query_last_block(blocking_ctx.indexer_ffi()) };
|
||||
|
||||
assert!(last_block_indexer_ffi_res.error.is_ok());
|
||||
|
||||
let last_block_indexer_ffi = unsafe { *last_block_indexer_ffi_res.value };
|
||||
|
||||
info!("Last block on ind now is {last_block_indexer}");
|
||||
info!("Last block on ind ffi now is {last_block_indexer_ffi}");
|
||||
|
||||
assert!(last_block_indexer > 1);
|
||||
assert!(last_block_indexer_ffi > 1);
|
||||
|
||||
assert_eq!(last_block_indexer, last_block_indexer_ffi);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -40,7 +73,6 @@ fn indexer_test_run_ffi() -> Result<()> {
|
||||
fn indexer_ffi_block_batching() -> Result<()> {
|
||||
let blocking_ctx = BlockingTestContextFFI::new()?;
|
||||
let runtime_wrapped = blocking_ctx.runtime();
|
||||
let ctx = blocking_ctx.ctx();
|
||||
|
||||
// WAIT
|
||||
info!("Waiting for indexer to parse blocks");
|
||||
@ -48,31 +80,36 @@ fn indexer_ffi_block_batching() -> Result<()> {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(L2_TO_L1_TIMEOUT_MILLIS)).await;
|
||||
});
|
||||
|
||||
let last_block_indexer = runtime_wrapped
|
||||
.block_on(ctx.indexer_client().get_last_finalized_block_id())
|
||||
.unwrap();
|
||||
let last_block_indexer_ffi_res = unsafe { query_last_block(blocking_ctx.indexer_ffi()) };
|
||||
|
||||
assert!(last_block_indexer_ffi_res.error.is_ok());
|
||||
|
||||
let last_block_indexer = unsafe { *last_block_indexer_ffi_res.value };
|
||||
|
||||
info!("Last block on ind now is {last_block_indexer}");
|
||||
|
||||
assert!(last_block_indexer > 1);
|
||||
|
||||
// Getting wide batch to fit all blocks (from latest backwards)
|
||||
let mut block_batch = runtime_wrapped
|
||||
.block_on(ctx.indexer_client().get_blocks(None, 100))
|
||||
.unwrap();
|
||||
let before_ffi = FfiOption::<u64>::from_none();
|
||||
let limit = 100;
|
||||
|
||||
// Reverse to check chain consistency from oldest to newest
|
||||
block_batch.reverse();
|
||||
let block_batch_ffi_res =
|
||||
unsafe { query_block_vec(blocking_ctx.indexer_ffi(), before_ffi, limit) };
|
||||
|
||||
// Checking chain consistency
|
||||
let mut prev_block_hash = block_batch.first().unwrap().header.hash;
|
||||
assert!(block_batch_ffi_res.error.is_ok());
|
||||
|
||||
for block in &block_batch[1..] {
|
||||
assert_eq!(block.header.prev_block_hash, prev_block_hash);
|
||||
let block_batch = unsafe { &*block_batch_ffi_res.value };
|
||||
|
||||
let mut last_block_prev_hash = unsafe { block_batch.get(0) }.header.prev_block_hash.data;
|
||||
|
||||
for i in 1..block_batch.len {
|
||||
let block = unsafe { block_batch.get(i) };
|
||||
|
||||
assert_eq!(last_block_prev_hash, block.header.hash.data);
|
||||
|
||||
info!("Block {} chain-consistent", block.header.block_id);
|
||||
|
||||
prev_block_hash = block.header.hash;
|
||||
last_block_prev_hash = block.header.prev_block_hash.data;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@ -82,6 +119,7 @@ fn indexer_ffi_block_batching() -> Result<()> {
|
||||
fn indexer_ffi_state_consistency() -> Result<()> {
|
||||
let mut blocking_ctx = BlockingTestContextFFI::new()?;
|
||||
let runtime_wrapped = blocking_ctx.runtime_clone();
|
||||
let indexer_ffi = blocking_ctx.indexer_ffi();
|
||||
let ctx = blocking_ctx.ctx_mut();
|
||||
|
||||
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
|
||||
@ -175,14 +213,21 @@ fn indexer_ffi_state_consistency() -> Result<()> {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(L2_TO_L1_TIMEOUT_MILLIS)).await;
|
||||
});
|
||||
|
||||
let acc1_ind_state = runtime_wrapped.block_on(
|
||||
ctx.indexer_client()
|
||||
.get_account(ctx.existing_public_accounts()[0].into()),
|
||||
)?;
|
||||
let acc2_ind_state = runtime_wrapped.block_on(
|
||||
ctx.indexer_client()
|
||||
.get_account(ctx.existing_public_accounts()[1].into()),
|
||||
)?;
|
||||
let acc1_ind_state_ffi =
|
||||
unsafe { query_account(indexer_ffi, (&ctx.existing_public_accounts()[0]).into()) };
|
||||
|
||||
assert!(acc1_ind_state_ffi.error.is_ok());
|
||||
|
||||
let acc1_ind_state_pre = unsafe { &*acc1_ind_state_ffi.value };
|
||||
let acc1_ind_state: indexer_service_protocol::Account = acc1_ind_state_pre.into();
|
||||
|
||||
let acc2_ind_state_ffi =
|
||||
unsafe { query_account(indexer_ffi, (&ctx.existing_public_accounts()[1]).into()) };
|
||||
|
||||
assert!(acc2_ind_state_ffi.error.is_ok());
|
||||
|
||||
let acc2_ind_state_pre = unsafe { &*acc2_ind_state_ffi.value };
|
||||
let acc2_ind_state: indexer_service_protocol::Account = acc2_ind_state_pre.into();
|
||||
|
||||
info!("Checking correct state transition");
|
||||
let acc1_seq_state =
|
||||
@ -208,6 +253,7 @@ fn indexer_ffi_state_consistency() -> Result<()> {
|
||||
fn indexer_ffi_state_consistency_with_labels() -> Result<()> {
|
||||
let mut blocking_ctx = BlockingTestContextFFI::new()?;
|
||||
let runtime_wrapped = blocking_ctx.runtime_clone();
|
||||
let indexer_ffi = blocking_ctx.indexer_ffi();
|
||||
let ctx = blocking_ctx.ctx_mut();
|
||||
|
||||
// Assign labels to both accounts
|
||||
@ -269,10 +315,14 @@ fn indexer_ffi_state_consistency_with_labels() -> Result<()> {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(L2_TO_L1_TIMEOUT_MILLIS)).await;
|
||||
});
|
||||
|
||||
let acc1_ind_state = runtime_wrapped.block_on(
|
||||
ctx.indexer_client()
|
||||
.get_account(ctx.existing_public_accounts()[0].into()),
|
||||
)?;
|
||||
let acc1_ind_state_ffi =
|
||||
unsafe { query_account(indexer_ffi, (&ctx.existing_public_accounts()[0]).into()) };
|
||||
|
||||
assert!(acc1_ind_state_ffi.error.is_ok());
|
||||
|
||||
let acc1_ind_state_pre = unsafe { &*acc1_ind_state_ffi.value };
|
||||
let acc1_ind_state: indexer_service_protocol::Account = acc1_ind_state_pre.into();
|
||||
|
||||
let acc1_seq_state =
|
||||
runtime_wrapped.block_on(sequencer_service_rpc::RpcClient::get_account(
|
||||
ctx.sequencer_client(),
|
||||
|
||||
229
integration_tests/tests/shared_accounts.rs
Normal file
229
integration_tests/tests/shared_accounts.rs
Normal file
@ -0,0 +1,229 @@
|
||||
#![expect(
|
||||
clippy::tests_outside_test_module,
|
||||
reason = "Integration test file, not inside a #[cfg(test)] module"
|
||||
)]
|
||||
#![expect(
|
||||
clippy::shadow_unrelated,
|
||||
reason = "Sequential wallet commands naturally reuse the `command` binding"
|
||||
)]
|
||||
|
||||
//! Shared account integration tests.
|
||||
//!
|
||||
//! Demonstrates:
|
||||
//! 1. Group creation and GMS distribution via seal/unseal.
|
||||
//! 2. Shared regular private account creation via `--for-gms`.
|
||||
//! 3. Funding a shared account from a public account.
|
||||
//! 4. Syncing discovers the funded shared account state.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use integration_tests::{TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, format_public_account_id};
|
||||
use log::info;
|
||||
use tokio::test;
|
||||
use wallet::cli::{
|
||||
Command, SubcommandReturnValue,
|
||||
account::{AccountSubcommand, NewSubcommand},
|
||||
group::GroupSubcommand,
|
||||
programs::native_token_transfer::AuthTransferSubcommand,
|
||||
};
|
||||
|
||||
/// Create a group, create a shared account from it, and verify registration.
|
||||
#[test]
|
||||
async fn group_create_and_shared_account_registration() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
// Create a group
|
||||
let command = Command::Group(GroupSubcommand::New {
|
||||
name: "test-group".into(),
|
||||
});
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
|
||||
// Verify group exists
|
||||
assert!(
|
||||
ctx.wallet()
|
||||
.storage()
|
||||
.user_data
|
||||
.group_key_holder("test-group")
|
||||
.is_some()
|
||||
);
|
||||
|
||||
// Create a shared regular private account from the group
|
||||
let command = Command::Account(AccountSubcommand::New(NewSubcommand::PrivateGms {
|
||||
group: "test-group".into(),
|
||||
label: Some("shared-acc".into()),
|
||||
pda: false,
|
||||
seed: None,
|
||||
program_id: None,
|
||||
}));
|
||||
|
||||
let result = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: shared_account_id,
|
||||
} = result
|
||||
else {
|
||||
anyhow::bail!("Expected RegisterAccount return value");
|
||||
};
|
||||
|
||||
// Verify shared account is registered in storage
|
||||
let entry = ctx
|
||||
.wallet()
|
||||
.storage()
|
||||
.user_data
|
||||
.shared_private_account(&shared_account_id)
|
||||
.context("Shared account not found in storage")?;
|
||||
assert_eq!(entry.group_label, "test-group");
|
||||
assert!(entry.pda_seed.is_none());
|
||||
|
||||
info!("Shared account registered: {shared_account_id}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// GMS seal/unseal round-trip via invite/join, verify key agreement.
|
||||
#[test]
|
||||
async fn group_invite_join_key_agreement() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
// Generate a sealing key
|
||||
let command = Command::Group(GroupSubcommand::NewSealingKey);
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
|
||||
// Create a group
|
||||
let command = Command::Group(GroupSubcommand::New {
|
||||
name: "alice-group".into(),
|
||||
});
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
|
||||
// Seal GMS for ourselves (simulating invite to another wallet)
|
||||
let sealing_sk = ctx
|
||||
.wallet()
|
||||
.storage()
|
||||
.user_data
|
||||
.sealing_secret_key
|
||||
.context("Sealing key not found")?;
|
||||
let sealing_pk =
|
||||
key_protocol::key_management::group_key_holder::SealingPublicKey::from_scalar(sealing_sk);
|
||||
|
||||
let holder = ctx
|
||||
.wallet()
|
||||
.storage()
|
||||
.user_data
|
||||
.group_key_holder("alice-group")
|
||||
.context("Group not found")?;
|
||||
let sealed = holder.seal_for(&sealing_pk);
|
||||
let sealed_hex = hex::encode(&sealed);
|
||||
|
||||
// Join under a different name (simulating Bob receiving the sealed GMS)
|
||||
let command = Command::Group(GroupSubcommand::Join {
|
||||
name: "bob-copy".into(),
|
||||
sealed: sealed_hex,
|
||||
});
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
|
||||
// Both derive the same keys for the same derivation seed
|
||||
let alice_holder = ctx
|
||||
.wallet()
|
||||
.storage()
|
||||
.user_data
|
||||
.group_key_holder("alice-group")
|
||||
.unwrap();
|
||||
let bob_holder = ctx
|
||||
.wallet()
|
||||
.storage()
|
||||
.user_data
|
||||
.group_key_holder("bob-copy")
|
||||
.unwrap();
|
||||
|
||||
let seed = [42_u8; 32];
|
||||
let alice_npk = alice_holder
|
||||
.derive_keys_for_shared_account(&seed)
|
||||
.generate_nullifier_public_key();
|
||||
let bob_npk = bob_holder
|
||||
.derive_keys_for_shared_account(&seed)
|
||||
.generate_nullifier_public_key();
|
||||
|
||||
assert_eq!(
|
||||
alice_npk, bob_npk,
|
||||
"Key agreement: same GMS produces same keys"
|
||||
);
|
||||
|
||||
info!("Key agreement verified via invite/join");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fund a shared account from a public account via auth-transfer, then sync.
|
||||
/// TODO: Requires auth-transfer init to work with shared accounts (authorization flow).
|
||||
#[test]
|
||||
#[ignore = "Requires auth-transfer init to work with shared accounts (authorization flow)"]
|
||||
async fn fund_shared_account_from_public() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
// Create group and shared account
|
||||
let command = Command::Group(GroupSubcommand::New {
|
||||
name: "fund-group".into(),
|
||||
});
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
|
||||
let command = Command::Account(AccountSubcommand::New(NewSubcommand::PrivateGms {
|
||||
group: "fund-group".into(),
|
||||
label: None,
|
||||
pda: false,
|
||||
seed: None,
|
||||
program_id: None,
|
||||
}));
|
||||
let result = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: shared_id,
|
||||
} = result
|
||||
else {
|
||||
anyhow::bail!("Expected RegisterAccount return value");
|
||||
};
|
||||
|
||||
// Initialize the shared account under auth-transfer
|
||||
let command = Command::AuthTransfer(AuthTransferSubcommand::Init {
|
||||
account_id: Some(format!("Private/{shared_id}")),
|
||||
account_label: None,
|
||||
});
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
// Fund from a public account
|
||||
let from_public = ctx.existing_public_accounts()[0];
|
||||
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
|
||||
from: Some(format_public_account_id(from_public)),
|
||||
from_label: None,
|
||||
to: Some(format!("Private/{shared_id}")),
|
||||
to_label: None,
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_identifier: None,
|
||||
amount: 100,
|
||||
});
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
// Sync private accounts
|
||||
let command = Command::Account(AccountSubcommand::SyncPrivate);
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
|
||||
// Verify the shared account was updated
|
||||
let entry = ctx
|
||||
.wallet()
|
||||
.storage()
|
||||
.user_data
|
||||
.shared_private_account(&shared_id)
|
||||
.context("Shared account not found after sync")?;
|
||||
|
||||
info!(
|
||||
"Shared account balance after funding: {}",
|
||||
entry.account.balance
|
||||
);
|
||||
assert_eq!(
|
||||
entry.account.balance, 100,
|
||||
"Shared account should have received 100"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -2,7 +2,7 @@ use aes_gcm::{Aes256Gcm, KeyInit as _, aead::Aead as _};
|
||||
use nssa_core::{
|
||||
SharedSecretKey,
|
||||
encryption::{Scalar, shared_key_derivation::Secp256k1Point},
|
||||
program::PdaSeed,
|
||||
program::{PdaSeed, ProgramId},
|
||||
};
|
||||
use rand::{RngCore as _, rngs::OsRng};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -12,10 +12,30 @@ use super::secret_holders::{PrivateKeyHolder, SecretSpendingKey};
|
||||
|
||||
/// Public key used to seal a `GroupKeyHolder` for distribution to a recipient.
|
||||
///
|
||||
/// Structurally identical to `ViewingPublicKey` (both are secp256k1 points), but given
|
||||
/// a distinct alias to clarify intent: viewing keys encrypt account state, sealing keys
|
||||
/// encrypt the GMS for off-chain distribution.
|
||||
pub type SealingPublicKey = Secp256k1Point;
|
||||
/// Wraps a secp256k1 point but is a distinct type from `ViewingPublicKey` to enforce
|
||||
/// key separation: viewing keys encrypt account state, sealing keys encrypt the GMS
|
||||
/// for off-chain distribution.
|
||||
pub struct SealingPublicKey(Secp256k1Point);
|
||||
|
||||
impl SealingPublicKey {
|
||||
/// Derive the sealing public key from a secret scalar.
|
||||
#[must_use]
|
||||
pub fn from_scalar(scalar: Scalar) -> Self {
|
||||
Self(Secp256k1Point::from_scalar(scalar))
|
||||
}
|
||||
|
||||
/// Construct from raw serialized bytes (e.g. received from another wallet).
|
||||
#[must_use]
|
||||
pub const fn from_bytes(bytes: Vec<u8>) -> Self {
|
||||
Self(Secp256k1Point(bytes))
|
||||
}
|
||||
|
||||
/// Returns the raw bytes for display or transmission.
|
||||
#[must_use]
|
||||
pub fn to_bytes(&self) -> &[u8] {
|
||||
&self.0.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Secret key used to unseal a `GroupKeyHolder` received from another member.
|
||||
pub type SealingSecretKey = Scalar;
|
||||
@ -83,28 +103,54 @@ impl GroupKeyHolder {
|
||||
|
||||
/// Derive a per-PDA [`SecretSpendingKey`] by mixing the seed into the SHA-256 input.
|
||||
///
|
||||
/// Each distinct `pda_seed` produces a distinct SSK in the full 256-bit space, so
|
||||
/// adversarial seed-grinding cannot collide two PDAs' derived keys under the same
|
||||
/// Each distinct `(program_id, pda_seed)` pair produces a distinct SSK in the full 256-bit
|
||||
/// space, so adversarial seed-grinding cannot collide two PDAs' derived keys under the same
|
||||
/// group. Uses the codebase's 32-byte protocol-versioned domain-separation convention.
|
||||
fn secret_spending_key_for_pda(&self, pda_seed: &PdaSeed) -> SecretSpendingKey {
|
||||
fn secret_spending_key_for_pda(
|
||||
&self,
|
||||
program_id: &ProgramId,
|
||||
pda_seed: &PdaSeed,
|
||||
) -> SecretSpendingKey {
|
||||
const PREFIX: &[u8; 32] = b"/LEE/v0.3/GroupKeyDerivation/SSK";
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
hasher.update(PREFIX);
|
||||
hasher.update(self.gms);
|
||||
for word in program_id {
|
||||
hasher.update(word.to_le_bytes());
|
||||
}
|
||||
hasher.update(pda_seed.as_ref());
|
||||
SecretSpendingKey(hasher.finalize_fixed().into())
|
||||
}
|
||||
|
||||
/// Derive keys for a specific PDA.
|
||||
/// Derive keys for a specific PDA under a given program.
|
||||
///
|
||||
/// All controllers holding the same GMS independently derive the same keys for the
|
||||
/// same PDA because the derivation is deterministic in (GMS, seed).
|
||||
/// same `(program_id, seed)` because the derivation is deterministic.
|
||||
#[must_use]
|
||||
pub fn derive_keys_for_pda(&self, pda_seed: &PdaSeed) -> PrivateKeyHolder {
|
||||
self.secret_spending_key_for_pda(pda_seed)
|
||||
pub fn derive_keys_for_pda(
|
||||
&self,
|
||||
program_id: &ProgramId,
|
||||
pda_seed: &PdaSeed,
|
||||
) -> PrivateKeyHolder {
|
||||
self.secret_spending_key_for_pda(program_id, pda_seed)
|
||||
.produce_private_key_holder(None)
|
||||
}
|
||||
|
||||
/// Derive keys for a shared regular (non-PDA) private account.
|
||||
///
|
||||
/// Uses a distinct domain separator from `derive_keys_for_pda` to prevent cross-domain
|
||||
/// key collisions. The `derivation_seed` should be a stable, unique 32-byte value
|
||||
/// (e.g. derived deterministically from the account's identifier).
|
||||
#[must_use]
|
||||
pub fn derive_keys_for_shared_account(&self, derivation_seed: &[u8; 32]) -> PrivateKeyHolder {
|
||||
const PREFIX: &[u8; 32] = b"/LEE/v0.3/GroupKeyDerivation/SHA";
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
hasher.update(PREFIX);
|
||||
hasher.update(self.gms);
|
||||
hasher.update(derivation_seed);
|
||||
SecretSpendingKey(hasher.finalize_fixed().into()).produce_private_key_holder(None)
|
||||
}
|
||||
|
||||
/// Encrypts this holder's GMS under the recipient's [`SealingPublicKey`].
|
||||
///
|
||||
/// Uses an ephemeral ECDH key exchange to derive a shared secret, then AES-256-GCM
|
||||
@ -118,7 +164,7 @@ impl GroupKeyHolder {
|
||||
let mut ephemeral_scalar: Scalar = [0_u8; 32];
|
||||
OsRng.fill_bytes(&mut ephemeral_scalar);
|
||||
let ephemeral_pubkey = Secp256k1Point::from_scalar(ephemeral_scalar);
|
||||
let shared = SharedSecretKey::new(&ephemeral_scalar, recipient_key);
|
||||
let shared = SharedSecretKey::new(&ephemeral_scalar, &recipient_key.0);
|
||||
let aes_key = Self::seal_kdf(&shared);
|
||||
let cipher = Aes256Gcm::new(&aes_key.into());
|
||||
|
||||
@ -195,6 +241,8 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
const TEST_PROGRAM_ID: ProgramId = [9; 8];
|
||||
|
||||
/// Two holders from the same GMS derive identical keys for the same PDA seed.
|
||||
#[test]
|
||||
fn same_gms_same_seed_produces_same_keys() {
|
||||
@ -203,8 +251,8 @@ mod tests {
|
||||
let holder_b = GroupKeyHolder::from_gms(gms);
|
||||
let seed = PdaSeed::new([1; 32]);
|
||||
|
||||
let keys_a = holder_a.derive_keys_for_pda(&seed);
|
||||
let keys_b = holder_b.derive_keys_for_pda(&seed);
|
||||
let keys_a = holder_a.derive_keys_for_pda(&TEST_PROGRAM_ID, &seed);
|
||||
let keys_b = holder_b.derive_keys_for_pda(&TEST_PROGRAM_ID, &seed);
|
||||
|
||||
assert_eq!(
|
||||
keys_a.generate_nullifier_public_key().to_byte_array(),
|
||||
@ -220,10 +268,10 @@ mod tests {
|
||||
let seed_b = PdaSeed::new([2; 32]);
|
||||
|
||||
let npk_a = holder
|
||||
.derive_keys_for_pda(&seed_a)
|
||||
.derive_keys_for_pda(&TEST_PROGRAM_ID, &seed_a)
|
||||
.generate_nullifier_public_key();
|
||||
let npk_b = holder
|
||||
.derive_keys_for_pda(&seed_b)
|
||||
.derive_keys_for_pda(&TEST_PROGRAM_ID, &seed_b)
|
||||
.generate_nullifier_public_key();
|
||||
|
||||
assert_ne!(npk_a.to_byte_array(), npk_b.to_byte_array());
|
||||
@ -237,10 +285,10 @@ mod tests {
|
||||
let seed = PdaSeed::new([1; 32]);
|
||||
|
||||
let npk_a = holder_a
|
||||
.derive_keys_for_pda(&seed)
|
||||
.derive_keys_for_pda(&TEST_PROGRAM_ID, &seed)
|
||||
.generate_nullifier_public_key();
|
||||
let npk_b = holder_b
|
||||
.derive_keys_for_pda(&seed)
|
||||
.derive_keys_for_pda(&TEST_PROGRAM_ID, &seed)
|
||||
.generate_nullifier_public_key();
|
||||
|
||||
assert_ne!(npk_a.to_byte_array(), npk_b.to_byte_array());
|
||||
@ -254,10 +302,10 @@ mod tests {
|
||||
let seed = PdaSeed::new([1; 32]);
|
||||
|
||||
let npk_original = original
|
||||
.derive_keys_for_pda(&seed)
|
||||
.derive_keys_for_pda(&TEST_PROGRAM_ID, &seed)
|
||||
.generate_nullifier_public_key();
|
||||
let npk_restored = restored
|
||||
.derive_keys_for_pda(&seed)
|
||||
.derive_keys_for_pda(&TEST_PROGRAM_ID, &seed)
|
||||
.generate_nullifier_public_key();
|
||||
|
||||
assert_eq!(npk_original.to_byte_array(), npk_restored.to_byte_array());
|
||||
@ -269,7 +317,7 @@ mod tests {
|
||||
let holder = GroupKeyHolder::from_gms([42_u8; 32]);
|
||||
let seed = PdaSeed::new([1; 32]);
|
||||
let npk = holder
|
||||
.derive_keys_for_pda(&seed)
|
||||
.derive_keys_for_pda(&TEST_PROGRAM_ID, &seed)
|
||||
.generate_nullifier_public_key();
|
||||
|
||||
assert_ne!(npk, NullifierPublicKey([0; 32]));
|
||||
@ -289,19 +337,17 @@ mod tests {
|
||||
|
||||
let holder = GroupKeyHolder::from_gms(gms);
|
||||
let npk = holder
|
||||
.derive_keys_for_pda(&seed)
|
||||
.derive_keys_for_pda(&TEST_PROGRAM_ID, &seed)
|
||||
.generate_nullifier_public_key();
|
||||
let account_id = AccountId::for_private_pda(&program_id, &seed, &npk, u128::MAX);
|
||||
|
||||
let expected_npk = NullifierPublicKey([
|
||||
185, 161, 225, 224, 20, 156, 173, 0, 6, 173, 74, 136, 16, 88, 71, 154, 101, 160, 224,
|
||||
162, 247, 98, 183, 210, 118, 130, 143, 237, 20, 112, 111, 114,
|
||||
]);
|
||||
|
||||
let expected_account_id = AccountId::new([
|
||||
251, 228, 245, 3, 160, 134, 97, 69, 187, 157, 170, 192, 165, 216, 166, 79, 179, 187,
|
||||
125, 146, 36, 192, 232, 110, 198, 47, 24, 10, 223, 25, 108, 5,
|
||||
136, 176, 234, 71, 208, 8, 143, 142, 126, 155, 132, 18, 71, 27, 88, 56, 100, 90, 79,
|
||||
215, 76, 92, 60, 166, 104, 35, 51, 91, 16, 114, 188, 112,
|
||||
]);
|
||||
// AccountId is derived from (program_id, seed, npk), so it changes when npk changes.
|
||||
// We verify npk is pinned, and AccountId is deterministically derived from it.
|
||||
let expected_account_id = AccountId::for_private_pda(&program_id, &seed, &expected_npk, u128::MAX);
|
||||
|
||||
assert_eq!(npk, expected_npk);
|
||||
assert_eq!(account_id, expected_account_id);
|
||||
@ -319,10 +365,10 @@ mod tests {
|
||||
|
||||
let seed = PdaSeed::new([1; 32]);
|
||||
let npk_original = original
|
||||
.derive_keys_for_pda(&seed)
|
||||
.derive_keys_for_pda(&TEST_PROGRAM_ID, &seed)
|
||||
.generate_nullifier_public_key();
|
||||
let npk_restored = restored
|
||||
.derive_keys_for_pda(&seed)
|
||||
.derive_keys_for_pda(&TEST_PROGRAM_ID, &seed)
|
||||
.generate_nullifier_public_key();
|
||||
|
||||
assert_eq!(npk_original, npk_restored);
|
||||
@ -340,7 +386,7 @@ mod tests {
|
||||
let seed = PdaSeed::new([5; 32]);
|
||||
|
||||
let group_npk = GroupKeyHolder::from_gms(shared_bytes)
|
||||
.derive_keys_for_pda(&seed)
|
||||
.derive_keys_for_pda(&TEST_PROGRAM_ID, &seed)
|
||||
.generate_nullifier_public_key();
|
||||
|
||||
let personal_npk = SecretSpendingKey(shared_bytes)
|
||||
@ -360,7 +406,7 @@ mod tests {
|
||||
let recipient_vpk = recipient_keys.generate_viewing_public_key();
|
||||
let recipient_vsk = recipient_keys.viewing_secret_key;
|
||||
|
||||
let sealed = holder.seal_for(&recipient_vpk);
|
||||
let sealed = holder.seal_for(&SealingPublicKey::from_bytes(recipient_vpk.0));
|
||||
let restored = GroupKeyHolder::unseal(&sealed, &recipient_vsk).expect("unseal");
|
||||
|
||||
assert_eq!(restored.dangerous_raw_gms(), holder.dangerous_raw_gms());
|
||||
@ -368,10 +414,10 @@ mod tests {
|
||||
let seed = PdaSeed::new([1; 32]);
|
||||
assert_eq!(
|
||||
holder
|
||||
.derive_keys_for_pda(&seed)
|
||||
.derive_keys_for_pda(&TEST_PROGRAM_ID, &seed)
|
||||
.generate_nullifier_public_key(),
|
||||
restored
|
||||
.derive_keys_for_pda(&seed)
|
||||
.derive_keys_for_pda(&TEST_PROGRAM_ID, &seed)
|
||||
.generate_nullifier_public_key(),
|
||||
);
|
||||
}
|
||||
@ -391,7 +437,7 @@ mod tests {
|
||||
.produce_private_key_holder(None)
|
||||
.viewing_secret_key;
|
||||
|
||||
let sealed = holder.seal_for(&recipient_vpk);
|
||||
let sealed = holder.seal_for(&SealingPublicKey::from_bytes(recipient_vpk.0));
|
||||
let result = GroupKeyHolder::unseal(&sealed, &wrong_vsk);
|
||||
assert!(matches!(result, Err(super::SealError::DecryptionFailed)));
|
||||
}
|
||||
@ -406,7 +452,7 @@ mod tests {
|
||||
let recipient_vpk = recipient_keys.generate_viewing_public_key();
|
||||
let recipient_vsk = recipient_keys.viewing_secret_key;
|
||||
|
||||
let mut sealed = holder.seal_for(&recipient_vpk);
|
||||
let mut sealed = holder.seal_for(&SealingPublicKey::from_bytes(recipient_vpk.0));
|
||||
// Flip a byte in the ciphertext portion (after ephemeral_pubkey + nonce)
|
||||
let last = sealed.len() - 1;
|
||||
sealed[last] ^= 0xFF;
|
||||
@ -425,8 +471,9 @@ mod tests {
|
||||
.produce_private_key_holder(None)
|
||||
.generate_viewing_public_key();
|
||||
|
||||
let sealed_a = holder.seal_for(&recipient_vpk);
|
||||
let sealed_b = holder.seal_for(&recipient_vpk);
|
||||
let sealing_key = SealingPublicKey::from_bytes(recipient_vpk.0);
|
||||
let sealed_a = holder.seal_for(&sealing_key);
|
||||
let sealed_b = holder.seal_for(&sealing_key);
|
||||
assert_ne!(sealed_a, sealed_b);
|
||||
}
|
||||
|
||||
@ -454,7 +501,7 @@ mod tests {
|
||||
.iter()
|
||||
.map(|gms| {
|
||||
GroupKeyHolder::from_gms(*gms)
|
||||
.derive_keys_for_pda(&seed)
|
||||
.derive_keys_for_pda(&TEST_PROGRAM_ID, &seed)
|
||||
.generate_nullifier_public_key()
|
||||
})
|
||||
.collect();
|
||||
@ -479,7 +526,7 @@ mod tests {
|
||||
let program_id: nssa_core::program::ProgramId = [1; 8];
|
||||
|
||||
// Derive Alice's keys
|
||||
let alice_keys = alice_holder.derive_keys_for_pda(&pda_seed);
|
||||
let alice_keys = alice_holder.derive_keys_for_pda(&TEST_PROGRAM_ID, &pda_seed);
|
||||
let alice_npk = alice_keys.generate_nullifier_public_key();
|
||||
|
||||
// Seal GMS for Bob using Bob's viewing key, Bob unseals
|
||||
@ -488,13 +535,13 @@ mod tests {
|
||||
let bob_vpk = bob_keys.generate_viewing_public_key();
|
||||
let bob_vsk = bob_keys.viewing_secret_key;
|
||||
|
||||
let sealed = alice_holder.seal_for(&bob_vpk);
|
||||
let sealed = alice_holder.seal_for(&SealingPublicKey::from_bytes(bob_vpk.0));
|
||||
let bob_holder =
|
||||
GroupKeyHolder::unseal(&sealed, &bob_vsk).expect("Bob should unseal the GMS");
|
||||
|
||||
// Key agreement: both derive identical NPK and AccountId
|
||||
let bob_npk = bob_holder
|
||||
.derive_keys_for_pda(&pda_seed)
|
||||
.derive_keys_for_pda(&TEST_PROGRAM_ID, &pda_seed)
|
||||
.generate_nullifier_public_key();
|
||||
assert_eq!(alice_npk, bob_npk);
|
||||
|
||||
@ -502,4 +549,52 @@ mod tests {
|
||||
let bob_account_id = AccountId::for_private_pda(&program_id, &pda_seed, &bob_npk, 0);
|
||||
assert_eq!(alice_account_id, bob_account_id);
|
||||
}
|
||||
|
||||
/// Same GMS + same derivation seed produces same keys for shared accounts.
|
||||
#[test]
|
||||
fn shared_account_same_gms_same_seed_produces_same_keys() {
|
||||
let gms = [42_u8; 32];
|
||||
let derivation_seed = [1_u8; 32];
|
||||
let holder_a = GroupKeyHolder::from_gms(gms);
|
||||
let holder_b = GroupKeyHolder::from_gms(gms);
|
||||
|
||||
let npk_a = holder_a
|
||||
.derive_keys_for_shared_account(&derivation_seed)
|
||||
.generate_nullifier_public_key();
|
||||
let npk_b = holder_b
|
||||
.derive_keys_for_shared_account(&derivation_seed)
|
||||
.generate_nullifier_public_key();
|
||||
|
||||
assert_eq!(npk_a, npk_b);
|
||||
}
|
||||
|
||||
/// Different derivation seeds produce different keys for shared accounts.
|
||||
#[test]
|
||||
fn shared_account_different_seeds_produce_different_keys() {
|
||||
let holder = GroupKeyHolder::from_gms([42_u8; 32]);
|
||||
let npk_a = holder
|
||||
.derive_keys_for_shared_account(&[1_u8; 32])
|
||||
.generate_nullifier_public_key();
|
||||
let npk_b = holder
|
||||
.derive_keys_for_shared_account(&[2_u8; 32])
|
||||
.generate_nullifier_public_key();
|
||||
|
||||
assert_ne!(npk_a, npk_b);
|
||||
}
|
||||
|
||||
/// PDA and shared account derivations from the same GMS + same bytes never collide.
|
||||
#[test]
|
||||
fn pda_and_shared_derivations_do_not_collide() {
|
||||
let holder = GroupKeyHolder::from_gms([42_u8; 32]);
|
||||
let bytes = [1_u8; 32];
|
||||
|
||||
let pda_npk = holder
|
||||
.derive_keys_for_pda(&TEST_PROGRAM_ID, &PdaSeed::new(bytes))
|
||||
.generate_nullifier_public_key();
|
||||
let shared_npk = holder
|
||||
.derive_keys_for_shared_account(&bytes)
|
||||
.generate_nullifier_public_key();
|
||||
|
||||
assert_ne!(pda_npk, shared_npk);
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,7 +21,22 @@ pub struct UserPrivateAccountData {
|
||||
pub accounts: Vec<(PrivateAccountKind, Account)>,
|
||||
}
|
||||
|
||||
/// Metadata for a shared account (GMS-derived), stored alongside the cached plaintext state.
|
||||
/// The group label and identifier (or PDA seed) are needed to re-derive keys during sync.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct SharedAccountEntry {
|
||||
pub group_label: String,
|
||||
pub identifier: Identifier,
|
||||
/// For PDA accounts, the seed and program ID used to derive keys via `derive_keys_for_pda`.
|
||||
/// `None` for regular shared accounts (keys derived from identifier via derivation seed).
|
||||
#[serde(default)]
|
||||
pub pda_seed: Option<nssa_core::program::PdaSeed>,
|
||||
#[serde(default)]
|
||||
pub pda_program_id: Option<nssa_core::program::ProgramId>,
|
||||
pub account: Account,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct NSSAUserData {
|
||||
/// Default public accounts.
|
||||
pub default_pub_account_signing_keys: BTreeMap<nssa::AccountId, nssa::PrivateKey>,
|
||||
@ -31,17 +46,16 @@ pub struct NSSAUserData {
|
||||
pub public_key_tree: KeyTreePublic,
|
||||
/// Tree of private keys.
|
||||
pub private_key_tree: KeyTreePrivate,
|
||||
/// Group key holders for private PDA groups, keyed by a human-readable label.
|
||||
/// Defaults to empty for backward compatibility with wallets that predate group PDAs.
|
||||
/// An older wallet binary that re-serializes this struct will drop the field.
|
||||
#[serde(default)]
|
||||
/// Group key holders for shared account management, keyed by a human-readable label.
|
||||
pub group_key_holders: BTreeMap<String, GroupKeyHolder>,
|
||||
/// Cached plaintext state of private PDA accounts, keyed by `AccountId`.
|
||||
/// Updated after each private PDA transaction by decrypting the circuit output.
|
||||
/// The sequencer only stores encrypted commitments, so this local cache is the
|
||||
/// only source of plaintext state for private PDAs.
|
||||
#[serde(default, alias = "group_pda_accounts")]
|
||||
pub pda_accounts: BTreeMap<nssa::AccountId, nssa_core::account::Account>,
|
||||
/// Cached plaintext state of shared private accounts (PDAs and regular shared accounts),
|
||||
/// keyed by `AccountId`. Each entry stores the group label and identifier needed
|
||||
/// to re-derive keys during sync.
|
||||
pub shared_private_accounts: BTreeMap<nssa::AccountId, SharedAccountEntry>,
|
||||
/// Dedicated sealing secret key for GMS distribution. Generated once via
|
||||
/// `wallet group new-sealing-key`. The corresponding public key is shared with
|
||||
/// group members so they can seal GMS for this wallet.
|
||||
pub sealing_secret_key: Option<nssa_core::encryption::Scalar>,
|
||||
}
|
||||
|
||||
impl NSSAUserData {
|
||||
@ -102,7 +116,8 @@ impl NSSAUserData {
|
||||
public_key_tree,
|
||||
private_key_tree,
|
||||
group_key_holders: BTreeMap::new(),
|
||||
pda_accounts: BTreeMap::new(),
|
||||
shared_private_accounts: BTreeMap::new(),
|
||||
sealing_secret_key: None,
|
||||
})
|
||||
}
|
||||
|
||||
@ -227,6 +242,42 @@ impl NSSAUserData {
|
||||
pub fn insert_group_key_holder(&mut self, label: String, holder: GroupKeyHolder) {
|
||||
self.group_key_holders.insert(label, holder);
|
||||
}
|
||||
|
||||
/// Returns the cached account for a shared private account, if it exists.
|
||||
#[must_use]
|
||||
pub fn shared_private_account(
|
||||
&self,
|
||||
account_id: &nssa::AccountId,
|
||||
) -> Option<&SharedAccountEntry> {
|
||||
self.shared_private_accounts.get(account_id)
|
||||
}
|
||||
|
||||
/// Inserts or replaces a shared private account entry.
|
||||
pub fn insert_shared_private_account(
|
||||
&mut self,
|
||||
account_id: nssa::AccountId,
|
||||
entry: SharedAccountEntry,
|
||||
) {
|
||||
self.shared_private_accounts.insert(account_id, entry);
|
||||
}
|
||||
|
||||
/// Updates the cached account state for a shared private account.
|
||||
pub fn update_shared_private_account_state(
|
||||
&mut self,
|
||||
account_id: &nssa::AccountId,
|
||||
account: nssa_core::account::Account,
|
||||
) {
|
||||
if let Some(entry) = self.shared_private_accounts.get_mut(account_id) {
|
||||
entry.account = account;
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterates over all shared private accounts.
|
||||
pub fn shared_private_accounts_iter(
|
||||
&self,
|
||||
) -> impl Iterator<Item = (&nssa::AccountId, &SharedAccountEntry)> {
|
||||
self.shared_private_accounts.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for NSSAUserData {
|
||||
@ -264,6 +315,92 @@ mod tests {
|
||||
fn group_key_holders_default_empty() {
|
||||
let user_data = NSSAUserData::default();
|
||||
assert!(user_data.group_key_holders.is_empty());
|
||||
assert!(user_data.shared_private_accounts.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_account_entry_serde_round_trip() {
|
||||
use nssa_core::program::PdaSeed;
|
||||
|
||||
let entry = SharedAccountEntry {
|
||||
group_label: String::from("test-group"),
|
||||
identifier: 42,
|
||||
pda_seed: None,
|
||||
pda_program_id: None,
|
||||
account: nssa_core::account::Account::default(),
|
||||
};
|
||||
let encoded = bincode::serialize(&entry).expect("serialize");
|
||||
let decoded: SharedAccountEntry = bincode::deserialize(&encoded).expect("deserialize");
|
||||
assert_eq!(decoded.group_label, "test-group");
|
||||
assert_eq!(decoded.identifier, 42);
|
||||
assert!(decoded.pda_seed.is_none());
|
||||
|
||||
let pda_entry = SharedAccountEntry {
|
||||
group_label: String::from("pda-group"),
|
||||
identifier: u128::MAX,
|
||||
pda_seed: Some(PdaSeed::new([7_u8; 32])),
|
||||
pda_program_id: Some([9; 8]),
|
||||
account: nssa_core::account::Account::default(),
|
||||
};
|
||||
let pda_encoded = bincode::serialize(&pda_entry).expect("serialize pda");
|
||||
let pda_decoded: SharedAccountEntry =
|
||||
bincode::deserialize(&pda_encoded).expect("deserialize pda");
|
||||
assert_eq!(pda_decoded.group_label, "pda-group");
|
||||
assert_eq!(pda_decoded.identifier, u128::MAX);
|
||||
assert_eq!(pda_decoded.pda_seed.unwrap(), PdaSeed::new([7_u8; 32]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_account_entry_none_pda_seed_round_trips() {
|
||||
// Verify that an entry with pda_seed=None serializes and deserializes correctly,
|
||||
// confirming the #[serde(default)] attribute works for backward compatibility.
|
||||
let entry = SharedAccountEntry {
|
||||
group_label: String::from("old"),
|
||||
identifier: 1,
|
||||
pda_seed: None,
|
||||
pda_program_id: None,
|
||||
account: nssa_core::account::Account::default(),
|
||||
};
|
||||
let encoded = bincode::serialize(&entry).expect("serialize");
|
||||
let decoded: SharedAccountEntry = bincode::deserialize(&encoded).expect("deserialize");
|
||||
assert_eq!(decoded.group_label, "old");
|
||||
assert_eq!(decoded.identifier, 1);
|
||||
assert!(decoded.pda_seed.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_account_derives_consistent_keys_from_group() {
|
||||
use nssa_core::program::PdaSeed;
|
||||
|
||||
let mut user_data = NSSAUserData::default();
|
||||
let gms_holder = GroupKeyHolder::from_gms([42_u8; 32]);
|
||||
user_data.insert_group_key_holder(String::from("my-group"), gms_holder);
|
||||
|
||||
let holder = user_data.group_key_holder("my-group").unwrap();
|
||||
|
||||
// Regular shared account: derive via tag
|
||||
let tag = [1_u8; 32];
|
||||
let keys_a = holder.derive_keys_for_shared_account(&tag);
|
||||
let keys_b = holder.derive_keys_for_shared_account(&tag);
|
||||
assert_eq!(
|
||||
keys_a.generate_nullifier_public_key(),
|
||||
keys_b.generate_nullifier_public_key(),
|
||||
);
|
||||
|
||||
// PDA shared account: derive via seed
|
||||
let seed = PdaSeed::new([2_u8; 32]);
|
||||
let pda_keys_a = holder.derive_keys_for_pda(&[9; 8], &seed);
|
||||
let pda_keys_b = holder.derive_keys_for_pda(&[9; 8], &seed);
|
||||
assert_eq!(
|
||||
pda_keys_a.generate_nullifier_public_key(),
|
||||
pda_keys_b.generate_nullifier_public_key(),
|
||||
);
|
||||
|
||||
// PDA and shared derivations don't collide
|
||||
assert_ne!(
|
||||
keys_a.generate_nullifier_public_key(),
|
||||
pda_keys_a.generate_nullifier_public_key(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@ -470,12 +470,13 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Group PDA deposit: creates a new PDA and transfers balance from the
|
||||
/// counterparty. Both accounts owned by `private_pda_spender`.
|
||||
/// PDA init: initializes a new PDA under `authenticated_transfer`'s ownership.
|
||||
/// The `auth_transfer_proxy` program chains to `authenticated_transfer` with `pda_seeds`
|
||||
/// to establish authorization and the private PDA binding.
|
||||
#[test]
|
||||
fn group_pda_deposit() {
|
||||
let program = Program::private_pda_spender();
|
||||
let noop = Program::noop();
|
||||
fn private_pda_init() {
|
||||
let program = Program::auth_transfer_proxy();
|
||||
let auth_transfer = Program::authenticated_transfer_program();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
@ -485,92 +486,129 @@ mod tests {
|
||||
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 0);
|
||||
let pda_pre = AccountWithMetadata::new(Account::default(), false, pda_id);
|
||||
|
||||
// Sender (mask 0, public, owned by this program, has balance)
|
||||
let auth_id = auth_transfer.id();
|
||||
let program_with_deps =
|
||||
ProgramWithDependencies::new(program, [(auth_id, auth_transfer)].into());
|
||||
|
||||
// is_withdraw=false triggers init path (1 pre-state)
|
||||
let instruction = Program::serialize_instruction((seed, auth_id, 0_u128, false)).unwrap();
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![pda_pre],
|
||||
instruction,
|
||||
vec![InputAccountIdentity::PrivatePdaInit {
|
||||
npk,
|
||||
ssk: shared_secret_pda,
|
||||
identifier: 0,
|
||||
}],
|
||||
&program_with_deps,
|
||||
);
|
||||
|
||||
let (output, _proof) = result.expect("PDA init should succeed");
|
||||
assert_eq!(output.new_commitments.len(), 1);
|
||||
}
|
||||
|
||||
/// PDA withdraw: chains to `authenticated_transfer` to move balance from PDA to recipient.
|
||||
/// Uses a default PDA (amount=0) because testing with a pre-funded PDA requires a
|
||||
/// two-tx sequence with membership proofs.
|
||||
#[test]
|
||||
fn private_pda_withdraw() {
|
||||
let program = Program::auth_transfer_proxy();
|
||||
let auth_transfer = Program::authenticated_transfer_program();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
let shared_secret_pda = SharedSecretKey::new(&[55; 32], &keys.vpk());
|
||||
|
||||
// PDA (new, private PDA)
|
||||
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 0);
|
||||
let pda_pre = AccountWithMetadata::new(Account::default(), false, pda_id);
|
||||
|
||||
// Recipient (public)
|
||||
let recipient_id = AccountId::new([88; 32]);
|
||||
let recipient_pre = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: auth_transfer.id(),
|
||||
balance: 10000,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
recipient_id,
|
||||
);
|
||||
|
||||
let auth_id = auth_transfer.id();
|
||||
let program_with_deps =
|
||||
ProgramWithDependencies::new(program, [(auth_id, auth_transfer)].into());
|
||||
|
||||
// is_withdraw=true, amount=0 (PDA has no balance yet)
|
||||
let instruction = Program::serialize_instruction((seed, auth_id, 0_u128, true)).unwrap();
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![pda_pre, recipient_pre],
|
||||
instruction,
|
||||
vec![
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
npk,
|
||||
ssk: shared_secret_pda,
|
||||
identifier: 0,
|
||||
},
|
||||
InputAccountIdentity::Public,
|
||||
],
|
||||
&program_with_deps,
|
||||
);
|
||||
|
||||
let (output, _proof) = result.expect("PDA withdraw should succeed");
|
||||
assert_eq!(output.new_commitments.len(), 1);
|
||||
}
|
||||
|
||||
/// Shared regular private account: receives funds via `authenticated_transfer` directly,
|
||||
/// no custom program needed. This demonstrates the non-PDA shared account flow where
|
||||
/// keys are derived from GMS via `derive_keys_for_shared_account`. The shared account
|
||||
/// uses the standard unauthorized private account path and works with auth-transfer's
|
||||
/// transfer path like any other private account.
|
||||
#[test]
|
||||
fn shared_account_receives_via_auth_transfer() {
|
||||
let program = Program::authenticated_transfer_program();
|
||||
let shared_keys = test_private_account_keys_1();
|
||||
let shared_npk = shared_keys.npk();
|
||||
let shared_identifier: u128 = 42;
|
||||
let shared_secret = SharedSecretKey::new(&[55; 32], &shared_keys.vpk());
|
||||
|
||||
// Sender: public account with balance, owned by auth-transfer
|
||||
let sender_id = AccountId::new([99; 32]);
|
||||
let sender_pre = AccountWithMetadata::new(
|
||||
let sender = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: program.id(),
|
||||
balance: 10000,
|
||||
balance: 1000,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
sender_id,
|
||||
);
|
||||
|
||||
let noop_id = noop.id();
|
||||
let program_with_deps = ProgramWithDependencies::new(program, [(noop_id, noop)].into());
|
||||
// Recipient: shared private account (new, unauthorized)
|
||||
let shared_account_id = AccountId::from((&shared_npk, shared_identifier));
|
||||
let recipient = AccountWithMetadata::new(Account::default(), false, shared_account_id);
|
||||
|
||||
let instruction = Program::serialize_instruction((seed, noop_id, 500_u128, true)).unwrap();
|
||||
|
||||
// PDA is mask 3 (private PDA), sender is mask 0 (public).
|
||||
// The noop chained call is required to establish the mask-3 (seed, npk) binding
|
||||
// that the circuit enforces for private PDAs. Without a caller providing pda_seeds,
|
||||
// the circuit's binding check rejects the account.
|
||||
let result = execute_and_prove(
|
||||
vec![pda_pre, sender_pre],
|
||||
instruction,
|
||||
vec![
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
npk,
|
||||
ssk: shared_secret_pda,
|
||||
identifier: 0,
|
||||
},
|
||||
InputAccountIdentity::Public,
|
||||
],
|
||||
&program_with_deps,
|
||||
);
|
||||
|
||||
let (output, _proof) = result.expect("group PDA deposit should succeed");
|
||||
// Only PDA (mask 3) produces a commitment; sender (mask 0) is public.
|
||||
assert_eq!(output.new_commitments.len(), 1);
|
||||
}
|
||||
|
||||
/// Group PDA spend binding: the noop chained call with `pda_seeds` establishes
|
||||
/// the mask-3 binding for an existing-but-default PDA. Uses amount=0 because
|
||||
/// testing with a pre-funded PDA requires a two-tx sequence with membership proofs.
|
||||
#[test]
|
||||
fn group_pda_spend_binding() {
|
||||
let program = Program::private_pda_spender();
|
||||
let noop = Program::noop();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
let shared_secret_pda = SharedSecretKey::new(&[55; 32], &keys.vpk());
|
||||
|
||||
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 0);
|
||||
let pda_pre = AccountWithMetadata::new(Account::default(), false, pda_id);
|
||||
|
||||
let bob_id = AccountId::new([88; 32]);
|
||||
let bob_pre = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: program.id(),
|
||||
balance: 10000,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
bob_id,
|
||||
);
|
||||
|
||||
let noop_id = noop.id();
|
||||
let program_with_deps = ProgramWithDependencies::new(program, [(noop_id, noop)].into());
|
||||
|
||||
let instruction = Program::serialize_instruction((seed, noop_id, 0_u128, false)).unwrap();
|
||||
let balance_to_move: u128 = 100;
|
||||
let instruction = Program::serialize_instruction(balance_to_move).unwrap();
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![pda_pre, bob_pre],
|
||||
vec![sender, recipient],
|
||||
instruction,
|
||||
vec![
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
npk,
|
||||
ssk: shared_secret_pda,
|
||||
identifier: 0,
|
||||
},
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivateUnauthorized {
|
||||
npk: shared_npk,
|
||||
ssk: shared_secret,
|
||||
identifier: shared_identifier,
|
||||
},
|
||||
],
|
||||
&program_with_deps,
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
let (output, _proof) = result.expect("group PDA spend binding should succeed");
|
||||
let (output, _proof) = result.expect("shared account receive should succeed");
|
||||
// Sender is public (no commitment), recipient is private (1 commitment)
|
||||
assert_eq!(output.new_commitments.len(), 1);
|
||||
}
|
||||
|
||||
|
||||
@ -313,12 +313,12 @@ mod tests {
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn private_pda_spender() -> Self {
|
||||
use test_program_methods::{PRIVATE_PDA_SPENDER_ELF, PRIVATE_PDA_SPENDER_ID};
|
||||
pub fn auth_transfer_proxy() -> Self {
|
||||
use test_program_methods::{AUTH_TRANSFER_PROXY_ELF, AUTH_TRANSFER_PROXY_ID};
|
||||
|
||||
Self {
|
||||
id: PRIVATE_PDA_SPENDER_ID,
|
||||
elf: PRIVATE_PDA_SPENDER_ELF.to_vec(),
|
||||
id: AUTH_TRANSFER_PROXY_ID,
|
||||
elf: AUTH_TRANSFER_PROXY_ELF.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
97
test_program_methods/guest/src/bin/auth_transfer_proxy.rs
Normal file
97
test_program_methods/guest/src/bin/auth_transfer_proxy.rs
Normal file
@ -0,0 +1,97 @@
|
||||
use nssa_core::program::{
|
||||
AccountPostState, ChainedCall, PdaSeed, ProgramId, ProgramInput, ProgramOutput,
|
||||
read_nssa_inputs,
|
||||
};
|
||||
|
||||
/// PDA authorization program that delegates balance operations to `authenticated_transfer`.
|
||||
///
|
||||
/// The PDA is owned by `authenticated_transfer`, not by this program. This program's role
|
||||
/// is solely to provide PDA authorization via `pda_seeds` in chained calls.
|
||||
///
|
||||
/// Instruction: `(pda_seed, auth_transfer_id, amount, is_withdraw)`.
|
||||
///
|
||||
/// **Init** (`is_withdraw = false`, 1 pre-state `[pda]`):
|
||||
/// Chains to `authenticated_transfer` with `instruction=0` (init path) and `pda_seeds=[seed]`
|
||||
/// to initialize the PDA under `authenticated_transfer`'s ownership.
|
||||
///
|
||||
/// **Withdraw** (`is_withdraw = true`, 2 pre-states `[pda, recipient]`):
|
||||
/// Chains to `authenticated_transfer` with the amount and `pda_seeds=[seed]` to authorize
|
||||
/// the PDA for a balance transfer. The actual balance modification happens in
|
||||
/// `authenticated_transfer`, not here.
|
||||
///
|
||||
/// **Deposit**: done directly via `authenticated_transfer` (no need for this program).
|
||||
type Instruction = (PdaSeed, ProgramId, u128, bool);
|
||||
|
||||
#[expect(
|
||||
clippy::allow_attributes,
|
||||
reason = "allow is needed because the clones are only redundant in test compilation"
|
||||
)]
|
||||
#[allow(
|
||||
clippy::redundant_clone,
|
||||
reason = "clones needed in non-test compilation"
|
||||
)]
|
||||
fn main() {
|
||||
let (
|
||||
ProgramInput {
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
pre_states,
|
||||
instruction: (pda_seed, auth_transfer_id, amount, is_withdraw),
|
||||
},
|
||||
instruction_words,
|
||||
) = read_nssa_inputs::<Instruction>();
|
||||
|
||||
if is_withdraw {
|
||||
let Ok([pda_pre, recipient_pre]) = <[_; 2]>::try_from(pre_states.clone()) else {
|
||||
panic!("expected exactly 2 pre_states for withdraw: [pda, recipient]");
|
||||
};
|
||||
|
||||
// Post-states stay unchanged in this program. The actual balance transfer
|
||||
// happens in the chained call to authenticated_transfer.
|
||||
let pda_post = AccountPostState::new(pda_pre.account.clone());
|
||||
let recipient_post = AccountPostState::new(recipient_pre.account.clone());
|
||||
|
||||
// Chain to authenticated_transfer with pda_seeds to authorize the PDA.
|
||||
// The circuit's resolve_authorization_and_record_bindings establishes the
|
||||
// private PDA (seed, npk) binding when pda_seeds match the private PDA derivation.
|
||||
let mut auth_pda_pre = pda_pre;
|
||||
auth_pda_pre.is_authorized = true;
|
||||
let auth_call =
|
||||
ChainedCall::new(auth_transfer_id, vec![auth_pda_pre, recipient_pre], &amount)
|
||||
.with_pda_seeds(vec![pda_seed]);
|
||||
|
||||
ProgramOutput::new(
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
instruction_words,
|
||||
pre_states,
|
||||
vec![pda_post, recipient_post],
|
||||
)
|
||||
.with_chained_calls(vec![auth_call])
|
||||
.write();
|
||||
} else {
|
||||
// Init: initialize the PDA under authenticated_transfer's ownership.
|
||||
let Ok([pda_pre]) = <[_; 1]>::try_from(pre_states.clone()) else {
|
||||
panic!("expected exactly 1 pre_state for init: [pda]");
|
||||
};
|
||||
|
||||
let pda_post = AccountPostState::new(pda_pre.account.clone());
|
||||
|
||||
// Chain to authenticated_transfer with instruction=0 (init path) and pda_seeds
|
||||
// to authorize the PDA. authenticated_transfer will claim it with Claim::Authorized.
|
||||
let mut auth_pda_pre = pda_pre;
|
||||
auth_pda_pre.is_authorized = true;
|
||||
let auth_call = ChainedCall::new(auth_transfer_id, vec![auth_pda_pre], &0_u128)
|
||||
.with_pda_seeds(vec![pda_seed]);
|
||||
|
||||
ProgramOutput::new(
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
instruction_words,
|
||||
pre_states,
|
||||
vec![pda_post],
|
||||
)
|
||||
.with_chained_calls(vec![auth_call])
|
||||
.write();
|
||||
}
|
||||
}
|
||||
@ -1,118 +0,0 @@
|
||||
use nssa_core::program::{
|
||||
AccountPostState, ChainedCall, Claim, PdaSeed, ProgramId, ProgramInput, ProgramOutput,
|
||||
read_nssa_inputs,
|
||||
};
|
||||
|
||||
/// Single program for group PDA operations. Owns and operates the PDA directly.
|
||||
///
|
||||
/// Instruction: `(pda_seed, noop_program_id, amount, is_deposit)`.
|
||||
/// Pre-states: `[group_pda, counterparty]`.
|
||||
///
|
||||
/// **Deposit** (`is_deposit = true`, new PDA):
|
||||
/// Claims PDA via `Claim::Pda(seed)`, increases PDA balance, decreases counterparty.
|
||||
/// Counterparty must be authorized and owned by this program (or uninitialized).
|
||||
///
|
||||
/// **Spend** (`is_deposit = false`, existing PDA):
|
||||
/// Decreases PDA balance (this program owns it), increases counterparty.
|
||||
/// Chains to a noop callee with `pda_seeds` to establish the mask-3 binding
|
||||
/// that the circuit requires for existing private PDAs.
|
||||
type Instruction = (PdaSeed, ProgramId, u128, bool);
|
||||
|
||||
#[expect(
|
||||
clippy::allow_attributes,
|
||||
reason = "allow is needed because the clones are only redundant in test compilation"
|
||||
)]
|
||||
#[allow(
|
||||
clippy::redundant_clone,
|
||||
reason = "clones needed in non-test compilation"
|
||||
)]
|
||||
fn main() {
|
||||
let (
|
||||
ProgramInput {
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
pre_states,
|
||||
instruction: (pda_seed, noop_id, amount, is_deposit),
|
||||
},
|
||||
instruction_words,
|
||||
) = read_nssa_inputs::<Instruction>();
|
||||
|
||||
let Ok([pda_pre, counterparty_pre]) = <[_; 2]>::try_from(pre_states.clone()) else {
|
||||
panic!("expected exactly 2 pre_states: [group_pda, counterparty]");
|
||||
};
|
||||
|
||||
if is_deposit {
|
||||
// Deposit: claim PDA, transfer balance from counterparty to PDA.
|
||||
// Both accounts must be owned by this program (or uninitialized) for
|
||||
// validate_execution to allow balance changes.
|
||||
assert!(
|
||||
counterparty_pre.is_authorized,
|
||||
"Counterparty must be authorized to deposit"
|
||||
);
|
||||
|
||||
let mut pda_account = pda_pre.account;
|
||||
let mut counterparty_account = counterparty_pre.account;
|
||||
|
||||
pda_account.balance = pda_account
|
||||
.balance
|
||||
.checked_add(amount)
|
||||
.expect("PDA balance overflow");
|
||||
counterparty_account.balance = counterparty_account
|
||||
.balance
|
||||
.checked_sub(amount)
|
||||
.expect("Counterparty has insufficient balance");
|
||||
|
||||
let pda_post = AccountPostState::new_claimed_if_default(pda_account, Claim::Pda(pda_seed));
|
||||
let counterparty_post = AccountPostState::new(counterparty_account);
|
||||
|
||||
ProgramOutput::new(
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
instruction_words,
|
||||
pre_states,
|
||||
vec![pda_post, counterparty_post],
|
||||
)
|
||||
.write();
|
||||
} else {
|
||||
// Spend: decrease PDA balance (owned by this program), increase counterparty.
|
||||
// Chain to noop with pda_seeds to establish the mask-3 binding for the
|
||||
// existing PDA. The noop's pre_states must match our post_states.
|
||||
// Authorization is enforced by the circuit's binding check, not here.
|
||||
|
||||
let mut pda_account = pda_pre.account.clone();
|
||||
let mut counterparty_account = counterparty_pre.account.clone();
|
||||
|
||||
pda_account.balance = pda_account
|
||||
.balance
|
||||
.checked_sub(amount)
|
||||
.expect("PDA has insufficient balance");
|
||||
counterparty_account.balance = counterparty_account
|
||||
.balance
|
||||
.checked_add(amount)
|
||||
.expect("Counterparty balance overflow");
|
||||
|
||||
let pda_post = AccountPostState::new(pda_account.clone());
|
||||
let counterparty_post = AccountPostState::new(counterparty_account.clone());
|
||||
|
||||
// Chain to noop solely to establish the mask-3 binding via pda_seeds.
|
||||
let mut noop_pda_pre = pda_pre;
|
||||
noop_pda_pre.account = pda_account;
|
||||
noop_pda_pre.is_authorized = true;
|
||||
|
||||
let mut noop_counterparty_pre = counterparty_pre;
|
||||
noop_counterparty_pre.account = counterparty_account;
|
||||
|
||||
let noop_call = ChainedCall::new(noop_id, vec![noop_pda_pre, noop_counterparty_pre], &())
|
||||
.with_pda_seeds(vec![pda_seed]);
|
||||
|
||||
ProgramOutput::new(
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
instruction_words,
|
||||
pre_states,
|
||||
vec![pda_post, counterparty_post],
|
||||
)
|
||||
.with_chained_calls(vec![noop_call])
|
||||
.write();
|
||||
}
|
||||
}
|
||||
@ -15,6 +15,7 @@ wallet.workspace = true
|
||||
nssa.workspace = true
|
||||
nssa_core.workspace = true
|
||||
sequencer_service_rpc = { workspace = true, features = ["client"] }
|
||||
|
||||
tokio.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
|
||||
@ -92,6 +92,23 @@ pub enum NewSubcommand {
|
||||
/// Label to assign to the new account.
|
||||
label: Option<String>,
|
||||
},
|
||||
/// Create a shared private account from a group's GMS.
|
||||
PrivateGms {
|
||||
/// Group name to derive keys from.
|
||||
group: String,
|
||||
#[arg(short, long)]
|
||||
/// Label to assign to the new account.
|
||||
label: Option<String>,
|
||||
#[arg(long)]
|
||||
/// Create a PDA account (requires --seed and --program-id).
|
||||
pda: bool,
|
||||
#[arg(long, requires = "pda")]
|
||||
/// PDA seed as 64-character hex string.
|
||||
seed: Option<String>,
|
||||
#[arg(long, requires = "pda")]
|
||||
/// Program ID as hex string.
|
||||
program_id: Option<String>,
|
||||
},
|
||||
/// Recommended for receiving from multiple senders: creates a key node (npk + vpk) without
|
||||
/// registering any account.
|
||||
PrivateAccountsKey {
|
||||
@ -183,9 +200,67 @@ impl WalletSubcommand for NewSubcommand {
|
||||
);
|
||||
|
||||
wallet_core.store_persistent_data().await?;
|
||||
|
||||
Ok(SubcommandReturnValue::RegisterAccount { account_id })
|
||||
}
|
||||
Self::PrivateGms {
|
||||
group,
|
||||
label,
|
||||
pda,
|
||||
seed,
|
||||
program_id,
|
||||
} => {
|
||||
if let Some(label) = &label
|
||||
&& wallet_core
|
||||
.storage
|
||||
.labels
|
||||
.values()
|
||||
.any(|l| l.to_string() == *label)
|
||||
{
|
||||
anyhow::bail!("Label '{label}' is already in use by another account");
|
||||
}
|
||||
|
||||
let info = if pda {
|
||||
let seed_hex = seed.context("--seed is required for PDA accounts")?;
|
||||
let pid_hex =
|
||||
program_id.context("--program-id is required for PDA accounts")?;
|
||||
|
||||
let seed_bytes: [u8; 32] = hex::decode(&seed_hex)
|
||||
.context("Invalid seed hex")?
|
||||
.try_into()
|
||||
.map_err(|_err| anyhow::anyhow!("Seed must be exactly 32 bytes"))?;
|
||||
let pda_seed = nssa_core::program::PdaSeed::new(seed_bytes);
|
||||
|
||||
let pid_bytes = hex::decode(&pid_hex).context("Invalid program ID hex")?;
|
||||
if pid_bytes.len() != 32 {
|
||||
anyhow::bail!("Program ID must be exactly 32 bytes");
|
||||
}
|
||||
let mut pid: nssa_core::program::ProgramId = [0; 8];
|
||||
for (i, chunk) in pid_bytes.chunks_exact(4).enumerate() {
|
||||
pid[i] = u32::from_le_bytes(chunk.try_into().unwrap());
|
||||
}
|
||||
|
||||
wallet_core.create_shared_pda_account(&group, pda_seed, pid)?
|
||||
} else {
|
||||
wallet_core.create_shared_regular_account(&group)?
|
||||
};
|
||||
|
||||
if let Some(label) = label {
|
||||
wallet_core
|
||||
.storage
|
||||
.labels
|
||||
.insert(info.account_id.to_string(), Label::new(label));
|
||||
}
|
||||
|
||||
println!("Shared account from group '{group}'");
|
||||
println!("AccountId: Private/{}", info.account_id);
|
||||
println!("NPK: {}", hex::encode(info.npk.0));
|
||||
println!("VPK: {}", hex::encode(&info.vpk.0));
|
||||
|
||||
wallet_core.store_persistent_data().await?;
|
||||
Ok(SubcommandReturnValue::RegisterAccount {
|
||||
account_id: info.account_id,
|
||||
})
|
||||
}
|
||||
Self::PrivateAccountsKey { cci } => {
|
||||
let chain_index = wallet_core.create_private_accounts_key(cci);
|
||||
|
||||
|
||||
@ -1,15 +1,13 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
use clap::Subcommand;
|
||||
use key_protocol::key_management::group_key_holder::GroupKeyHolder;
|
||||
use nssa::AccountId;
|
||||
use nssa_core::program::PdaSeed;
|
||||
use key_protocol::key_management::group_key_holder::{GroupKeyHolder, SealingPublicKey};
|
||||
|
||||
use crate::{
|
||||
WalletCore,
|
||||
cli::{SubcommandReturnValue, WalletSubcommand},
|
||||
};
|
||||
|
||||
/// Group PDA management commands.
|
||||
/// Group key management commands.
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
pub enum GroupSubcommand {
|
||||
/// Create a new group with a fresh random GMS.
|
||||
@ -17,36 +15,9 @@ pub enum GroupSubcommand {
|
||||
/// Human-readable name for the group.
|
||||
name: String,
|
||||
},
|
||||
/// Import a group from raw GMS bytes.
|
||||
Import {
|
||||
/// Human-readable name for the group.
|
||||
name: String,
|
||||
/// Raw GMS as 64-character hex string.
|
||||
#[arg(long)]
|
||||
gms: String,
|
||||
/// Epoch (defaults to 0).
|
||||
#[arg(long, default_value = "0")]
|
||||
epoch: u32,
|
||||
},
|
||||
/// Export the raw GMS hex for backup or manual distribution.
|
||||
Export {
|
||||
/// Group name.
|
||||
name: String,
|
||||
},
|
||||
/// List all groups with their epochs.
|
||||
/// List all groups.
|
||||
#[command(visible_alias = "ls")]
|
||||
List,
|
||||
/// Derive keys for a PDA seed and show the resulting AccountId.
|
||||
Derive {
|
||||
/// Group name.
|
||||
name: String,
|
||||
/// PDA seed as 64-character hex string.
|
||||
#[arg(long)]
|
||||
seed: String,
|
||||
/// Program ID as hex string (u32x8 little-endian).
|
||||
#[arg(long)]
|
||||
program_id: String,
|
||||
},
|
||||
/// Remove a group from the wallet.
|
||||
Remove {
|
||||
/// Group name.
|
||||
@ -56,26 +27,22 @@ pub enum GroupSubcommand {
|
||||
Invite {
|
||||
/// Group name.
|
||||
name: String,
|
||||
/// Recipient's viewing public key as hex string.
|
||||
/// Recipient's sealing public key as hex string.
|
||||
#[arg(long)]
|
||||
vpk: String,
|
||||
key: String,
|
||||
},
|
||||
/// Unseal a received GMS and store it (join a group).
|
||||
/// Uses the wallet's dedicated sealing key (generated via `new-sealing-key`).
|
||||
Join {
|
||||
/// Human-readable name to store the group under.
|
||||
name: String,
|
||||
/// Sealed GMS as hex string (from the inviter).
|
||||
#[arg(long)]
|
||||
sealed: String,
|
||||
/// Account label or Private/<id> whose VSK to use for decryption.
|
||||
#[arg(long)]
|
||||
account: String,
|
||||
},
|
||||
/// Ratchet the GMS to exclude removed members.
|
||||
Ratchet {
|
||||
/// Group name.
|
||||
name: String,
|
||||
},
|
||||
/// Generate a dedicated sealing key pair for GMS distribution.
|
||||
/// Share the printed public key with group members so they can seal GMS for you.
|
||||
NewSealingKey,
|
||||
}
|
||||
|
||||
impl WalletSubcommand for GroupSubcommand {
|
||||
@ -88,62 +55,17 @@ impl WalletSubcommand for GroupSubcommand {
|
||||
if wallet_core
|
||||
.storage()
|
||||
.user_data
|
||||
.get_group_key_holder(&name)
|
||||
.group_key_holder(&name)
|
||||
.is_some()
|
||||
{
|
||||
anyhow::bail!("Group '{name}' already exists");
|
||||
}
|
||||
|
||||
let holder = GroupKeyHolder::new();
|
||||
wallet_core
|
||||
.storage_mut()
|
||||
.user_data
|
||||
.insert_group_key_holder(name.clone(), holder);
|
||||
wallet_core.insert_group_key_holder(name.clone(), holder);
|
||||
wallet_core.store_persistent_data().await?;
|
||||
|
||||
println!("Created group '{name}' at epoch 0");
|
||||
Ok(SubcommandReturnValue::Empty)
|
||||
}
|
||||
|
||||
Self::Import { name, gms, epoch } => {
|
||||
if wallet_core
|
||||
.storage()
|
||||
.user_data
|
||||
.get_group_key_holder(&name)
|
||||
.is_some()
|
||||
{
|
||||
anyhow::bail!("Group '{name}' already exists");
|
||||
}
|
||||
|
||||
let gms_bytes: [u8; 32] = hex::decode(&gms)
|
||||
.context("Invalid GMS hex")?
|
||||
.try_into()
|
||||
.map_err(|_| anyhow::anyhow!("GMS must be exactly 32 bytes"))?;
|
||||
|
||||
let holder = GroupKeyHolder::from_gms_and_epoch(gms_bytes, epoch);
|
||||
wallet_core
|
||||
.storage_mut()
|
||||
.user_data
|
||||
.insert_group_key_holder(name.clone(), holder);
|
||||
wallet_core.store_persistent_data().await?;
|
||||
|
||||
println!("Imported group '{name}' at epoch {epoch}");
|
||||
Ok(SubcommandReturnValue::Empty)
|
||||
}
|
||||
|
||||
Self::Export { name } => {
|
||||
let holder = wallet_core
|
||||
.storage()
|
||||
.user_data
|
||||
.get_group_key_holder(&name)
|
||||
.context(format!("Group '{name}' not found"))?;
|
||||
|
||||
let gms_hex = hex::encode(holder.dangerous_raw_gms());
|
||||
let epoch = holder.epoch();
|
||||
|
||||
println!("Group: {name}");
|
||||
println!("Epoch: {epoch}");
|
||||
println!("GMS: {gms_hex}");
|
||||
println!("Created group '{name}'");
|
||||
Ok(SubcommandReturnValue::Empty)
|
||||
}
|
||||
|
||||
@ -152,60 +74,15 @@ impl WalletSubcommand for GroupSubcommand {
|
||||
if holders.is_empty() {
|
||||
println!("No groups found");
|
||||
} else {
|
||||
for (name, holder) in holders {
|
||||
println!("{name} (epoch {})", holder.epoch());
|
||||
for name in holders.keys() {
|
||||
println!("{name}");
|
||||
}
|
||||
}
|
||||
Ok(SubcommandReturnValue::Empty)
|
||||
}
|
||||
|
||||
Self::Derive {
|
||||
name,
|
||||
seed,
|
||||
program_id,
|
||||
} => {
|
||||
let holder = wallet_core
|
||||
.storage()
|
||||
.user_data
|
||||
.get_group_key_holder(&name)
|
||||
.context(format!("Group '{name}' not found"))?;
|
||||
|
||||
let seed_bytes: [u8; 32] = hex::decode(&seed)
|
||||
.context("Invalid seed hex")?
|
||||
.try_into()
|
||||
.map_err(|_| anyhow::anyhow!("Seed must be exactly 32 bytes"))?;
|
||||
let pda_seed = PdaSeed::new(seed_bytes);
|
||||
|
||||
let pid_bytes =
|
||||
hex::decode(&program_id).context("Invalid program ID hex")?;
|
||||
if pid_bytes.len() != 32 {
|
||||
anyhow::bail!("Program ID must be exactly 32 bytes");
|
||||
}
|
||||
let mut pid: nssa_core::program::ProgramId = [0; 8];
|
||||
for (i, chunk) in pid_bytes.chunks_exact(4).enumerate() {
|
||||
pid[i] = u32::from_le_bytes(chunk.try_into().unwrap());
|
||||
}
|
||||
|
||||
let keys = holder.derive_keys_for_pda(&pda_seed);
|
||||
let npk = keys.generate_nullifier_public_key();
|
||||
let vpk = keys.generate_viewing_public_key();
|
||||
let account_id = AccountId::for_private_pda(&pid, &pda_seed, &npk);
|
||||
|
||||
println!("Group: {name}");
|
||||
println!("NPK: {}", hex::encode(npk.0));
|
||||
println!("VPK: {}", hex::encode(&vpk.0));
|
||||
println!("AccountId: {account_id}");
|
||||
Ok(SubcommandReturnValue::Empty)
|
||||
}
|
||||
|
||||
Self::Remove { name } => {
|
||||
if wallet_core
|
||||
.storage_mut()
|
||||
.user_data
|
||||
.group_key_holders
|
||||
.remove(&name)
|
||||
.is_none()
|
||||
{
|
||||
if wallet_core.remove_group_key_holder(&name).is_none() {
|
||||
anyhow::bail!("Group '{name}' not found");
|
||||
}
|
||||
|
||||
@ -214,80 +91,66 @@ impl WalletSubcommand for GroupSubcommand {
|
||||
Ok(SubcommandReturnValue::Empty)
|
||||
}
|
||||
|
||||
Self::Invite { name, vpk } => {
|
||||
Self::Invite { name, key } => {
|
||||
let holder = wallet_core
|
||||
.storage()
|
||||
.user_data
|
||||
.get_group_key_holder(&name)
|
||||
.group_key_holder(&name)
|
||||
.context(format!("Group '{name}' not found"))?;
|
||||
|
||||
let vpk_bytes = hex::decode(&vpk).context("Invalid VPK hex")?;
|
||||
let recipient_vpk =
|
||||
nssa_core::encryption::shared_key_derivation::Secp256k1Point(vpk_bytes);
|
||||
let key_bytes = hex::decode(&key).context("Invalid key hex")?;
|
||||
let recipient_key =
|
||||
key_protocol::key_management::group_key_holder::SealingPublicKey::from_bytes(
|
||||
key_bytes,
|
||||
);
|
||||
|
||||
let sealed = holder.seal_for(&recipient_vpk);
|
||||
let sealed = holder.seal_for(&recipient_key);
|
||||
println!("{}", hex::encode(&sealed));
|
||||
Ok(SubcommandReturnValue::Empty)
|
||||
}
|
||||
|
||||
Self::Join {
|
||||
name,
|
||||
sealed,
|
||||
account,
|
||||
} => {
|
||||
Self::Join { name, sealed } => {
|
||||
if wallet_core
|
||||
.storage()
|
||||
.user_data
|
||||
.get_group_key_holder(&name)
|
||||
.group_key_holder(&name)
|
||||
.is_some()
|
||||
{
|
||||
anyhow::bail!("Group '{name}' already exists");
|
||||
}
|
||||
|
||||
let sealing_key =
|
||||
wallet_core.storage().user_data.sealing_secret_key.context(
|
||||
"No sealing key found. Run 'wallet group new-sealing-key' first.",
|
||||
)?;
|
||||
|
||||
let sealed_bytes = hex::decode(&sealed).context("Invalid sealed hex")?;
|
||||
|
||||
// Resolve the account to get the VSK
|
||||
let account_id: nssa::AccountId = account
|
||||
.parse()
|
||||
.context("Invalid account ID (use Private/<base58>)")?;
|
||||
let (keychain, _) = wallet_core
|
||||
.storage()
|
||||
.user_data
|
||||
.get_private_account(account_id)
|
||||
.context("Private account not found")?;
|
||||
let vsk = keychain.private_key_holder.viewing_secret_key;
|
||||
|
||||
let holder = GroupKeyHolder::unseal(&sealed_bytes, &vsk)
|
||||
let holder = GroupKeyHolder::unseal(&sealed_bytes, &sealing_key)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to unseal: {e:?}"))?;
|
||||
|
||||
let epoch = holder.epoch();
|
||||
wallet_core
|
||||
.storage_mut()
|
||||
.user_data
|
||||
.insert_group_key_holder(name.clone(), holder);
|
||||
wallet_core.insert_group_key_holder(name.clone(), holder);
|
||||
wallet_core.store_persistent_data().await?;
|
||||
|
||||
println!("Joined group '{name}' at epoch {epoch}");
|
||||
println!("Joined group '{name}'");
|
||||
Ok(SubcommandReturnValue::Empty)
|
||||
}
|
||||
|
||||
Self::Ratchet { name } => {
|
||||
let holder = wallet_core
|
||||
.storage_mut()
|
||||
.user_data
|
||||
.group_key_holders
|
||||
.get_mut(&name)
|
||||
.context(format!("Group '{name}' not found"))?;
|
||||
Self::NewSealingKey => {
|
||||
if wallet_core.storage().user_data.sealing_secret_key.is_some() {
|
||||
anyhow::bail!("Sealing key already exists. Each wallet has one sealing key.");
|
||||
}
|
||||
|
||||
let mut salt = [0_u8; 32];
|
||||
rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut salt);
|
||||
holder.ratchet(salt);
|
||||
let mut secret: nssa_core::encryption::Scalar = [0_u8; 32];
|
||||
rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut secret);
|
||||
let public_key = SealingPublicKey::from_scalar(secret);
|
||||
|
||||
let epoch = holder.epoch();
|
||||
wallet_core.set_sealing_secret_key(secret);
|
||||
wallet_core.store_persistent_data().await?;
|
||||
|
||||
println!("Ratcheted group '{name}' to epoch {epoch}");
|
||||
println!("Re-invite remaining members with 'group invite'");
|
||||
println!("Sealing key generated.");
|
||||
println!("Public key: {}", hex::encode(public_key.to_bytes()));
|
||||
println!("Share this public key with group members so they can seal GMS for you.");
|
||||
Ok(SubcommandReturnValue::Empty)
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ use crate::{
|
||||
account::AccountSubcommand,
|
||||
chain::ChainSubcommand,
|
||||
config::ConfigSubcommand,
|
||||
group::GroupSubcommand,
|
||||
programs::{
|
||||
amm::AmmProgramAgnosticSubcommand, ata::AtaSubcommand,
|
||||
native_token_transfer::AuthTransferSubcommand, pinata::PinataProgramAgnosticSubcommand,
|
||||
@ -25,6 +26,7 @@ use crate::{
|
||||
pub mod account;
|
||||
pub mod chain;
|
||||
pub mod config;
|
||||
pub mod group;
|
||||
pub mod programs;
|
||||
|
||||
pub(crate) trait WalletSubcommand {
|
||||
@ -57,6 +59,9 @@ pub enum Command {
|
||||
/// Associated Token Account program interaction subcommand.
|
||||
#[command(subcommand)]
|
||||
Ata(AtaSubcommand),
|
||||
/// Group key management (create, invite, join, derive keys).
|
||||
#[command(subcommand)]
|
||||
Group(GroupSubcommand),
|
||||
/// Check the wallet can connect to the node and builtin local programs
|
||||
/// match the remote versions.
|
||||
CheckHealth,
|
||||
@ -164,6 +169,7 @@ pub async fn execute_subcommand(
|
||||
Command::Token(token_subcommand) => token_subcommand.handle_subcommand(wallet_core).await?,
|
||||
Command::AMM(amm_subcommand) => amm_subcommand.handle_subcommand(wallet_core).await?,
|
||||
Command::Ata(ata_subcommand) => ata_subcommand.handle_subcommand(wallet_core).await?,
|
||||
Command::Group(group_subcommand) => group_subcommand.handle_subcommand(wallet_core).await?,
|
||||
Command::Config(config_subcommand) => {
|
||||
config_subcommand.handle_subcommand(wallet_core).await?
|
||||
}
|
||||
|
||||
@ -98,6 +98,21 @@ pub struct PersistentStorage {
|
||||
/// "2rnKprXqWGWJTkDZKsQbFXa4ctKRbapsdoTKQFnaVGG8").
|
||||
#[serde(default)]
|
||||
pub labels: HashMap<String, Label>,
|
||||
/// Group key holders for shared account management.
|
||||
#[serde(default)]
|
||||
pub group_key_holders: std::collections::BTreeMap<
|
||||
String,
|
||||
key_protocol::key_management::group_key_holder::GroupKeyHolder,
|
||||
>,
|
||||
/// Cached state of shared private accounts (PDA and regular).
|
||||
#[serde(default)]
|
||||
pub shared_private_accounts: std::collections::BTreeMap<
|
||||
nssa::AccountId,
|
||||
key_protocol::key_protocol_core::SharedAccountEntry,
|
||||
>,
|
||||
/// Dedicated sealing secret key for GMS distribution.
|
||||
#[serde(default)]
|
||||
pub sealing_secret_key: Option<nssa_core::encryption::Scalar>,
|
||||
}
|
||||
|
||||
impl PersistentStorage {
|
||||
|
||||
@ -204,6 +204,9 @@ pub fn produce_data_for_storage(
|
||||
accounts: vec_for_storage,
|
||||
last_synced_block,
|
||||
labels,
|
||||
group_key_holders: user_data.group_key_holders.clone(),
|
||||
shared_private_accounts: user_data.shared_private_accounts.clone(),
|
||||
sealing_secret_key: user_data.sealing_secret_key,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -52,6 +52,13 @@ pub enum AccDecodeData {
|
||||
Decode(nssa_core::SharedSecretKey, AccountId),
|
||||
}
|
||||
|
||||
/// Info returned when creating a shared account.
|
||||
pub struct SharedAccountInfo {
|
||||
pub account_id: AccountId,
|
||||
pub npk: nssa_core::NullifierPublicKey,
|
||||
pub vpk: nssa_core::encryption::ViewingPublicKey,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ExecutionFailureKind {
|
||||
#[error("Failed to get data from sequencer")]
|
||||
@ -99,6 +106,9 @@ impl WalletCore {
|
||||
accounts: persistent_accounts,
|
||||
last_synced_block,
|
||||
labels,
|
||||
group_key_holders,
|
||||
shared_private_accounts,
|
||||
sealing_secret_key,
|
||||
} = PersistentStorage::from_path(&storage_path).with_context(|| {
|
||||
format!(
|
||||
"Failed to read persistent storage at {}",
|
||||
@ -110,7 +120,13 @@ impl WalletCore {
|
||||
config_path,
|
||||
storage_path,
|
||||
config_overrides,
|
||||
|config| WalletChainStore::new(config, persistent_accounts, labels),
|
||||
|config| {
|
||||
let mut store = WalletChainStore::new(config, persistent_accounts, labels)?;
|
||||
store.user_data.group_key_holders = group_key_holders;
|
||||
store.user_data.shared_private_accounts = shared_private_accounts;
|
||||
store.user_data.sealing_secret_key = sealing_secret_key;
|
||||
Ok(store)
|
||||
},
|
||||
last_synced_block,
|
||||
)
|
||||
}
|
||||
@ -291,6 +307,161 @@ impl WalletCore {
|
||||
(account_id, cci)
|
||||
}
|
||||
|
||||
/// Insert a group key holder into storage.
|
||||
pub fn insert_group_key_holder(
|
||||
&mut self,
|
||||
name: String,
|
||||
holder: key_protocol::key_management::group_key_holder::GroupKeyHolder,
|
||||
) {
|
||||
self.storage.user_data.insert_group_key_holder(name, holder);
|
||||
}
|
||||
|
||||
/// Set the wallet's dedicated sealing secret key.
|
||||
pub const fn set_sealing_secret_key(&mut self, key: nssa_core::encryption::Scalar) {
|
||||
self.storage.user_data.sealing_secret_key = Some(key);
|
||||
}
|
||||
|
||||
/// Resolve an `AccountId` to the appropriate `PrivacyPreservingAccount` variant.
|
||||
/// Checks the key tree first, then shared private accounts.
|
||||
#[must_use]
|
||||
pub fn resolve_private_account(
|
||||
&self,
|
||||
account_id: nssa::AccountId,
|
||||
) -> Option<PrivacyPreservingAccount> {
|
||||
// Check key tree first
|
||||
if self
|
||||
.storage
|
||||
.user_data
|
||||
.get_private_account(account_id)
|
||||
.is_some()
|
||||
{
|
||||
return Some(PrivacyPreservingAccount::PrivateOwned(account_id));
|
||||
}
|
||||
|
||||
// Check shared private accounts
|
||||
let entry = self.storage.user_data.shared_private_account(&account_id)?;
|
||||
let holder = self
|
||||
.storage
|
||||
.user_data
|
||||
.group_key_holder(&entry.group_label)?;
|
||||
|
||||
if entry.pda_seed.is_some() {
|
||||
Some(PrivacyPreservingAccount::PrivatePdaOwned(account_id))
|
||||
} else {
|
||||
let derivation_seed = {
|
||||
use sha2::Digest as _;
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
hasher.update(b"/LEE/v0.3/SharedAccountTag/\x00\x00\x00\x00\x00");
|
||||
hasher.update(entry.identifier.to_le_bytes());
|
||||
let result: [u8; 32] = hasher.finalize().into();
|
||||
result
|
||||
};
|
||||
let keys = holder.derive_keys_for_shared_account(&derivation_seed);
|
||||
Some(PrivacyPreservingAccount::PrivateShared {
|
||||
nsk: keys.nullifier_secret_key,
|
||||
npk: keys.generate_nullifier_public_key(),
|
||||
vpk: keys.generate_viewing_public_key(),
|
||||
identifier: entry.identifier,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a group key holder from storage. Returns the removed holder if it existed.
|
||||
pub fn remove_group_key_holder(
|
||||
&mut self,
|
||||
name: &str,
|
||||
) -> Option<key_protocol::key_management::group_key_holder::GroupKeyHolder> {
|
||||
self.storage.user_data.group_key_holders.remove(name)
|
||||
}
|
||||
|
||||
/// Register a shared account in storage for sync tracking.
|
||||
fn register_shared_account(
|
||||
&mut self,
|
||||
account_id: AccountId,
|
||||
group_label: String,
|
||||
identifier: nssa_core::Identifier,
|
||||
pda_seed: Option<nssa_core::program::PdaSeed>,
|
||||
pda_program_id: Option<nssa_core::program::ProgramId>,
|
||||
) {
|
||||
use key_protocol::key_protocol_core::SharedAccountEntry;
|
||||
self.storage.user_data.insert_shared_private_account(
|
||||
account_id,
|
||||
SharedAccountEntry {
|
||||
group_label,
|
||||
identifier,
|
||||
pda_seed,
|
||||
pda_program_id,
|
||||
account: Account::default(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a shared PDA account from a group's GMS. Returns the `AccountId` and derived keys.
|
||||
pub fn create_shared_pda_account(
|
||||
&mut self,
|
||||
group_name: &str,
|
||||
pda_seed: nssa_core::program::PdaSeed,
|
||||
program_id: nssa_core::program::ProgramId,
|
||||
) -> Result<SharedAccountInfo> {
|
||||
let holder = self
|
||||
.storage
|
||||
.user_data
|
||||
.group_key_holder(group_name)
|
||||
.context(format!("Group '{group_name}' not found"))?;
|
||||
|
||||
let keys = holder.derive_keys_for_pda(&program_id, &pda_seed);
|
||||
let npk = keys.generate_nullifier_public_key();
|
||||
let vpk = keys.generate_viewing_public_key();
|
||||
let account_id = AccountId::for_private_pda(&program_id, &pda_seed, &npk, u128::MAX);
|
||||
|
||||
self.register_shared_account(
|
||||
account_id,
|
||||
String::from(group_name),
|
||||
u128::MAX,
|
||||
Some(pda_seed),
|
||||
Some(program_id),
|
||||
);
|
||||
|
||||
Ok(SharedAccountInfo {
|
||||
account_id,
|
||||
npk,
|
||||
vpk,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a shared regular private account from a group's GMS. Returns the `AccountId` and
|
||||
/// derived keys. The derivation seed is computed deterministically from a random identifier.
|
||||
pub fn create_shared_regular_account(&mut self, group_name: &str) -> Result<SharedAccountInfo> {
|
||||
let identifier: nssa_core::Identifier = rand::random();
|
||||
let derivation_seed = {
|
||||
use sha2::Digest as _;
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
hasher.update(b"/LEE/v0.3/SharedAccountTag/\x00\x00\x00\x00\x00");
|
||||
hasher.update(identifier.to_le_bytes());
|
||||
let result: [u8; 32] = hasher.finalize().into();
|
||||
result
|
||||
};
|
||||
|
||||
let holder = self
|
||||
.storage
|
||||
.user_data
|
||||
.group_key_holder(group_name)
|
||||
.context(format!("Group '{group_name}' not found"))?;
|
||||
|
||||
let keys = holder.derive_keys_for_shared_account(&derivation_seed);
|
||||
let npk = keys.generate_nullifier_public_key();
|
||||
let vpk = keys.generate_viewing_public_key();
|
||||
let account_id = AccountId::from((&npk, identifier));
|
||||
|
||||
self.register_shared_account(account_id, String::from(group_name), identifier, None, None);
|
||||
|
||||
Ok(SharedAccountInfo {
|
||||
account_id,
|
||||
npk,
|
||||
vpk,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get account balance.
|
||||
pub async fn get_account_balance(&self, acc: AccountId) -> Result<u128> {
|
||||
Ok(self.sequencer_client.get_account_balance(acc).await?)
|
||||
@ -559,6 +730,77 @@ impl WalletCore {
|
||||
self.storage
|
||||
.insert_private_account_data(affected_account_id, &kind, new_acc);
|
||||
}
|
||||
|
||||
// Scan for updates to shared accounts (GMS-derived).
|
||||
self.sync_shared_private_accounts_with_tx(&tx);
|
||||
}
|
||||
|
||||
fn sync_shared_private_accounts_with_tx(&mut self, tx: &PrivacyPreservingTransaction) {
|
||||
let shared_keys: Vec<_> = self
|
||||
.storage
|
||||
.user_data
|
||||
.shared_private_accounts_iter()
|
||||
.filter_map(|(&account_id, entry)| {
|
||||
let holder = self
|
||||
.storage
|
||||
.user_data
|
||||
.group_key_holder(&entry.group_label)?;
|
||||
|
||||
let keys = match (&entry.pda_seed, &entry.pda_program_id) {
|
||||
(Some(pda_seed), Some(program_id)) => {
|
||||
holder.derive_keys_for_pda(program_id, pda_seed)
|
||||
}
|
||||
(Some(_), None) => return None, // PDA without program_id, skip
|
||||
_ => {
|
||||
let derivation_seed = {
|
||||
use sha2::Digest as _;
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
hasher.update(b"/LEE/v0.3/SharedAccountTag/\x00\x00\x00\x00\x00");
|
||||
hasher.update(entry.identifier.to_le_bytes());
|
||||
let result: [u8; 32] = hasher.finalize().into();
|
||||
result
|
||||
};
|
||||
holder.derive_keys_for_shared_account(&derivation_seed)
|
||||
}
|
||||
};
|
||||
let npk = keys.generate_nullifier_public_key();
|
||||
let vpk = keys.generate_viewing_public_key();
|
||||
let vsk = keys.viewing_secret_key;
|
||||
Some((account_id, npk, vpk, vsk))
|
||||
})
|
||||
.collect();
|
||||
|
||||
for (account_id, npk, vpk, vsk) in shared_keys {
|
||||
let view_tag = EncryptedAccountData::compute_view_tag(&npk, &vpk);
|
||||
|
||||
for (ciph_id, encrypted_data) in tx
|
||||
.message()
|
||||
.encrypted_private_post_states
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
if encrypted_data.view_tag != view_tag {
|
||||
continue;
|
||||
}
|
||||
|
||||
let shared_secret = SharedSecretKey::new(&vsk, &encrypted_data.epk);
|
||||
let commitment = &tx.message.new_commitments[ciph_id];
|
||||
|
||||
if let Some((_decrypted_identifier, new_acc)) = nssa_core::EncryptionScheme::decrypt(
|
||||
&encrypted_data.ciphertext,
|
||||
&shared_secret,
|
||||
commitment,
|
||||
ciph_id
|
||||
.try_into()
|
||||
.expect("Ciphertext ID is expected to fit in u32"),
|
||||
) {
|
||||
info!("Synced shared account {account_id:#?} with new state {new_acc:#?}");
|
||||
self.storage
|
||||
.user_data
|
||||
.update_shared_private_account_state(&account_id, new_acc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
|
||||
@ -1,165 +0,0 @@
|
||||
use common::{HashType, transaction::NSSATransaction};
|
||||
use sequencer_service_rpc::RpcClient as _;
|
||||
use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder;
|
||||
use nssa::{AccountId, privacy_preserving_transaction::circuit};
|
||||
use nssa_core::{MembershipProof, SharedSecretKey, account::AccountWithMetadata};
|
||||
|
||||
use crate::{
|
||||
ExecutionFailureKind, WalletCore, helperfunctions::produce_random_nonces,
|
||||
transaction_utils::AccountPreparedData,
|
||||
};
|
||||
|
||||
impl WalletCore {
|
||||
pub async fn claim_pinata(
|
||||
&self,
|
||||
pinata_account_id: AccountId,
|
||||
winner_account_id: AccountId,
|
||||
solution: u128,
|
||||
) -> Result<HashType, 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.sequencer_client.send_transaction(NSSATransaction::Public(tx).into()).await?)
|
||||
}
|
||||
|
||||
pub async fn claim_pinata_private_owned_account_already_initialized(
|
||||
&self,
|
||||
pinata_account_id: AccountId,
|
||||
winner_account_id: AccountId,
|
||||
solution: u128,
|
||||
winner_proof: MembershipProof,
|
||||
) -> Result<(HashType, [SharedSecretKey; 1]), ExecutionFailureKind> {
|
||||
let AccountPreparedData {
|
||||
nsk: winner_nsk,
|
||||
npk: winner_npk,
|
||||
vpk: winner_vpk,
|
||||
auth_acc: winner_pre,
|
||||
proof: _,
|
||||
} = self
|
||||
.private_acc_preparation(winner_account_id, true, false)
|
||||
.await?;
|
||||
|
||||
let pinata_acc = self.get_account_public(pinata_account_id).await.unwrap();
|
||||
|
||||
let program = nssa::program::Program::pinata();
|
||||
|
||||
let pinata_pre = AccountWithMetadata::new(pinata_acc.clone(), false, pinata_account_id);
|
||||
|
||||
let eph_holder_winner = EphemeralKeyHolder::new(&winner_npk);
|
||||
let shared_secret_winner = eph_holder_winner.calculate_shared_secret_sender(&winner_vpk);
|
||||
|
||||
let (output, proof) = circuit::execute_and_prove(
|
||||
&[pinata_pre, winner_pre],
|
||||
&nssa::program::Program::serialize_instruction(solution).unwrap(),
|
||||
&[0, 1],
|
||||
&produce_random_nonces(1),
|
||||
&[(winner_npk, shared_secret_winner.clone())],
|
||||
&[(winner_nsk.unwrap())],
|
||||
&[winner_proof],
|
||||
&program.into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message =
|
||||
nssa::privacy_preserving_transaction::message::Message::try_from_circuit_output(
|
||||
vec![pinata_account_id],
|
||||
vec![],
|
||||
vec![(
|
||||
winner_npk,
|
||||
winner_vpk.clone(),
|
||||
eph_holder_winner.generate_ephemeral_public_key(),
|
||||
)],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let witness_set =
|
||||
nssa::privacy_preserving_transaction::witness_set::WitnessSet::for_message(
|
||||
&message,
|
||||
proof,
|
||||
&[],
|
||||
);
|
||||
let tx = nssa::privacy_preserving_transaction::PrivacyPreservingTransaction::new(
|
||||
message,
|
||||
witness_set,
|
||||
);
|
||||
|
||||
Ok((
|
||||
self.sequencer_client.send_transaction(NSSATransaction::PrivacyPreserving(tx).into()).await?,
|
||||
[shared_secret_winner],
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn claim_pinata_private_owned_account_not_initialized(
|
||||
&self,
|
||||
pinata_account_id: AccountId,
|
||||
winner_account_id: AccountId,
|
||||
solution: u128,
|
||||
) -> Result<(HashType, [SharedSecretKey; 1]), ExecutionFailureKind> {
|
||||
let AccountPreparedData {
|
||||
nsk: _,
|
||||
npk: winner_npk,
|
||||
vpk: winner_vpk,
|
||||
auth_acc: winner_pre,
|
||||
proof: _,
|
||||
} = self
|
||||
.private_acc_preparation(winner_account_id, false, false)
|
||||
.await?;
|
||||
|
||||
let pinata_acc = self.get_account_public(pinata_account_id).await.unwrap();
|
||||
|
||||
let program = nssa::program::Program::pinata();
|
||||
|
||||
let pinata_pre = AccountWithMetadata::new(pinata_acc.clone(), false, pinata_account_id);
|
||||
|
||||
let eph_holder_winner = EphemeralKeyHolder::new(&winner_npk);
|
||||
let shared_secret_winner = eph_holder_winner.calculate_shared_secret_sender(&winner_vpk);
|
||||
|
||||
let (output, proof) = circuit::execute_and_prove(
|
||||
&[pinata_pre, winner_pre],
|
||||
&nssa::program::Program::serialize_instruction(solution).unwrap(),
|
||||
&[0, 2],
|
||||
&produce_random_nonces(1),
|
||||
&[(winner_npk, shared_secret_winner.clone())],
|
||||
&[],
|
||||
&[],
|
||||
&program.into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message =
|
||||
nssa::privacy_preserving_transaction::message::Message::try_from_circuit_output(
|
||||
vec![pinata_account_id],
|
||||
vec![],
|
||||
vec![(
|
||||
winner_npk,
|
||||
winner_vpk.clone(),
|
||||
eph_holder_winner.generate_ephemeral_public_key(),
|
||||
)],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let witness_set =
|
||||
nssa::privacy_preserving_transaction::witness_set::WitnessSet::for_message(
|
||||
&message,
|
||||
proof,
|
||||
&[],
|
||||
);
|
||||
let tx = nssa::privacy_preserving_transaction::PrivacyPreservingTransaction::new(
|
||||
message,
|
||||
witness_set,
|
||||
);
|
||||
|
||||
Ok((
|
||||
self.sequencer_client.send_transaction(NSSATransaction::PrivacyPreserving(tx).into()).await?,
|
||||
[shared_secret_winner],
|
||||
))
|
||||
}
|
||||
}
|
||||
@ -30,6 +30,15 @@ pub enum PrivacyPreservingAccount {
|
||||
vpk: ViewingPublicKey,
|
||||
identifier: Identifier,
|
||||
},
|
||||
/// A shared regular private account with externally-provided keys (e.g. from GMS).
|
||||
/// Uses standard `AccountId = from((&npk, identifier))` with authorized/unauthorized private
|
||||
/// paths. Works with `authenticated_transfer` and all existing programs out of the box.
|
||||
PrivateShared {
|
||||
nsk: NullifierSecretKey,
|
||||
npk: NullifierPublicKey,
|
||||
vpk: ViewingPublicKey,
|
||||
identifier: Identifier,
|
||||
},
|
||||
}
|
||||
|
||||
impl PrivacyPreservingAccount {
|
||||
@ -46,6 +55,7 @@ impl PrivacyPreservingAccount {
|
||||
| Self::PrivateForeign { .. }
|
||||
| Self::PrivatePdaOwned(_)
|
||||
| Self::PrivatePdaForeign { .. }
|
||||
| Self::PrivateShared { .. }
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -146,6 +156,16 @@ impl AccountManager {
|
||||
};
|
||||
State::Private(pre)
|
||||
}
|
||||
PrivacyPreservingAccount::PrivateShared {
|
||||
nsk,
|
||||
npk,
|
||||
vpk,
|
||||
identifier,
|
||||
} => {
|
||||
let pre = private_shared_preparation(wallet, nsk, npk, vpk, identifier).await?;
|
||||
|
||||
State::Private(pre)
|
||||
}
|
||||
};
|
||||
|
||||
states.push(state);
|
||||
@ -280,40 +300,150 @@ async fn private_acc_preparation(
|
||||
account_id: AccountId,
|
||||
is_pda: bool,
|
||||
) -> Result<AccountPreparedData, ExecutionFailureKind> {
|
||||
let Some((from_keys, from_acc, from_identifier)) =
|
||||
if let Some((from_keys, from_acc, from_identifier)) =
|
||||
wallet.storage.user_data.get_private_account(account_id)
|
||||
else {
|
||||
return Err(ExecutionFailureKind::KeyNotFoundError);
|
||||
{
|
||||
let nsk = from_keys.private_key_holder.nullifier_secret_key;
|
||||
let from_npk = from_keys.nullifier_public_key;
|
||||
let from_vpk = from_keys.viewing_public_key;
|
||||
|
||||
// TODO: Remove this unwrap, error types must be compatible
|
||||
let proof = wallet
|
||||
.check_private_account_initialized(account_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// TODO: Technically we could allow unauthorized owned accounts, but currently we don't have
|
||||
// support from that in the wallet.
|
||||
let sender_pre = AccountWithMetadata::new(from_acc.clone(), true, account_id);
|
||||
|
||||
let eph_holder = EphemeralKeyHolder::new(&from_npk);
|
||||
let ssk = eph_holder.calculate_shared_secret_sender(&from_vpk);
|
||||
let epk = eph_holder.generate_ephemeral_public_key();
|
||||
|
||||
return Ok(AccountPreparedData {
|
||||
nsk: Some(nsk),
|
||||
npk: from_npk,
|
||||
identifier: from_identifier,
|
||||
vpk: from_vpk,
|
||||
pre_state: sender_pre,
|
||||
proof,
|
||||
ssk,
|
||||
epk,
|
||||
is_pda,
|
||||
});
|
||||
}
|
||||
|
||||
// Fallback: check shared storage for group PDAs
|
||||
let entry = wallet
|
||||
.storage
|
||||
.user_data
|
||||
.shared_private_account(&account_id)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?;
|
||||
|
||||
let pda_seed = entry.pda_seed.ok_or(ExecutionFailureKind::KeyNotFoundError)?;
|
||||
let program_id = entry.pda_program_id.ok_or(ExecutionFailureKind::KeyNotFoundError)?;
|
||||
|
||||
let holder = wallet
|
||||
.storage
|
||||
.user_data
|
||||
.group_key_holder(&entry.group_label)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?;
|
||||
|
||||
let keys = holder.derive_keys_for_pda(&program_id, &pda_seed);
|
||||
let nsk = keys.nullifier_secret_key;
|
||||
let npk = keys.generate_nullifier_public_key();
|
||||
let vpk = keys.generate_viewing_public_key();
|
||||
let identifier = entry.identifier;
|
||||
let acc = entry.account.clone();
|
||||
|
||||
let exists = acc != nssa_core::account::Account::default();
|
||||
let pre_state = AccountWithMetadata::new(acc, exists, account_id);
|
||||
|
||||
let proof = if exists {
|
||||
wallet
|
||||
.check_private_account_initialized(account_id)
|
||||
.await
|
||||
.unwrap_or(None)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let nsk = from_keys.private_key_holder.nullifier_secret_key;
|
||||
|
||||
let from_npk = from_keys.nullifier_public_key;
|
||||
let from_vpk = from_keys.viewing_public_key;
|
||||
|
||||
// TODO: Remove this unwrap, error types must be compatible
|
||||
let proof = wallet
|
||||
.check_private_account_initialized(account_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// TODO: Technically we could allow unauthorized owned accounts, but currently we don't have
|
||||
// support from that in the wallet.
|
||||
let sender_pre = AccountWithMetadata::new(from_acc.clone(), true, account_id);
|
||||
|
||||
let eph_holder = EphemeralKeyHolder::new(&from_npk);
|
||||
let ssk = eph_holder.calculate_shared_secret_sender(&from_vpk);
|
||||
let eph_holder = EphemeralKeyHolder::new(&npk);
|
||||
let ssk = eph_holder.calculate_shared_secret_sender(&vpk);
|
||||
let epk = eph_holder.generate_ephemeral_public_key();
|
||||
|
||||
Ok(AccountPreparedData {
|
||||
nsk: Some(nsk),
|
||||
npk: from_npk,
|
||||
identifier: from_identifier,
|
||||
vpk: from_vpk,
|
||||
pre_state: sender_pre,
|
||||
nsk: exists.then_some(nsk),
|
||||
npk,
|
||||
identifier,
|
||||
vpk,
|
||||
pre_state,
|
||||
proof,
|
||||
ssk,
|
||||
epk,
|
||||
is_pda,
|
||||
is_pda: true,
|
||||
})
|
||||
}
|
||||
|
||||
async fn private_shared_preparation(
|
||||
wallet: &WalletCore,
|
||||
nsk: NullifierSecretKey,
|
||||
npk: NullifierPublicKey,
|
||||
vpk: ViewingPublicKey,
|
||||
identifier: Identifier,
|
||||
) -> Result<AccountPreparedData, ExecutionFailureKind> {
|
||||
let account_id = nssa::AccountId::from((&npk, identifier));
|
||||
|
||||
let acc = wallet
|
||||
.storage
|
||||
.user_data
|
||||
.shared_private_account(&account_id)
|
||||
.map(|e| e.account.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
let exists = acc != nssa_core::account::Account::default();
|
||||
let pre_state = AccountWithMetadata::new(acc, exists, (&npk, identifier));
|
||||
|
||||
let proof = if exists {
|
||||
wallet
|
||||
.check_private_account_initialized(account_id)
|
||||
.await
|
||||
.unwrap_or(None)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let eph_holder = EphemeralKeyHolder::new(&npk);
|
||||
let ssk = eph_holder.calculate_shared_secret_sender(&vpk);
|
||||
let epk = eph_holder.generate_ephemeral_public_key();
|
||||
|
||||
Ok(AccountPreparedData {
|
||||
nsk: exists.then_some(nsk),
|
||||
npk,
|
||||
identifier,
|
||||
is_pda: false,
|
||||
vpk,
|
||||
pre_state,
|
||||
proof,
|
||||
ssk,
|
||||
epk,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn private_shared_is_private() {
|
||||
let acc = PrivacyPreservingAccount::PrivateShared {
|
||||
nsk: [0; 32],
|
||||
npk: NullifierPublicKey([1; 32]),
|
||||
vpk: ViewingPublicKey::from_scalar([2; 32]),
|
||||
identifier: 42,
|
||||
};
|
||||
assert!(acc.is_private());
|
||||
assert!(!acc.is_public());
|
||||
}
|
||||
}
|
||||
|
||||
@ -188,7 +188,9 @@ impl Ata<'_> {
|
||||
Program::serialize_instruction(instruction).expect("Instruction should serialize");
|
||||
|
||||
let accounts = vec![
|
||||
PrivacyPreservingAccount::PrivateOwned(owner_id),
|
||||
self.0
|
||||
.resolve_private_account(owner_id)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
PrivacyPreservingAccount::Public(definition_id),
|
||||
PrivacyPreservingAccount::Public(ata_id),
|
||||
];
|
||||
@ -223,7 +225,9 @@ impl Ata<'_> {
|
||||
Program::serialize_instruction(instruction).expect("Instruction should serialize");
|
||||
|
||||
let accounts = vec![
|
||||
PrivacyPreservingAccount::PrivateOwned(owner_id),
|
||||
self.0
|
||||
.resolve_private_account(owner_id)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
PrivacyPreservingAccount::Public(sender_ata_id),
|
||||
PrivacyPreservingAccount::Public(recipient_id),
|
||||
];
|
||||
@ -257,7 +261,9 @@ impl Ata<'_> {
|
||||
Program::serialize_instruction(instruction).expect("Instruction should serialize");
|
||||
|
||||
let accounts = vec![
|
||||
PrivacyPreservingAccount::PrivateOwned(owner_id),
|
||||
self.0
|
||||
.resolve_private_account(owner_id)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
PrivacyPreservingAccount::Public(holder_ata_id),
|
||||
PrivacyPreservingAccount::Public(definition_id),
|
||||
];
|
||||
|
||||
@ -16,7 +16,9 @@ impl NativeTokenTransfer<'_> {
|
||||
self.0
|
||||
.send_privacy_preserving_tx_with_pre_check(
|
||||
vec![
|
||||
PrivacyPreservingAccount::PrivateOwned(from),
|
||||
self.0
|
||||
.resolve_private_account(from)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
PrivacyPreservingAccount::Public(to),
|
||||
],
|
||||
instruction_data,
|
||||
|
||||
@ -14,9 +14,14 @@ impl NativeTokenTransfer<'_> {
|
||||
) -> Result<(HashType, SharedSecretKey), ExecutionFailureKind> {
|
||||
let instruction: u128 = 0;
|
||||
|
||||
let account = self
|
||||
.0
|
||||
.resolve_private_account(from)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?;
|
||||
|
||||
self.0
|
||||
.send_privacy_preserving_tx(
|
||||
vec![PrivacyPreservingAccount::PrivateOwned(from)],
|
||||
vec![account],
|
||||
Program::serialize_instruction(instruction).unwrap(),
|
||||
&Program::authenticated_transfer_program().into(),
|
||||
)
|
||||
@ -41,7 +46,9 @@ impl NativeTokenTransfer<'_> {
|
||||
self.0
|
||||
.send_privacy_preserving_tx_with_pre_check(
|
||||
vec![
|
||||
PrivacyPreservingAccount::PrivateOwned(from),
|
||||
self.0
|
||||
.resolve_private_account(from)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
PrivacyPreservingAccount::PrivateForeign {
|
||||
npk: to_npk,
|
||||
vpk: to_vpk,
|
||||
@ -69,12 +76,18 @@ impl NativeTokenTransfer<'_> {
|
||||
) -> Result<(HashType, [SharedSecretKey; 2]), ExecutionFailureKind> {
|
||||
let (instruction_data, program, tx_pre_check) = auth_transfer_preparation(balance_to_move);
|
||||
|
||||
let from_account = self
|
||||
.0
|
||||
.resolve_private_account(from)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?;
|
||||
let to_account = self
|
||||
.0
|
||||
.resolve_private_account(to)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?;
|
||||
|
||||
self.0
|
||||
.send_privacy_preserving_tx_with_pre_check(
|
||||
vec![
|
||||
PrivacyPreservingAccount::PrivateOwned(from),
|
||||
PrivacyPreservingAccount::PrivateOwned(to),
|
||||
],
|
||||
vec![from_account, to_account],
|
||||
instruction_data,
|
||||
&program.into(),
|
||||
tx_pre_check,
|
||||
|
||||
@ -18,7 +18,9 @@ impl NativeTokenTransfer<'_> {
|
||||
.send_privacy_preserving_tx_with_pre_check(
|
||||
vec![
|
||||
PrivacyPreservingAccount::Public(from),
|
||||
PrivacyPreservingAccount::PrivateOwned(to),
|
||||
self.0
|
||||
.resolve_private_account(to)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
],
|
||||
instruction_data,
|
||||
&program.into(),
|
||||
|
||||
@ -56,7 +56,9 @@ impl Pinata<'_> {
|
||||
.send_privacy_preserving_tx(
|
||||
vec![
|
||||
PrivacyPreservingAccount::Public(pinata_account_id),
|
||||
PrivacyPreservingAccount::PrivateOwned(winner_account_id),
|
||||
self.0
|
||||
.resolve_private_account(winner_account_id)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
],
|
||||
nssa::program::Program::serialize_instruction(solution).unwrap(),
|
||||
&nssa::program::Program::pinata().into(),
|
||||
|
||||
@ -74,7 +74,9 @@ impl Token<'_> {
|
||||
.send_privacy_preserving_tx(
|
||||
vec![
|
||||
PrivacyPreservingAccount::Public(definition_account_id),
|
||||
PrivacyPreservingAccount::PrivateOwned(supply_account_id),
|
||||
self.0
|
||||
.resolve_private_account(supply_account_id)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
],
|
||||
instruction_data,
|
||||
&Program::token().into(),
|
||||
@ -103,7 +105,9 @@ impl Token<'_> {
|
||||
self.0
|
||||
.send_privacy_preserving_tx(
|
||||
vec![
|
||||
PrivacyPreservingAccount::PrivateOwned(definition_account_id),
|
||||
self.0
|
||||
.resolve_private_account(definition_account_id)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
PrivacyPreservingAccount::Public(supply_account_id),
|
||||
],
|
||||
instruction_data,
|
||||
@ -133,8 +137,12 @@ impl Token<'_> {
|
||||
self.0
|
||||
.send_privacy_preserving_tx(
|
||||
vec![
|
||||
PrivacyPreservingAccount::PrivateOwned(definition_account_id),
|
||||
PrivacyPreservingAccount::PrivateOwned(supply_account_id),
|
||||
self.0
|
||||
.resolve_private_account(definition_account_id)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
self.0
|
||||
.resolve_private_account(supply_account_id)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
],
|
||||
instruction_data,
|
||||
&Program::token().into(),
|
||||
@ -227,8 +235,12 @@ impl Token<'_> {
|
||||
self.0
|
||||
.send_privacy_preserving_tx(
|
||||
vec![
|
||||
PrivacyPreservingAccount::PrivateOwned(sender_account_id),
|
||||
PrivacyPreservingAccount::PrivateOwned(recipient_account_id),
|
||||
self.0
|
||||
.resolve_private_account(sender_account_id)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
self.0
|
||||
.resolve_private_account(recipient_account_id)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
],
|
||||
instruction_data,
|
||||
&Program::token().into(),
|
||||
@ -259,7 +271,9 @@ impl Token<'_> {
|
||||
self.0
|
||||
.send_privacy_preserving_tx(
|
||||
vec![
|
||||
PrivacyPreservingAccount::PrivateOwned(sender_account_id),
|
||||
self.0
|
||||
.resolve_private_account(sender_account_id)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
PrivacyPreservingAccount::PrivateForeign {
|
||||
npk: recipient_npk,
|
||||
vpk: recipient_vpk,
|
||||
@ -293,7 +307,9 @@ impl Token<'_> {
|
||||
self.0
|
||||
.send_privacy_preserving_tx(
|
||||
vec![
|
||||
PrivacyPreservingAccount::PrivateOwned(sender_account_id),
|
||||
self.0
|
||||
.resolve_private_account(sender_account_id)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
PrivacyPreservingAccount::Public(recipient_account_id),
|
||||
],
|
||||
instruction_data,
|
||||
@ -325,7 +341,9 @@ impl Token<'_> {
|
||||
.send_privacy_preserving_tx(
|
||||
vec![
|
||||
PrivacyPreservingAccount::Public(sender_account_id),
|
||||
PrivacyPreservingAccount::PrivateOwned(recipient_account_id),
|
||||
self.0
|
||||
.resolve_private_account(recipient_account_id)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
],
|
||||
instruction_data,
|
||||
&Program::token().into(),
|
||||
@ -434,8 +452,12 @@ impl Token<'_> {
|
||||
self.0
|
||||
.send_privacy_preserving_tx(
|
||||
vec![
|
||||
PrivacyPreservingAccount::PrivateOwned(definition_account_id),
|
||||
PrivacyPreservingAccount::PrivateOwned(holder_account_id),
|
||||
self.0
|
||||
.resolve_private_account(definition_account_id)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
self.0
|
||||
.resolve_private_account(holder_account_id)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
],
|
||||
instruction_data,
|
||||
&Program::token().into(),
|
||||
@ -464,7 +486,9 @@ impl Token<'_> {
|
||||
self.0
|
||||
.send_privacy_preserving_tx(
|
||||
vec![
|
||||
PrivacyPreservingAccount::PrivateOwned(definition_account_id),
|
||||
self.0
|
||||
.resolve_private_account(definition_account_id)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
PrivacyPreservingAccount::Public(holder_account_id),
|
||||
],
|
||||
instruction_data,
|
||||
@ -496,7 +520,9 @@ impl Token<'_> {
|
||||
.send_privacy_preserving_tx(
|
||||
vec![
|
||||
PrivacyPreservingAccount::Public(definition_account_id),
|
||||
PrivacyPreservingAccount::PrivateOwned(holder_account_id),
|
||||
self.0
|
||||
.resolve_private_account(holder_account_id)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
],
|
||||
instruction_data,
|
||||
&Program::token().into(),
|
||||
@ -590,8 +616,12 @@ impl Token<'_> {
|
||||
self.0
|
||||
.send_privacy_preserving_tx(
|
||||
vec![
|
||||
PrivacyPreservingAccount::PrivateOwned(definition_account_id),
|
||||
PrivacyPreservingAccount::PrivateOwned(holder_account_id),
|
||||
self.0
|
||||
.resolve_private_account(definition_account_id)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
self.0
|
||||
.resolve_private_account(holder_account_id)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
],
|
||||
instruction_data,
|
||||
&Program::token().into(),
|
||||
@ -622,7 +652,9 @@ impl Token<'_> {
|
||||
self.0
|
||||
.send_privacy_preserving_tx(
|
||||
vec![
|
||||
PrivacyPreservingAccount::PrivateOwned(definition_account_id),
|
||||
self.0
|
||||
.resolve_private_account(definition_account_id)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
PrivacyPreservingAccount::PrivateForeign {
|
||||
npk: holder_npk,
|
||||
vpk: holder_vpk,
|
||||
@ -656,7 +688,9 @@ impl Token<'_> {
|
||||
self.0
|
||||
.send_privacy_preserving_tx(
|
||||
vec![
|
||||
PrivacyPreservingAccount::PrivateOwned(definition_account_id),
|
||||
self.0
|
||||
.resolve_private_account(definition_account_id)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
PrivacyPreservingAccount::Public(holder_account_id),
|
||||
],
|
||||
instruction_data,
|
||||
@ -688,7 +722,9 @@ impl Token<'_> {
|
||||
.send_privacy_preserving_tx(
|
||||
vec![
|
||||
PrivacyPreservingAccount::Public(definition_account_id),
|
||||
PrivacyPreservingAccount::PrivateOwned(holder_account_id),
|
||||
self.0
|
||||
.resolve_private_account(holder_account_id)
|
||||
.ok_or(ExecutionFailureKind::KeyNotFoundError)?,
|
||||
],
|
||||
instruction_data,
|
||||
&Program::token().into(),
|
||||
|
||||
@ -1,594 +0,0 @@
|
||||
use common::{HashType, transaction::NSSATransaction};
|
||||
use sequencer_service_rpc::RpcClient as _;
|
||||
use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder;
|
||||
use nssa::{
|
||||
Account, AccountId, PrivacyPreservingTransaction,
|
||||
privacy_preserving_transaction::{circuit, message::Message, witness_set::WitnessSet},
|
||||
program::Program,
|
||||
};
|
||||
use nssa_core::{
|
||||
Commitment, MembershipProof, NullifierPublicKey, NullifierSecretKey, SharedSecretKey,
|
||||
account::AccountWithMetadata, encryption::ViewingPublicKey, program::InstructionData,
|
||||
};
|
||||
|
||||
use crate::{ExecutionFailureKind, WalletCore, helperfunctions::produce_random_nonces};
|
||||
|
||||
pub(crate) struct AccountPreparedData {
|
||||
pub nsk: Option<NullifierSecretKey>,
|
||||
pub npk: NullifierPublicKey,
|
||||
pub vpk: ViewingPublicKey,
|
||||
pub auth_acc: AccountWithMetadata,
|
||||
pub proof: Option<MembershipProof>,
|
||||
}
|
||||
|
||||
impl WalletCore {
|
||||
pub(crate) async fn private_acc_preparation(
|
||||
&self,
|
||||
account_id: AccountId,
|
||||
is_authorized: bool,
|
||||
needs_proof: bool,
|
||||
) -> Result<AccountPreparedData, ExecutionFailureKind> {
|
||||
let Some((from_keys, from_acc)) = self
|
||||
.storage
|
||||
.user_data
|
||||
.get_private_account(&account_id)
|
||||
.cloned()
|
||||
else {
|
||||
return Err(ExecutionFailureKind::KeyNotFoundError);
|
||||
};
|
||||
|
||||
let mut nsk = None;
|
||||
let mut proof = None;
|
||||
|
||||
let from_npk = from_keys.nullifier_public_key;
|
||||
let from_vpk = from_keys.viewing_public_key;
|
||||
|
||||
let sender_commitment = Commitment::new(&from_npk, &from_acc);
|
||||
|
||||
let sender_pre = AccountWithMetadata::new(from_acc.clone(), is_authorized, &from_npk);
|
||||
|
||||
if is_authorized {
|
||||
nsk = Some(from_keys.private_key_holder.nullifier_secret_key);
|
||||
}
|
||||
|
||||
if needs_proof {
|
||||
proof = Some(
|
||||
self.sequencer_client
|
||||
.get_proof_for_commitment(sender_commitment)
|
||||
.await
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(AccountPreparedData {
|
||||
nsk,
|
||||
npk: from_npk,
|
||||
vpk: from_vpk,
|
||||
auth_acc: sender_pre,
|
||||
proof,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn private_tx_two_accs_all_init(
|
||||
&self,
|
||||
from: AccountId,
|
||||
to: AccountId,
|
||||
instruction_data: InstructionData,
|
||||
tx_pre_check: impl FnOnce(&Account, &Account) -> Result<(), ExecutionFailureKind>,
|
||||
program: Program,
|
||||
to_proof: MembershipProof,
|
||||
) -> Result<(HashType, [SharedSecretKey; 2]), ExecutionFailureKind> {
|
||||
let AccountPreparedData {
|
||||
nsk: from_nsk,
|
||||
npk: from_npk,
|
||||
vpk: from_vpk,
|
||||
auth_acc: sender_pre,
|
||||
proof: from_proof,
|
||||
} = self.private_acc_preparation(from, true, true).await?;
|
||||
|
||||
let AccountPreparedData {
|
||||
nsk: to_nsk,
|
||||
npk: to_npk,
|
||||
vpk: to_vpk,
|
||||
auth_acc: recipient_pre,
|
||||
proof: _,
|
||||
} = self.private_acc_preparation(to, true, false).await?;
|
||||
|
||||
tx_pre_check(&sender_pre.account, &recipient_pre.account)?;
|
||||
|
||||
let eph_holder_from = EphemeralKeyHolder::new(&from_npk);
|
||||
let shared_secret_from = eph_holder_from.calculate_shared_secret_sender(&from_vpk);
|
||||
|
||||
let eph_holder_to = EphemeralKeyHolder::new(&to_npk);
|
||||
let shared_secret_to = eph_holder_to.calculate_shared_secret_sender(&to_vpk);
|
||||
|
||||
let (output, proof) = circuit::execute_and_prove(
|
||||
&[sender_pre, recipient_pre],
|
||||
&instruction_data,
|
||||
&[1, 1],
|
||||
&produce_random_nonces(2),
|
||||
&[
|
||||
(from_npk, shared_secret_from.clone()),
|
||||
(to_npk, shared_secret_to.clone()),
|
||||
],
|
||||
&[
|
||||
(from_nsk.unwrap(), from_proof.unwrap()),
|
||||
(to_nsk.unwrap(), to_proof),
|
||||
],
|
||||
&program.into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![],
|
||||
vec![],
|
||||
vec![
|
||||
(
|
||||
from_npk,
|
||||
from_vpk.clone(),
|
||||
eph_holder_from.generate_ephemeral_public_key(),
|
||||
),
|
||||
(
|
||||
to_npk,
|
||||
to_vpk.clone(),
|
||||
eph_holder_to.generate_ephemeral_public_key(),
|
||||
),
|
||||
],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]);
|
||||
let tx = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
|
||||
Ok((
|
||||
self.sequencer_client.send_transaction(NSSATransaction::PrivacyPreserving(tx).into()).await?,
|
||||
[shared_secret_from, shared_secret_to],
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn private_tx_two_accs_receiver_uninit(
|
||||
&self,
|
||||
from: AccountId,
|
||||
to: AccountId,
|
||||
instruction_data: InstructionData,
|
||||
tx_pre_check: impl FnOnce(&Account, &Account) -> Result<(), ExecutionFailureKind>,
|
||||
program: Program,
|
||||
) -> Result<(HashType, [SharedSecretKey; 2]), ExecutionFailureKind> {
|
||||
let AccountPreparedData {
|
||||
nsk: from_nsk,
|
||||
npk: from_npk,
|
||||
vpk: from_vpk,
|
||||
auth_acc: sender_pre,
|
||||
proof: from_proof,
|
||||
} = self.private_acc_preparation(from, true, true).await?;
|
||||
|
||||
let AccountPreparedData {
|
||||
nsk: _,
|
||||
npk: to_npk,
|
||||
vpk: to_vpk,
|
||||
auth_acc: recipient_pre,
|
||||
proof: _,
|
||||
} = self.private_acc_preparation(to, false, false).await?;
|
||||
|
||||
tx_pre_check(&sender_pre.account, &recipient_pre.account)?;
|
||||
|
||||
let eph_holder_from = EphemeralKeyHolder::new(&from_npk);
|
||||
let shared_secret_from = eph_holder_from.calculate_shared_secret_sender(&from_vpk);
|
||||
|
||||
let eph_holder_to = EphemeralKeyHolder::new(&to_npk);
|
||||
let shared_secret_to = eph_holder_to.calculate_shared_secret_sender(&to_vpk);
|
||||
|
||||
let (output, proof) = circuit::execute_and_prove(
|
||||
&[sender_pre, recipient_pre],
|
||||
&instruction_data,
|
||||
&[1, 2],
|
||||
&produce_random_nonces(2),
|
||||
&[
|
||||
(from_npk, shared_secret_from.clone()),
|
||||
(to_npk, shared_secret_to.clone()),
|
||||
],
|
||||
&[(from_nsk.unwrap(), from_proof.unwrap())],
|
||||
&program.into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![],
|
||||
vec![],
|
||||
vec![
|
||||
(
|
||||
from_npk,
|
||||
from_vpk.clone(),
|
||||
eph_holder_from.generate_ephemeral_public_key(),
|
||||
),
|
||||
(
|
||||
to_npk,
|
||||
to_vpk.clone(),
|
||||
eph_holder_to.generate_ephemeral_public_key(),
|
||||
),
|
||||
],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]);
|
||||
let tx = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
|
||||
Ok((
|
||||
self.sequencer_client.send_transaction(NSSATransaction::PrivacyPreserving(tx).into()).await?,
|
||||
[shared_secret_from, shared_secret_to],
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn private_tx_two_accs_receiver_outer(
|
||||
&self,
|
||||
from: AccountId,
|
||||
to_npk: NullifierPublicKey,
|
||||
to_vpk: ViewingPublicKey,
|
||||
instruction_data: InstructionData,
|
||||
tx_pre_check: impl FnOnce(&Account, &Account) -> Result<(), ExecutionFailureKind>,
|
||||
program: Program,
|
||||
) -> Result<(HashType, [SharedSecretKey; 2]), ExecutionFailureKind> {
|
||||
let AccountPreparedData {
|
||||
nsk: from_nsk,
|
||||
npk: from_npk,
|
||||
vpk: from_vpk,
|
||||
auth_acc: sender_pre,
|
||||
proof: from_proof,
|
||||
} = self.private_acc_preparation(from, true, true).await?;
|
||||
|
||||
let to_acc = nssa_core::account::Account::default();
|
||||
|
||||
tx_pre_check(&sender_pre.account, &to_acc)?;
|
||||
|
||||
let recipient_pre = AccountWithMetadata::new(to_acc.clone(), false, &to_npk);
|
||||
|
||||
let eph_holder = EphemeralKeyHolder::new(&to_npk);
|
||||
|
||||
let shared_secret_from = eph_holder.calculate_shared_secret_sender(&from_vpk);
|
||||
let shared_secret_to = eph_holder.calculate_shared_secret_sender(&to_vpk);
|
||||
|
||||
let (output, proof) = circuit::execute_and_prove(
|
||||
&[sender_pre, recipient_pre],
|
||||
&instruction_data,
|
||||
&[1, 2],
|
||||
&produce_random_nonces(2),
|
||||
&[
|
||||
(from_npk, shared_secret_from.clone()),
|
||||
(to_npk, shared_secret_to.clone()),
|
||||
],
|
||||
&[(from_nsk.unwrap(), from_proof.unwrap())],
|
||||
&program.into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![],
|
||||
vec![],
|
||||
vec![
|
||||
(
|
||||
from_npk,
|
||||
from_vpk.clone(),
|
||||
eph_holder.generate_ephemeral_public_key(),
|
||||
),
|
||||
(
|
||||
to_npk,
|
||||
to_vpk.clone(),
|
||||
eph_holder.generate_ephemeral_public_key(),
|
||||
),
|
||||
],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]);
|
||||
|
||||
let tx = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
|
||||
Ok((
|
||||
self.sequencer_client.send_transaction(NSSATransaction::PrivacyPreserving(tx).into()).await?,
|
||||
[shared_secret_from, shared_secret_to],
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn deshielded_tx_two_accs(
|
||||
&self,
|
||||
from: AccountId,
|
||||
to: AccountId,
|
||||
instruction_data: InstructionData,
|
||||
tx_pre_check: impl FnOnce(&Account, &Account) -> Result<(), ExecutionFailureKind>,
|
||||
program: Program,
|
||||
) -> Result<(HashType, [nssa_core::SharedSecretKey; 1]), ExecutionFailureKind> {
|
||||
let AccountPreparedData {
|
||||
nsk: from_nsk,
|
||||
npk: from_npk,
|
||||
vpk: from_vpk,
|
||||
auth_acc: sender_pre,
|
||||
proof: from_proof,
|
||||
} = self.private_acc_preparation(from, true, true).await?;
|
||||
|
||||
let Ok(to_acc) = self.get_account_public(to).await else {
|
||||
return Err(ExecutionFailureKind::KeyNotFoundError);
|
||||
};
|
||||
|
||||
tx_pre_check(&sender_pre.account, &to_acc)?;
|
||||
|
||||
let recipient_pre = AccountWithMetadata::new(to_acc.clone(), false, to);
|
||||
|
||||
let eph_holder = EphemeralKeyHolder::new(&from_npk);
|
||||
let shared_secret = eph_holder.calculate_shared_secret_sender(&from_vpk);
|
||||
|
||||
let (output, proof) = circuit::execute_and_prove(
|
||||
&[sender_pre, recipient_pre],
|
||||
&instruction_data,
|
||||
&[1, 0],
|
||||
&produce_random_nonces(1),
|
||||
&[(from_npk, shared_secret.clone())],
|
||||
&[(from_nsk.unwrap(), from_proof.unwrap())],
|
||||
&program.into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![to],
|
||||
vec![],
|
||||
vec![(
|
||||
from_npk,
|
||||
from_vpk.clone(),
|
||||
eph_holder.generate_ephemeral_public_key(),
|
||||
)],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]);
|
||||
|
||||
let tx = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
|
||||
Ok((
|
||||
self.sequencer_client.send_transaction(NSSATransaction::PrivacyPreserving(tx).into()).await?,
|
||||
[shared_secret],
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn shielded_two_accs_all_init(
|
||||
&self,
|
||||
from: AccountId,
|
||||
to: AccountId,
|
||||
instruction_data: InstructionData,
|
||||
tx_pre_check: impl FnOnce(&Account, &Account) -> Result<(), ExecutionFailureKind>,
|
||||
program: Program,
|
||||
to_proof: MembershipProof,
|
||||
) -> Result<(HashType, [SharedSecretKey; 1]), ExecutionFailureKind> {
|
||||
let Ok(from_acc) = self.get_account_public(from).await else {
|
||||
return Err(ExecutionFailureKind::KeyNotFoundError);
|
||||
};
|
||||
|
||||
let AccountPreparedData {
|
||||
nsk: to_nsk,
|
||||
npk: to_npk,
|
||||
vpk: to_vpk,
|
||||
auth_acc: recipient_pre,
|
||||
proof: _,
|
||||
} = self.private_acc_preparation(to, true, false).await?;
|
||||
|
||||
tx_pre_check(&from_acc, &recipient_pre.account)?;
|
||||
|
||||
let sender_pre = AccountWithMetadata::new(from_acc.clone(), true, from);
|
||||
|
||||
let eph_holder = EphemeralKeyHolder::new(&to_npk);
|
||||
let shared_secret = eph_holder.calculate_shared_secret_sender(&to_vpk);
|
||||
|
||||
let (output, proof) = circuit::execute_and_prove(
|
||||
&[sender_pre, recipient_pre],
|
||||
&instruction_data,
|
||||
&[0, 1],
|
||||
&produce_random_nonces(1),
|
||||
&[(to_npk, shared_secret.clone())],
|
||||
&[(to_nsk.unwrap(), to_proof)],
|
||||
&program.into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![from],
|
||||
vec![from_acc.nonce],
|
||||
vec![(
|
||||
to_npk,
|
||||
to_vpk.clone(),
|
||||
eph_holder.generate_ephemeral_public_key(),
|
||||
)],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let signing_key = self.storage.user_data.get_pub_account_signing_key(&from);
|
||||
|
||||
let Some(signing_key) = signing_key else {
|
||||
return Err(ExecutionFailureKind::KeyNotFoundError);
|
||||
};
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[signing_key]);
|
||||
|
||||
let tx = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
|
||||
Ok((
|
||||
self.sequencer_client.send_transaction(NSSATransaction::PrivacyPreserving(tx).into()).await?,
|
||||
[shared_secret],
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn shielded_two_accs_receiver_uninit(
|
||||
&self,
|
||||
from: AccountId,
|
||||
to: AccountId,
|
||||
instruction_data: InstructionData,
|
||||
tx_pre_check: impl FnOnce(&Account, &Account) -> Result<(), ExecutionFailureKind>,
|
||||
program: Program,
|
||||
) -> Result<(HashType, [SharedSecretKey; 1]), ExecutionFailureKind> {
|
||||
let Ok(from_acc) = self.get_account_public(from).await else {
|
||||
return Err(ExecutionFailureKind::KeyNotFoundError);
|
||||
};
|
||||
|
||||
let AccountPreparedData {
|
||||
nsk: _,
|
||||
npk: to_npk,
|
||||
vpk: to_vpk,
|
||||
auth_acc: recipient_pre,
|
||||
proof: _,
|
||||
} = self.private_acc_preparation(to, false, false).await?;
|
||||
|
||||
tx_pre_check(&from_acc, &recipient_pre.account)?;
|
||||
|
||||
let sender_pre = AccountWithMetadata::new(from_acc.clone(), true, from);
|
||||
|
||||
let eph_holder = EphemeralKeyHolder::new(&to_npk);
|
||||
let shared_secret = eph_holder.calculate_shared_secret_sender(&to_vpk);
|
||||
|
||||
let (output, proof) = circuit::execute_and_prove(
|
||||
&[sender_pre, recipient_pre],
|
||||
&instruction_data,
|
||||
&[0, 2],
|
||||
&produce_random_nonces(1),
|
||||
&[(to_npk, shared_secret.clone())],
|
||||
&[],
|
||||
&program.into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![from],
|
||||
vec![from_acc.nonce],
|
||||
vec![(
|
||||
to_npk,
|
||||
to_vpk.clone(),
|
||||
eph_holder.generate_ephemeral_public_key(),
|
||||
)],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let signing_key = self.storage.user_data.get_pub_account_signing_key(&from);
|
||||
|
||||
let Some(signing_key) = signing_key else {
|
||||
return Err(ExecutionFailureKind::KeyNotFoundError);
|
||||
};
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[signing_key]);
|
||||
|
||||
let tx = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
|
||||
Ok((
|
||||
self.sequencer_client.send_transaction(NSSATransaction::PrivacyPreserving(tx).into()).await?,
|
||||
[shared_secret],
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn shielded_two_accs_receiver_outer(
|
||||
&self,
|
||||
from: AccountId,
|
||||
to_npk: NullifierPublicKey,
|
||||
to_vpk: ViewingPublicKey,
|
||||
instruction_data: InstructionData,
|
||||
tx_pre_check: impl FnOnce(&Account, &Account) -> Result<(), ExecutionFailureKind>,
|
||||
program: Program,
|
||||
) -> Result<HashType, ExecutionFailureKind> {
|
||||
let Ok(from_acc) = self.get_account_public(from).await else {
|
||||
return Err(ExecutionFailureKind::KeyNotFoundError);
|
||||
};
|
||||
|
||||
let to_acc = Account::default();
|
||||
|
||||
tx_pre_check(&from_acc, &to_acc)?;
|
||||
|
||||
let sender_pre = AccountWithMetadata::new(from_acc.clone(), true, from);
|
||||
let recipient_pre = AccountWithMetadata::new(to_acc.clone(), false, &to_npk);
|
||||
|
||||
let eph_holder = EphemeralKeyHolder::new(&to_npk);
|
||||
let shared_secret = eph_holder.calculate_shared_secret_sender(&to_vpk);
|
||||
|
||||
let (output, proof) = circuit::execute_and_prove(
|
||||
&[sender_pre, recipient_pre],
|
||||
&instruction_data,
|
||||
&[0, 2],
|
||||
&produce_random_nonces(1),
|
||||
&[(to_npk, shared_secret.clone())],
|
||||
&[],
|
||||
&program.into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![from],
|
||||
vec![from_acc.nonce],
|
||||
vec![(
|
||||
to_npk,
|
||||
to_vpk.clone(),
|
||||
eph_holder.generate_ephemeral_public_key(),
|
||||
)],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let signing_key = self.storage.user_data.get_pub_account_signing_key(&from);
|
||||
|
||||
let Some(signing_key) = signing_key else {
|
||||
return Err(ExecutionFailureKind::KeyNotFoundError);
|
||||
};
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[signing_key]);
|
||||
let tx = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
|
||||
Ok(self.sequencer_client.send_transaction(NSSATransaction::PrivacyPreserving(tx).into()).await?)
|
||||
}
|
||||
|
||||
pub async fn register_account_under_authenticated_transfers_programs_private(
|
||||
&self,
|
||||
from: AccountId,
|
||||
) -> Result<(HashType, [SharedSecretKey; 1]), ExecutionFailureKind> {
|
||||
let AccountPreparedData {
|
||||
nsk: _,
|
||||
npk: from_npk,
|
||||
vpk: from_vpk,
|
||||
auth_acc: sender_pre,
|
||||
proof: _,
|
||||
} = self.private_acc_preparation(from, false, false).await?;
|
||||
|
||||
let eph_holder_from = EphemeralKeyHolder::new(&from_npk);
|
||||
let shared_secret_from = eph_holder_from.calculate_shared_secret_sender(&from_vpk);
|
||||
|
||||
let instruction: u128 = 0;
|
||||
|
||||
let (output, proof) = circuit::execute_and_prove(
|
||||
&[sender_pre],
|
||||
&Program::serialize_instruction(instruction).unwrap(),
|
||||
&[2],
|
||||
&produce_random_nonces(1),
|
||||
&[(from_npk, shared_secret_from.clone())],
|
||||
&[],
|
||||
&Program::authenticated_transfer_program().into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![],
|
||||
vec![],
|
||||
vec![(
|
||||
from_npk,
|
||||
from_vpk.clone(),
|
||||
eph_holder_from.generate_ephemeral_public_key(),
|
||||
)],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]);
|
||||
let tx = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
|
||||
Ok((
|
||||
self.sequencer_client.send_transaction(NSSATransaction::PrivacyPreserving(tx).into()).await?,
|
||||
[shared_secret_from],
|
||||
))
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user