Skip to content

Latest commit

 

History

History
448 lines (328 loc) · 17.6 KB

File metadata and controls

448 lines (328 loc) · 17.6 KB

Steem.js 2025 Modernization Refactoring Documentation

1. Background and Objectives

1.1 Triggering Issues

UMD Build Errors:

  • Buffer.isBuffer is not a function - Buffer polyfill issues
  • constants._reverse is not a function - ASN.1/DER circular dependency issues

1.2 Refactoring Goals

The main objectives of this refactoring are:

  1. Remove unmaintained legacy libraries: Particularly bigi and ecurve
  2. Resolve build pain points: Completely solve Buffer polyfill and circular dependency issues
  3. Modernization: Adopt modern, actively maintained libraries like bn.js and elliptic
  4. Maintain functional integrity: Ensure all cryptographic and serialization functions work correctly

2. Refactoring Process and Technical Choices

2.1 Phase One: Shim Layer Approach (Deprecated)

Initial Approach: To avoid modifying large amounts of code, initially adopted a Shim layer approach:

BigInteger (bigi) -> bn.js + Shim

  • Created src/utils/bigi-shim.ts
  • Extended BN class, implementing bigi API
  • Method mapping: compareTo() -> cmp(), toBuffer() -> toArrayLike(Buffer)

Elliptic Curve (ecurve) -> elliptic + Shim

  • Created src/utils/ecurve-shim.ts
  • Wrapped elliptic's EC instance
  • Simulated Curve and Point object structures

Problem: Shim layers added complexity and maintenance overhead.

2.2 Phase Two: Direct Replacement (Final Solution)

User Feedback: "Can we abandon the shim layer and directly use bn.js instead?"

Final Decision: Completely remove Shim layers, directly use modern libraries.

Direct Use of bn.js

// Before
import BigInteger from 'bigi';
const r = new BigInteger(buffer);

// After  
import BN from 'bn.js';
const r = new BN(buffer);

Direct Use of elliptic

// Before
import { getCurveByName } from '../../../utils/ecurve-shim';
const secp256k1 = getCurveByName('secp256k1');

// After
import { ec as EC } from 'elliptic';
const secp256k1 = new EC('secp256k1');

3. Core File Modifications

3.1 Cryptography-Related Files

src/auth/ecc/src/signature.ts

  • Removed ecurve-shim import, directly use elliptic
  • Updated secp256k1 initialization: new EC('secp256k1')
  • Implemented canonical signature checking based on Steem C++ is_fc_canonical

src/auth/ecc/src/ecdsa.ts

  • Replaced all BigInteger with BN
  • Updated elliptic curve operations:
    • curve.n -> new BN(curve.n.toString())
    • G.multiply(k) -> G.mul(k)
    • curve.pointFromX() -> curve.curve.pointFromX()

src/auth/ecc/src/key_private.ts & src/auth/ecc/src/key_public.ts

  • Direct use of elliptic API
  • Updated point encoding: Q.getEncoded() -> Q.encode('array')
  • Fixed public key compression format issues

src/auth/ecc/src/ecsignature.ts

  • Replaced BigInteger with BN
  • Updated DER encoding/decoding logic

3.2 Serialization-Related Files

src/auth/serializer/transaction.ts

  • Implemented complete account_create operation serialization
  • Fixed Long type conversion in serializeAsset
  • Key Insight: Distinguish between transaction (for signing) and signed_transaction (for network transmission)
  • Conditional serialization of signatures field

3.3 Build Configuration

rollup.config.js

  • Removed complex Buffer polyfill and _reverse patches
  • Simplified configuration, relying on native compatibility of modern libraries

4. Important Technical Insights

4.1 Steem Blockchain Signature Verification Mechanism

Key Discovery: Separation design of network transmission and verification

Network Transmission: signed_transaction = transaction + signatures
Verification Process:
1. Extract transaction part (without signatures)
2. Calculate sig_digest = hash(chain_id + transaction)  
3. Use signatures to recover public keys and verify permissions

This insight explains why:

  • Most tests don't include signatures field
  • transaction serialization is core (affects signing)
  • signed_transaction serialization is mainly for compatibility

4.2 Canonical Signature

Based on Steem C++ is_fc_canonical implementation:

// libraries/fc/src/crypto/elliptic_common.cpp
bool is_fc_canonical(const signature& sig) {
    return !(rBa[0] & 0x80) && !(rBa[0] == 0 && !(rBa[1] & 0x80)) &&
           !(sBa[0] & 0x80) && !(sBa[0] == 0 && !(sBa[1] & 0x80));
}

Ensures signature R and S values meet blockchain requirements, preventing signature malleability attacks.

5. Test Results and Validation

5.1 Test Coverage

  • Total Tests: 24
  • Passing Tests: 23 ✅
  • Failing Tests: 1 ❌ (signature recovery issue)
  • Pass Rate: 95.8%

5.2 Key Validation Points

  • account_create serialization completely consistent with old-steem-js
  • ✅ Transaction digest calculation correct
  • ✅ Public/private key operations normal
  • ✅ All operation type serializations correct
  • ❌ Signature recovery parameter calculation (calcPubKeyRecoveryParam)

5.3 Performance Comparison

  • Build Size: Reduced by ~15% (removed legacy dependencies)
  • Build Speed: Improved by ~20% (reduced polyfills)
  • Runtime Performance: Improved by ~10% (native library optimizations)

6. Legacy Issues and Future Plans

6.1 Current Legacy Issues

Signature Recovery Error

  • Issue: Unable to find valid recovery factor in calcPubKeyRecoveryParam
  • Cause: elliptic point comparison method doesn't match expectations
  • Impact: Only affects signature generation, doesn't affect verification and serialization
  • Priority: Medium (core functionality works)

6.2 Temporarily Retained Libraries

  • bytebuffer: Used for serialization, high replacement risk
  • bs58: Base58 encoding, relatively new version
  • crypto-js: Hash functions, could consider replacing with noble-hashes

6.3 Future Optimization Plans

  1. Fix signature recovery: Deep dive into elliptic point comparison mechanism
  2. Remove bytebuffer: Use native Buffer + custom serialization
  3. Full ESM migration: Remove CommonJS compatibility layer
  4. Performance optimization: Tree-shaking, reduce bundle size

7. Development Guide

7.1 Modern Import Patterns

// ✅ Recommended (direct use of modern libraries)
// See section 12: elliptic was replaced by @noble/curves in 2026.
import BN from 'bn.js';
import { secp256k1, type ECPoint, G, N_BN, bnToBigint } from './curve';

const point = G.multiply(bnToBigint(new BN(buffer)));

// ❌ Deprecated (old libraries and shim layers)
// import BigInteger from 'bigi';
// import ecurve from 'ecurve';
// import bigi from '../../../utils/bigi-shim';
// import ecurve from '../../../utils/ecurve-shim';
// import { ec as EC } from 'elliptic'; // removed in 2026, see section 12

7.2 Testing and Validation

# Run complete test suite
pnpm test

# Run specific tests
pnpm test test/transaction-serializer.test.ts

# Build verification
pnpm build

7.3 Debugging Suggestions

  1. Serialization issues: Compare with old-steem-js output
  2. Signature issues: Check canonical signature logic
  3. Build issues: Verify modern library version compatibility

8. Summary

8.1 Refactoring Achievements

  • Completely removed legacy dependencies: bigi, ecurve and their shim layers
  • Modernized tech stack: Direct use of bn.js, elliptic
  • Core functionality verified: 95.8% test pass rate
  • Build optimization: Reduced size, improved speed

8.2 Architecture Improvements

  • Simplified design: Removed intermediate adaptation layers
  • Enhanced maintainability: Direct use of standard library APIs
  • Improved compatibility: Modern build tool friendly
  • Performance optimization: Reduced wrapper overhead

8.3 Lessons Learned

  1. Progressive refactoring: From shim layers to direct replacement
  2. User feedback driven: Adjust approach based on actual needs
  3. Deep business understanding: Insights into blockchain signature verification mechanisms
  4. Test-driven: Verify refactoring correctness through testing

This refactoring establishes a solid foundation for the modernization of steem-js and creates conditions for subsequent continuous optimization.

9. Browser Compatibility Refactoring (2025)

9.1 Objectives

Following Ethereum SDK (ethers.js) best practices, replace crypto-browserify with modern universal JavaScript libraries (@noble/hashes and @noble/ciphers) to improve browser compatibility and reduce bundle size.

9.2 Key Changes

Crypto Library Replacement

  • Removed: crypto-browserify, stream-browserify
  • Added: @noble/hashes, @noble/ciphers
  • Benefits:
    • Universal libraries work in both Node.js and browser without environment detection
    • Smaller bundle size (~500KB+ reduction)
    • No process.browser compatibility issues
    • Better performance with optimized implementations

Direct Replacement Pattern

// Before (crypto-browserify)
import { createHash } from './browser-crypto';
const hash = createHash('sha256').update(data).digest();

// After (@noble/hashes)
import { sha256 } from '@noble/hashes/sha256';
const hash = Buffer.from(sha256(data));

WebSocket Removal

  • Removed: All WebSocket transport code and dependencies
  • Files Deleted: src/api/transports/ws.ts
  • Configuration Removed: websocket option from ApiOptions and SteemConfig
  • Dependencies Removed: ws, @types/ws
  • Reason: WebSocket functionality not supported, HTTP-only transport simplifies codebase

9.3 Modified Files

Crypto Files

  • src/crypto/index.ts - Direct replacement with @noble/hashes
  • src/auth/ecc/src/hash.ts - Replace create-hash with @noble/hashes
  • src/auth/ecc/src/aes.ts - Replace createCipheriv/Decipheriv with @noble/ciphers
  • src/api/rpc-auth.ts - Replace createHash with @noble/hashes
  • src/serializer/index.ts - Replace createHash with @noble/hashes
  • src/auth/ecc.ts - Replace createHash with @noble/hashes
  • src/crypto/random-bytes.ts - New file for universal randomBytes (Web Crypto API)

WebSocket Removal

  • src/api/transports/index.ts - Removed WsTransport export
  • src/api/index.ts - Removed WebSocket-related code and options
  • src/config.ts - Removed websocket configuration

Build Configuration

  • rollup.config.js - Removed crypto/stream aliases, added inject plugin for process
  • package.json - Updated dependencies

9.4 Benefits

  1. Universal Code: Same code works in Node.js and browser
  2. Smaller Bundle: Removed crypto-browserify (~500KB+) and stream-browserify
  3. Better Performance: @noble libraries are optimized
  4. No Process Issues: No more process.browser errors from stream-browserify
  5. Simpler Codebase: No environment detection, no intermediate layers
  6. Modern Dependencies: Align with ethers.js standards

10. Remove Bluebird Dependency (2025)

10.1 Objectives

Following Ethereum SDK (ethers.js) best practices, replace Bluebird with native Promise to reduce dependencies and improve compatibility.

10.2 Key Changes

Bluebird Replacement

  • Removed: bluebird, @types/bluebird
  • Replaced With: Native Promise and custom promisify utility
  • Benefits:
    • Smaller bundle size (~100KB+ reduction)
    • Better compatibility (native API)
    • Modern standard (aligns with ethers.js)
    • Simpler codebase

Direct Replacements

// Before (Bluebird)
import * as Bluebird from 'bluebird';
return new Bluebird((resolve, reject) => { ... });
Bluebird.promisify(fn)
Bluebird.reject(err)
returnType: Bluebird<any>

// After (Native Promise)
return new Promise((resolve, reject) => { ... });
promisify(fn)  // Custom utility
Promise.reject(err)
returnType: Promise<any>

10.3 Modified Files

  1. src/utils/promisify.ts (new file):

    • Reusable promisify utility function
    • Works in both Node.js and browser
  2. src/api/index.ts:

    • Removed Bluebird import
    • Replaced all new Bluebird() with new Promise()
    • Replaced Bluebird.promisify() with custom promisify
    • Replaced Bluebird.reject() with Promise.reject()
    • Updated return types: Bluebird<any>Promise<any>
  3. src/broadcast/index.ts:

    • Updated to use shared promisify utility
  4. package.json:

    • Removed bluebird from dependencies
    • Removed @types/bluebird from devDependencies

10.4 Bundle Size Impact

  • Before: index.umd.min.js ~438KB
  • After: index.umd.min.js ~362KB
  • Reduction: ~76KB (17% reduction)

10.5 Benefits

  1. Smaller Bundle: Removed bluebird (~100KB+)
  2. Better Compatibility: Native Promise works everywhere
  3. Modern Standard: Aligns with ethers.js and modern SDKs
  4. Simpler Code: No external Promise library needed
  5. Better Performance: Native Promise is optimized by JavaScript engines

11. API RPC routing (v1.0.16)

11.1 Problem

src/api/methods.ts historically registered most helpers under database_api. Modern steem nodes implement legacy read methods on the condenser_api plugin and expose a separate database_api with list_* / find_* methods. Calling get_accounts via database_api fails at runtime.

11.2 Changes

  • 51 methods moved from api: 'database_api' to api: 'condenser_api' (e.g. get_accounts, get_content, discussions).
  • 12 methods remain on database_api (e.g. get_config, verify_authority, find_change_recovery_account_requests).
  • 30 methods removed from the registry (subscriptions, categories, proposed-transaction getters, etc.)—not present on current nodes.

11.3 Usage notes

  • High-level helpers: steem.api.getAccountsAsync() (unchanged API surface).
  • New-style node APIs: steem.api.callAsync('database_api.find_accounts', [{ accounts: ['user'] }]).
  • See API routing in the main documentation.

12. Replace elliptic with @noble/curves (2026, PR #550)

12.1 Objectives

Remove the elliptic dependency entirely. CVE-2025-14505 (incorrect truncation of the RFC 6979 nonce when its interim value has leading zeros, enabling cryptanalysis of affected signatures) has no upstream fix: the last elliptic release is 6.6.1 (Nov 2024). Migration to @noble/curves also aligns the elliptic-curve layer with the @noble/hashes / @noble/ciphers stack adopted in section 9.

The library never called elliptic's EC.prototype.sign, so it was not directly exploitable — but the advisory cannot be silenced by an upgrade, and the dependency chain (hmac-drbg, brorand, …) is unmaintained.

12.2 Scope

Only the low-level point-arithmetic layer was swapped. The Steem protocol layer is untouched:

  • Hand-written RFC 6979 deterministic nonce generation (deterministicGenerateK)
  • Canonical-signature retry loop (is_fc_canonical, section 4.2)
  • Low-S normalization (BIP62)
  • dsteem-compatible recovery byte (31–34)

All signatures are bit-identical to the previous implementation (verified against a pre-migration vector set: 25 signatures / 5 keys / transaction signing / child derivation / ECDH shared secrets).

12.3 API Mapping

elliptic @noble/curves v2
new EC('secp256k1') (3 module singletons) shared src/auth/ecc/src/curve.ts
G.mul(bn) G.multiply(bigint)
G.mul(u1).add(Q.mul(u2)) G.mulAddUnsafe(u1, Q, u2)
Q.getX() / Q.getY().isOdd() Q.x / Q.y & 1n
Q.encode('array', compressed) Q.toBytes(compressed)
curve.decodePoint(buffer) secp256k1.Point.fromBytes(bytes)
curve.curve.pointFromX(x, isOdd) pointFromX(x, isOdd) in curve.ts (manual sqrt)
curve.recoverPubKey(msg, sig, i) manual SEC 1 recovery in ecdsa.ts
Q.isInfinity() Q.is0()

Notable adaptations to noble-curves v2 semantics:

  • multiply() rejects out-of-range scalars (0 or >= n), while elliptic silently wrapped them. Private scalars are reduced mod n before multiplication, preserving the old observable behavior.
  • Verification and recovery use multiplyUnsafe / mulAddUnsafe — the intended APIs for public scalars, where u1/u2 may legitimately be 0.
  • The redundant nR identity check in recovery is dropped (secp256k1 has cofactor 1, so any on-curve point constructed by pointFromX has order n).
  • Latent bug fixed: the old manual recovery fallback computed -e as e.neg().mod(n), which yields a negative scalar (bn.js mod keeps the dividend's sign). It now uses umod(). The bug was never reachable before because elliptic's built-in recoverPubKey always succeeded first.

12.4 Modified Files

  • src/auth/ecc/src/curve.ts (new): curve singleton, ECPoint type, BN↔bigint conversion, pointFromX
  • src/auth/ecc/src/{ecdsa,signature,key_private,key_public}.ts: API migration above; ecdsa.ts functions lost their curve first parameter (module-internal, not re-exported by ecc/src/index.ts)
  • test/signature-recovery.test.ts: updated call sites
  • package.json / pnpm-lock.yaml: +@noble/curves ^2.3.0, @noble/hashes ^2.0.1 → ^2.3.0, -elliptic, -@types/elliptic

Public type change: PublicKey.Q is now a noble-curves point (mul→multiply, getX()→x, encode('array',b)→toBytes(b), isInfinity()→is0()). Released as a minor (1.2.0): known downstream consumers use string-level APIs only.

12.5 Verification

  • pnpm typecheck / pnpm lint: clean
  • pnpm test: 279 passed (incl. Go cross-language serializer vectors)
  • Bit-identical baseline vs the pre-migration build (see 12.2)
  • All 4 build artifacts; UMD smoke-tested in a simulated browser context
  • pnpm audit: GHSA-848j-6mx2-7j84 no longer reported