Skip to content

Latest commit

 

History

History
824 lines (671 loc) · 31.5 KB

File metadata and controls

824 lines (671 loc) · 31.5 KB

State Transition SDK Analysis Report

Overview

The state-transition-sdk is a framework for implementing off-chain token transactions with on-chain proofs to prevent double-spending. This approach allows for scalable token management where the tokens themselves are self-contained entities that carry their complete history, ownership information, and cryptographic proofs.

Core Concepts

1. Self-Contained Tokens

Unlike traditional blockchain tokens that exist as entries in a shared ledger, these tokens are complete packages containing:

  • Unique token ID and class ID
  • Token value (with 18 decimal precision like Ethereum)
  • Complete transaction history
  • Cryptographic proofs of ownership
  • State transition records

2. State Transition Machine

The system works as a state transition abstract machine where:

  • Tokens exist in specific states (owned by someone)
  • Transitions move tokens from one state to another
  • Each transition is cryptographically verified and proven unique
  • A token's history is a chain of these transitions

3. Privacy-Preserving Transactions

The protocol ensures privacy through several mechanisms:

  • One-time use public keys (derived from secret + nonce)
  • Pointer addressing (hiding ownership from observers)
  • Salt-based obfuscation of transaction details
  • Minimal on-chain footprint (only proofs, not token data)

Key Components

1. state_machine.js

This is the primary engine of the system, handling:

  • Token creation (minting)
  • Transaction generation
  • Transaction import/export
  • Verification of token status
  • Collection and management of tokens

Key functions:

  • mint(): Creates a new token with proper proofs
  • createTx(): Generates a transaction for sending tokens
  • importFlow(): Imports tokens and transactions
  • exportFlow(): Exports tokens and transactions
  • getTokenStatus(): Checks if a token is spendable and owned
  • collectTokens(): Gathers all spendable tokens for a user

2. token_manager.js

This is a CLI tool built on top of the state machine that provides user-friendly commands:

  • mint: Create new tokens
  • pointer: Generate recipient pointers for receiving tokens
  • send: Create transactions to send tokens
  • receive: Import and resolve transactions
  • summary: List all owned tokens

3. token.js

Defines the Token class that represents the token entity with methods to:

  • Validate the genesis (initial creation)
  • Apply transactions to change state
  • Update state based on transitions
  • Get token statistics

4. Web Interface

The SDK includes a browser-based UI (docs/index.html) that provides:

  • Token minting interface
  • Transaction creation
  • Recipient pointer generation
  • Token importing
  • Status checking

Token Transaction Flow

The transaction flow follows these steps:

  1. Minting (Creation):

    • Generate token ID, class, value
    • Create initial state with owner's pubkey
    • Get unicity proof from Unicity Aggregator
    • Construct token with genesis state
  2. Sending:

    • Recipient creates pointer (hash of pubkey + nonce)
    • Sender creates transaction with pointer as destination
    • Sender obtains unicity proof to prevent double-spending
    • Token + transaction exported as "transaction flow"
  3. Receiving:

    • Recipient imports transaction flow
    • Resolves destination pointer using their secret + nonce
    • Creates full destination state
    • Converts transaction to transition in token history
    • Token now owned by recipient

UniQuake Demo Integration Plan

Based on the analysis of the state-transition-SDK and the reference implementation in UniRoad, we have developed a focused integration plan specifically for the UniQuake demo. This plan addresses two primary goals:

  1. Play-to-earn Strategy: Implementing a token-based entry fee and winner reward system
  2. Game State Integrity: Generating Unicity proofs for game state to ensure consistent gameplay across all clients

Unlike the UniRoad implementation that uses Y.js for synchronization, our demo will leverage the existing P2P networking infrastructure in UniQuake for token transmission, without requiring Y.js.

Demo Architecture Overview

┌─────────────────────────┐     ┌──────────────────────────┐
│     Game Client         │     │     Game Server          │
│                         │     │                          │
│  ┌─────────────────┐    │     │  ┌─────────────────┐     │
│  │  Token Pool     │    │     │  │  Token Pool     │     │
│  └────────┬────────┘    │     │  └────────┬────────┘     │
│           │             │     │           │              │
│  ┌────────┴────────┐    │     │  ┌────────┴────────┐     │
│  │  TX Flow Engine │    │     │  │  TX Flow Engine │     │
│  └────────┬────────┘    │     │  └────────┬────────┘     │
│           │             │     │           │              │
│           │             │     │           │              │
└───────────┼─────────────┘     └───────────┼──────────────┘
            │                                │
            │                                │
            ▼                                ▼
┌──────────────────────────────────────────────────────────┐
│                   P2P Transport Layer                     │
│            (existing UniQuake infrastructure)             │
└──────────────────────────────────────────────────────────┘

1. Play-to-Earn Implementation

The play-to-earn feature will follow this flow:

  1. Client Session Entry:

    • Client generates and mints tokens with a predefined value (quake_test_coin class)
    • When joining a game server, client sends an entry token to the server
    • Server validates the token and allows access if valid
  2. Server Token Collection:

    • Server collects and stores tokens from all participating clients
    • Tokens are held in server's in-memory token pool during the game session
    • Server keeps track of which player contributed which tokens
  3. Winner Reward Distribution:

    • At the end of the game session, the server identifies the winner
    • Server transfers all collected tokens to the winner's address
    • Winner receives and validates the transferred tokens

2. Game State Integrity Verification

To ensure game state integrity:

  1. Periodic State Snapshotting:

    • Every 10 seconds, the server captures the complete game state
    • This state includes all player positions, scores, and other relevant data
    • The state is serialized into a deterministic format
  2. State Token Generation:

    • Server creates a unique token representing the game state
    • The token includes a hash of the game state in its immutable data
    • Server mints this token using the state-transition-SDK
  3. Unicity Proof Generation:

    • Server obtains a Unicity proof for the state token
    • This proof verifies the uniqueness and integrity of the game state
    • Proof is published to clients as verification of fair play
  4. Client Verification:

    • Clients receive the state token with Unicity proof
    • Clients can verify the proof to ensure game state integrity
    • Any mismatch indicates possible manipulation or inconsistency

Implementation Steps

Step 1: Create Token Service Module

Create lib/token-service.js that provides a wrapper around the tx-flow-engine:

const TXF = require('@unicitylabs/tx-flow-engine');

class TokenService {
    constructor(secret, username) {
        this.TXF = TXF;
        this.secret = secret;
        this.username = username;
        this.pubkey = TXF.generateRecipientPubkeyAddr(secret);
        this.transport = TXF.getHTTPTransport(TXF.defaultGateway());
        
        // Token class identifiers
        this.tokenTypes = {
            COIN: TXF.validateOrConvert('token_class', 'quake_test_coin'),
            STATE: TXF.validateOrConvert('token_class', 'quake_game_state')
        };
        
        // In-memory token storage
        this.tokenPool = {
            owned: new Map(), // Tokens owned by this user
            pending: new Map(), // Tokens waiting to be processed
            gameState: new Map() // Game state tokens for verification
        };
    }
    
    // Generate and mint a new coin token
    async mintCoin(value = '10') {
        const tokenId = this.TXF.generateRandom256BitHex();
        const nonce = this.TXF.generateRandom256BitHex();
        const salt = this.TXF.generateRandom256BitHex();
        
        const token = await this.TXF.mint({
            token_id: tokenId,
            token_class_id: this.tokenTypes.COIN,
            token_value: value,
            sign_alg: 'secp256k1',
            hash_alg: 'sha256',
            secret: this.secret,
            nonce,
            mint_salt: salt,
            transport: this.transport
        });
        
        // Store in token pool
        this.tokenPool.owned.set(tokenId, token);
        return token;
    }
    
    // Generate a game state token
    async createGameStateToken(gameState) {
        const tokenId = this.TXF.generateRandom256BitHex();
        const nonce = this.TXF.generateRandom256BitHex();
        const salt = this.TXF.generateRandom256BitHex();
        
        // Serialize and hash the game state
        const stateHash = this.TXF.hashObject(gameState);
        
        // Create immutable data with game state hash and timestamp
        const immutableData = JSON.stringify({
            state_hash: stateHash,
            timestamp: Date.now(),
            game_id: gameState.gameId,
            frame: gameState.frame
        });
        
        const token = await this.TXF.mint({
            token_id: tokenId,
            token_class_id: this.tokenTypes.STATE,
            token_value: '1', // Nominal value for state tokens
            immutable_data: immutableData,
            sign_alg: 'secp256k1',
            hash_alg: 'sha256',
            secret: this.secret,
            nonce,
            mint_salt: salt,
            transport: this.transport
        });
        
        // Store in game state pool
        this.tokenPool.gameState.set(tokenId, {
            token,
            originalState: gameState
        });
        
        return token;
    }
    
    // Send a token to a recipient
    async sendToken(token, recipientPubkey) {
        // Create a direct reference to recipient
        const salt = this.TXF.generateRandom256BitHex();
        const tx = await this.TXF.createTx(token, recipientPubkey, salt, this.secret, this.transport);
        
        // Export the token flow for transmission
        const tokenFlow = JSON.parse(this.TXF.exportFlow(token, tx));
        
        // Remove from owned pool as it's being sent
        this.tokenPool.owned.delete(token.tokenId);
        
        return tokenFlow;
    }
    
    // Process a received token
    async receiveToken(tokenFlowStr) {
        const tokenFlow = typeof tokenFlowStr === 'string' ? JSON.parse(tokenFlowStr) : tokenFlowStr;
        
        // Import the token flow
        const token = this.TXF.importFlow(JSON.stringify(tokenFlow), this.secret);
        
        // Check token status
        const status = await this.TXF.getTokenStatus(token, this.secret, this.transport);
        
        if (status.owned && status.unspent) {
            // Add to owned pool
            this.tokenPool.owned.set(token.tokenId, token);
            return { success: true, token };
        } else {
            return { 
                success: false, 
                error: 'Token is not valid, not owned, or already spent' 
            };
        }
    }
    
    // Collect spendable tokens for a given token class
    async getSpendableTokens(tokenClassId) {
        const result = [];
        
        for (const token of this.tokenPool.owned.values()) {
            if (token.tokenClass === tokenClassId) {
                const status = await this.TXF.getTokenStatus(token, this.secret, this.transport);
                if (status.owned && status.unspent) {
                    result.push(token);
                }
            }
        }
        
        return result;
    }
    
    // Verify a game state token
    async verifyGameStateToken(stateToken) {
        // Import the token
        const token = typeof stateToken === 'string' 
            ? this.TXF.importFlow(stateToken) 
            : stateToken;
        
        // Verify token status
        const status = await this.TXF.getTokenStatus(token, null, this.transport);
        
        if (!status.valid) {
            return { verified: false, error: 'Invalid token' };
        }
        
        // Get state data from token
        try {
            const stateData = JSON.parse(token.tokenData);
            return { 
                verified: true, 
                stateHash: stateData.state_hash,
                timestamp: stateData.timestamp,
                gameId: stateData.game_id,
                frame: stateData.frame
            };
        } catch (error) {
            return { verified: false, error: 'Invalid state data in token' };
        }
    }
}

module.exports = { TokenService };

Step 2: Integrate with Game Server for Play-to-Earn

Extend the game server to handle tokens for play-to-earn:

const { TokenService } = require('../lib/token-service');

class QuakeGameServer {
    constructor(serverConfig) {
        // Existing server initialization
        
        // Initialize token service
        this.tokenService = new TokenService(
            serverConfig.secret, 
            serverConfig.serverName
        );
        
        // Track session tokens
        this.sessionTokens = new Map(); // Map player ID to their entry token
        
        // Set up message handlers for token operations
        this.setupTokenHandlers();
    }
    
    setupTokenHandlers() {
        // Handle entry token from client
        this.on('token:entry', async (clientId, message) => {
            try {
                const { tokenFlow } = message;
                
                // Process and validate the token
                const result = await this.tokenService.receiveToken(tokenFlow);
                
                if (result.success) {
                    // Store the token associated with this client
                    this.sessionTokens.set(clientId, result.token);
                    
                    // Send acknowledgment to client
                    this.send(clientId, 'token:entry:ack', { 
                        success: true,
                        message: 'Entry token accepted'
                    });
                    
                    console.log(`Client ${clientId} joined with valid entry token`);
                } else {
                    // Reject invalid token
                    this.send(clientId, 'token:entry:ack', {
                        success: false,
                        message: 'Invalid entry token'
                    });
                    
                    // Optionally disconnect the client
                    this.disconnectClient(clientId, 'Invalid entry token');
                }
            } catch (error) {
                console.error(`Error processing entry token from ${clientId}:`, error);
                this.send(clientId, 'token:entry:ack', { 
                    success: false,
                    message: `Error: ${error.message}`
                });
            }
        });
    }
    
    // Handle game end and distribute rewards
    async endGameSession(gameResult) {
        try {
            const { winnerId } = gameResult;
            
            // Skip reward if no winner or no tokens collected
            if (!winnerId || this.sessionTokens.size === 0) {
                console.log('No winner or no tokens to distribute');
                return;
            }
            
            console.log(`Game ended. Winner: ${winnerId}. Distributing ${this.sessionTokens.size} tokens`);
            
            // Get winner's public key from connection data
            const winnerPubkey = this.getClientPubkey(winnerId);
            
            if (!winnerPubkey) {
                console.error('Cannot find winner pubkey');
                return;
            }
            
            // Send all collected tokens to the winner
            const sentTokenFlows = [];
            
            for (const [clientId, token] of this.sessionTokens.entries()) {
                // Skip the winner's own token
                if (clientId === winnerId) continue;
                
                try {
                    // Send token to winner
                    const tokenFlow = await this.tokenService.sendToken(token, winnerPubkey);
                    sentTokenFlows.push(tokenFlow);
                } catch (error) {
                    console.error(`Error sending token from ${clientId} to winner:`, error);
                }
            }
            
            // Notify winner about rewards
            this.send(winnerId, 'token:reward', {
                tokens: sentTokenFlows,
                message: `Congratulations! You've won ${sentTokenFlows.length} tokens.`
            });
            
            // Clear session tokens
            this.sessionTokens.clear();
            
        } catch (error) {
            console.error('Error distributing rewards:', error);
        }
    }
    
    // Generate game state token for integrity verification
    async generateGameStateToken() {
        try {
            // Get current game state
            const gameState = this.getCurrentGameState();
            
            // Create a token with this state
            const stateToken = await this.tokenService.createGameStateToken(gameState);
            
            // Broadcast state token to all clients for verification
            this.broadcast('game:state:token', {
                tokenFlow: JSON.parse(this.tokenService.TXF.exportFlow(stateToken)),
                timestamp: Date.now(),
                frame: gameState.frame
            });
            
            return stateToken;
        } catch (error) {
            console.error('Error generating game state token:', error);
            return null;
        }
    }
    
    // Start periodic game state verification
    startStateVerification(intervalMs = 10000) {
        // Clear any existing interval
        if (this.stateVerificationInterval) {
            clearInterval(this.stateVerificationInterval);
        }
        
        // Set up new interval
        this.stateVerificationInterval = setInterval(async () => {
            if (this.isGameActive) {
                await this.generateGameStateToken();
            }
        }, intervalMs);
    }
    
    // Stop state verification
    stopStateVerification() {
        if (this.stateVerificationInterval) {
            clearInterval(this.stateVerificationInterval);
            this.stateVerificationInterval = null;
        }
    }
}

Step 3: Implement Client-Side Token Handling

Extend the game client to work with the play-to-earn system:

const { TokenService } = require('../lib/token-service');

class QuakeGameClient {
    constructor(clientConfig) {
        // Existing client initialization
        
        // Initialize token service
        this.tokenService = new TokenService(
            clientConfig.secret,
            clientConfig.username
        );
        
        // Set up token message handlers
        this.setupTokenHandlers();
    }
    
    setupTokenHandlers() {
        // Handle token rewards from server
        this.on('token:reward', async (message) => {
            console.log('Received token rewards!', message.message);
            
            // Process each received token
            const receivedTokens = [];
            for (const tokenFlow of message.tokens) {
                try {
                    const result = await this.tokenService.receiveToken(tokenFlow);
                    if (result.success) {
                        receivedTokens.push(result.token);
                    }
                } catch (error) {
                    console.error('Error processing reward token:', error);
                }
            }
            
            // Update UI with new tokens
            this.updateTokenUI();
            
            console.log(`Successfully received ${receivedTokens.length} reward tokens`);
        });
        
        // Handle game state verification tokens
        this.on('game:state:token', async (message) => {
            try {
                const { tokenFlow, timestamp, frame } = message;
                
                // Verify the state token
                const verification = await this.tokenService.verifyGameStateToken(tokenFlow);
                
                if (verification.verified) {
                    // Check if client's game state matches the verified state
                    const localState = this.getCurrentGameState();
                    const localStateHash = this.tokenService.TXF.hashObject(localState);
                    
                    if (verification.stateHash !== localStateHash) {
                        console.error('Game state mismatch detected!');
                        // Handle desynchronization - could request a state sync
                        this.requestStateSync();
                    } else {
                        console.log(`Game state verified at frame ${frame}`);
                    }
                } else {
                    console.error('Invalid game state token received:', verification.error);
                }
            } catch (error) {
                console.error('Error processing game state token:', error);
            }
        });
    }
    
    // Join a game by sending an entry token
    async joinGameWithToken(serverInfo) {
        try {
            // Get or mint a coin token for entry
            let entryTokens = await this.tokenService.getSpendableTokens(
                this.tokenService.tokenTypes.COIN
            );
            
            // If no tokens available, mint a new one
            if (entryTokens.length === 0) {
                console.log('No entry tokens available, minting a new one...');
                const newToken = await this.tokenService.mintCoin('10');
                entryTokens = [newToken];
            }
            
            // Use the first available token
            const entryToken = entryTokens[0];
            
            // Connect to server
            await this.connect(serverInfo);
            
            // Send entry token to server
            const tokenFlow = JSON.parse(this.tokenService.TXF.exportFlow(entryToken));
            this.send('token:entry', { tokenFlow });
            
            console.log('Sent entry token to server, waiting for acknowledgment...');
            
            // Server will respond with token:entry:ack
        } catch (error) {
            console.error('Error joining game with token:', error);
            throw error;
        }
    }
    
    // Update token UI to show current token inventory
    updateTokenUI() {
        // Implementation depends on the UI framework
        if (this.onTokenUpdate) {
            // Get token counts
            const coinTokens = Array.from(this.tokenService.tokenPool.owned.values())
                .filter(token => token.tokenClass === this.tokenService.tokenTypes.COIN);
            
            this.onTokenUpdate({
                coinCount: coinTokens.length,
                totalValue: coinTokens.reduce((sum, token) => sum + BigInt(token.tokenValue), BigInt(0)).toString()
            });
        }
    }
}

Step 4: Implement Game State Serialization and Verification

Create a module to handle consistent game state hashing and verification:

// lib/game-state-verifier.js

class GameStateVerifier {
    constructor(txf) {
        this.TXF = txf;
    }
    
    // Create a deterministic serialization of game state
    serializeGameState(gameState) {
        // Extract essential state data
        const essentialState = {
            gameId: gameState.gameId,
            frame: gameState.frame,
            timestamp: gameState.timestamp,
            players: Object.fromEntries(
                Object.entries(gameState.players).map(([id, player]) => [
                    id,
                    {
                        position: player.position,
                        health: player.health,
                        score: player.score,
                        // Only include necessary fields for state verification
                    }
                ])
            ),
            // Include other essential state components
            // but exclude visual or non-deterministic elements
        };
        
        // Sort object keys for deterministic serialization
        return JSON.stringify(essentialState, (key, value) => {
            if (value && typeof value === 'object' && !Array.isArray(value)) {
                return Object.keys(value).sort().reduce((obj, key) => {
                    obj[key] = value[key];
                    return obj;
                }, {});
            }
            return value;
        });
    }
    
    // Hash a game state for compact representation
    hashGameState(gameState) {
        const serialized = this.serializeGameState(gameState);
        return this.TXF.sha256(serialized);
    }
    
    // Verify if a local state matches a remote state hash
    verifyStateHash(localState, remoteStateHash) {
        const localHash = this.hashGameState(localState);
        return localHash === remoteStateHash;
    }
    
    // Create a signed proof of the game state
    async createStateProof(gameState, secret) {
        const stateHash = this.hashGameState(gameState);
        
        // Create a signature of the state hash using the secret
        const signature = this.TXF.sign(stateHash, secret);
        
        return {
            stateHash,
            signature,
            timestamp: Date.now(),
            frame: gameState.frame,
            gameId: gameState.gameId
        };
    }
    
    // Verify a state proof against a public key
    verifyStateProof(proof, pubkey) {
        return this.TXF.verify(proof.stateHash, proof.signature, pubkey);
    }
}

module.exports = { GameStateVerifier };

Demo Implementation Flow

1. Play-to-Earn Flow

Client 1                  Game Server                  Client 2
   |                           |                           |
   |                           |                           |
   |-- Mint entry token        |                           |
   |                           |                           |
   |                           |                           |-- Mint entry token
   |                           |                           |
   |-- Connect + Send token -->|                           |
   |                           |-- Validate token          |
   |<-- Accept connection -----|                           |
   |                           |                           |
   |                           |<-- Connect + Send token --|
   |                           |-- Validate token          |
   |                           |-- Accept connection ------>|
   |                           |                           |
   |                           |                           |
   |<--------- Game session with both clients ------------>|
   |                           |                           |
   |                           |-- Determine winner (1)    |
   |                           |                           |
   |<-- Send collected tokens --|                           |
   |                           |                           |
   |-- Process received tokens |                           |
   |                           |                           |

2. Game State Verification Flow

Client 1                Game Server                Client 2                Unicity Service
   |                        |                         |                         |
   |                        |                         |                         |
   |                        |-- Capture game state    |                         |
   |                        |-- Hash game state       |                         |
   |                        |                         |                         |
   |                        |-- Request proof --------|------------------------>|
   |                        |<-- Return proof --------|-------------------------|
   |                        |                         |                         |
   |                        |-- Create state token    |                         |
   |                        |                         |                         |
   |<-- Send state token ---|                         |                         |
   |                        |-- Send state token ---->|                         |
   |                        |                         |                         |
   |-- Verify token         |                         |-- Verify token          |
   |-- Compare with local   |                         |-- Compare with local    |
   |   game state           |                         |   game state            |
   |                        |                         |                         |

Best Practices for the Demo

  1. Token Management:

    • Keep token operations simple and focused on the demo goals
    • Use a fixed value for entry tokens (e.g., '10')
    • Store tokens in memory for demo simplicity
    • Use descriptive token IDs for easier debugging
  2. Error Handling:

    • Implement robust error handling for token operations
    • Provide clear error messages in the UI/console
    • Create fallback mechanisms for failed token operations
    • Log all token-related events for troubleshooting
  3. Performance Considerations:

    • Keep game state tokens small by only including essential data
    • Generate state tokens at reasonable intervals (10s is good for demo)
    • Don't verify state during high-activity gameplay moments
    • Consider throttling token operations during intense gameplay
  4. Testing the Demo:

    • Test with multiple clients to verify correct token collection
    • Verify that all collected tokens are properly sent to the winner
    • Test state verification by deliberately modifying a client's state
    • Verify that the system correctly identifies and addresses state mismatches

Conclusion

This focused demo integration plan provides a clear path to implementing play-to-earn mechanics and game state verification in UniQuake using the state-transition-SDK. By leveraging the existing P2P networking infrastructure and implementing in-memory token management, the demo can showcase the power of blockchain-like token functionality without the complexity of distributed storage or external dependencies beyond the Unicity service itself.

The implementation demonstrates how games can incorporate verifiable digital assets and ensure gameplay integrity through cryptographic proofs, while maintaining the performance and responsiveness required for an action game like UniQuake.