Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ doesn't describe endpoints the released server lacks.
- `CONTRIBUTING.md`, expanded `.env.example`, and per-component READMEs.

### Fixed
- `POST /new_account` no longer intermittently returns a 500
(`NoAccountAccess`) when the RPC provider serves the follow-up wallet
registration's dry-run from a node that hasn't executed the just-mined
`newAccount` block; the propagation window is now retried with backoff.
Comment on lines +71 to +74
- Release builds no longer report a spurious `-modified` version suffix
(`.dockerignore` excluded a tracked file, dirtying `git describe` inside the
image build).
Expand Down
59 changes: 54 additions & 5 deletions k6/correctness/integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* k6 run k6/integration.spec.ts
* BASE_URL=https://your-instance.phala.network/core/v1 k6 run k6/integration.spec.ts
*/
import { sleep } from "k6";
import { checkAndLog, warnOnHttpFailures } from "../helpers.ts";
import { LitApiServerClient } from "../litApiServer.ts";
import { PRECREATED_ACCOUNTS } from "../setup.ts";
Expand Down Expand Up @@ -206,10 +207,41 @@ export default function (data: IntegrationSetupData) {
}, "addActionToGroup");

// ── 10a. listActions (account-level, no group_id) ────────────────────────
const listActionsAccountRes = client.listActions(
// The action metadata + group membership were written on-chain moments ago;
// list reads can briefly lag the write (the RPC can serve the read from a
// node that hasn't executed those blocks yet). Poll until the action is
// visible with a real id so propagation lag doesn't fail the deploy gate —
// a genuinely missing action still fails the checks after the retries, and
// a stale group listing would otherwise send hash 0x0 to
// update_action_metadata below.
const LIST_ACTIONS_ATTEMPTS = 5;
const findHelloWorld = (body: unknown): { id: string; name: string } | undefined => {
try {
const items = JSON.parse(body as string) as { id: string; name: string }[];
const item = Array.isArray(items) ? items.find((a) => a.name === "hello-world") : undefined;
// An all-zero id means the membership row landed but the metadata read
// is still stale — treat as not-yet-visible and keep polling.
return item && item.id && !/^(0x)?0*$/.test(item.id) ? item : undefined;
} catch {
return undefined;
}
};

let listActionsAccountRes = client.listActions(
{ page_number: 0, page_size: 10 },
authHeaders,
);
for (
let i = 1;
i < LIST_ACTIONS_ATTEMPTS && !findHelloWorld(listActionsAccountRes.response.body);
i++
) {
sleep(2);
listActionsAccountRes = client.listActions(
Comment on lines +238 to +240
{ page_number: 0, page_size: 10 },
authHeaders,
);
}
if (!assertOk("listActionsAccount", "GET /list_actions (account)", listActionsAccountRes)) return;
checkAndLog(listActionsAccountRes.response, {
"listActionsAccount returns array": (r) => {
Expand All @@ -229,11 +261,22 @@ export default function (data: IntegrationSetupData) {
},
}, "listActionsAccount");

// ── 10b. listActions (in group) ─────────────────────────────────────────
const listActionsRes = client.listActions(
// ── 10b. listActions (in group) — same propagation-lag polling as 10a ────
let listActionsRes = client.listActions(
{ group_id: parseInt(groupId), page_number: 0, page_size: 10 },
authHeaders,
);
for (
let i = 1;
i < LIST_ACTIONS_ATTEMPTS && !findHelloWorld(listActionsRes.response.body);
i++
) {
sleep(2);
listActionsRes = client.listActions(
Comment on lines +273 to +275
{ group_id: parseInt(groupId), page_number: 0, page_size: 10 },
authHeaders,
);
}
if (!assertOk("listActions", "GET /list_actions", listActionsRes)) return;
checkAndLog(listActionsRes.response, {
"listActions returns array": (r) => {
Expand All @@ -244,8 +287,14 @@ export default function (data: IntegrationSetupData) {
}
},
}, "listActions");
const listActionsBody = JSON.parse(listActionsRes.response.body as string) as { id: string; name: string }[];
const actionItem = listActionsBody.find((a) => a.name === "hello-world");
const actionItem = findHelloWorld(listActionsRes.response.body);
if (
!checkAndLog(listActionsRes.response, {
"listActions group contains added action with non-zero id": () => actionItem !== undefined,
}, "listActions")
) {
return;
}
const hashedCid = actionItem?.id ?? "";
// ── 10. addPkpToGroup ─────────────────────────────────────────────────────
const addPkpRes = client.addPkpToGroup(
Expand Down
50 changes: 41 additions & 9 deletions lit-api-server/src/core/account_management.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ use rocket::serde::json::Json;
/// to include the NotAllowedTo* error types. Track as follow-up.
const PERMISSION_ERROR_PATTERNS: &[&str] = &["NotAllowedTo", "NotMasterAccount", "NoAccountAccess"];

/// Attempts for the registerWalletDerivation follow-up write inside `new_account`
/// (1 initial + 3 retries, exponential backoff from 250ms). Only the stale-read
/// NoAccountAccess revert is retried — see the comment at the call site.
const REGISTER_WALLET_DERIVATION_ATTEMPTS: u32 = 4;

fn map_contract_error(e: anyhow::Error, context: &str) -> ApiStatus {
let msg = format!("{}", e);
if PERMISSION_ERROR_PATTERNS
Expand Down Expand Up @@ -125,15 +130,42 @@ pub async fn new_account(
}

// technically this is NOT a derivaton path at all, but it's a stand-in for now
accounts::register_wallet_derivation(
signer_pool,
&api_key,
wallet_address,
derivation_path,
"AMW",
"Account Master Wallet",
)
.await?;
//
// The newAccount receipt above proves the account exists on-chain, but
// send_transaction dry-runs this follow-up write via eth_call, and the RPC
// provider can serve that call from a node that hasn't executed the
// newAccount block yet — reverting NoAccountAccess for an account that was
// just mined. That stale-read window is the only way NoAccountAccess can
// occur here, so retry it briefly; any other error fails immediately.
let mut attempt = 0u32;
loop {
attempt += 1;
match accounts::register_wallet_derivation(
signer_pool.clone(),
Comment on lines +140 to +144
&api_key,
wallet_address,
derivation_path,
"AMW",
"Account Master Wallet",
)
.await
{
Ok(_) => break,
Err(e)
if attempt < REGISTER_WALLET_DERIVATION_ATTEMPTS
&& e.to_string().contains("NoAccountAccess") =>
{
let backoff_ms = 250u64 << (attempt - 1);
tracing::warn!(
"new_account: registerWalletDerivation simulation hit stale-read \
NoAccountAccess (attempt {attempt}/{REGISTER_WALLET_DERIVATION_ATTEMPTS}); \
retrying in {backoff_ms}ms"
);
tokio::time::sleep(std::time::Duration::from_millis(backoff_ms)).await;
}
Err(e) => return Err(e.into()),
}
}

// Best-effort: eagerly create the Stripe customer (with $0 balance), set the email
// if provided, and grant starter credits when STARTER_CREDITS_CENTS is configured.
Expand Down
Loading