mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-05-14 03:59:30 +00:00
Merge branch 'main' into schouhy/add-specs
This commit is contained in:
commit
a338a0b7ae
@ -14,6 +14,8 @@ ignore = [
|
||||
{ id = "RUSTSEC-2025-0141", reason = "`bincode` is unmaintained but continuing to use it." },
|
||||
{ id = "RUSTSEC-2023-0089", reason = "atomic-polyfill is pulled transitively via risc0-zkvm; waiting on upstream fix (see https://github.com/risc0/risc0/issues/3453)" },
|
||||
{ id = "RUSTSEC-2026-0097", reason = "`rand` v0.8.5 is present transitively from logos crates, modification may break integration" },
|
||||
{ id = "RUSTSEC-2026-0118", reason = "`hickory-proto` v0.25.0-alpha.5 is present transitively from logos crates, modification may break integration"},
|
||||
{ id = "RUSTSEC-2026-0119", reason = "`hickory-proto` v0.25.0-alpha.5 is present transitively from logos crates, modification may break integration"},
|
||||
]
|
||||
yanked = "deny"
|
||||
unused-ignored-advisory = "deny"
|
||||
|
||||
19
Cargo.lock
generated
19
Cargo.lock
generated
@ -2064,7 +2064,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de"
|
||||
dependencies = [
|
||||
"data-encoding",
|
||||
"syn 2.0.117",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2558,7 +2558,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -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",
|
||||
@ -6335,7 +6342,7 @@ version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -8120,7 +8127,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -9089,7 +9096,7 @@ dependencies = [
|
||||
"getrandom 0.4.2",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -10375,7 +10382,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@ -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" }
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
artifacts/test_program_methods/group_pda_spender.bin
Normal file
BIN
artifacts/test_program_methods/group_pda_spender.bin
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
artifacts/test_program_methods/pda_fund_spend_proxy.bin
Normal file
BIN
artifacts/test_program_methods/pda_fund_spend_proxy.bin
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -48,7 +48,7 @@ impl IndexerCore {
|
||||
.iter()
|
||||
.map(|init_comm_data| {
|
||||
let npk = &init_comm_data.npk;
|
||||
let account_id = nssa::AccountId::from((npk, 0));
|
||||
let account_id = nssa::AccountId::for_regular_private_account(npk, 0);
|
||||
|
||||
let mut acc = init_comm_data.account.clone();
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -59,12 +59,16 @@ impl InitialData {
|
||||
}
|
||||
|
||||
let mut private_charlie_key_chain = KeyChain::new_os_random();
|
||||
let mut private_charlie_account_id =
|
||||
AccountId::from((&private_charlie_key_chain.nullifier_public_key, 0));
|
||||
let mut private_charlie_account_id = AccountId::for_regular_private_account(
|
||||
&private_charlie_key_chain.nullifier_public_key,
|
||||
0,
|
||||
);
|
||||
|
||||
let mut private_david_key_chain = KeyChain::new_os_random();
|
||||
let mut private_david_account_id =
|
||||
AccountId::from((&private_david_key_chain.nullifier_public_key, 0));
|
||||
let mut private_david_account_id = AccountId::for_regular_private_account(
|
||||
&private_david_key_chain.nullifier_public_key,
|
||||
0,
|
||||
);
|
||||
|
||||
// Ensure consistent ordering
|
||||
if private_charlie_account_id > private_david_account_id {
|
||||
|
||||
@ -29,6 +29,7 @@ pub mod test_context_ffi;
|
||||
pub const TIME_TO_WAIT_FOR_BLOCK_SECONDS: u64 = 12;
|
||||
pub const NSSA_PROGRAM_FOR_TEST_DATA_CHANGER: &str = "data_changer.bin";
|
||||
pub const NSSA_PROGRAM_FOR_TEST_NOOP: &str = "noop.bin";
|
||||
pub const NSSA_PROGRAM_FOR_TEST_PDA_FUND_SPEND_PROXY: &str = "pda_fund_spend_proxy.bin";
|
||||
|
||||
const BEDROCK_SERVICE_WITH_OPEN_PORT: &str = "logos-blockchain-node-0";
|
||||
const BEDROCK_SERVICE_PORT: u16 = 18080;
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -605,14 +605,14 @@ async fn shielded_transfers_to_two_identifiers_same_npk() -> Result<()> {
|
||||
.await?;
|
||||
|
||||
// Both accounts must be discovered with the correct balances.
|
||||
let account_id_1 = AccountId::from((&npk, identifier_1));
|
||||
let account_id_1 = AccountId::for_regular_private_account(&npk, identifier_1);
|
||||
let acc_1 = ctx
|
||||
.wallet()
|
||||
.get_account_private(account_id_1)
|
||||
.context("account for identifier 1 not found after sync")?;
|
||||
assert_eq!(acc_1.balance, 100);
|
||||
|
||||
let account_id_2 = AccountId::from((&npk, identifier_2));
|
||||
let account_id_2 = AccountId::for_regular_private_account(&npk, identifier_2);
|
||||
let acc_2 = ctx
|
||||
.wallet()
|
||||
.get_account_private(account_id_2)
|
||||
|
||||
@ -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(),
|
||||
|
||||
309
integration_tests/tests/private_pda.rs
Normal file
309
integration_tests/tests/private_pda.rs
Normal file
@ -0,0 +1,309 @@
|
||||
#![expect(
|
||||
clippy::tests_outside_test_module,
|
||||
reason = "We don't care about these in tests"
|
||||
)]
|
||||
|
||||
use std::{path::PathBuf, time::Duration};
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use integration_tests::{
|
||||
NSSA_PROGRAM_FOR_TEST_PDA_FUND_SPEND_PROXY, TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext,
|
||||
verify_commitment_is_in_state,
|
||||
};
|
||||
use log::info;
|
||||
use nssa::{
|
||||
AccountId, ProgramId, privacy_preserving_transaction::circuit::ProgramWithDependencies,
|
||||
program::Program,
|
||||
};
|
||||
use nssa_core::{NullifierPublicKey, encryption::ViewingPublicKey, program::PdaSeed};
|
||||
use tokio::test;
|
||||
use wallet::{
|
||||
PrivacyPreservingAccount, WalletCore,
|
||||
cli::{Command, account::AccountSubcommand},
|
||||
};
|
||||
|
||||
/// Funds a private PDA via the proxy program with a chained call to `auth_transfer`.
|
||||
///
|
||||
/// A direct call to `auth_transfer` cannot establish the PDA-to-npk binding because it uses
|
||||
/// `Claim::Authorized` rather than `Claim::Pda`. Routing through the proxy provides the binding
|
||||
/// via `pda_seeds` in the chained call to `auth_transfer`.
|
||||
#[expect(
|
||||
clippy::too_many_arguments,
|
||||
reason = "test helper — grouping args would obscure intent"
|
||||
)]
|
||||
async fn fund_private_pda(
|
||||
wallet: &WalletCore,
|
||||
sender: AccountId,
|
||||
pda_account_id: AccountId,
|
||||
npk: NullifierPublicKey,
|
||||
vpk: ViewingPublicKey,
|
||||
identifier: u128,
|
||||
seed: PdaSeed,
|
||||
amount: u128,
|
||||
proxy_program: &ProgramWithDependencies,
|
||||
auth_transfer_id: ProgramId,
|
||||
) -> Result<()> {
|
||||
wallet
|
||||
.send_privacy_preserving_tx(
|
||||
vec![
|
||||
PrivacyPreservingAccount::Public(sender),
|
||||
PrivacyPreservingAccount::PrivatePdaForeign {
|
||||
account_id: pda_account_id,
|
||||
npk,
|
||||
vpk,
|
||||
identifier,
|
||||
},
|
||||
],
|
||||
Program::serialize_instruction((seed, amount, auth_transfer_id, true))
|
||||
.context("failed to serialize pda_fund_spend_proxy fund instruction")?,
|
||||
proxy_program,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Spends from an owned private PDA to a fresh private-foreign recipient.
|
||||
///
|
||||
/// Alice must own the PDA in the wallet (i.e. it must have been synced after a receive).
|
||||
#[expect(
|
||||
clippy::too_many_arguments,
|
||||
reason = "test helper — grouping args would obscure intent"
|
||||
)]
|
||||
async fn spend_private_pda(
|
||||
wallet: &WalletCore,
|
||||
pda_account_id: AccountId,
|
||||
recipient_npk: NullifierPublicKey,
|
||||
recipient_vpk: ViewingPublicKey,
|
||||
seed: PdaSeed,
|
||||
amount: u128,
|
||||
spend_program: &ProgramWithDependencies,
|
||||
auth_transfer_id: nssa::ProgramId,
|
||||
) -> Result<()> {
|
||||
wallet
|
||||
.send_privacy_preserving_tx(
|
||||
vec![
|
||||
PrivacyPreservingAccount::PrivatePdaOwned(pda_account_id),
|
||||
PrivacyPreservingAccount::PrivateForeign {
|
||||
npk: recipient_npk,
|
||||
vpk: recipient_vpk,
|
||||
identifier: 0,
|
||||
},
|
||||
],
|
||||
Program::serialize_instruction((seed, amount, auth_transfer_id, false))
|
||||
.context("failed to serialize pda_fund_spend_proxy instruction")?,
|
||||
spend_program,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Two private transfers go to distinct members of the same PDA family (same seed and npk,
|
||||
/// but identifier=0 and identifier=1). Alice then spends from both PDAs.
|
||||
///
|
||||
/// This exercises the full identifier-diversified private PDA lifecycle:
|
||||
/// receive(id=0), receive(id=1) → sync → spend(id=0), spend(id=1) → sync → assert.
|
||||
#[test]
|
||||
async fn private_pda_family_members_receive_and_spend() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
// ── Build alice's key chain ──────────────────────────────────────────────────────────────────
|
||||
let alice_chain_index = ctx.wallet_mut().create_private_accounts_key(None);
|
||||
let (alice_npk, alice_vpk) = {
|
||||
let node = ctx
|
||||
.wallet()
|
||||
.storage()
|
||||
.user_data
|
||||
.private_key_tree
|
||||
.key_map
|
||||
.get(&alice_chain_index)
|
||||
.context("key node was just inserted")?;
|
||||
let kc = &node.value.0;
|
||||
(kc.nullifier_public_key, kc.viewing_public_key.clone())
|
||||
};
|
||||
|
||||
let proxy = {
|
||||
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../artifacts/test_program_methods")
|
||||
.join(NSSA_PROGRAM_FOR_TEST_PDA_FUND_SPEND_PROXY);
|
||||
Program::new(std::fs::read(&path).with_context(|| format!("reading {path:?}"))?)
|
||||
.context("invalid pda_fund_spend_proxy binary")?
|
||||
};
|
||||
let auth_transfer = Program::authenticated_transfer_program();
|
||||
let proxy_id = proxy.id();
|
||||
let auth_transfer_id = auth_transfer.id();
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
let amount: u128 = 100;
|
||||
|
||||
let spend_program =
|
||||
ProgramWithDependencies::new(proxy, [(auth_transfer_id, auth_transfer)].into());
|
||||
|
||||
let alice_pda_0_id = AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, 0);
|
||||
let alice_pda_1_id = AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, 1);
|
||||
|
||||
// Use two different public senders to avoid nonce conflicts between the back-to-back txs.
|
||||
let senders = ctx.existing_public_accounts();
|
||||
let sender_0 = senders[0];
|
||||
let sender_1 = senders[1];
|
||||
|
||||
// ── Receive ──────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
info!("Sending to alice_pda_0 (identifier=0)");
|
||||
fund_private_pda(
|
||||
ctx.wallet(),
|
||||
sender_0,
|
||||
alice_pda_0_id,
|
||||
alice_npk,
|
||||
alice_vpk.clone(),
|
||||
0,
|
||||
seed,
|
||||
amount,
|
||||
&spend_program,
|
||||
auth_transfer_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
info!("Sending to alice_pda_1 (identifier=1)");
|
||||
fund_private_pda(
|
||||
ctx.wallet(),
|
||||
sender_1,
|
||||
alice_pda_1_id,
|
||||
alice_npk,
|
||||
alice_vpk.clone(),
|
||||
1,
|
||||
seed,
|
||||
amount,
|
||||
&spend_program,
|
||||
auth_transfer_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
info!("Waiting for block");
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
// Sync so alice's wallet discovers and stores both PDAs.
|
||||
wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Account(AccountSubcommand::SyncPrivate {}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Both PDAs must be discoverable and have the correct balance.
|
||||
let pda_0_account = ctx
|
||||
.wallet()
|
||||
.get_account_private(alice_pda_0_id)
|
||||
.context("alice_pda_0 not found after sync")?;
|
||||
assert_eq!(pda_0_account.balance, amount);
|
||||
|
||||
let pda_1_account = ctx
|
||||
.wallet()
|
||||
.get_account_private(alice_pda_1_id)
|
||||
.context("alice_pda_1 not found after sync")?;
|
||||
assert_eq!(pda_1_account.balance, amount);
|
||||
|
||||
// Commitments for both PDAs must be in the sequencer's state.
|
||||
let commitment_0 = ctx
|
||||
.wallet()
|
||||
.get_private_account_commitment(alice_pda_0_id)
|
||||
.context("commitment for alice_pda_0 missing")?;
|
||||
assert!(
|
||||
verify_commitment_is_in_state(commitment_0.clone(), ctx.sequencer_client()).await,
|
||||
"alice_pda_0 commitment not in state after receive"
|
||||
);
|
||||
|
||||
let commitment_1 = ctx
|
||||
.wallet()
|
||||
.get_private_account_commitment(alice_pda_1_id)
|
||||
.context("commitment for alice_pda_1 missing")?;
|
||||
assert!(
|
||||
verify_commitment_is_in_state(commitment_1.clone(), ctx.sequencer_client()).await,
|
||||
"alice_pda_1 commitment not in state after receive"
|
||||
);
|
||||
assert_ne!(
|
||||
commitment_0, commitment_1,
|
||||
"distinct identifiers must yield distinct commitments"
|
||||
);
|
||||
|
||||
// ── Spend ─────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
// Fresh recipients — hardcoded npks not in any wallet.
|
||||
let recipient_npk_0 = NullifierPublicKey([0xAA; 32]);
|
||||
let recipient_vpk_0 = ViewingPublicKey::from_scalar(recipient_npk_0.0);
|
||||
|
||||
let recipient_npk_1 = NullifierPublicKey([0xBB; 32]);
|
||||
let recipient_vpk_1 = ViewingPublicKey::from_scalar(recipient_npk_1.0);
|
||||
|
||||
let amount_spend_0: u128 = 13;
|
||||
let amount_spend_1: u128 = 37;
|
||||
|
||||
info!("Alice spending from alice_pda_0");
|
||||
spend_private_pda(
|
||||
ctx.wallet(),
|
||||
alice_pda_0_id,
|
||||
recipient_npk_0,
|
||||
recipient_vpk_0,
|
||||
seed,
|
||||
amount_spend_0,
|
||||
&spend_program,
|
||||
auth_transfer_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
info!("Alice spending from alice_pda_1");
|
||||
spend_private_pda(
|
||||
ctx.wallet(),
|
||||
alice_pda_1_id,
|
||||
recipient_npk_1,
|
||||
recipient_vpk_1,
|
||||
seed,
|
||||
amount_spend_1,
|
||||
&spend_program,
|
||||
auth_transfer_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
info!("Waiting for block");
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Account(AccountSubcommand::SyncPrivate {}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// After spending, PDAs should have the remaining balance.
|
||||
let pda_0_spent = ctx
|
||||
.wallet()
|
||||
.get_account_private(alice_pda_0_id)
|
||||
.context("alice_pda_0 not found after spend sync")?;
|
||||
assert_eq!(pda_0_spent.balance, amount - amount_spend_0);
|
||||
|
||||
let pda_1_spent = ctx
|
||||
.wallet()
|
||||
.get_account_private(alice_pda_1_id)
|
||||
.context("alice_pda_1 not found after spend sync")?;
|
||||
assert_eq!(pda_1_spent.balance, amount - amount_spend_1);
|
||||
|
||||
// Post-spend commitments must be in state.
|
||||
let post_spend_commitment_0 = ctx
|
||||
.wallet()
|
||||
.get_private_account_commitment(alice_pda_0_id)
|
||||
.context("post-spend commitment for alice_pda_0 missing")?;
|
||||
assert!(
|
||||
verify_commitment_is_in_state(post_spend_commitment_0, ctx.sequencer_client()).await,
|
||||
"alice_pda_0 post-spend commitment not in state"
|
||||
);
|
||||
|
||||
let post_spend_commitment_1 = ctx
|
||||
.wallet()
|
||||
.get_private_account_commitment(alice_pda_1_id)
|
||||
.context("post-spend commitment for alice_pda_1 missing")?;
|
||||
assert!(
|
||||
verify_commitment_is_in_state(post_spend_commitment_1, ctx.sequencer_client()).await,
|
||||
"alice_pda_1 post-spend commitment not in state"
|
||||
);
|
||||
|
||||
info!("Private PDA family member receive-and-spend test passed");
|
||||
Ok(())
|
||||
}
|
||||
231
integration_tests/tests/shared_accounts.rs
Normal file
231
integration_tests/tests/shared_accounts.rs
Normal file
@ -0,0 +1,231 @@
|
||||
#![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,
|
||||
identifier: 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,
|
||||
identifier: 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(())
|
||||
}
|
||||
@ -220,7 +220,7 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction {
|
||||
data: Data::default(),
|
||||
},
|
||||
true,
|
||||
AccountId::from((&sender_npk, 0)),
|
||||
AccountId::for_regular_private_account(&sender_npk, 0),
|
||||
);
|
||||
let recipient_nsk = [2; 32];
|
||||
let recipient_vsk = [99; 32];
|
||||
@ -229,7 +229,7 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction {
|
||||
let recipient_pre = AccountWithMetadata::new(
|
||||
Account::default(),
|
||||
false,
|
||||
AccountId::from((&recipient_npk, 0)),
|
||||
AccountId::for_regular_private_account(&recipient_npk, 0),
|
||||
);
|
||||
|
||||
let eph_holder_from = EphemeralKeyHolder::new(&sender_npk);
|
||||
|
||||
@ -801,7 +801,7 @@ fn test_wallet_ffi_transfer_shielded() -> Result<()> {
|
||||
let (to, to_keys) = unsafe {
|
||||
let mut out_keys = FfiPrivateAccountKeys::default();
|
||||
wallet_ffi_create_private_accounts_key(wallet_ffi_handle, &raw mut out_keys);
|
||||
let account_id = nssa::AccountId::from((&out_keys.npk(), 0_u128));
|
||||
let account_id = nssa::AccountId::for_regular_private_account(&out_keys.npk(), 0_u128);
|
||||
let to: FfiBytes32 = (&account_id).into();
|
||||
(to, out_keys)
|
||||
};
|
||||
@ -935,7 +935,7 @@ fn test_wallet_ffi_transfer_private() -> Result<()> {
|
||||
let (to, to_keys) = unsafe {
|
||||
let mut out_keys = FfiPrivateAccountKeys::default();
|
||||
wallet_ffi_create_private_accounts_key(wallet_ffi_handle, &raw mut out_keys);
|
||||
let account_id = nssa::AccountId::from((&out_keys.npk(), 0_u128));
|
||||
let account_id = nssa::AccountId::for_regular_private_account(&out_keys.npk(), 0_u128);
|
||||
let to: FfiBytes32 = (&account_id).into();
|
||||
(to, out_keys)
|
||||
};
|
||||
|
||||
@ -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,18 +337,18 @@ 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);
|
||||
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([
|
||||
236, 138, 175, 184, 194, 233, 144, 109, 157, 51, 193, 120, 83, 110, 147, 90, 154, 57,
|
||||
148, 236, 12, 92, 135, 38, 253, 79, 88, 143, 161, 175, 46, 144,
|
||||
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);
|
||||
@ -318,10 +366,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);
|
||||
@ -339,7 +387,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)
|
||||
@ -359,7 +407,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());
|
||||
@ -367,10 +415,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(),
|
||||
);
|
||||
}
|
||||
@ -390,7 +438,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)));
|
||||
}
|
||||
@ -405,7 +453,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;
|
||||
@ -424,8 +472,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);
|
||||
}
|
||||
|
||||
@ -453,7 +502,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();
|
||||
@ -478,7 +527,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
|
||||
@ -487,18 +536,66 @@ 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);
|
||||
|
||||
let alice_account_id = AccountId::for_private_pda(&program_id, &pda_seed, &alice_npk);
|
||||
let bob_account_id = AccountId::for_private_pda(&program_id, &pda_seed, &bob_npk);
|
||||
let alice_account_id = AccountId::for_private_pda(&program_id, &pda_seed, &alice_npk, 0);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
use k256::{Scalar, elliptic_curve::PrimeField as _};
|
||||
use nssa_core::{Identifier, NullifierPublicKey, encryption::ViewingPublicKey};
|
||||
use nssa_core::{NullifierPublicKey, PrivateAccountKind, encryption::ViewingPublicKey};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::key_management::{
|
||||
@ -10,7 +10,7 @@ use crate::key_management::{
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct ChildKeysPrivate {
|
||||
pub value: (KeyChain, Vec<(Identifier, nssa::Account)>),
|
||||
pub value: (KeyChain, Vec<(PrivateAccountKind, nssa::Account)>),
|
||||
pub ccc: [u8; 32],
|
||||
/// Can be [`None`] if root.
|
||||
pub cci: Option<u32>,
|
||||
@ -115,9 +115,11 @@ impl KeyTreeNode for ChildKeysPrivate {
|
||||
}
|
||||
|
||||
fn account_ids(&self) -> impl Iterator<Item = nssa::AccountId> {
|
||||
self.value.1.iter().map(|(identifier, _)| {
|
||||
nssa::AccountId::from((&self.value.0.nullifier_public_key, *identifier))
|
||||
})
|
||||
let npk = self.value.0.nullifier_public_key;
|
||||
self.value
|
||||
.1
|
||||
.iter()
|
||||
.map(move |(kind, _)| nssa::AccountId::for_private_account(&npk, kind))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -274,7 +274,10 @@ impl KeyTree<ChildKeysPrivate> {
|
||||
identifier: Identifier,
|
||||
) -> Option<nssa::AccountId> {
|
||||
let node = self.key_map.get(cci)?;
|
||||
let account_id = nssa::AccountId::from((&node.value.0.nullifier_public_key, identifier));
|
||||
let account_id = nssa::AccountId::for_regular_private_account(
|
||||
&node.value.0.nullifier_public_key,
|
||||
identifier,
|
||||
);
|
||||
if self.account_id_map.contains_key(&account_id) {
|
||||
return None;
|
||||
}
|
||||
@ -319,6 +322,7 @@ mod tests {
|
||||
use std::{collections::HashSet, str::FromStr as _};
|
||||
|
||||
use nssa::AccountId;
|
||||
use nssa_core::PrivateAccountKind;
|
||||
|
||||
use super::*;
|
||||
|
||||
@ -532,7 +536,7 @@ mod tests {
|
||||
.get_mut(&ChainIndex::from_str("/1").unwrap())
|
||||
.unwrap();
|
||||
acc.value.1.push((
|
||||
0,
|
||||
PrivateAccountKind::Regular(0),
|
||||
nssa::Account {
|
||||
balance: 2,
|
||||
..nssa::Account::default()
|
||||
@ -544,7 +548,7 @@ mod tests {
|
||||
.get_mut(&ChainIndex::from_str("/2").unwrap())
|
||||
.unwrap();
|
||||
acc.value.1.push((
|
||||
0,
|
||||
PrivateAccountKind::Regular(0),
|
||||
nssa::Account {
|
||||
balance: 3,
|
||||
..nssa::Account::default()
|
||||
@ -556,7 +560,7 @@ mod tests {
|
||||
.get_mut(&ChainIndex::from_str("/0/1").unwrap())
|
||||
.unwrap();
|
||||
acc.value.1.push((
|
||||
0,
|
||||
PrivateAccountKind::Regular(0),
|
||||
nssa::Account {
|
||||
balance: 5,
|
||||
..nssa::Account::default()
|
||||
@ -568,7 +572,7 @@ mod tests {
|
||||
.get_mut(&ChainIndex::from_str("/1/0").unwrap())
|
||||
.unwrap();
|
||||
acc.value.1.push((
|
||||
0,
|
||||
PrivateAccountKind::Regular(0),
|
||||
nssa::Account {
|
||||
balance: 6,
|
||||
..nssa::Account::default()
|
||||
|
||||
@ -3,7 +3,7 @@ use std::collections::BTreeMap;
|
||||
use anyhow::Result;
|
||||
use k256::AffinePoint;
|
||||
use nssa::{Account, AccountId};
|
||||
use nssa_core::Identifier;
|
||||
use nssa_core::{Identifier, PrivateAccountKind};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::key_management::{
|
||||
@ -18,10 +18,25 @@ pub type PublicKey = AffinePoint;
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct UserPrivateAccountData {
|
||||
pub key_chain: KeyChain,
|
||||
pub accounts: Vec<(Identifier, Account)>,
|
||||
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 {
|
||||
@ -65,10 +79,11 @@ impl NSSAUserData {
|
||||
) -> bool {
|
||||
let mut check_res = true;
|
||||
for (account_id, entry) in accounts_keys_map {
|
||||
let any_match = entry.accounts.iter().any(|(identifier, _)| {
|
||||
nssa::AccountId::from((&entry.key_chain.nullifier_public_key, *identifier))
|
||||
== *account_id
|
||||
});
|
||||
let npk = &entry.key_chain.nullifier_public_key;
|
||||
let any_match = entry
|
||||
.accounts
|
||||
.iter()
|
||||
.any(|(kind, _)| nssa::AccountId::for_private_account(npk, kind) == *account_id);
|
||||
if !any_match {
|
||||
println!("No matching entry found for account_id {account_id}");
|
||||
check_res = false;
|
||||
@ -101,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,
|
||||
})
|
||||
}
|
||||
|
||||
@ -169,24 +185,27 @@ impl NSSAUserData {
|
||||
) -> Option<(KeyChain, nssa_core::account::Account, Identifier)> {
|
||||
// Check default accounts
|
||||
if let Some(entry) = self.default_user_private_accounts.get(&account_id) {
|
||||
for (identifier, account) in &entry.accounts {
|
||||
let expected_id =
|
||||
nssa::AccountId::from((&entry.key_chain.nullifier_public_key, *identifier));
|
||||
if expected_id == account_id {
|
||||
return Some((entry.key_chain.clone(), account.clone(), *identifier));
|
||||
}
|
||||
let npk = &entry.key_chain.nullifier_public_key;
|
||||
if let Some((kind, account)) = entry
|
||||
.accounts
|
||||
.iter()
|
||||
.find(|(kind, _)| nssa::AccountId::for_private_account(npk, kind) == account_id)
|
||||
{
|
||||
return Some((entry.key_chain.clone(), account.clone(), kind.identifier()));
|
||||
}
|
||||
return None;
|
||||
}
|
||||
// Check tree
|
||||
if let Some(node) = self.private_key_tree.get_node(account_id) {
|
||||
let key_chain = &node.value.0;
|
||||
for (identifier, account) in &node.value.1 {
|
||||
let expected_id =
|
||||
nssa::AccountId::from((&key_chain.nullifier_public_key, *identifier));
|
||||
if expected_id == account_id {
|
||||
return Some((key_chain.clone(), account.clone(), *identifier));
|
||||
}
|
||||
let npk = &key_chain.nullifier_public_key;
|
||||
if let Some((kind, account)) = node
|
||||
.value
|
||||
.1
|
||||
.iter()
|
||||
.find(|(kind, _)| nssa::AccountId::for_private_account(npk, kind) == account_id)
|
||||
{
|
||||
return Some((key_chain.clone(), account.clone(), kind.identifier()));
|
||||
}
|
||||
}
|
||||
None
|
||||
@ -223,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 {
|
||||
@ -260,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]
|
||||
|
||||
@ -30,8 +30,8 @@ pub enum InputAccountIdentity {
|
||||
Public,
|
||||
/// Init of an authorized standalone private account: no membership proof. The `pre_state`
|
||||
/// must be `Account::default()`. The `account_id` is derived as
|
||||
/// `AccountId::from((&NullifierPublicKey::from(nsk), identifier))` and matched against
|
||||
/// `pre_state.account_id`.
|
||||
/// `AccountId::for_regular_private_account(&NullifierPublicKey::from(nsk), identifier)` and
|
||||
/// matched against `pre_state.account_id`.
|
||||
PrivateAuthorizedInit {
|
||||
ssk: SharedSecretKey,
|
||||
nsk: NullifierSecretKey,
|
||||
@ -53,19 +53,22 @@ pub enum InputAccountIdentity {
|
||||
identifier: Identifier,
|
||||
},
|
||||
/// Init of a private PDA, unauthorized. The npk-to-account_id binding is proven upstream
|
||||
/// via `Claim::Pda(seed)` or a caller's `pda_seeds` match. Identifier is fixed by
|
||||
/// convention to `PRIVATE_PDA_FIXED_IDENTIFIER` and not carried per-input.
|
||||
/// via `Claim::Pda(seed)` or a caller's `pda_seeds` match. The identifier diversifies the
|
||||
/// PDA within the `(program_id, seed, npk)` family: `AccountId::for_private_pda` uses it
|
||||
/// as the 4th input.
|
||||
PrivatePdaInit {
|
||||
npk: NullifierPublicKey,
|
||||
ssk: SharedSecretKey,
|
||||
identifier: Identifier,
|
||||
},
|
||||
/// Update of an existing private PDA, authorized, with membership proof. `npk` is derived
|
||||
/// from `nsk`. Authorization is established upstream by a caller `pda_seeds` match or a
|
||||
/// previously-seen authorization in a chained call. Identifier is fixed.
|
||||
/// previously-seen authorization in a chained call.
|
||||
PrivatePdaUpdate {
|
||||
ssk: SharedSecretKey,
|
||||
nsk: NullifierSecretKey,
|
||||
membership_proof: MembershipProof,
|
||||
identifier: Identifier,
|
||||
},
|
||||
}
|
||||
|
||||
@ -83,13 +86,17 @@ impl InputAccountIdentity {
|
||||
)
|
||||
}
|
||||
|
||||
/// For private PDA variants, return the nullifier public key. `Init` carries it directly;
|
||||
/// `Update` derives it from `nsk`. For non-PDA variants returns `None`.
|
||||
/// For private PDA variants, return the `(npk, identifier)` pair. `Init` carries both
|
||||
/// directly; `Update` derives `npk` from `nsk`. For non-PDA variants returns `None`.
|
||||
#[must_use]
|
||||
pub fn npk_if_private_pda(&self) -> Option<NullifierPublicKey> {
|
||||
pub fn npk_if_private_pda(&self) -> Option<(NullifierPublicKey, Identifier)> {
|
||||
match self {
|
||||
Self::PrivatePdaInit { npk, .. } => Some(*npk),
|
||||
Self::PrivatePdaUpdate { nsk, .. } => Some(NullifierPublicKey::from(nsk)),
|
||||
Self::PrivatePdaInit {
|
||||
npk, identifier, ..
|
||||
} => Some((*npk, *identifier)),
|
||||
Self::PrivatePdaUpdate {
|
||||
nsk, identifier, ..
|
||||
} => Some((NullifierPublicKey::from(nsk), *identifier)),
|
||||
Self::Public
|
||||
| Self::PrivateAuthorizedInit { .. }
|
||||
| Self::PrivateAuthorizedUpdate { .. }
|
||||
|
||||
@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
|
||||
#[cfg(feature = "host")]
|
||||
pub use shared_key_derivation::{EphemeralPublicKey, EphemeralSecretKey, ViewingPublicKey};
|
||||
|
||||
use crate::{Commitment, Identifier, account::Account};
|
||||
use crate::{Commitment, account::Account, program::PrivateAccountKind};
|
||||
#[cfg(feature = "host")]
|
||||
pub mod shared_key_derivation;
|
||||
|
||||
@ -40,13 +40,14 @@ impl EncryptionScheme {
|
||||
#[must_use]
|
||||
pub fn encrypt(
|
||||
account: &Account,
|
||||
identifier: Identifier,
|
||||
kind: &PrivateAccountKind,
|
||||
shared_secret: &SharedSecretKey,
|
||||
commitment: &Commitment,
|
||||
output_index: u32,
|
||||
) -> Ciphertext {
|
||||
// Plaintext: identifier (16 bytes, little-endian) || account bytes
|
||||
let mut buffer = identifier.to_le_bytes().to_vec();
|
||||
// Plaintext: PrivateAccountKind::HEADER_LEN bytes header || account bytes.
|
||||
// Both variants produce the same header length — see PrivateAccountKind::to_header_bytes.
|
||||
let mut buffer = kind.to_header_bytes().to_vec();
|
||||
buffer.extend_from_slice(&account.to_bytes());
|
||||
Self::symmetric_transform(&mut buffer, shared_secret, commitment, output_index);
|
||||
Ciphertext(buffer)
|
||||
@ -89,17 +90,19 @@ impl EncryptionScheme {
|
||||
shared_secret: &SharedSecretKey,
|
||||
commitment: &Commitment,
|
||||
output_index: u32,
|
||||
) -> Option<(Identifier, Account)> {
|
||||
) -> Option<(PrivateAccountKind, Account)> {
|
||||
use std::io::Cursor;
|
||||
let mut buffer = ciphertext.0.clone();
|
||||
Self::symmetric_transform(&mut buffer, shared_secret, commitment, output_index);
|
||||
|
||||
if buffer.len() < 16 {
|
||||
if buffer.len() < PrivateAccountKind::HEADER_LEN {
|
||||
return None;
|
||||
}
|
||||
let identifier = Identifier::from_le_bytes(buffer[..16].try_into().unwrap());
|
||||
let header: &[u8; PrivateAccountKind::HEADER_LEN] =
|
||||
buffer[..PrivateAccountKind::HEADER_LEN].try_into().unwrap();
|
||||
let kind = PrivateAccountKind::from_header_bytes(header)?;
|
||||
|
||||
let mut cursor = Cursor::new(&buffer[16..]);
|
||||
let mut cursor = Cursor::new(&buffer[PrivateAccountKind::HEADER_LEN..]);
|
||||
Account::from_cursor(&mut cursor)
|
||||
.inspect_err(|err| {
|
||||
println!(
|
||||
@ -112,6 +115,43 @@ impl EncryptionScheme {
|
||||
);
|
||||
})
|
||||
.ok()
|
||||
.map(|account| (identifier, account))
|
||||
.map(|account| (kind, account))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
account::{Account, AccountId},
|
||||
program::PdaSeed,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn encrypt_same_length_for_account_and_pda() {
|
||||
let account = Account::default();
|
||||
let secret = SharedSecretKey([0_u8; 32]);
|
||||
let commitment = crate::Commitment::new(&AccountId::new([0_u8; 32]), &Account::default());
|
||||
|
||||
let account_ct = EncryptionScheme::encrypt(
|
||||
&account,
|
||||
&PrivateAccountKind::Regular(42),
|
||||
&secret,
|
||||
&commitment,
|
||||
0,
|
||||
);
|
||||
let pda_ct = EncryptionScheme::encrypt(
|
||||
&account,
|
||||
&PrivateAccountKind::Pda {
|
||||
program_id: [1_u32; 8],
|
||||
seed: PdaSeed::new([2_u8; 32]),
|
||||
identifier: 42,
|
||||
},
|
||||
&secret,
|
||||
&commitment,
|
||||
0,
|
||||
);
|
||||
|
||||
assert_eq!(account_ct.0.len(), pda_ct.0.len());
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@ pub use commitment::{
|
||||
};
|
||||
pub use encryption::{EncryptionScheme, SharedSecretKey};
|
||||
pub use nullifier::{Identifier, Nullifier, NullifierPublicKey, NullifierSecretKey};
|
||||
pub use program::PrivateAccountKind;
|
||||
|
||||
pub mod account;
|
||||
mod circuit_io;
|
||||
|
||||
@ -12,10 +12,11 @@ pub type Identifier = u128;
|
||||
#[cfg_attr(any(feature = "host", test), derive(Hash))]
|
||||
pub struct NullifierPublicKey(pub [u8; 32]);
|
||||
|
||||
impl From<(&NullifierPublicKey, Identifier)> for AccountId {
|
||||
fn from(value: (&NullifierPublicKey, Identifier)) -> Self {
|
||||
let (npk, identifier) = value;
|
||||
|
||||
impl AccountId {
|
||||
/// Derives an [`AccountId`] for a regular (non-PDA) private account from the nullifier public
|
||||
/// key and identifier.
|
||||
#[must_use]
|
||||
pub fn for_regular_private_account(npk: &NullifierPublicKey, identifier: Identifier) -> Self {
|
||||
// 32 bytes prefix || 32 bytes npk || 16 bytes identifier
|
||||
let mut bytes = [0; 80];
|
||||
bytes[0..32].copy_from_slice(PRIVATE_ACCOUNT_ID_PREFIX);
|
||||
@ -31,6 +32,12 @@ impl From<(&NullifierPublicKey, Identifier)> for AccountId {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(&NullifierPublicKey, Identifier)> for AccountId {
|
||||
fn from((npk, identifier): (&NullifierPublicKey, Identifier)) -> Self {
|
||||
Self::for_regular_private_account(npk, identifier)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for NullifierPublicKey {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.0.as_slice()
|
||||
@ -155,7 +162,7 @@ mod tests {
|
||||
253, 105, 164, 89, 84, 40, 191, 182, 119, 64, 255, 67, 142,
|
||||
]);
|
||||
|
||||
let account_id = AccountId::from((&npk, 0));
|
||||
let account_id = AccountId::for_regular_private_account(&npk, 0);
|
||||
|
||||
assert_eq!(account_id, expected_account_id);
|
||||
}
|
||||
@ -172,7 +179,7 @@ mod tests {
|
||||
56, 247, 99, 121, 165, 182, 234, 255, 19, 127, 191, 72,
|
||||
]);
|
||||
|
||||
let account_id = AccountId::from((&npk, 1));
|
||||
let account_id = AccountId::for_regular_private_account(&npk, 1);
|
||||
|
||||
assert_eq!(account_id, expected_account_id);
|
||||
}
|
||||
@ -190,7 +197,7 @@ mod tests {
|
||||
19, 245, 25, 214, 162, 209, 135, 252, 82, 27, 2, 174, 196,
|
||||
]);
|
||||
|
||||
let account_id = AccountId::from((&npk, identifier));
|
||||
let account_id = AccountId::for_regular_private_account(&npk, identifier);
|
||||
|
||||
assert_eq!(account_id, expected_account_id);
|
||||
}
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[cfg(any(feature = "host", test))]
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use risc0_zkvm::{DeserializeOwned, guest::env, serde::Deserializer};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
BlockId, NullifierPublicKey, Timestamp,
|
||||
BlockId, Identifier, NullifierPublicKey, Timestamp,
|
||||
account::{Account, AccountId, AccountWithMetadata},
|
||||
};
|
||||
|
||||
@ -27,7 +26,18 @@ pub struct ProgramInput<T> {
|
||||
/// Each program can derive up to `2^256` unique account IDs by choosing different
|
||||
/// seeds. PDAs allow programs to control namespaced account identifiers without
|
||||
/// collisions between programs.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)]
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
Copy,
|
||||
Eq,
|
||||
PartialEq,
|
||||
Hash,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
BorshSerialize,
|
||||
BorshDeserialize,
|
||||
)]
|
||||
pub struct PdaSeed([u8; 32]);
|
||||
|
||||
impl PdaSeed {
|
||||
@ -35,6 +45,11 @@ impl PdaSeed {
|
||||
pub const fn new(value: [u8; 32]) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn as_bytes(&self) -> &[u8; 32] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for PdaSeed {
|
||||
@ -43,6 +58,55 @@ impl AsRef<[u8]> for PdaSeed {
|
||||
}
|
||||
}
|
||||
|
||||
/// Discriminates the type of private account a ciphertext belongs to, carrying the data needed
|
||||
/// to reconstruct the account's [`AccountId`] on the receiver side.
|
||||
///
|
||||
/// [`AccountId`]: crate::account::AccountId
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
|
||||
pub enum PrivateAccountKind {
|
||||
Regular(Identifier),
|
||||
Pda {
|
||||
program_id: ProgramId,
|
||||
seed: PdaSeed,
|
||||
identifier: Identifier,
|
||||
},
|
||||
}
|
||||
|
||||
impl PrivateAccountKind {
|
||||
/// Borsh layout (all integers little-endian, variant index is u8):
|
||||
///
|
||||
/// ```text
|
||||
/// Regular(ident): 0x00 || ident (16 LE) || [0u8; 64]
|
||||
/// Pda { program_id, seed, ident }: 0x01 || program_id (32) || seed (32) || ident (16 LE)
|
||||
/// ```
|
||||
///
|
||||
/// Both variants are zero-padded to the same length so all ciphertexts are the same size,
|
||||
/// preventing observers from distinguishing `Regular` from `Pda` via ciphertext length.
|
||||
/// `HEADER_LEN` equals the borsh size of the largest variant (`Pda`): 1 + 32 + 32 + 16 = 81.
|
||||
pub const HEADER_LEN: usize = 81;
|
||||
|
||||
#[must_use]
|
||||
pub const fn identifier(&self) -> Identifier {
|
||||
match self {
|
||||
Self::Regular(identifier) | Self::Pda { identifier, .. } => *identifier,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn to_header_bytes(&self) -> [u8; Self::HEADER_LEN] {
|
||||
let mut bytes = [0_u8; Self::HEADER_LEN];
|
||||
let serialized = borsh::to_vec(self).expect("borsh serialization is infallible");
|
||||
bytes[..serialized.len()].copy_from_slice(&serialized);
|
||||
bytes
|
||||
}
|
||||
|
||||
#[cfg(feature = "host")]
|
||||
#[must_use]
|
||||
pub fn from_header_bytes(bytes: &[u8; Self::HEADER_LEN]) -> Option<Self> {
|
||||
BorshDeserialize::deserialize(&mut bytes.as_ref()).ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl AccountId {
|
||||
/// Derives an [`AccountId`] for a public PDA from the program ID and seed.
|
||||
#[must_use]
|
||||
@ -65,27 +129,31 @@ impl AccountId {
|
||||
)
|
||||
}
|
||||
|
||||
/// Derives an [`AccountId`] for a private PDA from the program ID, seed, and nullifier
|
||||
/// public key.
|
||||
/// Derives an [`AccountId`] for a private PDA from the program ID, seed, nullifier public
|
||||
/// key, and identifier.
|
||||
///
|
||||
/// Unlike public PDAs ([`AccountId::for_public_pda`]), this includes the `npk` in the
|
||||
/// derivation, making the address unique per group of controllers sharing viewing keys.
|
||||
/// The `identifier` further diversifies the address, so a single `(program_id, seed, npk)`
|
||||
/// tuple controls a family of 2^128 addresses.
|
||||
#[must_use]
|
||||
pub fn for_private_pda(
|
||||
program_id: &ProgramId,
|
||||
seed: &PdaSeed,
|
||||
npk: &NullifierPublicKey,
|
||||
identifier: Identifier,
|
||||
) -> Self {
|
||||
use risc0_zkvm::sha::{Impl, Sha256 as _};
|
||||
const PRIVATE_PDA_PREFIX: &[u8; 32] = b"/LEE/v0.3/AccountId/PrivatePDA/\x00";
|
||||
|
||||
let mut bytes = [0_u8; 128];
|
||||
let mut bytes = [0_u8; 144];
|
||||
bytes[0..32].copy_from_slice(PRIVATE_PDA_PREFIX);
|
||||
let program_id_bytes: &[u8] =
|
||||
bytemuck::try_cast_slice(program_id).expect("ProgramId should be castable to &[u8]");
|
||||
bytes[32..64].copy_from_slice(program_id_bytes);
|
||||
bytes[64..96].copy_from_slice(&seed.0);
|
||||
bytes[96..128].copy_from_slice(&npk.to_byte_array());
|
||||
bytes[128..144].copy_from_slice(&identifier.to_le_bytes());
|
||||
Self::new(
|
||||
Impl::hash_bytes(&bytes)
|
||||
.as_bytes()
|
||||
@ -93,6 +161,21 @@ impl AccountId {
|
||||
.expect("Hash output must be exactly 32 bytes long"),
|
||||
)
|
||||
}
|
||||
|
||||
/// Derives the [`AccountId`] for a private account from the nullifier public key and kind.
|
||||
#[must_use]
|
||||
pub fn for_private_account(npk: &NullifierPublicKey, kind: &PrivateAccountKind) -> Self {
|
||||
match kind {
|
||||
PrivateAccountKind::Regular(identifier) => {
|
||||
Self::for_regular_private_account(npk, *identifier)
|
||||
}
|
||||
PrivateAccountKind::Pda {
|
||||
program_id,
|
||||
seed,
|
||||
identifier,
|
||||
} => Self::for_private_pda(program_id, seed, npk, *identifier),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||
@ -851,19 +934,20 @@ mod tests {
|
||||
// ---- AccountId::for_private_pda tests ----
|
||||
|
||||
/// Pins `AccountId::for_private_pda` against a hardcoded expected output for a specific
|
||||
/// `(program_id, seed, npk)` triple. Any change to `PRIVATE_PDA_PREFIX`, byte ordering,
|
||||
/// or the underlying hash breaks this test.
|
||||
/// `(program_id, seed, npk, identifier)` tuple. Any change to `PRIVATE_PDA_PREFIX`, byte
|
||||
/// ordering, or the underlying hash breaks this test.
|
||||
#[test]
|
||||
fn for_private_pda_matches_pinned_value() {
|
||||
let program_id: ProgramId = [1; 8];
|
||||
let seed = PdaSeed::new([2; 32]);
|
||||
let npk = NullifierPublicKey([3; 32]);
|
||||
let identifier: Identifier = u128::MAX;
|
||||
let expected = AccountId::new([
|
||||
132, 198, 103, 173, 244, 211, 188, 217, 249, 99, 126, 205, 152, 120, 192, 47, 13, 53,
|
||||
133, 3, 17, 69, 92, 243, 140, 94, 182, 211, 218, 75, 215, 45,
|
||||
59, 239, 182, 97, 14, 220, 96, 115, 238, 133, 143, 33, 234, 82, 237, 255, 148, 110, 54,
|
||||
124, 98, 159, 245, 101, 146, 182, 150, 54, 37, 62, 25, 17,
|
||||
]);
|
||||
assert_eq!(
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk),
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk, identifier),
|
||||
expected
|
||||
);
|
||||
}
|
||||
@ -876,8 +960,8 @@ mod tests {
|
||||
let npk_a = NullifierPublicKey([3; 32]);
|
||||
let npk_b = NullifierPublicKey([4; 32]);
|
||||
assert_ne!(
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk_a),
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk_b),
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk_a, u128::MAX),
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk_b, u128::MAX),
|
||||
);
|
||||
}
|
||||
|
||||
@ -889,8 +973,8 @@ mod tests {
|
||||
let seed_b = PdaSeed::new([5; 32]);
|
||||
let npk = NullifierPublicKey([3; 32]);
|
||||
assert_ne!(
|
||||
AccountId::for_private_pda(&program_id, &seed_a, &npk),
|
||||
AccountId::for_private_pda(&program_id, &seed_b, &npk),
|
||||
AccountId::for_private_pda(&program_id, &seed_a, &npk, u128::MAX),
|
||||
AccountId::for_private_pda(&program_id, &seed_b, &npk, u128::MAX),
|
||||
);
|
||||
}
|
||||
|
||||
@ -902,8 +986,25 @@ mod tests {
|
||||
let seed = PdaSeed::new([2; 32]);
|
||||
let npk = NullifierPublicKey([3; 32]);
|
||||
assert_ne!(
|
||||
AccountId::for_private_pda(&program_id_a, &seed, &npk),
|
||||
AccountId::for_private_pda(&program_id_b, &seed, &npk),
|
||||
AccountId::for_private_pda(&program_id_a, &seed, &npk, u128::MAX),
|
||||
AccountId::for_private_pda(&program_id_b, &seed, &npk, u128::MAX),
|
||||
);
|
||||
}
|
||||
|
||||
/// Different identifiers produce different addresses for the same `(program_id, seed, npk)`,
|
||||
/// confirming that each `(program_id, seed, npk)` tuple controls a family of 2^128 addresses.
|
||||
#[test]
|
||||
fn for_private_pda_differs_for_different_identifier() {
|
||||
let program_id: ProgramId = [1; 8];
|
||||
let seed = PdaSeed::new([2; 32]);
|
||||
let npk = NullifierPublicKey([3; 32]);
|
||||
assert_ne!(
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk, 0),
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk, 1),
|
||||
);
|
||||
assert_ne!(
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk, 0),
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk, u128::MAX),
|
||||
);
|
||||
}
|
||||
|
||||
@ -914,14 +1015,62 @@ mod tests {
|
||||
let program_id: ProgramId = [1; 8];
|
||||
let seed = PdaSeed::new([2; 32]);
|
||||
let npk = NullifierPublicKey([3; 32]);
|
||||
let private_id = AccountId::for_private_pda(&program_id, &seed, &npk);
|
||||
let private_id = AccountId::for_private_pda(&program_id, &seed, &npk, u128::MAX);
|
||||
let public_id = AccountId::for_public_pda(&program_id, &seed);
|
||||
assert_ne!(private_id, public_id);
|
||||
}
|
||||
|
||||
// ---- compute_public_authorized_pdas tests ----
|
||||
#[cfg(feature = "host")]
|
||||
#[test]
|
||||
fn private_account_kind_header_round_trips() {
|
||||
let regular = PrivateAccountKind::Regular(42);
|
||||
let pda = PrivateAccountKind::Pda {
|
||||
program_id: [1_u32; 8],
|
||||
seed: PdaSeed::new([2_u8; 32]),
|
||||
identifier: u128::MAX,
|
||||
};
|
||||
assert_eq!(
|
||||
PrivateAccountKind::from_header_bytes(®ular.to_header_bytes()),
|
||||
Some(regular)
|
||||
);
|
||||
assert_eq!(
|
||||
PrivateAccountKind::from_header_bytes(&pda.to_header_bytes()),
|
||||
Some(pda)
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "host")]
|
||||
#[test]
|
||||
fn private_account_kind_unknown_discriminant_returns_none() {
|
||||
let mut bytes = [0_u8; PrivateAccountKind::HEADER_LEN];
|
||||
bytes[0] = 0xFF;
|
||||
assert_eq!(PrivateAccountKind::from_header_bytes(&bytes), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_private_account_dispatches_correctly() {
|
||||
let program_id: ProgramId = [1; 8];
|
||||
let seed = PdaSeed::new([2; 32]);
|
||||
let npk = NullifierPublicKey([3; 32]);
|
||||
let identifier: Identifier = 77;
|
||||
|
||||
assert_eq!(
|
||||
AccountId::for_private_account(&npk, &PrivateAccountKind::Regular(identifier)),
|
||||
AccountId::for_regular_private_account(&npk, identifier),
|
||||
);
|
||||
assert_eq!(
|
||||
AccountId::for_private_account(
|
||||
&npk,
|
||||
&PrivateAccountKind::Pda {
|
||||
program_id,
|
||||
seed,
|
||||
identifier
|
||||
}
|
||||
),
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk, identifier),
|
||||
);
|
||||
}
|
||||
|
||||
/// `compute_public_authorized_pdas` returns the public PDA addresses for the caller's seeds.
|
||||
#[test]
|
||||
fn compute_public_authorized_pdas_with_seeds() {
|
||||
let caller: ProgramId = [1; 8];
|
||||
|
||||
@ -176,9 +176,10 @@ mod tests {
|
||||
#![expect(clippy::shadow_unrelated, reason = "We don't care about it in tests")]
|
||||
|
||||
use nssa_core::{
|
||||
Commitment, DUMMY_COMMITMENT_HASH, EncryptionScheme, Nullifier, SharedSecretKey,
|
||||
Commitment, DUMMY_COMMITMENT_HASH, EncryptionScheme, Nullifier,
|
||||
PrivacyPreservingCircuitOutput, SharedSecretKey,
|
||||
account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data},
|
||||
program::PdaSeed,
|
||||
program::{PdaSeed, PrivateAccountKind},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
@ -192,6 +193,21 @@ mod tests {
|
||||
},
|
||||
};
|
||||
|
||||
fn decrypt_kind(
|
||||
output: &PrivacyPreservingCircuitOutput,
|
||||
ssk: &SharedSecretKey,
|
||||
idx: usize,
|
||||
) -> PrivateAccountKind {
|
||||
let (kind, _) = EncryptionScheme::decrypt(
|
||||
&output.ciphertexts[idx],
|
||||
ssk,
|
||||
&output.new_commitments[idx],
|
||||
u32::try_from(idx).expect("idx fits in u32"),
|
||||
)
|
||||
.unwrap();
|
||||
kind
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prove_privacy_preserving_execution_circuit_public_and_private_pre_accounts() {
|
||||
let recipient_keys = test_private_account_keys_1();
|
||||
@ -206,7 +222,7 @@ mod tests {
|
||||
AccountId::new([0; 32]),
|
||||
);
|
||||
|
||||
let recipient_account_id = AccountId::from((&recipient_keys.npk(), 0));
|
||||
let recipient_account_id = AccountId::for_regular_private_account(&recipient_keys.npk(), 0);
|
||||
let recipient = AccountWithMetadata::new(Account::default(), false, recipient_account_id);
|
||||
|
||||
let balance_to_move: u128 = 37;
|
||||
@ -280,12 +296,12 @@ mod tests {
|
||||
data: Data::default(),
|
||||
},
|
||||
true,
|
||||
AccountId::from((&sender_keys.npk(), 0)),
|
||||
AccountId::for_regular_private_account(&sender_keys.npk(), 0),
|
||||
);
|
||||
let sender_account_id = AccountId::from((&sender_keys.npk(), 0));
|
||||
let sender_account_id = AccountId::for_regular_private_account(&sender_keys.npk(), 0);
|
||||
let commitment_sender = Commitment::new(&sender_account_id, &sender_pre.account);
|
||||
|
||||
let recipient_account_id = AccountId::from((&recipient_keys.npk(), 0));
|
||||
let recipient_account_id = AccountId::for_regular_private_account(&recipient_keys.npk(), 0);
|
||||
let recipient = AccountWithMetadata::new(Account::default(), false, recipient_account_id);
|
||||
let balance_to_move: u128 = 37;
|
||||
|
||||
@ -381,7 +397,7 @@ mod tests {
|
||||
let pre = AccountWithMetadata::new(
|
||||
Account::default(),
|
||||
false,
|
||||
AccountId::from((&account_keys.npk(), 0)),
|
||||
AccountId::for_regular_private_account(&account_keys.npk(), 0),
|
||||
);
|
||||
|
||||
let validity_window_chain_caller = Program::validity_window_chain_caller();
|
||||
@ -418,105 +434,423 @@ mod tests {
|
||||
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
|
||||
}
|
||||
|
||||
/// Group PDA deposit: creates a new PDA and transfers balance from the
|
||||
/// counterparty. Both accounts owned by `private_pda_spender`.
|
||||
/// A private PDA claimed with a non-default identifier produces a ciphertext that decrypts
|
||||
/// to `PrivateAccountKind::Pda` carrying the correct `(program_id, seed, identifier)`.
|
||||
#[test]
|
||||
fn group_pda_deposit() {
|
||||
let program = Program::private_pda_spender();
|
||||
let noop = Program::noop();
|
||||
fn private_pda_claim_with_custom_identifier_encrypts_correct_kind() {
|
||||
let program = Program::pda_claimer();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
let identifier: u128 = 99;
|
||||
let shared_secret = SharedSecretKey::new(&[55; 32], &keys.vpk());
|
||||
|
||||
let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, identifier);
|
||||
let pre_state = AccountWithMetadata::new(Account::default(), false, account_id);
|
||||
|
||||
let (output, _proof) = execute_and_prove(
|
||||
vec![pre_state],
|
||||
Program::serialize_instruction(seed).unwrap(),
|
||||
vec![InputAccountIdentity::PrivatePdaInit {
|
||||
npk,
|
||||
ssk: shared_secret,
|
||||
identifier,
|
||||
}],
|
||||
&program.clone().into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
decrypt_kind(&output, &shared_secret, 0),
|
||||
PrivateAccountKind::Pda {
|
||||
program_id: program.id(),
|
||||
seed,
|
||||
identifier
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 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 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]);
|
||||
let shared_secret_pda = SharedSecretKey::new(&[55; 32], &keys.vpk());
|
||||
|
||||
// PDA (new, mask 3)
|
||||
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk);
|
||||
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();
|
||||
let balance_to_move: u128 = 100;
|
||||
let instruction = Program::serialize_instruction(balance_to_move).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],
|
||||
vec![sender, recipient],
|
||||
instruction,
|
||||
vec![
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
npk,
|
||||
ssk: shared_secret_pda,
|
||||
},
|
||||
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 deposit should succeed");
|
||||
// Only PDA (mask 3) produces a commitment; sender (mask 0) is public.
|
||||
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);
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// `PrivateAuthorizedInit` with a non-default identifier produces a ciphertext that decrypts
|
||||
/// to `PrivateAccountKind::Regular` carrying the correct identifier.
|
||||
#[test]
|
||||
fn group_pda_spend_binding() {
|
||||
let program = Program::private_pda_spender();
|
||||
let noop = Program::noop();
|
||||
fn private_authorized_init_encrypts_regular_kind_with_identifier() {
|
||||
let program = 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());
|
||||
let identifier: u128 = 99;
|
||||
let ssk = SharedSecretKey::new(&[55; 32], &keys.vpk());
|
||||
let account_id = AccountId::for_regular_private_account(&keys.npk(), identifier);
|
||||
let pre = AccountWithMetadata::new(Account::default(), true, account_id);
|
||||
|
||||
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk);
|
||||
let pda_pre = AccountWithMetadata::new(Account::default(), false, pda_id);
|
||||
let (output, _) = execute_and_prove(
|
||||
vec![pre],
|
||||
Program::serialize_instruction(0_u128).unwrap(),
|
||||
vec![InputAccountIdentity::PrivateAuthorizedInit {
|
||||
ssk,
|
||||
nsk: keys.nsk,
|
||||
identifier,
|
||||
}],
|
||||
&program.into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let bob_id = AccountId::new([88; 32]);
|
||||
let bob_pre = AccountWithMetadata::new(
|
||||
assert_eq!(
|
||||
decrypt_kind(&output, &ssk, 0),
|
||||
PrivateAccountKind::Regular(identifier)
|
||||
);
|
||||
}
|
||||
|
||||
/// `PrivateUnauthorized` with a non-default identifier produces a ciphertext that decrypts
|
||||
/// to `PrivateAccountKind::Regular` carrying the correct identifier.
|
||||
#[test]
|
||||
fn private_unauthorized_init_encrypts_regular_kind_with_identifier() {
|
||||
let program = Program::authenticated_transfer_program();
|
||||
let keys = test_private_account_keys_1();
|
||||
let identifier: u128 = 99;
|
||||
let ssk = SharedSecretKey::new(&[55; 32], &keys.vpk());
|
||||
|
||||
let sender = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: program.id(),
|
||||
balance: 10000,
|
||||
balance: 1,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
bob_id,
|
||||
AccountId::new([0; 32]),
|
||||
);
|
||||
let recipient_id = AccountId::for_regular_private_account(&keys.npk(), identifier);
|
||||
let recipient = AccountWithMetadata::new(Account::default(), false, recipient_id);
|
||||
|
||||
let (output, _) = execute_and_prove(
|
||||
vec![sender, recipient],
|
||||
Program::serialize_instruction(1_u128).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivateUnauthorized {
|
||||
npk: keys.npk(),
|
||||
ssk,
|
||||
identifier,
|
||||
},
|
||||
],
|
||||
&program.into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
decrypt_kind(&output, &ssk, 0),
|
||||
PrivateAccountKind::Regular(identifier)
|
||||
);
|
||||
}
|
||||
|
||||
/// `PrivateAuthorizedUpdate` with a non-default identifier produces a ciphertext that decrypts
|
||||
/// to `PrivateAccountKind::Regular` carrying the correct identifier.
|
||||
#[test]
|
||||
fn private_authorized_update_encrypts_regular_kind_with_identifier() {
|
||||
let program = Program::authenticated_transfer_program();
|
||||
let keys = test_private_account_keys_1();
|
||||
let identifier: u128 = 99;
|
||||
let ssk = SharedSecretKey::new(&[55; 32], &keys.vpk());
|
||||
let account_id = AccountId::for_regular_private_account(&keys.npk(), identifier);
|
||||
let account = Account {
|
||||
program_owner: program.id(),
|
||||
balance: 1,
|
||||
..Account::default()
|
||||
};
|
||||
let commitment = Commitment::new(&account_id, &account);
|
||||
let mut commitment_set = CommitmentSet::with_capacity(1);
|
||||
commitment_set.extend(std::slice::from_ref(&commitment));
|
||||
|
||||
let sender = AccountWithMetadata::new(account, true, account_id);
|
||||
let recipient = AccountWithMetadata::new(Account::default(), true, AccountId::new([0; 32]));
|
||||
|
||||
let (output, _) = execute_and_prove(
|
||||
vec![sender, recipient],
|
||||
Program::serialize_instruction(1_u128).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
ssk,
|
||||
nsk: keys.nsk,
|
||||
membership_proof: commitment_set.get_proof_for(&commitment).unwrap(),
|
||||
identifier,
|
||||
},
|
||||
InputAccountIdentity::Public,
|
||||
],
|
||||
&program.into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
decrypt_kind(&output, &ssk, 0),
|
||||
PrivateAccountKind::Regular(identifier)
|
||||
);
|
||||
}
|
||||
|
||||
/// `PrivatePdaUpdate` with a non-default identifier produces a ciphertext that decrypts
|
||||
/// to `PrivateAccountKind::Pda` carrying the correct `(program_id, seed, identifier)`.
|
||||
#[test]
|
||||
fn private_pda_update_encrypts_pda_kind_with_identifier() {
|
||||
let program = Program::pda_fund_spend_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 identifier: u128 = 99;
|
||||
let ssk = SharedSecretKey::new(&[55; 32], &keys.vpk());
|
||||
|
||||
let auth_transfer_id = auth_transfer.id();
|
||||
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, identifier);
|
||||
let pda_account = Account {
|
||||
program_owner: auth_transfer_id,
|
||||
balance: 1,
|
||||
..Account::default()
|
||||
};
|
||||
let pda_commitment = Commitment::new(&pda_id, &pda_account);
|
||||
let mut commitment_set = CommitmentSet::with_capacity(1);
|
||||
commitment_set.extend(std::slice::from_ref(&pda_commitment));
|
||||
|
||||
let pda_pre = AccountWithMetadata::new(pda_account, true, pda_id);
|
||||
let recipient_pre =
|
||||
AccountWithMetadata::new(Account::default(), true, AccountId::new([0; 32]));
|
||||
|
||||
let program_with_deps = ProgramWithDependencies::new(
|
||||
program.clone(),
|
||||
[(auth_transfer_id, auth_transfer)].into(),
|
||||
);
|
||||
|
||||
let noop_id = noop.id();
|
||||
let program_with_deps = ProgramWithDependencies::new(program, [(noop_id, noop)].into());
|
||||
let (output, _) = execute_and_prove(
|
||||
vec![pda_pre, recipient_pre],
|
||||
Program::serialize_instruction((seed, 1_u128, auth_transfer_id, false)).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivatePdaUpdate {
|
||||
ssk,
|
||||
nsk: keys.nsk,
|
||||
membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(),
|
||||
identifier,
|
||||
},
|
||||
InputAccountIdentity::Public,
|
||||
],
|
||||
&program_with_deps,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let instruction = Program::serialize_instruction((seed, noop_id, 0_u128, false)).unwrap();
|
||||
assert_eq!(
|
||||
decrypt_kind(&output, &ssk, 0),
|
||||
PrivateAccountKind::Pda {
|
||||
program_id: program.id(),
|
||||
seed,
|
||||
identifier
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn private_pda_init_identifier_mismatch_fails() {
|
||||
let program = Program::pda_claimer();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
let shared_secret = SharedSecretKey::new(&[55; 32], &keys.vpk());
|
||||
|
||||
let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 5);
|
||||
let pre_state = AccountWithMetadata::new(Account::default(), false, account_id);
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![pda_pre, bob_pre],
|
||||
instruction,
|
||||
vec![pre_state],
|
||||
Program::serialize_instruction(seed).unwrap(),
|
||||
vec![InputAccountIdentity::PrivatePdaInit {
|
||||
npk,
|
||||
ssk: shared_secret,
|
||||
identifier: 99,
|
||||
}],
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn private_pda_update_identifier_mismatch_fails() {
|
||||
let program = Program::pda_fund_spend_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 ssk = SharedSecretKey::new(&[55; 32], &keys.vpk());
|
||||
|
||||
let auth_transfer_id = auth_transfer.id();
|
||||
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 5);
|
||||
let pda_account = Account {
|
||||
program_owner: auth_transfer_id,
|
||||
balance: 1,
|
||||
..Account::default()
|
||||
};
|
||||
let pda_commitment = Commitment::new(&pda_id, &pda_account);
|
||||
let mut commitment_set = CommitmentSet::with_capacity(1);
|
||||
commitment_set.extend(std::slice::from_ref(&pda_commitment));
|
||||
|
||||
let pda_pre = AccountWithMetadata::new(pda_account, true, pda_id);
|
||||
let recipient_pre =
|
||||
AccountWithMetadata::new(Account::default(), true, AccountId::new([0; 32]));
|
||||
|
||||
let program_with_deps =
|
||||
ProgramWithDependencies::new(program, [(auth_transfer_id, auth_transfer)].into());
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![pda_pre, recipient_pre],
|
||||
Program::serialize_instruction((seed, 1_u128, auth_transfer_id, false)).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
npk,
|
||||
ssk: shared_secret_pda,
|
||||
InputAccountIdentity::PrivatePdaUpdate {
|
||||
ssk,
|
||||
nsk: keys.nsk,
|
||||
membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(),
|
||||
identifier: 99,
|
||||
},
|
||||
InputAccountIdentity::Public,
|
||||
],
|
||||
&program_with_deps,
|
||||
);
|
||||
|
||||
let (output, _proof) = result.expect("group PDA spend binding should succeed");
|
||||
assert_eq!(output.new_commitments.len(), 1);
|
||||
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
|
||||
}
|
||||
}
|
||||
|
||||
@ -140,7 +140,8 @@ impl Message {
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use nssa_core::{
|
||||
Commitment, EncryptionScheme, Nullifier, NullifierPublicKey, SharedSecretKey,
|
||||
Commitment, EncryptionScheme, Nullifier, NullifierPublicKey, PrivateAccountKind,
|
||||
SharedSecretKey,
|
||||
account::{Account, AccountId, Nonce},
|
||||
encryption::{EphemeralPublicKey, ViewingPublicKey},
|
||||
program::{BlockValidityWindow, TimestampValidityWindow},
|
||||
@ -168,10 +169,10 @@ pub mod tests {
|
||||
|
||||
let encrypted_private_post_states = Vec::new();
|
||||
|
||||
let account_id2 = nssa_core::account::AccountId::from((&npk2, 0));
|
||||
let account_id2 = nssa_core::account::AccountId::for_regular_private_account(&npk2, 0);
|
||||
let new_commitments = vec![Commitment::new(&account_id2, &account2)];
|
||||
|
||||
let account_id1 = nssa_core::account::AccountId::from((&npk1, 0));
|
||||
let account_id1 = nssa_core::account::AccountId::for_regular_private_account(&npk1, 0);
|
||||
let old_commitment = Commitment::new(&account_id1, &account1);
|
||||
let new_nullifiers = vec![(
|
||||
Nullifier::for_account_update(&old_commitment, &nsk1),
|
||||
@ -247,12 +248,18 @@ pub mod tests {
|
||||
let npk = NullifierPublicKey::from(&[1; 32]);
|
||||
let vpk = ViewingPublicKey::from_scalar([2; 32]);
|
||||
let account = Account::default();
|
||||
let account_id = nssa_core::account::AccountId::from((&npk, 0));
|
||||
let account_id = nssa_core::account::AccountId::for_regular_private_account(&npk, 0);
|
||||
let commitment = Commitment::new(&account_id, &account);
|
||||
let esk = [3; 32];
|
||||
let shared_secret = SharedSecretKey::new(&esk, &vpk);
|
||||
let epk = EphemeralPublicKey::from_scalar(esk);
|
||||
let ciphertext = EncryptionScheme::encrypt(&account, 0, &shared_secret, &commitment, 2);
|
||||
let ciphertext = EncryptionScheme::encrypt(
|
||||
&account,
|
||||
&PrivateAccountKind::Regular(0),
|
||||
&shared_secret,
|
||||
&commitment,
|
||||
2,
|
||||
);
|
||||
let encrypted_account_data =
|
||||
EncryptedAccountData::new(ciphertext.clone(), &npk, &vpk, epk.clone());
|
||||
|
||||
|
||||
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -332,6 +332,16 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn pda_fund_spend_proxy() -> Self {
|
||||
use test_program_methods::{PDA_FUND_SPEND_PROXY_ELF, PDA_FUND_SPEND_PROXY_ID};
|
||||
|
||||
Self {
|
||||
id: PDA_FUND_SPEND_PROXY_ID,
|
||||
elf: PDA_FUND_SPEND_PROXY_ELF.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn changer_claimer() -> Self {
|
||||
use test_program_methods::{CHANGER_CLAIMER_ELF, CHANGER_CLAIMER_ID};
|
||||
|
||||
@ -459,7 +459,7 @@ pub mod tests {
|
||||
|
||||
#[must_use]
|
||||
pub fn with_private_account(mut self, keys: &TestPrivateKeys, account: &Account) -> Self {
|
||||
let account_id = AccountId::from((&keys.npk(), 0));
|
||||
let account_id = AccountId::for_regular_private_account(&keys.npk(), 0);
|
||||
let commitment = Commitment::new(&account_id, account);
|
||||
self.private_state.0.extend(&[commitment]);
|
||||
self
|
||||
@ -618,8 +618,8 @@ pub mod tests {
|
||||
..Account::default()
|
||||
};
|
||||
|
||||
let account_id1 = AccountId::from((&keys1.npk(), 0));
|
||||
let account_id2 = AccountId::from((&keys2.npk(), 0));
|
||||
let account_id1 = AccountId::for_regular_private_account(&keys1.npk(), 0);
|
||||
let account_id2 = AccountId::for_regular_private_account(&keys2.npk(), 0);
|
||||
|
||||
let init_commitment1 = Commitment::new(&account_id1, &account);
|
||||
let init_commitment2 = Commitment::new(&account_id2, &account);
|
||||
@ -1256,6 +1256,12 @@ pub mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn test_public_account_keys_2() -> TestPublicKeys {
|
||||
TestPublicKeys {
|
||||
signing_key: PrivateKey::try_new([38; 32]).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn test_private_account_keys_1() -> TestPrivateKeys {
|
||||
TestPrivateKeys {
|
||||
nsk: [13; 32],
|
||||
@ -1326,7 +1332,7 @@ pub mod tests {
|
||||
state: &V03State,
|
||||
) -> PrivacyPreservingTransaction {
|
||||
let program = Program::authenticated_transfer_program();
|
||||
let sender_account_id = AccountId::from((&sender_keys.npk(), 0));
|
||||
let sender_account_id = AccountId::for_regular_private_account(&sender_keys.npk(), 0);
|
||||
let sender_commitment = Commitment::new(&sender_account_id, sender_private_account);
|
||||
let sender_pre = AccountWithMetadata::new(
|
||||
sender_private_account.clone(),
|
||||
@ -1390,7 +1396,7 @@ pub mod tests {
|
||||
state: &V03State,
|
||||
) -> PrivacyPreservingTransaction {
|
||||
let program = Program::authenticated_transfer_program();
|
||||
let sender_account_id = AccountId::from((&sender_keys.npk(), 0));
|
||||
let sender_account_id = AccountId::for_regular_private_account(&sender_keys.npk(), 0);
|
||||
let sender_commitment = Commitment::new(&sender_account_id, sender_private_account);
|
||||
let sender_pre = AccountWithMetadata::new(
|
||||
sender_private_account.clone(),
|
||||
@ -1505,8 +1511,8 @@ pub mod tests {
|
||||
&state,
|
||||
);
|
||||
|
||||
let sender_account_id = AccountId::from((&sender_keys.npk(), 0));
|
||||
let recipient_account_id = AccountId::from((&recipient_keys.npk(), 0));
|
||||
let sender_account_id = AccountId::for_regular_private_account(&sender_keys.npk(), 0);
|
||||
let recipient_account_id = AccountId::for_regular_private_account(&recipient_keys.npk(), 0);
|
||||
let expected_new_commitment_1 = Commitment::new(
|
||||
&sender_account_id,
|
||||
&Account {
|
||||
@ -1584,7 +1590,7 @@ pub mod tests {
|
||||
&state,
|
||||
);
|
||||
|
||||
let sender_account_id = AccountId::from((&sender_keys.npk(), 0));
|
||||
let sender_account_id = AccountId::for_regular_private_account(&sender_keys.npk(), 0);
|
||||
let expected_new_commitment = Commitment::new(
|
||||
&sender_account_id,
|
||||
&Account {
|
||||
@ -2185,6 +2191,7 @@ pub mod tests {
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
npk,
|
||||
ssk: shared_secret,
|
||||
identifier: u128::MAX,
|
||||
},
|
||||
],
|
||||
&program.into(),
|
||||
@ -2206,7 +2213,7 @@ pub mod tests {
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
let shared_secret = SharedSecretKey::new(&[55; 32], &keys.vpk());
|
||||
|
||||
let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk);
|
||||
let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, u128::MAX);
|
||||
let pre_state = AccountWithMetadata::new(Account::default(), false, account_id);
|
||||
|
||||
let result = execute_and_prove(
|
||||
@ -2215,6 +2222,7 @@ pub mod tests {
|
||||
vec![InputAccountIdentity::PrivatePdaInit {
|
||||
npk,
|
||||
ssk: shared_secret,
|
||||
identifier: u128::MAX,
|
||||
}],
|
||||
&program.into(),
|
||||
);
|
||||
@ -2244,7 +2252,7 @@ pub mod tests {
|
||||
// `account_id` is derived from `npk_a`, but `npk_b` is supplied for this pre_state.
|
||||
// `AccountId::for_private_pda(program, seed, npk_b) != account_id`, so the claim check in
|
||||
// the circuit must reject.
|
||||
let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk_a);
|
||||
let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk_a, u128::MAX);
|
||||
let pre_state = AccountWithMetadata::new(Account::default(), false, account_id);
|
||||
|
||||
let result = execute_and_prove(
|
||||
@ -2253,6 +2261,7 @@ pub mod tests {
|
||||
vec![InputAccountIdentity::PrivatePdaInit {
|
||||
npk: npk_b,
|
||||
ssk: shared_secret,
|
||||
identifier: u128::MAX,
|
||||
}],
|
||||
&program.into(),
|
||||
);
|
||||
@ -2274,7 +2283,7 @@ pub mod tests {
|
||||
let seed = PdaSeed::new([77; 32]);
|
||||
let shared_secret = SharedSecretKey::new(&[55; 32], &keys.vpk());
|
||||
|
||||
let account_id = AccountId::for_private_pda(&delegator.id(), &seed, &npk);
|
||||
let account_id = AccountId::for_private_pda(&delegator.id(), &seed, &npk, u128::MAX);
|
||||
let pre_state = AccountWithMetadata::new(Account::default(), false, account_id);
|
||||
|
||||
let callee_id = callee.id();
|
||||
@ -2287,6 +2296,7 @@ pub mod tests {
|
||||
vec![InputAccountIdentity::PrivatePdaInit {
|
||||
npk,
|
||||
ssk: shared_secret,
|
||||
identifier: u128::MAX,
|
||||
}],
|
||||
&program_with_deps,
|
||||
);
|
||||
@ -2311,7 +2321,7 @@ pub mod tests {
|
||||
let wrong_delegated_seed = PdaSeed::new([88; 32]);
|
||||
let shared_secret = SharedSecretKey::new(&[55; 32], &keys.vpk());
|
||||
|
||||
let account_id = AccountId::for_private_pda(&delegator.id(), &claim_seed, &npk);
|
||||
let account_id = AccountId::for_private_pda(&delegator.id(), &claim_seed, &npk, u128::MAX);
|
||||
let pre_state = AccountWithMetadata::new(Account::default(), false, account_id);
|
||||
|
||||
let callee_id = callee.id();
|
||||
@ -2324,6 +2334,7 @@ pub mod tests {
|
||||
vec![InputAccountIdentity::PrivatePdaInit {
|
||||
npk,
|
||||
ssk: shared_secret,
|
||||
identifier: u128::MAX,
|
||||
}],
|
||||
&program_with_deps,
|
||||
);
|
||||
@ -2348,8 +2359,8 @@ pub mod tests {
|
||||
let shared_a = SharedSecretKey::new(&[66; 32], &keys_a.vpk());
|
||||
let shared_b = SharedSecretKey::new(&[77; 32], &keys_b.vpk());
|
||||
|
||||
let account_a = AccountId::for_private_pda(&program.id(), &seed, &keys_a.npk());
|
||||
let account_b = AccountId::for_private_pda(&program.id(), &seed, &keys_b.npk());
|
||||
let account_a = AccountId::for_private_pda(&program.id(), &seed, &keys_a.npk(), u128::MAX);
|
||||
let account_b = AccountId::for_private_pda(&program.id(), &seed, &keys_b.npk(), u128::MAX);
|
||||
|
||||
let pre_a = AccountWithMetadata::new(Account::default(), false, account_a);
|
||||
let pre_b = AccountWithMetadata::new(Account::default(), false, account_b);
|
||||
@ -2361,10 +2372,12 @@ pub mod tests {
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
npk: keys_a.npk(),
|
||||
ssk: shared_a,
|
||||
identifier: u128::MAX,
|
||||
},
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
npk: keys_b.npk(),
|
||||
ssk: shared_b,
|
||||
identifier: u128::MAX,
|
||||
},
|
||||
],
|
||||
&program.into(),
|
||||
@ -2394,7 +2407,7 @@ pub mod tests {
|
||||
|
||||
// Simulate a previously-claimed private PDA: program_owner != DEFAULT, is_authorized =
|
||||
// true, account_id derived via the private formula.
|
||||
let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk);
|
||||
let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, u128::MAX);
|
||||
let owned_pre_state = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: program.id(),
|
||||
@ -2410,6 +2423,7 @@ pub mod tests {
|
||||
vec![InputAccountIdentity::PrivatePdaInit {
|
||||
npk,
|
||||
ssk: shared_secret,
|
||||
identifier: u128::MAX,
|
||||
}],
|
||||
&program.into(),
|
||||
);
|
||||
@ -2812,7 +2826,7 @@ pub mod tests {
|
||||
balance: 100,
|
||||
..Account::default()
|
||||
};
|
||||
let sender_account_id = AccountId::from((&sender_keys.npk(), 0));
|
||||
let sender_account_id = AccountId::for_regular_private_account(&sender_keys.npk(), 0);
|
||||
let sender_commitment = Commitment::new(&sender_account_id, &sender_private_account);
|
||||
let sender_init_nullifier = Nullifier::for_account_initialization(&sender_account_id);
|
||||
let mut state = V03State::new_with_genesis_accounts(
|
||||
@ -2905,8 +2919,8 @@ pub mod tests {
|
||||
(&to_keys.npk(), 0),
|
||||
);
|
||||
|
||||
let from_account_id = AccountId::from((&from_keys.npk(), 0));
|
||||
let to_account_id = AccountId::from((&to_keys.npk(), 0));
|
||||
let from_account_id = AccountId::for_regular_private_account(&from_keys.npk(), 0);
|
||||
let to_account_id = AccountId::for_regular_private_account(&to_keys.npk(), 0);
|
||||
let from_commitment = Commitment::new(&from_account_id, &from_account.account);
|
||||
let to_commitment = Commitment::new(&to_account_id, &to_account.account);
|
||||
let from_init_nullifier = Nullifier::for_account_initialization(&from_account_id);
|
||||
@ -3266,7 +3280,7 @@ pub mod tests {
|
||||
let result = state.transition_from_privacy_preserving_transaction(&tx, 1, 0);
|
||||
assert!(result.is_ok());
|
||||
|
||||
let account_id = AccountId::from((&private_keys.npk(), 0));
|
||||
let account_id = AccountId::for_regular_private_account(&private_keys.npk(), 0);
|
||||
let nullifier = Nullifier::for_account_initialization(&account_id);
|
||||
assert!(state.private_state.1.contains(&nullifier));
|
||||
}
|
||||
@ -3315,7 +3329,7 @@ pub mod tests {
|
||||
.transition_from_privacy_preserving_transaction(&tx, 1, 0)
|
||||
.unwrap();
|
||||
|
||||
let account_id = AccountId::from((&private_keys.npk(), 0));
|
||||
let account_id = AccountId::for_regular_private_account(&private_keys.npk(), 0);
|
||||
let nullifier = Nullifier::for_account_initialization(&account_id);
|
||||
assert!(state.private_state.1.contains(&nullifier));
|
||||
}
|
||||
@ -3372,7 +3386,7 @@ pub mod tests {
|
||||
);
|
||||
|
||||
// Verify the account is now initialized (nullifier exists)
|
||||
let account_id = AccountId::from((&private_keys.npk(), 0));
|
||||
let account_id = AccountId::for_regular_private_account(&private_keys.npk(), 0);
|
||||
let nullifier = Nullifier::for_account_initialization(&account_id);
|
||||
assert!(state.private_state.1.contains(&nullifier));
|
||||
|
||||
@ -3527,7 +3541,7 @@ pub mod tests {
|
||||
let recipient_account =
|
||||
AccountWithMetadata::new(Account::default(), true, (&recipient_keys.npk(), 0));
|
||||
|
||||
let recipient_account_id = AccountId::from((&recipient_keys.npk(), 0));
|
||||
let recipient_account_id = AccountId::for_regular_private_account(&recipient_keys.npk(), 0);
|
||||
let recipient_commitment =
|
||||
Commitment::new(&recipient_account_id, &recipient_account.account);
|
||||
let recipient_init_nullifier = Nullifier::for_account_initialization(&recipient_account_id);
|
||||
@ -4286,4 +4300,225 @@ pub mod tests {
|
||||
"program with spoofed caller_program_id in output should be rejected"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_private_pda_family_members_receive_and_spend() {
|
||||
let funder_keys = test_public_account_keys_1();
|
||||
let alice_keys = test_private_account_keys_1();
|
||||
let alice_npk = alice_keys.npk();
|
||||
|
||||
let proxy = Program::pda_fund_spend_proxy();
|
||||
let auth_transfer = Program::authenticated_transfer_program();
|
||||
let proxy_id = proxy.id();
|
||||
let auth_transfer_id = auth_transfer.id();
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
let amount: u128 = 100;
|
||||
|
||||
let program_with_deps =
|
||||
ProgramWithDependencies::new(proxy, [(auth_transfer_id, auth_transfer)].into());
|
||||
|
||||
let funder_id = funder_keys.account_id();
|
||||
let alice_pda_0_id = AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, 0);
|
||||
let alice_pda_1_id = AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, 1);
|
||||
let recipient_id = test_public_account_keys_2().account_id();
|
||||
let recipient_signing_key = test_public_account_keys_2().signing_key;
|
||||
|
||||
let mut state = V03State::new_with_genesis_accounts(&[(funder_id, 500)], vec![], 0);
|
||||
|
||||
let alice_pda_0_account = Account {
|
||||
program_owner: auth_transfer_id,
|
||||
balance: amount,
|
||||
nonce: Nonce::private_account_nonce_init(&alice_pda_0_id),
|
||||
..Account::default()
|
||||
};
|
||||
let alice_pda_1_account = Account {
|
||||
program_owner: auth_transfer_id,
|
||||
balance: amount,
|
||||
nonce: Nonce::private_account_nonce_init(&alice_pda_1_id),
|
||||
..Account::default()
|
||||
};
|
||||
|
||||
let alice_shared_0 = SharedSecretKey::new(&[10; 32], &alice_keys.vpk());
|
||||
let alice_shared_1 = SharedSecretKey::new(&[11; 32], &alice_keys.vpk());
|
||||
|
||||
// Fund alice_pda_0
|
||||
{
|
||||
let funder_account = state.get_account_by_id(funder_id);
|
||||
let funder_nonce = funder_account.nonce;
|
||||
let (output, proof) = execute_and_prove(
|
||||
vec![
|
||||
AccountWithMetadata::new(funder_account, true, funder_id),
|
||||
AccountWithMetadata::new(Account::default(), false, alice_pda_0_id),
|
||||
],
|
||||
Program::serialize_instruction((seed, amount, auth_transfer_id, true)).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
npk: alice_npk,
|
||||
ssk: alice_shared_0,
|
||||
identifier: 0,
|
||||
},
|
||||
],
|
||||
&program_with_deps,
|
||||
)
|
||||
.unwrap();
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![funder_id],
|
||||
vec![funder_nonce],
|
||||
vec![(
|
||||
alice_npk,
|
||||
alice_keys.vpk(),
|
||||
EphemeralPublicKey::from_scalar([10; 32]),
|
||||
)],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[&funder_keys.signing_key]);
|
||||
state
|
||||
.transition_from_privacy_preserving_transaction(
|
||||
&PrivacyPreservingTransaction::new(message, witness_set),
|
||||
1,
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Fund alice_pda_1
|
||||
{
|
||||
let funder_account = state.get_account_by_id(funder_id);
|
||||
let funder_nonce = funder_account.nonce;
|
||||
let (output, proof) = execute_and_prove(
|
||||
vec![
|
||||
AccountWithMetadata::new(funder_account, true, funder_id),
|
||||
AccountWithMetadata::new(Account::default(), false, alice_pda_1_id),
|
||||
],
|
||||
Program::serialize_instruction((seed, amount, auth_transfer_id, true)).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
npk: alice_npk,
|
||||
ssk: alice_shared_1,
|
||||
identifier: 1,
|
||||
},
|
||||
],
|
||||
&program_with_deps,
|
||||
)
|
||||
.unwrap();
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![funder_id],
|
||||
vec![funder_nonce],
|
||||
vec![(
|
||||
alice_npk,
|
||||
alice_keys.vpk(),
|
||||
EphemeralPublicKey::from_scalar([11; 32]),
|
||||
)],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[&funder_keys.signing_key]);
|
||||
state
|
||||
.transition_from_privacy_preserving_transaction(
|
||||
&PrivacyPreservingTransaction::new(message, witness_set),
|
||||
2,
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let commitment_pda_0 = Commitment::new(&alice_pda_0_id, &alice_pda_0_account);
|
||||
let commitment_pda_1 = Commitment::new(&alice_pda_1_id, &alice_pda_1_account);
|
||||
|
||||
assert!(state.get_proof_for_commitment(&commitment_pda_0).is_some());
|
||||
assert!(state.get_proof_for_commitment(&commitment_pda_1).is_some());
|
||||
|
||||
// Alice spends alice_pda_0 into the public recipient.
|
||||
{
|
||||
let recipient_account = state.get_account_by_id(recipient_id);
|
||||
let (output, proof) = execute_and_prove(
|
||||
vec![
|
||||
AccountWithMetadata::new(alice_pda_0_account, true, alice_pda_0_id),
|
||||
AccountWithMetadata::new(recipient_account, true, recipient_id),
|
||||
],
|
||||
Program::serialize_instruction((seed, amount, auth_transfer_id, false)).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivatePdaUpdate {
|
||||
ssk: alice_shared_0,
|
||||
nsk: alice_keys.nsk,
|
||||
membership_proof: state
|
||||
.get_proof_for_commitment(&commitment_pda_0)
|
||||
.expect("pda_0 must be in state"),
|
||||
identifier: 0,
|
||||
},
|
||||
InputAccountIdentity::Public,
|
||||
],
|
||||
&program_with_deps,
|
||||
)
|
||||
.unwrap();
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![recipient_id],
|
||||
vec![Nonce(0)],
|
||||
vec![(
|
||||
alice_npk,
|
||||
alice_keys.vpk(),
|
||||
EphemeralPublicKey::from_scalar([10; 32]),
|
||||
)],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_signing_key]);
|
||||
state
|
||||
.transition_from_privacy_preserving_transaction(
|
||||
&PrivacyPreservingTransaction::new(message, witness_set),
|
||||
3,
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Alice spends alice_pda_1 into the same public recipient.
|
||||
{
|
||||
let recipient_account = state.get_account_by_id(recipient_id);
|
||||
let (output, proof) = execute_and_prove(
|
||||
vec![
|
||||
AccountWithMetadata::new(alice_pda_1_account, true, alice_pda_1_id),
|
||||
AccountWithMetadata::new(recipient_account, false, recipient_id),
|
||||
],
|
||||
Program::serialize_instruction((seed, amount, auth_transfer_id, false)).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivatePdaUpdate {
|
||||
ssk: alice_shared_1,
|
||||
nsk: alice_keys.nsk,
|
||||
membership_proof: state
|
||||
.get_proof_for_commitment(&commitment_pda_1)
|
||||
.expect("pda_1 must be in state"),
|
||||
identifier: 1,
|
||||
},
|
||||
InputAccountIdentity::Public,
|
||||
],
|
||||
&program_with_deps,
|
||||
)
|
||||
.unwrap();
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![recipient_id],
|
||||
vec![],
|
||||
vec![(
|
||||
alice_npk,
|
||||
alice_keys.vpk(),
|
||||
EphemeralPublicKey::from_scalar([11; 32]),
|
||||
)],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]);
|
||||
state
|
||||
.transition_from_privacy_preserving_transaction(
|
||||
&PrivacyPreservingTransaction::new(message, witness_set),
|
||||
4,
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
assert_eq!(state.get_account_by_id(recipient_id).balance, 2 * amount);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet, VecDeque, hash_map::Entry},
|
||||
collections::{HashMap, VecDeque, hash_map::Entry},
|
||||
convert::Infallible,
|
||||
};
|
||||
|
||||
use nssa_core::{
|
||||
Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme, Identifier,
|
||||
InputAccountIdentity, MembershipProof, Nullifier, NullifierPublicKey, NullifierSecretKey,
|
||||
PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, SharedSecretKey,
|
||||
PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, PrivateAccountKind,
|
||||
SharedSecretKey,
|
||||
account::{Account, AccountId, AccountWithMetadata, Nonce},
|
||||
compute_digest_for_path,
|
||||
program::{
|
||||
@ -17,25 +18,26 @@ use nssa_core::{
|
||||
};
|
||||
use risc0_zkvm::{guest::env, serde::to_vec};
|
||||
|
||||
const PRIVATE_PDA_FIXED_IDENTIFIER: Identifier = u128::MAX;
|
||||
|
||||
/// State of the involved accounts before and after program execution.
|
||||
struct ExecutionState {
|
||||
pre_states: Vec<AccountWithMetadata>,
|
||||
post_states: HashMap<AccountId, Account>,
|
||||
block_validity_window: BlockValidityWindow,
|
||||
timestamp_validity_window: TimestampValidityWindow,
|
||||
/// Positions (in `pre_states`) of private-PDA accounts whose supplied npk has been bound
|
||||
/// to their `AccountId` via a proven `AccountId::for_private_pda(program_id, seed, npk)`
|
||||
/// check.
|
||||
/// Positions (in `pre_states`) of private-PDA accounts whose supplied npk has been bound to
|
||||
/// their `AccountId` via a proven `AccountId::for_private_pda(program_id, seed, npk,
|
||||
/// identifier)` check.
|
||||
/// Two proof paths populate this set: a `Claim::Pda(seed)` in a program's `post_state` on
|
||||
/// that `pre_state`, or a caller's `ChainedCall.pda_seeds` entry matching that `pre_state`
|
||||
/// under the private derivation. Binding is an idempotent property, not an event: the same
|
||||
/// position can legitimately be bound through both paths in the same tx (e.g. a program
|
||||
/// claims a private PDA and then delegates it to a callee), and the set uses `contains`,
|
||||
/// not `assert!(insert)`. After the main loop, every private-PDA position must appear in
|
||||
/// this set; otherwise the npk is unbound and the circuit rejects.
|
||||
private_pda_bound_positions: HashSet<usize>,
|
||||
/// claims a private PDA and then delegates it to a callee), and the map uses `contains_key`,
|
||||
/// not `assert!(insert)`. After the main loop, every private-PDA position must appear in this
|
||||
/// map; otherwise the npk is unbound and the circuit rejects.
|
||||
/// The stored `(ProgramId, PdaSeed)` is the owner program and seed, used in
|
||||
/// `compute_circuit_output` to construct `PrivateAccountKind::Pda { program_id, seed,
|
||||
/// identifier }`.
|
||||
private_pda_bound_positions: HashMap<usize, (ProgramId, PdaSeed)>,
|
||||
/// Across the whole transaction, each `(program_id, seed)` pair may resolve to at most one
|
||||
/// `AccountId`. A seed under a program can derive a family of accounts, one public PDA and
|
||||
/// one private PDA per distinct npk. Without this check, a single `pda_seeds: [S]` entry in
|
||||
@ -45,12 +47,12 @@ struct ExecutionState {
|
||||
/// `AccountId` entry or as an equality check against the existing one, making the rule: one
|
||||
/// `(program, seed)` → one account per tx.
|
||||
pda_family_binding: HashMap<(ProgramId, PdaSeed), AccountId>,
|
||||
/// Map from a private-PDA `pre_state`'s position in `account_identities` to the npk that
|
||||
/// variant supplies for that position. Populated once in `derive_from_outputs` by walking
|
||||
/// Map from a private-PDA `pre_state`'s position in `account_identities` to the (npk,
|
||||
/// identifier) supplied for that position. Built once in `derive_from_outputs` by walking
|
||||
/// `account_identities` and consulting `npk_if_private_pda`. Used later by the claim and
|
||||
/// caller-seeds authorization paths to verify
|
||||
/// `AccountId::for_private_pda(program_id, seed, npk) == pre_state.account_id`.
|
||||
private_pda_npk_by_position: HashMap<usize, NullifierPublicKey>,
|
||||
/// `AccountId::for_private_pda(program_id, seed, npk, identifier) == pre_state.account_id`.
|
||||
private_pda_npk_by_position: HashMap<usize, (NullifierPublicKey, Identifier)>,
|
||||
}
|
||||
|
||||
impl ExecutionState {
|
||||
@ -60,14 +62,15 @@ impl ExecutionState {
|
||||
program_id: ProgramId,
|
||||
program_outputs: Vec<ProgramOutput>,
|
||||
) -> Self {
|
||||
// Build position → npk map for private-PDA pre_states, indexed by position in
|
||||
// `account_identities`. The vec is documented as 1:1 with the program's pre_state order,
|
||||
// so position here matches `pre_state_position` used downstream in
|
||||
// Build position → (npk, identifier) map for private-PDA pre_states, indexed by position
|
||||
// in `account_identities`. The vec is documented as 1:1 with the program's pre_state
|
||||
// order, so position here matches `pre_state_position` used downstream in
|
||||
// `validate_and_sync_states`.
|
||||
let mut private_pda_npk_by_position: HashMap<usize, NullifierPublicKey> = HashMap::new();
|
||||
let mut private_pda_npk_by_position: HashMap<usize, (NullifierPublicKey, Identifier)> =
|
||||
HashMap::new();
|
||||
for (pos, account_identity) in account_identities.iter().enumerate() {
|
||||
if let Some(npk) = account_identity.npk_if_private_pda() {
|
||||
private_pda_npk_by_position.insert(pos, npk);
|
||||
if let Some((npk, identifier)) = account_identity.npk_if_private_pda() {
|
||||
private_pda_npk_by_position.insert(pos, (npk, identifier));
|
||||
}
|
||||
}
|
||||
|
||||
@ -105,7 +108,7 @@ impl ExecutionState {
|
||||
post_states: HashMap::new(),
|
||||
block_validity_window,
|
||||
timestamp_validity_window,
|
||||
private_pda_bound_positions: HashSet::new(),
|
||||
private_pda_bound_positions: HashMap::new(),
|
||||
pda_family_binding: HashMap::new(),
|
||||
private_pda_npk_by_position,
|
||||
};
|
||||
@ -208,7 +211,9 @@ impl ExecutionState {
|
||||
for (pos, account_identity) in account_identities.iter().enumerate() {
|
||||
if account_identity.is_private_pda() {
|
||||
assert!(
|
||||
execution_state.private_pda_bound_positions.contains(&pos),
|
||||
execution_state
|
||||
.private_pda_bound_positions
|
||||
.contains_key(&pos),
|
||||
"private PDA pre_state at position {pos} has no proven (seed, npk) binding via Claim::Pda or caller pda_seeds"
|
||||
);
|
||||
}
|
||||
@ -353,18 +358,24 @@ impl ExecutionState {
|
||||
);
|
||||
}
|
||||
Claim::Pda(seed) => {
|
||||
let npk = self
|
||||
let (npk, identifier) = self
|
||||
.private_pda_npk_by_position
|
||||
.get(&pre_state_position)
|
||||
.expect(
|
||||
"private PDA pre_state must have an npk in the position map",
|
||||
);
|
||||
let pda = AccountId::for_private_pda(&program_id, &seed, npk);
|
||||
let pda =
|
||||
AccountId::for_private_pda(&program_id, &seed, npk, *identifier);
|
||||
assert_eq!(
|
||||
pre_account_id, pda,
|
||||
"Invalid private PDA claim for account {pre_account_id}"
|
||||
);
|
||||
self.private_pda_bound_positions.insert(pre_state_position);
|
||||
bind_private_pda_position(
|
||||
&mut self.private_pda_bound_positions,
|
||||
pre_state_position,
|
||||
program_id,
|
||||
seed,
|
||||
);
|
||||
assert_family_binding(
|
||||
&mut self.pda_family_binding,
|
||||
program_id,
|
||||
@ -428,6 +439,24 @@ fn assert_family_binding(
|
||||
}
|
||||
}
|
||||
|
||||
fn bind_private_pda_position(
|
||||
map: &mut HashMap<usize, (ProgramId, PdaSeed)>,
|
||||
position: usize,
|
||||
program_id: ProgramId,
|
||||
seed: PdaSeed,
|
||||
) {
|
||||
match map.entry(position) {
|
||||
Entry::Occupied(e) => assert_eq!(
|
||||
*e.get(),
|
||||
(program_id, seed),
|
||||
"Duplicate binding at position {position}: conflicting (program_id, seed)"
|
||||
),
|
||||
Entry::Vacant(e) => {
|
||||
e.insert((program_id, seed));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve the authorization state of a `pre_state` seen again in a chained call and record
|
||||
/// any resulting bindings. Returns `true` if the `pre_state` is authorized through either a
|
||||
/// previously-seen authorization or a matching caller seed (under the public or private
|
||||
@ -443,8 +472,8 @@ fn assert_family_binding(
|
||||
)]
|
||||
fn resolve_authorization_and_record_bindings(
|
||||
pda_family_binding: &mut HashMap<(ProgramId, PdaSeed), AccountId>,
|
||||
private_pda_bound_positions: &mut HashSet<usize>,
|
||||
private_pda_npk_by_position: &HashMap<usize, NullifierPublicKey>,
|
||||
private_pda_bound_positions: &mut HashMap<usize, (ProgramId, PdaSeed)>,
|
||||
private_pda_npk_by_position: &HashMap<usize, (NullifierPublicKey, Identifier)>,
|
||||
pre_account_id: AccountId,
|
||||
pre_state_position: usize,
|
||||
caller_program_id: Option<ProgramId>,
|
||||
@ -457,8 +486,9 @@ fn resolve_authorization_and_record_bindings(
|
||||
if AccountId::for_public_pda(&caller, seed) == pre_account_id {
|
||||
return Some((*seed, false, caller));
|
||||
}
|
||||
if let Some(npk) = private_pda_npk_by_position.get(&pre_state_position)
|
||||
&& AccountId::for_private_pda(&caller, seed, npk) == pre_account_id
|
||||
if let Some((npk, identifier)) =
|
||||
private_pda_npk_by_position.get(&pre_state_position)
|
||||
&& AccountId::for_private_pda(&caller, seed, npk, *identifier) == pre_account_id
|
||||
{
|
||||
return Some((*seed, true, caller));
|
||||
}
|
||||
@ -469,7 +499,12 @@ fn resolve_authorization_and_record_bindings(
|
||||
if let Some((seed, is_private_form, caller)) = matched_caller_seed {
|
||||
assert_family_binding(pda_family_binding, caller, seed, pre_account_id);
|
||||
if is_private_form {
|
||||
private_pda_bound_positions.insert(pre_state_position);
|
||||
bind_private_pda_position(
|
||||
private_pda_bound_positions,
|
||||
pre_state_position,
|
||||
caller,
|
||||
seed,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -477,7 +512,7 @@ fn resolve_authorization_and_record_bindings(
|
||||
}
|
||||
|
||||
fn compute_circuit_output(
|
||||
execution_state: ExecutionState,
|
||||
mut execution_state: ExecutionState,
|
||||
account_identities: &[InputAccountIdentity],
|
||||
) -> PrivacyPreservingCircuitOutput {
|
||||
let mut output = PrivacyPreservingCircuitOutput {
|
||||
@ -490,6 +525,7 @@ fn compute_circuit_output(
|
||||
timestamp_validity_window: execution_state.timestamp_validity_window,
|
||||
};
|
||||
|
||||
let pda_seed_by_position = std::mem::take(&mut execution_state.private_pda_bound_positions);
|
||||
let states_iter = execution_state.into_states_iter();
|
||||
assert_eq!(
|
||||
account_identities.len(),
|
||||
@ -498,7 +534,9 @@ fn compute_circuit_output(
|
||||
);
|
||||
|
||||
let mut output_index = 0;
|
||||
for (account_identity, (pre_state, post_state)) in account_identities.iter().zip(states_iter) {
|
||||
for (pos, (account_identity, (pre_state, post_state))) in
|
||||
account_identities.iter().zip(states_iter).enumerate()
|
||||
{
|
||||
match account_identity {
|
||||
InputAccountIdentity::Public => {
|
||||
output.public_pre_states.push(pre_state);
|
||||
@ -509,12 +547,8 @@ fn compute_circuit_output(
|
||||
nsk,
|
||||
identifier,
|
||||
} => {
|
||||
assert_ne!(
|
||||
*identifier, PRIVATE_PDA_FIXED_IDENTIFIER,
|
||||
"Identifier must be different from {PRIVATE_PDA_FIXED_IDENTIFIER}. This is reserved for private PDA."
|
||||
);
|
||||
let npk = NullifierPublicKey::from(nsk);
|
||||
let account_id = AccountId::from((&npk, *identifier));
|
||||
let account_id = AccountId::for_regular_private_account(&npk, *identifier);
|
||||
|
||||
assert_eq!(account_id, pre_state.account_id, "AccountId mismatch");
|
||||
assert!(
|
||||
@ -538,7 +572,7 @@ fn compute_circuit_output(
|
||||
&mut output_index,
|
||||
post_state,
|
||||
&account_id,
|
||||
*identifier,
|
||||
&PrivateAccountKind::Regular(*identifier),
|
||||
ssk,
|
||||
new_nullifier,
|
||||
new_nonce,
|
||||
@ -550,12 +584,8 @@ fn compute_circuit_output(
|
||||
membership_proof,
|
||||
identifier,
|
||||
} => {
|
||||
assert_ne!(
|
||||
*identifier, PRIVATE_PDA_FIXED_IDENTIFIER,
|
||||
"Identifier must be different from {PRIVATE_PDA_FIXED_IDENTIFIER}. This is reserved for private PDA."
|
||||
);
|
||||
let npk = NullifierPublicKey::from(nsk);
|
||||
let account_id = AccountId::from((&npk, *identifier));
|
||||
let account_id = AccountId::for_regular_private_account(&npk, *identifier);
|
||||
|
||||
assert_eq!(account_id, pre_state.account_id, "AccountId mismatch");
|
||||
assert!(
|
||||
@ -576,7 +606,7 @@ fn compute_circuit_output(
|
||||
&mut output_index,
|
||||
post_state,
|
||||
&account_id,
|
||||
*identifier,
|
||||
&PrivateAccountKind::Regular(*identifier),
|
||||
ssk,
|
||||
new_nullifier,
|
||||
new_nonce,
|
||||
@ -587,11 +617,7 @@ fn compute_circuit_output(
|
||||
ssk,
|
||||
identifier,
|
||||
} => {
|
||||
assert_ne!(
|
||||
*identifier, PRIVATE_PDA_FIXED_IDENTIFIER,
|
||||
"Identifier must be different from {PRIVATE_PDA_FIXED_IDENTIFIER}. This is reserved for private PDA."
|
||||
);
|
||||
let account_id = AccountId::from((npk, *identifier));
|
||||
let account_id = AccountId::for_regular_private_account(npk, *identifier);
|
||||
|
||||
assert_eq!(account_id, pre_state.account_id, "AccountId mismatch");
|
||||
assert_eq!(
|
||||
@ -615,13 +641,17 @@ fn compute_circuit_output(
|
||||
&mut output_index,
|
||||
post_state,
|
||||
&account_id,
|
||||
*identifier,
|
||||
&PrivateAccountKind::Regular(*identifier),
|
||||
ssk,
|
||||
new_nullifier,
|
||||
new_nonce,
|
||||
);
|
||||
}
|
||||
InputAccountIdentity::PrivatePdaInit { npk: _, ssk } => {
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
npk: _,
|
||||
ssk,
|
||||
identifier,
|
||||
} => {
|
||||
// The npk-to-account_id binding is established upstream in
|
||||
// `validate_and_sync_states` via `Claim::Pda(seed)` or a caller `pda_seeds`
|
||||
// match. Here we only enforce the init pre-conditions. The supplied npk on
|
||||
@ -645,12 +675,19 @@ fn compute_circuit_output(
|
||||
let new_nonce = Nonce::private_account_nonce_init(&pre_state.account_id);
|
||||
|
||||
let account_id = pre_state.account_id;
|
||||
let (pda_program_id, seed) = pda_seed_by_position
|
||||
.get(&pos)
|
||||
.expect("PrivatePdaInit position must be in pda_seed_by_position");
|
||||
emit_private_output(
|
||||
&mut output,
|
||||
&mut output_index,
|
||||
post_state,
|
||||
&account_id,
|
||||
PRIVATE_PDA_FIXED_IDENTIFIER,
|
||||
&PrivateAccountKind::Pda {
|
||||
program_id: *pda_program_id,
|
||||
seed: *seed,
|
||||
identifier: *identifier,
|
||||
},
|
||||
ssk,
|
||||
new_nullifier,
|
||||
new_nonce,
|
||||
@ -660,6 +697,7 @@ fn compute_circuit_output(
|
||||
ssk,
|
||||
nsk,
|
||||
membership_proof,
|
||||
identifier,
|
||||
} => {
|
||||
// The npk binding is established upstream. Authorization must already be set;
|
||||
// an unauthorized PrivatePdaUpdate would mean the prover supplied an nsk for an
|
||||
@ -679,12 +717,19 @@ fn compute_circuit_output(
|
||||
let new_nonce = pre_state.account.nonce.private_account_nonce_increment(nsk);
|
||||
|
||||
let account_id = pre_state.account_id;
|
||||
let (pda_program_id, seed) = pda_seed_by_position
|
||||
.get(&pos)
|
||||
.expect("PrivatePdaUpdate position must be in pda_seed_by_position");
|
||||
emit_private_output(
|
||||
&mut output,
|
||||
&mut output_index,
|
||||
post_state,
|
||||
&account_id,
|
||||
PRIVATE_PDA_FIXED_IDENTIFIER,
|
||||
&PrivateAccountKind::Pda {
|
||||
program_id: *pda_program_id,
|
||||
seed: *seed,
|
||||
identifier: *identifier,
|
||||
},
|
||||
ssk,
|
||||
new_nullifier,
|
||||
new_nonce,
|
||||
@ -705,7 +750,7 @@ fn emit_private_output(
|
||||
output_index: &mut u32,
|
||||
post_state: Account,
|
||||
account_id: &AccountId,
|
||||
identifier: Identifier,
|
||||
kind: &PrivateAccountKind,
|
||||
shared_secret: &SharedSecretKey,
|
||||
new_nullifier: (Nullifier, CommitmentSetDigest),
|
||||
new_nonce: Nonce,
|
||||
@ -718,7 +763,7 @@ fn emit_private_output(
|
||||
let commitment_post = Commitment::new(account_id, &post_with_updated_nonce);
|
||||
let encrypted_account = EncryptionScheme::encrypt(
|
||||
&post_with_updated_nonce,
|
||||
identifier,
|
||||
kind,
|
||||
shared_secret,
|
||||
&commitment_post,
|
||||
*output_index,
|
||||
|
||||
@ -49,7 +49,7 @@ pub enum Instruction {
|
||||
|
||||
pub fn compute_ata_seed(owner_id: AccountId, definition_id: AccountId) -> PdaSeed {
|
||||
use risc0_zkvm::sha::{Impl, Sha256};
|
||||
let mut bytes = [0u8; 64];
|
||||
let mut bytes = [0_u8; 64];
|
||||
bytes[0..32].copy_from_slice(&owner_id.to_bytes());
|
||||
bytes[32..64].copy_from_slice(&definition_id.to_bytes());
|
||||
PdaSeed::new(
|
||||
|
||||
@ -141,7 +141,7 @@ impl<BP: BlockPublisherTrait> SequencerCore<BP> {
|
||||
.iter()
|
||||
.map(|init_comm_data| {
|
||||
let npk = &init_comm_data.npk;
|
||||
let account_id = nssa::AccountId::from((npk, 0));
|
||||
let account_id = nssa::AccountId::for_regular_private_account(npk, 0);
|
||||
|
||||
let mut acc = init_comm_data.account.clone();
|
||||
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
70
test_program_methods/guest/src/bin/pda_fund_spend_proxy.rs
Normal file
70
test_program_methods/guest/src/bin/pda_fund_spend_proxy.rs
Normal file
@ -0,0 +1,70 @@
|
||||
use nssa_core::{
|
||||
account::AccountWithMetadata,
|
||||
program::{
|
||||
AccountPostState, ChainedCall, PdaSeed, ProgramId, ProgramInput, ProgramOutput,
|
||||
read_nssa_inputs,
|
||||
},
|
||||
};
|
||||
use risc0_zkvm::serde::to_vec;
|
||||
|
||||
/// Proxy for interacting with private PDAs via `auth_transfer`.
|
||||
///
|
||||
/// The `is_fund` flag selects the operating mode:
|
||||
///
|
||||
/// - `false` (Spend): `pre_states = [pda (authorized), recipient]`. Debits the PDA. The PDA-to-npk
|
||||
/// binding is established via `pda_seeds` in the chained call to `auth_transfer`.
|
||||
///
|
||||
/// - `true` (Fund): `pre_states = [sender (authorized), pda (foreign/uninitialized)]`. Credits the
|
||||
/// PDA. A direct call to `auth_transfer` cannot bind the PDA because `auth_transfer` uses
|
||||
/// `Claim::Authorized`, not `Claim::Pda`. Routing through this proxy establishes the binding via
|
||||
/// `pda_seeds` in the chained call.
|
||||
type Instruction = (PdaSeed, u128, ProgramId, bool);
|
||||
|
||||
fn main() {
|
||||
let (
|
||||
ProgramInput {
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
pre_states,
|
||||
instruction: (seed, amount, auth_transfer_id, is_fund),
|
||||
},
|
||||
instruction_words,
|
||||
) = read_nssa_inputs::<Instruction>();
|
||||
|
||||
let Ok([first, second]) = <[_; 2]>::try_from(pre_states) else {
|
||||
return;
|
||||
};
|
||||
|
||||
assert!(first.is_authorized, "first pre_state must be authorized");
|
||||
|
||||
let chained_pre_states = if is_fund {
|
||||
let pda_authorized = AccountWithMetadata {
|
||||
account: second.account.clone(),
|
||||
account_id: second.account_id,
|
||||
is_authorized: true,
|
||||
};
|
||||
vec![first.clone(), pda_authorized]
|
||||
} else {
|
||||
vec![first.clone(), second.clone()]
|
||||
};
|
||||
|
||||
let first_post = AccountPostState::new(first.account.clone());
|
||||
let second_post = AccountPostState::new(second.account.clone());
|
||||
|
||||
let chained_call = ChainedCall {
|
||||
program_id: auth_transfer_id,
|
||||
instruction_data: to_vec(&amount).unwrap(),
|
||||
pre_states: chained_pre_states,
|
||||
pda_seeds: vec![seed],
|
||||
};
|
||||
|
||||
ProgramOutput::new(
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
instruction_words,
|
||||
vec![first, second],
|
||||
vec![first_post, second_post],
|
||||
)
|
||||
.with_chained_calls(vec![chained_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();
|
||||
}
|
||||
}
|
||||
@ -103,7 +103,10 @@ pub struct PrivateAccountPrivateInitialData {
|
||||
impl PrivateAccountPrivateInitialData {
|
||||
#[must_use]
|
||||
pub fn account_id(&self) -> nssa::AccountId {
|
||||
nssa::AccountId::from((&self.key_chain.nullifier_public_key, self.identifier))
|
||||
nssa::AccountId::for_regular_private_account(
|
||||
&self.key_chain.nullifier_public_key,
|
||||
self.identifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -208,7 +211,7 @@ pub fn initial_state() -> V03State {
|
||||
.iter()
|
||||
.map(|init_comm_data| {
|
||||
let npk = &init_comm_data.npk;
|
||||
let account_id = nssa::AccountId::from((npk, 0));
|
||||
let account_id = nssa::AccountId::for_regular_private_account(npk, 0);
|
||||
|
||||
let mut acc = init_comm_data.account.clone();
|
||||
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -11,6 +11,7 @@ use key_protocol::{
|
||||
};
|
||||
use log::debug;
|
||||
use nssa::program::Program;
|
||||
use nssa_core::PrivateAccountKind;
|
||||
|
||||
use crate::config::{InitialAccountData, Label, PersistentAccountData, WalletConfig};
|
||||
|
||||
@ -72,8 +73,8 @@ impl WalletChainStore {
|
||||
PersistentAccountData::Private(data) => {
|
||||
let npk = data.data.value.0.nullifier_public_key;
|
||||
let chain_index = data.chain_index;
|
||||
for identifier in &data.identifiers {
|
||||
let account_id = nssa::AccountId::from((&npk, *identifier));
|
||||
for kind in &data.kinds {
|
||||
let account_id = nssa::AccountId::for_private_account(&npk, kind);
|
||||
private_tree
|
||||
.account_id_map
|
||||
.insert(account_id, chain_index.clone());
|
||||
@ -89,7 +90,10 @@ impl WalletChainStore {
|
||||
data.account_id(),
|
||||
UserPrivateAccountData {
|
||||
key_chain: data.key_chain,
|
||||
accounts: vec![(data.identifier, data.account)],
|
||||
accounts: vec![(
|
||||
PrivateAccountKind::Regular(data.identifier),
|
||||
data.account,
|
||||
)],
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -135,7 +139,7 @@ impl WalletChainStore {
|
||||
account_id,
|
||||
UserPrivateAccountData {
|
||||
key_chain: data.key_chain,
|
||||
accounts: vec![(data.identifier, account)],
|
||||
accounts: vec![(PrivateAccountKind::Regular(data.identifier), account)],
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -190,7 +194,7 @@ impl WalletChainStore {
|
||||
pub fn insert_private_account_data(
|
||||
&mut self,
|
||||
account_id: nssa::AccountId,
|
||||
identifier: nssa_core::Identifier,
|
||||
kind: &PrivateAccountKind,
|
||||
account: nssa_core::account::Account,
|
||||
) {
|
||||
debug!("inserting at address {account_id}, this account {account:?}");
|
||||
@ -202,10 +206,10 @@ impl WalletChainStore {
|
||||
.entry(account_id)
|
||||
{
|
||||
let entry = entry.get_mut();
|
||||
if let Some((_, acc)) = entry.accounts.iter_mut().find(|(id, _)| *id == identifier) {
|
||||
if let Some((_, acc)) = entry.accounts.iter_mut().find(|(k, _)| k == kind) {
|
||||
*acc = account;
|
||||
} else {
|
||||
entry.accounts.push((identifier, account));
|
||||
entry.accounts.push((kind.clone(), account));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@ -228,24 +232,21 @@ impl WalletChainStore {
|
||||
.key_map
|
||||
.get_mut(&chain_index)
|
||||
{
|
||||
if let Some((_, acc)) = node.value.1.iter_mut().find(|(id, _)| *id == identifier) {
|
||||
if let Some((_, acc)) = node.value.1.iter_mut().find(|(k, _)| k == kind) {
|
||||
*acc = account;
|
||||
} else {
|
||||
node.value.1.push((identifier, account));
|
||||
node.value.1.push((kind.clone(), account));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Node not yet in account_id_map — find it by checking all nodes
|
||||
for (ci, node) in &mut self.user_data.private_key_tree.key_map {
|
||||
let expected_id =
|
||||
nssa::AccountId::from((&node.value.0.nullifier_public_key, identifier));
|
||||
if expected_id == account_id {
|
||||
if let Some((_, acc)) =
|
||||
node.value.1.iter_mut().find(|(id, _)| *id == identifier)
|
||||
{
|
||||
let npk = &node.value.0.nullifier_public_key;
|
||||
if nssa::AccountId::for_private_account(npk, kind) == account_id {
|
||||
if let Some((_, acc)) = node.value.1.iter_mut().find(|(k, _)| k == kind) {
|
||||
*acc = account;
|
||||
} else {
|
||||
node.value.1.push((identifier, account));
|
||||
node.value.1.push((kind.clone(), account));
|
||||
}
|
||||
// Register in account_id_map
|
||||
self.user_data
|
||||
@ -291,7 +292,7 @@ mod tests {
|
||||
data: public_data,
|
||||
}),
|
||||
PersistentAccountData::Private(Box::new(PersistentAccountDataPrivate {
|
||||
identifiers: vec![],
|
||||
kinds: vec![],
|
||||
chain_index: ChainIndex::root(),
|
||||
data: private_data,
|
||||
})),
|
||||
|
||||
@ -92,6 +92,27 @@ 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>,
|
||||
#[arg(long, requires = "pda")]
|
||||
/// Identifier that diversifies this PDA within the (`program_id`, seed, npk) family.
|
||||
/// Defaults to a random value if not specified.
|
||||
identifier: Option<u128>,
|
||||
},
|
||||
/// Recommended for receiving from multiple senders: creates a key node (npk + vpk) without
|
||||
/// registering any account.
|
||||
PrivateAccountsKey {
|
||||
@ -183,9 +204,73 @@ impl WalletSubcommand for NewSubcommand {
|
||||
);
|
||||
|
||||
wallet_core.store_persistent_data().await?;
|
||||
|
||||
Ok(SubcommandReturnValue::RegisterAccount { account_id })
|
||||
}
|
||||
Self::PrivateGms {
|
||||
group,
|
||||
label,
|
||||
pda,
|
||||
seed,
|
||||
program_id,
|
||||
identifier,
|
||||
} => {
|
||||
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,
|
||||
identifier.unwrap_or_else(rand::random),
|
||||
)?
|
||||
} 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?
|
||||
}
|
||||
|
||||
@ -28,7 +28,7 @@ pub struct PersistentAccountDataPublic {
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PersistentAccountDataPrivate {
|
||||
pub identifiers: Vec<nssa_core::Identifier>,
|
||||
pub kinds: Vec<nssa_core::PrivateAccountKind>,
|
||||
pub chain_index: ChainIndex,
|
||||
pub data: ChildKeysPrivate,
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -166,10 +166,10 @@ pub fn produce_data_for_storage(
|
||||
}
|
||||
|
||||
for (chain_index, node) in &user_data.private_key_tree.key_map {
|
||||
let identifiers = node.value.1.iter().map(|(id, _)| *id).collect();
|
||||
let kinds = node.value.1.iter().map(|(kind, _)| kind.clone()).collect();
|
||||
vec_for_storage.push(
|
||||
PersistentAccountDataPrivate {
|
||||
identifiers,
|
||||
kinds,
|
||||
chain_index: chain_index.clone(),
|
||||
data: node.clone(),
|
||||
}
|
||||
@ -188,12 +188,12 @@ pub fn produce_data_for_storage(
|
||||
}
|
||||
|
||||
for entry in user_data.default_user_private_accounts.values() {
|
||||
for (identifier, account) in &entry.accounts {
|
||||
for (kind, account) in &entry.accounts {
|
||||
vec_for_storage.push(
|
||||
InitialAccountData::Private(Box::new(PrivateAccountPrivateInitialData {
|
||||
account: account.clone(),
|
||||
key_chain: entry.key_chain.clone(),
|
||||
identifier: *identifier,
|
||||
identifier: kind.identifier(),
|
||||
}))
|
||||
.into(),
|
||||
);
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user