Skip to content

Commit 90d0768

Browse files
authored
Merge pull request #2622 from nervosnetwork/testnet
Deploy to mainnet
2 parents 9f5e078 + 48c117a commit 90d0768

File tree

6 files changed

+207
-12
lines changed

6 files changed

+207
-12
lines changed
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
class BitcoinVoutSpentCheckerWorker
2+
include Sidekiq::Job
3+
sidekiq_options retry: 0, queue: :bitcoin
4+
5+
ZERO_TXID = "0000000000000000000000000000000000000000000000000000000000000000".freeze
6+
7+
def perform
8+
unspent_outpoints.each do |txid, index|
9+
fallback_networks = ENV["CKB_NET_MODE"] == CKB::MODE::MAINNET ? [:mainnet] : %i[testnet signet]
10+
fallback_networks.each do |network|
11+
result = check_outspent(txid, index, network: network)
12+
break if %i[spent unspent].include?(result)
13+
end
14+
end
15+
end
16+
17+
def unspent_outpoints
18+
desired_limit = ENV["CKB_NET_MODE"] == CKB::MODE::MAINNET ? 166 : 2000
19+
vouts = BitcoinVout.includes(:bitcoin_transaction).without_op_return.where(consumed_by_id: nil).order(id: :asc).limit(desired_limit * 2)
20+
if (last_id = $redis.get("btc:vout:last_request_id")).present?
21+
vouts = vouts.where("id > ?", last_id.to_i)
22+
end
23+
24+
outpoints = []
25+
vout_ids = []
26+
27+
vouts.each do |vout|
28+
key = [vout.bitcoin_transaction.txid, vout.index]
29+
next if outpoints.include?(key)
30+
31+
outpoints << key
32+
vout_ids << vout.id
33+
break if outpoints.size >= desired_limit
34+
end
35+
36+
$redis.set("btc:vout:last_request_id", vout_ids.max)
37+
outpoints
38+
end
39+
40+
def check_outspent(txid, index, network:)
41+
cache_key = "unisat:#{network}:tx:#{txid}"
42+
vouts = Rails.cache.read(cache_key)
43+
if vouts.nil?
44+
vouts = fetch_unisat_vouts(txid, network)
45+
Rails.cache.write(cache_key, vouts, expires_in: 30.minutes) if vouts.present?
46+
end
47+
48+
return :not_found if vouts.nil?
49+
50+
out = vouts.detect { |o| o["vout"] == index }
51+
spent_txid = out&.dig("txidSpent")
52+
53+
if spent_txid.present? && spent_txid != ZERO_TXID
54+
resolve_binding_status(spent_txid, txid, index)
55+
Rails.logger.info("Unisat: #{txid}:#{index} on #{network} => spent => #{spent_txid}")
56+
return :spent
57+
end
58+
59+
Rails.logger.info("Unisat: #{txid}:#{index} on #{network} => unspent")
60+
:unspent
61+
rescue StandardError => e
62+
Rails.logger.error("check_outspent failed #{txid}:#{index} on #{network} - #{e.class}: #{e.message}")
63+
:unknown
64+
end
65+
66+
def fetch_unisat_vouts(txid, network)
67+
# testnet: 10 RPS, 864000/day
68+
# mainnet: 5 RPS, 2000/day
69+
host = ENV.fetch("UNISAT_#{network.to_s.upcase}_HOST", nil)
70+
token = ENV.fetch("UNISAT_#{network.to_s.upcase}_TOKEN", nil)
71+
headers = { "accept" => "application/json", "Authorization" => "Bearer #{token}" }
72+
73+
all = []
74+
cursor = 0
75+
size = 1000
76+
77+
loop do
78+
url = "#{host}/v1/indexer/tx/#{txid}/outs?cursor=#{cursor}&size=#{size}"
79+
response = HTTP.timeout(60).headers(headers).get(url)
80+
sleep(network == :mainnet ? 0.4 : 0.2)
81+
82+
body = JSON.parse(response.body.to_s)
83+
if body["code"] != 0 || !body["data"].is_a?(Array)
84+
# Rails.logger.warn("Unisat error #{txid} on #{network}: #{body['msg']}")
85+
return nil
86+
end
87+
88+
all += body["data"]
89+
break if body["data"].size < size
90+
91+
cursor += size
92+
end
93+
94+
all
95+
rescue StandardError => e
96+
Rails.logger.error("fetch_unisat_vouts failed: #{txid} on #{network} - #{e.class}: #{e.message}")
97+
nil
98+
end
99+
100+
def resolve_binding_status(consumed_txid, txid, index)
101+
consumed_by = find_consumed_by_transaction(consumed_txid)
102+
return unless consumed_by
103+
104+
bitcoin_vouts = BitcoinVout.includes(:bitcoin_transaction).
105+
where(
106+
bitcoin_transactions: { txid: },
107+
bitcoin_vouts: { index:, op_return: false },
108+
)
109+
related_cell_outputs = bitcoin_vouts.filter_map(&:cell_output)
110+
111+
if related_cell_outputs.all?(&:live?)
112+
bitcoin_vouts.update_all(status: "binding")
113+
else
114+
bitcoin_vouts.each do |vout|
115+
next unless vout.cell_output
116+
next if vout.normal? || vout.unbound?
117+
118+
status = vout.cell_output.dead? ? "normal" : "unbound"
119+
vout.update(consumed_by:, status:)
120+
end
121+
end
122+
end
123+
124+
def find_consumed_by_transaction(txid)
125+
# check whether consumed_by has been synchronized
126+
consumed_by = BitcoinTransaction.find_by(txid:)
127+
unless consumed_by
128+
raw_tx = fetch_raw_transaction(txid)
129+
return nil unless raw_tx
130+
131+
consumed_by = BitcoinTransaction.create!(
132+
txid: raw_tx["txid"],
133+
tx_hash: raw_tx["hash"],
134+
time: raw_tx["time"],
135+
block_hash: raw_tx["blockhash"],
136+
block_height: 0,
137+
)
138+
end
139+
consumed_by
140+
end
141+
142+
def fetch_raw_transaction(txid)
143+
data = Rails.cache.read(txid)
144+
data ||= Bitcoin::Rpc.instance.getrawtransaction(txid, 2)
145+
Rails.cache.write(txid, data, expires_in: 10.minutes) unless Rails.cache.exist?(txid)
146+
data["result"]
147+
rescue StandardError => e
148+
Rails.logger.error "get bitcoin raw transaction #{txid} failed: #{e}"
149+
nil
150+
end
151+
end
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# 1.Check every pending transaction in the pool if rejected
2+
# 2.When a pending tx import to db and the same tx was import by node processor,it will exist two same tx with different status
3+
class CleanRejectedTxWorker
4+
include Sidekiq::Worker
5+
sidekiq_options retry: 0
6+
7+
def perform
8+
rejected_tx_ids = CkbTransaction.tx_rejected.where("created_at < ?", 4.hours.ago).limit(100).pluck(:id)
9+
rejected_output_ids = CellOutput.rejected.where(ckb_transaction_id: rejected_tx_ids).pluck(:id)
10+
CellDatum.where(cell_output_id: rejected_output_ids).delete_all
11+
CellOutput.where(id: rejected_output_ids).delete_all
12+
CellInput.where(ckb_transaction_id: rejected_tx_ids).delete_all
13+
AccountBook.where(ckb_transaction_id: rejected_tx_ids).delete_all
14+
CellDependency.where(ckb_transaction_id: rejected_tx_ids).delete_all
15+
Witness.where(ckb_transaction_id: rejected_tx_ids).delete_all
16+
HeaderDependency.where(ckb_transaction_id: rejected_tx_ids).delete_all
17+
RejectReason.where(ckb_transaction_id: rejected_tx_ids).delete_all
18+
CkbTransaction.where(id: rejected_tx_ids).delete_all
19+
end
20+
end

lib/scheduler.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,4 +139,12 @@ def call_worker(clz)
139139
call_worker FiberGraphDetectWorker
140140
end
141141

142+
s.every "2h", overlap: false do
143+
call_worker BitcoinVoutSpentCheckerWorker
144+
end
145+
146+
s.every "1h", overlap: false do
147+
call_worker CleanRejectedTxWorker
148+
end
149+
142150
s.join

test/factories/cell_output.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
block
44
address
55
status { "live" }
6-
capacity { 10**8 * 8 }
6+
capacity { (10**8) * 8 }
77
transient do
88
data { nil }
99
end
@@ -51,7 +51,7 @@
5151
cell.address.increment! :live_cells_count
5252
end
5353
income = cell.ckb_transaction.outputs.where(address: cell.address).sum(:capacity) - cell.ckb_transaction.inputs.where(address: cell.address).sum(:capacity)
54-
AccountBook.upsert({ ckb_transaction_id: cell.ckb_transaction_id, address_id: cell.address_id, block_number: cell.block.number, tx_index: cell.ckb_transaction.tx_index, income: },
54+
AccountBook.upsert({ ckb_transaction_id: cell.ckb_transaction_id, address_id: cell.address_id, block_number: cell.block&.number, tx_index: cell.ckb_transaction.tx_index, income: },
5555
unique_by: %i[address_id ckb_transaction_id])
5656
end
5757
end

test/factories/ckb_transaction.rb

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
tx_hash { "0x#{SecureRandom.hex(32)}" }
55
sequence(:tx_index) { |n| n }
66
tx_status { "committed" }
7-
block_number { block.number }
8-
block_timestamp { block.timestamp }
7+
block_number { block&.number }
8+
block_timestamp { block&.timestamp }
99
transaction_fee { 100 }
1010
version { 0 }
1111
bytes { 2000 }
@@ -81,17 +81,17 @@
8181

8282
trait :with_cell_output_and_lock_and_type_script do
8383
after(:create) do |ckb_transaction, _evaluator|
84-
output1 = create(:cell_output, capacity: 10**8 * 8,
84+
output1 = create(:cell_output, capacity: (10**8) * 8,
8585
ckb_transaction:,
8686
block: ckb_transaction.block,
8787
tx_hash: ckb_transaction.tx_hash,
8888
cell_index: 0)
89-
output2 = create(:cell_output, capacity: 10**8 * 8,
89+
output2 = create(:cell_output, capacity: (10**8) * 8,
9090
ckb_transaction:,
9191
block: ckb_transaction.block,
9292
tx_hash: ckb_transaction.tx_hash,
9393
cell_index: 1)
94-
output3 = create(:cell_output, capacity: 10**8 * 8,
94+
output3 = create(:cell_output, capacity: (10**8) * 8,
9595
ckb_transaction:,
9696
block: ckb_transaction.block,
9797
tx_hash: ckb_transaction.tx_hash,
@@ -115,10 +115,10 @@
115115
tx = create(:ckb_transaction, :with_cell_output_and_lock_script,
116116
block:)
117117
if evaluator.contained_address_ids.present?
118-
create(:cell_output, capacity: 10**8 * 8,
118+
create(:cell_output, capacity: (10**8) * 8,
119119
ckb_transaction:, block: ckb_transaction.block, tx_hash: ckb_transaction.tx_hash, cell_index: index, address_id: evaluator.contained_address_ids.first)
120120
else
121-
create(:cell_output, capacity: 10**8 * 8,
121+
create(:cell_output, capacity: (10**8) * 8,
122122
ckb_transaction:, block: ckb_transaction.block, tx_hash: ckb_transaction.tx_hash, cell_index: index)
123123
end
124124
previous_output = { tx_hash: tx.tx_hash, index: 0 }
@@ -133,7 +133,7 @@
133133

134134
trait :with_single_output do
135135
after(:create) do |ckb_transaction|
136-
create(:cell_output, capacity: 10**8 * 8,
136+
create(:cell_output, capacity: (10**8) * 8,
137137
ckb_transaction:,
138138
block: ckb_transaction.block,
139139
tx_hash: ckb_transaction.tx_hash,
@@ -144,7 +144,7 @@
144144
trait :cell_base_with_multiple_inputs_and_outputs do
145145
after(:create) do |ckb_transaction|
146146
15.times do |index|
147-
create(:cell_output, capacity: 10**8 * 8,
147+
create(:cell_output, capacity: (10**8) * 8,
148148
ckb_transaction:,
149149
block: ckb_transaction.block,
150150
tx_hash: ckb_transaction.tx_hash,
@@ -160,7 +160,7 @@
160160
trait :with_cell_base do
161161
after(:create) do |ckb_transaction|
162162
15.times do |index|
163-
create(:cell_output, capacity: 10**8 * 8,
163+
create(:cell_output, capacity: (10**8) * 8,
164164
ckb_transaction:,
165165
block: ckb_transaction.block,
166166
tx_hash: ckb_transaction.tx_hash,
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
require "test_helper"
2+
3+
class CleanRejectedTxWorkerTest < ActiveSupport::TestCase
4+
test "should clean rejected tx" do
5+
tx = create(:ckb_transaction, block: nil, created_at: 5.hours.ago, tx_status: "rejected")
6+
create(:cell_output, ckb_transaction: tx, status: "rejected", block: nil)
7+
8+
Sidekiq::Testing.inline!
9+
assert_changes -> { CkbTransaction.count }, from: 1, to: 0 do
10+
CleanRejectedTxWorker.perform_async
11+
end
12+
assert_equal 0, CellOutput.count
13+
assert_equal 0, HeaderDependency.count
14+
assert_equal 0, Witness.count
15+
end
16+
end

0 commit comments

Comments
 (0)