Skip to content

Fix/cr 1.4.2 - #19

Open
SlavaSereb wants to merge 61 commits into
mainfrom
fix/cr-1.4.2
Open

Fix/cr 1.4.2#19
SlavaSereb wants to merge 61 commits into
mainfrom
fix/cr-1.4.2

Conversation

@SlavaSereb

Copy link
Copy Markdown
Collaborator

No description provided.

SlavaSereb added 30 commits June 2, 2026 14:06
Add security middleware with configurable settings via environment variables:

- CORS: Cross-origin resource sharing with configurable allowed origins
- Rate limiting: Configurable window and max requests (default: 100/15min)
- Body size limits: Configurable max request body size (default: 1mb)
- Optional API key authentication: When enabled, requires X-API-Key header

All settings are optional with sensible defaults. Public endpoints
(/health, /api-docs, /docs) bypass authentication when enabled.

New files:
- src/middleware/security.ts: Security middleware implementation

Modified files:
- src/server.ts: Apply security middleware after express.json()
- .env.example: Document new security environment variables
- package.json: Add cors and express-rate-limit dependencies

Resolves: #1
Add bounds validation to prevent loss-of-funds from excessive fees:

- MIN_PROTOCOL_FEE: 155,381 lovelace (Cardano protocol minFeeB)
- MAX_REASONABLE_FEE: 10,000,000 lovelace (10 ADA)

The 10 ADA upper bound is sufficient for even complex multi-asset
transactions while protecting against accidental high fee values
that could drain funds.

Applied to all schemas accepting optional fee parameter:
- delegateToDRepRequestSchema
- registerAsDRepRequestSchema
- castVoteRequestSchema

Uses a reusable optionalFeeSchema for consistency.
Add support for overriding Cardano protocol parameters at SDK initialization
to handle hard fork changes without requiring an SDK update.

New ProtocolParams interface with configurable values:
- minFeeA: Fee coefficient (lovelace per tx byte)
- minFeeB: Fee constant (base lovelace fee)
- coinsPerUtxoByte: For min-ada calculations
- stakeKeyDeposit: Stake key registration deposit
- drepDeposit: DRep registration deposit

Usage:
  const sdk = await FireblocksCardanoRawSDK.createInstance({
    ...config,
    protocolParams: { minFeeA: 44, minFeeB: 155381 }
  });

New files:
- src/utils/protocolParams.ts: Protocol params singleton with get/set/reset

Modified files:
- src/types/config.ts: Add ProtocolParams interface
- src/FireblocksCardanoRawSDK.ts: Accept protocolParams in createInstance
- src/api/validation.ts: Use configurable minFeeB for fee validation
- src/utils/index.ts: Export protocolParams functions
- .env.example: Document protocol param environment variables
Previously, fetchUtxos silently returned empty arrays when:
- Network errors occurred (catch block)
- API returned success=false

This masked real errors, making debugging difficult and causing
downstream code to incorrectly assume the address had no UTXOs.

Now throws SdkApiError with:
- Descriptive error message including the address
- Error type: UTXO_FETCH_ERROR
- Service: iagon-api

Empty UTXO results from a successful API call (address has no UTXOs)
still return [] as this is a valid state.
Add documentation about the in-memory UTXO locking mechanism and its
limitations for multi-replica deployments.

Key points documented:
- UTXO locks are process-local (in-memory)
- Multi-replica deployments may experience double-spend attempts
- Mitigation strategies: sticky sessions, external locking, single replica

New section "Deployment Considerations" added before Security Considerations
with code example showing automatic UTXO locking behavior.
Previously, staking and governance operations did not use the UTXO
locking mechanism, allowing concurrent operations on the same vault
to potentially select the same UTXO (double-spend risk).

Changes:
- UtxoProvider.findAddressWithSuitableUtxo now:
  - Filters out already-locked UTXOs before selection
  - Locks the selected UTXO and returns a release function
- All staking/governance operations now release UTXO locks in finally blocks
- AddressWithUtxo interface includes a release() function
- Added lockOne() helper to UtxoLockManager for single-UTXO locking

Operations updated:
- registerStakingCredential
- delegateToPool
- deregisterStakingCredential
- withdrawRewards
- delegateToDRep
- registerAsDRep
- castVote

Locks auto-expire after UTXO_LOCK_TTL_MS (2 minutes) as a safety net.
- Add security configuration section documenting CORS, rate limiting,
  and API key authentication setup with examples
- Add protocol parameters configuration section explaining how to
  override Cardano protocol params at SDK initialization
- Update environment variables table with all security-related vars
  (MAX_BODY_SIZE, RATE_LIMIT_*, CORS_ORIGINS, API_KEY_*)

Related to fixes #1 (security middleware) and #5 (protocol params)
- Runs lint, typecheck, and build on every push to main and PR
- Runs E2E staking info test (read-only) after build passes
- Runs E2E ADA transfer test (3 ADA between vault 0 and 1)
- PRs to main will be blocked if CI fails

This addresses review issue #3 by adding automatic CI while keeping
the manual workflow_dispatch tests for more complex scenarios.
Mount /api/webhook with express.raw() so the raw request body is preserved
and signature verification runs before any JSON parsing. The webhook handler
now verifies the Fireblocks signature against the raw buffer and only
parses JSON after successful verification, ensuring untrusted payloads are
never deserialized prior to authentication.
resolveRecipientAddress now parses recipientAddress with
Address.from_bech32 and rejects malformed bech32 strings, then checks
that the embedded network id matches the SDK's configured network
(mainnet=1, preprod=0). This prevents accidental cross-network sends and
surfaces a clear validation error instead of a downstream parse failure.
Convert netAmount and feeAmount to BigInt before passing them to cbor2 in
buildPayload. JS Number only safely represents integers up to 2^53; on-chain
lovelace amounts can in theory exceed that bound. Using BigInt forces cbor2
to emit CBOR unsigned integers (major type 0) without precision loss.
Add isSpendableUtxo helper that rejects UTxOs carrying datum_hash or
script_hash, and apply it alongside the existing utxo-lock filter in
fetchAndSelectUtxosForCnt / Ada / MultiToken and in the staking UTxO
provider. These outputs require a Plutus or native-script witness and
are unspendable with the Ed25519 keys held in Fireblocks vaults.
Conway CDDL defines gov_action_id.gov_action_index as uint .size 2.
Cap the value at 65535 in the cast-vote request schema so out-of-range
indices are rejected at the API boundary instead of producing an
invalid CBOR transaction body.
Replace the bare regex check on drepId with a bech32-aware validator that
decodes drep1.../drep_script1... strings, verifies the checksum, and
confirms the payload is exactly 28 bytes (the credential length).
Hex form keeps the existing 56-char check.
Move the recipient-address parse/network-id check out of the private
SDK method and into assertRecipientAddress in utils/cardano.ts so it
can be unit-tested directly. Behaviour is unchanged; the SDK now calls
the helper through its existing resolveRecipientAddress chokepoint.
Cover the behaviour introduced by the M-01..M-08 commits:
 - M-01: webhook handler verifies signature against the raw Buffer
   before parsing JSON; rejects non-buffer/empty bodies, bad signatures
   and signature-valid-but-malformed JSON.
 - M-02: assertRecipientAddress accepts matching-network addresses and
   rejects malformed bech32 / cross-network addresses with descriptive
   ValidationError info.
 - M-04: buildPayload emits CBOR major-type-0 uint bytes for lovelace
   and fee, including the 8-byte form for values above 2^32 and proper
   rounding of fractional inputs.
 - M-06: isSpendableUtxo excludes UTxOs with datum_hash or script_hash.
 - M-07: governanceActionId.index rejects values above uint16 max.
 - M-08: DRep ID validation accepts hex and bech32, rejects bad
   checksums, wrong-length payloads and unknown HRPs.

Switched the test script to node --experimental-vm-modules so Jest can
load cbor2 (ESM-only) when running the new buildPayload tests.
handleError previously echoed SdkApiError.errorInfo and .service back to
the API consumer, leaking internal context (queried vault account ids,
upstream service names, dependency metadata). Keep both fields in the
server-side log only; the public JSON now exposes just success, error
message, statusCode and error type.
… last estimate

Previously convergeTransactionFee logged a warning and returned the last
currentFee if the loop did not converge within TX_FEE_MAX_ITERATIONS.
That value can be below the actual minimum fee for the resulting tx
body, leaving callers to build and submit an underpaid transaction that
the network rejects (or, worse, that silently differs from the declared
fee). Throw an SdkApiError on non-convergence so the failure surfaces
immediately and the caller can retry or escalate.
encodeDRepId previously used HRP "drep_script" for script-based DReps,
which is non-standard: per CIP-129 the HRP is always "drep" and the
type is encoded in the leading header byte (0x22 key, 0x23 script).
decodeDRepId required a 29-byte payload, while the request validator
required 28 bytes, so legitimate CIP-129 inputs were rejected at the
API edge while legitimate CIP-105 inputs failed inside the SDK.

After this change:
 - encodeDRepId emits CIP-129 (HRP=drep, 29 bytes).
 - decodeDRepId accepts CIP-129 (29 bytes, header-tagged) AND CIP-105
   legacy (28 bytes, HRP-tagged: drep / drep_script).
 - isValidDrepId mirrors decodeDRepId: hex, CIP-105 (28B), or CIP-129
   (29B with HRP=drep only). 29-byte payloads with HRP=drep_script
   are rejected as malformed.
Add CardanoConstants.MAX_TX_SIZE_BYTES (16384) and assertTxSizeWithinLimit
in utils/cardano. Call it from FireblocksCardanoRawSDK.signTransaction
and from StakingService just before forwarding to Fireblocks, so a
transaction body that would exceed the protocol maxTxSize is rejected
with a TxSizeLimitExceeded error instead of consuming a signing quota
and being refused by the node.
When an exception was not an SdkApiError, handleError surfaced the raw
error.message back to the API consumer, which can leak stack-derived
details, file paths, internal identifiers, or upstream failure messages.
Log the full message and stack server-side and return a generic
Internal server error response with a stable type discriminator.
fetchAllVaultHistory previously made one history call per address and
treated whatever subset the API chose to return as the complete result.
For accounts with more than one page of history, getAllTransactionHistory
and getAllDetailedTxHistory silently dropped older transactions.

Introduce fetchAllPagesForAddress which walks the upstream pagination
(hasMore + short-page fallback) until the stream ends, with a hard cap
of 1000 pages * 100 rows = 100k transactions per address. When the
caller supplies an explicit limit, the original single-page behaviour
is preserved so existing query shapes keep working.
StakingValidator.checkDelegationPrerequisites previously fetched
getPoolInfo and discarded the response. Now it inspects the result and
throws PoolNotFound (404) when the pool does not exist or PoolRetired
(400) when the pool has at least one retirement certificate filed,
preventing delegation transactions that the chain would either reject
or that would be effectively wasted on a soon-to-be-retired pool.
IagonApiService hardcoded the upstream base URL at the class field
level via the iagonBaseUrl module constant, leaving no override path
for staging or private Iagon deployments. The URL is now resolved at
construction time from (in order): an explicit constructor arg, the
IAGON_BASE_URL environment variable, then the built-in default.
FireblocksCardanoRawSDK.createInstance forwards an optional
iagonBaseUrl param so library consumers can override without touching
the environment. Documented in README and .env.example.
signTransaction allocated PublicKey, Vkey, Ed25519Signature, Vkeywitness,
Vkeywitnesses, TransactionWitnessSet and Transaction handles from the
cardano-serialization-lib WASM module but did not free them on early
returns or thrown exceptions. The returned Transaction was also never
freed by any caller after submission. Each Fireblocks signing request
therefore leaked a handful of WASM allocations.

After this change:
 - signTransaction holds every intermediate handle in named locals and
   releases them in a try/catch around the success path, so a throw at
   any stage frees everything that was allocated up to that point.
 - All four call sites (CNT transfer, multi-token transfer, single-batch
   consolidation, batched consolidation) wrap submitTransaction in a
   try/finally that calls signedTransaction.free() once the CBOR has
   been serialized for the upstream POST.
The repo only ships a Dockerfile - there is no docker-compose.yml - so
README instructions telling users to run docker-compose up/logs/down/
restart all fail. Replace the docker-compose snippets with equivalent
plain docker run / docker logs / docker stop / docker restart commands
and remove docker-compose.yml from the project-tree listing.
SdkManager.getSdk read from sdkPool, and on a miss awaited
sdkFactory(...) and then stored the result. Two requests for the same
uncached vaultAccountId arriving on the same event-loop tick both pass
the cache check, each call the factory, and the second factory result
overwrites the first - the orphaned SDK keeps its underlying connections
open with no future getSdk hit to reuse it.

Add a pendingSdkCreations map keyed by vaultAccountId. On cache miss,
the first caller registers its in-flight Promise and runs the factory;
concurrent callers for the same key await that Promise instead of
starting their own. The entry is removed in a finally block so a failed
creation does not pin the key. useCount is incremented per caller that
joins, which also fixes the previous off-by-one where the first caller's
write reset useCount to 1.

Adds src/__tests__/pool/sdkManager.test.ts covering: shared instance
across 8 concurrent waiters, distinct allocations for distinct keys,
and recovery after a rejected in-flight creation.
Add a unit-tests job that runs npm test (302 tests across 9 suites) in
parallel with lint-and-build. Both e2e jobs (staking-info, ada-transfer)
now depend on it via needs: [lint-and-build, unit-tests] so a unit-test
regression skips the mainnet e2e runs instead of burning real funds.
submitTransfer now throws SdkApiError(TX_SUBMIT_REJECTED) carrying the
upstream Iagon error string. submitTransaction propagates SdkApiError
unchanged so the staking-service IncompleteWithdrawals retry detector
finally sees the substrings it matches on.
checkRegistrationStatus caught every exception and returned false. An
Iagon outage during deregistration was indistinguishable from a
genuinely unregistered stake key, causing the SDK to skip the
deregistration silently and leave the user's 2 ADA deposit locked.
Now only an SdkApiError with statusCode 404 maps to false; everything
else propagates to the caller.
…built CBOR

Route all hand-built CBOR coins through a guarded toCoinBigInt helper that rejects NaN, negative, and unsafe-integer values before signing. Type withdrawal maps as WithdrawalMap (bigint values). Add the audit-prescribed >2^53 CBOR encoding tests, which also cover the withdrawal (body key 5) encoding until rewards accrue for on-chain verification.
@socket-security

socket-security Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​@​types/​cors@​2.8.191001009180100
Addednpm/​cors@​2.8.610010010084100
Addednpm/​express-rate-limit@​8.6.19910010093100

View full report

SlavaSereb and others added 2 commits August 4, 2026 10:29
Ports the audit follow-up remediations onto the 1.4.x line, reconciled with
this branch's parallel work (spendable-UTxO filtering, staking-path UTxO
locking, CIP-129 DRep decoding, error-propagating fetchUtxos, protocol-param
constants).

- S-6: guard full UTxO-payload / CBOR logging behind DEBUG (eager args).
- S-3: assert single-output value size <= MAX_VALUE_SIZE before signing.
- S-7: size-aware min-fee floor (assertFeeCoversSize) on the raw staking/gov
  path, enforced before signing.
- M-13: client-facing throws -> typed SdkApiError (4xx), incl. the CIP-129
  DRep validation throws, with rethrow guards in catch-all wrappers.
- OC-2: batched consolidation locks each batch, filters spendable+locked UTxOs,
  and confirms spends on-chain (waitForUtxosSpent) instead of a fixed delay.
- S-4: aggregate multiple pure-ADA UTxOs from one address (spendable-filtered)
  to fund staking/gov deposits, atomically locking the whole set; add
  FRAGMENTED_PURE_ADA error for cross-address fragmentation.
- M-14: fetchAllPages() + getAll* variants; rewards/history return all pages.

Hardening carried over from review:
- UTXO_LOCK_TTL_MS (240s) >= TX_CONFIRM_TIMEOUT_MS invariant (+ test).
- assertOutputMeetsMinUtxo(): reject below-min change output before signing.

AddressWithUtxo now carries the aggregated utxos[] + totalAmount and a
release() for the held lock; TransactionBuildContext takes utxos[].

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sets the workflow_dispatch default network to mainnet for the ADA-transfer and
staking e2e workflows (the token-transfer workflow and test scripts already
carry the basePath e2e updates on this branch).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants