Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions scripts/rpc-checks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ the receipts archive), `Filecoin.ChainGetTipSetByHeight`.
## Usage

```
check_rpc.rb [--only m1,m2,...] <network> <start_epoch> [end_epoch]
check_rpc.rb [--only m1,m2,...] <start_epoch> [end_epoch]
methods: blocks, receipts, tipsets, logs (default: all)
env: FOREST_RPC_URL overrides the node URL (default localhost:2345/rpc/v1)
```

The network (mainnet/calibnet) is auto-detected from the node via
`Filecoin.StateNetworkName`, so the right dataset is always compared against.

Exit codes (CI): `0` all pass · `1` any mismatch (dominates) · `2` no mismatches but an
archive day was unavailable (not yet published), so coverage is partial.

Expand All @@ -24,16 +27,16 @@ The node should run with `--no-gc`, or historical state may be pruned mid-run.

```sh
bundle install
bundle exec ruby check_rpc.rb calibnet 3865654 3871413
FOREST_RPC_URL=localhost:2347/rpc/v1 bundle exec ruby check_rpc.rb --only logs calibnet 3865787
bundle exec ruby check_rpc.rb 3865654 3871413
FOREST_RPC_URL=localhost:2347/rpc/v1 bundle exec ruby check_rpc.rb --only logs 3865787
```

## Run via Docker

```sh
docker build -t forest-rpc-checks .
# --network host so the container can reach the node on localhost:
docker run --rm --network host forest-rpc-checks calibnet 3865654 3865663
docker run --rm --network host forest-rpc-checks 3865654 3865663
docker run --rm --network host -e FOREST_RPC_URL=localhost:2347/rpc/v1 \
forest-rpc-checks --only receipts,logs calibnet 3865654 3865663
forest-rpc-checks --only receipts,logs 3865654 3865663
```
49 changes: 37 additions & 12 deletions scripts/rpc-checks/check_rpc.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,15 @@
SECONDS_IN_DAY = 24 * 60 * 60
EPOCHS_IN_DAY = SECONDS_IN_DAY / SECONDS_IN_EPOCH
DIFF_LIMIT = 20
# A diff line embeds whole JSON values; a null-vs-document mismatch would
# otherwise print the entire archive entry (100 KB+) on a single line.
DIFF_LINE_LIMIT = 512
# What counts as a transient network error, for both the node and the dataset.
NET_ERRORS = [IOError, SystemCallError, Net::OpenTimeout, Net::ReadTimeout].freeze

# Node network name (Filecoin.StateNetworkName) -> dataset path segment.
NETWORKS = { 'mainnet' => 'mainnet', 'calibrationnet' => 'calibnet' }.freeze

# Method -> daily archive file in the dataset. logs has no archive of its own:
# its reference is derived from the receipts archive by flattening every
# receipt's logs.
Expand Down Expand Up @@ -74,10 +80,17 @@ def deep_diff(node, archive, path = '$')
in [Array => a, Array => b]
diff_arrays(a, b, path)
else
["#{path}: node=#{node.to_json} archive=#{archive.to_json}"]
["#{path}: node=#{excerpt(node)} archive=#{excerpt(archive)}"]
end
end

# Cap each side of a leaf diff line separately, so a huge node value can never
# push the archive side out of the clipped line (and vice versa).
def excerpt(value, limit = DIFF_LINE_LIMIT / 4)
json = value.to_json
json.size > limit ? "#{json[0, limit]}… (#{json.size} chars)" : json
end

def diff_arrays(node, archive, path)
header = []
header << "#{path}: array sizes differ (node=#{node.size}, archive=#{archive.size})" if node.size != archive.size
Expand Down Expand Up @@ -279,18 +292,21 @@ def null_round(epoch, resp, agreed:, **detail)
def fail_with(header, lines)
@failed = true
@out << "MISMATCH #{header}"
@out.concat(lines.first(DIFF_LIMIT).map { " #{it}" })
@out.concat(lines.first(DIFF_LIMIT).map { " #{clip(it)}" })
@out << " … #{lines.size - DIFF_LIMIT} more" if lines.size > DIFF_LIMIT
end

def clip(line) = line.size > DIFF_LINE_LIMIT ? "#{line[0, DIFF_LINE_LIMIT]}… (#{line.size} chars)" : line
end

# --- CLI ----------------------------------------------------------------------

methods = Checker::METHODS
parser = OptionParser.new do |o|
o.banner = <<~BANNER
Usage: #{File.basename($PROGRAM_NAME)} [--only m1,m2,...] <network> <start_epoch> [end_epoch]
Usage: #{File.basename($PROGRAM_NAME)} [--only m1,m2,...] <start_epoch> [end_epoch]
env: FOREST_RPC_URL overrides the node URL (default localhost:2345/rpc/v1)
The network is auto-detected from the node.
BANNER
o.on('--only LIST', Array, "Methods to run (#{methods.join(', ')}; default all)") { methods = it }
end
Expand All @@ -299,21 +315,30 @@ def fail_with(header, lines)
rescue OptionParser::ParseError => e
abort "#{e.message}\n#{parser.help}"
end
net, start_epoch, end_epoch = ARGV
abort parser.help if net.nil? || start_epoch.nil?
abort "Unknown network: #{net}" unless %w[mainnet calibnet].include?(net)
start_epoch, end_epoch, extra = ARGV
abort parser.help if start_epoch.nil? || !extra.nil?
unknown = methods - Checker::METHODS
abort "Unknown method(s): #{unknown.join(', ')} (expected: #{Checker::METHODS.join(', ')})" unless unknown.empty?

range = Integer(start_epoch)..Integer(end_epoch || start_epoch)
range = begin
Integer(start_epoch)..Integer(end_epoch || start_epoch)
rescue ArgumentError
abort "Epochs must be integers (note: the network argument is gone, it is auto-detected).\n#{parser.help}"
end
rpc_url = ENV.fetch('FOREST_RPC_URL', 'localhost:2345/rpc/v1')

# Genesis timestamp comes from the node itself, so it stays correct across networks.
genesis = begin
Rpc.new(rpc_url).call('Filecoin.ChainGetGenesis', []).dig('result', 'Blocks', 0, 'Timestamp')
rescue StandardError
nil
# The network and the genesis timestamp both come from the node itself, so the
# right dataset is always compared against, whatever the node runs.
begin
rpc = Rpc.new(rpc_url)
network_name = rpc.call('Filecoin.StateNetworkName', [])['result']
genesis = rpc.call('Filecoin.ChainGetGenesis', []).dig('result', 'Blocks', 0, 'Timestamp')
rescue StandardError => e
abort "Failed to query the node at #{rpc_url}: #{e.message}"
end
abort "Failed to fetch network name from #{rpc_url}" if network_name.nil?
net = NETWORKS[network_name]
abort "No dataset for network #{network_name.inspect} (expected: #{NETWORKS.keys.join(', ')})" if net.nil?
abort "Failed to fetch genesis timestamp from #{rpc_url}" unless genesis.is_a?(Integer)

# The methods are independent: run each in its own thread (blocks alone makes
Expand Down