Last active
July 13, 2025 13:21
-
-
Save homakov/0678179ec5bbb123cf77befd26bb1cb8 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// === TYPES === | |
declare const console: any; | |
let DEBUG = true; | |
interface ServerInput { | |
serverTxs: ServerTx[]; | |
entityInputs: EntityInput[]; | |
} | |
interface ServerTx { | |
type: 'importReplica'; | |
entityId: string; | |
signerId: string; | |
data: { | |
validators: string[]; | |
threshold: number; | |
isProposer: boolean; | |
}; | |
} | |
interface EntityInput { | |
entityId: string; | |
signerId: string; | |
entityTxs?: EntityTx[]; | |
precommits?: string[]; | |
proposedFrame?: string; | |
} | |
interface EntityTx { | |
type: string; | |
data: any; | |
} | |
// === STATE === | |
interface EntityState { | |
height: number; | |
nonces: Map<string, number>; | |
messages: string[]; | |
validators: string[]; | |
threshold: number; | |
} | |
interface ProposedEntityFrame { | |
height: number; | |
txs: EntityTx[]; | |
hash: string; | |
newState: EntityState; | |
signatures: Set<string>; | |
} | |
interface EntityReplica { | |
entityId: string; | |
signerId: string; | |
state: EntityState; | |
mempool: EntityTx[]; | |
proposal?: ProposedEntityFrame; | |
isProposer: boolean; | |
} | |
interface ServerState { | |
replicas: Map<string, EntityReplica>; | |
height: number; | |
timestamp: number; | |
} | |
// === ENTITY PROCESSING === | |
const applyEntityTx = (entityState: EntityState, entityTx: EntityTx): EntityState => { | |
if (entityTx.type === 'chat') { | |
const { from, message } = entityTx.data; | |
const currentNonce = entityState.nonces.get(from) || 0; | |
// Create new state (immutable at transaction level) | |
const newEntityState = { | |
...entityState, | |
nonces: new Map(entityState.nonces), | |
messages: [...entityState.messages] | |
}; | |
newEntityState.nonces.set(from, currentNonce + 1); | |
newEntityState.messages.push(`${from}: ${message}`); | |
return newEntityState; | |
} | |
return entityState; | |
}; | |
const applyEntityFrame = (entityState: EntityState, entityTxs: EntityTx[]): EntityState => { | |
return entityTxs.reduce((currentEntityState, entityTx) => applyEntityTx(currentEntityState, entityTx), entityState); | |
}; | |
// === PROCESSING === | |
const processEntityInput = (entityReplica: EntityReplica, entityInput: EntityInput): EntityInput[] => { | |
const entityOutbox: EntityInput[] = []; | |
// Add transactions to mempool (mutable for performance) | |
if (entityInput.entityTxs?.length) { | |
entityReplica.mempool.push(...entityInput.entityTxs); | |
if (DEBUG) console.log(` → Added ${entityInput.entityTxs.length} txs to mempool (total: ${entityReplica.mempool.length})`); | |
} | |
// Handle proposed frame (PROPOSE phase) | |
if (entityInput.proposedFrame && !entityReplica.proposal) { | |
const frameSignature = `sig_${entityReplica.signerId}_${entityInput.proposedFrame}`; | |
const proposerId = entityReplica.state.validators[0]; // First validator is proposer | |
entityOutbox.push({ | |
entityId: entityInput.entityId, | |
signerId: proposerId, | |
precommits: [frameSignature] | |
}); | |
if (DEBUG) console.log(` → Signed proposal, sending precommit to ${proposerId}`); | |
} | |
// Handle precommits (SIGN phase) | |
if (entityInput.precommits?.length && entityReplica.proposal) { | |
// Collect signatures (mutable for performance) | |
entityInput.precommits.forEach(frameSignature => entityReplica.proposal!.signatures.add(frameSignature)); | |
if (DEBUG) console.log(` → Collected ${entityInput.precommits.length} signatures (total: ${entityReplica.proposal.signatures.size}/${entityReplica.state.threshold})`); | |
// Check threshold | |
if (entityReplica.proposal.signatures.size >= entityReplica.state.threshold) { | |
// Commit phase - use pre-computed state | |
entityReplica.state = entityReplica.proposal.newState; | |
if (DEBUG) console.log(` → Threshold reached! Committing frame, height: ${entityReplica.state.height}`); | |
// Save proposal data before clearing | |
const committedSignatures = Array.from(entityReplica.proposal.signatures); | |
const committedHash = entityReplica.proposal.hash; | |
// Clear state (mutable) | |
entityReplica.mempool.length = 0; | |
entityReplica.proposal = undefined; | |
// Notify all validators | |
entityReplica.state.validators.forEach(validatorId => { | |
entityOutbox.push({ | |
entityId: entityInput.entityId, | |
signerId: validatorId, | |
precommits: committedSignatures, | |
proposedFrame: committedHash | |
}); | |
}); | |
if (DEBUG) console.log(` → Sending commit notifications to ${entityReplica.state.validators.length} validators`); | |
} | |
} | |
// Handle commit notifications (when receiving finalized frame from proposer) | |
if (entityInput.precommits?.length && entityInput.proposedFrame && !entityReplica.proposal && entityInput.precommits.length >= entityReplica.state.threshold) { | |
// This is a commit notification from proposer, apply the frame | |
// We need to reconstruct the frame from the hash (simplified approach) | |
// In real implementation, we'd validate the frame content | |
if (DEBUG) console.log(` → Received commit notification with ${entityInput.precommits.length} signatures`); | |
// For now, just apply the mempool that should match the committed frame | |
if (entityReplica.mempool.length > 0) { | |
const newEntityState = applyEntityFrame(entityReplica.state, entityReplica.mempool); | |
entityReplica.state = newEntityState; | |
entityReplica.mempool.length = 0; | |
if (DEBUG) console.log(` → Applied commit, new state: ${entityReplica.state.messages.length} messages`); | |
} | |
} | |
// Auto-propose if mempool not empty and we're proposer | |
if (entityReplica.isProposer && entityReplica.mempool.length > 0 && !entityReplica.proposal) { | |
// Compute new state once during proposal | |
const newEntityState = applyEntityFrame(entityReplica.state, entityReplica.mempool); | |
entityReplica.proposal = { | |
height: entityReplica.state.height + 1, | |
txs: [...entityReplica.mempool], | |
hash: `frame_${entityReplica.state.height + 1}_${Date.now()}`, | |
newState: newEntityState, | |
signatures: new Set<string>() | |
}; | |
if (DEBUG) console.log(` → Auto-proposing frame ${entityReplica.proposal.hash} with ${entityReplica.proposal.txs.length} txs`); | |
// Send proposal to all validators | |
entityReplica.state.validators.forEach(validatorId => { | |
entityOutbox.push({ | |
entityId: entityInput.entityId, | |
signerId: validatorId, | |
proposedFrame: entityReplica.proposal!.hash, | |
entityTxs: entityReplica.proposal!.txs | |
}); | |
}); | |
} | |
return entityOutbox; | |
}; | |
const processServerInput = (serverState: ServerState, serverInput: ServerInput): EntityInput[] => { | |
const entityOutbox: EntityInput[] = []; | |
if (DEBUG) { | |
console.log(`\n=== TICK ${serverState.height} ===`); | |
console.log(`Server inputs: ${serverInput.serverTxs.length} serverTxs, ${serverInput.entityInputs.length} entityInputs`); | |
} | |
// Process server transactions (replica imports) | |
serverInput.serverTxs.forEach(serverTx => { | |
if (serverTx.type === 'importReplica') { | |
if (DEBUG) console.log(`Importing replica ${serverTx.entityId}:${serverTx.signerId} (proposer: ${serverTx.data.isProposer})`); | |
const replicaKey = `${serverTx.entityId}:${serverTx.signerId}`; | |
serverState.replicas.set(replicaKey, { | |
entityId: serverTx.entityId, | |
signerId: serverTx.signerId, | |
state: { | |
height: 0, | |
nonces: new Map(), | |
messages: [], | |
validators: serverTx.data.validators, | |
threshold: serverTx.data.threshold | |
}, | |
mempool: [], | |
isProposer: serverTx.data.isProposer | |
}); | |
} | |
}); | |
// Process entity inputs | |
serverInput.entityInputs.forEach(entityInput => { | |
const replicaKey = `${entityInput.entityId}:${entityInput.signerId}`; | |
const entityReplica = serverState.replicas.get(replicaKey); | |
if (entityReplica) { | |
if (DEBUG) { | |
console.log(`Processing input for ${replicaKey}:`); | |
if (entityInput.entityTxs?.length) console.log(` → ${entityInput.entityTxs.length} transactions`); | |
if (entityInput.proposedFrame) console.log(` → Proposed frame: ${entityInput.proposedFrame}`); | |
if (entityInput.precommits?.length) console.log(` → ${entityInput.precommits.length} precommits`); | |
} | |
const entityOutputs = processEntityInput(entityReplica, entityInput); | |
entityOutbox.push(...entityOutputs); | |
} | |
}); | |
// Update server state (mutable) | |
serverState.height++; | |
serverState.timestamp = Date.now(); | |
if (DEBUG && entityOutbox.length > 0) { | |
console.log(`Outputs: ${entityOutbox.length} messages`); | |
entityOutbox.forEach((output, i) => { | |
console.log(` ${i+1}. → ${output.signerId} (${output.entityTxs ? `${output.entityTxs.length} txs` : ''}${output.proposedFrame ? ` proposal: ${output.proposedFrame.slice(0,10)}...` : ''}${output.precommits ? ` ${output.precommits.length} precommits` : ''})`); | |
}); | |
} | |
if (DEBUG) { | |
console.log(`Replica states:`); | |
serverState.replicas.forEach((replica, key) => { | |
console.log(` ${key}: mempool=${replica.mempool.length}, messages=${replica.state.messages.length}, proposal=${replica.proposal ? '✓' : '✗'}`); | |
}); | |
} | |
return entityOutbox; | |
}; | |
// === DEMO === | |
const processUntilEmpty = (serverState: ServerState, inputs: EntityInput[]) => { | |
let outputs = inputs; | |
while (outputs.length > 0) { | |
outputs = processServerInput(serverState, { serverTxs: [], entityInputs: outputs }); | |
} | |
}; | |
const runDemo = () => { | |
const serverState: ServerState = { replicas: new Map(), height: 0, timestamp: Date.now() }; | |
if (DEBUG) console.log('🚀 Starting XLN Consensus Demo'); | |
// Import replicas (separate serverTx for each, like in real distributed servers) | |
const validators = ['alice', 'bob', 'carol']; | |
processServerInput(serverState, { | |
serverTxs: validators.map((signerId, index) => ({ | |
type: 'importReplica' as const, | |
entityId: 'chat', | |
signerId, | |
data: { | |
validators, | |
threshold: 2, | |
isProposer: index === 0 | |
} | |
})), | |
entityInputs: [] | |
}); | |
// Send multiple chat messages and process consensus | |
processUntilEmpty(serverState, [{ | |
entityId: 'chat', | |
signerId: 'alice', | |
entityTxs: [{ type: 'chat', data: { from: 'alice', message: 'Hello world!' } }] | |
}]); | |
// All transactions must go through proposer (alice) for consensus | |
processUntilEmpty(serverState, [{ | |
entityId: 'chat', | |
signerId: 'alice', // proposer receives transaction from bob | |
entityTxs: [{ type: 'chat', data: { from: 'bob', message: 'Hi Alice! Bob here.' } }] | |
}]); | |
processUntilEmpty(serverState, [{ | |
entityId: 'chat', | |
signerId: 'alice', // proposer receives transaction from carol | |
entityTxs: [{ type: 'chat', data: { from: 'carol', message: 'Hey everyone! Carol joining.' } }] | |
}]); | |
processUntilEmpty(serverState, [{ | |
entityId: 'chat', | |
signerId: 'alice', | |
entityTxs: [ | |
{ type: 'chat', data: { from: 'alice', message: 'Nice to see you both!' } }, | |
{ type: 'chat', data: { from: 'alice', message: 'How is the consensus working?' } } | |
] | |
}]); | |
if (DEBUG) { | |
console.log('\n✅ Final State:'); | |
const allMessages: string[][] = []; | |
serverState.replicas.forEach((replica, key) => { | |
console.log(`${key}: ${replica.state.messages.length} messages, height ${replica.state.height}`); | |
if (replica.state.messages.length > 0) { | |
replica.state.messages.forEach((msg, i) => console.log(` ${i+1}. ${msg}`)); | |
} | |
allMessages.push([...replica.state.messages]); | |
}); | |
// Check consensus - all replicas should have identical message history | |
const firstMessages = allMessages[0]; | |
const allIdentical = allMessages.every(messages => | |
messages.length === firstMessages.length && | |
messages.every((msg, i) => msg === firstMessages[i]) | |
); | |
console.log(`\n🔍 Consensus Check: ${allIdentical ? '✅ SUCCESS' : '❌ FAILED'} - All replicas have ${allIdentical ? 'identical' : 'different'} chat history`); | |
if (allIdentical) { | |
console.log(`📊 Total messages agreed upon: ${firstMessages.length}`); | |
} | |
} | |
// Return immutable snapshot for API boundary | |
return { | |
replicas: new Map(serverState.replicas), | |
height: serverState.height, | |
timestamp: serverState.timestamp | |
}; | |
}; | |
const main = () => { | |
const finalState = runDemo(); | |
return finalState; | |
}; | |
// Auto-run demo | |
main(); | |
export { runDemo, processServerInput, main }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment