Skip to content

Commit 0009a31

Browse files
committed
feat(test-utils): let a concurrency spec pick the deployer
The constructor ran on whichever instance `Object.values` yielded first, so the deploying party was decided by the order names happened to be written in. On a module whose initializer reads a witness, that seeds the shared ledger with one party's secrets and the spec has no way to see it: the choice is invisible at the call site and changes if someone reorders the names. Naming it makes the choice explicit where it matters and an error where it is wrong. The default is unchanged, so nothing existing moves, and the note core has no initializer, so nothing here needed it yet. Dry only. The live backend deploys from the wallet pool's own deployer, which is not a party, so the mapping is a question for whoever builds that seam rather than something to guess at now. Refs: #743 (comment)
1 parent 1cb449f commit 0009a31

3 files changed

Lines changed: 98 additions & 4 deletions

File tree

contracts/test-utils/concurrency/DryReplayHarness.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ export class DryReplayHarness<P> implements ConcurrencyHarness<ChargedState> {
7979
this.privateState = options.privateState;
8080
this.address = options.contractAddress ?? dummyContractAddress();
8181
this.coinPublicKey = options.coinPublicKey ?? DEFAULT_COIN_PUBLIC_KEY;
82-
this.current = this.deploy(options.constructorArgs ?? []);
82+
this.current = this.deploy(options.deployer, options.constructorArgs ?? []);
8383
}
8484

8585
/** The ledger as it stands, for a spec that wants to read it. */
@@ -176,9 +176,29 @@ export class DryReplayHarness<P> implements ConcurrencyHarness<ChargedState> {
176176
).state;
177177
}
178178

179-
/** Every actor shares one deployed ledger; it lives here, not on them. */
180-
private deploy(constructorArgs: readonly unknown[]): ChargedState {
181-
const [deployer] = Object.values(this.contracts);
179+
/**
180+
* Every actor shares one deployed ledger; it lives here, not on them.
181+
*
182+
* The constructor runs on ONE party's instance, so its witnesses are that
183+
* party's. `deployerName` names which, for a module whose initializer reads
184+
* one.
185+
*/
186+
private deploy(
187+
deployerName: string | undefined,
188+
constructorArgs: readonly unknown[],
189+
): ChargedState {
190+
if (
191+
deployerName !== undefined &&
192+
this.contracts[deployerName] === undefined
193+
) {
194+
throw new Error(
195+
`concurrency harness: unknown deployer '${deployerName}'`,
196+
);
197+
}
198+
const deployer =
199+
deployerName === undefined
200+
? Object.values(this.contracts)[0]
201+
: this.contracts[deployerName];
182202
if (deployer === undefined) {
183203
throw new Error('concurrency harness: no contracts given');
184204
}

contracts/test-utils/concurrency/test/DryReplayHarness.test.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,23 @@ const stubContract = (): ReplayableContract<Record<string, never>> =>
2828
},
2929
}) as unknown as ReplayableContract<Record<string, never>>;
3030

31+
/** Like {@link stubContract}, but names itself when its constructor runs. */
32+
const recordingContract = (
33+
deployed: string[],
34+
name: string,
35+
): ReplayableContract<Record<string, never>> =>
36+
({
37+
initialState: () => {
38+
deployed.push(name);
39+
return {
40+
currentPrivateState: {},
41+
currentContractState: new ContractState(),
42+
currentZswapLocalState: {},
43+
};
44+
},
45+
impureCircuits: {},
46+
}) as unknown as ReplayableContract<Record<string, never>>;
47+
3148
const options = () => ({
3249
contracts: { alice: stubContract() },
3350
privateState: {},
@@ -94,3 +111,51 @@ describe('createConcurrencyHarness', () => {
94111
);
95112
});
96113
});
114+
115+
// ---------------------------------------------------------------------------
116+
// Which party's instance runs the constructor
117+
// ---------------------------------------------------------------------------
118+
119+
/**
120+
* The constructor's witnesses are the deploying party's, so on a module with an
121+
* initializer this decides whose secrets seed the shared ledger.
122+
*/
123+
describe('DryReplayHarness deployer', () => {
124+
const recording = () => {
125+
const deployed: string[] = [];
126+
return {
127+
deployed,
128+
contracts: {
129+
alice: recordingContract(deployed, 'alice'),
130+
bob: recordingContract(deployed, 'bob'),
131+
},
132+
privateState: {},
133+
};
134+
};
135+
136+
it('should deploy with the first party by default', () => {
137+
const { deployed, ...options } = recording();
138+
139+
createDryHarness(options);
140+
141+
expect(deployed).toStrictEqual(['alice']);
142+
});
143+
144+
it('should deploy with the named party', () => {
145+
const { deployed, ...options } = recording();
146+
147+
createDryHarness({ ...options, deployer: 'bob' });
148+
149+
expect(deployed).toStrictEqual(['bob']);
150+
});
151+
152+
it('should reject a deployer it does not know', () => {
153+
const { deployed, ...options } = recording();
154+
155+
expect(() => createDryHarness({ ...options, deployer: 'carol' })).toThrow(
156+
"concurrency harness: unknown deployer 'carol'",
157+
);
158+
// Nothing deployed: the name is checked before a constructor runs.
159+
expect(deployed).toStrictEqual([]);
160+
});
161+
});

contracts/test-utils/concurrency/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,15 @@ export interface HarnessOptions<P> {
109109
readonly contracts: Readonly<Record<string, ReplayableContract<P>>>;
110110
/** Private state handed to every circuit context. */
111111
readonly privateState: P;
112+
/**
113+
* Which party's instance runs the constructor. Defaults to the first named.
114+
*
115+
* The constructor's witnesses come from this party, so on a module with an
116+
* initializer that reads one, the deployed ledger is seeded with that party's
117+
* secrets. Only the dry backend honours it: the live backend deploys from the
118+
* wallet pool's own deployer, which is not a party at all.
119+
*/
120+
readonly deployer?: string;
112121
/** Constructor arguments, if the contract takes any. */
113122
readonly constructorArgs?: readonly unknown[];
114123
readonly contractAddress?: ContractAddress;

0 commit comments

Comments
 (0)