Skip to content

Transaction: sets without tag 258 gain the tag on re-encode, changing the transaction id #574

Description

@solidsnakedev

Summary

A transaction whose sets are plain or indefinite arrays, with no tag 258, comes back from Transaction.fromCBORHex then Transaction.toCBORHex with d90102 added and a definite length. Nothing else was changed. The body bytes change, so the transaction id changes and every witness already collected no longer verifies. Transaction.addVKeyWitnessesHex does the same, because it decodes and re-encodes rather than splicing.

wallet sends inputs as:   00 81 [input]
SDK writes back:          00 d90102 81 [input]

The ledger accepts both forms and hashes the bytes as received:

  • cardano-ledger Decoder.hs L910-916: from protocol version 9, "Set tag 258 is permitted, but not enforced", with definite or indefinite length.
  • decodeSetLikeEnforceNoDuplicates L1053 and Annotated.hs L238, L258 use allowTag setTag for body and witness sets.
  • Core.hs L651: txIdTxBody = TxId . hashAnnotated, over the original body bytes.

Affected

packages/evolution/src/TransactionBody.ts

  • decode accepts tagged and untagged sets (L323-328)
  • encode always emits CBOR.Tag 258 for keys 0, 4, 13, 14, 18, 20 (L227, L249, L275, L284, L301, L312)

packages/evolution/src/TransactionWitnessSet.ts

  • encode always emits tag 258 for keys 0, 1, 2, 3, 4, 6, 7 (L323, L332, L341, L348, L357, L380, L387)

packages/evolution/src/CBOR.ts

  • encodeTagSync (L1545) uses a captured format only when it is a tag node. The captured node for an untagged set is an array, so the whole subtree is encoded with no format, and nested choices such as an input index written as 1800 are lost too.

Fix

Let the captured format decide whether the tag is written. When the format at a tag-258 position is an array node, encode the inner array with that node and omit the tag. With no captured format, keep writing the tag, so freshly built transactions do not change. This covers both uses of the format tree:

  • a plain round trip reproduces the input exactly;
  • an edit, such as a witness appended to an untagged list, keeps the original form of everything around it.

Regression test

Oracle is the node; CML agrees on every case below.

  • given: 84 a3 00 81 [input] 01 81 [output] 02 [fee] a0 f5 f6, and the same with 00 9f [input] ff
  • before fix: toCBORHex(fromCBORHex(hex)) adds d90102; the body hash differs from blake2b-256 of the original body
  • after fix: byte-identical, and addVKeyWitnessesHex leaves the body bytes unchanged
  • also: untagged vkey witnesses (a1 00 81 ...) and untagged certificates, required signers, collateral, reference inputs
  • control: tagged sets keep the tag; a builder-made transaction is unchanged

Devnet, raw submission through Ogmios:

  • original bytes signed over their own body: accepted
  • same transaction after addVKeyWitnessesHex: rejected, 3100 invalid signatures

Must FAIL on main today and PASS after the fix.

Reference

Same class as #235 (fixed in #236): the format cache is replayed only where the encoder produces the same shape the decoder read.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions