Skip to content

Dotrain source subject is derived two ways: generate emits under keccak(cbor item map) while DotrainSourceV1::hash and the subject field docs say keccak(content) #219

Description

@thedavidmeister

Unit

CLI generate subcommand + metaboard emit tx-data (crates/cli/src/metaboard.rs, crates/cli/src/meta/types/dotrain/source_v1.rs) — AMT group g5-interpretercallermeta-v1.

Intent oracle

  • DotrainSourceEmitData.subject doc comment (crates/cli/src/metaboard.rs): "Keccak256 hash of the content (hex-encoded with 0x prefix)".
  • DotrainSourceV1::hash() doc comment (crates/cli/src/meta/types/dotrain/source_v1.rs): "Returns the hash of the dotrain source code" — implemented as keccak256(self.0.as_bytes()), i.e. hash of the raw content.
  • IMetaV1_2 NatSpec (src/interface/unstable/IMetaV1_2.sol): subject "will often be a hash of some data/thing that this metadata is about" — interpretation is context specific, so the crate's own convention is the oracle for the dotrain-source context.

Observed behaviour

generate_dotrain_source_emit_tx_data derives subject as document.hash(false) = keccak256 of the CBOR-encoded meta item map (payload + magic + contentType + ...), NOT keccak256 of the raw source content.

So the same crate exposes two different subject derivations for the same dotrain source:

  1. emit half (rain-metadata generate source / generate_dotrain_source_emit_tx_data): keccak256(cbor_encode(item))
  2. fetch half (DotrainSourceV1::hash() + DotrainSourceV1::fetch_by_subject(subject, url)): the only hash the type itself offers is keccak256(content)

A consumer that computes DotrainSourceV1::hash(&source) and calls fetch_by_subject with it will never find a meta published via the generate flow, because that meta was emitted under the item-map hash.

Violated property

Content-addressable round-trip within one crate: subject(emit) == subject(fetch key) for the same source content; and doc/impl agreement for DotrainSourceEmitData.subject.

Verified repro

let content = "#main _ _: int-add(1 2) int-add(2 3)";
let tx = generate_dotrain_source_emit_tx_data(content).unwrap();
let content_hash = alloy::primitives::hex::encode_prefixed(DotrainSourceV1(content.to_string()).hash());
assert_ne!(tx.subject, content_hash); // passes today: the two subjects differ

Concrete values for content = "#main _ _: int-add(1 2) int-add(2 3)" (from the built CLI at bba50a7):

  • generate flow subject (keccak of cbor item map): 0xaeb47b040e0c281065bdaed6a3c81284c6ad0be63eed471b3b75dd5672c1c68e
  • DotrainSourceV1::hash / keccak256 of the raw content: 0x55d083bf6ee7d68b9f5f9138c1f0c7d42a906fd079efe2825fe6a4d80329e104

A fetch_by_subject(0x55d0…) lookup can never find the meta emitted under 0xaeb4….

Triage framing (neutral)

Either derivation may be the intended convention:

  • If "subject = keccak256 of the raw content" (as the field doc and DotrainSourceV1::hash say) is intended, the generate flow emits under the wrong subject and DotrainSourceEmitData.subject's value is mislabeled.
  • If "subject = keccak256 of the CBOR item map" (document.hash(false)) is intended, then the DotrainSourceEmitData.subject doc comment is wrong and DotrainSourceV1::hash() is a footgun as the natural-looking key for fetch_by_subject.

Not adjudicating which; flagging the inconsistency for triage. Existing tests only pin internal consistency (subject-in-calldata == subject field), not the cross-API convention.

Metadata

Metadata

Assignees

No one assigned

    Labels

    adversarialFound by adversarial review/mutation testingauditAudit finding; counted by the org health scan

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions