mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-05-13 19:49:29 +00:00
feat: add basic commands for communicating with keycard
This commit is contained in:
parent
190c811f10
commit
04344f1fce
@ -13,6 +13,7 @@ ignore = [
|
||||
{ id = "RUSTSEC-2025-0055", reason = "`tracing-subscriber` v0.2.25 pulled in by ark-relations v0.4.0 - will be addressed before mainnet" },
|
||||
{ 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" },
|
||||
]
|
||||
yanked = "deny"
|
||||
unused-ignored-advisory = "deny"
|
||||
|
||||
150
Cargo.lock
generated
150
Cargo.lock
generated
@ -1303,7 +1303,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "befbfd072a8e81c02f8c507aefce431fe5e7d051f83d48a23ffc9b9fe5a11799"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"heck",
|
||||
"heck 0.5.0",
|
||||
"indexmap 2.13.0",
|
||||
"log",
|
||||
"proc-macro2",
|
||||
@ -1450,7 +1450,7 @@ version = "4.5.55"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
@ -1715,15 +1715,6 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core2"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpp_demangle"
|
||||
version = "0.4.5"
|
||||
@ -2192,7 +2183,7 @@ version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e92f10a49176cbffacaedabfaa11d51db1ea0f80a83c26e1873b43cd1742c24"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"proc-macro2-diagnostics",
|
||||
]
|
||||
@ -3016,6 +3007,12 @@ dependencies = [
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
@ -3569,6 +3566,15 @@ dependencies = [
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indoc"
|
||||
version = "2.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.4"
|
||||
@ -3888,7 +3894,7 @@ version = "0.26.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2da3f8ab5ce1bb124b6d082e62dffe997578ceaf0aeb9f3174a214589dc00f07"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"heck 0.5.0",
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -5123,6 +5129,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mempool"
|
||||
version = "0.1.0"
|
||||
@ -5249,11 +5264,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "multihash"
|
||||
version = "0.19.3"
|
||||
version = "0.19.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b430e7953c29dd6a09afc29ff0bb69c6e306329ee6794700aee27b76a1aea8d"
|
||||
checksum = "89ace881e3f514092ce9efbcb8f413d0ad9763860b828981c2de51ddc666936c"
|
||||
dependencies = [
|
||||
"core2",
|
||||
"no_std_io2",
|
||||
"unsigned-varint",
|
||||
]
|
||||
|
||||
@ -5314,6 +5329,15 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "no_std_io2"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a3564ce7035b1e4778d8cb6cacebb5d766b5e8fe5a75b9e441e33fb61a872c6"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "no_std_strings"
|
||||
version = "0.1.3"
|
||||
@ -5388,7 +5412,7 @@ version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -6134,7 +6158,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.11.0",
|
||||
"itertools 0.14.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
@ -6147,7 +6171,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.11.0",
|
||||
"itertools 0.14.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
@ -6185,6 +6209,69 @@ dependencies = [
|
||||
"parking_lot",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3"
|
||||
version = "0.21.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5e00b96a521718e08e03b1a622f01c8a8deb50719335de3f60b3b3950f069d8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"indoc",
|
||||
"libc",
|
||||
"memoffset",
|
||||
"parking_lot",
|
||||
"portable-atomic",
|
||||
"pyo3-build-config",
|
||||
"pyo3-ffi",
|
||||
"pyo3-macros",
|
||||
"unindent",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-build-config"
|
||||
version = "0.21.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7883df5835fafdad87c0d888b266c8ec0f4c9ca48a5bed6bbb592e8dedee1b50"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"target-lexicon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-ffi"
|
||||
version = "0.21.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01be5843dc60b916ab4dad1dca6d20b9b4e6ddc8e15f50c47fe6d85f1fb97403"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pyo3-build-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-macros"
|
||||
version = "0.21.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77b34069fc0682e11b31dbd10321cbf94808394c56fd996796ce45217dfac53c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"pyo3-macros-backend",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-macros-backend"
|
||||
version = "0.21.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08260721f32db5e1a5beae69a55553f56b99bd0e1c3e6e0a5e8851a9d0f5a85c"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"proc-macro2",
|
||||
"pyo3-build-config",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.9"
|
||||
@ -7132,7 +7219,7 @@ dependencies = [
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs 0.26.11",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -7143,9 +7230,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.10"
|
||||
version = "0.103.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef"
|
||||
checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
@ -7872,7 +7959,7 @@ version = "0.27.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
@ -7997,6 +8084,12 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.12.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.26.0"
|
||||
@ -8845,6 +8938,12 @@ version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "unindent"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
|
||||
|
||||
[[package]]
|
||||
name = "unit-prefix"
|
||||
version = "0.5.2"
|
||||
@ -9012,6 +9111,7 @@ dependencies = [
|
||||
"nssa",
|
||||
"nssa_core",
|
||||
"optfield",
|
||||
"pyo3",
|
||||
"rand 0.8.5",
|
||||
"sequencer_service_rpc",
|
||||
"serde",
|
||||
@ -9551,7 +9651,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"heck 0.5.0",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
@ -9562,7 +9662,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"heck 0.5.0",
|
||||
"indexmap 2.13.0",
|
||||
"prettyplease",
|
||||
"syn 2.0.117",
|
||||
|
||||
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.
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.
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.
Binary file not shown.
@ -382,8 +382,8 @@ impl ProgramOutput {
|
||||
}
|
||||
|
||||
/// Representation of a number as `lo + hi * 2^128`.
|
||||
#[derive(PartialEq, Eq)]
|
||||
struct WrappedBalanceSum {
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct WrappedBalanceSum {
|
||||
lo: u128,
|
||||
hi: u128,
|
||||
}
|
||||
@ -393,7 +393,7 @@ impl WrappedBalanceSum {
|
||||
///
|
||||
/// Returns [`None`] if balance sum overflows `lo + hi * 2^128` representation, which is not
|
||||
/// expected in practical scenarios.
|
||||
fn from_balances(balances: impl Iterator<Item = u128>) -> Option<Self> {
|
||||
pub fn from_balances(balances: impl Iterator<Item = u128>) -> Option<Self> {
|
||||
let mut wrapped = Self { lo: 0, hi: 0 };
|
||||
|
||||
for balance in balances {
|
||||
@ -408,6 +408,75 @@ impl WrappedBalanceSum {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for WrappedBalanceSum {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if self.hi == 0 {
|
||||
write!(f, "{}", self.lo)
|
||||
} else {
|
||||
write!(f, "{} * 2^128 + {}", self.hi, self.lo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u128> for WrappedBalanceSum {
|
||||
fn from(value: u128) -> Self {
|
||||
Self { lo: value, hi: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ExecutionValidationError {
|
||||
#[error("Pre-state account IDs are not unique")]
|
||||
PreStateAccountIdsNotUnique,
|
||||
|
||||
#[error(
|
||||
"Pre-state and post-state lengths do not match: pre-state length {pre_state_length}, post-state length {post_state_length}"
|
||||
)]
|
||||
MismatchedPreStatePostStateLength {
|
||||
pre_state_length: usize,
|
||||
post_state_length: usize,
|
||||
},
|
||||
|
||||
#[error("Unallowed modification of nonce for account {account_id}")]
|
||||
ModifiedNonce { account_id: AccountId },
|
||||
|
||||
#[error("Unallowed modification of program owner for account {account_id}")]
|
||||
ModifiedProgramOwner { account_id: AccountId },
|
||||
|
||||
#[error(
|
||||
"Trying to decrease balance of account {account_id} owned by {owner_program_id:?} in a program {executing_program_id:?} which is not the owner"
|
||||
)]
|
||||
UnauthorizedBalanceDecrease {
|
||||
account_id: AccountId,
|
||||
owner_program_id: ProgramId,
|
||||
executing_program_id: ProgramId,
|
||||
},
|
||||
|
||||
#[error(
|
||||
"Unauthorized modification of data for account {account_id} which is not default and not owned by executing program {executing_program_id:?}"
|
||||
)]
|
||||
UnauthorizedDataModification {
|
||||
account_id: AccountId,
|
||||
executing_program_id: ProgramId,
|
||||
},
|
||||
|
||||
#[error(
|
||||
"Post-state for account {account_id} has default program owner but pre-state was not default"
|
||||
)]
|
||||
NonDefaultAccountWithDefaultOwner { account_id: AccountId },
|
||||
|
||||
#[error("Total balance across accounts overflowed 2^256 - 1")]
|
||||
BalanceSumOverflow,
|
||||
|
||||
#[error(
|
||||
"Total balance across accounts is not preserved: total balance in pre-states {total_balance_pre_states}, total balance in post-states {total_balance_post_states}"
|
||||
)]
|
||||
MismatchedTotalBalance {
|
||||
total_balance_pre_states: WrappedBalanceSum,
|
||||
total_balance_post_states: WrappedBalanceSum,
|
||||
},
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn compute_authorized_pdas(
|
||||
caller_program_id: Option<ProgramId>,
|
||||
@ -448,31 +517,39 @@ pub fn read_nssa_inputs<T: DeserializeOwned>() -> (ProgramInput<T>, InstructionD
|
||||
/// - `pre_states`: The list of input accounts, each annotated with authorization metadata.
|
||||
/// - `post_states`: The list of resulting accounts after executing the program logic.
|
||||
/// - `executing_program_id`: The identifier of the program that was executed.
|
||||
#[must_use]
|
||||
pub fn validate_execution(
|
||||
pre_states: &[AccountWithMetadata],
|
||||
post_states: &[AccountPostState],
|
||||
executing_program_id: ProgramId,
|
||||
) -> bool {
|
||||
) -> Result<(), ExecutionValidationError> {
|
||||
// 1. Check account ids are all different
|
||||
if !validate_uniqueness_of_account_ids(pre_states) {
|
||||
return false;
|
||||
return Err(ExecutionValidationError::PreStateAccountIdsNotUnique);
|
||||
}
|
||||
|
||||
// 2. Lengths must match
|
||||
if pre_states.len() != post_states.len() {
|
||||
return false;
|
||||
return Err(
|
||||
ExecutionValidationError::MismatchedPreStatePostStateLength {
|
||||
pre_state_length: pre_states.len(),
|
||||
post_state_length: post_states.len(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
for (pre, post) in pre_states.iter().zip(post_states) {
|
||||
// 3. Nonce must remain unchanged
|
||||
if pre.account.nonce != post.account.nonce {
|
||||
return false;
|
||||
return Err(ExecutionValidationError::ModifiedNonce {
|
||||
account_id: pre.account_id,
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Program ownership changes are not allowed
|
||||
if pre.account.program_owner != post.account.program_owner {
|
||||
return false;
|
||||
return Err(ExecutionValidationError::ModifiedProgramOwner {
|
||||
account_id: pre.account_id,
|
||||
});
|
||||
}
|
||||
|
||||
let account_program_owner = pre.account.program_owner;
|
||||
@ -481,7 +558,11 @@ pub fn validate_execution(
|
||||
if post.account.balance < pre.account.balance
|
||||
&& account_program_owner != executing_program_id
|
||||
{
|
||||
return false;
|
||||
return Err(ExecutionValidationError::UnauthorizedBalanceDecrease {
|
||||
account_id: pre.account_id,
|
||||
owner_program_id: account_program_owner,
|
||||
executing_program_id,
|
||||
});
|
||||
}
|
||||
|
||||
// 6. Data changes only allowed if owned by executing program or if account pre state has
|
||||
@ -490,13 +571,20 @@ pub fn validate_execution(
|
||||
&& pre.account != Account::default()
|
||||
&& account_program_owner != executing_program_id
|
||||
{
|
||||
return false;
|
||||
return Err(ExecutionValidationError::UnauthorizedDataModification {
|
||||
account_id: pre.account_id,
|
||||
executing_program_id,
|
||||
});
|
||||
}
|
||||
|
||||
// 7. If a post state has default program owner, the pre state must have been a default
|
||||
// account
|
||||
if post.account.program_owner == DEFAULT_PROGRAM_ID && pre.account != Account::default() {
|
||||
return false;
|
||||
return Err(
|
||||
ExecutionValidationError::NonDefaultAccountWithDefaultOwner {
|
||||
account_id: pre.account_id,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -505,20 +593,23 @@ pub fn validate_execution(
|
||||
let Some(total_balance_pre_states) =
|
||||
WrappedBalanceSum::from_balances(pre_states.iter().map(|pre| pre.account.balance))
|
||||
else {
|
||||
return false;
|
||||
return Err(ExecutionValidationError::BalanceSumOverflow);
|
||||
};
|
||||
|
||||
let Some(total_balance_post_states) =
|
||||
WrappedBalanceSum::from_balances(post_states.iter().map(|post| post.account.balance))
|
||||
else {
|
||||
return false;
|
||||
return Err(ExecutionValidationError::BalanceSumOverflow);
|
||||
};
|
||||
|
||||
if total_balance_pre_states != total_balance_post_states {
|
||||
return false;
|
||||
return Err(ExecutionValidationError::MismatchedTotalBalance {
|
||||
total_balance_pre_states,
|
||||
total_balance_post_states,
|
||||
});
|
||||
}
|
||||
|
||||
true
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_uniqueness_of_account_ids(pre_states: &[AccountWithMetadata]) -> bool {
|
||||
|
||||
@ -1,12 +1,16 @@
|
||||
use std::io;
|
||||
|
||||
use nssa_core::{
|
||||
account::{Account, AccountId},
|
||||
program::ProgramId,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! ensure {
|
||||
($cond:expr, $err:expr) => {
|
||||
if !$cond {
|
||||
return Err($err);
|
||||
return Err($err.into());
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -17,7 +21,7 @@ pub enum NssaError {
|
||||
InvalidInput(String),
|
||||
|
||||
#[error("Program violated execution rules")]
|
||||
InvalidProgramBehavior,
|
||||
InvalidProgramBehavior(#[from] InvalidProgramBehaviorError),
|
||||
|
||||
#[error("Serialization error: {0}")]
|
||||
InstructionSerializationError(String),
|
||||
@ -32,15 +36,15 @@ pub enum NssaError {
|
||||
InvalidPublicKey(#[source] k256::schnorr::Error),
|
||||
|
||||
#[error("Invalid hex for public key")]
|
||||
InvalidHexPublicKey(hex::FromHexError),
|
||||
InvalidHexPublicKey(#[source] hex::FromHexError),
|
||||
|
||||
#[error("Risc0 error: {0}")]
|
||||
#[error("Failed to write program input: {0}")]
|
||||
ProgramWriteInputFailed(String),
|
||||
|
||||
#[error("Risc0 error: {0}")]
|
||||
#[error("Failed to execute program: {0}")]
|
||||
ProgramExecutionFailed(String),
|
||||
|
||||
#[error("Risc0 error: {0}")]
|
||||
#[error("Failed to prove program: {0}")]
|
||||
ProgramProveFailed(String),
|
||||
|
||||
#[error("Invalid transaction: {0}")]
|
||||
@ -77,6 +81,61 @@ pub enum NssaError {
|
||||
OutOfValidityWindow,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum InvalidProgramBehaviorError {
|
||||
#[error(
|
||||
"Inconsistent pre-state for account {account_id} : expected {expected:?}, actual {actual:?}"
|
||||
)]
|
||||
InconsistentAccountPreState {
|
||||
account_id: AccountId,
|
||||
// Boxed to reduce the size of the error type
|
||||
expected: Box<Account>,
|
||||
actual: Box<Account>,
|
||||
},
|
||||
|
||||
#[error(
|
||||
"Inconsistent authorization for account {account_id} : expected {expected_authorization}, actual {actual_authorization}"
|
||||
)]
|
||||
InconsistentAccountAuthorization {
|
||||
account_id: AccountId,
|
||||
expected_authorization: bool,
|
||||
actual_authorization: bool,
|
||||
},
|
||||
|
||||
#[error("Program ID mismatch: expected {expected:?}, actual {actual:?}")]
|
||||
MismatchedProgramId {
|
||||
expected: ProgramId,
|
||||
actual: ProgramId,
|
||||
},
|
||||
|
||||
#[error("Caller program ID mismatch: expected {expected:?}, actual {actual:?}")]
|
||||
MismatchedCallerProgramId {
|
||||
expected: Option<ProgramId>,
|
||||
actual: Option<ProgramId>,
|
||||
},
|
||||
|
||||
#[error(transparent)]
|
||||
ExecutionValidationFailed(#[from] nssa_core::program::ExecutionValidationError),
|
||||
|
||||
#[error("Trying to claim account {account_id} which is not default")]
|
||||
ClaimedNonDefaultAccount { account_id: AccountId },
|
||||
|
||||
#[error("Trying to claim account {account_id} which is not authorized")]
|
||||
ClaimedUnauthorizedAccount { account_id: AccountId },
|
||||
|
||||
#[error("PDA claim mismatch: expected {expected:?}, actual {actual:?}")]
|
||||
MismatchedPdaClaim {
|
||||
expected: AccountId,
|
||||
actual: AccountId,
|
||||
},
|
||||
|
||||
#[error("Default account {account_id} was modified without being claimed")]
|
||||
DefaultAccountModifiedWithoutClaim { account_id: AccountId },
|
||||
|
||||
#[error("Called program {program_id:?} which is not listed in dependencies")]
|
||||
UndeclaredProgramDependency { program_id: ProgramId },
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ use nssa_core::{
|
||||
use risc0_zkvm::{ExecutorEnv, InnerReceipt, ProverOpts, Receipt, default_prover};
|
||||
|
||||
use crate::{
|
||||
error::NssaError,
|
||||
error::{InvalidProgramBehaviorError, NssaError},
|
||||
program::Program,
|
||||
program_methods::{PRIVACY_PRESERVING_CIRCUIT_ELF, PRIVACY_PRESERVING_CIRCUIT_ID},
|
||||
state::MAX_NUMBER_CHAINED_CALLS,
|
||||
@ -113,9 +113,11 @@ pub fn execute_and_prove(
|
||||
env_builder.add_assumption(inner_receipt);
|
||||
|
||||
for new_call in program_output.chained_calls.into_iter().rev() {
|
||||
let next_program = dependencies
|
||||
.get(&new_call.program_id)
|
||||
.ok_or(NssaError::InvalidProgramBehavior)?;
|
||||
let next_program = dependencies.get(&new_call.program_id).ok_or(
|
||||
InvalidProgramBehaviorError::UndeclaredProgramDependency {
|
||||
program_id: new_call.program_id,
|
||||
},
|
||||
)?;
|
||||
chained_calls.push_front((new_call, next_program, Some(chained_call.program_id)));
|
||||
}
|
||||
|
||||
|
||||
@ -366,12 +366,15 @@ pub mod tests {
|
||||
Timestamp,
|
||||
account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data},
|
||||
encryption::{EphemeralPublicKey, Scalar, ViewingPublicKey},
|
||||
program::{BlockValidityWindow, PdaSeed, ProgramId, TimestampValidityWindow},
|
||||
program::{
|
||||
BlockValidityWindow, ExecutionValidationError, PdaSeed, ProgramId,
|
||||
TimestampValidityWindow, WrappedBalanceSum,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
PublicKey, PublicTransaction, V03State,
|
||||
error::NssaError,
|
||||
error::{InvalidProgramBehaviorError, NssaError},
|
||||
execute_and_prove,
|
||||
privacy_preserving_transaction::{
|
||||
PrivacyPreservingTransaction,
|
||||
@ -933,10 +936,11 @@ pub mod tests {
|
||||
|
||||
#[test]
|
||||
fn program_should_fail_if_modifies_nonces() {
|
||||
let initial_data = [(AccountId::new([1; 32]), 100)];
|
||||
let account_id = AccountId::new([1; 32]);
|
||||
let initial_data = [(account_id, 100)];
|
||||
let mut state =
|
||||
V03State::new_with_genesis_accounts(&initial_data, vec![], 0).with_test_programs();
|
||||
let account_ids = vec![AccountId::new([1; 32])];
|
||||
let account_ids = vec![account_id];
|
||||
let program_id = Program::nonce_changer_program().id();
|
||||
let message =
|
||||
public_transaction::Message::try_new(program_id, account_ids, vec![], ()).unwrap();
|
||||
@ -945,7 +949,14 @@ pub mod tests {
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(NssaError::InvalidProgramBehavior(
|
||||
InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::ModifiedNonce { account_id: err_account_id }
|
||||
)
|
||||
)) if err_account_id == account_id
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -962,7 +973,17 @@ pub mod tests {
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(NssaError::InvalidProgramBehavior(
|
||||
InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::MismatchedPreStatePostStateLength {
|
||||
pre_state_length,
|
||||
post_state_length
|
||||
}
|
||||
)
|
||||
)) if pre_state_length == 1 && post_state_length == 2
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -979,7 +1000,17 @@ pub mod tests {
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(NssaError::InvalidProgramBehavior(
|
||||
InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::MismatchedPreStatePostStateLength {
|
||||
pre_state_length,
|
||||
post_state_length
|
||||
}
|
||||
)
|
||||
)) if pre_state_length == 2 && post_state_length == 1
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1003,7 +1034,12 @@ pub mod tests {
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(NssaError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::ModifiedProgramOwner { account_id: err_account_id }
|
||||
))) if err_account_id == account_id
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1027,7 +1063,12 @@ pub mod tests {
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(NssaError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::ModifiedProgramOwner { account_id: err_account_id }
|
||||
))) if err_account_id == account_id
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1051,7 +1092,12 @@ pub mod tests {
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(NssaError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::ModifiedProgramOwner { account_id: err_account_id }
|
||||
))) if err_account_id == account_id
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1075,16 +1121,21 @@ pub mod tests {
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(NssaError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::ModifiedProgramOwner { account_id: err_account_id }
|
||||
))) if err_account_id == account_id
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_should_fail_if_transfers_balance_from_non_owned_account() {
|
||||
let initial_data = [(AccountId::new([1; 32]), 100)];
|
||||
let mut state =
|
||||
V03State::new_with_genesis_accounts(&initial_data, vec![], 0).with_test_programs();
|
||||
let sender_account_id = AccountId::new([1; 32]);
|
||||
let receiver_account_id = AccountId::new([2; 32]);
|
||||
let initial_data = [(sender_account_id, 100)];
|
||||
let mut state =
|
||||
V03State::new_with_genesis_accounts(&initial_data, vec![], 0).with_test_programs();
|
||||
let balance_to_move: u128 = 1;
|
||||
let program_id = Program::simple_balance_transfer().id();
|
||||
assert_ne!(
|
||||
@ -1103,7 +1154,12 @@ pub mod tests {
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(NssaError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::UnauthorizedBalanceDecrease { account_id: err_account_id, owner_program_id, executing_program_id }
|
||||
))) if err_account_id == sender_account_id && owner_program_id != program_id && executing_program_id == program_id
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1128,7 +1184,12 @@ pub mod tests {
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(NssaError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::UnauthorizedDataModification { account_id: err_account_id, executing_program_id }
|
||||
))) if err_account_id == account_id && executing_program_id == program_id
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1146,7 +1207,12 @@ pub mod tests {
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(NssaError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::MismatchedTotalBalance { total_balance_pre_states, total_balance_post_states }
|
||||
))) if total_balance_pre_states == 0.into() && total_balance_post_states == 1.into()
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1175,7 +1241,12 @@ pub mod tests {
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(NssaError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::MismatchedTotalBalance { total_balance_pre_states, total_balance_post_states }
|
||||
))) if total_balance_pre_states == 100.into() && total_balance_post_states == 99.into()
|
||||
));
|
||||
}
|
||||
|
||||
fn test_public_account_keys_1() -> TestPublicKeys {
|
||||
@ -3088,7 +3159,12 @@ pub mod tests {
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(NssaError::InvalidProgramBehavior(
|
||||
InvalidProgramBehaviorError::ClaimedNonDefaultAccount { account_id: err_account_id }
|
||||
)) if err_account_id == account_id
|
||||
));
|
||||
}
|
||||
|
||||
/// This test ensures that even if a malicious program tries to perform overflow of balances
|
||||
@ -3134,7 +3210,22 @@ pub mod tests {
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&sender_key]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
let res = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
assert!(matches!(res, Err(NssaError::InvalidProgramBehavior)));
|
||||
let expected_total_balance_pre_states = WrappedBalanceSum::from_balances(
|
||||
[sender_init_balance, recipient_init_balance].into_iter(),
|
||||
)
|
||||
.unwrap();
|
||||
let expected_total_balance_post_states = WrappedBalanceSum::from_balances(
|
||||
[sender_init_balance, recipient_init_balance, u128::MAX, 1].into_iter(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
res,
|
||||
Err(NssaError::InvalidProgramBehavior(
|
||||
InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::MismatchedTotalBalance { total_balance_pre_states, total_balance_post_states }
|
||||
)
|
||||
)) if total_balance_pre_states == expected_total_balance_pre_states && total_balance_post_states == expected_total_balance_post_states
|
||||
));
|
||||
|
||||
let sender_post = state.get_account_by_id(sender_id);
|
||||
let recipient_post = state.get_account_by_id(recipient_id);
|
||||
@ -3379,7 +3470,14 @@ pub mod tests {
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
// Should fail - cannot modify data without claiming the account
|
||||
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(NssaError::InvalidProgramBehavior(
|
||||
InvalidProgramBehaviorError::DefaultAccountModifiedWithoutClaim {
|
||||
account_id: err_account_id
|
||||
}
|
||||
)) if err_account_id == account_id
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@ -14,7 +14,7 @@ use nssa_core::{
|
||||
|
||||
use crate::{
|
||||
V03State, ensure,
|
||||
error::NssaError,
|
||||
error::{InvalidProgramBehaviorError, NssaError},
|
||||
privacy_preserving_transaction::{
|
||||
PrivacyPreservingTransaction, circuit::Proof, message::Message,
|
||||
},
|
||||
@ -145,39 +145,52 @@ impl ValidatedStateDiff {
|
||||
.unwrap_or_else(|| state.get_account_by_id(account_id));
|
||||
ensure!(
|
||||
pre.account == expected_pre,
|
||||
NssaError::InvalidProgramBehavior
|
||||
InvalidProgramBehaviorError::InconsistentAccountPreState {
|
||||
account_id,
|
||||
expected: Box::new(expected_pre),
|
||||
actual: Box::new(pre.account.clone())
|
||||
}
|
||||
);
|
||||
|
||||
// Check that authorization flags are consistent with the provided ones or
|
||||
// authorized by program through the PDA mechanism
|
||||
let expected_is_authorized = is_authorized(&account_id);
|
||||
ensure!(
|
||||
pre.is_authorized == is_authorized(&account_id),
|
||||
NssaError::InvalidProgramBehavior
|
||||
pre.is_authorized == expected_is_authorized,
|
||||
InvalidProgramBehaviorError::InconsistentAccountAuthorization {
|
||||
account_id,
|
||||
expected_authorization: expected_is_authorized,
|
||||
actual_authorization: pre.is_authorized
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Verify that the program output's self_program_id matches the expected program ID.
|
||||
ensure!(
|
||||
program_output.self_program_id == chained_call.program_id,
|
||||
NssaError::InvalidProgramBehavior
|
||||
InvalidProgramBehaviorError::MismatchedProgramId {
|
||||
expected: chained_call.program_id,
|
||||
actual: program_output.self_program_id
|
||||
}
|
||||
);
|
||||
|
||||
// Verify that the program output's caller_program_id matches the actual caller.
|
||||
ensure!(
|
||||
program_output.caller_program_id == caller_program_id,
|
||||
NssaError::InvalidProgramBehavior
|
||||
InvalidProgramBehaviorError::MismatchedCallerProgramId {
|
||||
expected: caller_program_id,
|
||||
actual: program_output.caller_program_id,
|
||||
}
|
||||
);
|
||||
|
||||
// Verify execution corresponds to a well-behaved program.
|
||||
// See the # Programs section for the definition of the `validate_execution` method.
|
||||
ensure!(
|
||||
validate_execution(
|
||||
&program_output.pre_states,
|
||||
&program_output.post_states,
|
||||
chained_call.program_id,
|
||||
),
|
||||
NssaError::InvalidProgramBehavior
|
||||
);
|
||||
validate_execution(
|
||||
&program_output.pre_states,
|
||||
&program_output.post_states,
|
||||
chained_call.program_id,
|
||||
)
|
||||
.map_err(InvalidProgramBehaviorError::ExecutionValidationFailed)?;
|
||||
|
||||
// Verify validity window
|
||||
ensure!(
|
||||
@ -192,27 +205,33 @@ impl ValidatedStateDiff {
|
||||
let Some(claim) = post.required_claim() else {
|
||||
continue;
|
||||
};
|
||||
let account_id = program_output.pre_states[i].account_id;
|
||||
|
||||
// The invoked program can only claim accounts with default program id.
|
||||
ensure!(
|
||||
post.account().program_owner == DEFAULT_PROGRAM_ID,
|
||||
NssaError::InvalidProgramBehavior
|
||||
InvalidProgramBehaviorError::ClaimedNonDefaultAccount { account_id }
|
||||
);
|
||||
|
||||
let account_id = program_output.pre_states[i].account_id;
|
||||
|
||||
match claim {
|
||||
Claim::Authorized => {
|
||||
// The program can only claim accounts that were authorized by the signer.
|
||||
ensure!(
|
||||
is_authorized(&account_id),
|
||||
NssaError::InvalidProgramBehavior
|
||||
InvalidProgramBehaviorError::ClaimedUnauthorizedAccount { account_id }
|
||||
);
|
||||
}
|
||||
Claim::Pda(seed) => {
|
||||
// The program can only claim accounts that correspond to the PDAs it is
|
||||
// authorized to claim.
|
||||
let pda = AccountId::from((&chained_call.program_id, &seed));
|
||||
ensure!(account_id == pda, NssaError::InvalidProgramBehavior);
|
||||
ensure!(
|
||||
account_id == pda,
|
||||
InvalidProgramBehaviorError::MismatchedPdaClaim {
|
||||
expected: pda,
|
||||
actual: account_id
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -238,7 +257,7 @@ impl ValidatedStateDiff {
|
||||
}
|
||||
|
||||
// Check that all modified uninitialized accounts where claimed
|
||||
for post in state_diff.iter().filter_map(|(account_id, post)| {
|
||||
for (account_id, post) in state_diff.iter().filter_map(|(account_id, post)| {
|
||||
let pre = state.get_account_by_id(*account_id);
|
||||
if pre.program_owner != DEFAULT_PROGRAM_ID {
|
||||
return None;
|
||||
@ -246,11 +265,11 @@ impl ValidatedStateDiff {
|
||||
if pre == *post {
|
||||
return None;
|
||||
}
|
||||
Some(post)
|
||||
Some((*account_id, post))
|
||||
}) {
|
||||
ensure!(
|
||||
post.program_owner != DEFAULT_PROGRAM_ID,
|
||||
NssaError::InvalidProgramBehavior
|
||||
InvalidProgramBehaviorError::DefaultAccountModifiedWithoutClaim { account_id }
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -125,12 +125,17 @@ impl ExecutionState {
|
||||
|
||||
// Check that the program is well behaved.
|
||||
// See the # Programs section for the definition of the `validate_execution` method.
|
||||
let execution_valid = validate_execution(
|
||||
let validated_execution = validate_execution(
|
||||
&program_output.pre_states,
|
||||
&program_output.post_states,
|
||||
chained_call.program_id,
|
||||
);
|
||||
assert!(execution_valid, "Bad behaved program");
|
||||
if let Err(err) = validated_execution {
|
||||
panic!(
|
||||
"Invalid program behavior in program {:?}: {err}",
|
||||
chained_call.program_id
|
||||
);
|
||||
}
|
||||
|
||||
for next_call in program_output.chained_calls.iter().rev() {
|
||||
chained_calls.push_front((next_call.clone(), Some(chained_call.program_id)));
|
||||
|
||||
@ -18,6 +18,7 @@ amm_core.workspace = true
|
||||
testnet_initial_state.workspace = true
|
||||
ata_core.workspace = true
|
||||
bip39.workspace = true
|
||||
pyo3 = { version = "0.21", features = ["auto-initialize"] }
|
||||
|
||||
anyhow.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
199
wallet/src/cli/keycard.rs
Normal file
199
wallet/src/cli/keycard.rs
Normal file
@ -0,0 +1,199 @@
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use crate::{
|
||||
WalletCore,
|
||||
cli::{SubcommandReturnValue, WalletSubcommand, keycard_wallet::KeycardWallet, python_path},
|
||||
};
|
||||
|
||||
/// Represents generic chain CLI subcommand.
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
pub enum KeycardSubcommand {
|
||||
Available,
|
||||
Connect {
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
)]
|
||||
pin: Option<String>,
|
||||
},
|
||||
Load {
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
)]
|
||||
mnemonic: Option<String>,
|
||||
},
|
||||
Remove,
|
||||
}
|
||||
|
||||
/// Represents generic register CLI subcommand.
|
||||
/*
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
pub enum NewSubcommand {
|
||||
/// Register new public account.
|
||||
Public {
|
||||
#[arg(long)]
|
||||
/// Chain index of a parent node.
|
||||
cci: Option<ChainIndex>,
|
||||
#[arg(short, long)]
|
||||
/// Label to assign to the new account.
|
||||
label: Option<String>,
|
||||
},
|
||||
/// Register new private account.
|
||||
Private {
|
||||
#[arg(long)]
|
||||
/// Chain index of a parent node.
|
||||
cci: Option<ChainIndex>,
|
||||
#[arg(short, long)]
|
||||
/// Label to assign to the new account.
|
||||
label: Option<String>,
|
||||
},
|
||||
}
|
||||
*/
|
||||
/*
|
||||
impl WalletSubcommand for NewSubcommand {
|
||||
async fn handle_subcommand(
|
||||
self,
|
||||
wallet_core: &mut WalletCore,
|
||||
) -> Result<SubcommandReturnValue> {
|
||||
match self {
|
||||
Self::Public { cci, label } => {
|
||||
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 (account_id, chain_index) = wallet_core.create_new_account_public(cci);
|
||||
|
||||
let private_key = wallet_core
|
||||
.storage
|
||||
.user_data
|
||||
.get_pub_account_signing_key(account_id)
|
||||
.unwrap();
|
||||
|
||||
let public_key = PublicKey::new_from_private_key(private_key);
|
||||
|
||||
if let Some(label) = label {
|
||||
wallet_core
|
||||
.storage
|
||||
.labels
|
||||
.insert(account_id.to_string(), Label::new(label));
|
||||
}
|
||||
|
||||
println!(
|
||||
"Generated new account with account_id Public/{account_id} at path {chain_index}"
|
||||
);
|
||||
println!("With pk {}", hex::encode(public_key.value()));
|
||||
|
||||
wallet_core.store_persistent_data().await?;
|
||||
|
||||
Ok(SubcommandReturnValue::RegisterAccount { account_id })
|
||||
}
|
||||
Self::Private { cci, label } => {
|
||||
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 (account_id, chain_index) = wallet_core.create_new_account_private(cci);
|
||||
|
||||
if let Some(label) = label {
|
||||
wallet_core
|
||||
.storage
|
||||
.labels
|
||||
.insert(account_id.to_string(), Label::new(label));
|
||||
}
|
||||
|
||||
let (key, _) = wallet_core
|
||||
.storage
|
||||
.user_data
|
||||
.get_private_account(account_id)
|
||||
.unwrap();
|
||||
|
||||
println!(
|
||||
"Generated new account with account_id Private/{account_id} at path {chain_index}",
|
||||
);
|
||||
println!("With npk {}", hex::encode(key.nullifier_public_key.0));
|
||||
println!(
|
||||
"With vpk {}",
|
||||
hex::encode(key.viewing_public_key.to_bytes())
|
||||
);
|
||||
|
||||
wallet_core.store_persistent_data().await?;
|
||||
|
||||
Ok(SubcommandReturnValue::RegisterAccount { account_id })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
impl WalletSubcommand for KeycardSubcommand {
|
||||
#[expect(clippy::cognitive_complexity, reason = "TODO: fix later")]
|
||||
async fn handle_subcommand(
|
||||
self,
|
||||
_wallet_core: &mut WalletCore,
|
||||
) -> Result<SubcommandReturnValue> {
|
||||
match self {
|
||||
Self::Available => {
|
||||
Python::with_gil(|py| {
|
||||
python_path::add_python_path(py).expect("keycard_wallet.py not found");
|
||||
|
||||
let wallet = KeycardWallet::new(py).expect("Expect keycard wallet");
|
||||
|
||||
let _available = wallet.is_unpaired_keycard_available(py);
|
||||
});
|
||||
|
||||
Ok(SubcommandReturnValue::Empty)
|
||||
},
|
||||
Self::Connect { pin } => {
|
||||
// TODO This should be persistent.
|
||||
Python::with_gil(|py| {
|
||||
python_path::add_python_path(py).expect("keycard_wallet.py not found");
|
||||
|
||||
let wallet = KeycardWallet::new(py).expect("Expect keycard wallet");
|
||||
|
||||
let _ = wallet.setup_communication(py, pin.expect("TODO"));
|
||||
});
|
||||
|
||||
Ok(SubcommandReturnValue::Empty)
|
||||
},
|
||||
Self::Load { mnemonic } => {
|
||||
// TODO This should be persistent.
|
||||
Python::with_gil(|py| {
|
||||
python_path::add_python_path(py).expect("keycard_wallet.py not found");
|
||||
|
||||
let wallet = KeycardWallet::new(py).expect("Expect keycard wallet");
|
||||
|
||||
let _ = wallet.load_account_keys(py, &mnemonic.expect("TODO"));
|
||||
});
|
||||
|
||||
Ok(SubcommandReturnValue::Empty)
|
||||
},
|
||||
Self::Remove => {
|
||||
// TODO This should be persistent.
|
||||
Python::with_gil(|py| {
|
||||
python_path::add_python_path(py).expect("keycard_wallet.py not found");
|
||||
|
||||
let wallet = KeycardWallet::new(py).expect("Expect keycard wallet");
|
||||
|
||||
let _ = wallet.remove_account_keys(py);
|
||||
});
|
||||
|
||||
Ok(SubcommandReturnValue::Empty)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
123
wallet/src/cli/keycard_wallet.rs
Normal file
123
wallet/src/cli/keycard_wallet.rs
Normal file
@ -0,0 +1,123 @@
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::PyAny;
|
||||
|
||||
/// Rust wrapper around the Python KeycardWallet class.
|
||||
/// Holds a persistent Python object in memory.
|
||||
pub struct KeycardWallet {
|
||||
instance: Py<PyAny>,
|
||||
}
|
||||
|
||||
impl KeycardWallet {
|
||||
/// Create a new Python KeycardWallet instance
|
||||
pub fn new(py: Python) -> PyResult<Self> {
|
||||
let module = py.import_bound("keycard_wallet")?;
|
||||
let class = module.getattr("KeycardWallet")?;
|
||||
|
||||
let instance = class.call0()?;
|
||||
|
||||
Ok(Self {
|
||||
instance: instance.into_py(py),
|
||||
})
|
||||
}
|
||||
|
||||
/// Calls Python: is_unpaired_keycard_available()
|
||||
pub fn is_unpaired_keycard_available(&self, py: Python) -> PyResult<bool> {
|
||||
self.instance
|
||||
.bind(py) // replaces as_ref(py)
|
||||
.call_method0("is_unpaired_keycard_available")?
|
||||
.extract()
|
||||
}
|
||||
|
||||
pub fn setup_communication(&self, py: Python, pin: String) -> PyResult<bool> {
|
||||
let py_pin = pyo3::types::PyString::new_bound(py, &pin);
|
||||
|
||||
self.instance
|
||||
.bind(py)
|
||||
.call_method1("setup_communication", (py_pin,))?
|
||||
.extract()
|
||||
}
|
||||
|
||||
pub fn disconnect(&self, py: Python) -> PyResult<bool> {
|
||||
self.instance
|
||||
.bind(py)
|
||||
.call_method0("disconnect")?
|
||||
.extract()
|
||||
}
|
||||
|
||||
pub fn get_public_signing_key(&self, py: Python) -> PyResult<[u8; 32]> {
|
||||
self.instance
|
||||
.bind(py)
|
||||
.call_method0("get_public_signing_key")?
|
||||
.extract()
|
||||
}
|
||||
|
||||
pub fn derive_path(&self, py: Python, path: Vec<u32>) -> PyResult<()> {
|
||||
let path = Self::convert_path_to_string(path);
|
||||
|
||||
self.instance
|
||||
.bind(py)
|
||||
.call_method1("change_path", (path,))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn convert_path_to_string(path: Vec<u32>) -> String {
|
||||
format!(
|
||||
"m/{}",
|
||||
path.iter()
|
||||
.map(|n| n.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("'/")
|
||||
)
|
||||
}
|
||||
|
||||
pub fn sign_message_current_key(&self, py: Python, message: &[u8; 32]) -> PyResult<[u8; 64]> {
|
||||
let py_message = pyo3::types::PyBytes::new_bound(py, message);
|
||||
|
||||
let py_signature: Vec<u8> = self.instance
|
||||
.bind(py)
|
||||
.call_method1("sign_message_current_key", (py_message,))?
|
||||
.getattr("signature")? // or "bytes", "data", "value", etc.
|
||||
.extract()?;
|
||||
|
||||
let signature: [u8; 64] = py_signature
|
||||
.try_into()
|
||||
.map_err(|_| PyErr::new::<pyo3::exceptions::PyValueError, _>(
|
||||
"Expected signature of exactly 64 bytes"
|
||||
))?;
|
||||
|
||||
Ok(signature)
|
||||
}
|
||||
|
||||
pub fn sign_message_with_path(&self, py: Python, path: Vec<u32>, message: &[u8; 32]) -> PyResult<[u8; 64]> {
|
||||
let py_message = pyo3::types::PyBytes::new_bound(py, message);
|
||||
let path = Self::convert_path_to_string(path);
|
||||
|
||||
let py_signature: Vec<u8> = self.instance
|
||||
.bind(py)
|
||||
.call_method1("sign_message_with_path", (path, py_message))?
|
||||
.getattr("signature")? // or "bytes", "data", "value", etc.
|
||||
.extract()?;
|
||||
|
||||
let signature: [u8; 64] = py_signature
|
||||
.try_into()
|
||||
.map_err(|_| PyErr::new::<pyo3::exceptions::PyValueError, _>(
|
||||
"Expected signature of exactly 64 bytes"
|
||||
))?;
|
||||
|
||||
Ok(signature)
|
||||
}
|
||||
|
||||
pub fn remove_account_keys(&self, py: Python) -> PyResult<()> {
|
||||
self.instance
|
||||
.bind(py)
|
||||
.call_method0("remove_account_keys")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_account_keys(&self, py: Python, mnemonic: &str) -> PyResult<()> {
|
||||
self.instance
|
||||
.bind(py)
|
||||
.call_method1("load_account_keys", (mnemonic,))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -14,6 +14,7 @@ use crate::{
|
||||
account::AccountSubcommand,
|
||||
chain::ChainSubcommand,
|
||||
config::ConfigSubcommand,
|
||||
keycard::KeycardSubcommand,
|
||||
programs::{
|
||||
amm::AmmProgramAgnosticSubcommand, ata::AtaSubcommand,
|
||||
native_token_transfer::AuthTransferSubcommand, pinata::PinataProgramAgnosticSubcommand,
|
||||
@ -25,7 +26,10 @@ use crate::{
|
||||
pub mod account;
|
||||
pub mod chain;
|
||||
pub mod config;
|
||||
pub mod keycard;
|
||||
pub mod programs;
|
||||
pub mod python_path;
|
||||
pub mod keycard_wallet;
|
||||
|
||||
pub(crate) trait WalletSubcommand {
|
||||
async fn handle_subcommand(self, wallet_core: &mut WalletCore)
|
||||
@ -73,6 +77,8 @@ pub enum Command {
|
||||
},
|
||||
/// Deploy a program.
|
||||
DeployProgram { binary_filepath: PathBuf },
|
||||
#[command(subcommand)]
|
||||
Keycard(KeycardSubcommand),
|
||||
}
|
||||
|
||||
/// To execute commands, env var `NSSA_WALLET_HOME_DIR` must be set into directory with config.
|
||||
@ -121,6 +127,9 @@ pub async fn execute_subcommand(
|
||||
Command::Pinata(pinata_subcommand) => {
|
||||
pinata_subcommand.handle_subcommand(wallet_core).await?
|
||||
}
|
||||
Command::Keycard(keycard_subcommand) => {
|
||||
keycard_subcommand.handle_subcommand(wallet_core).await?
|
||||
}
|
||||
Command::CheckHealth => {
|
||||
let remote_program_ids = wallet_core
|
||||
.sequencer_client
|
||||
|
||||
43
wallet/src/cli/python/keycard_testing.py
Normal file
43
wallet/src/cli/python/keycard_testing.py
Normal file
@ -0,0 +1,43 @@
|
||||
import keycard_wallet as keycard_wallet
|
||||
import time # For testing
|
||||
|
||||
|
||||
|
||||
my_wallet = keycard_wallet.KeycardWallet()
|
||||
print("Setup communication with card...", my_wallet.setup_communication())
|
||||
|
||||
#my_wallet.load_account_keys()
|
||||
|
||||
#pub_key = my_wallet.get_public_signing_key()
|
||||
|
||||
# TODO: now I want to specify LEE
|
||||
#print(f"Public key: {list(pub_key)}")
|
||||
|
||||
#my_wallet.debug_key_export()
|
||||
#priv_key = my_wallet.get_private_signing_key()
|
||||
#print(f"Private key: {list(priv_key)}")
|
||||
|
||||
#my_wallet.remove_account_keys()
|
||||
|
||||
print("Disconnection", my_wallet.disconnect()) # To not do a stupid
|
||||
|
||||
"""
|
||||
my_wallet.setup_communication()
|
||||
|
||||
|
||||
|
||||
# TODO: issues here
|
||||
#priv_key = my_wallet.get_private_key()
|
||||
#print(f"Private key: {list(priv_key)}")
|
||||
|
||||
|
||||
#signature = my_wallet.sign_message_current_key()
|
||||
#print(f"Signature: {signature.signature.hex()}")
|
||||
|
||||
#signature = my_wallet.sign_message_with_path("m/44'/60'/0'/0/1")
|
||||
#print(f"Signature: {signature.signature.hex()}")
|
||||
|
||||
my_wallet.disconnect() # To not do a stupid
|
||||
"""
|
||||
|
||||
|
||||
189
wallet/src/cli/python/keycard_wallet.py
Normal file
189
wallet/src/cli/python/keycard_wallet.py
Normal file
@ -0,0 +1,189 @@
|
||||
from smartcard.System import readers
|
||||
from keycard.exceptions import APDUError, TransportError
|
||||
from ecdsa import VerifyingKey, SECP256k1
|
||||
|
||||
from keycard.keycard import KeyCard
|
||||
|
||||
from mnemonic import Mnemonic
|
||||
from keycard import constants
|
||||
|
||||
import keycard
|
||||
|
||||
PIN = '123456'
|
||||
DEFAULT_PAIRING_PASSWORD = "KeycardDefaultPairing"
|
||||
|
||||
class KeycardWallet:
|
||||
def __init__(self):
|
||||
self.card = KeyCard()
|
||||
self.pairing_index = None
|
||||
self.pairing_key = None
|
||||
|
||||
def _is_smart_card_reader_detected(self) -> bool:
|
||||
try:
|
||||
return len(readers()) > 0
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _is_keycard_detected(self) -> bool:
|
||||
try:
|
||||
KeyCard().select()
|
||||
return True
|
||||
except (TransportError, APDUError, Exception):
|
||||
# No readers, no card, or card doesn't respond.
|
||||
return False
|
||||
|
||||
# Wrapped
|
||||
def is_unpaired_keycard_available(self) -> bool:
|
||||
if not self._is_smart_card_reader_detected():
|
||||
return False
|
||||
elif not self._is_keycard_detected():
|
||||
return False
|
||||
return True
|
||||
|
||||
# Wrapped
|
||||
def setup_communication(self, pin = PIN, password = DEFAULT_PAIRING_PASSWORD) -> bool:
|
||||
try:
|
||||
self.card.select()
|
||||
|
||||
if not self.card.is_initialized:
|
||||
# TODO: need to be able to initialize a card.
|
||||
return False
|
||||
|
||||
if self.pairing_index is None:
|
||||
pairing_index, pairing_key = self.card.pair(password) #Testing
|
||||
self.pairing_index = pairing_index
|
||||
self.pairing_key = pairing_key
|
||||
|
||||
self.card.open_secure_channel(pairing_index, pairing_key)
|
||||
self.card.verify_pin(PIN)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
return False
|
||||
|
||||
"""
|
||||
# Needs to be more robust to handle card removal and reinsertion
|
||||
def is_selected_card_available(self) -> bool:
|
||||
if self.transport.connection is None:
|
||||
return False
|
||||
|
||||
try:
|
||||
#TODO: fix this up Try a lightweight operation
|
||||
# Card is present
|
||||
self.card.send_apdu(cla=0x00, ins=0xA4, p1=0x04, p2=0x00, data=b'')
|
||||
# return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
# TODO: attempt to prevent a new card from being inserted
|
||||
return self.card.is_selected
|
||||
|
||||
"""
|
||||
|
||||
# Wrapped
|
||||
def disconnect(self) -> bool:
|
||||
try:
|
||||
self.card.unpair(self.pairing_index)
|
||||
self.pairing_index = None
|
||||
self.pairing_key = None
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Error during disconnect: {e}")
|
||||
return False
|
||||
|
||||
# TODO: add path?
|
||||
# Wrapped
|
||||
def get_public_signing_key(self):
|
||||
uncompressed_pub_key = self.card.export_current_key(public_only=True).public_key
|
||||
|
||||
# Convert to VerifyingKey object
|
||||
vk = VerifyingKey.from_string(uncompressed_pub_key, curve=SECP256k1)
|
||||
|
||||
return vk.to_string("compressed")[1:]
|
||||
|
||||
"""
|
||||
# TODO: don't think this possible; blocked by firmware
|
||||
def get_private_signing_key(self):
|
||||
try:
|
||||
exported = self.card.export_current_key(public_only=False)
|
||||
print(f"Exported key: {exported}")
|
||||
print(f"Public key: {exported.public_key.hex() if exported.public_key else 'None'}")
|
||||
print(f"Private key: {exported.private_key.hex() if exported.private_key else 'None'}")
|
||||
print(f"Chain code: {exported.chain_code.hex() if exported.chain_code else 'None'}")
|
||||
|
||||
if exported.private_key is None:
|
||||
raise ValueError("No private key returned - key may not be loaded on card")
|
||||
|
||||
return exported.private_key
|
||||
except Exception as e:
|
||||
print(f"Error exporting key: {e}")
|
||||
raise
|
||||
"""
|
||||
# TODO: delete this function
|
||||
def debug_key_export(self):
|
||||
"""Debug why key export fails with SW=6985"""
|
||||
|
||||
# 1. Check if a key exists
|
||||
try:
|
||||
status = self.card.status
|
||||
print(f"Status: {status}")
|
||||
except Exception as e:
|
||||
print(f"Cannot get status: {e}")
|
||||
|
||||
# 2. Try public key export first
|
||||
try:
|
||||
exported = self.card.export_current_key(public_only=True)
|
||||
print(f"Public key export: {exported.public_key.hex() if exported.public_key else 'None'}")
|
||||
except Exception as e:
|
||||
print(f"Public key export failed: {e}")
|
||||
|
||||
# 3. Check if key needs to be generated
|
||||
try:
|
||||
key_uid = self. card.generate_key()
|
||||
print(f"Generated key UID: {key_uid.hex()}")
|
||||
except Exception as e:
|
||||
print(f"Key generation failed: {e}")
|
||||
|
||||
# 4. Try private export again
|
||||
try:
|
||||
exported = self.card.export_current_key(public_only=False)
|
||||
if exported.private_key:
|
||||
print(f"Private key: {exported.private_key.hex()}")
|
||||
else:
|
||||
print("Private key is None - key may not allow export")
|
||||
except Exception as e:
|
||||
print(f"Private key export failed: {e}")
|
||||
|
||||
#TODO: check well formed?
|
||||
# Wrapped
|
||||
def change_path(self, path):
|
||||
self.card.derive_key(path)
|
||||
|
||||
# Message must be 32 bytes
|
||||
# TODO: rename to current_path
|
||||
# Wrapped
|
||||
def sign_message_current_key(self, message = b"TestMessageMustBe32Bytes!\x00\x00\x00\x00\x00\x00\x00"):
|
||||
# Message must be sent bytes
|
||||
return self.card.sign(message, constants.SigningAlgorithm.SCHNORR_BIP340)
|
||||
|
||||
|
||||
# Does not update the path
|
||||
# Wrapped
|
||||
def sign_message_with_path(self, path, message = b"TestMessageMustBe32Bytes!\x00\x00\x00\x00\x00\x00\x00"):
|
||||
# must be sent bytes
|
||||
return self.card.sign_with_path(message, path, False, constants.SigningAlgorithm.SCHNORR_BIP340)
|
||||
|
||||
# Wrapped
|
||||
def remove_account_keys(self):
|
||||
self.card.remove_key()
|
||||
|
||||
# TODO: update to accept a different language?
|
||||
def load_account_keys(self, mnemonic) :
|
||||
mnemo = Mnemonic("english")
|
||||
seed = mnemo.to_seed(mnemonic, passphrase="")
|
||||
|
||||
# Load the seed onto the card
|
||||
result = self.card.load_key(
|
||||
key_type= constants.LoadKeyType.BIP39_SEED,
|
||||
lee_seed=seed
|
||||
)
|
||||
39
wallet/src/cli/python_path.rs
Normal file
39
wallet/src/cli/python_path.rs
Normal file
@ -0,0 +1,39 @@
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::PyList;
|
||||
|
||||
/// Adds the project's `python/` directory and venv site-packages to Python's sys.path
|
||||
pub fn add_python_path(py: Python) -> PyResult<()> {
|
||||
let current_dir = env::current_dir()
|
||||
.expect("Failed to get current working directory");
|
||||
|
||||
let paths_to_add: Vec<PathBuf> = vec![
|
||||
current_dir.join("python"),
|
||||
current_dir.join("python").join("keycard-py-lee-schnorr"), // ← add this
|
||||
//current_dir.join("python").join("keycard-py-lee-schnorr").join(".venv").join("Lib").join("site-packages"),
|
||||
];
|
||||
|
||||
// Sanity check — warns early if a path doesn't exist
|
||||
for path in &paths_to_add {
|
||||
if !path.exists() {
|
||||
eprintln!("Warning: Python path does not exist: {:?}", path);
|
||||
}
|
||||
}
|
||||
|
||||
let sys = py.import_bound("sys")?;
|
||||
let sys_path: &PyList = sys.getattr("path")?.extract()?;
|
||||
|
||||
for path in &paths_to_add {
|
||||
let path_str = path
|
||||
.to_str()
|
||||
.expect("Invalid path");
|
||||
|
||||
// Avoid duplicating the path
|
||||
if !sys_path.iter().any(|p| p.extract::<&str>().unwrap_or("") == path_str) {
|
||||
sys_path.insert(0, path_str)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user