Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions packages/cashscript/src/Argument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,20 @@ export function encodeFunctionArgument(argument: FunctionArgument, typeStr: stri
throw Error(`Value for type ${type} should be a Uint8Array or hex string`);
}

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

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

// Bounded bytes types require a correctly sized argument
Expand Down
110 changes: 109 additions & 1 deletion packages/cashscript/test/e2e/HodlVault.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@ import {
ElectrumNetworkProvider,
Network,
TransactionBuilder,
SignatureAlgorithm,
HashType,
} from '../../src/index.js';
import {
alicePriv,
alicePub,
oracle,
oraclePub,
} from '../fixture/vars.js';
import { gatherUtxos, getTxOutputs } from '../test-util.js';
import { gatherUtxos, getTxOutputs, itOrSkip } from '../test-util.js';
import { FailedRequireError } from '../../src/Errors.js';
import artifact from '../fixture/hodl_vault.artifact.js';
import { randomUtxo } from '../../src/utils.js';
import { placeholder } from '@cashscript/utils';

describe('HodlVault', () => {
const provider = process.env.TESTS_USE_CHIPNET
Expand Down Expand Up @@ -95,5 +98,110 @@ describe('HodlVault', () => {
const txOutputs = getTxOutputs(tx);
expect(txOutputs).toEqual(expect.arrayContaining([{ to, amount }]));
});

it('should succeed when price is high enough, ECDSA sig and datasig', async () => {
// given
const message = oracle.createMessage(100000n, 30000n);
const oracleSig = oracle.signMessage(message, SignatureAlgorithm.ECDSA);
const to = hodlVault.address;
const amount = 10000n;
const { utxos, changeAmount } = gatherUtxos(await hodlVault.getUtxos(), { amount, fee: 2000n });

const signatureTemplate = new SignatureTemplate(alicePriv, HashType.SIGHASH_ALL, SignatureAlgorithm.ECDSA);

// when
const tx = await new TransactionBuilder({ provider })
.addInputs(utxos, hodlVault.unlock.spend(signatureTemplate, oracleSig, message))
.addOutput({ to: to, amount: amount })
.addOutput({ to: to, amount: changeAmount })
.setLocktime(100_000)
.send();

// then
const txOutputs = getTxOutputs(tx);
expect(txOutputs).toEqual(expect.arrayContaining([{ to, amount }]));
});

itOrSkip(!Boolean(process.env.TESTS_USE_CHIPNET), 'should succeed with precomputed ECDSA signature', async () => {
// given
const cleanProvider = new MockNetworkProvider();
const contract = new Contract(artifact, [alicePub, oraclePub, 99000n, 30000n], { provider: cleanProvider });
cleanProvider.addUtxo(contract.address, {
satoshis: 100000n,
txid: '11'.repeat(32),
vout: 0,
});
const message = oracle.createMessage(100000n, 30000n);
const oracleSig = oracle.signMessage(message, SignatureAlgorithm.ECDSA);
const to = contract.address;
const amount = 10000n;
const { utxos, changeAmount } = gatherUtxos(await contract.getUtxos(), { amount, fee: 2000n });
const signature = '3045022100aa004a425c0c911594c0333164f990c760991b7f84272f35d98c9c6617d9c53602207dfe4729224d4e61496dff11963982cf79f05d623a6e4004b5f50b7cefa7175241';

// when
const tx = await new TransactionBuilder({ provider: cleanProvider })
.addInputs(utxos, contract.unlock.spend(signature, oracleSig, message))
.addOutput({ to: to, amount: amount })
.addOutput({ to: to, amount: changeAmount })
.setLocktime(100_000)
.send();

// then
const txOutputs = getTxOutputs(tx);
expect(txOutputs).toEqual(expect.arrayContaining([{ to, amount }]));
});

it('should fail to accept wrong signature lengths', async () => {
// given
const message = oracle.createMessage(100000n, 30000n);
const oracleSig = oracle.signMessage(message, SignatureAlgorithm.ECDSA);
const to = hodlVault.address;
const amount = 10000n;
const { utxos, changeAmount } = gatherUtxos(await hodlVault.getUtxos(), { amount, fee: 2000n });

// sig: unlocker should throw when given an improper length
expect(() => hodlVault.unlock.spend(placeholder(100), oracleSig, message)).toThrow("Found type 'bytes100' where type 'sig' was expected");

// sig: unlocker should not throw when given a proper length, but transaction should fail on invalid sig
// Note that this fails with "FailedTransactionEvaluationError" because an invalid signature encoding is NOT a failed
// require statement
await expect(new TransactionBuilder({ provider })
.addInputs(utxos, hodlVault.unlock.spend(placeholder(71), oracleSig, message))
.addOutput({ to: to, amount: amount })
.addOutput({ to: to, amount: changeAmount })
.setLocktime(100_000)
.send()).rejects.toThrow('HodlVault.cash:27 Error in transaction at input 0 in contract HodlVault.cash at line 27');

// sig: unlocker should not throw when given an empty byte array, but transaction should fail on require statement
// Note that this fails with "FailedRequireError" because a zero-length signature IS a failed require statement
await expect(new TransactionBuilder({ provider })
.addInputs(utxos, hodlVault.unlock.spend(placeholder(0), oracleSig, message))
.addOutput({ to: to, amount: amount })
.addOutput({ to: to, amount: changeAmount })
.setLocktime(100_000)
.send()).rejects.toThrow('HodlVault.cash:27 Require statement failed at input 0 in contract HodlVault.cash at line 27');

// datasig: unlocker should throw when given an improper length
const signatureTemplate = new SignatureTemplate(alicePriv, HashType.SIGHASH_ALL, SignatureAlgorithm.ECDSA);
expect(() => hodlVault.unlock.spend(signatureTemplate, placeholder(100), message)).toThrow("Found type 'bytes100' where type 'datasig' was expected");

// datasig: unlocker should not throw when given a proper length, but transaction should fail on invalid sig
// TODO: This somehow fails with "FailedRequireError" instead of "FailedTransactionEvaluationError", check why
await expect(new TransactionBuilder({ provider })
.addInputs(utxos, hodlVault.unlock.spend(signatureTemplate, placeholder(64), message))
.addOutput({ to: to, amount: amount })
.addOutput({ to: to, amount: changeAmount })
.setLocktime(100_000)
.send()).rejects.toThrow('HodlVault.cash:26 Require statement failed at input 0 in contract HodlVault.cash at line 26');

// datasig: unlocker should not throw when given an empty byte array, but transaction should fail on require statement
// Note that this fails with "FailedRequireError" because a zero-length signature IS a failed require statement
await expect(new TransactionBuilder({ provider })
.addInputs(utxos, hodlVault.unlock.spend(signatureTemplate, placeholder(0), message))
.addOutput({ to: to, amount: amount })
.addOutput({ to: to, amount: changeAmount })
.setLocktime(100_000)
.send()).rejects.toThrow('HodlVault.cash:26 Require statement failed at input 0 in contract HodlVault.cash at line 26');
});
});
});
8 changes: 6 additions & 2 deletions packages/cashscript/test/fixture/PriceOracle.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { padMinimallyEncodedVmNumber, flattenBinArray, secp256k1 } from '@bitauth/libauth';
import { encodeInt, sha256 } from '@cashscript/utils';
import { SignatureAlgorithm } from '../../src/index.js';

export class PriceOracle {
constructor(public privateKey: Uint8Array) {}
Expand All @@ -12,8 +13,11 @@ export class PriceOracle {
return flattenBinArray([encodedBlockHeight, encodedBchUsdPrice]);
}

signMessage(message: Uint8Array): Uint8Array {
const signature = secp256k1.signMessageHashSchnorr(this.privateKey, sha256(message));
signMessage(message: Uint8Array, signatureAlgorithm: SignatureAlgorithm = SignatureAlgorithm.SCHNORR): Uint8Array {
const signature = signatureAlgorithm === SignatureAlgorithm.SCHNORR ?
secp256k1.signMessageHashSchnorr(this.privateKey, sha256(message)) :
secp256k1.signMessageHashDER(this.privateKey, sha256(message));

if (typeof signature === 'string') throw new Error();
return signature;
}
Expand Down
1 change: 1 addition & 0 deletions website/docs/releases/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ title: Release Notes
- :boom: **BREAKING**: Remove deprecated "old" transaction builder (`contract.functions`).
- :boom: **BREAKING**: No longer seed the MockNetworkProvider with any test UTXOs.
- :sparkles: Add a configurable `vmTarget` option to `MockNetworkProvider`.
- :sparkles: Add support for ECDSA signatures in contract unlockers for `sig` and `datasig` parameters.
- :hammer_and_wrench: Improve libauth template generation.

## v0.11.5
Expand Down
Loading