Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a5b4ed7
Merge master (v0.23.0 release) back into develop
jdogresorg Oct 12, 2026
4af41f7
Fix 7 review findings in xchain-encoder
jdogresorg Oct 12, 2026
fa3e0e9
Port genesis identity helpers from decoder
jdogresorg Oct 12, 2026
a4e882b
Expose node tier mismatch in status readiness
jdogresorg Oct 12, 2026
c159b4c
test: guard encoder decoder chain identity parity
jdogresorg Oct 12, 2026
9ae9e9f
Merge: Port genesis identity helpers from decoder
jdogresorg Oct 12, 2026
d49c3bf
Merge: Expose node tier mismatch in status readiness
jdogresorg Oct 12, 2026
360d407
Merge: test: guard encoder decoder chain identity parity
jdogresorg Oct 12, 2026
caf9335
Bump axios to 1.20.0 and refresh fast-uri, ip-address, proxy-addr and…
jdogresorg Oct 13, 2026
86c3957
Refuse non-validator create_tx on mainnet before launch
jdogresorg Oct 13, 2026
d4a0803
Move the prelaunch gate into config and split its tests
jdogresorg Oct 13, 2026
4dc3be9
Merge: Move the prelaunch gate into config and split its tests
jdogresorg Oct 13, 2026
c4985d9
Route contributions to develop and guard master against non-release p…
jdogresorg Oct 13, 2026
4f7a5f7
Price transactions at their full size, select inputs against the fina…
jdogresorg Oct 14, 2026
b0b4c11
Fund multi-chunk P2WSH reveals with first-leg headroom and a surplus …
jdogresorg Oct 14, 2026
5d66dee
Fix review round findings for xchain-encoder (2026-10-14)
jdogresorg Oct 14, 2026
79f5490
Size bare-multisig dust from each coin's relay rate and refuse a reve…
jdogresorg Oct 15, 2026
03d00a5
Re-vendor coin registry for the mainnet genesis re-pin
jdogresorg Oct 17, 2026
603ac2b
Add transaction status lookup RPC
jdogresorg Oct 17, 2026
d317416
Keep status lookup within structure limits
jdogresorg Oct 17, 2026
83764a8
Require structured mempool miss codes
jdogresorg Oct 17, 2026
d7f39c5
Keep transaction lookups within source limits
jdogresorg Oct 17, 2026
d5cfc4f
Merge: Keep transaction lookups within source limits
jdogresorg Oct 17, 2026
4ef8cf9
Keep fee tiers ordered by confirmation speed
jdogresorg Oct 17, 2026
a8dfcf6
Keep fee ordering within source limit
jdogresorg Oct 17, 2026
3609492
Use relay fallback for implausible test-chain fees
jdogresorg Oct 17, 2026
d89ebef
Merge: Keep fee ordering within source limit
jdogresorg Oct 17, 2026
ec41581
Narrow fee estimation suite title pin
jdogresorg Oct 17, 2026
5fcda6a
Merge: Narrow fee estimation suite title pin
jdogresorg Oct 17, 2026
a9f9a14
Merge master into develop to level it before the v0.24.0 cut
jdogresorg Oct 17, 2026
83020a0
Release v0.24.0
jdogresorg Oct 17, 2026
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
2 changes: 2 additions & 0 deletions .ci-siblings
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,5 @@ xchain-sdk
# The inbound UTXO-record contract, the one seam carrying records INTO the
# encoder rather than out of it.
xchain-utxo-tracker
# The mainnet prelaunch validator wire-format set against the consensus copy.
xchain-indexer
4 changes: 4 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
<!-- Thanks for contributing to XChain Platform. -->

## Base branch

- [ ] This pull request targets `develop`. `master` only takes release merges, and a pull request into it from a fork cannot be merged.

## What this changes

<!-- One or two sentences on what changed and why. Link an issue if there is one. -->
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ on:
# so the merge commit a release tag points at has its own green run.
branches: [master, develop]
pull_request:
branches: [master]
branches: [master, develop]

jobs:
ci:
Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.24.0] - 2026-10-17

### Added
- Added a transaction status lookup method and exposed a node tier mismatch in status readiness.

### Changed
- Priced transactions at their full size, selected inputs against the final fee and refused sub-dust payments.
- Funded multi-chunk P2WSH reveals with first-leg headroom and a surplus sweep.
- Refused non-validator transaction creation on mainnet before launch.

### Fixed
- Sized bare-multisig dust from each coin's relay rate and refused a reveal that does not spend its funded scripts.
- Kept fee tiers ordered by confirmation speed and used the relay fallback for implausible test-chain fees.

### Security
- Bumped axios, fast-uri, ip-address, proxy-addr and brace-expansion to resolve security advisories.

## [0.23.0] - 2026-10-11

### Added
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ Run the no-external-services tiers before every commit (`npm run ci`, `npm run t

Match the existing log style: a concise subject line, then a short body explaining what changed and why.

- Branch off `master` and keep history linear (rebase, don't merge).
- Fork the repository, branch off `develop`, and open your pull request against `develop`. Keep history linear (rebase, don't merge). `master` is what production runs and only ever receives release merges from `develop`; a pull request into `master` from a fork or any other branch fails the `base-branch guard` check and cannot be merged.
- One logical change per commit; don't batch unrelated work.
- **No `Co-Authored-By` trailers.** This is a project policy.
- **Never `--no-verify`.** If a hook fails, fix the cause; don't bypass it.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ npm run api
| `ENCODER_MAX_CONCURRENT_REQUESTS` | No | `50` | Global cap on requests served at once across all client IPs; excess gets an immediate 429 + `Retry-After` instead of queueing. `/status` and `/openrpc.json` are exempt, over `GET` or `HEAD`, in any letter case and with or without a trailing slash; `0` disables |
| `ENCODER_MAX_CONCURRENT_PROBES` | No | `16` | Private concurrency reserve for the two exempt probe routes, so healthchecks stay answerable while the cap above sheds without becoming an uncapped bypass; `0` disables |
| `ENCODER_TRUST_PROXY` | No | `loopback, uniquelocal` | Express `trust proxy` setting; controls which hop the per-IP rate limiter keys the client IP on. `false`, a hop count, or an address/CIDR list per the Express docs |
| `ENCODER_MAINNET_PRELAUNCH_OVERRIDE` | No | Unset | On a mainnet encoder, `create_tx` refuses every action outside the consensus validator set before unix time `1798761600` with `-32010` and `data.reason` `MAINNET_PRELAUNCH` (`data.opens_at` carries the time). The set is read by name and format, exactly as the indexer's launch gate reads it: `ANCHOR` formats 0 to 3, `PRICE` format 0, `NODEPROOF` format 0, `ROLLCALL` formats 0 and 1, and the quorum `ATTEST` formats 1, 3, 4, 5 and 6. `PRICE` format 1, `ATTEST` formats 0 and 2, `STAKE`, `UNSTAKE`, `DELEGATE`, `SLASH` and every other action are refused. Set `1`, `true`, `yes` or `on` to lift the refusal for every action; read live on every call, any other value leaves it in force. Set it only on the foundation's own bootstrap encoder that builds the seed sources' pre-launch actions (the encoder cannot see the pinned seed sources), never on a public encoder. Testnet and regtest are never refused |
| `ENCODER_MAINTENANCE_FILE` | No | `/tmp/xchain-encoder-maintenance.json` | Where the encoder looks for an operator-declared scheduled-maintenance window. `health` and `GET /status` report it as `maintenance` beside the readiness fields, so a status board can tell a planned outage from a fault; it never changes a readiness field or the 503. See [Scheduled maintenance](#scheduled-maintenance) |
| `CORS_ORIGIN` | No | Disabled | Allowed CORS origin(s): `*` for any, one origin, or a comma-separated allowlist matched per-origin (browser wallet shells each send a different origin). A stray `*` inside a list is not a wildcard, so the grant fails closed |

Expand Down
6 changes: 3 additions & 3 deletions bin/pins/at1-identity.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"capturedAt": "2026-10-09T19:55:43.652Z",
"capturedAt": "2026-10-17T05:41:32.673Z",
"coins": {
"src/coins/BTC.js": "182baf5173e3846b94a5edc1ae15913a54fb266addfc45a3cf9a013bd9ec16c6",
"src/coins/BTC.js": "6868afc2d18911ba112234c980c094fda71cfc3e744a00f493def781c7610f8d",
"src/coins/LTC.js": "6155195bfa5fdfeab77dec3802ce67b5cc47a77695e81a4a5545d980e4c421dd",
"src/coins/DOGE.js": "4659b45fb7a5c959ce2777ab9d5e7c193f3515f03cdd8cd23bb6b9c4d012fc11",
"src/coins/DOGE.js": "29ac02299b0c779f4e3eda579eeaa9f673c454b73cfe9978a35f03f0e9aed33c",
"src/coins/index.js": "08b678d934fa647d3c7d14dc5ccdaeb86722ee32a3662de69e4a31cd7b09234c",
"src/coins/consensus_pin.js": "342bf7af224a19ea0a98ff9cfb2c9a70b3fd5be75de7ecef2db05936f82baa63"
},
Expand Down
38 changes: 29 additions & 9 deletions bin/pins/at1-suite-titles.json
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,9 @@
"Encoder input validator validateDataParam rejects a rawData code point above U+00FF (latin-1 would truncate it)",
"Encoder input validator validateDataParam still accepts every value the wire encodings carry losslessly"
],
"4d2fee8572bf0a2f": [
"BlockchainConnector.getFeePerKilobyte() test-chain implausible estimate uses the no-estimate fallback instead of clamping an estimate above the sanity ceiling"
],
"4dd0d30bb214ff45": [
"two-phase reveal package prefund @regression @tier1 MAX_CPFP_UPLIFT_SAT=0 turns the prefund off entirely",
"two-phase reveal package prefund @regression @tier1 adds nothing when the commit already pays the target rate on its own bytes",
Expand Down Expand Up @@ -1071,6 +1074,19 @@
"E2E-1: Full ACTION-to-PSBT Pipeline E2E-1.27: LIST comma-separated items encoded successfully",
"E2E-1: Full ACTION-to-PSBT Pipeline E2E-1.27: LIST full addresses auto-select P2SH"
],
"87462cdada9ac477": [
"BlockchainConnector getMempoolEntry returns null for an HTTP 200 RPC -5 mempool miss",
"BlockchainConnector getMempoolEntry returns null for an HTTP 500 RPC -5 mempool miss",
"BlockchainConnector getMempoolEntry sends getmempoolentry with the txid and returns the entry",
"BlockchainConnector getMempoolEntry throws a transport error instead of treating it as absent",
"BlockchainConnector getMempoolEntry throws an unexpected RPC error instead of treating it as absent",
"get_tx_status transaction location rejects a malformed txid before either lookup",
"get_tx_status transaction location returns chain after a mempool miss when get_tx_block finds the txid",
"get_tx_status transaction location returns mempool and does not ask the tracker when getmempoolentry finds the txid",
"get_tx_status transaction location returns unknown only when both the mempool and chain lookups miss",
"get_tx_status transaction location throws rather than returning unknown when the node lookup fails",
"get_tx_status transaction location throws rather than returning unknown when the tracker lookup fails"
],
"880c961e264c0519": [
"XChainEncoder TAPROOT envelope sign/finalize/extract round trip (commit -> reveal -> estimator accuracy) signs both halves; the signed commit txid matches the pre-built reveal outpoint"
],
Expand Down Expand Up @@ -1396,7 +1412,7 @@
"E2E-6: Multi-Chain Validation E2E-6.3: litecoin-regtest OP_RETURN creates valid OP_RETURN PSBT with correct dust threshold",
"E2E-6: Multi-Chain Validation E2E-6.7: Bitcoin P2WSH creates P2WSH witness output on bitcoin-regtest",
"E2E-6: Multi-Chain Validation E2E-6.8: Bitcoin MULTISIGN (dust=546) multisig output at 546 sats",
"E2E-6: Multi-Chain Validation E2E-6.9: Litecoin MULTISIGN (dust=5460) multisig output at 5460 sats",
"E2E-6: Multi-Chain Validation E2E-6.9: Litecoin MULTISIGN (dust=5460) multisig output at 7860 sats",
"E2E-6: Multi-Chain Validation Fee floor uses chain-specific dust Litecoin fee floor is 5460, not 546",
"E2E-6: Multi-Chain Validation bitcoin-regtest P2SH creates P2SH with output value >= chain dust threshold",
"E2E-6: Multi-Chain Validation dogecoin-regtest P2SH creates P2SH with output value >= chain dust threshold",
Expand Down Expand Up @@ -2216,7 +2232,7 @@
"REG-04: Validator Functions REG-04.7: validateDust() throws RangeError for negative value",
"REG-04: Validator Functions REG-04.7: validateDust() throws TypeError for non-integer string"
],
"fae26ed352b38e34": [
"e1b10cc67b1fee13": [
"BlockchainConnector.getFeePerKilobyte() does NOT fall back to the relayfee floor on mainnet",
"BlockchainConnector.getFeePerKilobyte() falls back to 10x the relayfee floor on testnet when estimatesmartfee has no data",
"BlockchainConnector.getFeePerKilobyte() falls back to the regtest default when relayfee is unavailable",
Expand All @@ -2229,10 +2245,10 @@
"BlockchainConnector.getFeePerKilobyte() rethrows transport errors",
"BlockchainConnector.getFeePerKilobyte() returns feerate when present (non-regtest)",
"BlockchainConnector.getFeePerKilobyte() sanity ceiling on the raw non-regtest estimate clamps a spiked mainnet estimate to the coin-default ceiling and logs loudly",
"BlockchainConnector.getFeePerKilobyte() sanity ceiling on the raw non-regtest estimate clamps a spiked testnet estimate using the dogecoin-scale default when NETWORK says dogecoin",
"BlockchainConnector.getFeePerKilobyte() sanity ceiling on the raw non-regtest estimate honours FEE_ESTIMATE_SANITY_CEILING as an override",
"BlockchainConnector.getFeePerKilobyte() sanity ceiling on the raw non-regtest estimate ignores a non-positive or unparseable override and keeps the coin default",
"BlockchainConnector.getFeePerKilobyte() sanity ceiling on the raw non-regtest estimate passes through an estimate under the ceiling unchanged",
"BlockchainConnector.getFeePerKilobyte() sanity ceiling on the raw non-regtest estimate treats a spiked testnet estimate as unavailable and uses the relayfee fallback",
"BlockchainConnector.getFeePerKilobyte() sends estimatesmartfee with the blocksNumber param (non-regtest)",
"BlockchainConnector.getFeePerKilobyte() still prefers the estimate over the relayfee floor on testnet when one exists",
"BlockchainConnector.getFeePerKilobyte() throws on testnet when neither an estimate nor a relayfee is available",
Expand Down Expand Up @@ -2298,13 +2314,14 @@
},
"scripts": {
"test": {
"fileCount": 135,
"titleCount": 1020,
"fileCount": 136,
"titleCount": 1031,
"files": {
"test/unit/adapters/evm_adapter.test.js": "2c605d4ec40ce8e5",
"test/unit/api/broadcast_tx_logging.test.js": "2f6d87eaf711d8a0",
"test/unit/api/cors_preflight.test.js": "78e833e8f04d5380",
"test/unit/api/encoder_stress_sweep.test.js": "358184203c4ccfba",
"test/unit/api/get_tx_status.test.js": "87462cdada9ac477",
"test/unit/api/health_serve_readiness.test.js": "99f46ad865902c51",
"test/unit/api/json_rpc_real_app.test.js": "7174ac9d6bdce059",
"test/unit/api/jsonrpc_body_guard.test.js": "58706b2a54fa0008",
Expand All @@ -2316,11 +2333,12 @@
"test/unit/blockchain_connector.test/03_get_transaction_hex.test.js": "845d953a12be6745",
"test/unit/blockchain_connector.test/04_send_raw_transaction.test.js": "e65ba2021038db9f",
"test/unit/blockchain_connector.test/05_send_raw_transaction_maxfeerate_retry.test.js": "b70b109181c9f1fc",
"test/unit/blockchain_connector.test/06_get_fee_per_kilobyte.test.js": "fae26ed352b38e34",
"test/unit/blockchain_connector.test/06_get_fee_per_kilobyte.test.js": "e1b10cc67b1fee13",
"test/unit/blockchain_connector.test/07_rpc_credential_log_sanitization.test.js": "72c9c5f2d2749796",
"test/unit/blockchain_connector.test/08_fee_estimate_rpc_error_detail.test.js": "2842761e884c6f3b",
"test/unit/blockchain_connector.test/09_get_block_count.test.js": "8034f6c2b890b23c",
"test/unit/blockchain_connector.test/10_sanitize_rpc_error_parity.test.js": "2883f0e9d248552e",
"test/unit/blockchain_connector.test/13_test_chain_implausible_estimate.test.js": "4d2fee8572bf0a2f",
"test/unit/build/apply_bufferutils_patch.test.js": "3eb6eaeabe786d34",
"test/unit/build/compression.test.js": "68868c37d76d5eb2",
"test/unit/build/compression.test/01_action_string_helpers.test.js": "f6cecc24770508b5",
Expand Down Expand Up @@ -2584,13 +2602,14 @@
}
},
"test:unit": {
"fileCount": 135,
"titleCount": 1020,
"fileCount": 136,
"titleCount": 1031,
"files": {
"test/unit/adapters/evm_adapter.test.js": "2c605d4ec40ce8e5",
"test/unit/api/broadcast_tx_logging.test.js": "2f6d87eaf711d8a0",
"test/unit/api/cors_preflight.test.js": "78e833e8f04d5380",
"test/unit/api/encoder_stress_sweep.test.js": "358184203c4ccfba",
"test/unit/api/get_tx_status.test.js": "87462cdada9ac477",
"test/unit/api/health_serve_readiness.test.js": "99f46ad865902c51",
"test/unit/api/json_rpc_real_app.test.js": "7174ac9d6bdce059",
"test/unit/api/jsonrpc_body_guard.test.js": "58706b2a54fa0008",
Expand All @@ -2602,11 +2621,12 @@
"test/unit/blockchain_connector.test/03_get_transaction_hex.test.js": "845d953a12be6745",
"test/unit/blockchain_connector.test/04_send_raw_transaction.test.js": "e65ba2021038db9f",
"test/unit/blockchain_connector.test/05_send_raw_transaction_maxfeerate_retry.test.js": "b70b109181c9f1fc",
"test/unit/blockchain_connector.test/06_get_fee_per_kilobyte.test.js": "fae26ed352b38e34",
"test/unit/blockchain_connector.test/06_get_fee_per_kilobyte.test.js": "e1b10cc67b1fee13",
"test/unit/blockchain_connector.test/07_rpc_credential_log_sanitization.test.js": "72c9c5f2d2749796",
"test/unit/blockchain_connector.test/08_fee_estimate_rpc_error_detail.test.js": "2842761e884c6f3b",
"test/unit/blockchain_connector.test/09_get_block_count.test.js": "8034f6c2b890b23c",
"test/unit/blockchain_connector.test/10_sanitize_rpc_error_parity.test.js": "2883f0e9d248552e",
"test/unit/blockchain_connector.test/13_test_chain_implausible_estimate.test.js": "4d2fee8572bf0a2f",
"test/unit/build/apply_bufferutils_patch.test.js": "3eb6eaeabe786d34",
"test/unit/build/compression.test.js": "68868c37d76d5eb2",
"test/unit/build/compression.test/01_action_string_helpers.test.js": "f6cecc24770508b5",
Expand Down
9 changes: 5 additions & 4 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ version: '3'
services:
xchain_encoder:
build: .
# tini as PID 1 so it, not node, reaps zombies left by healthcheck
# probes; node itself never reaps orphaned children. Dockerfile's
# exec-form node CMD stays PID 1's direct child and still gets SIGTERM
# straight from tini's forwarding, so that signal path is unchanged.
# Docker's own init as PID 1, on top of the image's tini ENTRYPOINT, the
# same tree xchain-node's `docker run --init` gives production:
# docker-init -> tini -> node. docker-init reaps zombies left by
# healthcheck probes (node never reaps orphaned children), and SIGTERM
# reaches node forwarded through both inits.
init: true
# The image bakes no .env (Dockerfile); the README's .env is handed to the
# container here instead.
Expand Down
20 changes: 19 additions & 1 deletion docs/openrpc.build.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ const METHODS = [
+ 'record ({commitTxid, commitVout, commitValue, commitAddress, internalPubkey, tapleafHash, '
+ 'controlBlock, revealFee}) that the wallet must persist durably BEFORE broadcasting the commit; '
+ 'sign both, broadcast commit then reveal. TAPROOT requires compressedPubKey (the envelope internal '
+ 'key), segwit-only inputs, and accepts payloads up to the 400,000-byte envelope ceiling.',
+ 'key), segwit-only inputs, and accepts payloads up to the 400,000-byte envelope ceiling. '
+ 'An unknown top-level param is refused with -32602 rather than ignored, as unknown options keys are.',
params: [
{ name: 'pubkey', required: true, schema: str('caller identity: sender address (base58 or bech32) or compressed public key hex (02/03 + 64 hex); uncompressed (04...) keys are rejected') },
{ name: 'data', schema: str('ACTION payload string (pipe-delimited, from xchain-sdk createAction)') },
Expand Down Expand Up @@ -119,6 +120,23 @@ const METHODS = [
params: [{ name: 'address', required: true, schema: str('address to query') }],
result: { name: 'utxos', schema: { type: 'array', items: { type: 'object' } } },
},
{
name: 'get_tx_status',
summary: 'Classify a transaction as present in the node mempool, confirmed on chain, or unknown.',
description: 'Checks the coin node mempool first, then the UTXO tracker chain index. '
+ 'A lookup failure is an error and is never reported as an unknown transaction.',
params: [{ name: 'txid', required: true, schema: str('transaction id (64-character hexadecimal string)') }],
result: {
name: 'transaction_status',
schema: {
type: 'object',
properties: {
status: str('transaction location', { enum: ['mempool', 'chain', 'unknown'] }),
},
required: ['status'],
},
},
},
{
name: 'get_tx_block',
summary: 'Locate the confirmed block containing a transaction (proxied from xchain-utxo-tracker).',
Expand Down
Loading
Loading