Distributed Sessions
The prepare → sign → collect → create pattern for multi-party sessions
Distributed Sessions
One of the most powerful (and initially confusing) patterns in Yellow SDK is distributed session creation. This allows multiple parties to collectively create a session without requiring a trusted central authority to hold signatures.
The Problem
Traditional approaches require one party to:
- Collect everyone's private keys OR
- Act as a trusted intermediary OR
- Collect signatures sequentially (slow!)
Yellow SDK solves this with a distributed signing pattern where:
- Each party signs independently (in parallel!)
- No party needs to trust the coordinator
- All signatures are verified by the ClearNode
- Only valid multi-party agreements succeed
The Pattern
1. PREPARE → Server creates unsigned request
2. SIGN → All parties sign in parallel
3. COLLECT → Server gathers all signatures
4. CREATE → Server submits to ClearNode
This pattern enables trustless coordination - the server can't create a session without valid signatures from all participants.
Step-by-Step Walkthrough
Step 1: Prepare the Request
The session initiator (usually a game server) prepares the session request:
const sessionRequest = serverClient.prepareSession({
participants: [
player1.address,
player2.address,
server.address
],
allocations: [
{ participant: player1.address, asset: 'USDC', amount: '1.00' },
{ participant: player2.address, asset: 'USDC', amount: '1.00' },
{ participant: server.address, asset: 'USDC', amount: '0' },
],
});
This creates an unsigned request object:
{
req: [
requestId,
'create_app_session',
[
{
definition: {
protocol: 'NitroRPC/0.4',
participants: [...],
weights: [0, 0, 100], // Server controlled
quorum: 100,
challenge: 0,
nonce: 1696420800000
},
allocations: [...]
}
],
timestamp
],
sig: [] // ← Empty! No signatures yet
}
Key insight: prepareSession()
creates a deterministic request that everyone can independently verify and sign.
Step 2: Distribute for Signing
In a real application, the server sends this request to all participants:
// Via HTTP, WebSocket, or any communication channel
POST /api/game/sign-session
Body: {
sessionRequest: sessionRequest,
gameId: "game-123"
}
Step 3: Each Participant Signs
Each participant receives the request and signs it independently:
// Player 1's client
const signature1 = await player1Client.signSessionRequest(sessionRequest);
// Returns: "0xabc123..." (65-byte signature)
// Player 2's client
const signature2 = await player2Client.signSessionRequest(sessionRequest);
// Returns: "0xdef456..."
// Server also signs
const signatureServer = await serverClient.signSessionRequest(sessionRequest);
// Returns: "0x789xyz..."
What's being signed:
// Each party signs the entire request object
const messageToSign = JSON.stringify(sessionRequest.req);
const signature = await wallet.signMessage(messageToSign);
Important: Everyone signs the same data (the req
field), ensuring agreement on session parameters.
Step 4: Collect Signatures
Participants return their signatures to the initiator:
// Via HTTP response, WebSocket message, etc.
{
signature: "0xabc123...",
participant: "0xPlayer1Address"
}
The server collects all signatures:
const signatures = [
signatureServer, // ← Server signature FIRST!
signature1, // ← Then players in allocation order
signature2,
];
Critical: Signature order matters! The pattern is:
- Server signature first (the weight holder)
- Then non-zero allocation participants in the order they appear in allocations
Step 5: Create the Session
With all signatures collected, the server creates the session:
const sessionId = await serverClient.createSession(
sessionRequest,
[signatureServer as `0x${string}`, signature1 as `0x${string}`, signature2 as `0x${string}`]
);
console.log(`Session created: ${sessionId}`);
// → "0x0ac588b2924edbbbe34bb4c51d089771bd7bd7018136c8c4317624112a8c9f79"
The ClearNode:
- Verifies all signatures match participants
- Checks allocations don't exceed channel capacity
- Creates the session and broadcasts to all participants
- Returns the unique session ID
Real-World Implementation
Here's how this looks in a complete game server:
// ============================================
// SERVER: Game lobby endpoint
// ============================================
app.post('/api/game/:gameId/start', async (req, res) => {
const game = await db.games.findById(req.params.gameId);
const players = game.players; // Array of player addresses
// 1. PREPARE
const sessionRequest = serverClient.prepareSession({
participants: [...players, serverAddress],
allocations: players.map(p => ({
participant: p.address,
asset: 'USDC',
amount: game.entryFee
})).concat([
{ participant: serverAddress, asset: 'USDC', amount: '0' }
]),
});
// 2. DISTRIBUTE - Broadcast to all players via WebSocket
players.forEach(player => {
wsConnections.get(player.address).send({
type: 'SESSION_SIGN_REQUEST',
data: { sessionRequest, gameId: game.id }
});
});
// 3. COLLECT - Wait for signatures (with timeout)
const signatures = await collectSignatures(game.id, players.length, 30000);
// 4. CREATE
const sessionId = await serverClient.createSession(
sessionRequest,
[await serverClient.signSessionRequest(sessionRequest), ...signatures]
);
// Update game state
await db.games.update(game.id, { sessionId, status: 'ACTIVE' });
res.json({ sessionId });
});
// ============================================
// CLIENT: Player receives sign request
// ============================================
ws.on('message', async (msg) => {
if (msg.type === 'SESSION_SIGN_REQUEST') {
// Player reviews session terms (UI shows entry fee, players, etc.)
const userConfirmed = await showSessionApprovalUI(msg.data.sessionRequest);
if (userConfirmed) {
// Sign the request
const signature = await playerClient.signSessionRequest(msg.data.sessionRequest);
// Send back to server
ws.send({
type: 'SESSION_SIGNATURE',
data: {
gameId: msg.data.gameId,
signature,
participant: playerAddress
}
});
}
}
});
// ============================================
// SERVER: Collect signatures helper
// ============================================
async function collectSignatures(gameId, expectedCount, timeoutMs) {
return new Promise((resolve, reject) => {
const signatures = [];
const timeout = setTimeout(() => {
reject(new Error('Signature collection timeout'));
}, timeoutMs);
wsServer.on('message', (msg) => {
if (msg.type === 'SESSION_SIGNATURE' && msg.data.gameId === gameId) {
signatures.push(msg.data.signature);
if (signatures.length === expectedCount) {
clearTimeout(timeout);
resolve(signatures);
}
}
});
});
}
Signature Order Rules
The order of signatures in the array matters because ClearNode verifies them against participant order:
Rule 1: Weight Holder First
If you have a server-controlled session (weights: [0, 0, 100]), the server's signature must be first:
const signatures = [
serverSig, // ← Must be first (has weight 100)
player1Sig, // ← Then players
player2Sig,
];
Rule 2: Non-Zero Allocations
Only participants with non-zero allocations need to sign (in the order they appear):
allocations: [
{ participant: player1, asset: 'USDC', amount: '1.00' }, // ← Signs (position 0)
{ participant: player2, asset: 'USDC', amount: '1.00' }, // ← Signs (position 1)
{ participant: server, asset: 'USDC', amount: '0' }, // ← Signs if weight > 0
]
// If server has weight, order is:
signatures: [serverSig, player1Sig, player2Sig]
// If server has no weight (peer-to-peer), order is:
signatures: [player1Sig, player2Sig]
Rule 3: Consistent Ordering
The order must match between:
participants
arrayallocations
arraysignatures
array (after weight holder)
Handling Failures
Timeout Collecting Signatures
try {
const signatures = await collectSignatures(gameId, playerCount, 30000);
} catch (error) {
console.error('Failed to collect all signatures:', error);
// Notify players game is cancelled
notifyPlayers(gameId, 'SESSION_CANCELLED', {
reason: 'Not all players signed in time'
});
// Clean up game state
await db.games.update(gameId, { status: 'CANCELLED' });
}
Invalid Signature
try {
const sessionId = await serverClient.createSession(request, signatures);
} catch (error) {
// ClearNode rejected due to invalid signature
if (error.message.includes('invalid signature')) {
console.error('One or more signatures invalid');
// Identify which signature failed (ClearNode doesn't tell you which)
// May need to re-request signatures
}
}
Insufficient Channel Balance
try {
const sessionId = await serverClient.createSession(request, signatures);
} catch (error) {
if (error.message.includes('insufficient balance')) {
// One or more players doesn't have enough channel capacity
console.error('Player has insufficient funds');
// Notify the specific player to deposit more
// Or reduce the entry fee and restart signing
}
}
Optimizing the Flow
Parallel Signing
Since all participants sign the same request, signatures can happen in parallel:
// ✗ Bad: Sequential (slow!)
const sig1 = await client1.signSessionRequest(request);
const sig2 = await client2.signSessionRequest(request);
const sig3 = await client3.signSessionRequest(request);
// ✓ Good: Parallel (fast!)
const [sig1, sig2, sig3] = await Promise.all([
client1.signSessionRequest(request),
client2.signSessionRequest(request),
client3.signSessionRequest(request),
]);
Pre-Approval
For games with known participants, you can prepare sessions in advance:
// During matchmaking, prepare session
const sessionRequest = prepareSession({ ... });
// Players pre-sign while waiting
const preSignatures = await collectSignatures(...);
// When game starts, instantly create
const sessionId = await createSession(sessionRequest, preSignatures);
// → Nearly instant session creation!
Security Considerations
1. Verify Request Before Signing
Clients should always verify what they're signing:
async function signSessionRequest(request) {
// Extract session parameters
const { participants, allocations } = request.req[2][0];
// Show user what they're agreeing to
const userApproved = await showApprovalUI({
participants,
yourStake: allocations.find(a => a.participant === myAddress).amount,
otherPlayers: participants.filter(p => p !== myAddress)
});
if (!userApproved) {
throw new Error('User rejected session');
}
// Only sign after user confirmation
return await client.signSessionRequest(request);
}
2. Validate Participant List
Ensure you're only playing with expected participants:
// Check participants match expected players
const expectedPlayers = ['0xAlice', '0xBob', '0xCharlie'];
const actualPlayers = sessionRequest.req[2][0].definition.participants;
if (!expectedPlayers.every(p => actualPlayers.includes(p))) {
throw new Error('Unexpected participants in session');
}
3. Verify Allocations
Check allocations match the agreed game rules:
const myAllocation = allocations.find(a => a.participant === myAddress);
if (parseUSDC(myAllocation.amount) > myMaxStake) {
throw new Error(`Stake ${myAllocation.amount} exceeds maximum ${myMaxStake}`);
}
Common Patterns
Pattern 1: Tournament Bracket
Create multiple sessions in sequence:
// Round 1: 4 games
const round1Sessions = await Promise.all([
createDistributedSession([player1, player2]),
createDistributedSession([player3, player4]),
createDistributedSession([player5, player6]),
createDistributedSession([player7, player8]),
]);
// Determine winners...
// Round 2: 2 games
const round2Sessions = await Promise.all([
createDistributedSession([winner1, winner2]),
createDistributedSession([winner3, winner4]),
]);
Pattern 2: Drop-In/Drop-Out
Allow players to join existing lobbies:
// Start with 2 players
let sessionRequest = prepareSession([player1, player2]);
// Player 3 wants to join
sessionRequest = prepareSession([player1, player2, player3]);
// Collect ALL signatures again (previous ones are now invalid)
const signatures = await collectAll([player1, player2, player3]);
const sessionId = await createSession(sessionRequest, signatures);
Pattern 3: Retry Logic
Handle signature collection failures gracefully:
async function createSessionWithRetry(request, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const signatures = await collectSignatures(request, 30000);
return await createSession(request, signatures);
} catch (error) {
console.log(`Attempt ${attempt} failed:`, error.message);
if (attempt === maxRetries) {
throw new Error('Failed to create session after retries');
}
// Wait before retry
await new Promise(r => setTimeout(r, 1000 * attempt));
}
}
}
Implementation
See the distributed session pattern in action:
prepareSession()
- Prepares unsigned session requestsignSessionRequest()
- Signs session request with participant keycreateSession()
- Submits signatures to ClearNode
Next Steps
- Session Lifecycle: Manage active sessions and cleanup
- Complete Game: See distributed sessions in a real multiplayer game