Skip to content

Commit

Permalink
refactor: bitcoin transaction detect worker
Browse files Browse the repository at this point in the history
  • Loading branch information
rabbitz committed Mar 26, 2024
1 parent 5ef91de commit c3f6f45
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 94 deletions.
110 changes: 110 additions & 0 deletions app/jobs/import_bitcoin_utxo_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
class ImportBitcoinUtxoJob < ApplicationJob
queue_as :bitcoin

def perform(txid, out_index, cell_id)
ApplicationRecord.transaction do
cell_output = CellOutput.find_by(id: cell_id)
unless cell_output
raise ArgumentError, "Missing cell_output(#{cell_id}), txid(#{txid}), out_index(#{out_index})"
end

vout_attributes = []

# build bitcoin transaction
raw_tx = fetch_raw_transaction(txid)
tx = build_tranaction!(raw_tx)

# build op_returns
op_returns = build_op_returns!(raw_tx, tx, cell_output.ckb_transaction, vout_attributes)
vout_attributes.concat(op_returns) if op_returns.present?

# build vout
vout_attributes << build_vout!(raw_tx, tx, out_index, cell_output)

BitcoinVout.upsert_all(vout_attributes, unique_by: %i[bitcoin_transaction_id index]) if vout_attributes.present?
end
end

def build_tranaction!(raw_tx)
tx = BitcoinTransaction.find_by(txid: raw_tx["txid"])
return tx if tx

# avoid making multiple RPC requests
block_header = rpc.getblockheader(raw_tx["blockhash"])
BitcoinTransaction.create!(
txid: raw_tx["txid"],
tx_hash: raw_tx["hash"],
time: raw_tx["time"],
block_hash: raw_tx["blockhash"],
block_height: block_header["height"],
)
end

def build_op_returns!(raw_tx, tx, ckb_tx, v_attributes)
op_returns = []

raw_tx["vout"].each do |vout|
data = vout.dig("scriptPubKey", "hex")
script_pubkey = Bitcoin::Script.parse_from_payload(data.htb)
next unless script_pubkey.op_return?

op_return = {
bitcoin_transaction_id: tx.id,
bitcoin_address_id: nil,
data:,
index: vout.dig("n"),
asm: vout.dig("scriptPubKey", "asm"),
op_return: true,
ckb_transaction_id: ckb_tx.id,
cell_output_id: nil,
address_id: nil,
}

op_returns << op_return if v_attributes.exclude?(op_return)
end

op_returns
end

def build_vout!(raw_tx, tx, out_index, cell_output)
vout = raw_tx["vout"].find { _1["n"] == out_index }
raise ArgumentError, "Missing vout txid: #{raw_tx['txid']} index: #{out_index}" unless vout

address_hash = vout.dig("scriptPubKey", "address")
raise ArgumentError, "Missing vout address: #{raw_tx['txid']} index: #{out_index}" unless address_hash

address = build_address!(address_hash, cell_output)
{
bitcoin_transaction_id: tx.id,
bitcoin_address_id: address.id,
data: vout.dig("scriptPubKey", "hex"),
index: vout.dig("n"),
asm: vout.dig("scriptPubKey", "asm"),
op_return: false,
ckb_transaction_id: cell_output.ckb_transaction_id,
cell_output_id: cell_output.id,
address_id: cell_output.address_id,
}
end

def build_address!(address_hash, cell_output)
bitcoin_address = BitcoinAddress.find_or_create_by!(address_hash:)
BitcoinAddressMapping.
create_with(bitcoin_address_id: bitcoin_address.id).
find_or_create_by!(ckb_address_id: cell_output.address_id)

bitcoin_address
end

def fetch_raw_transaction(txid)
tx_json = Kredis.json txid, expires_in: 1.hour
if tx_json.value.blank?
tx_json.value = rpc.getrawtransaction(txid, 2)
end
tx_json.value
end

def rpc
@rpc ||= Bitcoin::Rpc.instance
end
end
4 changes: 4 additions & 0 deletions app/models/ckb_sync/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ def xudt_code_hash
Settings.xudt_code_hash
end

def rgbpp_code_hash
Settings.rgbpp_code_hash
end

METHOD_NAMES.each do |name|
define_method name do |*params|
call_rpc(name, params:)
Expand Down
16 changes: 13 additions & 3 deletions app/utils/ckb_utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -644,8 +644,18 @@ def self.parse_omiga_inscription_data(hex_data)
{ mint_limit: }
end

def self.parse_rgb_args(_args)
# TODO
["15dede3b31ed87bb6b1d668222127a7b308c1beb6fe99bf4a3f076bcae8e93fe", 4]
def self.is_rgbpp_lock_cell?(lock_script)
lock_script.code_hash == CkbSync::Api.instance.rgbpp_code_hash && lock_script.hash_type == "type"
end

# * https://learnmeabitcoin.com/technical/general/byte-order/
# Whenever you're working with transaction/block hashes internally (e.g. inside raw bitcoin data), you use the natural byte order.
# Whenever you're displaying or searching for transaction/block hashes, you use the reverse byte order.
def self.parse_rgbpp_args(args)
args = args.delete_prefix("0x")
out_index = [args[0..7]].pack("H*").unpack1("v")
txid = args[8..-1].scan(/../).reverse.join

[txid, out_index]
end
end
133 changes: 42 additions & 91 deletions app/workers/bitcoin_transaction_detect_worker.rb
Original file line number Diff line number Diff line change
@@ -1,106 +1,57 @@
class BitcoinTransactionDetectWorker
class BitcoinUtxoDetectWorker
include Sidekiq::Worker
sidekiq_options queue: "bitcoin"

INIT_BITCOIN_BLOCK_HEIGHT = 100_000

attr_accessor :block

def perform(block_id)
@block = Block.find_by(id: block_id)
return unless @block

ApplicationRecord.transaction do
vout_attributes = []

transacitons = @block.ckb_transactions.limit(min_transactions_count)
transacitons.each_with_index do |transaction, index|
next if transaction.bitcoin_vouts.exists?

txid = bitcoin_txids[index]
raw_transaction = rpc.getrawtransaction(txid, 2)
bitcoin_transaction = build_tranaction!(raw_transaction)

cell_output = transaction.cell_outputs.first
vout_attribute = build_vout_attributes!(raw_transaction, bitcoin_transaction, cell_output)
next unless vout_attribute

vout_attributes << vout_attribute
@block.ckb_transactions.each do |transaction|
vin_attributes = []

# import cell_inputs utxo
transaction.cell_inputs.each do |cell|
previous_cell_output = cell.previous_cell_output
next unless previous_cell_output

lock_script = previous_cell_output.lock_script
next unless CkbUtils2.is_rgbpp_lock_cell?(lock_script)

# import previous bitcoin transaction if prev vout is missing
import_utxo!(lock_script.args, previous_cell_output.id, transaction.id)

previous_vout = BitcoinVout.find_by(cell_output_id: previous_cell_output.id)
vin_attributes << {
previous_bitcoin_vout_id: previous_vout.id,
ckb_transaction_id: transaction.id,
cell_input_id: cell.id,
}
end

if vin_attributes.present?
BitcoinVin.upsert_all(vin_attributes, unique_by: %i[ckb_transaction_id cell_input_id])
end

# import cell_outputs utxo
transaction.cell_outputs.each do |cell|
lock_script = cell.lock_script
next unless CkbUtils2.is_rgbpp_lock_cell?(lock_script)

import_utxo!(lock_script.args, cell.id, transaction.id)
end
end

return if vout_attributes.blank?

BitcoinVout.upsert_all(vout_attributes, unique_by: %i[bitcoin_transaction_id index])
end
end

private

def bitcoin_txids
return @txids if @txids

block_hash = rpc.getblockhash(bitcoin_block_height)
# verbose set to 1 for JSON object
block = rpc.getblock(block_hash, 1)
@txids = block["tx"]
end
def import_utxo!(args, cell_id, _tx_id)
txid, out_index = CkbUtils2.parse_rgbpp_args(args)

def bitcoin_block_height
transaction = BitcoinTransaction.last
transaction ? transaction.block_height + 1 : 100_000
end

def min_transactions_count
[block.ckb_transactions_count, bitcoin_txids.count].min
end

def build_tranaction!(raw_tx)
tx = BitcoinTransaction.find_by(txid: raw_tx["txid"])
return tx if tx

# avoid making multiple RPC requests
block_header = rpc.getblockheader(raw_tx["blockhash"])
BitcoinTransaction.create!(
txid: raw_tx["txid"],
tx_hash: raw_tx["hash"],
time: raw_tx["time"],
block_hash: raw_tx["blockhash"],
block_height: block_header["height"],
)
end

def build_vout_attributes!(raw_tx, tx, cell_output)
vout = raw_tx["vout"].find { _1["n"] == cell_output.cell_index }
vout ||= raw_tx["vout"][0]

address_hash = vout.dig("scriptPubKey", "address")
return unless address_hash

bitcoin_address = build_address!(address_hash, cell_output)

{
bitcoin_transaction_id: tx.id,
bitcoin_address_id: bitcoin_address.id,
data: "6a24aa21a9ed5e53af6963d02d7fcf87695798a0715951bd03fb05f524015d88324636141f42",
index: vout.dig("scriptPubKey", "n"),
asm: "OP_RETURN aa21a9ed5e53af6963d02d7fcf87695798a0715951bd03fb05f524015d88324636141f42",
op_return: true,
ckb_transaction_id: cell_output.ckb_transaction_id,
cell_output_id: cell_output.id,
address_id: cell_output.address_id,
}
end

def build_address!(address_hash, cell_output)
bitcoin_address = BitcoinAddress.find_or_create_by(address_hash:)
BitcoinAddressMapping.
create_with(bitcoin_address_id: bitcoin_address.id).
find_or_create_by!(ckb_address_id: cell_output.address_id)

bitcoin_address
end

def rpc
@rpc ||= Bitcoin::Rpc.instance
unless BitcoinTransaction.includes(:bitcoin_vouts).where(
bitcoin_transactions: { txid: },
bitcoin_vouts: { index: out_index, cell_output_id: cell_id },
).exists?
ImportBitcoinUtxoJob.perform_now(txid, out_index, cell_id)
end
end
end
3 changes: 3 additions & 0 deletions config/settings.mainnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,6 @@ type_id_code_hash: "0x0000000000000000000000000000000000000000000000000054595045
homepage_transactions_records_count: 15
homepage_block_records_count: 15
proposal_window: 10

# rgbpp code hash
rgbpp_code_hash: "0x0000000000000000000000000000000000000000000000000000000000000000"
3 changes: 3 additions & 0 deletions config/settings.testnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,6 @@ type_id_code_hash: "0x0000000000000000000000000000000000000000000000000054595045
homepage_transactions_records_count: 15
homepage_block_records_count: 15
proposal_window: 10

# rgbpp code hash
rgbpp_code_hash: "0xaddb21cf401d1a609e60198f850c4417ff03f98462ba4a3f101fa2741f481228"

0 comments on commit c3f6f45

Please sign in to comment.