Bitcoin implementation in typescript

Introduction

I am responsible for the transaction part in a group project about blockchain and bitcoin, and here is my code. Just for archive.

library

Bitcoin is using elliptic and secp256k1

1
2
3
4
import { SHA256 } from 'crypto-js';

var EC = require('elliptic').ec;
var ec = new EC('secp256k1');
1
2
3
4
5
6
7
8
9
10
11
12
export class Signature {
static sign(priKey: string, msg: string): string {
const key = ec.keyFromPrivate(priKey, 'hex');
const signature = key.sign(msg).toDER();
return signature;
}

static verify(pubKey: string, sig: string, msg: string): boolean {
const key = ec.keyFromPublic(pubKey, 'hex');
return key.verify(msg, sig);
}
}

Class in a Transaction

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82

export class RegularTxIn extends TxIn {
txOutId: string;
txOutIndex: number;
signature: string;

constructor(txOutId: string, txOutIndex: number, priKey) {
super();
this.txOutId = txOutId;
this.txOutIndex = txOutIndex;
this.signature = this.createSig(priKey, this.msgHash());
}

createSig(priKey: string, msg: string): string {
return Signature.sign(priKey, msg);
}

msgHash(): string {
return SHA256(SHA256(this.txOutId + this.txOutIndex)).toString();
}
}

export class CoinbaseTxIn extends TxIn {
public blockHeight: number;

constructor(blockHeight: number) {
super();
this.blockHeight = blockHeight;
}
}

export class RegularTxIn extends TxIn {
txOutId: string;
txOutIndex: number;
signature: string;

constructor(txOutId: string, txOutIndex: number, priKey) {
super();
this.txOutId = txOutId;
this.txOutIndex = txOutIndex;
this.signature = this.createSig(priKey, this.msgHash());
}

createSig(priKey: string, msg: string): string {
return Signature.sign(priKey, msg);
}

msgHash(): string {
return SHA256(SHA256(this.txOutId + this.txOutIndex)).toString();
}
}

export class CoinbaseTxIn extends TxIn {
public blockHeight: number;

constructor(blockHeight: number) {
super();
this.blockHeight = blockHeight;
}
}

export class TxOut {
address: string; //public key
amount: number;

constructor(address: string, amount: number) {
this.address = address;
this.amount = amount;
}
}

export class UTXO {
txId: string;
txOut: TxOut;
txIndex: number;

constructor(txId: string, txOut: TxOut, txIndex: number) {
this.txId = txId;
this.txOut = txOut;
this.txIndex = txIndex;
}
}

Transaction Class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
export class Transaction {
type: string;
id: string;
txIns: TxIn[];
txOuts: TxOut[];

constructor(ins: TxIn[], outs: TxOut[]) {
this.txIns = ins;
this.txOuts = outs;
this.id = this.setId();
}

setId(): string {
let txInContent: string;
if (this instanceof RegularTx) {
txInContent = this.txIns
.map((regularTxIn: RegularTxIn) => regularTxIn.txOutId + regularTxIn.txOutIndex)
.reduce((a, b) => a + b, '');
} else {
txInContent = this.txIns
.map((coinbaseTxIn: CoinbaseTxIn) => coinbaseTxIn.blockHeight)
.reduce((a, b) => a + b, '');
}

const txOutContent: string = this.txOuts
.map((txOuts: TxOut) => txOuts.address + txOuts.amount)
.reduce((a, b) => a + b, '');

return SHA256(SHA256(txInContent + txOutContent)).toString();
}

public static createRegularTx(
senderPubKey: string,
senderPriKey: string,
receiverPubKey: string,
receiveAmount: number,
fee: number,
) {
const utxo = this.findUTXO(senderPubKey);
let sumUTXO = 0;
const txIns = [];
const txOuts = [];
let i = 0;
utxo.forEach((val) => {
//the sum of UTXO of a pubkey
sumUTXO += val.amount;
// Create input object for each UTXO, sign the input by user private key
i++;
txIns.push(new RegularTxIn(val.id, i, senderPriKey));
});
const totalAmountToSpend = receiveAmount + fee;
if (sumUTXO < totalAmountToSpend) {
// Not enough money
return; //exception
}
for (let n = 0; n < txIns.length; n++) {
// verify the input by signature
const checker = Signature.verify(utxo[i].address, txIns[i].signature, txIns[i].msgHash());
if (!checker) {
return; //exception
}
}
//Create out put to receiver by PP2K
txOuts.push(new TxOut(receiverPubKey, receiveAmount));
//return change to the sender
const change = sumUTXO - receiveAmount - fee;
if (change > 0) {
txOuts.push(new TxOut(senderPubKey, change));
}
const tx = new Transaction(txIns, txOuts);
// tx.setId()
return tx;
}

public static findUTXO(senderPubKey) {
const allBlock = [];
const allTxOut = [];
const allTxIn = [];
allBlock.forEach((block) => {
const txs = block.txs;
txs.forEach((tx) => {
const txOuts = tx.txOuts;
txOuts.forEach((out) => {
if (out.address == senderPubKey) {
allTxOut.push(out);
}
});
});
});
return [];
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export class RegularTx extends Transaction {
type: 'regular';
txIns: RegularTxIn[];

constructor(ins: RegularTxIn[], outs: TxOut[]) {
super(ins, outs);
}
}

export class CoinBaseTx extends Transaction {
type: 'coinbase';
txIns: CoinbaseTxIn[];

constructor(ins: CoinbaseTxIn, outs: TxOut[]) {
super([ins], outs);
}
}

Create Transaction and UTXO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
public async createTx(
senderPubKey: string,
senderPriKey: string,
receiverPubKey: string,
receiveAmount: number,
fee: number,
) {
const utxos = await this.getUXTO(senderPubKey);
let sumUTXO = 0;

const txIns = utxos.map((tx) => {
sumUTXO += tx.txOut.amount;
return new RegularTxIn(tx.txId, tx.txIndex, senderPriKey);
});

const txOuts = [];

const totalAmountToSpend = receiveAmount + fee;

if (sumUTXO < totalAmountToSpend) {
// Not enough money
throw new Error('Insufficient Balance'); //exception
}

for (let i = 0; i < txIns.length; i++) {
// verify the input by signature
// TODO: check if utxos[i].txOut.address is correct or not
const checker = Signature.verify(utxos[i].txOut.address, txIns[i].signature, txIns[i].msgHash());

if (!checker) {
throw new Error('Invalid txIns'); //exception
}
}

//Create out put to receiver by PP2K
txOuts.push(new TxOut(receiverPubKey, receiveAmount));
//return change to the sender
const change = sumUTXO - receiveAmount - fee;
txOuts.push(new TxOut(senderPubKey, change));

const tx = new RegularTx(txIns, txOuts);
tx.setId();

this.broadcast(tx);
await this.transactionPoolService.addTransaction(tx);

return tx;
}

public async getAllUTXO(): Promise<UTXO[]> {
const outs: UTXO[] = [];
// loop all the blocks
for (let i = 0; i < this.blockService.getBlockHeight(); i++) {
const currentBlockHash = this.blockService.getBlockHash(i);
const currentBlock = await this.blockService.getBlock(currentBlockHash);
const currentTx = currentBlock.data.transactions; // need to convert to list of Transaction

// loop all the transaction
currentTx.forEach((currentTx) => {
const txID = currentTx.id;
const txOuts = currentTx.txOuts;
const txIns = currentTx.txIns;

// Create UTXO object for each TxOutPut, push to UTXO
txOuts.forEach((txOut, i) => {
outs.push(new UTXO(txID, txOut, i)); // create UTXO obj that store tx, txid and index
});

// Remove the spent money
txIns.forEach((tx) => {
if (tx instanceof RegularTxIn) {
const outID = tx.txOutId;
const outIndex = tx.txOutIndex;
const indexOfTx = outs.findIndex((obj) => {
return obj.txId == outID && obj.txIndex == outIndex;
});

if (indexOfTx >= 0) {
outs.splice(indexOfTx, 1);
}
}
});
});
}
return outs;
}

public async getUXTO(pubKey: string): Promise<UTXO[]> {
const allUtxo = await this.getAllUTXO();

return allUtxo.filter((tx) => {
return tx.txOut.address === pubKey;
});
}