Skip to content
Merged
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
52 changes: 35 additions & 17 deletions private-counter/pinocchio/tests/pinocchio-private-counter.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
type AccountInfo,
Keypair,
PublicKey,
SystemProgram,
Expand All @@ -18,7 +19,6 @@ import {
MAGIC_PROGRAM_ID,
PERMISSION_PROGRAM_ID,
getAuthToken,
GetCommitmentSignature,
} from "@magicblock-labs/ephemeral-rollups-sdk";
import * as nacl from "tweetnacl";
import path from "path";
Expand Down Expand Up @@ -688,22 +688,40 @@ describe(
console.log(`(ER) Undelegate txHash: ${txHash}`);
expect(txHash).toBeDefined();

const commitHash = await GetCommitmentSignature(
txHash,
connectionEphemeralRollup,
);
console.log(`(ER) Commit txHash: ${commitHash}`);
expect(commitHash).toBeDefined();

const result = await connectionBaseLayer.confirmTransaction(commitHash);
console.log(`(Base Layer) Commit result: ${result}`);
expect(result.value.err).toBeNull();

let counter = await connectionBaseLayer.getAccountInfo(counterPda, {
commitment: "confirmed",
});
expect(counter?.owner.equals(PROGRAM_ID)).toBe(true);
});
// Wait for the ER to commit + undelegate the counter back to the base
// layer, i.e. until it is owned by our program again. This polls base-layer
// ownership directly instead of resolving the commit signature from the ER's
// ScheduledCommitSent logs: the local committor can re-send the finalize tx
// after a transient error, and the duplicate then fails on-chain (the
// original already landed) — which surfaces as "Unable to find Commitment
// signature" even though the account did come back.
let counter: AccountInfo<Buffer> | null = null;
let lastError: unknown;
for (let attempt = 0; attempt < 60; attempt += 1) {
if (attempt > 0) {
await new Promise((resolve) => setTimeout(resolve, 1000));
}
try {
counter = await connectionBaseLayer.getAccountInfo(counterPda, {
commitment: "confirmed",
});
if (counter?.owner.equals(PROGRAM_ID)) {
break;
}
lastError = new Error(
`expected ${PROGRAM_ID.toBase58()}, got ${counter?.owner.toBase58() ?? "missing account"}`,
);
} catch (error) {
lastError = error;
}
}
console.log(`(Base Layer) Counter owner: ${counter?.owner.toBase58()}`);
if (!counter?.owner.equals(PROGRAM_ID)) {
throw new Error(
`Counter was not undelegated back to the base layer in time: ${lastError}`,
);
}
}, 90_000);
},
{ timeout: 30000 },
);
8 changes: 8 additions & 0 deletions scripts/test-locally.sh
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,14 @@ run_test() {
if [ "$test_failed" = true ]; then
# Show full output on failure
cat "$test_log"
# Settlement failures (commit / undelegate never landing) only explain
# themselves in the ER's own logs, so surface those alongside the test output.
if grep -qiE "error|warn" "$REPO_ROOT/mb-stack.log" 2>/dev/null; then
echo ""
echo "----- mb-stack.log (WARN/ERROR lines, last 40) -----"
grep -iE "error|warn" "$REPO_ROOT/mb-stack.log" | tail -40
echo "----- end of mb-stack.log excerpt -----"
fi
else
# Show only stage completion markers on success with timing
local stages_completed=""
Expand Down
44 changes: 37 additions & 7 deletions spl-tokens/anchor/tests/spl-tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ import {
import { SplTokens } from "../target/types/spl_tokens";
import {
delegateSpl,
deriveEphemeralAta,
deriveRentPda,
GetCommitmentSignature,
EPHEMERAL_SPL_TOKEN_PROGRAM_ID,
transferSpl,
undelegateIx,
withdrawSpl,
Expand Down Expand Up @@ -102,6 +103,31 @@ describe("spl-tokens", () => {
);
};

// Poll the base layer until `account` is owned by the ephemeral SPL token
// program again, i.e. the ER's commit + undelegate for it has landed.
const waitForUndelegation = async (account: PublicKey): Promise<void> => {
let lastError: unknown;
for (let attempt = 0; attempt < 60; attempt += 1) {
if (attempt > 0) {
await sleep(1000);
}
try {
const info = await connection.getAccountInfo(account, "confirmed");
if (info?.owner.equals(EPHEMERAL_SPL_TOKEN_PROGRAM_ID)) {
return;
}
lastError = new Error(
`expected ${EPHEMERAL_SPL_TOKEN_PROGRAM_ID.toBase58()}, got ${info?.owner.toBase58() ?? "missing account"}`,
);
} catch (error) {
lastError = error;
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
throw new Error(
`${account.toBase58()} was not undelegated back to the base layer in time: ${lastError}`,
);
};

/**
* Create a fresh mint and two recipients, each funded with SOL and holding
* {@link TOKEN_AMOUNT} SPL tokens. Returns the mint, owners and their ATAs.
Expand Down Expand Up @@ -288,9 +314,12 @@ describe("spl-tokens", () => {
// Undelegate each owner in the ER (one per tx — combined undelegates are flaky
// in CI). Withdraw runs on the base layer and requires each ephemeral ATA to be
// owned by the SDK program again, which only happens once that owner's
// undelegation has committed back to base — so wait for BOTH commits before
// undelegation has committed back to base — so wait for BOTH before
// withdrawing (waiting for one races the other's withdraw → InvalidAccountOwner).
const commits: string[] = [];
// Ownership is polled on the base layer rather than resolved through the ER's
// commit signature: the local committor can re-send the finalize tx after a
// transient error and the duplicate then fails on-chain (the original already
// landed), which surfaces as "Unable to find Commitment signature".
for (const owner of [recipientA, recipientB]) {
const sgn = await providerEphemeralRollup.sendAndConfirm(
new anchor.web3.Transaction().add(
Expand All @@ -300,12 +329,13 @@ describe("spl-tokens", () => {
{ commitment: "confirmed", skipPreflight: true },
);
console.log(`Undelegate ${owner.publicKey.toBase58()} signature: ${sgn}`);
commits.push(
await GetCommitmentSignature(sgn, providerEphemeralRollup.connection),
);
}
await Promise.all(
commits.map((c) => connection.confirmTransaction(c, "confirmed")),
[recipientA, recipientB].map((owner) =>
waitForUndelegation(
deriveEphemeralAta(owner.publicKey, mint.publicKey)[0],
),
),
);

// Withdraw both balances back to their base-layer ATAs via the SDK helper.
Expand Down
Loading