Skip to content

Commit 17a8cd2

Browse files
committed
Add tests for sibling introspection
1 parent 7eb032e commit 17a8cd2

File tree

5 files changed

+261
-6
lines changed

5 files changed

+261
-6
lines changed

packages/cashscript/test/e2e/MultiContract.test.ts

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import {
77
TransactionBuilder,
88
} from '../../src/index.js';
99
import {
10+
alicePkh,
11+
alicePriv,
12+
alicePub,
1013
bobAddress,
1114
bobPkh,
1215
bobPriv,
@@ -16,16 +19,18 @@ import {
1619
carolPub,
1720
} from '../fixture/vars.js';
1821
import { Network } from '../../src/interfaces.js';
19-
import { randomUtxo } from '../../src/utils.js';
22+
import { addressToLockScript, randomUtxo } from '../../src/utils.js';
2023
import p2pkhArtifact from '../fixture/p2pkh.artifact.js';
2124
import twtArtifact from '../fixture/transfer_with_timeout.artifact.js';
2225
import { getTxOutputs } from '../test-util.js';
26+
import SiblingIntrospectionArtifact from '../fixture/SiblingIntrospection.artifact.js';
2327

2428
describe('Multi Contract', () => {
2529
const provider = process.env.TESTS_USE_MOCKNET
2630
? new MockNetworkProvider()
2731
: new ElectrumNetworkProvider(Network.CHIPNET);
2832

33+
const aliceSignatureTemplate = new SignatureTemplate(alicePriv);
2934
const bobSignatureTemplate = new SignatureTemplate(bobPriv);
3035
const carolSignatureTemplate = new SignatureTemplate(carolPriv);
3136

@@ -200,8 +205,55 @@ describe('Multi Contract', () => {
200205
const txOutputs = getTxOutputs(tx);
201206
expect(txOutputs).toEqual(expect.arrayContaining(outputs));
202207
});
208+
});
209+
210+
describe('Sibling introspection', () => {
211+
const correctContract = new Contract(p2pkhArtifact, [alicePkh], { provider });
212+
const incorrectContract = new Contract(p2pkhArtifact, [bobPkh], { provider });
213+
214+
const correctLockingBytecode = addressToLockScript(correctContract.address);
215+
const siblingIntrospectionContract = new Contract(SiblingIntrospectionArtifact, [correctLockingBytecode], { provider });
203216

217+
const correctContractUtxo = randomUtxo();
218+
const incorrectContractUtxo = randomUtxo();
219+
const siblingIntrospectionUtxo = randomUtxo();
220+
221+
(provider as any).addUtxo?.(correctContract.address, correctContractUtxo);
222+
(provider as any).addUtxo?.(incorrectContract.address, incorrectContractUtxo);
223+
(provider as any).addUtxo?.(siblingIntrospectionContract.address, siblingIntrospectionUtxo);
224+
225+
it('should succeed when introspecting correct sibling UTXOs', async () => {
226+
// given
227+
const tx = await new TransactionBuilder({ provider })
228+
.addInput(siblingIntrospectionUtxo, siblingIntrospectionContract.unlock.spend())
229+
.addInput(correctContractUtxo, correctContract.unlock.spend(alicePub, aliceSignatureTemplate))
230+
.addOutput({ to: siblingIntrospectionContract.address, amount: siblingIntrospectionUtxo.satoshis })
231+
.addOutput({ to: correctContract.address, amount: correctContractUtxo.satoshis - 2000n })
232+
.send();
233+
234+
// then
235+
const txOutputs = getTxOutputs(tx);
236+
expect(txOutputs).toEqual(
237+
expect.arrayContaining([
238+
{ to: siblingIntrospectionContract.address, amount: siblingIntrospectionUtxo.satoshis },
239+
{ to: correctContract.address, amount: correctContractUtxo.satoshis - 2000n },
240+
]),
241+
);
242+
});
243+
244+
it('should fail when introspecting incorrect sibling UTXOs', async () => {
245+
// given
246+
const txPromise = new TransactionBuilder({ provider })
247+
.addInput(siblingIntrospectionUtxo, siblingIntrospectionContract.unlock.spend())
248+
.addInput(incorrectContractUtxo, incorrectContract.unlock.spend(bobPub, bobSignatureTemplate))
249+
.addOutput({ to: siblingIntrospectionContract.address, amount: siblingIntrospectionUtxo.satoshis })
250+
.addOutput({ to: incorrectContract.address, amount: incorrectContractUtxo.satoshis - 2000n })
251+
.send();
252+
253+
// then
254+
await expect(txPromise).rejects.toThrow(FailedRequireError);
255+
await expect(txPromise).rejects.toThrow('SiblingIntrospection.cash:7 Require statement failed at input 0 in contract SiblingIntrospection.cash at line 7 with the following message: input bytecode should match.');
256+
await expect(txPromise).rejects.toThrow("Failing statement: require(inputBytecode == expectedLockingBytecode, 'input bytecode should match')");
257+
});
204258
});
205259
});
206-
207-
// TODO: Add test that introspects "sibling" UTXOs
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
export default {
2+
contractName: 'SiblingIntrospection',
3+
constructorInputs: [
4+
{
5+
name: 'expectedLockingBytecode',
6+
type: 'bytes',
7+
},
8+
],
9+
abi: [
10+
{
11+
name: 'spend',
12+
inputs: [],
13+
},
14+
],
15+
bytecode: 'OP_INPUTINDEX OP_0 OP_NUMEQUALVERIFY OP_1 OP_UTXOBYTECODE OP_OVER OP_EQUALVERIFY OP_1 OP_OUTPUTBYTECODE OP_EQUAL',
16+
source: 'contract SiblingIntrospection(bytes expectedLockingBytecode) {\n function spend() {\n require(this.activeInputIndex == 0);\n\n bytes inputBytecode = tx.inputs[1].lockingBytecode;\n console.log(\'inputBytecode:\', inputBytecode);\n require(inputBytecode == expectedLockingBytecode, \'input bytecode should match\');\n\n bytes outputBytecode = tx.outputs[1].lockingBytecode;\n console.log(\'outputBytecode:\', outputBytecode);\n require(outputBytecode == expectedLockingBytecode, \'output bytecode should match\');\n }\n}\n',
17+
debug: {
18+
bytecode: 'c0009d51c7788851cd87',
19+
sourceMap: '3:16:3:37;:41::42;:8::44:1;5:40:5:41:0;:30::58:1;7:33:7:56:0;:8::89:1;9:42:9:43:0;:31::60:1;11:16:11:57',
20+
logs: [
21+
{
22+
ip: 6,
23+
line: 6,
24+
data: [
25+
'inputBytecode:',
26+
{
27+
stackIndex: 0,
28+
type: 'bytes',
29+
ip: 6,
30+
},
31+
],
32+
},
33+
{
34+
ip: 10,
35+
line: 10,
36+
data: [
37+
'outputBytecode:',
38+
{
39+
stackIndex: 0,
40+
type: 'bytes',
41+
ip: 10,
42+
},
43+
],
44+
},
45+
],
46+
requires: [
47+
{
48+
ip: 3,
49+
line: 3,
50+
},
51+
{
52+
ip: 7,
53+
line: 7,
54+
message: 'input bytecode should match',
55+
},
56+
{
57+
ip: 11,
58+
line: 11,
59+
message: 'output bytecode should match',
60+
},
61+
],
62+
},
63+
compiler: {
64+
name: 'cashc',
65+
version: '0.11.0-next.4',
66+
},
67+
updatedAt: '2025-06-10T10:02:43.222Z',
68+
} as const;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
contract SiblingIntrospection(bytes expectedLockingBytecode) {
2+
function spend() {
3+
require(this.activeInputIndex == 0);
4+
5+
bytes inputBytecode = tx.inputs[1].lockingBytecode;
6+
console.log("inputBytecode:", inputBytecode);
7+
require(inputBytecode == expectedLockingBytecode, "input bytecode should match");
8+
9+
bytes outputBytecode = tx.outputs[1].lockingBytecode;
10+
console.log("outputBytecode:", outputBytecode);
11+
require(outputBytecode == expectedLockingBytecode, "output bytecode should match");
12+
}
13+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
{
2+
"contractName": "SiblingIntrospection",
3+
"constructorInputs": [
4+
{
5+
"name": "expectedLockingBytecode",
6+
"type": "bytes"
7+
}
8+
],
9+
"abi": [
10+
{
11+
"name": "spend",
12+
"inputs": []
13+
}
14+
],
15+
"bytecode": "OP_INPUTINDEX OP_0 OP_NUMEQUALVERIFY OP_1 OP_UTXOBYTECODE OP_OVER OP_EQUALVERIFY OP_1 OP_OUTPUTBYTECODE OP_EQUAL",
16+
"source": "contract SiblingIntrospection(bytes expectedLockingBytecode) {\n function spend() {\n require(this.activeInputIndex == 0);\n\n bytes inputBytecode = tx.inputs[1].lockingBytecode;\n console.log(\"inputBytecode:\", inputBytecode);\n require(inputBytecode == expectedLockingBytecode, \"input bytecode should match\");\n\n bytes outputBytecode = tx.outputs[1].lockingBytecode;\n console.log(\"outputBytecode:\", outputBytecode);\n require(outputBytecode == expectedLockingBytecode, \"output bytecode should match\");\n }\n}\n",
17+
"debug": {
18+
"bytecode": "c0009d51c7788851cd87",
19+
"sourceMap": "3:16:3:37;:41::42;:8::44:1;5:40:5:41:0;:30::58:1;7:33:7:56:0;:8::89:1;9:42:9:43:0;:31::60:1;11:16:11:57",
20+
"logs": [
21+
{
22+
"ip": 6,
23+
"line": 6,
24+
"data": [
25+
"inputBytecode:",
26+
{
27+
"stackIndex": 0,
28+
"type": "bytes",
29+
"ip": 6
30+
}
31+
]
32+
},
33+
{
34+
"ip": 10,
35+
"line": 10,
36+
"data": [
37+
"outputBytecode:",
38+
{
39+
"stackIndex": 0,
40+
"type": "bytes",
41+
"ip": 10
42+
}
43+
]
44+
}
45+
],
46+
"requires": [
47+
{
48+
"ip": 3,
49+
"line": 3
50+
},
51+
{
52+
"ip": 7,
53+
"line": 7,
54+
"message": "input bytecode should match"
55+
},
56+
{
57+
"ip": 11,
58+
"line": 11,
59+
"message": "output bytecode should match"
60+
}
61+
]
62+
},
63+
"compiler": {
64+
"name": "cashc",
65+
"version": "0.11.0-next.4"
66+
},
67+
"updatedAt": "2025-06-10T10:02:42.898Z"
68+
}

packages/cashscript/test/multi-contract-debugging.test.ts

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import {
44
randomUtxo,
55
SignatureTemplate,
66
TransactionBuilder,
7-
} from './../src/index.js';
7+
} from '../src/index.js';
88
import {
9+
alicePkh,
10+
alicePriv,
911
alicePub,
1012
bobAddress,
1113
bobPkh,
@@ -16,6 +18,8 @@ import p2pkhArtifact from './fixture/p2pkh.artifact.js';
1618
import bigintArtifact from './fixture/bigint.artifact.js';
1719
import '../src/test/JestExtensions.js';
1820
import { ARTIFACT_FUNCTION_NAME_COLLISION, ARTIFACT_NAME_COLLISION, ARTIFACT_CONTRACT_NAME_COLLISION, ARTIFACT_SAME_NAME_DIFFERENT_PATH } from './fixture/debugging/multicontract_debugging_contracts.js';
21+
import { addressToLockScript } from '../src/utils.js';
22+
import SiblingIntrospectionArtifact from './fixture/SiblingIntrospection.artifact.js';
1923

2024
const bobSignatureTemplate = new SignatureTemplate(bobPriv);
2125

@@ -169,8 +173,58 @@ describe('Multi-Contract-Debugging tests', () => {
169173
await expect(transaction).toFailRequireWith('BigInt.cash');
170174
});
171175

172-
it.todo('should fail with correct error message when introspected output bytecode of a different contract does not match');
173-
it.todo('should fail with correct error message when introspected input bytecode of a different contract does not match');
176+
describe('Sibling introspection', () => {
177+
const correctContract = new Contract(p2pkhArtifact, [alicePkh], { provider });
178+
const incorrectContract = new Contract(p2pkhArtifact, [bobPkh], { provider });
179+
180+
const correctLockingBytecode = addressToLockScript(correctContract.address);
181+
const siblingIntrospectionContract = new Contract(
182+
SiblingIntrospectionArtifact,
183+
[correctLockingBytecode],
184+
{ provider },
185+
);
186+
187+
const correctContractUtxo = randomUtxo();
188+
const incorrectContractUtxo = randomUtxo();
189+
const siblingIntrospectionUtxo = randomUtxo();
190+
191+
(provider as any).addUtxo?.(correctContract.address, correctContractUtxo);
192+
(provider as any).addUtxo?.(incorrectContract.address, incorrectContractUtxo);
193+
(provider as any).addUtxo?.(siblingIntrospectionContract.address, siblingIntrospectionUtxo);
194+
195+
it('should not throw fail any require statements when introspecting correct sibling UTXOs', () => {
196+
const tx = new TransactionBuilder({ provider })
197+
.addInput(siblingIntrospectionUtxo, siblingIntrospectionContract.unlock.spend())
198+
.addInput(correctContractUtxo, correctContract.unlock.spend(alicePub, new SignatureTemplate(alicePriv)))
199+
.addOutput({ to: siblingIntrospectionContract.address, amount: siblingIntrospectionUtxo.satoshis })
200+
.addOutput({ to: correctContract.address, amount: correctContractUtxo.satoshis - 2000n });
201+
202+
expect(tx).not.toFailRequire();
203+
});
204+
205+
it('should fail with correct error message when introspected input bytecode of a sibling UTXO does not match', () => {
206+
const tx = new TransactionBuilder({ provider })
207+
.addInput(siblingIntrospectionUtxo, siblingIntrospectionContract.unlock.spend())
208+
.addInput(incorrectContractUtxo, incorrectContract.unlock.spend(bobPub, bobSignatureTemplate))
209+
.addOutput({ to: siblingIntrospectionContract.address, amount: siblingIntrospectionUtxo.satoshis })
210+
.addOutput({ to: correctContract.address, amount: incorrectContractUtxo.satoshis - 2000n });
211+
212+
expect(tx).toFailRequireWith('SiblingIntrospection.cash:7 Require statement failed at input 0 in contract SiblingIntrospection.cash at line 7 with the following message: input bytecode should match.');
213+
expect(tx).toFailRequireWith('Failing statement: require(inputBytecode == expectedLockingBytecode, \'input bytecode should match\')');
214+
});
215+
216+
it('should fail with correct error message when introspected output bytecode of a sibling UTXO does not match', () => {
217+
const tx = new TransactionBuilder({ provider })
218+
.addInput(siblingIntrospectionUtxo, siblingIntrospectionContract.unlock.spend())
219+
.addInput(correctContractUtxo, correctContract.unlock.spend(alicePub, new SignatureTemplate(alicePriv)))
220+
.addOutput({ to: siblingIntrospectionContract.address, amount: siblingIntrospectionUtxo.satoshis })
221+
.addOutput({ to: incorrectContract.address, amount: correctContractUtxo.satoshis - 2000n });
222+
223+
expect(tx).toFailRequireWith('SiblingIntrospection.cash:11 Require statement failed at input 0 in contract SiblingIntrospection.cash at line 11 with the following message: output bytecode should match.');
224+
expect(tx).toFailRequireWith('Failing statement: require(outputBytecode == expectedLockingBytecode, "output bytecode should match")');
225+
});
226+
});
227+
174228
it.todo('should still work with duplicate custom require messages across contracts');
175229

176230
it('should still work if contract or function parameters have the same name across contracts', () => {

0 commit comments

Comments
 (0)