Skip to content

get-block-height ignores --profile and ARCH_PROFILE and queries the default profile #13

Description

@Kewe63

Summary

In the official v0.10.0 Linux CLI, get-block-height sends its get_block_count request to the stored default profile, ignoring both an explicit --profile selection and ARCH_PROFILE.

The reproduction creates two profiles through the real CLI and observes requests at two independent loopback HTTP listeners. Changing the stored default changes the destination (passing control); selecting the other profile per invocation does not.

Affected release and environment

  • Release: v0.10.0
  • Artifact: arch-cli-x86_64-unknown-linux-gnu
  • SHA-256: 82a3113791d5770cd3f336638d7c33f133fd88553ac769a649995fdd0913256a
  • --version: arch-cli 0.10.0 (preceded by the welcome message)
  • Audited distribution main: d4ef60273dbebcd236d7952465c4d438f3dc9a19
  • Distribution release-tag commit: 5fad528b21efbdf642d51ac3f7006fa31929a9ac
  • Release manifest source SHA: 63e75c075949c127d11601409184392202240b52
  • Ubuntu 24.04.4 LTS / WSL2, Linux 5.15.167.4, x86_64; glibc 2.39, OpenSSL 3.0.13, Python 3.12.3.

The distribution commit and binary source SHA represent different repositories; no identity mismatch is alleged. Older/nightly binaries were not tested, so the introduction version is unknown.

Supported workflow and expected behavior

An operator configures two local node endpoints and uses --profile b get-block-height to query the second without changing the global default.

The same binary's help documents:

-p, --profile <PROFILE>  Profile to use for configuration [env: ARCH_PROFILE=]

config create-profile --help documents --arch-node-url as the "Arch Network node URL". The test reads the real generated TOML back and verifies both URLs before querying. The selected profile's URL should therefore determine the request destination.

Actual behavior

Configuration Expected endpoint Observed endpoint
Default a; no override a a — control passes
Change default to b; no override b b — control passes
Default a; --profile b b a
Default a; ARCH_PROFILE=b b a
Default a; ARCH_PROFILE=a --profile b b a

All acceptance cases explicitly use --network-mode localnet.

Reproduction

Prerequisites: Linux with user/network namespaces, unshare, /usr/sbin/ip, Python 3.11+, curl, and the pinned binary. No Docker, Bitcoin, Electrs, Titan, wallets, or funded accounts are needed for this request-destination test.

In a fresh directory, save the Python script below as test_profile_routing.py, then run:

mkdir -p artifacts evidence
curl -fL https://github.com/Arch-Network/arch-node/releases/download/v0.10.0/arch-cli-x86_64-unknown-linux-gnu -o artifacts/arch-cli-x86_64-unknown-linux-gnu
sha256sum artifacts/arch-cli-x86_64-unknown-linux-gnu
chmod 755 artifacts/arch-cli-x86_64-unknown-linux-gnu
unshare -Urn -- python3 test_profile_routing.py

The executed test hashes the binary itself, uses fresh HOME/XDG directories per case, and enables only loopback inside an isolated network namespace. The listeners return HTTP 503 and never fabricate an Arch RPC result: the assertion concerns the destination selected before receiving that response. Consequently each CLI query exits 1, including the passing routing controls; that exit code is not the defect.

Complete executed regression test (Python standard library only)
"""Black-box regression: selected profile must determine get-block-height's destination.
Run: unshare -Urn -- python3 test_profile_routing.py
Requires the official v0.10.0 CLI alongside this audit's artifacts directory.
The HTTP observers return real HTTP 503 responses, never fake Arch RPC results.
Only the request destination is asserted, not node readiness or response semantics.
"""
import hashlib,http.server,json,os,pathlib,subprocess,tempfile,threading,tomllib,unittest
ROOT=pathlib.Path(__file__).resolve().parent
CLI=ROOT/'artifacts/arch-cli-x86_64-unknown-linux-gnu'
LOG=ROOT/'evidence/profile-routing-commands.jsonl'
EXPECTED_SHA256='82a3113791d5770cd3f336638d7c33f133fd88553ac769a649995fdd0913256a'
class Routing(unittest.TestCase):
 def setUp(self):
  self.assertEqual(hashlib.sha256(CLI.read_bytes()).hexdigest(),EXPECTED_SHA256)
  self.home=tempfile.TemporaryDirectory(prefix='profile-repro-',dir=ROOT)
  self.env={'PATH':'/usr/bin:/bin','HOME':self.home.name,'XDG_CONFIG_HOME':self.home.name+'/config','LANG':'C.UTF-8'}
  self.requests=[];requests=self.requests
  class Observer(http.server.BaseHTTPRequestHandler):
   def do_POST(self):
    body=self.rfile.read(int(self.headers['Content-Length']))
    requests.append({'port':self.server.server_port,'path':self.path,'body':json.loads(body)})
    self.send_response(503);self.send_header('Content-Length','0');self.end_headers()
   def log_message(self,*args):pass
  self.servers=[];self.threads=[]
  for name in ['a','b']:
   s=http.server.ThreadingHTTPServer(('127.0.0.1',0),Observer)
   t=threading.Thread(target=s.serve_forever,kwargs={'poll_interval':0.01},daemon=True);t.start()
   self.servers.append(s);self.threads.append(t)
   url='http://127.0.0.1:'+str(s.server_port)
   p=self.call(['config','create-profile',name,'--bitcoin-node-endpoint','http://127.0.0.1:18443','--bitcoin-node-username','unused','--bitcoin-node-password','synthetic-unused','--bitcoin-network','regtest','--arch-node-url',url,'--titan-url','http://127.0.0.1:3030'])
   self.assertEqual(p.returncode,0,p.stderr)
   cfg=tomllib.loads((pathlib.Path(self.home.name)/'config/arch/config.toml').read_text())
   self.assertEqual(cfg['profiles'][name]['arch_node_url'],url)
  self.assertEqual(self.call(['config','set-default-profile','a']).returncode,0)
 def tearDown(self):
  for s in self.servers:s.shutdown();s.server_close()
  for t in self.threads:t.join(timeout=2)
  self.home.cleanup()
 def call(self,args,env=None):
  p=subprocess.run([str(CLI)]+args,cwd=self.home.name,env=env or self.env,capture_output=True,text=True,timeout=10)
  with LOG.open('a') as f:f.write(json.dumps({'test':self.id(),'cwd':self.home.name,'command':p.args,'ARCH_PROFILE':(env or self.env).get('ARCH_PROFILE'),'exit_code':p.returncode,'stdout':p.stdout,'stderr':p.stderr,'requests':list(self.requests)})+'\n')
  return p
 def query(self,options,expected,env=None):
  self.requests.clear()
  p=self.call(['--network-mode','localnet']+options+['get-block-height'],env)
  self.assertEqual(p.returncode,1,'The HTTP observers intentionally return 503')
  self.assertEqual(len(self.requests),1,self.requests)
  self.assertEqual(self.requests[0]['body']['method'],'get_block_count')
  actual=self.requests[0]['port'];want=self.servers[expected].server_port
  print(json.dumps({'test':self.id(),'default_a_port':self.servers[0].server_port,'selected_b_port':self.servers[1].server_port,'actual_port':actual,'expected_port':want,'cli_exit':p.returncode}),flush=True)
  self.assertEqual(actual,want,'get-block-height sent the request to the wrong profile endpoint')
 def test_control_default_a_routes_to_a(self):self.query([],0)
 def test_control_change_default_to_b_routes_to_b(self):
  self.assertEqual(self.call(['config','set-default-profile','b']).returncode,0)
  self.query([],1)
 def test_explicit_profile_b_routes_to_b(self):self.query(['--profile','b'],1)
 def test_environment_profile_b_routes_to_b(self):self.query([],1,dict(self.env,ARCH_PROFILE='b'))
 def test_explicit_b_overrides_environment_a(self):self.query(['--profile','b'],1,dict(self.env,ARCH_PROFILE='a'))
if __name__=='__main__':
 assert [x['ifname'] for x in json.loads(subprocess.check_output(['/usr/sbin/ip','-j','link']))]==['lo'],'Use unshare -Urn'
 subprocess.run(['/usr/sbin/ip','link','set','lo','up'],check=True)
 unittest.main(verbosity=2)

Observed output

Repeated clean runs, including immediately before filing, produced 5 tests: 2 passing controls, 3 failures; suite exit 1.

Concrete explicit-profile example from the pre-filing run:

profile a: http://127.0.0.1:39957
profile b: http://127.0.0.1:37981
config set-default-profile a
--network-mode localnet --profile b get-block-height

actual_port: 39957
expected_port: 37981
CLI exit: 1
stderr: Error: Failed to get block height
Observed request method: get_block_count

Ran 5 tests in 0.625s
FAILED (failures=3)

In the same run, changing the default to b correctly sent the request to b's port 40685. Ports vary because each test uses fresh listeners.

Confirmed impact and severity

Suggested priority: Medium / P2, functional correctness. A read-only operator query contacts a different configured endpoint than requested. Per-invocation profile selection cannot be relied on when querying a non-default local node.

The proven routing workaround is config set-default-profile b. This changes global configuration rather than supplying a per-command override; separate configuration directories are preferable for concurrent scripts.

This report does not claim a wrong block-height value was successfully returned, a misdirected transaction, fund loss, or effects on other subcommands/networks. The controls establish correct routing, not successful full-node RPC operation. No live network was contacted by the CLI.

Source-access limitation and suggested fix direction

This repository distributes binaries and does not include the CLI implementation. The observed failure is that explicit/environment profile selection does not determine this command's destination while the stored default does. No internal function or source line is asserted as the cause.

Please inspect profile resolution in the source repository and pass the selected profile's arch_node_url into this command's RPC path. Preserve explicit selector/environment/default precedence. The included two-endpoint checks can serve as regression coverage; full-node happy-path coverage would be complementary. No production fix is included.

Duplicate research and limitations

The accessible arch-node tracker was reviewed across open/closed issues, merged/closed-unmerged PRs, comments and review discussions. Immediately before filing, the tracker and current main/release identities were refreshed; searches for get-block-height, profile, and ARCH_PROFILE returned no matching issue/PR.

There is related historical work: v0.5.6 release notes mention honoring ARCH_NODE_URL and removing localhost fallbacks, linking Arch-Network/arch-network#1362. That linked PR returned HTTP 404 with the available authenticated access, so its patch/discussion could not be compared. No equivalent report was found in the accessible distribution tracker, but exact overlap with inaccessible source-repository work remains unresolved. Please link any existing source issue if this is already tracked.

Prepared with AI assistance; all reported test results came from actual execution of the pinned official artifact.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions