Blockchain 14 min read

Understanding Bitcoin’s UTXO Model and Building It with Node.js

This article explains Bitcoin’s innovative UTXO (unspent transaction output) model, why traditional account‑based systems are unsuitable, and provides a complete Node.js implementation covering transaction inputs, outputs, coinbase and regular transfers, complete with code examples and execution details.

Weidian Tech Team
Weidian Tech Team
Weidian Tech Team
Understanding Bitcoin’s UTXO Model and Building It with Node.js

UTXO Model in Bitcoin

Bitcoin replaces the traditional account‑based transaction model with an unspent transaction output (UTXO) model to avoid storage pressure, privacy leaks, and the inability to create relational tables on a blockchain. Each transaction references previous unspent outputs (UTXOs) via inputs and creates new outputs that lock BTC amounts with scripts.

In the UTXO model, a transaction input points to a previous transaction output that can be spent; it contains the previous transaction ID, the output index, and an unlocking script that proves ownership. A transaction output stores the BTC amount and a locking script (typically a public‑key hash) that can only be unlocked with the corresponding private key.

UTXO Implementation in Node.js

Transaction Input

<code>export class Input {
    private txId: string;
    private outputIndex: number;
    private unlockScript: string;

    public get $txId(): string { return this.txId; }
    public set $txId(value: string) { this.txId = value; }

    public get $outputIndex(): number { return this.outputIndex; }
    public set $outputIndex(value: number) { this.outputIndex = value; }

    public get $unlockScript(): string { return this.unlockScript; }
    public set $unlockScript(value: string) { this.unlockScript = value; }

    constructor(txId: string, index: number, unlockScript: string) {
        this.txId = txId;
        this.outputIndex = index;
        this.unlockScript = unlockScript;
    }

    public static createInputsFromUnserialize(objs: Array<Input>) {
        let ins = [];
        objs.forEach(obj => {
            ins.push(new Input(obj.txId, obj.outputIndex, obj.unlockScript));
        });
        return ins;
    }

    canUnlock(privateKey: string): boolean {
        if (privateKey == this.unlockScript) {
            return true;
        } else {
            return false;
        }
    }
}
</code>

The

txId

identifies the transaction containing the UTXO,

outputIndex

selects the specific output, and

unlockScript

is a simplified script that checks the provided private key.

Transaction Output

<code>import * as rsaConfig from '../../rsa.json';
export class Output {
    private value: number;
    private lockScript: string;
    private txId: string;
    private index: number;

    public get $index(): number { return this.index; }
    public set $index(value: number) { this.index = value; }

    public get $txId(): string { return this.txId; }
    public set $txId(value: string) { this.txId = value; }

    public get $value(): number { return this.value; }
    public set $value(value: number) { this.value = value; }

    constructor(value: number, publicKey: string) {
        this.value = value;
        this.lockScript = publicKey;
    }

    public static createOnputsFromUnserialize(objs: Array<Output>) {
        let outs = [];
        objs.forEach(obj => {
            outs.push(new Output(obj.value, obj.lockScript));
        });
        return outs;
    }

    canUnlock(privateKey: string): boolean {
        if (privateKey == rsaConfig[this.lockScript]) {
            return true;
        } else {
            return false;
        }
    }
}
</code>

The

value

field holds the amount of BTC, while

lockScript

(here simplified to a public key) locks the output until the matching private key unlocks it.

A Transaction

A transaction consists of multiple inputs and outputs and is identified by a unique

txId

. The implementation serializes inputs and outputs, concatenates them with a timestamp, and hashes the result with SHA‑256 to produce the transaction ID.

<code>export class Transaction {
    private txId: string;
    private inputTxs: Array<Input>;
    private outputTxs: Array<Output>;

    constructor(txId: string, inputs: Array<Input>, outputs: Array<Output>) {
        this.txId = txId;
        this.inputTxs = inputs;
        this.outputTxs = outputs;
    }

    public get $txId(): string { return this.txId; }
    public set $txId(value: string) { this.txId = value; }
    public get $inputTxs(): Array<Input> { return this.inputTxs; }
    public set $inputTxs(value: Array<Input>) { this.inputTxs = value; }
    public get $outputTxs(): Array<Output> { return this.outputTxs; }
    public set $outputTxs(value: Array<Output>) { this.outputTxs = value; }

    public setTxId() {
        let sha256 = crypto.createHash('sha256');
        sha256.update(JSON.stringify(this.inputTxs) + JSON.stringify(this.outputTxs) + Date.now(), 'utf8');
        this.txId = sha256.digest('hex');
    }
}
</code>

Coinbase Transaction

A coinbase transaction has no inputs (input index = -1 and empty txId) and rewards the miner with newly created BTC plus transaction fees.

<code>public static createCoinbaseTx(pubKey: string, info: string) {
    let input = new Input('', -1, info);
    let output = new Output(AWARD, pubKey);
    let tx = new Transaction('', [input], [output]);
    tx.setTxId();
    return tx;
}

public static isCoinbaseTx(tx: Transaction) {
    if (tx.$inputTxs.length == 1 && tx.$inputTxs[0].$outputIndex == -1 && tx.$inputTxs[0].$txId == '') {
        return true;
    } else {
        return false;
    }
}
</code>

Transfer Transaction

To transfer BTC, the sender’s UTXOs are gathered, inputs are created, outputs are generated for the recipient and (if needed) change back to the sender, and the transaction ID is computed.

<code>public static createTransaction(from: string, fromPubkey: string, fromKey: string, to: string, toPubkey: string, coin: number) {
    let outputs = this.findUTXOToTransfer(fromKey, coin);
    console.log(`UTXOToTransfer: ${JSON.stringify(outputs)}, from: ${from} to ${to} transfer ${coin}`);
    let inputTx = [], sum = 0, outputTx = [];
    outputs.forEach(o => {
        sum += o.$value;
        inputTx.push(new Input(o.$txId, o.$index, fromKey));
    });
    if (sum < coin) {
        throw Error(`Insufficient balance, transfer failed! from ${from} to ${to} ${coin}btc, but only have ${sum}btc`);
    }
    outputTx.push(new Output(coin, toPubkey));
    if (sum > coin) {
        outputTx.push(new Output(sum - coin, fromPubkey));
    }
    let tx = new Transaction('', inputTx, outputTx);
    tx.setTxId();
    return tx;
}
</code>

The method

findUTXOToTransfer

searches all unspent outputs belonging to the sender, verifies them with

output.canUnlock(secreteKey)

, and selects enough UTXOs to cover the requested amount.

Conclusion

The presented UTXO implementation is a simplified educational version based on Bitcoin’s design. The source code is open‑source at the author’s GitHub repository (feature/utxo branch) for further exploration and improvement.

Node.jsBlockchainCryptocurrencyBitcoinUTXO
Weidian Tech Team
Written by

Weidian Tech Team

The Weidian Technology Platform is an open hub for consolidating technical knowledge. Guided by a spirit of sharing, we publish diverse tech insights and experiences to grow and look ahead together.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.