OP_RETURN

Complete examples for embedding arbitrary data on the BSV blockchain using OP_RETURN outputs.

Overview

OP_RETURN is a Bitcoin script opcode that marks transaction outputs as provably unspendable, making it ideal for storing arbitrary data on the blockchain. This feature is widely used for timestamping, document verification, application protocols, and on-chain data storage. BSV's unbounded block size makes it practical to store substantial amounts of data using OP_RETURN.

Related SDK Components:

Simple OP_RETURN Data Storage

import { Transaction, PrivateKey, P2PKH, Script, OP } from '@bsv/sdk'

/**
 * Simple OP_RETURN Data Storage
 *
 * Store arbitrary data in OP_RETURN outputs
 */
class OpReturnDataStore {
  /**
   * Create an OP_RETURN script with data
   */
  createOpReturnScript(data: Buffer | Buffer[]): Script {
    const script = new Script()

    // Add OP_FALSE (OP_0) and OP_RETURN
    script.writeOpCode(OP.OP_FALSE)
    script.writeOpCode(OP.OP_RETURN)

    // Add data chunks
    const dataArray = Array.isArray(data) ? data : [data]
    for (const chunk of dataArray) {
      script.writeBin(chunk)
    }

    return script
  }

  /**
   * Store text data on blockchain
   */
  async storeText(
    senderKey: PrivateKey,
    text: string,
    utxo: {
      txid: string
      vout: number
      satoshis: number
      script: Script
    }
  ): Promise<Transaction> {
    try {
      const tx = new Transaction()

      // Add input
      tx.addInput({
        sourceTXID: utxo.txid,
        sourceOutputIndex: utxo.vout,
        unlockingScriptTemplate: new P2PKH().unlock(senderKey),
        sequence: 0xffffffff
      })

      // Add OP_RETURN output with text data
      const textBuffer = Buffer.from(text, 'utf8')
      tx.addOutput({
        satoshis: 0, // OP_RETURN outputs are provably unspendable
        lockingScript: this.createOpReturnScript(textBuffer)
      })

      // Add change output
      const fee = 500
      const change = utxo.satoshis - fee

      if (change > 546) {
        tx.addOutput({
          satoshis: change,
          lockingScript: new P2PKH().lock(senderKey.toPublicKey().toHash())
        })
      }

      await tx.sign()

      console.log('Text stored on blockchain')
      console.log('Transaction ID:', tx.id('hex'))
      console.log('Data size:', textBuffer.length, 'bytes')

      return tx
    } catch (error) {
      throw new Error(`Failed to store text: ${error.message}`)
    }
  }

  /**
   * Store multiple data fields
   */
  async storeMultipleFields(
    senderKey: PrivateKey,
    fields: string[],
    utxo: {
      txid: string
      vout: number
      satoshis: number
      script: Script
    }
  ): Promise<Transaction> {
    try {
      const tx = new Transaction()

      tx.addInput({
        sourceTXID: utxo.txid,
        sourceOutputIndex: utxo.vout,
        unlockingScriptTemplate: new P2PKH().unlock(senderKey),
        sequence: 0xffffffff
      })

      // Convert fields to buffers
      const dataBuffers = fields.map(field => Buffer.from(field, 'utf8'))

      // Add OP_RETURN output
      tx.addOutput({
        satoshis: 0,
        lockingScript: this.createOpReturnScript(dataBuffers)
      })

      // Add change
      const fee = 500
      const change = utxo.satoshis - fee

      if (change > 546) {
        tx.addOutput({
          satoshis: change,
          lockingScript: new P2PKH().lock(senderKey.toPublicKey().toHash())
        })
      }

      await tx.sign()

      console.log('Multiple fields stored')
      console.log('Fields:', fields.length)
      console.log('Transaction ID:', tx.id('hex'))

      return tx
    } catch (error) {
      throw new Error(`Failed to store fields: ${error.message}`)
    }
  }

  /**
   * Parse OP_RETURN data from script
   */
  parseOpReturnData(script: Script): Buffer[] {
    const chunks = script.chunks
    const data: Buffer[] = []

    // Skip OP_FALSE (index 0) and OP_RETURN (index 1)
    for (let i = 2; i < chunks.length; i++) {
      if (chunks[i].buf) {
        data.push(chunks[i].buf)
      }
    }

    return data
  }

  /**
   * Extract OP_RETURN data from transaction
   */
  extractData(tx: Transaction): Buffer[] | null {
    for (const output of tx.outputs) {
      const chunks = output.lockingScript.chunks

      // Check if this is an OP_RETURN output
      if (chunks.length >= 2 && chunks[1].opCodeNum === OP.OP_RETURN) {
        return this.parseOpReturnData(output.lockingScript)
      }
    }

    return null
  }
}

/**
 * Usage Example
 */
async function simpleOpReturnExample() {
  const dataStore = new OpReturnDataStore()
  const privateKey = PrivateKey.fromRandom()

  const utxo = {
    txid: 'funding-tx-id...',
    vout: 0,
    satoshis: 50000,
    script: new P2PKH().lock(privateKey.toPublicKey().toHash())
  }

  // Store simple text
  const tx1 = await dataStore.storeText(
    privateKey,
    'Hello, BSV!',
    utxo
  )

  // Store multiple fields
  const tx2 = await dataStore.storeMultipleFields(
    privateKey,
    ['app_name', 'my_app', 'version', '1.0.0'],
    utxo
  )

  // Extract data
  const extractedData = dataStore.extractData(tx1)
  if (extractedData) {
    console.log('Extracted text:', extractedData[0].toString('utf8'))
  }
}

Document Timestamping

JSON Data Storage

File Metadata Registry

See Also

SDK Components:

Learning Paths:

Last updated