Skip to content

Commit

Permalink
✨ Session Keys
Browse files Browse the repository at this point in the history
  • Loading branch information
notdanilo committed Jan 24, 2025
1 parent ede6485 commit 336fc37
Show file tree
Hide file tree
Showing 10 changed files with 432 additions and 138 deletions.
20 changes: 20 additions & 0 deletions clients/bolt-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,26 @@ export function FindEntityPda({
)[0];
}

export function FindSessionTokenPda({
targetProgram,
sessionSigner,
authority,
}: {
targetProgram: PublicKey;
sessionSigner: PublicKey;
authority: PublicKey;
}) {
return PublicKey.findProgramAddressSync(
[
Buffer.from("session_token"),
targetProgram.toBytes(),
sessionSigner.toBytes(),
authority.toBytes(),
],
SessionProgram.programId,
)[0];
}

// TODO: seed must be Uint8Array like the other FindPda functions
export function FindComponentPda({
componentId,
Expand Down
49 changes: 49 additions & 0 deletions clients/bolt-sdk/src/world/transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@ import {
SerializeArgs,
SYSVAR_INSTRUCTIONS_PUBKEY,
World,
SessionProgram,
FindSessionTokenPda,
} from "../index";
import BN from "bn.js";
import type web3 from "@solana/web3.js";
import {
type Connection,
Keypair,
type PublicKey,
Transaction,
type TransactionInstruction,
Expand Down Expand Up @@ -47,6 +50,49 @@ export async function InitializeRegistry({
};
}

export async function CreateSession({
sessionSigner,
authority,
targetProgram,
topUp,
validity,
}: {
sessionSigner?: Keypair;
authority: PublicKey;
targetProgram: PublicKey;
topUp?: boolean;
validity?: BN;
}): Promise<{
instruction: TransactionInstruction;
transaction: Transaction;
sessionToken: PublicKey;
sessionSigner: Keypair;
}> {
sessionSigner = sessionSigner ?? Keypair.generate();
const sessionToken = FindSessionTokenPda({
targetProgram,
sessionSigner: sessionSigner.publicKey,
authority,
});
topUp = topUp ?? false;
let instruction = await SessionProgram.methods
.createSession(topUp, validity ?? null)
.accounts({
sessionSigner: sessionSigner.publicKey,
authority,
targetProgram,
sessionToken,
})
.instruction();
const transaction = new Transaction().add(instruction);
return {
instruction,
transaction,
sessionToken,
sessionSigner,
};
}

/**
* Create the transaction to Initialize a new world
* @param payer
Expand Down Expand Up @@ -438,13 +484,15 @@ export async function ApplySystem({
world,
extraAccounts,
args,
sessionToken,
}: {
authority: PublicKey;
systemId: PublicKey;
entities: ApplySystemEntity[];
world: PublicKey;
extraAccounts?: web3.AccountMeta[];
args?: object;
sessionToken?: PublicKey;
}): Promise<{ instruction: TransactionInstruction; transaction: Transaction }> {
const instruction = await createApplySystemInstruction({
authority,
Expand All @@ -453,6 +501,7 @@ export async function ApplySystem({
world,
extraAccounts,
args,
sessionToken,
});
const transaction = new Transaction().add(instruction);
return {
Expand Down
20 changes: 12 additions & 8 deletions crates/bolt-lang/attribute/bolt-program/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,14 +150,18 @@ fn generate_update(component_type: &Type) -> (TokenStream2, TokenStream2) {
pub fn update(ctx: Context<Update>, data: Vec<u8>) -> Result<()> {
// TODO: Should we also check if the session token authority can be the World::id?
if let Some(session_token) = &ctx.accounts.session_token {
let validity_ctx = bolt_lang::session_keys::ValidityChecker {
session_token: session_token.clone(),
session_signer: ctx.accounts.authority.clone(),
authority: ctx.accounts.bolt_component.bolt_metadata.authority.clone(),
target_program: crate::id(),
};
require!(session_token.validate(validity_ctx)?, bolt_lang::session_keys::SessionError::InvalidToken);
require_eq!(ctx.accounts.bolt_component.bolt_metadata.authority, session_token.authority, bolt_lang::session_keys::SessionError::InvalidToken);
if ctx.accounts.bolt_component.bolt_metadata.authority == World::id() {
require!(Clock::get()?.unix_timestamp < session_token.valid_until, bolt_lang::session_keys::SessionError::InvalidToken);
} else {
let validity_ctx = bolt_lang::session_keys::ValidityChecker {
session_token: session_token.clone(),
session_signer: ctx.accounts.authority.clone(),
authority: ctx.accounts.bolt_component.bolt_metadata.authority.clone(),
target_program: crate::id(),
};
require!(session_token.validate(validity_ctx)?, bolt_lang::session_keys::SessionError::InvalidToken);
require_eq!(ctx.accounts.bolt_component.bolt_metadata.authority, session_token.authority, bolt_lang::session_keys::SessionError::InvalidToken);
}
} else {
require!(ctx.accounts.bolt_component.bolt_metadata.authority == World::id() || (ctx.accounts.bolt_component.bolt_metadata.authority == *ctx.accounts.authority.key && ctx.accounts.authority.is_signer), BoltError::InvalidAuthority);
}
Expand Down
6 changes: 0 additions & 6 deletions tests/framework.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,20 +71,14 @@ export class Framework {
entity1Pda: PublicKey;
entity2Pda: PublicKey;
entity4Pda: PublicKey;
entity5Pda: PublicKey;

componentPositionEntity1Pda: PublicKey;
componentVelocityEntity1Pda: PublicKey;

componentPositionEntity4Pda: PublicKey;
componentPositionEntity5Pda: PublicKey;

sessionSigner: Keypair;
sessionToken: PublicKey;

constructor() {
this.secondAuthority = Keypair.generate().publicKey;
this.sessionSigner = Keypair.generate();
this.worldProgram = anchor.workspace.World;
this.exampleComponentPosition = anchor.workspace.Position;
this.exampleComponentVelocity = anchor.workspace.Velocity;
Expand Down
40 changes: 19 additions & 21 deletions tests/intermediate-level/permissioning/component.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,37 @@
import { expect } from "chai";
import { AddEntity, ApplySystem, InitializeComponent } from "../../../clients/bolt-sdk/lib";
import { anchor, AddEntity, ApplySystem, InitializeComponent } from "../../../clients/bolt-sdk/lib";
import { Keypair } from "@solana/web3.js";

export function component(framework) {
describe("Component authority", () => {
it("Add entity 5", async () => {
let entity: anchor.web3.PublicKey;
let component: anchor.web3.PublicKey;

it("Add authority test entity", async () => {
const addEntity = await AddEntity({
payer: framework.provider.wallet.publicKey,
world: framework.worldPda,
connection: framework.provider.connection,
});
await framework.provider.sendAndConfirm(addEntity.transaction);
framework.entity5Pda = addEntity.entityPda; // Saved for later
entity = addEntity.entityPda; // Saved for later
});

it("Initialize Position Component on Entity 5 (with authority)", async () => {
it("Initialize position component with authority on authority test entity", async () => {
const initializeComponent = await InitializeComponent({
payer: framework.provider.wallet.publicKey,
entity: framework.entity5Pda,
entity: entity,
componentId: framework.exampleComponentPosition.programId,
authority: framework.provider.wallet.publicKey,
});
await framework.provider.sendAndConfirm(initializeComponent.transaction);
framework.componentPositionEntity5Pda = initializeComponent.componentPda; // Saved for later
component = initializeComponent.componentPda; // Saved for later
});

it("Apply Fly System on Entity 5 (should fail with wrong authority)", async () => {
it("Shouldn't apply fly system on authority test entity with wrong authority", async () => {
const positionBefore =
await framework.exampleComponentPosition.account.position.fetch(
framework.componentPositionEntity5Pda,
component,
);

let keypair = Keypair.generate();
Expand All @@ -39,22 +42,17 @@ export function component(framework) {
world: framework.worldPda,
entities: [
{
entity: framework.entity5Pda,
entity: entity,
components: [
{ componentId: framework.exampleComponentPosition.programId },
],
},
],
});
applySystem.transaction.recentBlockhash = (
await framework.provider.connection.getLatestBlockhash()
).blockhash;
applySystem.transaction.feePayer = framework.provider.wallet.publicKey;
applySystem.transaction.sign(keypair);

let failed = false;
try {
await framework.provider.sendAndConfirm(applySystem.transaction);
await framework.provider.sendAndConfirm(applySystem.transaction, [keypair]);
} catch (error) {
failed = true;
expect(error.logs.join("\n")).to.contain("Error Code: InvalidAuthority");
Expand All @@ -63,18 +61,18 @@ export function component(framework) {

const positionAfter =
await framework.exampleComponentPosition.account.position.fetch(
framework.componentPositionEntity5Pda,
component,
);

expect(positionBefore.x.toNumber()).to.equal(positionAfter.x.toNumber());
expect(positionBefore.y.toNumber()).to.equal(positionAfter.y.toNumber());
expect(positionBefore.z.toNumber()).to.equal(positionAfter.z.toNumber());
});

it("Apply Fly System on Entity 5 should succeed with correct authority", async () => {
it("Apply Fly System on authority test entity should succeed with correct authority", async () => {
const positionBefore =
await framework.exampleComponentPosition.account.position.fetch(
framework.componentPositionEntity5Pda,
component,
);

const applySystem = await ApplySystem({
Expand All @@ -83,7 +81,7 @@ export function component(framework) {
world: framework.worldPda,
entities: [
{
entity: framework.entity5Pda,
entity,
components: [
{ componentId: framework.exampleComponentPosition.programId },
],
Expand All @@ -92,10 +90,10 @@ export function component(framework) {
});

await framework.provider.sendAndConfirm(applySystem.transaction);

const positionAfter =
await framework.exampleComponentPosition.account.position.fetch(
framework.componentPositionEntity5Pda,
component,
);

expect(positionAfter.x.toNumber()).to.equal(positionBefore.x.toNumber());
Expand Down
43 changes: 3 additions & 40 deletions tests/intermediate-level/permissioning/world.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export function world(framework) {
);
});

it("Whitelist System", async () => {
it("Whitelist Fly System", async () => {
const approveSystem = await ApproveSystem({
authority: framework.provider.wallet.publicKey,
systemToApprove: framework.systemFly.programId,
Expand All @@ -84,7 +84,7 @@ export function world(framework) {
expect(worldAccount.systems.length).to.be.greaterThan(0);
});

it("Whitelist System 2", async () => {
it("Whitelist Apply Velocity System system", async () => {
const approveSystem = await ApproveSystem({
authority: framework.provider.wallet.publicKey,
systemToApprove: framework.systemApplyVelocity.programId,
Expand Down Expand Up @@ -120,7 +120,7 @@ export function world(framework) {
await framework.provider.sendAndConfirm(applySystem.transaction);
});

it("Remove System 1", async () => {
it("Remove Fly System", async () => {
const removeSystem = await RemoveSystem({
authority: framework.provider.wallet.publicKey,
systemToRemove: framework.systemFly.programId,
Expand Down Expand Up @@ -162,42 +162,5 @@ export function world(framework) {
}
expect(invalid).to.equal(true);
});

it("Check invalid component init without CPI", async () => {
let invalid = false;
try {
await framework.exampleComponentPosition.methods
.initialize()
.accounts({
payer: framework.provider.wallet.publicKey,
data: framework.componentPositionEntity5Pda,
entity: framework.entity5Pda,
authority: framework.provider.wallet.publicKey,
})
.rpc();
} catch (error) {
expect(error.message).to.contain("Error Code: InvalidCaller");
invalid = true;
}
expect(invalid).to.equal(true);
});

it("Check invalid component update without CPI", async () => {
let invalid = false;
try {
await framework.exampleComponentPosition.methods
.update(Buffer.from(""))
.accounts({
boltComponent: framework.componentPositionEntity4Pda,
authority: framework.provider.wallet.publicKey,
sessionToken: null,
})
.rpc();
} catch (error) {
expect(error.message).to.contain("Error Code: InvalidCaller");
invalid = true;
}
expect(invalid).to.equal(true);
});
});
}
Loading

0 comments on commit 336fc37

Please sign in to comment.