From 7220489f7a33df10d07e6b9c8fde95fc0e6d8e6f Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sun, 8 Aug 2021 11:20:26 -0700 Subject: [PATCH] feat(swingset): hash kernel state changes into 'crankhash' The multiple members of a consensus machines are supposed to perform identical exection of every crank. To detect any possible divergence as quickly as possible, the kernel maintains the "crankhash": a constantly-updated string which incorporates (by SHA256 hash) a copy of every DB write and delete. `controller.getCrankHash()` can be run after one or more cranks have finished, and the resulting hex string can be e.g. stored in a host application consensus state vector. If two members diverge in a way that causes their swingset state to differ, they will have different crankhashes, and the consensus state vectors will diverge. This should cause at least one of them to fall out of consensus. Some keys are excluded from consensus: currently just those involving vat snapshots and the truncation (non-initial starting point) of the transcript. refs #3442 --- packages/SwingSet/src/controller.js | 6 + packages/SwingSet/src/hasher.js | 32 ++++ .../SwingSet/src/kernel/initializeKernel.js | 4 +- packages/SwingSet/src/kernel/kernel.js | 7 +- .../SwingSet/src/kernel/state/kernelKeeper.js | 28 +++- .../src/kernel/state/storageWrapper.js | 62 ++++++- packages/SwingSet/test/test-clist.js | 5 +- packages/SwingSet/test/test-controller.js | 5 + packages/SwingSet/test/test-gc-kernel.js | 2 + packages/SwingSet/test/test-hasher.js | 36 +++++ packages/SwingSet/test/test-kernel.js | 2 + packages/SwingSet/test/test-state.js | 152 ++++++++++++++++-- packages/SwingSet/test/test-vpid-kernel.js | 2 + 13 files changed, 315 insertions(+), 28 deletions(-) create mode 100644 packages/SwingSet/src/hasher.js create mode 100644 packages/SwingSet/test/test-hasher.js diff --git a/packages/SwingSet/src/controller.js b/packages/SwingSet/src/controller.js index 181b0eabd105..fa30b4c72a10 100644 --- a/packages/SwingSet/src/controller.js +++ b/packages/SwingSet/src/controller.js @@ -12,6 +12,7 @@ import { assert, details as X } from '@agoric/assert'; import { importBundle } from '@agoric/import-bundle'; import { xsnap, recordXSnap } from '@agoric/xsnap'; +import { createSHA256 } from './hasher.js'; import engineGC from './engine-gc.js'; import { WeakRef, FinalizationRegistry } from './weakref.js'; import { startSubprocessWorker } from './spawnSubprocessWorker.js'; @@ -271,6 +272,7 @@ export async function makeSwingsetController( WeakRef, FinalizationRegistry, gcAndFinalize: makeGcAndFinalize(engineGC), + createSHA256, }; const kernelOptions = { verbose, warehousePolicy, overrideVatManagerOptions }; @@ -327,6 +329,10 @@ export async function makeSwingsetController( return defensiveCopy(kernel.getStatus()); }, + getCrankHash() { + return kernel.getCrankHash(); + }, + pinVatRoot(vatName) { const vatID = kernel.vatNameToID(vatName); const kref = kernel.getRootObject(vatID); diff --git a/packages/SwingSet/src/hasher.js b/packages/SwingSet/src/hasher.js new file mode 100644 index 000000000000..491596158de5 --- /dev/null +++ b/packages/SwingSet/src/hasher.js @@ -0,0 +1,32 @@ +import { assert } from '@agoric/assert'; + +import { createHash } from 'crypto'; + +/** + * @typedef { (initial: string?) => { + * add: (more: string) => void, + * finish: () => string, + * } + * } CreateSHA256 + */ + +/** @type { CreateSHA256 } */ +function createSHA256(initial = undefined) { + const hash = createHash('sha256'); + let done = false; + function add(more) { + assert(!done); + hash.update(more); + } + function finish() { + assert(!done); + done = true; + return hash.digest('hex'); + } + if (initial) { + add(initial); + } + return harden({ add, finish }); +} +harden(createSHA256); +export { createSHA256 }; diff --git a/packages/SwingSet/src/kernel/initializeKernel.js b/packages/SwingSet/src/kernel/initializeKernel.js index 5ee988fbbb9b..e0b04a9ec88d 100644 --- a/packages/SwingSet/src/kernel/initializeKernel.js +++ b/packages/SwingSet/src/kernel/initializeKernel.js @@ -2,6 +2,7 @@ import { makeMarshal, Far } from '@agoric/marshal'; import { assert, details as X } from '@agoric/assert'; +import { createSHA256 } from '../hasher.js'; import { assertKnownOptions } from '../assertOptions.js'; import { insistVatID } from './id.js'; import { makeVatSlot } from '../parseVatSlots.js'; @@ -17,7 +18,8 @@ export function initializeKernel(config, hostStorage, verbose = false) { const logStartup = verbose ? console.debug : () => 0; insistStorageAPI(hostStorage.kvStore); - const kernelKeeper = makeKernelKeeper(hostStorage); + const kernelSlog = null; + const kernelKeeper = makeKernelKeeper(hostStorage, kernelSlog, createSHA256); const wasInitialized = kernelKeeper.getInitialized(); assert(!wasInitialized); diff --git a/packages/SwingSet/src/kernel/kernel.js b/packages/SwingSet/src/kernel/kernel.js index acf927b347ec..31e7074c3807 100644 --- a/packages/SwingSet/src/kernel/kernel.js +++ b/packages/SwingSet/src/kernel/kernel.js @@ -126,6 +126,7 @@ export default function buildKernel( WeakRef, FinalizationRegistry, gcAndFinalize, + createSHA256, } = kernelEndowments; deviceEndowments = { ...deviceEndowments }; // copy so we can modify const { @@ -142,7 +143,7 @@ export default function buildKernel( ? makeSlogger(slogCallbacks, writeSlogObject) : makeDummySlogger(slogCallbacks, makeConsole); - const kernelKeeper = makeKernelKeeper(hostStorage, kernelSlog); + const kernelKeeper = makeKernelKeeper(hostStorage, kernelSlog, createSHA256); let started = false; @@ -1206,6 +1207,10 @@ export default function buildKernel( }); }, + getCrankHash() { + return kernelKeeper.getCrankHash(); + }, + dump() { // note: dump().log is not deterministic, since log() does not go // through the syscall interface (and we replay transcripts one vat at diff --git a/packages/SwingSet/src/kernel/state/kernelKeeper.js b/packages/SwingSet/src/kernel/state/kernelKeeper.js index 3954f6049093..bcd83dccd4f0 100644 --- a/packages/SwingSet/src/kernel/state/kernelKeeper.js +++ b/packages/SwingSet/src/kernel/state/kernelKeeper.js @@ -131,18 +131,43 @@ const FIRST_METER_ID = 1n; /** * @param {HostStore} hostStorage * @param {KernelSlog} kernelSlog + * @param {CreateSHA256} createSHA256 */ -export default function makeKernelKeeper(hostStorage, kernelSlog) { +export default function makeKernelKeeper( + hostStorage, + kernelSlog, + createSHA256, +) { // the kernelKeeper wraps the host's raw key-value store in a crank buffer const rawKVStore = hostStorage.kvStore; insistStorageAPI(rawKVStore); + /** + * @param { string } key + * @returns { boolean } + */ + function isConsensusKey(key) { + if (/^v\d+\.lastSnapshot$/.test(key)) { + return false; + } + if (key.startsWith('snapshot.')) { + return false; + } + return true; + } + const { abortCrank, commitCrank, enhancedCrankBuffer: kvStore } = wrapStorage( rawKVStore, + createSHA256, + isConsensusKey, ); insistEnhancedStorageAPI(kvStore); const { streamStore, snapStore } = hostStorage; + function getCrankHash() { + return rawKVStore.get('crankhash'); + } + /** * @param {string} key * @returns {string} @@ -1337,6 +1362,7 @@ export default function makeKernelKeeper(hostStorage, kernelSlog) { kvStore, abortCrank, commitCrank, + getCrankHash, dump, }); diff --git a/packages/SwingSet/src/kernel/state/storageWrapper.js b/packages/SwingSet/src/kernel/state/storageWrapper.js index 746928e07f6f..e6afcfb09f25 100644 --- a/packages/SwingSet/src/kernel/state/storageWrapper.js +++ b/packages/SwingSet/src/kernel/state/storageWrapper.js @@ -18,19 +18,29 @@ import { insistStorageAPI } from '../../storageAPI.js'; * that buffers any mutations until told to commit them. * * @param {*} kvStore The storage object that this crank buffer will be based on. - * + * @param {CreateSHA256} createSHA256 + * @param { (key: string) => bool } isConsensusKey * @returns {*} an object { - * crankBuffer, // crank buffer as described, wrapping `kvStore` - * commitCrank, // function to save buffered mutations to `kvStore` - * abortCrank, // function to discard buffered mutations + * crankBuffer, // crank buffer as described, wrapping `kvStore` + * commitCrank, // function to save buffered mutations to `kvStore` + * abortCrank, // function to discard buffered mutations * } */ -export function buildCrankBuffer(kvStore) { +export function buildCrankBuffer( + kvStore, + createSHA256, + isConsensusKey = () => true, +) { insistStorageAPI(kvStore); + let crankhasher; + function resetCrankHash() { + crankhasher = createSHA256(); + } // to avoid confusion, additions and deletions should never share a key const additions = new Map(); const deletions = new Set(); + resetCrankHash(); const crankBuffer = { has(key) { @@ -76,17 +86,32 @@ export function buildCrankBuffer(kvStore) { assert.typeof(value, 'string'); additions.set(key, value); deletions.delete(key); + if (isConsensusKey(key)) { + crankhasher.add('add'); + crankhasher.add('\n'); + crankhasher.add(key); + crankhasher.add('\n'); + crankhasher.add(value); + crankhasher.add('\n'); + } }, delete(key) { assert.typeof(key, 'string'); additions.delete(key); deletions.add(key); + if (isConsensusKey(key)) { + crankhasher.add('delete'); + crankhasher.add('\n'); + crankhasher.add(key); + crankhasher.add('\n'); + } }, }; /** - * Flush any buffered mutations to the underlying storage. + * Flush any buffered mutations to the underlying storage, and update the + * crankhash. */ function commitCrank() { for (const [key, value] of additions) { @@ -97,6 +122,22 @@ export function buildCrankBuffer(kvStore) { } additions.clear(); deletions.clear(); + const crankhash = crankhasher.finish(); + resetCrankHash(); + + let oldCrankhash = kvStore.get('crankhash'); + if (oldCrankhash === undefined) { + oldCrankhash = ''; + } + const hasher = createSHA256('crankhash\n'); + hasher.add(oldCrankhash); + hasher.add('\n'); + hasher.add(crankhash); + hasher.add('\n'); + const newCrankhash = hasher.finish(); + kvStore.set('crankhash', newCrankhash); + + return { crankhash, newCrankhash }; } /** @@ -105,6 +146,7 @@ export function buildCrankBuffer(kvStore) { function abortCrank() { additions.clear(); deletions.clear(); + resetCrankHash(); } return harden({ crankBuffer, commitCrank, abortCrank }); @@ -166,9 +208,13 @@ export function addHelpers(kvStore) { // write-back buffer wrapper (the CrankBuffer), but the keeper is unaware of // that. -export function wrapStorage(kvStore) { +export function wrapStorage(kvStore, createSHA256, isConsensusKey) { insistStorageAPI(kvStore); - const { crankBuffer, commitCrank, abortCrank } = buildCrankBuffer(kvStore); + const { crankBuffer, commitCrank, abortCrank } = buildCrankBuffer( + kvStore, + createSHA256, + isConsensusKey, + ); const enhancedCrankBuffer = addHelpers(crankBuffer); return { enhancedCrankBuffer, commitCrank, abortCrank }; } diff --git a/packages/SwingSet/test/test-clist.js b/packages/SwingSet/test/test-clist.js index 791555ce9294..e518aad0a488 100644 --- a/packages/SwingSet/test/test-clist.js +++ b/packages/SwingSet/test/test-clist.js @@ -2,13 +2,14 @@ import { test } from '../tools/prepare-test-env-ava.js'; // eslint-disable-next-line import/order import { initSimpleSwingStore } from '@agoric/swing-store-simple'; +import { createSHA256 } from '../src/hasher.js'; import { makeDummySlogger } from '../src/kernel/slogger.js'; import makeKernelKeeper from '../src/kernel/state/kernelKeeper.js'; test(`clist reachability`, async t => { const slog = makeDummySlogger({}); const hostStorage = initSimpleSwingStore(); - const kk = makeKernelKeeper(hostStorage, slog); + const kk = makeKernelKeeper(hostStorage, slog, createSHA256); const s = kk.kvStore; kk.createStartingKernelState('local'); const vatID = kk.allocateUnusedVatID(); @@ -93,7 +94,7 @@ test(`clist reachability`, async t => { test('getImporters', async t => { const slog = makeDummySlogger({}); const hostStorage = initSimpleSwingStore(); - const kk = makeKernelKeeper(hostStorage, slog); + const kk = makeKernelKeeper(hostStorage, slog, createSHA256); kk.createStartingKernelState('local'); const vatID1 = kk.allocateUnusedVatID(); diff --git a/packages/SwingSet/test/test-controller.js b/packages/SwingSet/test/test-controller.js index 1815a1b88be9..71d806d260f8 100644 --- a/packages/SwingSet/test/test-controller.js +++ b/packages/SwingSet/test/test-controller.js @@ -86,6 +86,11 @@ async function simpleCall(t) { controller.log('2'); t.is(controller.dump().log[1], '2'); + + // hash determined experimentally: will change if the initial kernel state + // ever changes + const h = '08b771e7bbf966c2b4dd98737c5e7e827fc4e688160557579f89649f87e4ec4f'; + t.is(controller.getCrankHash(), h); } test('simple call', async t => { diff --git a/packages/SwingSet/test/test-gc-kernel.js b/packages/SwingSet/test/test-gc-kernel.js index da4b0cb06b8f..e394ac36a7de 100644 --- a/packages/SwingSet/test/test-gc-kernel.js +++ b/packages/SwingSet/test/test-gc-kernel.js @@ -4,6 +4,7 @@ import { test } from '../tools/prepare-test-env-ava.js'; import { WeakRef, FinalizationRegistry } from '../src/weakref.js'; import { waitUntilQuiescent } from '../src/waitUntilQuiescent.js'; +import { createSHA256 } from '../src/hasher.js'; import buildKernel from '../src/kernel/index.js'; import { initializeKernel } from '../src/kernel/initializeKernel.js'; @@ -51,6 +52,7 @@ function makeEndowments() { writeSlogObject, WeakRef, FinalizationRegistry, + createSHA256, }; } diff --git a/packages/SwingSet/test/test-hasher.js b/packages/SwingSet/test/test-hasher.js new file mode 100644 index 000000000000..8d5c9cfeedac --- /dev/null +++ b/packages/SwingSet/test/test-hasher.js @@ -0,0 +1,36 @@ +import { test } from '../tools/prepare-test-env-ava.js'; + +// eslint-disable-next-line import/order +import { createSHA256 } from '../src/hasher.js'; + +test('createSHA256', t => { + t.is( + createSHA256().finish(), + 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', + ); + + const h1 = createSHA256('a'); + t.is( + h1.finish(), + 'ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb', + ); + + const h2 = createSHA256(); + h2.add('a'); + t.is( + h2.finish(), + 'ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb', + ); + + const h3 = createSHA256('a'); + h3.add('b'); + t.is( + h3.finish(), + 'fb8e20fc2e4c3f248c60c39bd652f3c1347298bb977b8b4d5903b85055620603', + ); + + const h4 = createSHA256(); + h4.finish(); + t.throws(h4.add); + t.throws(h4.finish); +}); diff --git a/packages/SwingSet/test/test-kernel.js b/packages/SwingSet/test/test-kernel.js index 9591664790dd..d14824b1375b 100644 --- a/packages/SwingSet/test/test-kernel.js +++ b/packages/SwingSet/test/test-kernel.js @@ -5,6 +5,7 @@ import anylogger from 'anylogger'; import { assert, details as X } from '@agoric/assert'; import { WeakRef, FinalizationRegistry } from '../src/weakref.js'; import { waitUntilQuiescent } from '../src/waitUntilQuiescent.js'; +import { createSHA256 } from '../src/hasher.js'; import buildKernel from '../src/kernel/index.js'; import { initializeKernel } from '../src/kernel/initializeKernel.js'; @@ -62,6 +63,7 @@ function makeEndowments() { makeConsole, WeakRef, FinalizationRegistry, + createSHA256, }; } diff --git a/packages/SwingSet/test/test-state.js b/packages/SwingSet/test/test-state.js index 2c81cf7c0454..177e0e6f0555 100644 --- a/packages/SwingSet/test/test-state.js +++ b/packages/SwingSet/test/test-state.js @@ -1,11 +1,14 @@ +/* eslint-disable no-useless-concat */ +// eslint-disable-next-line import/order import { test } from '../tools/prepare-test-env-ava.js'; - // eslint-disable-next-line import/order +import { createHash } from 'crypto'; import { initSimpleSwingStore, getAllState, setAllState, } from '@agoric/swing-store-simple'; +import { createSHA256 } from '../src/hasher.js'; import { buildHostDBInMemory } from '../src/hostStorage.js'; import { buildBlockBuffer } from '../src/blockBuffer.js'; import makeKernelKeeper from '../src/kernel/state/kernelKeeper.js'; @@ -18,7 +21,10 @@ function checkState(t, getState, expected) { const state = getState(); const got = []; for (const key of Object.getOwnPropertyNames(state)) { - got.push([key, state[key]]); + if (key !== 'crankhash') { + // the hash is just too annoying to compare against + got.push([key, state[key]]); + } } function compareStrings(a, b) { if (a > b) { @@ -118,7 +124,10 @@ test('blockBuffer fulfills storage API', t => { test('crankBuffer fulfills storage API', t => { const store = initSimpleSwingStore(); - const { crankBuffer, commitCrank } = buildCrankBuffer(store.kvStore); + const { crankBuffer, commitCrank } = buildCrankBuffer( + store.kvStore, + createSHA256, + ); testStorage(t, crankBuffer, () => getAllState(store).kvStuff, commitCrank); }); @@ -127,6 +136,7 @@ test('crankBuffer can abortCrank', t => { const { blockBuffer, commitBlock } = buildBlockBuffer(hostDB); const { crankBuffer: s, commitCrank, abortCrank } = buildCrankBuffer( blockBuffer, + createSHA256, ); s.set('foo', 'f'); @@ -235,7 +245,7 @@ function buildKeeperStorageInMemory() { function duplicateKeeper(getState) { const store = initSimpleSwingStore(); setAllState(store, { kvStuff: getState(), streamStuff: new Map() }); - return makeKernelKeeper(store); + return makeKernelKeeper(store, null, createSHA256); } test('hostStorage param guards', async t => { @@ -253,7 +263,7 @@ test('hostStorage param guards', async t => { test('kernel state', async t => { const store = buildKeeperStorageInMemory(); const { getState } = store; - const k = makeKernelKeeper(store); + const k = makeKernelKeeper(store, null, createSHA256); t.truthy(!k.getInitialized()); k.createStartingKernelState('local'); k.setInitialized(); @@ -280,7 +290,7 @@ test('kernel state', async t => { test('kernelKeeper vat names', async t => { const store = buildKeeperStorageInMemory(); const { getState } = store; - const k = makeKernelKeeper(store); + const k = makeKernelKeeper(store, null, createSHA256); k.createStartingKernelState('local'); const v1 = k.allocateVatIDForNameIfNeeded('vatname5'); @@ -325,7 +335,7 @@ test('kernelKeeper vat names', async t => { test('kernelKeeper device names', async t => { const store = buildKeeperStorageInMemory(); const { getState } = store; - const k = makeKernelKeeper(store); + const k = makeKernelKeeper(store, null, createSHA256); k.createStartingKernelState('local'); const d7 = k.allocateDeviceIDForNameIfNeeded('devicename5'); @@ -370,7 +380,7 @@ test('kernelKeeper device names', async t => { test('kernelKeeper runQueue', async t => { const store = buildKeeperStorageInMemory(); const { getState } = store; - const k = makeKernelKeeper(store); + const k = makeKernelKeeper(store, null, createSHA256); k.createStartingKernelState('local'); t.truthy(k.isRunQueueEmpty()); @@ -407,7 +417,7 @@ test('kernelKeeper runQueue', async t => { test('kernelKeeper promises', async t => { const store = buildKeeperStorageInMemory(); const { getState } = store; - const k = makeKernelKeeper(store); + const k = makeKernelKeeper(store, null, createSHA256); k.createStartingKernelState('local'); const p1 = k.addKernelPromiseForVat('v4'); @@ -529,7 +539,7 @@ test('kernelKeeper promises', async t => { test('kernelKeeper promise resolveToData', async t => { const store = buildKeeperStorageInMemory(); - const k = makeKernelKeeper(store); + const k = makeKernelKeeper(store, null, createSHA256); k.createStartingKernelState('local'); const p1 = k.addKernelPromiseForVat('v4'); @@ -551,7 +561,7 @@ test('kernelKeeper promise resolveToData', async t => { test('kernelKeeper promise reject', async t => { const store = buildKeeperStorageInMemory(); - const k = makeKernelKeeper(store); + const k = makeKernelKeeper(store, null, createSHA256); k.createStartingKernelState('local'); const p1 = k.addKernelPromiseForVat('v4'); @@ -574,7 +584,7 @@ test('kernelKeeper promise reject', async t => { test('vatKeeper', async t => { const store = buildKeeperStorageInMemory(); const { getState } = store; - const k = makeKernelKeeper(store); + const k = makeKernelKeeper(store, null, createSHA256); k.createStartingKernelState('local'); const v1 = k.allocateVatIDForNameIfNeeded('name1'); @@ -611,7 +621,7 @@ test('vatKeeper', async t => { test('vatKeeper.getOptions', async t => { const store = buildKeeperStorageInMemory(); - const k = makeKernelKeeper(store); + const k = makeKernelKeeper(store, null, createSHA256); k.createStartingKernelState('local'); const v1 = k.allocateVatIDForNameIfNeeded('name1'); @@ -629,14 +639,14 @@ test('vatKeeper.getOptions', async t => { test('XS vatKeeper defaultManagerType', async t => { const store = buildKeeperStorageInMemory(); - const k = makeKernelKeeper(store); + const k = makeKernelKeeper(store, null, createSHA256); k.createStartingKernelState('xs-worker'); t.is(k.getDefaultManagerType(), 'xs-worker'); }); test('meters', async t => { const store = buildKeeperStorageInMemory(); - const k = makeKernelKeeper(store); + const k = makeKernelKeeper(store, null, createSHA256); k.createStartingKernelState('local'); const m1 = k.allocateMeter(100n, 10n); const m2 = k.allocateMeter(200n, 150n); @@ -673,3 +683,115 @@ test('meters', async t => { t.deepEqual(k.deductMeter(m3, 1000n), { underflow: false, notify: false }); t.deepEqual(k.getMeter(m3), { remaining: 'unlimited', threshold: 5n }); }); + +test('crankhash', t => { + const store = buildKeeperStorageInMemory(); + const k = makeKernelKeeper(store, null, createSHA256); + k.createStartingKernelState('local'); + k.commitCrank(); + // the initial state additions happen to hash to this: + const oldCrankhash = + '03cc440eb84aeb06d16dd270b4042c1ab2335b14b2b80f1bac48ab032cd3f3b5'; + + k.kvStore.set('one', '1'); + let h = createHash('sha256'); + h.update('add\n' + 'one\n' + '1\n'); + const expCrankhash = h.digest('hex'); + t.is( + expCrankhash, + '29dedad4ccd119b6f7d80109590cc357c69eb4f03210cdbc9b1c982cd228fd8b', + ); + + h = createHash('sha256'); + h.update('crankhash\n'); + h.update(`${oldCrankhash}\n${expCrankhash}\n`); + const expNewCrankhash = h.digest('hex'); + t.is( + expNewCrankhash, + '30bb5fcaeb9344fb5c4ef3c959cca7436b6e88b08f5165c92024426d1ab2ac31', + ); + + const { crankhash, newCrankhash } = k.commitCrank(); + t.is(crankhash, expCrankhash); + t.is(newCrankhash, expNewCrankhash); + t.is(store.kvStore.get('crankhash'), expNewCrankhash); +}); + +test('crankhash - skip keys', t => { + const store = buildKeeperStorageInMemory(); + const k = makeKernelKeeper(store, null, createSHA256); + k.createStartingKernelState('local'); + k.commitCrank(); + + k.kvStore.set('one', '1'); + const h = createHash('sha256'); + h.update('add\n' + 'one\n' + '1\n'); + const expCrankhash = h.digest('hex'); + t.is( + expCrankhash, + '29dedad4ccd119b6f7d80109590cc357c69eb4f03210cdbc9b1c982cd228fd8b', + ); + t.is(k.commitCrank().crankhash, expCrankhash); + + // certain local keys are excluded from consensus, and should not affect + // the hash + k.kvStore.set('one', '1'); + k.kvStore.set('snapshot.XYZ', '["vat1234"]'); + k.kvStore.set('v1234.lastSnapshot', '{"snapshotID":"XYZ","startPos":4}'); + t.is(k.commitCrank().crankhash, expCrankhash); +}); + +test('crankhash - duplicate set', t => { + // setting the same key multiple times counts as divergence + + const store = buildKeeperStorageInMemory(); + const k = makeKernelKeeper(store, null, createSHA256); + k.createStartingKernelState('local'); + k.commitCrank(); + + k.kvStore.set('one', '1'); + const h = createHash('sha256'); + h.update('add\n' + 'one\n' + '1\n'); + const expCrankhash = h.digest('hex'); + t.is( + expCrankhash, + '29dedad4ccd119b6f7d80109590cc357c69eb4f03210cdbc9b1c982cd228fd8b', + ); + t.is(k.commitCrank().crankhash, expCrankhash); + + k.kvStore.set('one', '1'); + k.kvStore.set('one', '1'); + const h2 = createHash('sha256'); + h2.update('add\n' + 'one\n' + '1\n'); + h2.update('add\n' + 'one\n' + '1\n'); + const expCrankhash2 = h2.digest('hex'); + t.is( + expCrankhash2, + '6e82c45c44062ceb71cf242a79aa76578a2dd3002e0b76d756790418914ccc34', + ); + t.is(k.commitCrank().crankhash, expCrankhash2); +}); + +test('crankhash - set and delete', t => { + // setting and deleting a key is different than never setting it + + const store = buildKeeperStorageInMemory(); + const k = makeKernelKeeper(store, null, createSHA256); + k.createStartingKernelState('local'); + k.commitCrank(); + + const h1 = createHash('sha256'); + const expCrankhash1 = h1.digest('hex'); + t.is(k.commitCrank().crankhash, expCrankhash1); // empty + + const h2 = createHash('sha256'); + h2.update('add\n' + 'one\n' + '1\n'); + h2.update('delete\n' + 'one\n'); + const expCrankhash2 = h2.digest('hex'); + + k.kvStore.set('one', '1'); + k.kvStore.delete('one'); + t.is(k.commitCrank().crankhash, expCrankhash2); + + t.not(expCrankhash1, expCrankhash2); +}); diff --git a/packages/SwingSet/test/test-vpid-kernel.js b/packages/SwingSet/test/test-vpid-kernel.js index 41d5b331512b..ade8e6877b1f 100644 --- a/packages/SwingSet/test/test-vpid-kernel.js +++ b/packages/SwingSet/test/test-vpid-kernel.js @@ -5,6 +5,7 @@ import anylogger from 'anylogger'; import { assert, details as X } from '@agoric/assert'; import { WeakRef, FinalizationRegistry } from '../src/weakref.js'; import { waitUntilQuiescent } from '../src/waitUntilQuiescent.js'; +import { createSHA256 } from '../src/hasher.js'; import { provideHostStorage } from '../src/hostStorage.js'; import buildKernel from '../src/kernel/index.js'; @@ -41,6 +42,7 @@ function makeEndowments() { makeConsole, WeakRef, FinalizationRegistry, + createSHA256, }; }