This commit is contained in:
Your Name
2025-09-18 10:17:34 -04:00
parent 1b28f78f44
commit ecf248dfc2
4 changed files with 669 additions and 129 deletions

View File

@@ -478,7 +478,7 @@
bounceSection.id = `bounce-${bounceId}`;
bounceSection.innerHTML = `
<h2>🏀 Bounce ${bounceId} (Kind 22222 Routing Event)</h2>
<h2 id="bounce-${bounceId}-header">🏀 Bounce ${bounceId} (Kind 22222 Routing Event)</h2>
<div class="input-group">
<label for="superball-pubkey-${bounceId}">Superball Pubkey:</label>
<div style="display: flex; gap: 10px;">
@@ -506,7 +506,7 @@
<label for="audit-tag-${bounceId}">Audit Tag (auto-generated):</label>
<input type="text" id="audit-tag-${bounceId}" readonly style="background: #f5f5f5;">
</div>
<button onclick="createBounce(${bounceId})">Create Bounce ${bounceId}</button>
<button onclick="createBounce(${bounceId})" id="create-bounce-btn-${bounceId}">Create Bounce ${bounceId}</button>
<div id="bounce-${bounceId}-display" class="json-display"></div>
<div style="text-align: right; margin-top: 5px;">
@@ -524,8 +524,37 @@
// Automatically generate and fill in the audit tag
const auditTag = generateAuditTag();
document.getElementById(`audit-tag-${bounceId}`).value = auditTag;
// Update bounce labels to reflect execution order
updateBounceLabels();
}
// Update bounce labels to reflect execution order (newest bounce is Bounce 1, oldest is Bounce N)
function updateBounceLabels() {
// Get all existing bounce sections
const bounceContainer = document.getElementById('bounces-container');
const bounceSections = bounceContainer.querySelectorAll('.bounce-section');
// Update labels in reverse order (newest first gets Bounce 1)
bounceSections.forEach((section, index) => {
const bounceId = section.id.replace('bounce-', '');
const executionOrder = bounceSections.length - index; // Reverse the index
// Update the header
const header = document.getElementById(`bounce-${bounceId}-header`);
if (header) {
header.textContent = `🏀 Bounce ${executionOrder} (Kind 22222 Routing Event)`;
}
// Update the create button text
const createBtn = document.getElementById(`create-bounce-btn-${bounceId}`);
if (createBtn) {
createBtn.textContent = `Create Bounce ${executionOrder}`;
}
});
console.log('INFO: Updated bounce labels for execution order');
}
// Update all timeline absolute times continuously
function updateAllTimelineTimes() {
@@ -634,10 +663,11 @@
// Only add 'p' field if this isn't the last bounce
if (!isLastBounce) {
// Get the superball pubkey from the previous bounce
// Get the superball pubkey from the previous bounce (the next hop in the chain)
const prevBounce = bounces[bounces.length - 1];
routingInstructions.p = prevBounce.superballPubkey;
}
// Note: If this IS the last bounce (first one created), no 'p' field means final posting
// Create the payload to encrypt
const payload = {
@@ -712,6 +742,9 @@
generateVisualization();
console.log('SUCCESS', `Bounce ${bounceId} created successfully`);
// Update bounce labels after creation to reflect execution order
updateBounceLabels();
} catch (error) {
console.log('ERROR', `Failed to create bounce ${bounceId}: ${error.message}`);
@@ -779,12 +812,12 @@
}
try {
// Get the first (outermost) bounce to publish
const firstBounce = bounces[0];
const routingEvent = firstBounce.routingEvent;
// Get the last (outermost) bounce to publish - the most recently created bounce
const outermostBounce = bounces[bounces.length - 1];
const routingEvent = outermostBounce.routingEvent;
// Get relays to publish to - use the first bounce's target relays
const targetRelays = firstBounce.payload.routing.relays;
// Get relays to publish to - use the outermost bounce's target relays
const targetRelays = outermostBounce.payload.routing.relays;
if (!targetRelays || targetRelays.length === 0) {
alert('No target relays configured for the first bounce');

View File

@@ -898,6 +898,14 @@
if (message[0] === 'EVENT' && message[1] === subscriptionId) {
const nostrEvent = message[2];
addLogEntry('success', `Received EVENT from ${relayUrl}: ${nostrEvent.id.substring(0, 16)}...`);
// Truncate content for readability
const truncatedEvent = { ...nostrEvent };
if (truncatedEvent.content && truncatedEvent.content.length > 10) {
const content = truncatedEvent.content;
truncatedEvent.content = content.substring(0, 5) + '...' + content.substring(content.length - 5);
}
addLogEntry('info', `Full received event:\n${JSON.stringify(truncatedEvent, null, 2)}`);
handleIncomingEvent(nostrEvent);
} else if (message[0] === 'EOSE' && message[1] === subscriptionId) {
addLogEntry('info', `End of stored events from ${relayUrl}`);
@@ -928,18 +936,59 @@
try {
// Decrypt the event payload
const decryptedPayload = await decryptRoutingEvent(event);
let decryptedPayload = await decryptRoutingEvent(event);
if (!decryptedPayload) {
addLogEntry('error', `Failed to decrypt event ${event.id.substring(0,16)}...`);
return;
}
addLogEntry('success', `Successfully decrypted routing event ${event.id.substring(0,16)}...`);
addLogEntry('success', `First decryption successful for event ${event.id.substring(0,16)}...`);
// Parse routing instructions
// Check payload type according to corrected DAEMON.md protocol
if (decryptedPayload.padding !== undefined) {
addLogEntry('info', `Detected Type 2 (Padding Payload) - discarding padding and performing second decryption`);
// This is a padding layer from previous daemon - discard padding and decrypt again
const innerEvent = decryptedPayload.event;
addLogEntry('info', `Discarding padding: "${decryptedPayload.padding}"`);
// Second decryption to get the actual routing instructions that were encrypted for me
decryptedPayload = await decryptRoutingEvent(innerEvent);
if (!decryptedPayload) {
addLogEntry('error', `Failed to decrypt inner event ${event.id.substring(0,16)}...`);
return;
}
addLogEntry('success', `Second decryption successful - found my original routing instructions from builder`);
} else {
addLogEntry('info', `Detected Type 1 (Routing Payload) - processing routing instructions directly`);
}
// Log the complete decrypted payload with truncated content
const truncatedPayload = { ...decryptedPayload };
if (truncatedPayload.event && truncatedPayload.event.content && truncatedPayload.event.content.length > 10) {
const content = truncatedPayload.event.content;
truncatedPayload.event.content = content.substring(0, 5) + '...' + content.substring(content.length - 5);
}
addLogEntry('info', `Final routing payload:\n${JSON.stringify(truncatedPayload, null, 2)}`);
// Parse routing instructions (these are from the builder, specific to this daemon)
const { event: wrappedEvent, routing } = decryptedPayload;
if (!routing) {
addLogEntry('error', `No routing instructions found in final payload for ${event.id.substring(0,16)}...`);
return;
}
// DEBUG: Log routing decision
addLogEntry('info', `DEBUG routing.p = "${routing.p}" (type: ${typeof routing.p})`);
if (routing.add_padding_bytes) {
addLogEntry('info', `DEBUG add_padding_bytes = ${routing.add_padding_bytes}`);
}
addLogEntry('info', `DEBUG Will ${routing.p ? 'FORWARD to next hop' : 'POST FINAL EVENT'}`);
if (!validateRoutingInstructions(routing)) {
addLogEntry('error', `Invalid routing instructions in event ${event.id.substring(0,16)}...`);
return;
@@ -990,7 +1039,7 @@
return true;
}
// Process a queued event
// Process a queued event according to corrected DAEMON.md protocol
async function processQueuedEvent(queueItem) {
if (!daemonRunning) return;
@@ -1001,19 +1050,19 @@
try {
const { wrappedEvent, routing } = queueItem;
// Apply padding if specified
let eventToForward = { ...wrappedEvent };
if (routing.padding) {
eventToForward = applyPadding(eventToForward, routing.padding);
}
// Check if this is final posting or continued routing
addLogEntry('info', `DEBUG Decision point - routing.p = "${routing.p}" (${typeof routing.p})`);
if (routing.add_padding_bytes) {
addLogEntry('info', `DEBUG add_padding_bytes = ${routing.add_padding_bytes} (will add padding when forwarding)`);
}
addLogEntry('info', `DEBUG Will ${routing.p ? 'FORWARD with padding wrapper' : 'POST FINAL EVENT'}`);
if (routing.p) {
// Continue routing to next Superball
await forwardToNextSuperball(eventToForward, routing);
// Continue routing to next Superball with padding-only wrapper
await forwardToNextSuperball(wrappedEvent, routing);
} else {
// Final posting - post original event directly
await postFinalEvent(eventToForward, routing.relays);
// Final posting - post the wrapped event directly
await postFinalEvent(wrappedEvent, routing.relays);
}
processedEvents++;
@@ -1037,43 +1086,16 @@
}
}
// Apply padding to event
// Legacy padding function - no longer used in corrected protocol
// Padding is now handled during forwarding with add_padding_bytes
function applyPadding(event, paddingInstruction) {
if (!paddingInstruction || typeof paddingInstruction !== 'string') return event;
const match = paddingInstruction.match(/^([+-])(\d+)$/);
if (!match) return event;
const [, operation, amount] = match;
const padding = '1'.repeat(parseInt(amount));
const modifiedEvent = { ...event };
if (operation === '+') {
// Add padding
if (!modifiedEvent.tags.find(tag => tag[0] === 'padding')) {
modifiedEvent.tags.push(['padding', padding]);
}
addLogEntry('info', `Added ${amount} bytes of padding`);
} else if (operation === '-') {
// Remove padding
const paddingTagIndex = modifiedEvent.tags.findIndex(tag => tag[0] === 'padding');
if (paddingTagIndex !== -1) {
const currentPadding = modifiedEvent.tags[paddingTagIndex][1] || '';
const newPadding = currentPadding.substring(parseInt(amount));
if (newPadding.length > 0) {
modifiedEvent.tags[paddingTagIndex][1] = newPadding;
} else {
modifiedEvent.tags.splice(paddingTagIndex, 1);
}
}
addLogEntry('info', `Removed ${amount} bytes of padding`);
}
return modifiedEvent;
// This function is deprecated in the corrected protocol
// Padding is now generated during forwarding based on routing.add_padding_bytes
addLogEntry('info', 'Legacy padding function called - no action taken (using new protocol)');
return event;
}
// Forward event to next Superball (Always Rewrap)
// Forward event to next Superball with padding-only wrapper (DAEMON.md corrected protocol)
async function forwardToNextSuperball(event, routing) {
addLogEntry('info', `Forwarding to next Superball: ${routing.p.substring(0,16)}...`);
@@ -1081,54 +1103,84 @@
const ephemeralKey = window.NostrTools.generateSecretKey();
const ephemeralPubkey = window.NostrTools.getPublicKey(ephemeralKey);
// Create new encrypted payload
const payload = {
event: event,
routing: {
relays: routing.relays,
delay: routing.delay,
padding: routing.padding,
p: routing.p,
audit: routing.audit,
payment: routing.payment
// Generate padding based on add_padding_bytes instruction
let paddingData = '';
if (routing.add_padding_bytes && routing.add_padding_bytes > 0) {
// Create random padding of specified length
const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
for (let i = 0; i < routing.add_padding_bytes; i++) {
paddingData += chars.charAt(Math.floor(Math.random() * chars.length));
}
addLogEntry('info', `Generated ${paddingData.length} bytes of padding`);
}
// Create padding-only payload (NEVER create routing instructions - only builder does that)
const paddingPayload = {
event: event, // This is the still-encrypted inner event
padding: paddingData // Padding to discard
};
// Encrypt to next Superball
addLogEntry('info', `DEBUG Creating padding payload with ${paddingData.length} bytes of padding`);
// Log the complete padding payload with truncated content before encryption
const truncatedPaddingPayload = { ...paddingPayload };
if (truncatedPaddingPayload.event && truncatedPaddingPayload.event.content && truncatedPaddingPayload.event.content.length > 10) {
const content = truncatedPaddingPayload.event.content;
truncatedPaddingPayload.event.content = content.substring(0, 5) + '...' + content.substring(content.length - 5);
}
if (truncatedPaddingPayload.padding && truncatedPaddingPayload.padding.length > 10) {
const padding = truncatedPaddingPayload.padding;
truncatedPaddingPayload.padding = padding.substring(0, 5) + '...' + padding.substring(padding.length - 5);
}
addLogEntry('info', `Padding payload to encrypt:\n${JSON.stringify(truncatedPaddingPayload, null, 2)}`);
// Encrypt padding payload to next Superball
let ephemeralKeyHex;
if (window.NostrTools.utils && window.NostrTools.utils.bytesToHex) {
ephemeralKeyHex = window.NostrTools.utils.bytesToHex(ephemeralKey);
} else if (window.NostrTools.bytesToHex) {
ephemeralKeyHex = window.NostrTools.bytesToHex(ephemeralKey);
} else {
// Fallback: convert Uint8Array to hex manually
ephemeralKeyHex = Array.from(ephemeralKey).map(b => b.toString(16).padStart(2, '0')).join('');
}
const conversationKey = window.NostrTools.nip44.v2.utils.getConversationKey(
window.NostrTools.bytesToHex(ephemeralKey),
ephemeralKeyHex,
routing.p
);
const encryptedContent = window.NostrTools.nip44.v2.encrypt(
JSON.stringify(payload),
JSON.stringify(paddingPayload),
conversationKey
);
// Create new routing event
// Create new routing event (forwarding to next hop)
const routingEvent = {
kind: 22222,
content: encryptedContent,
tags: [
['p', routing.p], // Next Superball
['p', routing.audit] // Audit tag (looks like pubkey)
['p', routing.audit] // Audit tag (camouflage)
],
created_at: Math.floor(Date.now() / 1000)
};
// Add padding tag if present
const paddingTag = event.tags.find(tag => tag[0] === 'padding');
if (paddingTag) {
routingEvent.tags.push(['padding', paddingTag[1]]);
}
// Sign with ephemeral key
const signedEvent = window.NostrTools.finalizeEvent(routingEvent, ephemeralKey);
// Log the complete rewrapped event with truncated content before publishing
const truncatedSignedEvent = { ...signedEvent };
if (truncatedSignedEvent.content && truncatedSignedEvent.content.length > 10) {
const content = truncatedSignedEvent.content;
truncatedSignedEvent.content = content.substring(0, 5) + '...' + content.substring(content.length - 5);
}
addLogEntry('info', `Padding-wrapped event to publish:\n${JSON.stringify(truncatedSignedEvent, null, 2)}`);
// Publish to specified relays
await publishToRelays(signedEvent, routing.relays);
addLogEntry('success', `Forwarded event with audit tag ${routing.audit.substring(0,16)}...`);
addLogEntry('success', `Forwarded with padding layer to ${routing.p.substring(0,16)}... (audit: ${routing.audit.substring(0,16)}...)`);
}
// Post final event directly to relays
@@ -1147,6 +1199,14 @@
try {
await Promise.any(pool.publish(relays, event));
addLogEntry('success', `Published to relays: ${relays.join(', ')}`);
// Truncate content for readability
const truncatedEvent = { ...event };
if (truncatedEvent.content && truncatedEvent.content.length > 10) {
const content = truncatedEvent.content;
truncatedEvent.content = content.substring(0, 5) + '...' + content.substring(content.length - 5);
}
addLogEntry('info', `Full published event:\n${JSON.stringify(truncatedEvent, null, 2)}`);
} catch (aggregateError) {
const errorMessages = aggregateError.errors.map((err, index) =>
`${relays[index]}: ${err.message}`
@@ -1249,9 +1309,11 @@
logEntries.slice().reverse().forEach(entry => {
const div = document.createElement('div');
div.className = `log-entry ${entry.type}`;
div.innerHTML = `
<span class="log-timestamp">${entry.timestamp}</span> ${entry.message}
`;
// Handle multiline messages (like JSON) with proper formatting
const messageContent = entry.message.includes('\n') ?
`<span class="log-timestamp">${entry.timestamp}</span><br><pre style="margin: 5px 0; font-family: monospace; font-size: 11px; white-space: pre-wrap;">${entry.message}</pre>` :
`<span class="log-timestamp">${entry.timestamp}</span> ${entry.message}`;
div.innerHTML = messageContent;
container.appendChild(div);
});