Skip to content

Commit 214730f

Browse files
mainnet-patrkalis
andcommitted
Better support for ECDSA signatures in ABI arguments (#319)
Co-authored-by: Rosco Kalis <roscokalis@gmail.com>
1 parent 6e6b3a1 commit 214730f

File tree

4 files changed

+128
-11
lines changed

4 files changed

+128
-11
lines changed

packages/cashscript/src/Argument.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,16 +59,20 @@ export function encodeFunctionArgument(argument: FunctionArgument, typeStr: stri
5959
throw Error(`Value for type ${type} should be a Uint8Array or hex string`);
6060
}
6161

62-
// Redefine SIG as a bytes65 so it is included in the size checks below
63-
// Note that ONLY Schnorr signatures are accepted
64-
if (type === PrimitiveType.SIG && argument.byteLength !== 0) {
65-
type = new BytesType(65);
62+
// Redefine SIG as a bytes65 (Schnorr) or bytes71, bytes72, bytes73 (ECDSA) or bytes0 (for NULLFAIL)
63+
if (type === PrimitiveType.SIG) {
64+
if (![0, 65, 71, 72, 73].includes(argument.byteLength)) {
65+
throw new TypeError(`bytes${argument.byteLength}`, type);
66+
}
67+
type = new BytesType(argument.byteLength);
6668
}
6769

68-
// Redefine DATASIG as a bytes64 so it is included in the size checks below
69-
// Note that ONLY Schnorr signatures are accepted
70-
if (type === PrimitiveType.DATASIG && argument.byteLength !== 0) {
71-
type = new BytesType(64);
70+
// Redefine DATASIG as a bytes64 (Schnorr) or bytes70, bytes71, bytes72 (ECDSA) or bytes0 (for NULLFAIL)
71+
if (type === PrimitiveType.DATASIG) {
72+
if (![0, 64, 70, 71, 72].includes(argument.byteLength)) {
73+
throw new TypeError(`bytes${argument.byteLength}`, type);
74+
}
75+
type = new BytesType(argument.byteLength);
7276
}
7377

7478
// Bounded bytes types require a correctly sized argument

packages/cashscript/test/e2e/HodlVault.test.ts

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,20 @@ import {
55
ElectrumNetworkProvider,
66
Network,
77
TransactionBuilder,
8+
SignatureAlgorithm,
9+
HashType,
810
} from '../../src/index.js';
911
import {
1012
alicePriv,
1113
alicePub,
1214
oracle,
1315
oraclePub,
1416
} from '../fixture/vars.js';
15-
import { gatherUtxos, getTxOutputs } from '../test-util.js';
17+
import { gatherUtxos, getTxOutputs, itOrSkip } from '../test-util.js';
1618
import { FailedRequireError } from '../../src/Errors.js';
1719
import artifact from '../fixture/hodl_vault.artifact.js';
1820
import { randomUtxo } from '../../src/utils.js';
21+
import { placeholder } from '@cashscript/utils';
1922

2023
describe('HodlVault', () => {
2124
const provider = process.env.TESTS_USE_CHIPNET
@@ -95,5 +98,110 @@ describe('HodlVault', () => {
9598
const txOutputs = getTxOutputs(tx);
9699
expect(txOutputs).toEqual(expect.arrayContaining([{ to, amount }]));
97100
});
101+
102+
it('should succeed when price is high enough, ECDSA sig and datasig', async () => {
103+
// given
104+
const message = oracle.createMessage(100000n, 30000n);
105+
const oracleSig = oracle.signMessage(message, SignatureAlgorithm.ECDSA);
106+
const to = hodlVault.address;
107+
const amount = 10000n;
108+
const { utxos, changeAmount } = gatherUtxos(await hodlVault.getUtxos(), { amount, fee: 2000n });
109+
110+
const signatureTemplate = new SignatureTemplate(alicePriv, HashType.SIGHASH_ALL, SignatureAlgorithm.ECDSA);
111+
112+
// when
113+
const tx = await new TransactionBuilder({ provider })
114+
.addInputs(utxos, hodlVault.unlock.spend(signatureTemplate, oracleSig, message))
115+
.addOutput({ to: to, amount: amount })
116+
.addOutput({ to: to, amount: changeAmount })
117+
.setLocktime(100_000)
118+
.send();
119+
120+
// then
121+
const txOutputs = getTxOutputs(tx);
122+
expect(txOutputs).toEqual(expect.arrayContaining([{ to, amount }]));
123+
});
124+
125+
itOrSkip(!Boolean(process.env.TESTS_USE_CHIPNET), 'should succeed with precomputed ECDSA signature', async () => {
126+
// given
127+
const cleanProvider = new MockNetworkProvider();
128+
const contract = new Contract(artifact, [alicePub, oraclePub, 99000n, 30000n], { provider: cleanProvider });
129+
cleanProvider.addUtxo(contract.address, {
130+
satoshis: 100000n,
131+
txid: '11'.repeat(32),
132+
vout: 0,
133+
});
134+
const message = oracle.createMessage(100000n, 30000n);
135+
const oracleSig = oracle.signMessage(message, SignatureAlgorithm.ECDSA);
136+
const to = contract.address;
137+
const amount = 10000n;
138+
const { utxos, changeAmount } = gatherUtxos(await contract.getUtxos(), { amount, fee: 2000n });
139+
const signature = '3045022100aa004a425c0c911594c0333164f990c760991b7f84272f35d98c9c6617d9c53602207dfe4729224d4e61496dff11963982cf79f05d623a6e4004b5f50b7cefa7175241';
140+
141+
// when
142+
const tx = await new TransactionBuilder({ provider: cleanProvider })
143+
.addInputs(utxos, contract.unlock.spend(signature, oracleSig, message))
144+
.addOutput({ to: to, amount: amount })
145+
.addOutput({ to: to, amount: changeAmount })
146+
.setLocktime(100_000)
147+
.send();
148+
149+
// then
150+
const txOutputs = getTxOutputs(tx);
151+
expect(txOutputs).toEqual(expect.arrayContaining([{ to, amount }]));
152+
});
153+
154+
it('should fail to accept wrong signature lengths', async () => {
155+
// given
156+
const message = oracle.createMessage(100000n, 30000n);
157+
const oracleSig = oracle.signMessage(message, SignatureAlgorithm.ECDSA);
158+
const to = hodlVault.address;
159+
const amount = 10000n;
160+
const { utxos, changeAmount } = gatherUtxos(await hodlVault.getUtxos(), { amount, fee: 2000n });
161+
162+
// sig: unlocker should throw when given an improper length
163+
expect(() => hodlVault.unlock.spend(placeholder(100), oracleSig, message)).toThrow("Found type 'bytes100' where type 'sig' was expected");
164+
165+
// sig: unlocker should not throw when given a proper length, but transaction should fail on invalid sig
166+
// Note that this fails with "FailedTransactionEvaluationError" because an invalid signature encoding is NOT a failed
167+
// require statement
168+
await expect(new TransactionBuilder({ provider })
169+
.addInputs(utxos, hodlVault.unlock.spend(placeholder(71), oracleSig, message))
170+
.addOutput({ to: to, amount: amount })
171+
.addOutput({ to: to, amount: changeAmount })
172+
.setLocktime(100_000)
173+
.send()).rejects.toThrow('HodlVault.cash:27 Error in transaction at input 0 in contract HodlVault.cash at line 27');
174+
175+
// sig: unlocker should not throw when given an empty byte array, but transaction should fail on require statement
176+
// Note that this fails with "FailedRequireError" because a zero-length signature IS a failed require statement
177+
await expect(new TransactionBuilder({ provider })
178+
.addInputs(utxos, hodlVault.unlock.spend(placeholder(0), oracleSig, message))
179+
.addOutput({ to: to, amount: amount })
180+
.addOutput({ to: to, amount: changeAmount })
181+
.setLocktime(100_000)
182+
.send()).rejects.toThrow('HodlVault.cash:27 Require statement failed at input 0 in contract HodlVault.cash at line 27');
183+
184+
// datasig: unlocker should throw when given an improper length
185+
const signatureTemplate = new SignatureTemplate(alicePriv, HashType.SIGHASH_ALL, SignatureAlgorithm.ECDSA);
186+
expect(() => hodlVault.unlock.spend(signatureTemplate, placeholder(100), message)).toThrow("Found type 'bytes100' where type 'datasig' was expected");
187+
188+
// datasig: unlocker should not throw when given a proper length, but transaction should fail on invalid sig
189+
// TODO: This somehow fails with "FailedRequireError" instead of "FailedTransactionEvaluationError", check why
190+
await expect(new TransactionBuilder({ provider })
191+
.addInputs(utxos, hodlVault.unlock.spend(signatureTemplate, placeholder(64), message))
192+
.addOutput({ to: to, amount: amount })
193+
.addOutput({ to: to, amount: changeAmount })
194+
.setLocktime(100_000)
195+
.send()).rejects.toThrow('HodlVault.cash:26 Require statement failed at input 0 in contract HodlVault.cash at line 26');
196+
197+
// datasig: unlocker should not throw when given an empty byte array, but transaction should fail on require statement
198+
// Note that this fails with "FailedRequireError" because a zero-length signature IS a failed require statement
199+
await expect(new TransactionBuilder({ provider })
200+
.addInputs(utxos, hodlVault.unlock.spend(signatureTemplate, placeholder(0), message))
201+
.addOutput({ to: to, amount: amount })
202+
.addOutput({ to: to, amount: changeAmount })
203+
.setLocktime(100_000)
204+
.send()).rejects.toThrow('HodlVault.cash:26 Require statement failed at input 0 in contract HodlVault.cash at line 26');
205+
});
98206
});
99207
});

packages/cashscript/test/fixture/PriceOracle.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { padMinimallyEncodedVmNumber, flattenBinArray, secp256k1 } from '@bitauth/libauth';
22
import { encodeInt, sha256 } from '@cashscript/utils';
3+
import { SignatureAlgorithm } from '../../src/index.js';
34

45
export class PriceOracle {
56
constructor(public privateKey: Uint8Array) {}
@@ -12,8 +13,11 @@ export class PriceOracle {
1213
return flattenBinArray([encodedBlockHeight, encodedBchUsdPrice]);
1314
}
1415

15-
signMessage(message: Uint8Array): Uint8Array {
16-
const signature = secp256k1.signMessageHashSchnorr(this.privateKey, sha256(message));
16+
signMessage(message: Uint8Array, signatureAlgorithm: SignatureAlgorithm = SignatureAlgorithm.SCHNORR): Uint8Array {
17+
const signature = signatureAlgorithm === SignatureAlgorithm.SCHNORR ?
18+
secp256k1.signMessageHashSchnorr(this.privateKey, sha256(message)) :
19+
secp256k1.signMessageHashDER(this.privateKey, sha256(message));
20+
1721
if (typeof signature === 'string') throw new Error();
1822
return signature;
1923
}

website/docs/releases/release-notes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ title: Release Notes
1010
- :boom: **BREAKING**: Remove deprecated "old" transaction builder (`contract.functions`).
1111
- :boom: **BREAKING**: No longer seed the MockNetworkProvider with any test UTXOs.
1212
- :sparkles: Add a configurable `vmTarget` option to `MockNetworkProvider`.
13+
- :sparkles: Add support for ECDSA signatures in contract unlockers for `sig` and `datasig` parameters.
1314
- :hammer_and_wrench: Improve libauth template generation.
1415

1516
## v0.11.5

0 commit comments

Comments
 (0)