Skip to content

Commit bac5967

Browse files
committed
Enforce bigint for all script numbers and bch amounts
- Use `bigint` rather than `number` for all instances of "script numbers" (e.g. function arguments) and satoshi amounts` - Replace `contract.getRedeemScriptHex()` with `contract.bytecode` - Update UTXO selection to use bigint (multiplied by 1e6 for "decimal points")
1 parent 704ed2b commit bac5967

37 files changed

+195
-190
lines changed

packages/cashscript/src/Argument.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
import { TypeError } from './Errors.js';
1111
import SignatureTemplate from './SignatureTemplate.js';
1212

13-
export type Argument = number | bigint | boolean | string | Uint8Array | SignatureTemplate;
13+
export type Argument = bigint | boolean | string | Uint8Array | SignatureTemplate;
1414

1515
export function encodeArgument(
1616
argument: Argument,
@@ -26,7 +26,7 @@ export function encodeArgument(
2626
}
2727

2828
if (type === PrimitiveType.INT) {
29-
if (typeof argument !== 'number' && typeof argument !== 'bigint') {
29+
if (typeof argument !== 'bigint') {
3030
throw new TypeError(typeof argument, type);
3131
}
3232
return encodeInt(argument);

packages/cashscript/src/Contract.ts

+4-6
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { ElectrumNetworkProvider } from './network/index.js';
2222
export class Contract {
2323
name: string;
2424
address: string;
25+
bytecode: string;
2526
bytesize: number;
2627
opcount: number;
2728

@@ -74,23 +75,20 @@ export class Contract {
7475

7576
this.name = artifact.contractName;
7677
this.address = scriptToAddress(this.redeemScript, this.provider.network);
78+
this.bytecode = binToHex(scriptToBytecode(this.redeemScript));
7779
this.bytesize = calculateBytesize(this.redeemScript);
7880
this.opcount = countOpcodes(this.redeemScript);
7981
}
8082

81-
async getBalance(): Promise<number> {
83+
async getBalance(): Promise<bigint> {
8284
const utxos = await this.getUtxos();
83-
return utxos.reduce((acc, utxo) => acc + utxo.satoshis, 0);
85+
return utxos.reduce((acc, utxo) => acc + utxo.satoshis, BigInt(0));
8486
}
8587

8688
async getUtxos(): Promise<Utxo[]> {
8789
return this.provider.getUtxos(this.address);
8890
}
8991

90-
getRedeemScriptHex(): string {
91-
return binToHex(scriptToBytecode(this.redeemScript));
92-
}
93-
9492
private createFunction(abiFunction: AbiFunction, selector?: number): ContractFunction {
9593
return (...args: Argument[]) => {
9694
if (abiFunction.inputs.length !== args.length) {

packages/cashscript/src/Errors.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export class TypeError extends Error {
88
}
99

1010
export class OutputSatoshisTooSmallError extends Error {
11-
constructor(satoshis: number) {
11+
constructor(satoshis: bigint) {
1212
super(`Tried to add an output with ${satoshis} satoshis, which is less than the DUST limit (${DUST_LIMIT})`);
1313
}
1414
}

packages/cashscript/src/Transaction.ts

+50-24
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
addressToLockScript,
3737
createSighashPreimage,
3838
validateRecipient,
39+
utxoComparator,
3940
} from './utils.js';
4041
import { P2SH_OUTPUT_SIZE, DUST_LIMIT } from './constants.js';
4142
import NetworkProvider from './network/NetworkProvider.js';
@@ -49,9 +50,9 @@ export class Transaction {
4950

5051
private sequence = 0xfffffffe;
5152
private locktime: number;
52-
private hardcodedFee: number;
53-
private feePerByte = 1.0;
54-
private minChange = DUST_LIMIT;
53+
private feePerByte: number = 1.0;
54+
private hardcodedFee: bigint;
55+
private minChange: bigint = DUST_LIMIT;
5556

5657
constructor(
5758
private address: string,
@@ -90,11 +91,11 @@ export class Transaction {
9091
return this;
9192
}
9293

93-
to(to: string, amount: number): this;
94+
to(to: string, amount: bigint): this;
9495
to(outputs: Recipient[]): this;
9596

96-
to(toOrOutputs: string | Recipient[], amount?: number): this {
97-
if (typeof toOrOutputs === 'string' && typeof amount === 'number') {
97+
to(toOrOutputs: string | Recipient[], amount?: bigint): this {
98+
if (typeof toOrOutputs === 'string' && typeof amount === 'bigint') {
9899
return this.to([{ to: toOrOutputs, amount }]);
99100
}
100101

@@ -122,7 +123,7 @@ export class Transaction {
122123
return this;
123124
}
124125

125-
withHardcodedFee(hardcodedFee: number): this {
126+
withHardcodedFee(hardcodedFee: bigint): this {
126127
this.hardcodedFee = hardcodedFee;
127128
return this;
128129
}
@@ -132,13 +133,13 @@ export class Transaction {
132133
return this;
133134
}
134135

135-
withMinChange(minChange: number): this {
136+
withMinChange(minChange: bigint): this {
136137
this.minChange = minChange;
137138
return this;
138139
}
139140

140141
withoutChange(): this {
141-
return this.withMinChange(Number.MAX_VALUE);
142+
return this.withMinChange(BigInt(Number.MAX_VALUE));
142143
}
143144

144145
async build(): Promise<string> {
@@ -160,7 +161,7 @@ export class Transaction {
160161
? addressToLockScript(output.to)
161162
: output.to;
162163

163-
const satoshis = bigIntToBinUint64LE(BigInt(output.amount));
164+
const satoshis = bigIntToBinUint64LE(output.amount);
164165

165166
return { lockingBytecode, satoshis };
166167
});
@@ -294,35 +295,38 @@ export class Transaction {
294295
// Add one extra byte per input to over-estimate tx-in count
295296
const inputSize = getInputSize(placeholderScript) + 1;
296297

298+
// Note that we use the addPrecision function to add "decimal points" to BigInt numbers
299+
297300
// Calculate amount to send and base fee (excluding additional fees per UTXO)
298-
const amount = this.outputs.reduce((acc, output) => acc + output.amount, 0);
299-
let fee = this.hardcodedFee ?? getTxSizeWithoutInputs(this.outputs) * this.feePerByte;
301+
let amount = addPrecision(this.outputs.reduce((acc, output) => acc + output.amount, BigInt(0)));
302+
let fee = addPrecision(this.hardcodedFee ?? getTxSizeWithoutInputs(this.outputs) * this.feePerByte);
300303

301304
// Select and gather UTXOs and calculate fees and available funds
302-
let satsAvailable = 0;
305+
let satsAvailable = BigInt(0);
303306
if (this.inputs.length > 0) {
304-
// If inputs are already defined, the user provided the UTXOs
305-
// and we perform no further UTXO selection
306-
if (!this.hardcodedFee) fee += this.inputs.length * inputSize * this.feePerByte;
307-
satsAvailable = this.inputs.reduce((acc, input) => acc + input.satoshis, 0);
307+
// If inputs are already defined, the user provided the UTXOs and we perform no further UTXO selection
308+
if (!this.hardcodedFee) fee += addPrecision(this.inputs.length * inputSize * this.feePerByte);
309+
satsAvailable = addPrecision(this.inputs.reduce((acc, input) => acc + input.satoshis, BigInt(0)));
308310
} else {
309311
// If inputs are not defined yet, we retrieve the contract's UTXOs and perform selection
310312
const utxos = await this.provider.getUtxos(this.address);
311313

312314
// We sort the UTXOs mainly so there is consistent behaviour between network providers
313315
// even if they report UTXOs in a different order
314-
utxos.sort((a, b) => b.satoshis - a.satoshis);
316+
utxos.sort(utxoComparator).reverse();
315317

316318
for (const utxo of utxos) {
317319
this.inputs.push(utxo);
318-
satsAvailable += utxo.satoshis;
319-
if (!this.hardcodedFee) fee += inputSize * this.feePerByte;
320+
satsAvailable += addPrecision(utxo.satoshis);
321+
if (!this.hardcodedFee) fee += addPrecision(inputSize * this.feePerByte);
320322
if (satsAvailable > amount + fee) break;
321323
}
322324
}
323325

324-
// Fee per byte can be a decimal number, but we need the total fee to be an integer
325-
fee = Math.ceil(fee);
326+
// Remove "decimal points" from BigInt numbers (rounding up for fee, down for others)
327+
satsAvailable = removePrecisionFloor(satsAvailable);
328+
amount = removePrecisionFloor(amount);
329+
fee = removePrecisionCeil(fee);
326330

327331
// Calculate change and check available funds
328332
let change = satsAvailable - amount - fee;
@@ -331,9 +335,9 @@ export class Transaction {
331335
throw new Error(`Insufficient funds: available (${satsAvailable}) < needed (${amount + fee}).`);
332336
}
333337

334-
// Account for the fee of a change output
338+
// Account for the fee of adding a change output
335339
if (!this.hardcodedFee) {
336-
change -= P2SH_OUTPUT_SIZE;
340+
change -= BigInt(P2SH_OUTPUT_SIZE * this.feePerByte);
337341
}
338342

339343
// Add a change output if applicable
@@ -342,3 +346,25 @@ export class Transaction {
342346
}
343347
}
344348
}
349+
350+
// Note: the below is a very simple implementation of a "decimal point" system for BigInt numbers
351+
// It is safe to use for UTXO fee calculations due to its low numbers, but should not be used for other purposes
352+
// Also note that multiplication and division between two "decimal" bigints is not supported
353+
354+
// High precision may not work with some 'number' inputs, so we set the default to 6 "decimal places"
355+
const addPrecision = (amount: number | bigint, precision: number = 6): bigint => {
356+
if (typeof amount === 'number') {
357+
return BigInt(Math.ceil(amount * 10 ** precision));
358+
}
359+
360+
return amount * BigInt(10 ** precision);
361+
};
362+
363+
const removePrecisionFloor = (amount: bigint, precision: number = 6): bigint => (
364+
amount / (BigInt(10) ** BigInt(precision))
365+
);
366+
367+
const removePrecisionCeil = (amount: bigint, precision: number = 6): bigint => {
368+
const multiplier = BigInt(10) ** BigInt(precision);
369+
return (amount + multiplier - BigInt(1)) / multiplier;
370+
};

packages/cashscript/src/constants.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export const DUST_LIMIT = 546;
1+
export const DUST_LIMIT = BigInt(546);
22
export const P2PKH_OUTPUT_SIZE = 34;
33
export const P2SH_OUTPUT_SIZE = 32;
44
export const VERSION_SIZE = 4;

packages/cashscript/src/index.ts

-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ export {
1616
export * from './Errors.js';
1717
export {
1818
NetworkProvider,
19-
BitboxNetworkProvider,
2019
BitcoinRpcNetworkProvider,
2120
ElectrumNetworkProvider,
2221
FullStackNetworkProvider,

packages/cashscript/src/interfaces.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type SignatureTemplate from './SignatureTemplate.js';
44
export interface Utxo {
55
txid: string;
66
vout: number;
7-
satoshis: number;
7+
satoshis: bigint;
88
}
99

1010
export interface SignableUtxo extends Utxo {
@@ -17,12 +17,12 @@ export function isSignableUtxo(utxo: Utxo): utxo is SignableUtxo {
1717

1818
export interface Recipient {
1919
to: string;
20-
amount: number;
20+
amount: bigint;
2121
}
2222

2323
export interface Output {
2424
to: string | Uint8Array;
25-
amount: number;
25+
amount: bigint;
2626
}
2727

2828
export enum SignatureAlgorithm {

packages/cashscript/src/network/BitboxNetworkProvider.ts

-39
This file was deleted.

packages/cashscript/src/network/BitcoinRpcNetworkProvider.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export default class BitcoinRpcNetworkProvider implements NetworkProvider {
2020
const utxos = result.map((utxo) => ({
2121
txid: utxo.txid,
2222
vout: utxo.vout,
23-
satoshis: utxo.amount * 1e8,
23+
satoshis: BigInt(utxo.amount * 1e8),
2424
}));
2525

2626
return utxos;

packages/cashscript/src/network/ElectrumNetworkProvider.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -61,16 +61,14 @@ export default class ElectrumNetworkProvider implements NetworkProvider {
6161
const utxos = result.map((utxo) => ({
6262
txid: utxo.tx_hash,
6363
vout: utxo.tx_pos,
64-
satoshis: utxo.value,
65-
height: utxo.height,
64+
satoshis: BigInt(utxo.value),
6665
}));
6766

6867
return utxos;
6968
}
7069

7170
async getBlockHeight(): Promise<number> {
7271
const { height } = await this.performRequest('blockchain.headers.subscribe') as BlockHeader;
73-
7472
return height;
7573
}
7674

packages/cashscript/src/network/FullStackNetworkProvider.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ export default class FullStackNetworkProvider implements NetworkProvider {
2121
const utxos = (result.utxos ?? []).map((utxo: ElectrumUtxo) => ({
2222
txid: utxo.tx_hash,
2323
vout: utxo.tx_pos,
24-
satoshis: utxo.value,
25-
height: utxo.height,
24+
satoshis: BigInt(utxo.value),
2625
}));
2726

2827
return utxos;
@@ -33,7 +32,7 @@ export default class FullStackNetworkProvider implements NetworkProvider {
3332
}
3433

3534
async getRawTransaction(txid: string): Promise<string> {
36-
return this.bchjs.RawTransactions.getRawTransaction(txid) as Promise<string>;
35+
return this.bchjs.RawTransactions.getRawTransaction(txid);
3736
}
3837

3938
async sendRawTransaction(txHex: string): Promise<string> {
-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
export { default as NetworkProvider } from './NetworkProvider.js';
2-
export { default as BitboxNetworkProvider } from './BitboxNetworkProvider.js';
32
export { default as BitcoinRpcNetworkProvider } from './BitcoinRpcNetworkProvider.js';
43
export { default as ElectrumNetworkProvider } from './ElectrumNetworkProvider.js';
54
export { default as FullStackNetworkProvider } from './FullStackNetworkProvider.js';

0 commit comments

Comments
 (0)