📢 @steemit/[email protected] — Server-side transaction signature verification is finally usablesteemCreated with Sketch.

in #steem3 days ago

Release channel: next (dist-tag @next on npm)
Version: 1.0.20
Date: 2026-07-15
Install: npm install @steemit/steem-js@next

Note: the latest tag still points to the legacy 0.8.0. The 2025 rewrite
ships under the next tag, so use @steemit/steem-js@next (or pin
@steemit/[email protected]) to get this release.


Headline

steem.auth.verifyTransaction is fixed and steem.auth.serializeTransaction is
now exported.
Wallets, relay services, and backends can finally perform
cryptographic verification that a signed transaction was produced by the claimed
account's key — before forwarding it to the chain.

What was broken

steem.auth.verifyTransaction() verified signatures against
Buffer.from(JSON.stringify(transaction)). But signTransaction signs over the
binary digest sha256(chain_id ‖ serializeTransaction(trx)). The two never matched,
so verifyTransaction returned false for every legitimately-signed
transaction — the function was completely unusable.

This blocked any downstream service from doing real server-side signature
verification; the only check available was transaction-shape validation, with
the actual cryptographic check deferred to chain-level rejection on broadcast.

What changed in 1.0.20

Fixed — verifyTransaction uses the correct digest

verifyTransaction(transaction, publicKey) now reconstructs the exact digest that
signTransaction signs over, and verifies each signature against it:

digest = sha256(chain_id ‖ serializeTransaction(normalizedTrx))

Correctness details:

  • Mirrors signTransaction's serialization path exactly (transaction.toBuffer).
  • The signatures field is excluded from the digest — signTransaction
    serializes before attaching signatures, so verification strips signatures
    before serializing too.
  • Returns true if any signature is valid for publicKey; otherwise false.

Added — serializeTransaction exported

steem.auth.serializeTransaction(trx) returns the binary Buffer of a
transaction, identical to what signTransaction uses internally. Downstream apps
can now rebuild the signing digest themselves:

import { sha256 } from '@noble/hashes/sha2';

const buf = steem.auth.serializeTransaction(tx); // Buffer
const chainId = Buffer.alloc(32, 0);              // mainnet chain_id (all-zero)
const digest = Buffer.from(sha256(Buffer.concat([chainId, buf])));

Type declarations are emitted automatically by the TypeScript build
(dist/auth/index.d.ts).

Quick start — sign → verify round-trip

const { steem } = require('@steemit/steem-js');

const wif = '5JLw5dgQAx6rhZEgNN5C2ds1V47RweGshynFSWFbaMohsYsBvE8';
const publicKey = steem.auth.wifToPublic(wif);

const tx = {
  ref_block_num: 123,
  ref_block_prefix: 456789,
  expiration: '2026-07-10T00:00:00',
  operations: [['transfer', {
    from: 'alice', to: 'bob', amount: '1.000 STEEM', memo: ''
  }]],
  extensions: [],
};

// Client signs locally
const signedTx = steem.auth.signTransaction(tx, [wif]);

// Server verifies before relaying
const isValid = steem.auth.verifyTransaction(signedTx, publicKey);
console.log(isValid); // true

// Wrong key / tampered tx / missing signatures → false

Security use case — relay/wallet server verification

This release closes a defense-in-depth gap for services that relay signed
transactions on behalf of clients. A wallet backend can now, on receipt of a
client-submitted signed transaction, independently prove the signature belongs to
the claimed account's key before broadcasting:

function verifyClientTransaction(signedTx, expectedPublicKey) {
  if (!signedTx?.signatures?.length) return false;
  return steem.auth.verifyTransaction(signedTx, expectedPublicKey);
}

// In a relay route:
//   const account = await steem.api.getAccountsAsync([username]);
//   const expectedPubKey = account[0].active.key_auths[0][0];
//   if (!verifyClientTransaction(clientSignedTx, expectedPubKey)) {
//     return res.status(403).json({ error: 'Invalid signature' });
//   }
//   await steem.broadcast.sendAsync(clientSignedTx);

Previously such a server could only validate transaction shape (signatures
present, finite ref_block fields, non-empty operations); it could not
cryptographically prove the signer. This was tracked as a deferred Critical
finding in the wallet security audit (mitigated to Low by per-route
operation-type enforcement + HMAC CSRF, but not fully closed until this upstream
fix shipped).

Also in this release

  • Build fix (#540): the [email protected] patch (which eliminates the Node 20+
    new Buffer() DEP0005 deprecation warning, originally landed in 1.0.17) had
    silently regressed — pnpm 10+ stopped reading patchedDependencies from
    package.json. It is migrated to pnpm-workspace.yaml and re-applied; dist/
    no longer emits new Buffer().

Documentation

  • docs/README.md
    Authentication section: new verifyTransaction / serializeTransaction entries
    with params and runnable examples.
  • docs/signature-verification-examples.md
    new "Transaction Signature Verification" section (round-trip, server-side
    relay verification, rejected cases, manual digest construction).

Upgrade

npm install @steemit/steem-js@next
# or pin exactly:
npm install @steemit/[email protected]

No breaking changes. signTransaction is untouched; cross-language serialization
fixtures are unchanged and remain green.

Links

Sort:  

Upvoted! Thank you for supporting witness @jswit.