Skip to content

Commit 2805bf3

Browse files
committed
feat(swingset): hash kernel state changes into 'crankhash'
The multiple members of a consensus machine 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
1 parent b58196d commit 2805bf3

13 files changed

+315
-28
lines changed

packages/SwingSet/src/controller.js

+6
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { assert, details as X } from '@agoric/assert';
1212
import { importBundle } from '@agoric/import-bundle';
1313
import { xsnap, recordXSnap } from '@agoric/xsnap';
1414

15+
import { createSHA256 } from './hasher.js';
1516
import engineGC from './engine-gc.js';
1617
import { WeakRef, FinalizationRegistry } from './weakref.js';
1718
import { startSubprocessWorker } from './spawnSubprocessWorker.js';
@@ -271,6 +272,7 @@ export async function makeSwingsetController(
271272
WeakRef,
272273
FinalizationRegistry,
273274
gcAndFinalize: makeGcAndFinalize(engineGC),
275+
createSHA256,
274276
};
275277

276278
const kernelOptions = { verbose, warehousePolicy, overrideVatManagerOptions };
@@ -327,6 +329,10 @@ export async function makeSwingsetController(
327329
return defensiveCopy(kernel.getStatus());
328330
},
329331

332+
getCrankHash() {
333+
return kernel.getCrankHash();
334+
},
335+
330336
pinVatRoot(vatName) {
331337
const vatID = kernel.vatNameToID(vatName);
332338
const kref = kernel.getRootObject(vatID);

packages/SwingSet/src/hasher.js

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { assert } from '@agoric/assert';
2+
3+
import { createHash } from 'crypto';
4+
5+
/**
6+
* @typedef { (initial: string?) => {
7+
* add: (more: string) => void,
8+
* finish: () => string,
9+
* }
10+
* } CreateSHA256
11+
*/
12+
13+
/** @type { CreateSHA256 } */
14+
function createSHA256(initial = undefined) {
15+
const hash = createHash('sha256');
16+
let done = false;
17+
function add(more) {
18+
assert(!done);
19+
hash.update(more);
20+
}
21+
function finish() {
22+
assert(!done);
23+
done = true;
24+
return hash.digest('hex');
25+
}
26+
if (initial) {
27+
add(initial);
28+
}
29+
return harden({ add, finish });
30+
}
31+
harden(createSHA256);
32+
export { createSHA256 };

packages/SwingSet/src/kernel/initializeKernel.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { makeMarshal, Far } from '@agoric/marshal';
44
import { assert, details as X } from '@agoric/assert';
5+
import { createSHA256 } from '../hasher.js';
56
import { assertKnownOptions } from '../assertOptions.js';
67
import { insistVatID } from './id.js';
78
import { makeVatSlot } from '../parseVatSlots.js';
@@ -17,7 +18,8 @@ export function initializeKernel(config, hostStorage, verbose = false) {
1718
const logStartup = verbose ? console.debug : () => 0;
1819
insistStorageAPI(hostStorage.kvStore);
1920

20-
const kernelKeeper = makeKernelKeeper(hostStorage);
21+
const kernelSlog = null;
22+
const kernelKeeper = makeKernelKeeper(hostStorage, kernelSlog, createSHA256);
2123

2224
const wasInitialized = kernelKeeper.getInitialized();
2325
assert(!wasInitialized);

packages/SwingSet/src/kernel/kernel.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ export default function buildKernel(
126126
WeakRef,
127127
FinalizationRegistry,
128128
gcAndFinalize,
129+
createSHA256,
129130
} = kernelEndowments;
130131
deviceEndowments = { ...deviceEndowments }; // copy so we can modify
131132
const {
@@ -142,7 +143,7 @@ export default function buildKernel(
142143
? makeSlogger(slogCallbacks, writeSlogObject)
143144
: makeDummySlogger(slogCallbacks, makeConsole);
144145

145-
const kernelKeeper = makeKernelKeeper(hostStorage, kernelSlog);
146+
const kernelKeeper = makeKernelKeeper(hostStorage, kernelSlog, createSHA256);
146147

147148
let started = false;
148149

@@ -1206,6 +1207,10 @@ export default function buildKernel(
12061207
});
12071208
},
12081209

1210+
getCrankHash() {
1211+
return kernelKeeper.getCrankHash();
1212+
},
1213+
12091214
dump() {
12101215
// note: dump().log is not deterministic, since log() does not go
12111216
// through the syscall interface (and we replay transcripts one vat at

packages/SwingSet/src/kernel/state/kernelKeeper.js

+27-1
Original file line numberDiff line numberDiff line change
@@ -131,18 +131,43 @@ const FIRST_METER_ID = 1n;
131131
/**
132132
* @param {HostStore} hostStorage
133133
* @param {KernelSlog} kernelSlog
134+
* @param {import('../../hasher.js').CreateSHA256} createSHA256
134135
*/
135-
export default function makeKernelKeeper(hostStorage, kernelSlog) {
136+
export default function makeKernelKeeper(
137+
hostStorage,
138+
kernelSlog,
139+
createSHA256,
140+
) {
136141
// the kernelKeeper wraps the host's raw key-value store in a crank buffer
137142
const rawKVStore = hostStorage.kvStore;
138143
insistStorageAPI(rawKVStore);
139144

145+
/**
146+
* @param { string } key
147+
* @returns { boolean }
148+
*/
149+
function isConsensusKey(key) {
150+
if (/^v\d+\.lastSnapshot$/.test(key)) {
151+
return false;
152+
}
153+
if (key.startsWith('snapshot.')) {
154+
return false;
155+
}
156+
return true;
157+
}
158+
140159
const { abortCrank, commitCrank, enhancedCrankBuffer: kvStore } = wrapStorage(
141160
rawKVStore,
161+
createSHA256,
162+
isConsensusKey,
142163
);
143164
insistEnhancedStorageAPI(kvStore);
144165
const { streamStore, snapStore } = hostStorage;
145166

167+
function getCrankHash() {
168+
return rawKVStore.get('crankhash');
169+
}
170+
146171
/**
147172
* @param {string} key
148173
* @returns {string}
@@ -1337,6 +1362,7 @@ export default function makeKernelKeeper(hostStorage, kernelSlog) {
13371362
kvStore,
13381363
abortCrank,
13391364
commitCrank,
1365+
getCrankHash,
13401366

13411367
dump,
13421368
});

packages/SwingSet/src/kernel/state/storageWrapper.js

+54-8
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,29 @@ import { insistStorageAPI } from '../../storageAPI.js';
1818
* that buffers any mutations until told to commit them.
1919
*
2020
* @param {*} kvStore The storage object that this crank buffer will be based on.
21-
*
21+
* @param {CreateSHA256} createSHA256
22+
* @param { (key: string) => bool } isConsensusKey
2223
* @returns {*} an object {
23-
* crankBuffer, // crank buffer as described, wrapping `kvStore`
24-
* commitCrank, // function to save buffered mutations to `kvStore`
25-
* abortCrank, // function to discard buffered mutations
24+
* crankBuffer, // crank buffer as described, wrapping `kvStore`
25+
* commitCrank, // function to save buffered mutations to `kvStore`
26+
* abortCrank, // function to discard buffered mutations
2627
* }
2728
*/
28-
export function buildCrankBuffer(kvStore) {
29+
export function buildCrankBuffer(
30+
kvStore,
31+
createSHA256,
32+
isConsensusKey = () => true,
33+
) {
2934
insistStorageAPI(kvStore);
35+
let crankhasher;
36+
function resetCrankHash() {
37+
crankhasher = createSHA256();
38+
}
3039

3140
// to avoid confusion, additions and deletions should never share a key
3241
const additions = new Map();
3342
const deletions = new Set();
43+
resetCrankHash();
3444

3545
const crankBuffer = {
3646
has(key) {
@@ -76,17 +86,32 @@ export function buildCrankBuffer(kvStore) {
7686
assert.typeof(value, 'string');
7787
additions.set(key, value);
7888
deletions.delete(key);
89+
if (isConsensusKey(key)) {
90+
crankhasher.add('add');
91+
crankhasher.add('\n');
92+
crankhasher.add(key);
93+
crankhasher.add('\n');
94+
crankhasher.add(value);
95+
crankhasher.add('\n');
96+
}
7997
},
8098

8199
delete(key) {
82100
assert.typeof(key, 'string');
83101
additions.delete(key);
84102
deletions.add(key);
103+
if (isConsensusKey(key)) {
104+
crankhasher.add('delete');
105+
crankhasher.add('\n');
106+
crankhasher.add(key);
107+
crankhasher.add('\n');
108+
}
85109
},
86110
};
87111

88112
/**
89-
* Flush any buffered mutations to the underlying storage.
113+
* Flush any buffered mutations to the underlying storage, and update the
114+
* crankhash.
90115
*/
91116
function commitCrank() {
92117
for (const [key, value] of additions) {
@@ -97,6 +122,22 @@ export function buildCrankBuffer(kvStore) {
97122
}
98123
additions.clear();
99124
deletions.clear();
125+
const crankhash = crankhasher.finish();
126+
resetCrankHash();
127+
128+
let oldCrankhash = kvStore.get('crankhash');
129+
if (oldCrankhash === undefined) {
130+
oldCrankhash = '';
131+
}
132+
const hasher = createSHA256('crankhash\n');
133+
hasher.add(oldCrankhash);
134+
hasher.add('\n');
135+
hasher.add(crankhash);
136+
hasher.add('\n');
137+
const newCrankhash = hasher.finish();
138+
kvStore.set('crankhash', newCrankhash);
139+
140+
return { crankhash, newCrankhash };
100141
}
101142

102143
/**
@@ -105,6 +146,7 @@ export function buildCrankBuffer(kvStore) {
105146
function abortCrank() {
106147
additions.clear();
107148
deletions.clear();
149+
resetCrankHash();
108150
}
109151

110152
return harden({ crankBuffer, commitCrank, abortCrank });
@@ -166,9 +208,13 @@ export function addHelpers(kvStore) {
166208
// write-back buffer wrapper (the CrankBuffer), but the keeper is unaware of
167209
// that.
168210

169-
export function wrapStorage(kvStore) {
211+
export function wrapStorage(kvStore, createSHA256, isConsensusKey) {
170212
insistStorageAPI(kvStore);
171-
const { crankBuffer, commitCrank, abortCrank } = buildCrankBuffer(kvStore);
213+
const { crankBuffer, commitCrank, abortCrank } = buildCrankBuffer(
214+
kvStore,
215+
createSHA256,
216+
isConsensusKey,
217+
);
172218
const enhancedCrankBuffer = addHelpers(crankBuffer);
173219
return { enhancedCrankBuffer, commitCrank, abortCrank };
174220
}

packages/SwingSet/test/test-clist.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ import { test } from '../tools/prepare-test-env-ava.js';
22

33
// eslint-disable-next-line import/order
44
import { initSimpleSwingStore } from '@agoric/swing-store-simple';
5+
import { createSHA256 } from '../src/hasher.js';
56
import { makeDummySlogger } from '../src/kernel/slogger.js';
67
import makeKernelKeeper from '../src/kernel/state/kernelKeeper.js';
78

89
test(`clist reachability`, async t => {
910
const slog = makeDummySlogger({});
1011
const hostStorage = initSimpleSwingStore();
11-
const kk = makeKernelKeeper(hostStorage, slog);
12+
const kk = makeKernelKeeper(hostStorage, slog, createSHA256);
1213
const s = kk.kvStore;
1314
kk.createStartingKernelState('local');
1415
const vatID = kk.allocateUnusedVatID();
@@ -93,7 +94,7 @@ test(`clist reachability`, async t => {
9394
test('getImporters', async t => {
9495
const slog = makeDummySlogger({});
9596
const hostStorage = initSimpleSwingStore();
96-
const kk = makeKernelKeeper(hostStorage, slog);
97+
const kk = makeKernelKeeper(hostStorage, slog, createSHA256);
9798

9899
kk.createStartingKernelState('local');
99100
const vatID1 = kk.allocateUnusedVatID();

packages/SwingSet/test/test-controller.js

+5
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@ async function simpleCall(t) {
8686

8787
controller.log('2');
8888
t.is(controller.dump().log[1], '2');
89+
90+
// hash determined experimentally: will change if the initial kernel state
91+
// ever changes
92+
const h = '08b771e7bbf966c2b4dd98737c5e7e827fc4e688160557579f89649f87e4ec4f';
93+
t.is(controller.getCrankHash(), h);
8994
}
9095

9196
test('simple call', async t => {

packages/SwingSet/test/test-gc-kernel.js

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { test } from '../tools/prepare-test-env-ava.js';
44

55
import { WeakRef, FinalizationRegistry } from '../src/weakref.js';
66
import { waitUntilQuiescent } from '../src/waitUntilQuiescent.js';
7+
import { createSHA256 } from '../src/hasher.js';
78

89
import buildKernel from '../src/kernel/index.js';
910
import { initializeKernel } from '../src/kernel/initializeKernel.js';
@@ -51,6 +52,7 @@ function makeEndowments() {
5152
writeSlogObject,
5253
WeakRef,
5354
FinalizationRegistry,
55+
createSHA256,
5456
};
5557
}
5658

packages/SwingSet/test/test-hasher.js

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { test } from '../tools/prepare-test-env-ava.js';
2+
3+
// eslint-disable-next-line import/order
4+
import { createSHA256 } from '../src/hasher.js';
5+
6+
test('createSHA256', t => {
7+
t.is(
8+
createSHA256().finish(),
9+
'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
10+
);
11+
12+
const h1 = createSHA256('a');
13+
t.is(
14+
h1.finish(),
15+
'ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb',
16+
);
17+
18+
const h2 = createSHA256();
19+
h2.add('a');
20+
t.is(
21+
h2.finish(),
22+
'ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb',
23+
);
24+
25+
const h3 = createSHA256('a');
26+
h3.add('b');
27+
t.is(
28+
h3.finish(),
29+
'fb8e20fc2e4c3f248c60c39bd652f3c1347298bb977b8b4d5903b85055620603',
30+
);
31+
32+
const h4 = createSHA256();
33+
h4.finish();
34+
t.throws(h4.add);
35+
t.throws(h4.finish);
36+
});

packages/SwingSet/test/test-kernel.js

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import anylogger from 'anylogger';
55
import { assert, details as X } from '@agoric/assert';
66
import { WeakRef, FinalizationRegistry } from '../src/weakref.js';
77
import { waitUntilQuiescent } from '../src/waitUntilQuiescent.js';
8+
import { createSHA256 } from '../src/hasher.js';
89

910
import buildKernel from '../src/kernel/index.js';
1011
import { initializeKernel } from '../src/kernel/initializeKernel.js';
@@ -62,6 +63,7 @@ function makeEndowments() {
6263
makeConsole,
6364
WeakRef,
6465
FinalizationRegistry,
66+
createSHA256,
6567
};
6668
}
6769

0 commit comments

Comments
 (0)