From 4da8bfb0e94bfa8f7d6d9127d7ffd6c1db91bcf2 Mon Sep 17 00:00:00 2001 From: Igor Konnov Date: Tue, 17 Dec 2024 21:12:36 +0100 Subject: [PATCH 1/6] add basic code for full state aggregation and state load/store --- solarkraft/src/fetcher/aggregator.ts | 95 +++++++++++++ solarkraft/src/fetcher/storage.ts | 119 +++++++++++++++- solarkraft/test/unit/aggregator.test.ts | 178 ++++++++++++++++++++++++ solarkraft/test/unit/storage.test.ts | 77 +++++++++- 4 files changed, 465 insertions(+), 4 deletions(-) create mode 100644 solarkraft/src/fetcher/aggregator.ts create mode 100644 solarkraft/test/unit/aggregator.test.ts diff --git a/solarkraft/src/fetcher/aggregator.ts b/solarkraft/src/fetcher/aggregator.ts new file mode 100644 index 0000000..1eaf05e --- /dev/null +++ b/solarkraft/src/fetcher/aggregator.ts @@ -0,0 +1,95 @@ +/** + * @license + * [Apache-2.0](https://github.com/freespek/solarkraft/blob/main/LICENSE) + */ +// State aggregator. We do not expect this aggregator to be efficient. +// It is a proof of concept that is needed to implement input generation. +// +// See: https://github.com/freespek/solarkraft/issues/153 +// +// Igor Konnov, 2024 + +import { + ContractCallEntry, + emptyContractStorage, + FieldsMap, + FullState, + MultiContractStorage, +} from './storage.js' + +/** + * Apply the updates from a contract call to the state. + * @param state the state to update + * @param callEntry the call entry to apply + * @returns the updated state + */ +export function aggregate( + state: FullState, + callEntry: ContractCallEntry +): FullState { + if (!callEntry.txSuccess) { + return state + } + + return { + contractId: callEntry.contractId, + timestamp: callEntry.timestamp, + height: callEntry.height, + latestTxHash: callEntry.txHash, + storage: updateContractStorage( + state.storage, + callEntry.oldStorage, + callEntry.storage + ), + } +} + +function updateContractStorage( + fullStorage: MultiContractStorage, + oldStorage: MultiContractStorage, + storage: MultiContractStorage +): MultiContractStorage { + let updatedStorage = fullStorage + for (const [contractId, contractStorage] of storage) { + const contractFullStorage = + fullStorage.get(contractId) ?? emptyContractStorage() + const contractOldStorage = + oldStorage.get(contractId) ?? emptyContractStorage() + updatedStorage = updatedStorage.set(contractId, { + instance: updateFieldsMap( + contractFullStorage.instance, + contractOldStorage.instance, + contractStorage.instance + ), + persistent: updateFieldsMap( + contractFullStorage.persistent, + contractOldStorage.persistent, + contractStorage.persistent + ), + temporary: updateFieldsMap( + contractFullStorage.temporary, + contractOldStorage.temporary, + contractStorage.temporary + ), + }) + } + return updatedStorage +} + +function updateFieldsMap( + fullStorageFields: FieldsMap, + oldFieldsInCall: FieldsMap, + fieldsInCall: FieldsMap +): FieldsMap { + let updatedFields = fullStorageFields + // note that storage entries in ContractCallEntry are small subsets of the full storage + for (const key of oldFieldsInCall.keys()) { + if (!fieldsInCall.has(key)) { + updatedFields = updatedFields.delete(key) + } + } + for (const [key, value] of fieldsInCall) { + updatedFields = updatedFields.set(key, value) + } + return updatedFields +} diff --git a/solarkraft/src/fetcher/storage.ts b/solarkraft/src/fetcher/storage.ts index 943b2f4..fbf09ef 100644 --- a/solarkraft/src/fetcher/storage.ts +++ b/solarkraft/src/fetcher/storage.ts @@ -178,6 +178,56 @@ export interface FetcherState { heights: OrderedMap } +/** + * This entry collects the full contract state. + * This is obviously expensive to store, so it should be improved, + * if the MVP proves successful. + */ +export interface FullState { + /** + * The number of seconds elapsed since unix epoch of when the latest contract ledger was closed. + */ + timestamp: number + /** + * The ledger number that this state corresponds to. + */ + height: number + /** + * The hash of the latest transaction that led to this state. + */ + latestTxHash: string + /** + * The address of the contract being called. + */ + contractId: string + /** + * Ordered mapping from contract address to instance/persistent/temporary storage + * of the respective contract. The contract storage for a given durability is + * an ordered mapping from field names to their native values (JS). + * + * This mapping contains values only for the fields that have been created + * or updated by a transaction in the past. It may happen that + * `storage` contains fewer fields than `oldStorage`, when the contract + * deletes some fields from the storage. Also, fields may be cleared from `storage` + * when the storage goes over TTL. + */ + storage: MultiContractStorage +} + +/** + * The empty full state that is to be initialized. + * @returns an empty full state + */ +export function emptyFullState(): FullState { + return { + timestamp: 0, + height: 0, + latestTxHash: '', + contractId: '', + storage: emptyMultiContractStorage(), + } +} + /** * Given the solarkraft home, construct the path to store the transactions. * @param solarkraftHome path to solarkraft home (or project directory) @@ -191,6 +241,7 @@ export function storagePath(solarkraftHome: string): string { * Store a contract call entry in the file storage. * @param home the storage root directory * @param entry a call entry + * @returns the filename, where the entry was stored */ export function saveContractCallEntry(home: string, entry: ContractCallEntry) { const filename = getEntryFilename(storagePath(home), entry) @@ -261,6 +312,41 @@ export function loadContractCallEntry( }) } +/** + * Load full state of a contract from the storage. + * @param solarkraftHome the .solarkraft directory + * @param contractId the contract address + * @returns the loaded full state + */ +export function loadContractFullState( + solarkraftHome: string, + contractId: string +): Result { + const filename = getFullStateFilename( + storagePath(solarkraftHome), + contractId + ) + const contents = readFileSync(filename) + const loaded = JSONbig.parse(contents) + return right({ + ...loaded, + storage: storageFromJS(loaded.storage), + }) +} + +/** + * Store contract full state in the file storage. + * @param home the storage root directory + * @param state a state of the contract + * @returns the filename, where the state was stored + */ +export function saveContractFullState(home: string, state: FullState): string { + const filename = getFullStateFilename(storagePath(home), state.contractId) + const contents = JSONbig.stringify(state) + writeFileSync(filename, contents) + return filename +} + /** * Generate storage entries for a given contract id in a path. * @param contractId contract identifier (address) @@ -351,10 +437,22 @@ export function saveFetcherState(home: string, state: FetcherState): string { * @returns the filename */ function getEntryFilename(root: string, entry: ContractCallEntry) { - const dir = getOrCreateDirectory(root, entry) + const dir = getOrCreateEntryParentDir(root, entry) return join(dir, `entry-${entry.txHash}.json`) } +/** + * Get the directory name to store contract state. + * + * @param root storage root + * @param contractId the contract id to retrieve the state for + * @returns the directory name + */ +function getFullStateFilename(root: string, contractId: string) { + const dir = getOrCreateContractDir(root, contractId) + return join(dir, 'state.json') +} + /** * Get the filename for the fetcher state. * @@ -374,8 +472,23 @@ function getFetcherStateFilename(root: string) { * @param entry call entry * @returns the directory name */ -function getOrCreateDirectory(root: string, entry: ContractCallEntry) { - const directory = join(root, entry.contractId, entry.height.toString()) +function getOrCreateEntryParentDir(root: string, entry: ContractCallEntry) { + const contractDir = getOrCreateContractDir(root, entry.contractId) + const directory = join(contractDir, entry.height.toString()) + mkdirSync(directory, { recursive: true }) + return directory +} + +/** + * Return the contract directory. + * If this directory does not exist, create it recursively. + * + * @param root storage root + * @param contractId contract address + * @returns the directory name + */ +function getOrCreateContractDir(root: string, contractId: string) { + const directory = join(root, contractId) mkdirSync(directory, { recursive: true }) return directory } diff --git a/solarkraft/test/unit/aggregator.test.ts b/solarkraft/test/unit/aggregator.test.ts new file mode 100644 index 0000000..4840b96 --- /dev/null +++ b/solarkraft/test/unit/aggregator.test.ts @@ -0,0 +1,178 @@ +/** + * State aggregation tests. + * + * Igor Konnov, 2024 + */ + +import { describe, it } from 'mocha' +import { expect } from 'chai' +import { OrderedMap } from 'immutable' +import { + ContractCallEntry, + ContractStorage, + emptyFieldsMap, + emptyFullState, + emptyMultiContractStorage, + FullState, +} from '../../src/fetcher/storage.js' +import { aggregate } from '../../src/fetcher/aggregator.js' + +// these are unit tests, as we are trying to minimize the number of e2e tests +describe('State aggregation', () => { + it('updates empty state on successful transaction', () => { + const empty = emptyFullState() + const contractId = + 'CCQURSVQRCMNZPLNYA4AMP2JUODZ5QOLG5XCLQWEJAEE3NBLR5ZWZ5KX' + const callEntry: ContractCallEntry = { + timestamp: 123, + height: 1234, + txHash: '9fb1', + txSuccess: true, + contractId: contractId, + method: 'set_i32', + methodArgs: [42], + returnValue: 0, + // the fields are irrelevant, only the storage + fields: emptyFieldsMap(), + oldFields: emptyFieldsMap(), + // oldStorage is irrelevant, only the storage + oldStorage: emptyMultiContractStorage(), + // the storage is very much relevant + storage: OrderedMap([ + [ + contractId, + { + // we set i32 to different values in different storages, because we can + instance: OrderedMap([['i32', 42]]), + persistent: OrderedMap([['i32', 142]]), + temporary: OrderedMap([['i32', 242]]), + }, + ], + ]), + } + + const nextState = aggregate(empty, callEntry) + expect(nextState.contractId).to.equal(contractId) + expect(nextState.timestamp).to.equal(callEntry.timestamp) + expect(nextState.height).to.equal(callEntry.height) + expect(nextState.latestTxHash).to.equal(callEntry.txHash) + expect(nextState.storage.get(contractId).instance.get('i32')).to.equal( + 42 + ) + expect( + nextState.storage.get(contractId).persistent.get('i32') + ).to.equal(142) + expect(nextState.storage.get(contractId).temporary.get('i32')).to.equal( + 242 + ) + }) + + /* eslint-disable @typescript-eslint/no-unused-expressions */ + it('updates previous state on successful transaction', () => { + const contractId = + 'CCQURSVQRCMNZPLNYA4AMP2JUODZ5QOLG5XCLQWEJAEE3NBLR5ZWZ5KX' + const fullStorage = OrderedMap([ + [ + contractId, + { + instance: OrderedMap([ + ['i32', 42], + ['i64', 123], + ['i128', 1001], + ]), + persistent: OrderedMap([ + ['i32', 142], + ['i64', 124], + ['i128', 1002], + ]), + temporary: OrderedMap([ + ['i32', 242], + ['i64', 125], + ['i128', 1003], + ]), + }, + ], + ]) + const prevState: FullState = { + timestamp: 1, + height: 2, + latestTxHash: '9fb0', + contractId: contractId, + storage: fullStorage, + } + const callEntry: ContractCallEntry = { + timestamp: 123, + height: 1234, + txHash: '9fb1', + txSuccess: true, + contractId: contractId, + method: 'update', + methodArgs: [42], + returnValue: 0, + // the fields are irrelevant, only the storage + fields: emptyFieldsMap(), + oldFields: emptyFieldsMap(), + // oldStorage is relevant for finding removed keys + oldStorage: OrderedMap([ + [ + contractId, + { + instance: OrderedMap([ + ['i32', 42], + ['i64', 123], + ]), + persistent: OrderedMap([ + ['i32', 142], + ['i64', 124], + ]), + temporary: OrderedMap([ + ['i32', 242], + ['i64', 125], + ]), + }, + ], + ]), + // the storage is very much relevant + storage: OrderedMap([ + [ + contractId, + { + instance: OrderedMap([['i32', 101]]), + persistent: OrderedMap([['i32', 102]]), + temporary: OrderedMap([['i32', 103]]), + }, + ], + ]), + } + + const nextState = aggregate(prevState, callEntry) + expect(nextState.contractId).to.equal(contractId) + expect(nextState.timestamp).to.equal(callEntry.timestamp) + expect(nextState.height).to.equal(callEntry.height) + expect(nextState.latestTxHash).to.equal(callEntry.txHash) + expect(nextState.storage.get(contractId).instance.get('i32')).to.equal( + 101 + ) + expect( + nextState.storage.get(contractId).persistent.get('i32') + ).to.equal(102) + expect(nextState.storage.get(contractId).temporary.get('i32')).to.equal( + 103 + ) + expect(nextState.storage.get(contractId).instance.get('i64')).to.be + .undefined + expect(nextState.storage.get(contractId).persistent.get('i64')).to.be + .undefined + expect(nextState.storage.get(contractId).temporary.get('i64')).to.be + .undefined + expect(nextState.storage.get(contractId).instance.get('i128')).to.equal( + 1001 + ) + expect( + nextState.storage.get(contractId).persistent.get('i128') + ).to.equal(1002) + expect( + nextState.storage.get(contractId).temporary.get('i128') + ).to.equal(1003) + }) +}) diff --git a/solarkraft/test/unit/storage.test.ts b/solarkraft/test/unit/storage.test.ts index da79047..cfe30cf 100644 --- a/solarkraft/test/unit/storage.test.ts +++ b/solarkraft/test/unit/storage.test.ts @@ -4,7 +4,7 @@ * Igor Konnov, 2024 */ -import { assert } from 'chai' +import { assert, expect } from 'chai' import { mkdtempSync } from 'fs' import { describe, it } from 'mocha' import { tmpdir } from 'node:os' @@ -15,6 +15,9 @@ import { loadContractCallEntry, saveContractCallEntry, storagePath, + FullState, + saveContractFullState, + loadContractFullState, } from '../../src/fetcher/storage.js' import { OrderedMap } from 'immutable' @@ -120,4 +123,76 @@ describe('Solarkraft storage', () => { }, }) }) + + function storeFullStateFixture() { + const root = mkdtempSync(join(tmpdir(), 'solarkraft-storage-')) + const contractId = + 'CCQURSVQRCMNZPLNYA4AMP2JUODZ5QOLG5XCLQWEJAEE3NBLR5ZWZ5KX' + const fullStorage = OrderedMap([ + [ + contractId, + { + instance: OrderedMap([ + ['i32', 42], + ['i64', 123], + ['i128', 1001], + ]), + persistent: OrderedMap([ + ['i32', 142], + ['i64', 124], + ['i128', 1002], + ]), + temporary: OrderedMap([ + ['i32', 242], + ['i64', 125], + ['i128', 1003], + ]), + }, + ], + ]) + const state: FullState = { + timestamp: 1, + height: 2, + latestTxHash: '9fb0', + contractId: contractId, + storage: fullStorage, + } + const filename = saveContractFullState(root, state) + return [root, filename] + } + + it('store full state', () => { + const [root, filename] = storeFullStateFixture() + assert(root !== undefined, 'expected root') + assert(filename !== undefined, 'expected filename') + }) + + it('load full state', () => { + const [root, filename] = storeFullStateFixture() + assert(filename !== undefined, 'expected filename') + const state = loadContractFullState(root, CONTRACT_ID).unwrap() + expect(state.contractId).to.equal(CONTRACT_ID) + expect(state.timestamp).to.equal(1n) + expect(state.height).to.equal(2n) + expect(state.latestTxHash).to.equal('9fb0') + expect(state.storage.toJS()).to.deep.equal({ + [CONTRACT_ID]: { + instance: { + i32: 42n, + i64: 123n, + i128: 1001n, + }, + persistent: { + i32: 142n, + i64: 124n, + i128: 1002n, + }, + temporary: { + i32: 242n, + i64: 125n, + i128: 1003n, + }, + }, + }) + }) }) From 206d493709fefbaa07d8b24e39a23c06d1a129be Mon Sep 17 00:00:00 2001 From: Igor Konnov Date: Wed, 18 Dec 2024 18:43:48 +0100 Subject: [PATCH 2/6] implement state aggregation --- solarkraft/src/aggregate.ts | 92 +++++++++++++++++++ .../src/{fetcher => aggregator}/aggregator.ts | 9 +- solarkraft/src/fetcher/storage.ts | 11 ++- solarkraft/src/main.ts | 34 +++++++ solarkraft/test/e2e/verify.test.ts | 25 ++++- solarkraft/test/unit/aggregator.test.ts | 6 +- 6 files changed, 163 insertions(+), 14 deletions(-) create mode 100644 solarkraft/src/aggregate.ts rename solarkraft/src/{fetcher => aggregator}/aggregator.ts (92%) diff --git a/solarkraft/src/aggregate.ts b/solarkraft/src/aggregate.ts new file mode 100644 index 0000000..960befc --- /dev/null +++ b/solarkraft/src/aggregate.ts @@ -0,0 +1,92 @@ +/** + * @license + * [Apache-2.0](https://github.com/freespek/solarkraft/blob/main/LICENSE) + */ +/** + * A command to aggregate the full contract state from the collected transactions. + * This command is potentially expensive. In the long run, it makes sense to + * collect the states from the archival nodes. For the time being, we aggregate + * the states from the transactions directly, in order to evaluate the approach. + * + * We need this feature primarily for input generation. This is an experimental + * feature in Phase 1. We always aggregate the state, starting with the minimal + * available height. Obviously, there is a lot of room for improvement here. + * + * Although it is tempting to aggregate transactions directly in fetch.ts, + * this is the wrong approach. Horizon may give us transactions out of order, + * so we need to sort them by height before state aggregation. + * + * Igor Konnov, 2024 + */ + +import { existsSync, writeFileSync } from 'fs' +import { join } from 'path' +import { JSONbig } from './globals.js' +import { + emptyFullState, + loadContractCallEntry, + storagePath, + yieldListEntriesForContract, +} from './fetcher/storage.js' +import { applyCallToState } from './aggregator/aggregator.js' + +/** + * Aggregate the fetched transactions to compute the full contract state. + * @param args the aggregator arguments + */ +export async function aggregate(args: any) { + const storageRoot = storagePath(args.home) + if (!existsSync(storageRoot)) { + console.error(`The storage is empty. Run 'solarkraft fetch'`) + return + } + + const contractId = args.id + + // We have to sort the entries by height. Hence, we read them first and then sort. + let lastEntry = undefined + const entries = [] + for (const e of yieldListEntriesForContract( + contractId, + join(storageRoot, contractId) + )) { + if (e.height <= args.heightTo) { + entries.push(e) + } + if (lastEntry && lastEntry.height === e.height) { + // this should not happen on the testnet, as there is only one transaction per height + console.warn( + `Height ${e.height}: transactions ${e.txHash} and ${lastEntry.txHash} may be out of order` + ) + } + lastEntry = e + } + // sort the entries + entries.sort((a, b) => a.height - b.height) + + // now we can aggregate the state + let nentries = 0 + let state = emptyFullState() + for (const entry of entries) { + nentries++ + const txEntry = loadContractCallEntry(args.home, entry.txHash) + if (txEntry.isRight()) { + if (args.verbose) { + console.log(`Height ${entry.height}: applied ${entry.txHash}`) + } + state = applyCallToState(state, txEntry.value) + } else { + console.error( + `Failed to load the transaction ${entry.txHash}: ${txEntry.value}` + ) + return + } + } + + // save the aggregated state + const contents = JSONbig.stringify(state) + writeFileSync(args.out, contents) + if (args.verbose) { + console.log(`Aggregated ${nentries} transactions into ${args.out}`) + } +} diff --git a/solarkraft/src/fetcher/aggregator.ts b/solarkraft/src/aggregator/aggregator.ts similarity index 92% rename from solarkraft/src/fetcher/aggregator.ts rename to solarkraft/src/aggregator/aggregator.ts index 1eaf05e..f4ef121 100644 --- a/solarkraft/src/fetcher/aggregator.ts +++ b/solarkraft/src/aggregator/aggregator.ts @@ -15,7 +15,7 @@ import { FieldsMap, FullState, MultiContractStorage, -} from './storage.js' +} from '../fetcher/storage.js' /** * Apply the updates from a contract call to the state. @@ -23,11 +23,14 @@ import { * @param callEntry the call entry to apply * @returns the updated state */ -export function aggregate( +export function applyCallToState( state: FullState, callEntry: ContractCallEntry ): FullState { - if (!callEntry.txSuccess) { + if (callEntry.txSuccess !== true) { + console.warn( + `Transaction ${callEntry.txHash} failed (${callEntry.txSuccess})` + ) return state } diff --git a/solarkraft/src/fetcher/storage.ts b/solarkraft/src/fetcher/storage.ts index fbf09ef..adea0a9 100644 --- a/solarkraft/src/fetcher/storage.ts +++ b/solarkraft/src/fetcher/storage.ts @@ -326,6 +326,9 @@ export function loadContractFullState( storagePath(solarkraftHome), contractId ) + if (!existsSync(filename)) { + return left(`No state found for contract ${contractId}`) + } const contents = readFileSync(filename) const loaded = JSONbig.parse(contents) return right({ @@ -361,7 +364,8 @@ export function* yieldListEntriesForContract( if (dirent.isDirectory() && /^[0-9]+$/.exec(dirent.name)) { // This directory may contain several transactions for the same height. const height = Number.parseInt(dirent.name) - for (const ledgerDirent of readdirSync(join(path, dirent.name), { + const dirPath = join(path, dirent.name) + for (const ledgerDirent of readdirSync(dirPath, { withFileTypes: true, })) { // match all storage entries, which may be reported in different cases @@ -370,10 +374,7 @@ export function* yieldListEntriesForContract( ) if (ledgerDirent.isFile() && matcher) { const txHash = matcher[1] - const filename = join( - ledgerDirent.path, - `entry-${txHash}.json` - ) + const filename = join(dirPath, `entry-${txHash}.json`) const contents = JSONbig.parse( readFileSync(filename, 'utf-8') ) diff --git a/solarkraft/src/main.ts b/solarkraft/src/main.ts index fa74d40..c2ffd76 100644 --- a/solarkraft/src/main.ts +++ b/solarkraft/src/main.ts @@ -11,6 +11,7 @@ import { fetch } from './fetch.js' import { verify } from './verify.js' import { list } from './list.js' import { SOLARKRAFT_DEFAULT_HOME } from './globals.js' +import { aggregate } from './aggregate.js' // The default options present in every command const defaultOpts = (yargs: any) => @@ -65,6 +66,38 @@ const fetchCmd = { handler: fetch, } +// aggregate: state aggregator +const aggregateCmd = { + command: ['aggregate'], + desc: 'aggregate the fetched Soroban transactions to compute the full contract state', + builder: (yargs: any) => + defaultOpts(yargs) + .option('id', { + desc: 'Contract id', + type: 'string', + require: true, + }) + .option('out', { + desc: 'The name of the file to output the state', + type: 'string', + require: false, + default: 'state.json', + }) + .option('heightTo', { + desc: 'The maximum height (ledger) to aggregate up to', + type: 'number', + require: false, + default: Infinity, + }) + .option('verbose', { + desc: 'Print verbose output', + type: 'string', + require: false, + default: false, + }), + handler: aggregate, +} + // verify: transaction verifier const verifyCmd = { command: ['verify'], @@ -106,6 +139,7 @@ const listCmd = { function main() { return yargs(process.argv.slice(2)) .command(fetchCmd) + .command(aggregateCmd) .command(verifyCmd) .command(listCmd) .demandCommand(1) diff --git a/solarkraft/test/e2e/verify.test.ts b/solarkraft/test/e2e/verify.test.ts index 8e99075..1161f9b 100644 --- a/solarkraft/test/e2e/verify.test.ts +++ b/solarkraft/test/e2e/verify.test.ts @@ -169,8 +169,7 @@ describe('fetches the setter contract', () => { } // we need this to run the loop below - it(`fetched ${expectedTransactions} transactions`, async function done() { - this.timeout(timeout) + it(`fetched ${expectedTransactions} transactions`, async () => { await waitForEntries(timeout) // count the entries via yieldListEntriesForContract let txCount = 0 @@ -188,7 +187,27 @@ describe('fetches the setter contract', () => { txCount === expectedTransactions, `expected ${expectedTransactions} transactions` ) - done() + }) + + // this test is co-located with the verification test, as it also needs the fetcher storage + it(`aggregates ${expectedTransactions} transactions`, async () => { + await waitForEntries(timeout) + spawn( + 'solarkraft', + [ + 'aggregate', + `--home=${solarkraftHome}`, + `--id=${SETTER_CONTRACT_ADDR}`, + `--verbose=true`, + ], + { verbose: true } + ) + .wait( + `Aggregated ${expectedTransactions} transactions into state.json` + ) + .run((err) => { + assert(!err, `verification error: ${err}`) + }) }) describe('verifies the setter contract', async () => { diff --git a/solarkraft/test/unit/aggregator.test.ts b/solarkraft/test/unit/aggregator.test.ts index 4840b96..d62bc3a 100644 --- a/solarkraft/test/unit/aggregator.test.ts +++ b/solarkraft/test/unit/aggregator.test.ts @@ -15,7 +15,7 @@ import { emptyMultiContractStorage, FullState, } from '../../src/fetcher/storage.js' -import { aggregate } from '../../src/fetcher/aggregator.js' +import { applyCallToState } from '../../src/aggregator/aggregator.js' // these are unit tests, as we are trying to minimize the number of e2e tests describe('State aggregation', () => { @@ -51,7 +51,7 @@ describe('State aggregation', () => { ]), } - const nextState = aggregate(empty, callEntry) + const nextState = applyCallToState(empty, callEntry) expect(nextState.contractId).to.equal(contractId) expect(nextState.timestamp).to.equal(callEntry.timestamp) expect(nextState.height).to.equal(callEntry.height) @@ -145,7 +145,7 @@ describe('State aggregation', () => { ]), } - const nextState = aggregate(prevState, callEntry) + const nextState = applyCallToState(prevState, callEntry) expect(nextState.contractId).to.equal(contractId) expect(nextState.timestamp).to.equal(callEntry.timestamp) expect(nextState.height).to.equal(callEntry.height) From 33756dd31da1e33981bb07c1b936c1d9089986aa Mon Sep 17 00:00:00 2001 From: Igor Konnov Date: Thu, 19 Dec 2024 14:20:46 +0100 Subject: [PATCH 3/6] Apply suggestions from code review Co-authored-by: Thomas Pani --- solarkraft/src/aggregate.ts | 2 +- solarkraft/src/aggregator/aggregator.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/solarkraft/src/aggregate.ts b/solarkraft/src/aggregate.ts index 960befc..6303ece 100644 --- a/solarkraft/src/aggregate.ts +++ b/solarkraft/src/aggregate.ts @@ -5,7 +5,7 @@ /** * A command to aggregate the full contract state from the collected transactions. * This command is potentially expensive. In the long run, it makes sense to - * collect the states from the archival nodes. For the time being, we aggregate + * collect the states from the history archives. For the time being, we aggregate * the states from the transactions directly, in order to evaluate the approach. * * We need this feature primarily for input generation. This is an experimental diff --git a/solarkraft/src/aggregator/aggregator.ts b/solarkraft/src/aggregator/aggregator.ts index f4ef121..110b122 100644 --- a/solarkraft/src/aggregator/aggregator.ts +++ b/solarkraft/src/aggregator/aggregator.ts @@ -29,7 +29,7 @@ export function applyCallToState( ): FullState { if (callEntry.txSuccess !== true) { console.warn( - `Transaction ${callEntry.txHash} failed (${callEntry.txSuccess})` + `Skipping failed transaction ${callEntry.txHash}` ) return state } From 3c43b19a9432c075a7035eda5630383e2a9074a4 Mon Sep 17 00:00:00 2001 From: Igor Konnov Date: Thu, 19 Dec 2024 14:23:58 +0100 Subject: [PATCH 4/6] improve comments --- solarkraft/src/aggregate.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/solarkraft/src/aggregate.ts b/solarkraft/src/aggregate.ts index 6303ece..30b232d 100644 --- a/solarkraft/src/aggregate.ts +++ b/solarkraft/src/aggregate.ts @@ -4,9 +4,12 @@ */ /** * A command to aggregate the full contract state from the collected transactions. - * This command is potentially expensive. In the long run, it makes sense to - * collect the states from the history archives. For the time being, we aggregate - * the states from the transactions directly, in order to evaluate the approach. + * This command is potentially expensive, as the full contract state potentially + * grows much faster than individual transactions (think of multiple pair-to-pair + * token transfers between a large number of accounts). In the long run, it makes + * sense to collect the states from the history archives. For the time being, + * we aggregate the states from the transactions directly, in order to evaluate + * the approach. * * We need this feature primarily for input generation. This is an experimental * feature in Phase 1. We always aggregate the state, starting with the minimal From ad588af692b6e98a9ecde82ee34a48a5d7636c96 Mon Sep 17 00:00:00 2001 From: Igor Konnov Date: Thu, 19 Dec 2024 14:24:12 +0100 Subject: [PATCH 5/6] fix formatting --- solarkraft/src/aggregator/aggregator.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/solarkraft/src/aggregator/aggregator.ts b/solarkraft/src/aggregator/aggregator.ts index 110b122..d6efcf9 100644 --- a/solarkraft/src/aggregator/aggregator.ts +++ b/solarkraft/src/aggregator/aggregator.ts @@ -28,9 +28,7 @@ export function applyCallToState( callEntry: ContractCallEntry ): FullState { if (callEntry.txSuccess !== true) { - console.warn( - `Skipping failed transaction ${callEntry.txHash}` - ) + console.warn(`Skipping failed transaction ${callEntry.txHash}`) return state } From a1d06847aaf769c6959b757ba691586fe10b3552 Mon Sep 17 00:00:00 2001 From: Igor Konnov Date: Thu, 19 Dec 2024 14:42:19 +0100 Subject: [PATCH 6/6] Apply suggestions from code review Co-authored-by: Thomas Pani --- solarkraft/src/fetcher/storage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solarkraft/src/fetcher/storage.ts b/solarkraft/src/fetcher/storage.ts index adea0a9..4c04e35 100644 --- a/solarkraft/src/fetcher/storage.ts +++ b/solarkraft/src/fetcher/storage.ts @@ -243,7 +243,7 @@ export function storagePath(solarkraftHome: string): string { * @param entry a call entry * @returns the filename, where the entry was stored */ -export function saveContractCallEntry(home: string, entry: ContractCallEntry) { +export function saveContractCallEntry(home: string, entry: ContractCallEntry): string { const filename = getEntryFilename(storagePath(home), entry) const verificationStatus: VerificationStatus = entry.verificationStatus ?? VerificationStatus.Unknown