You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add public single-signer AccountBlock-v1 unsigned preparation, canonical hashing, and verified signature attachment
Status
Maintainer-aligned implementation plan. Work may proceed from the current main after #34 as three independently reviewable and shippable phases. At plan acceptance, main was 00a37f824e9eb3376c6063e489b6e412d42c2018.
This is additive work. Existing prepareBlock() and send() behavior must remain unchanged. This issue does not add publication, signer callbacks, wallet lifecycle management, cancellation, Dynamic Plasma, or a redesign of #27 multisig.
Signature verification must match the pinned Go verifier exactly.
Commit every pinned Go vector as a literal test fixture. Crypto.verify ships only if it matches every vector. Noble { zip215: false } is not assumed to be equivalent; any divergence must be reviewed explicitly.
Use ZnnAccountBlockException, ZnnAccountBlockExceptionCode, and ZnnAccountBlockExceptionStage, following the SDK's Znn*Exception naming.
The public method is zenon.prepareUnsigned(template, payer).
The current getTxHash result is a legacy regression vector. A separately and independently sourced canonical vector is also required, and both must pass.
The pinned Go expectation accepts 198 vectors and rejects 570 vectors. The literal fixture must preserve source IDs, inputs, flags, and both accepted and rejected outcomes.
Ordinary-valid corpus: Go's 128-vector sign.input.gz, SHA-256 ae02ad0b05f2aa54bbc1a90cd87d1733932caa298a243e69d6623f27d1251d5c, plus the official S >= L rejection case.
The fixture is read-only test evidence: no update, capture, regenerate, rewrite, or bless path may derive expected values from the candidate implementation.
Intended public surface
The completed work adds:
computeAccountBlockHash
computeAccountBlockHashBytes
AccountBlockPayerIdentity
PreparedUnsignedAccountBlock
verifyAccountBlockSignature
attachAccountBlockSignature
ZnnAccountBlockException
ZnnSDKException at the package root
Zenon.prepareUnsigned(template, payer)
Internal helpers such as prepareUnsigned, getTxHash, normalizeAccountBlockAmount, prepareUnsignedTemplate, cloneAccountBlockTemplate, bytesEqual, and createPreparedUnsignedAccountBlock are not exported from the package root.
Phase 1 — exception taxonomy, hash module, and the block.ts seam
Part A — hash module
Files:
test/utilities/accountBlockHash.spec.ts (new)
src/utilities/errors.ts
src/utilities/accountBlockHash.ts (new)
src/utilities/block.ts
Work:
Before moving production code, pin a fully specified current-HEAD getTxHash result as a legacy regression vector.
Add a separately sourced canonical AccountBlock-v1 hash vector. It must not be generated by the candidate implementation.
Add ZnnAccountBlockExceptionStage, ZnnAccountBlockExceptionCode, and ZnnAccountBlockException below ZnnBlockUtilitiesException.
Move getTxHash verbatim from block.ts into the client-free leaf module accountBlockHash.ts.
Add AccountBlockHashInput, normalizeAccountBlockAmount, module-private input validation, computeAccountBlockHashBytes, and computeAccountBlockHash.
Import getTxHash into block.ts and re-export it there for existing internal consumers. Do not export it from the package root.
Keep the canonical serializer in exactly one implementation.
Acceptance:
Both the legacy regression vector and independent canonical vector pass before and after the move.
test/utilities/block.spec.ts passes without assertion changes.
computeAccountBlockHash agrees with getTxHash for well-formed input.
Validation failures use the new exception taxonomy and the expected code/stage pairs.
The amount parity matrix uses 9007199254740994, the exactly representable Number immediately above 2^53; do not use 2^53 + 1.
Returned hash bytes are detached from live Hash.core storage.
accountBlockHash.ts has no value dependency on pow/pow.js or zenon.js.
Hash field order, widths, encodings, SHA3-256 use, excluded fields, and zero-PoW nonce bytes remain byte-for-byte unchanged.
Part B — unsigned preparation seam
Work:
Change checkAndSetFields to accept address: Address and publicKey: Buffer instead of a KeyPair.
Add internal prepareUnsignedTemplate, composed from checkAndSetFields and setDifficulty.
Keep prepareBlock behavior by calling prepareUnsignedTemplate with the current key pair's address/public key and then calling the existing hash/signature stage.
Field mutation order, RPC order, zero-PoW nonce, fused Plasma selection, error messages, mutate-in-place behavior, returned object identity, hashing, and signing remain unchanged.
Part A and Part B are separate commits within the Phase 1 PR so either can be reverted independently.
Phase 2 — Go-compatible Crypto.verify and Base64 round trips
Files:
src/crypto/crypto.ts
src/model/nom/accountBlock.ts
test/crypto/crypto.spec.ts
test/model/nom/accountBlock.spec.ts (new if absent)
a committed versioned literal Go-verifier fixture under test/fixtures/
Work:
Add Crypto.verify(signature, message, publicKey) and initialize SHA-512 before verification.
Preserve exact pinned Go-verifier behavior. The implementation must pass every committed literal Go vector; a Noble option alone is not acceptance evidence.
If any pinned vector diverges, stop and review the behavior instead of silently hardening or weakening it.
Decode publicKey and signature as Base64 in both account-block fromJson implementations, preserving the empty-buffer fallback.
Add valid-signature, tampered-message, wrong-key, first-call initialization, and full pinned-vector tests.
Add toJson() → fromJson() byte-equality tests for both AccountBlockTemplate and AccountBlock.
Acceptance:
Every pinned Go vector matches.
Ordinary valid signatures verify; tampered messages and wrong keys do not.
Verification works as the first Crypto call in a fresh process.
Both account-block types round-trip the original public-key and signature bytes.
The Base64 behavior change is called out explicitly in the commit body.
The crypto and Base64 edits remain separately revertible commits.
Phase 3 — offline surface and public API
Part A — offline surface
Files:
src/utilities/accountBlockSigning.ts (new)
test/utilities/accountBlockSigning.spec.ts (new)
Work:
Add bytesEqual, AccountBlockPayerIdentity, PreparedUnsignedAccountBlock, createPreparedUnsignedAccountBlock, cloneAccountBlockTemplate, verifyAccountBlockSignature, and attachAccountBlockSignature.
Copy byte inputs and outputs defensively.
Make signature verification a total boolean predicate: malformed inputs return false rather than throwing.
Recompute and verify the AccountBlock hash before accepting or attaching a signature.
Validate address, recipient, and token-standard core lengths before any toString() comparison so malformed cores cannot escape as bare bech32 errors.
Add the public two-argument Zenon.prepareUnsigned(template, payer) method.
Export the public classes, hash helpers, verification/attachment functions, ZnnAccountBlockException, and ZnnSDKException from the package root.
Do not root-export internal orchestration or cloning helpers.
Acceptance:
The caller's input template is not mutated.
The prepared handle stores detached data and normalizes amount to bigint.
Zero-PoW preparation retains the canonical zero nonce behavior.
prepareUnsigned plus verified attachment produces the same hash and signature as prepareBlock for equivalent send and receive inputs.
verifyAccountBlockSignature never throws for malformed input.
Package-root ESM, declarations, packed consumers, and browser imports expose the intended public surface only.
Part A and Part B are separate commits within the Phase 3 PR.
Validation and delivery
Each phase is a separate, independently reviewable Draft PR and must pass:
targeted tests for the phase;
the complete test suite;
lint;
coverage;
the full build;
package-root and packed-consumer export checks.
Phase 1 lands before Phase 3. Phase 2 is independent of Phase 1 and may proceed separately, but Phase 3 requires both. Keep every PR limited to its phase.
Non-goals
changing existing prepareBlock() or send() semantics;
Add public single-signer AccountBlock-v1 unsigned preparation, canonical hashing, and verified signature attachment
Status
Maintainer-aligned implementation plan. Work may proceed from the current
mainafter #34 as three independently reviewable and shippable phases. At plan acceptance,mainwas00a37f824e9eb3376c6063e489b6e412d42c2018.This is additive work. Existing
prepareBlock()andsend()behavior must remain unchanged. This issue does not add publication, signer callbacks, wallet lifecycle management, cancellation, Dynamic Plasma, or a redesign of #27 multisig.This body incorporates the three-phase implementation plan and the resolved contract decisions.
Resolved contract decisions
Crypto.verifyships only if it matches every vector. Noble{ zip215: false }is not assumed to be equivalent; any divergence must be reviewed explicitly.ZnnAccountBlockException,ZnnAccountBlockExceptionCode, andZnnAccountBlockExceptionStage, following the SDK'sZnn*Exceptionnaming.zenon.prepareUnsigned(template, payer).getTxHashresult is a legacy regression vector. A separately and independently sourced canonical vector is also required, and both must pass.Pinned protocol evidence
zenon-network/go-zenon@667a69d9e9a418edf7580b08492ba5dcb9efd63a.1.24.1, source commit339c903a75c3fe936fb4ed6c355d15e6081d6af3.filippo.io/mostly-harmless/ed25519vectors@v0.0.0-20210322192420-30a2d7243a94, repository commit30a2d7243a949c5ec04d5026956319569200a40f, 768-vector JSON SHA-256ed3dd0730ec5ef2dbfd629186fbefeb6299cbbf9a6ed94af0478f07291acacca.sign.input.gz, SHA-256ae02ad0b05f2aa54bbc1a90cd87d1733932caa298a243e69d6623f27d1251d5c, plus the officialS >= Lrejection case.The fixture is read-only test evidence: no update, capture, regenerate, rewrite, or bless path may derive expected values from the candidate implementation.
Intended public surface
The completed work adds:
computeAccountBlockHashcomputeAccountBlockHashBytesAccountBlockPayerIdentityPreparedUnsignedAccountBlockverifyAccountBlockSignatureattachAccountBlockSignatureZnnAccountBlockExceptionZnnSDKExceptionat the package rootZenon.prepareUnsigned(template, payer)Internal helpers such as
prepareUnsigned,getTxHash,normalizeAccountBlockAmount,prepareUnsignedTemplate,cloneAccountBlockTemplate,bytesEqual, andcreatePreparedUnsignedAccountBlockare not exported from the package root.Phase 1 — exception taxonomy, hash module, and the block.ts seam
Part A — hash module
Files:
test/utilities/accountBlockHash.spec.ts(new)src/utilities/errors.tssrc/utilities/accountBlockHash.ts(new)src/utilities/block.tsWork:
getTxHashresult as a legacy regression vector.ZnnAccountBlockExceptionStage,ZnnAccountBlockExceptionCode, andZnnAccountBlockExceptionbelowZnnBlockUtilitiesException.getTxHashverbatim fromblock.tsinto the client-free leaf moduleaccountBlockHash.ts.AccountBlockHashInput,normalizeAccountBlockAmount, module-private input validation,computeAccountBlockHashBytes, andcomputeAccountBlockHash.getTxHashintoblock.tsand re-export it there for existing internal consumers. Do not export it from the package root.Acceptance:
test/utilities/block.spec.tspasses without assertion changes.computeAccountBlockHashagrees withgetTxHashfor well-formed input.9007199254740994, the exactly representable Number immediately above2^53; do not use2^53 + 1.Hash.corestorage.accountBlockHash.tshas no value dependency onpow/pow.jsorzenon.js.Part B — unsigned preparation seam
Work:
checkAndSetFieldsto acceptaddress: AddressandpublicKey: Bufferinstead of aKeyPair.prepareUnsignedTemplate, composed fromcheckAndSetFieldsandsetDifficulty.prepareBlockbehavior by callingprepareUnsignedTemplatewith the current key pair's address/public key and then calling the existing hash/signature stage.setDifficulty,setHashAndSignature,getTxSignature,getPoWData,autofillTxParameters,send,isSendBlock, andisReceiveBlockbehavior unchanged.Acceptance:
Phase 2 — Go-compatible Crypto.verify and Base64 round trips
Files:
src/crypto/crypto.tssrc/model/nom/accountBlock.tstest/crypto/crypto.spec.tstest/model/nom/accountBlock.spec.ts(new if absent)test/fixtures/Work:
Crypto.verify(signature, message, publicKey)and initialize SHA-512 before verification.publicKeyandsignatureas Base64 in both account-blockfromJsonimplementations, preserving the empty-buffer fallback.toJson()→fromJson()byte-equality tests for bothAccountBlockTemplateandAccountBlock.Acceptance:
Cryptocall in a fresh process.Phase 3 — offline surface and public API
Part A — offline surface
Files:
src/utilities/accountBlockSigning.ts(new)test/utilities/accountBlockSigning.spec.ts(new)Work:
bytesEqual,AccountBlockPayerIdentity,PreparedUnsignedAccountBlock,createPreparedUnsignedAccountBlock,cloneAccountBlockTemplate,verifyAccountBlockSignature, andattachAccountBlockSignature.falserather than throwing.Acceptance:
ZnnAccountBlockExceptionwith the defined code/stage pairs and never leaks a bare parser or encoding error.Part B — online orchestration and exports
Files:
src/utilities/unsignedBlock.ts(new)src/zenon.tssrc/index.tstest/utilities/unsignedBlock.spec.ts(new)Work:
prepareUnsigned(zenonInstance, template, payer).toString()comparison so malformed cores cannot escape as barebech32errors.Zenon.prepareUnsigned(template, payer)method.ZnnAccountBlockException, andZnnSDKExceptionfrom the package root.Acceptance:
amounttobigint.prepareUnsignedplus verified attachment produces the same hash and signature asprepareBlockfor equivalent send and receive inputs.verifyAccountBlockSignaturenever throws for malformed input.Validation and delivery
Each phase is a separate, independently reviewable Draft PR and must pass:
Phase 1 lands before Phase 3. Phase 2 is independent of Phase 1 and may proceed separately, but Phase 3 requires both. Keep every PR limited to its phase.
Non-goals
prepareBlock()orsend()semantics;