diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4b12dff..2aa08fc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -135,7 +135,7 @@ jobs: - name: ๐Ÿงช Run unit tests run: | - echo "Running all 46 unit tests..." + echo "Running all 77 unit tests..." npm test - name: ๐Ÿ“Š Test summary @@ -145,8 +145,9 @@ jobs: echo "Test Results Summary:" echo "- Address tests: Should be 21/21 โœ…" echo "- WOTS tests: Should be 14/14 โœ…" + echo "- Deterministic tests: Should be 31/31 โœ…" echo "- Transaction SDK tests: Should be 11/11 โœ…" - echo "- Total: Should be 46/46 โœ…" + echo "- Total: Should be 77/77 โœ…" echo "================================" package-validation: diff --git a/README.md b/README.md index 36b5c3d..6bf0b19 100644 --- a/README.md +++ b/README.md @@ -965,6 +965,53 @@ console.log('Found:', result.found); --- +#### `getNetworkDsaHash(accountTag, apiUrl)` +Retrieves just the current DSA Hash for an account tag from the network. This is a convenience function for spend index recovery scenarios. + +```javascript +import { getNetworkDsaHash, deriveKeypairForSpend } from 'mochimo'; + +// Recover spend index after database loss +const networkDsaHash = await getNetworkDsaHash( + '9f810c2447a76e93b17ebff96c0b29952e4355f1', + 'https://api.mochimo.org' +); + +if (!networkDsaHash) { + console.log('Account not found or never spent - spend index is 0'); +} else { + // Iterate to find matching spend index + for (let spendIndex = 0; spendIndex < 1000; spendIndex++) { + const keypair = deriveKeypairForSpend(masterSeed, spendIndex, accountIndex); + // Extract DSA component (last 20 bytes of 40-byte implicit address) + const derivedDsaHash = keypair.dsaHash.toString('hex').slice(40, 80); + + if (derivedDsaHash === networkDsaHash) { + console.log('Recovered spend index:', spendIndex); + break; + } + } +} +``` + +**Parameters**: +- `accountTag` (string|Buffer) โ€“ Account tag to query (20 bytes / 40 hex characters) +- `apiUrl` (string) โ€“ Mesh API endpoint, e.g. `https://api.mochimo.org` + +**Returns**: Promise resolving to: +- `string` โ€“ Current DSA hash as hex string (40 characters / 20 bytes) +- `null` โ€“ If account not found on blockchain or never spent + +**Use Cases**: +- Disaster recovery after database loss +- Spend index verification and audit +- System migration validation +- Network fork recovery + +**See**: [examples/exchange/5-recover-spend-index.js](examples/exchange/5-recover-spend-index.js) for complete recovery workflow + +--- + ### Low-Level Exports (Advanced Users) These functions are exported for advanced users who need low-level control. Most integrators should use the higher-level functions above. diff --git a/examples/exchange/5-recover-spend-index.js b/examples/exchange/5-recover-spend-index.js index 947c5ed..fc258e6 100644 --- a/examples/exchange/5-recover-spend-index.js +++ b/examples/exchange/5-recover-spend-index.js @@ -21,7 +21,7 @@ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; -import { deriveKeypairForSpend } from '../../src/core/deterministic.js'; +import { deriveKeypairForSpend, getNetworkDsaHash } from '../../src/index.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -78,10 +78,51 @@ console.log(' This proves the current spend index based on blockchain state.'); console.log(); // ============================================================================ -// STEP 2: Query Network for Current Account State +// STEP 2: Query Network for Current DSA Hash (Using SDK Helper) // ============================================================================ -console.log('Step 2: Query Network for Current Account State'); +console.log('Step 2: Query Network for Current DSA Hash'); +console.log('-'.repeat(70)); +console.log(); + +console.log('Using SDK function: getNetworkDsaHash()'); +console.log(); + +let networkDsaHash = null; + +try { + networkDsaHash = await getNetworkDsaHash(userAccount.account_tag, API_URL); + + if (networkDsaHash) { + console.log('โœ“ Current DSA Hash Retrieved from Network:'); + console.log(' DSA Hash:', networkDsaHash); + console.log(); + console.log('โ„น๏ธ This represents the WOTS+ public key hash currently'); + console.log(' associated with this account on the blockchain.'); + console.log(); + } else { + console.log('โ„น๏ธ Account not found or never spent'); + console.log(' This is normal for new accounts that haven\'t sent transactions.'); + console.log(' Current spend index: 0'); + console.log(); + } +} catch (error) { + console.error('โš ๏ธ Error querying network:', error.message); + console.error(); + console.error('This could indicate:'); + console.error(' - Network connectivity issues'); + console.error(' - Invalid account tag format'); + console.error(' - API endpoint unavailable'); + console.error(); + console.error('For this demo, we\'ll continue with iteration method.'); + console.error(); +} + +// ============================================================================ +// STEP 3: Alternative - Manual Account Info Query (Old Method) +// ============================================================================ + +console.log('Step 3: Legacy Method - Manual Account Balance Query'); console.log('-'.repeat(70)); console.log(); @@ -127,11 +168,8 @@ async function getAccountInfo(accountTag) { } console.log(); - // For this demo, we need to extract the DSA Hash from transaction history - // or use the search API to find the last transaction - console.log('โ„น๏ธ Note: To get the current DSA Hash, we need to check'); - console.log(' transaction history or use the tag resolution endpoint.'); - console.log(' For this example, we\'ll iterate to find it.'); + console.log('โ„น๏ธ Note: The SDK\'s getNetworkDsaHash() function above'); + console.log(' provides a simpler way to get just the DSA Hash.'); console.log(); return data; @@ -221,25 +259,28 @@ async function getCurrentDsaFromTransactions(accountTag) { await getCurrentDsaFromTransactions(userAccount.account_tag); // ============================================================================ -// STEP 4: Recover Spend Index by Iteration +// STEP 5: Recover Spend Index by Iteration // ============================================================================ -console.log('Step 4: Recover Spend Index by Iteration'); +console.log('Step 5: Recover Spend Index by Iteration'); console.log('-'.repeat(70)); console.log(); -console.log('๐Ÿ” Recovery Method: Brute Force Iteration'); +console.log('๐Ÿ” Recovery Method: Iterative Keypair Comparison'); console.log(' We will generate WOTS+ keypairs for each spend index'); -console.log(' and compare with known/expected values.'); +console.log(' and compare DSA hashes until we find a match.'); console.log(); -console.log('โš ๏ธ In production, you would:'); -console.log(' 1. Query network for current DSA Hash using tag resolution API'); -console.log(' 2. Iterate through spend indices until DSA Hash matches'); -console.log(' 3. That tells you the current spend index'); +if (networkDsaHash) { + console.log('โœ“ Using DSA Hash from network:', networkDsaHash.substring(0, 20) + '...'); + console.log(' We will iterate to find which spend index produces this hash.'); +} else { + console.log('โ„น๏ธ No DSA Hash available from network (account not spent)'); + console.log(' We will verify our stored spend index by deriving keypairs.'); +} console.log(); -async function recoverSpendIndex(masterSeed, accountIndex, accountTag, currentStoredIndex) { +async function recoverSpendIndex(masterSeed, accountIndex, accountTag, currentStoredIndex, targetDsaHash) { console.log('Starting spend index recovery...'); console.log(' Account Index:', accountIndex); console.log(' Account Tag:', accountTag); @@ -256,30 +297,57 @@ async function recoverSpendIndex(masterSeed, accountIndex, accountTag, currentSt const keypairs = []; for (let spendIndex = 0; spendIndex <= Math.min(currentStoredIndex + 5, MAX_ITERATIONS); spendIndex++) { const keypair = deriveKeypairForSpend(masterSeed, spendIndex, accountIndex); + + // Extract just the DSA Hash component (last 20 bytes of the 40-byte implicit address) + const dsaHashComponent = keypair.dsaHash.toString('hex').slice(40, 80); + keypairs.push({ spendIndex, accountTag: keypair.accountTagHex, - dsaHash: keypair.dsaHashHex + dsaHash: keypair.dsaHashHex, + dsaHashComponent }); + // If we have a target DSA Hash from the network, check for match + if (targetDsaHash && dsaHashComponent === targetDsaHash) { + console.log(`๐ŸŽฏ MATCH FOUND at spend index ${spendIndex}!`); + console.log(` Network DSA Hash: ${targetDsaHash}`); + console.log(` Derived DSA Hash: ${dsaHashComponent}`); + foundIndex = spendIndex; + break; + } + if (spendIndex <= 5 || spendIndex === currentStoredIndex || spendIndex === currentStoredIndex + 1) { - console.log(` Spend ${spendIndex}: DSA = ${keypair.dsaHashHex.substring(0, 30)}...`); + console.log(` Spend ${spendIndex}: DSA Component = ${dsaHashComponent.substring(0, 30)}...`); } } console.log(); - // In production: Compare each DSA Hash with the network's current value - // For this demo, we'll verify our stored index matches + // Verification against stored index console.log('๐Ÿ“Š Verification:'); - console.log(' Stored spend index:', currentStoredIndex); - - if (currentStoredIndex < keypairs.length) { + + if (targetDsaHash && foundIndex !== -1) { + console.log(' โœ“ Recovered from network DSA Hash'); + console.log(' Found spend index:', foundIndex); + console.log(' Stored spend index:', currentStoredIndex); + console.log(' Match:', foundIndex === currentStoredIndex ? 'โœ“ Yes' : 'โœ— No - DATABASE OUT OF SYNC!'); + console.log(); + + if (foundIndex !== currentStoredIndex) { + console.log('โš ๏ธ WARNING: Stored spend index does not match blockchain state!'); + console.log(' This indicates database corruption or missed transaction.'); + console.log(' Update database to use recovered index:', foundIndex); + console.log(); + } + } else if (currentStoredIndex < keypairs.length) { + console.log(' No network DSA Hash available (account not spent)'); + console.log(' Verifying stored spend index:', currentStoredIndex); + const storedKeypair = keypairs[currentStoredIndex]; - console.log(' Expected DSA Hash:', storedKeypair.dsaHash); + console.log(' DSA Hash for index', currentStoredIndex + ':', storedKeypair.dsaHashComponent); console.log(); - console.log('โœ“ Verification successful!'); - console.log(' The stored spend index matches the derived keypair.'); + console.log('โœ“ Stored index verified (will be current after first spend)'); foundIndex = currentStoredIndex; } @@ -296,14 +364,15 @@ const recoveredIndex = await recoverSpendIndex( masterSeed, userAccount.account_index, userAccount.account_tag, - userAccount.spend_index + userAccount.spend_index, + networkDsaHash // Pass the DSA Hash we got from getNetworkDsaHash() ); // ============================================================================ -// STEP 5: Production Implementation Example +// STEP 6: Production Implementation Example // ============================================================================ -console.log('Step 5: Production Recovery Implementation'); +console.log('Step 6: Production Recovery Implementation'); console.log('-'.repeat(70)); console.log(); @@ -311,16 +380,24 @@ console.log('๐Ÿ“ In a real exchange, the recovery process would be:'); console.log(); console.log('```javascript'); +console.log('import { getNetworkDsaHash, deriveKeypairForSpend } from "mochimo";'); +console.log(); console.log('async function recoverSpendIndexFromNetwork(accountTag, masterSeed, accountIndex) {'); console.log(' // 1. Query network for current DSA Hash'); -console.log(' const networkDsaHash = await getNetworkDsaHash(accountTag);'); +console.log(' const networkDsaHash = await getNetworkDsaHash(accountTag, "https://api.mochimo.org");'); +console.log(' '); +console.log(' if (!networkDsaHash) {'); +console.log(' // Account not found or never spent - spend index is 0'); +console.log(' return 0;'); +console.log(' }'); console.log(' '); console.log(' // 2. Iterate through spend indices'); console.log(' for (let spendIndex = 0; spendIndex < MAX_ITERATIONS; spendIndex++) {'); console.log(' const keypair = deriveKeypairForSpend(masterSeed, spendIndex, accountIndex);'); console.log(' '); -console.log(' // 3. Check if DSA Hash matches network'); -console.log(' if (keypair.dsaHashHex === networkDsaHash) {'); +console.log(' // 3. Extract DSA Hash component and compare with network'); +console.log(' const derivedDsaHash = keypair.dsaHash.toString("hex").slice(40, 80);'); +console.log(' if (derivedDsaHash === networkDsaHash) {'); console.log(' console.log(`Found matching spend index: ${spendIndex}`);'); console.log(' '); console.log(' // 4. Update database'); diff --git a/examples/exchange/EXCHANGE_INTEGRATION.md b/examples/exchange/EXCHANGE_INTEGRATION.md index cd00780..961d358 100644 --- a/examples/exchange/EXCHANGE_INTEGRATION.md +++ b/examples/exchange/EXCHANGE_INTEGRATION.md @@ -838,7 +838,7 @@ spendIndex++; ### Example 4: Recover Spend Index -**File:** `4-recover-spend-index.js` +**File:** `5-recover-spend-index.js` **What it demonstrates:** - Account recovery from blockchain state @@ -848,12 +848,15 @@ spendIndex++; **Key Code:** ```javascript -// Get current ledger address from network -const accountInfo = await fetch( - `${NETWORK_ENDPOINT}/balance/${accountTag}` -); -const networkAddress = accountInfo.address; // 40-byte full address -const networkDsaHash = networkAddress.slice(40); // Last 20 bytes (DSA PK Hash component) +import { getNetworkDsaHash, deriveKeypairForSpend } from 'mochimo'; + +// Get current DSA Hash from network +const networkDsaHash = await getNetworkDsaHash(accountTag, 'https://api.mochimo.org'); + +if (!networkDsaHash) { + console.log('Account not found or never spent - spend index is 0'); + return 0; +} // Iterate to find matching spend index for (let testIndex = 0; testIndex < 1000; testIndex++) { @@ -863,9 +866,11 @@ for (let testIndex = 0; testIndex < 1000; testIndex++) { if (derivedDsaHash === networkDsaHash) { console.log('Recovered spend index:', testIndex); - break; + return testIndex; } } + +throw new Error('Could not recover spend index within reasonable range'); ``` **Use Cases:** diff --git a/src/index.js b/src/index.js index 545c085..0675cc7 100644 --- a/src/index.js +++ b/src/index.js @@ -32,7 +32,7 @@ export { createTransaction, signTransaction, serializeTransaction } from './core // Network functionality export { broadcastTransaction, getNetworkStatus } from './network/broadcast.js'; -export { getAccountBalance, resolveTag } from './network/account.js'; +export { getAccountBalance, resolveTag, getNetworkDsaHash } from './network/account.js'; // Utilities export { diff --git a/src/network/account.js b/src/network/account.js index a3fc59b..08a3665 100644 --- a/src/network/account.js +++ b/src/network/account.js @@ -168,6 +168,59 @@ export async function resolveTag(tag, apiUrl) { } } +/** + * Get the current DSA Hash for an Account Tag from the network + * + * This is a convenience function for spend index recovery scenarios. + * It queries the Mochimo network and returns just the DSA Hash component, + * which represents the current WOTS+ public key hash for the account. + * + * Use this when recovering spend indices after database loss/corruption: + * you can iterate through possible spend indices, derive keypairs, and + * compare their DSA hashes with this network value to find the current index. + * + * @param {string|Buffer} accountTag - Account Tag (20 bytes / 40 hex characters) + * @param {string} apiUrl - API endpoint URL (e.g., 'https://api.mochimo.org') + * @returns {Promise} Current DSA Hash as hex string (40 chars), or null if account not found + * + * @example + * // Recover spend index after database loss + * const networkDsaHash = await getNetworkDsaHash(accountTag, 'https://api.mochimo.org'); + * if (!networkDsaHash) { + * // Account not found or never spent + * return; + * } + * + * // Iterate to find matching spend index + * for (let spendIndex = 0; spendIndex < 1000; spendIndex++) { + * const keypair = deriveKeypairForSpend(masterSeed, spendIndex, accountIndex); + * const derivedDsaHash = keypair.dsaHash.toString('hex').slice(40, 80); // Extract DSA component + * + * if (derivedDsaHash === networkDsaHash) { + * // Found matching spend index + * break; + * } + * } + */ +export async function getNetworkDsaHash(accountTag, apiUrl) { + try { + const result = await resolveTag(accountTag, apiUrl); + + if (!result.found || !result.dsaHash) { + return null; + } + + return result.dsaHash; + } catch (error) { + // If account not found, return null (not an error for new accounts) + if (error.message.includes('not found') || error.message.includes('404')) { + return null; + } + // Re-throw other errors (network issues, invalid input, etc.) + throw error; + } +} + /** * Format balance from nanoMCM to MCM * @param {number|string} nanoMCM - Balance in nanoMCM