From 28a30eed752e4c0c714101f737a5bff66164344b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Abadesso?= Date: Thu, 3 Oct 2024 10:17:21 -0300 Subject: [PATCH 1/4] fix: handling utxos with empty scripts being spent --- .../__tests__/integration/balances.test.ts | 107 ++++++++++++++++++ .../daemon/__tests__/integration/config.ts | 15 ++- .../custom_script.balances.ts | 16 +++ .../scenario_configs/empty_script.balances.ts | 16 +++ .../integration/scripts/docker-compose.yml | 21 ++++ packages/daemon/src/utils/wallet.ts | 8 +- 6 files changed, 179 insertions(+), 4 deletions(-) create mode 100644 packages/daemon/__tests__/integration/scenario_configs/custom_script.balances.ts create mode 100644 packages/daemon/__tests__/integration/scenario_configs/empty_script.balances.ts diff --git a/packages/daemon/__tests__/integration/balances.test.ts b/packages/daemon/__tests__/integration/balances.test.ts index 97355e64..f08369dc 100644 --- a/packages/daemon/__tests__/integration/balances.test.ts +++ b/packages/daemon/__tests__/integration/balances.test.ts @@ -14,6 +14,9 @@ import unvoidedScenarioBalances from './scenario_configs/unvoided_transactions.b import reorgScenarioBalances from './scenario_configs/reorg.balances'; import singleChainBlocksAndTransactionsBalances from './scenario_configs/single_chain_blocks_and_transactions.balances'; import invalidMempoolBalances from './scenario_configs/invalid_mempool_transaction.balances'; +import emptyScriptBalances from './scenario_configs/empty_script.balances'; +import customScriptBalances from './scenario_configs/custom_script.balances'; + import { DB_NAME, DB_USER, @@ -28,6 +31,10 @@ import { SINGLE_CHAIN_BLOCKS_AND_TRANSACTIONS_PORT, SINGLE_CHAIN_BLOCKS_AND_TRANSACTIONS_LAST_EVENT, INVALID_MEMPOOL_TRANSACTION_LAST_EVENT, + CUSTOM_SCRIPT_PORT, + CUSTOM_SCRIPT_LAST_EVENT, + EMPTY_SCRIPT_PORT, + EMPTY_SCRIPT_LAST_EVENT, } from './config'; jest.mock('../../src/config', () => { @@ -273,3 +280,103 @@ describe('invalid mempool transactions scenario', () => { }); }); }); + +describe('custom script scenario', () => { + beforeAll(() => { + jest.spyOn(Services, 'fetchMinRewardBlocks').mockImplementation(async () => 300); + }); + + it('should do a full sync and the balances should match', async () => { + // @ts-ignore + getConfig.mockReturnValue({ + NETWORK: 'testnet', + SERVICE_NAME: 'daemon-test', + CONSOLE_LEVEL: 'debug', + TX_CACHE_SIZE: 100, + BLOCK_REWARD_LOCK: 300, + FULLNODE_PEER_ID: 'simulator_peer_id', + STREAM_ID: 'simulator_stream_id', + FULLNODE_NETWORK: 'simulator_network', + FULLNODE_HOST: `127.0.0.1:${CUSTOM_SCRIPT_PORT}`, + USE_SSL: false, + DB_ENDPOINT, + DB_NAME, + DB_USER, + DB_PASS, + DB_PORT, + }); + + const machine = interpret(SyncMachine); + + await new Promise((resolve) => { + machine.onTransition(async (state) => { + const addressBalances = await fetchAddressBalances(mysql); + if (state.matches('CONNECTED.idle')) { + // @ts-ignore + const lastSyncedEvent = await getLastSyncedEvent(mysql); + console.log(lastSyncedEvent); + if (lastSyncedEvent?.last_event_id === CUSTOM_SCRIPT_LAST_EVENT) { + // @ts-ignore + expect(validateBalances(addressBalances, customScriptBalances)); + + machine.stop(); + + resolve(); + } + } + }); + + machine.start(); + }); + }); +}); + +describe('empty script scenario', () => { + beforeAll(() => { + jest.spyOn(Services, 'fetchMinRewardBlocks').mockImplementation(async () => 300); + }); + + it('should do a full sync and the balances should match', async () => { + // @ts-ignore + getConfig.mockReturnValue({ + NETWORK: 'testnet', + SERVICE_NAME: 'daemon-test', + CONSOLE_LEVEL: 'debug', + TX_CACHE_SIZE: 100, + BLOCK_REWARD_LOCK: 300, + FULLNODE_PEER_ID: 'simulator_peer_id', + STREAM_ID: 'simulator_stream_id', + FULLNODE_NETWORK: 'simulator_network', + FULLNODE_HOST: `127.0.0.1:${EMPTY_SCRIPT_PORT}`, + USE_SSL: false, + DB_ENDPOINT, + DB_NAME, + DB_USER, + DB_PASS, + DB_PORT, + }); + + const machine = interpret(SyncMachine); + + await new Promise((resolve) => { + machine.onTransition(async (state) => { + const addressBalances = await fetchAddressBalances(mysql); + if (state.matches('CONNECTED.idle')) { + // @ts-ignore + const lastSyncedEvent = await getLastSyncedEvent(mysql); + if (lastSyncedEvent?.last_event_id === EMPTY_SCRIPT_LAST_EVENT) { + console.log('EMPTY SCRIPT SCNEARIO'); + // @ts-ignore + expect(validateBalances(addressBalances, emptyScriptBalances)); + + machine.stop(); + + resolve(); + } + } + }); + + machine.start(); + }); + }); +}); diff --git a/packages/daemon/__tests__/integration/config.ts b/packages/daemon/__tests__/integration/config.ts index 53619d93..2ffa6812 100644 --- a/packages/daemon/__tests__/integration/config.ts +++ b/packages/daemon/__tests__/integration/config.ts @@ -28,4 +28,17 @@ export const SINGLE_CHAIN_BLOCKS_AND_TRANSACTIONS_LAST_EVENT = 37; export const INVALID_MEMPOOL_TRANSACTION_PORT = 8085; export const INVALID_MEMPOOL_TRANSACTION_LAST_EVENT = 40; -export const SCENARIOS = ['UNVOIDED_SCENARIO', 'REORG_SCENARIO', 'SINGLE_CHAIN_BLOCKS_AND_TRANSACTIONS', 'INVALID_MEMPOOL_TRANSACTION']; +export const CUSTOM_SCRIPT_PORT = 8086; +export const CUSTOM_SCRIPT_LAST_EVENT = 37; + +export const EMPTY_SCRIPT_PORT = 8087; +export const EMPTY_SCRIPT_LAST_EVENT = 37; + +export const SCENARIOS = [ + 'UNVOIDED_SCENARIO', + 'REORG_SCENARIO', + 'SINGLE_CHAIN_BLOCKS_AND_TRANSACTIONS', + 'INVALID_MEMPOOL_TRANSACTION', + 'EMPTY_SCRIPT', + 'CUSTOM_SCRIPT', +]; diff --git a/packages/daemon/__tests__/integration/scenario_configs/custom_script.balances.ts b/packages/daemon/__tests__/integration/scenario_configs/custom_script.balances.ts new file mode 100644 index 00000000..804cfdcf --- /dev/null +++ b/packages/daemon/__tests__/integration/scenario_configs/custom_script.balances.ts @@ -0,0 +1,16 @@ +export default { + "HVayMofEDh4XGsaQJeRJKhutYxYodYNop6": 100000000000, + "HJQbEERnD5Ak3f2dsi8zAmsZrCWTT8FZns": 6400, + "HRSYchTEsFFpZAkgSTMsohNGQ6eLPyhXvJ": 6400, + "HQfVqxyxQV4BHwnsMnRXpZGmwPYiNSVmMu": 6400, + "HPnkpR2vnBuCoZCEnRZNHMBtf8ygeSidbW": 6400, + "HPNvtPZaDF44i6CL91u4BvZPu6z2xPNt26": 6400, + "HQijr325t63VJFdc4vYkaTyd87oeBLpSed": 6400, + "H8fCNrYGkj4B6VzKgtRiHBgoSxM31d65JR": 6400, + "HAqrADnn7GyyT68fSX8zmtsRNFyabPzoRQ": 6400, + "HRTH6uGo7zn3LWrosBYn7eXkwAeHAHTRh8": 6400, + "HJPSMHCFv2dRb78wZPMsAzwLQHSkBpfuLn": 6400, + "HFtz2f59Lms4p3Jfgtsr73s97MbJHsRENh": 1000, + "H9hHteu9QdAS5p6X743Mpfue6G19rV9GeY": 5400, + "HSd6PqXesUmHHv6MoN24aUiMuw7Pdcxrwk": 6400 +}; diff --git a/packages/daemon/__tests__/integration/scenario_configs/empty_script.balances.ts b/packages/daemon/__tests__/integration/scenario_configs/empty_script.balances.ts new file mode 100644 index 00000000..e901327a --- /dev/null +++ b/packages/daemon/__tests__/integration/scenario_configs/empty_script.balances.ts @@ -0,0 +1,16 @@ +export default { + "HVayMofEDh4XGsaQJeRJKhutYxYodYNop6": 100000000000, + "HFtz2f59Lms4p3Jfgtsr73s97MbJHsRENh": 6400, + "HJQbEERnD5Ak3f2dsi8zAmsZrCWTT8FZns": 6400, + "HRSYchTEsFFpZAkgSTMsohNGQ6eLPyhXvJ": 6400, + "HQfVqxyxQV4BHwnsMnRXpZGmwPYiNSVmMu": 6400, + "HPnkpR2vnBuCoZCEnRZNHMBtf8ygeSidbW": 6400, + "HPNvtPZaDF44i6CL91u4BvZPu6z2xPNt26": 6400, + "HQijr325t63VJFdc4vYkaTyd87oeBLpSed": 6400, + "H8fCNrYGkj4B6VzKgtRiHBgoSxM31d65JR": 6400, + "HAqrADnn7GyyT68fSX8zmtsRNFyabPzoRQ": 6400, + "HRTH6uGo7zn3LWrosBYn7eXkwAeHAHTRh8": 6400, + "HRQe4CXj8AZXzSmuNztU8iQR74QTQMbnTs": 1000, + "HNqTfEASfdx7H4vMUGzfD2HyD3GeKuxjTJ": 5400, + "HRH8Wbmr1A3BrLswSBhvVE4hhsv4jUdyVA": 6400 +}; diff --git a/packages/daemon/__tests__/integration/scripts/docker-compose.yml b/packages/daemon/__tests__/integration/scripts/docker-compose.yml index 8296ba8a..4d55e618 100644 --- a/packages/daemon/__tests__/integration/scripts/docker-compose.yml +++ b/packages/daemon/__tests__/integration/scripts/docker-compose.yml @@ -9,6 +9,7 @@ services: MYSQL_ROOT_PASSWORD: hathor ports: - "3380:3306" + unvoided_transaction: image: hathornetwork/hathor-core:stable command: [ @@ -46,5 +47,25 @@ services: ports: - "8085:8080" + custom_scripts: + image: hathor/hathor-core + command: [ + "events_simulator", + "--scenario", "CUSTOM_SCRIPT", + "--seed", "1" + ] + ports: + - "8086:8080" + + empty_script: + image: hathor/hathor-core + command: [ + "events_simulator", + "--scenario", "EMPTY_SCRIPT", + "--seed", "1" + ] + ports: + - "8087:8080" + networks: database: diff --git a/packages/daemon/src/utils/wallet.ts b/packages/daemon/src/utils/wallet.ts index d9b6a34c..4e0ac6cb 100644 --- a/packages/daemon/src/utils/wallet.ts +++ b/packages/daemon/src/utils/wallet.ts @@ -125,7 +125,9 @@ export const getAddressBalanceMap = ( for (const input of inputs) { if (!input.decoded) { - throw new Error('Input has no decoded script'); + // If we're unable to decode the script, we will also be unable to + // calculate the balance, so just skip this input. + continue; } const address = input.decoded?.address; @@ -294,11 +296,11 @@ export const prepareInputs = (inputs: EventTxInput[], tokens: string[]): TxInput // @ts-ignore script: utxo.script, token, - decoded: { + decoded: output.decoded ? { type: output.decoded.type, address: output.decoded.address, timelock: output.decoded.timelock, - }, + } : null, }; return [...newInputs, input]; From c27067c95fb0448a20d70580b99a696b312bd2b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Abadesso?= Date: Thu, 3 Oct 2024 11:05:19 -0300 Subject: [PATCH 2/4] refactor: helper to roll until an event --- .../__tests__/integration/balances.test.ts | 152 ++++-------------- .../__tests__/integration/utils/index.ts | 22 ++- 2 files changed, 54 insertions(+), 120 deletions(-) diff --git a/packages/daemon/__tests__/integration/balances.test.ts b/packages/daemon/__tests__/integration/balances.test.ts index f08369dc..7319c7b1 100644 --- a/packages/daemon/__tests__/integration/balances.test.ts +++ b/packages/daemon/__tests__/integration/balances.test.ts @@ -4,12 +4,13 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ + import * as Services from '../../src/services'; import { SyncMachine } from '../../src/machines'; import { interpret } from 'xstate'; -import { getLastSyncedEvent, getDbConnection } from '../../src/db'; +import { getDbConnection } from '../../src/db'; import { Connection } from 'mysql2/promise'; -import { cleanDatabase, fetchAddressBalances, validateBalances } from './utils'; +import { cleanDatabase, fetchAddressBalances, transitionUntilEvent, validateBalances } from './utils'; import unvoidedScenarioBalances from './scenario_configs/unvoided_transactions.balances'; import reorgScenarioBalances from './scenario_configs/reorg.balances'; import singleChainBlocksAndTransactionsBalances from './scenario_configs/single_chain_blocks_and_transactions.balances'; @@ -111,25 +112,11 @@ describe('unvoided transaction scenario', () => { const machine = interpret(SyncMachine); - await new Promise((resolve) => { - machine.onTransition(async (state) => { - if (state.matches('CONNECTED.idle')) { - // @ts-ignore - const lastSyncedEvent = await getLastSyncedEvent(mysql); - if (lastSyncedEvent?.last_event_id === UNVOIDED_SCENARIO_LAST_EVENT) { - const addressBalances = await fetchAddressBalances(mysql); - // @ts-ignore - expect(validateBalances(addressBalances, unvoidedScenarioBalances)); - - machine.stop(); - - resolve(); - } - } - }); - - machine.start(); - }); + // @ts-ignore + await transitionUntilEvent(mysql, machine, UNVOIDED_SCENARIO_LAST_EVENT); + const addressBalances = await fetchAddressBalances(mysql); + // @ts-ignore + expect(validateBalances(addressBalances, unvoidedScenarioBalances)); }); }); @@ -160,25 +147,11 @@ describe('reorg scenario', () => { const machine = interpret(SyncMachine); - await new Promise((resolve) => { - machine.onTransition(async (state) => { - if (state.matches('CONNECTED.idle')) { - // @ts-ignore - const lastSyncedEvent = await getLastSyncedEvent(mysql); - if (lastSyncedEvent?.last_event_id === REORG_SCENARIO_LAST_EVENT) { - const addressBalances = await fetchAddressBalances(mysql); - // @ts-ignore - expect(validateBalances(addressBalances, reorgScenarioBalances)); - - machine.stop(); - - resolve(); - } - } - }); - - machine.start(); - }); + // @ts-ignore + await transitionUntilEvent(mysql, machine, REORG_SCENARIO_LAST_EVENT); + const addressBalances = await fetchAddressBalances(mysql); + // @ts-ignore + expect(validateBalances(addressBalances, reorgScenarioBalances)); }); }); @@ -209,25 +182,11 @@ describe('single chain blocks and transactions scenario', () => { const machine = interpret(SyncMachine); - await new Promise((resolve) => { - machine.onTransition(async (state) => { - if (state.matches('CONNECTED.idle')) { - // @ts-ignore - const lastSyncedEvent = await getLastSyncedEvent(mysql); - if (lastSyncedEvent?.last_event_id === SINGLE_CHAIN_BLOCKS_AND_TRANSACTIONS_LAST_EVENT) { - const addressBalances = await fetchAddressBalances(mysql); - // @ts-ignore - expect(validateBalances(addressBalances, singleChainBlocksAndTransactionsBalances)); - - machine.stop(); - - resolve(); - } - } - }); - - machine.start(); - }); + // @ts-ignore + await transitionUntilEvent(mysql, machine, SINGLE_CHAIN_BLOCKS_AND_TRANSACTIONS_LAST_EVENT); + const addressBalances = await fetchAddressBalances(mysql); + // @ts-ignore + expect(validateBalances(addressBalances, singleChainBlocksAndTransactionsBalances)); }); }); @@ -258,26 +217,11 @@ describe('invalid mempool transactions scenario', () => { const machine = interpret(SyncMachine); - await new Promise((resolve) => { - machine.onTransition(async (state) => { - const addressBalances = await fetchAddressBalances(mysql); - if (state.matches('CONNECTED.idle')) { - // @ts-ignore - const lastSyncedEvent = await getLastSyncedEvent(mysql); - console.log(lastSyncedEvent); - if (lastSyncedEvent?.last_event_id === INVALID_MEMPOOL_TRANSACTION_LAST_EVENT) { - // @ts-ignore - expect(validateBalances(addressBalances, invalidMempoolBalances)); - - machine.stop(); - - resolve(); - } - } - }); - - machine.start(); - }); + // @ts-ignore + await transitionUntilEvent(mysql, machine, INVALID_MEMPOOL_TRANSACTION_LAST_EVENT); + const addressBalances = await fetchAddressBalances(mysql); + // @ts-ignore + expect(validateBalances(addressBalances, invalidMempoolBalances)); }); }); @@ -308,26 +252,11 @@ describe('custom script scenario', () => { const machine = interpret(SyncMachine); - await new Promise((resolve) => { - machine.onTransition(async (state) => { - const addressBalances = await fetchAddressBalances(mysql); - if (state.matches('CONNECTED.idle')) { - // @ts-ignore - const lastSyncedEvent = await getLastSyncedEvent(mysql); - console.log(lastSyncedEvent); - if (lastSyncedEvent?.last_event_id === CUSTOM_SCRIPT_LAST_EVENT) { - // @ts-ignore - expect(validateBalances(addressBalances, customScriptBalances)); - - machine.stop(); - - resolve(); - } - } - }); - - machine.start(); - }); + // @ts-ignore + await transitionUntilEvent(mysql, machine, CUSTOM_SCRIPT_LAST_EVENT); + const addressBalances = await fetchAddressBalances(mysql); + // @ts-ignore + expect(validateBalances(addressBalances, customScriptBalances)); }); }); @@ -358,25 +287,10 @@ describe('empty script scenario', () => { const machine = interpret(SyncMachine); - await new Promise((resolve) => { - machine.onTransition(async (state) => { - const addressBalances = await fetchAddressBalances(mysql); - if (state.matches('CONNECTED.idle')) { - // @ts-ignore - const lastSyncedEvent = await getLastSyncedEvent(mysql); - if (lastSyncedEvent?.last_event_id === EMPTY_SCRIPT_LAST_EVENT) { - console.log('EMPTY SCRIPT SCNEARIO'); - // @ts-ignore - expect(validateBalances(addressBalances, emptyScriptBalances)); - - machine.stop(); - - resolve(); - } - } - }); - - machine.start(); - }); + // @ts-ignore + await transitionUntilEvent(mysql, machine, EMPTY_SCRIPT_LAST_EVENT); + const addressBalances = await fetchAddressBalances(mysql); + // @ts-ignore + expect(validateBalances(addressBalances, emptyScriptBalances)); }); }); diff --git a/packages/daemon/__tests__/integration/utils/index.ts b/packages/daemon/__tests__/integration/utils/index.ts index 238e0eb4..b8bc0cf7 100644 --- a/packages/daemon/__tests__/integration/utils/index.ts +++ b/packages/daemon/__tests__/integration/utils/index.ts @@ -5,7 +5,9 @@ * LICENSE file in the root directory of this source tree. */ import { Connection } from 'mysql2/promise'; -import { AddressBalance, AddressBalanceRow } from '../../../src/types'; +import { Interpreter } from 'xstate'; +import { getLastSyncedEvent } from '../../../src/db'; +import { AddressBalance, AddressBalanceRow, Context, Event } from '../../../src/types'; export const cleanDatabase = async (mysql: Connection): Promise => { const TABLES = [ @@ -74,3 +76,21 @@ export const validateBalances = async ( } } }; + +export async function transitionUntilEvent(mysql: Connection, machine: Interpreter, eventId: number) { + return await new Promise((resolve) => { + machine.onTransition(async (state) => { + if (state.matches('CONNECTED.idle')) { + // @ts-ignore + const lastSyncedEvent = await getLastSyncedEvent(mysql); + if (lastSyncedEvent?.last_event_id === eventId) { + machine.stop(); + + resolve(); + } + } + }); + + machine.start(); + }); +} From 402c55e16e72c32741043b59f9ea6678cd3c10b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Abadesso?= Date: Fri, 4 Oct 2024 13:14:52 -0300 Subject: [PATCH 3/4] chore: using latest :stable on custom and empty script docker compose --- .../daemon/__tests__/integration/scripts/docker-compose.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/daemon/__tests__/integration/scripts/docker-compose.yml b/packages/daemon/__tests__/integration/scripts/docker-compose.yml index 4d55e618..afa5b5d4 100644 --- a/packages/daemon/__tests__/integration/scripts/docker-compose.yml +++ b/packages/daemon/__tests__/integration/scripts/docker-compose.yml @@ -1,5 +1,3 @@ -version: "3.9" - services: mysql: image: mysql @@ -48,7 +46,7 @@ services: - "8085:8080" custom_scripts: - image: hathor/hathor-core + image: hathornetwork/hathor-core:stable command: [ "events_simulator", "--scenario", "CUSTOM_SCRIPT", @@ -58,7 +56,7 @@ services: - "8086:8080" empty_script: - image: hathor/hathor-core + image: hathornetwork/hathor-core:stable command: [ "events_simulator", "--scenario", "EMPTY_SCRIPT", From 4c9b1813027efc14aad37fb294424af60957b210 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Abadesso?= Date: Fri, 4 Oct 2024 13:18:46 -0300 Subject: [PATCH 4/4] refactor: removed unused logs --- packages/daemon/__tests__/integration/utils/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/daemon/__tests__/integration/utils/index.ts b/packages/daemon/__tests__/integration/utils/index.ts index b8bc0cf7..fd38b055 100644 --- a/packages/daemon/__tests__/integration/utils/index.ts +++ b/packages/daemon/__tests__/integration/utils/index.ts @@ -70,8 +70,6 @@ export const validateBalances = async ( const totalBalanceA = balanceA.lockedBalance + balanceA.unlockedBalance; if (totalBalanceA !== balanceB) { - console.log(totalBalanceA); - console.log(balanceB); throw new Error(`Balances are not equal for address: ${address}, expected: ${balanceB}, received: ${totalBalanceA}`); } }