LanaCoin Technical Documentation

This technical documentation explains the detailed implementation of transaction creation, signing, and broadcasting in our LanaCoin wallet system.

Transaction Creation Process

UTXO Selection

Our wallet implements careful selection of unspent transaction outputs (UTXOs) to cover the required amount plus fees:

// Python code from electrum_client.py
def select_inputs(self, unspent, amount, send_all=False):
    """
    Select inputs from unspent outputs to cover the amount + fee.
    If send_all is True, it will use all unspent outputs.
    """
    if send_all:
        # For "send all" we use all available UTXOs
        return unspent
    
    # Sort UTXOs by value (largest first)
    sorted_unspent = sorted(unspent, key=lambda utxo: utxo['value'], reverse=True)
    
    selected = []
    total_selected = 0
    
    # Select UTXOs until we have enough to cover the amount
    for utxo in sorted_unspent:
        selected.append(utxo)
        total_selected += utxo['value']
        if total_selected >= amount:
            break
    
    # If we don't have enough funds, return all unspent
    if total_selected < amount:
        return sorted_unspent
    
    return selected

Fee Calculation

Fee calculation is based on transaction size with a safety buffer:

// Python code from app.py
# For LanaCoin transactions, we'll always use 2 inputs:
# 1. Main transaction input (user funds)
# 2. System donation input (if applicable)

# Always set fixed input count to 2 as per requirements
fixed_input_count = 2

# Outputs include the recipient, donation, and change if applicable
output_count = 2  # Default: recipient + change

# Calculate transaction size using the formula
# Formula: 10 + (num_inputs * 148) + (num_outputs * 34)
estimated_size = 10 + (fixed_input_count * 148) + (output_count * 34)

# Calculate base fee using the estimated size and fee rate
base_fee = estimated_size * fee_rate

# Apply 10% safety buffer as required
safe_fee = base_fee * 1.10

# Ensure minimum fee of 0.0001 LANA (10,000 lanoshis) per input
min_fee_per_input = 0.0001
min_total_fee = min_fee_per_input * fixed_input_count

# Use the larger of safe_fee or minimum fee
fee = max(safe_fee, min_total_fee)

Safe Lana Donation Handling

Every transaction includes a donation to support the LanaCoin ecosystem:

// Donation is calculated as 1% of the transaction amount
donation_percent = 0.01  // 1% donation
donation_satoshis = int(amount_satoshis * donation_percent)

// Donation is capped at 0.5 EUR
max_donation_eur = 0.5
max_donation_lana = max_donation_eur / rate_eur_per_lana
max_donation_satoshis = int(max_donation_lana * 100000000)

// Use the smaller value between percentage and max cap
donation_satoshis = min(donation_satoshis, max_donation_satoshis)

// Donation address is fixed
donation_address = "LNW3yUPf4jUGLwQmFsuvuZX79kLGR41VqE"

Dust Output Prevention

To prevent dust outputs that could be rejected by the network:

// Dust handling logic
dust_limit = 546  // satoshis (standard Bitcoin/LanaCoin dust limit)

// Check if change amount is below dust limit
if (0 < change_amount && change_amount < dust_limit) {
    // Add dust to fee instead of creating a dust output
    fee_satoshis += change_amount
    fee = round(fee_satoshis / 100000000, 8)  // Update fee in LANA
    
    // Set change to zero
    change_amount = 0
}

Transaction Construction

LanaCoin transactions have the following structure:

// Transaction structure
tx_data = {
    // Transaction metadata
    "version": 1,
    "sender": sender_address,
    "destination": destination_address,
    "amount": amount,
    "fee": fee,
    "balance": balance,
    "nTime": current_timestamp,  // Current Unix timestamp for nTime field
    
    // Inputs section with scriptPubKey data for each input
    "inputs": [
        {
            "txid": "previous_transaction_id",
            "vout": output_index,
            "value": amount_in_satoshis,
            "scriptPubKey": "hex_script_pub_key",
            "sequence": 0xffffffff
        },
        // More inputs...
    ],
    
    // Outputs section with destination, donation, then change (if applicable)
    "outputs": [
        {
            "address": destination_address,
            "amount": amount_satoshis,
            "pubKeyHash": destination_pubkey_hash,
            "description": "Payment to destination"
        },
        {
            "address": donation_address,
            "amount": donation_satoshis,
            "pubKeyHash": donation_pubkey_hash,
            "description": "Safe Lana Donation"
        },
        // If change_amount > 0, include a change output
        {
            "address": sender_address,
            "amount": change_amount,
            "pubKeyHash": sender_pubkey_hash,
            "description": "Change back to sender"
        }
    ]
}

Transaction Signing

Transaction signing follows the Bitcoin signature protocol with LanaCoin-specific additions:

// Code from offline-signer.js
async createAndSignTransaction(wif) {
    try {
        const tx = this.transactionData;
        const decoded = this.base58decode(wif);
        const privBytes = decoded.slice(1, 33);
        const privHex = this.bytesToHex(privBytes);

        // Initialize elliptic curve key pair
        const key = this.ec.keyFromPrivate(privHex);
        const pub = key.getPublic();
        const pubX = pub.getX().toString(16).padStart(64, '0');
        const pubY = pub.getY().toString(16).padStart(64, '0');
        const pubkey = this.hexToBytes('04' + pubX + pubY);

        // Transaction header
        const version = new Uint8Array([1, 0, 0, 0]);
        const nTime = new Uint8Array((() => {
            const t = tx.nTime || Math.floor(Date.now() / 1000);
            return [t & 0xff, (t >> 8) & 0xff, (t >> 16) & 0xff, (t >> 24) & 0xff];
        })());

        const inputCount = tx.inputs.length;
        const validOutputs = tx.outputs.filter(output => output && output.amount > 0);
        const outputCount = new Uint8Array([validOutputs.length]);
        const outputBuffers = validOutputs.map(o => this.constructOutput(o));
        const outputsData = new Uint8Array(outputBuffers.reduce((acc, b) => [...acc, ...b], []));

        // Array to store all signatures and inputs
        const signedInputs = [];
        
        // Process and sign each input
        for (let i = 0; i < inputCount; i++) {
            const input = tx.inputs[i];
            const txid = new Uint8Array(input.txid.match(/.{2}/g).reverse().map(b => parseInt(b, 16)));
            const vout = new Uint8Array(this.toLittleEndian(input.vout, 4));
            const sequence = new Uint8Array([0xff, 0xff, 0xff, 0xff]);
            const scriptPubKey = this.hexToBytes(input.scriptPubKey);
            
            // Build preimage with ALL inputs, but only set scriptPubKey for the one being signed
            let inputsPreimage = [];
            
            // Include all inputs in the preimage
            for (let j = 0; j < inputCount; j++) {
                const other = tx.inputs[j];
                const otherTxid = new Uint8Array(other.txid.match(/.{2}/g).reverse().map(b => parseInt(b, 16)));
                const otherVout = new Uint8Array(this.toLittleEndian(other.vout, 4));
                const otherSequence = new Uint8Array([0xff, 0xff, 0xff, 0xff]);
                
                // Only use scriptPubKey for the input being signed, others get empty script
                const otherScript = (j === i) ? this.hexToBytes(other.scriptPubKey) : new Uint8Array([]);
                const scriptLen = new Uint8Array([otherScript.length]);
                
                // Add this input to the preimage
                inputsPreimage.push(...otherTxid, ...otherVout, ...scriptLen, ...otherScript, ...otherSequence);
            }
            
            // Create preimage with ALL inputs (proper Bitcoin/LanaCoin sighash construction)
            const preimage = new Uint8Array([
                ...version,
                ...nTime,
                inputCount,  // All inputs are included
                ...inputsPreimage, // But only one has scriptPubKey
                ...outputCount,  // Dynamic output count
                ...outputsData,  // Dynamic outputs data
                0, 0, 0, 0,      // locktime
                0x01, 0x00, 0x00, 0x00 // SIGHASH_ALL
            ]);
            
            // Create signature for this input
            const sighash = await this.doubleSha256(preimage);
            const sigObj = key.sign(sighash, { canonical: true });
            const derSig = this.hexToBytes(sigObj.toDER('hex'));
            const sigWithHashType = new Uint8Array([...derSig, 0x01]);
            
            // Construct script sig for this input using canonical push encoding
            const scriptSig = new Uint8Array([
                ...this.pushData(sigWithHashType),
                ...this.pushData(pubkey)
            ]);
            
            // Store the input data
            signedInputs.push({
                txid: txid,
                vout: vout,
                scriptSig: scriptSig,
                sequence: sequence
            });
        }
        
        // Build final transaction
        let finalTxArray = [...version, ...nTime, inputCount];
        
        // Add all inputs
        for (const input of signedInputs) {
            finalTxArray = [
                ...finalTxArray,
                ...input.txid,
                ...input.vout,
                input.scriptSig.length,
                ...input.scriptSig,
                ...input.sequence
            ];
        }
        
        // Add output count and outputs
        finalTxArray = [
            ...finalTxArray,
            ...outputCount,
            ...outputsData
        ];
        
        // Add locktime (only once)
        const locktime = new Uint8Array([0, 0, 0, 0]);
        const finalTx = new Uint8Array([...finalTxArray, ...locktime]);
        
        // Convert to hex for broadcasting
        const finalHex = this.bytesToHex(finalTx);
        
        // Validate the hex before returning
        const validationResult = this.validateHex(finalHex);
        if (validationResult !== "OK") {
            throw new Error(`Transaction validation failed: ${validationResult}`);
        }
        
        return finalHex;
    } catch (error) {
        console.error('Transaction signing error:', error);
        throw error;
    }
}

Data Push Format

For correct serialization of signatures and public keys:

/**
 * Helper function to correctly push data onto the stack according to Bitcoin script rules
 * Small data uses simple length prefix, larger data uses OP_PUSHDATA opcodes
 */
pushData(data) {
    const length = data.length;
    
    if (length < 0x4c) {
        // For small data (< 76 bytes), use a simple length byte
        return new Uint8Array([length, ...data]);
    } else if (length < 0x100) {
        // For medium data (< 256 bytes), use OP_PUSHDATA1
        return new Uint8Array([0x4c, length, ...data]);
    } else if (length < 0x10000) {
        // For larger data (< 65536 bytes), use OP_PUSHDATA2
        return new Uint8Array([
            0x4d, 
            length & 0xff, 
            (length >> 8) & 0xff, 
            ...data
        ]);
    } else {
        // For very large data, use OP_PUSHDATA4
        return new Uint8Array([
            0x4e, 
            length & 0xff, 
            (length >> 8) & 0xff, 
            (length >> 16) & 0xff, 
            (length >> 24) & 0xff, 
            ...data
        ]);
    }
}

Transaction Broadcasting

After signing, the transaction is broadcast to the LanaCoin network:

// Code from app.py - broadcast_transaction route
@app.route('/broadcast_transaction', methods=['POST'])
def broadcast_transaction():
    try:
        data = request.json
        raw_tx = data.get('hex')
        
        # Validate the input
        if not raw_tx or not isinstance(raw_tx, str):
            return jsonify({'error': 'Missing or invalid transaction hex'}), 400
            
        # Additional validation to ensure the transaction is properly formatted
        if not electrum_client.validate_transaction_hex(raw_tx):
            return jsonify({'error': 'Invalid transaction format'}), 400
            
        # Broadcast the transaction
        result = electrum_client.broadcast_transaction(raw_tx)
        
        if not result.get('success'):
            return jsonify({
                'success': False,
                'error': result.get('error', 'Unknown broadcast error')
            }), 400
            
        # Return the transaction ID along with success message
        return jsonify({
            'success': True,
            'txid': result.get('txid'),
            'message': 'Transaction broadcast successfully!'
        })
        
    except Exception as e:
        return jsonify({
            'success': False,
            'error': str(e)
        }), 400

Electrum Server Connection

The electrum client sends the transaction to LanaCoin's Electrum network:

# Code from electrum_client.py
def broadcast_transaction(self, raw_tx):
    """
    Broadcast a signed transaction to the LanaCoin network
    
    Args:
        raw_tx (str): The hex-encoded signed transaction
        
    Returns:
        dict: Result of the broadcast operation
    """
    try:
        # Broadcast transaction using blockchain.transaction.broadcast method
        result = self.send_request('blockchain.transaction.broadcast', [raw_tx])
        
        # If successful, result will be the transaction ID (txid)
        if isinstance(result, str) and len(result) == 64:
            return {
                'success': True,
                'txid': result
            }
        else:
            # If we get here, the result is not a txid, likely an error
            return {
                'success': False,
                'error': f'Unexpected response: {result}'
            }
            
    except Exception as e:
        return {
            'success': False,
            'error': str(e)
        }