Skip to content

Standardize asset introspection with canonical asset id#97

Merged
louisinger merged 2 commits into
masterfrom
fix/canonical-asset-id
Jun 16, 2026
Merged

Standardize asset introspection with canonical asset id#97
louisinger merged 2 commits into
masterfrom
fix/canonical-asset-id

Conversation

@msinkec

@msinkec msinkec commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR standardizes asset introspection around canonical AssetIDs represented as (asset_txid, asset_gidx).

It separates an asset’s issuance group index from its current packet position, ensures intent inputs use the issuance transaction ID rather than the intent source transaction ID, and adds strict AssetID operand validation.

Opcode Changes

Opcode Input Output Change
OP_INSPECTASSETGROUPASSETID k asset_txid asset_gidx Returns the canonical AssetID. Fresh issuances resolve to (current_txid, k).
OP_INSPECTASSETGROUPCTRL k asset_txid asset_gidx 1 or empty 0 0 Resolves group-based control references to canonical AssetIDs.
OP_FINDASSETGROUPBYASSETID asset_txid asset_gidx k 1 or 0 0 Finds the current packet position using the complete canonical AssetID.
OP_INSPECTOUTASSETAT output_index asset_index asset_txid asset_gidx amount Emits canonical asset_gidx instead of the current packet position.
OP_INSPECTOUTASSETLOOKUP output_index asset_txid asset_gidx amount 1 or 0 0 Matches outputs by the complete canonical AssetID.
OP_INSPECTINASSETAT input_index asset_index asset_txid asset_gidx amount Always emits the issuance AssetID, including for intent inputs.
OP_INSPECTINASSETLOOKUP input_index asset_txid asset_gidx amount 1 or 0 0 Matches canonical AssetIDs and no longer accepts intent source txids.

Validation

Malformed AssetIDs now produce script errors for:

  • Asset transaction IDs that are not exactly 32 bytes
  • Negative asset_gidx values
  • asset_gidx values greater than 65535
  • Oversized ScriptNums
  • Non-minimally encoded ScriptNums

Additional Changes

  • Added shared helpers for resolving, parsing, validating, and comparing canonical AssetIDs.
  • Updated README opcode documentation and terminology.
  • Updated fuzz generators to produce canonical AssetIDs.
  • Updated covenant and contract identity examples.
  • Expanded coverage for shared issuance txids, distinct issuance indices, fresh issuances, intent inputs, invalid operands, wrong IDs, and control-asset resolution.

Testing

  • go test ./... -count=1 in pkg/arkade
  • TestContractIdWithAssetIdentity
  • git diff --check

TestAssetAccountCovenant reaches the local emulator but currently fails during setup with VTXO_NOT_FOUND, unrelated to these opcode changes.

@msinkec msinkec self-assigned this Jun 12, 2026

@arkanaai arkanaai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arkana Code Review — PR #97: Standardize asset introspection with canonical asset id

Reviewer: Arkana (automated deep review)
Verdict: Request changes — one code-level bug, one critical cross-repo breakage, needs migration guidance


Overall Assessment

This is a well-motivated refactor. The semantic shift from packet-position-based gidx to canonical issuance-index-based asset_gidx is the right architectural call: it makes AssetIDs position-independent and composable across transactions. The helper extraction (resolveAssetID, popAssetID, assetIDEqual) dramatically improves readability and eliminates 5+ duplicated resolve-or-derive blocks. Test coverage is thorough and the new edge-case tests (same txid/distinct gidx, fresh issuance round-trips, operand validation) are exactly what was missing.

However, this is a breaking change to opcode semantics in a protocol-level system. Two issues must be resolved before merge.


🔴 CRITICAL: Cross-repo breakage — solver/bancod Banco swap scripts

File: solver/pkg/banco/contract/offer.go:288 (and identical code in bancod)

The solver builds fulfill scripts that hardcode gidx=0 (via OP_0):

b.AddOp(txscript.OP_0) // output index 0
b.AddData(o.WantAsset.Txid[:])
b.AddOp(txscript.OP_0) // asset group lookup index 0  ← BREAKS
b.AddOp(arkade.OP_INSPECTOUTASSETLOOKUP)

Before this PR: OP_INSPECTOUTASSETLOOKUP matched by packet position. Passing gidx=0 meant "check the first packet group." The taker controls packet layout, so this worked.

After this PR: OP_INSPECTOUTASSETLOOKUP matches by canonical AssetID (txid, asset_gidx). If WantAsset.Index != 0, the lookup will never match — the fulfill script silently fails, and the swap is bricked.

Fix: The solver must pass o.WantAsset.Index instead of hardcoding 0:

b.AddInt64(int64(o.WantAsset.Index)) // canonical asset_gidx

Also affected: layerzero-usdt0-arkade-demo/internal/scripts/builders.go uses a FINDASSETGROUPBYASSETID → INSPECTOUTASSETLOOKUP pattern where the packet position k returned by FIND is fed as gidx to LOOKUP. After this PR, LOOKUP expects canonical asset_gidx, not k. The layerzero demo should pass the original canonical (asset_txid, asset_gidx) directly to LOOKUP instead of the intermediate k.

Recommendation: This PR should either:

  1. Ship with companion PRs to solver, bancod, layerzero-demo, and compiler, OR
  2. Document a migration guide and coordinate the rollout

🟡 MEDIUM: Duplicate canonical AssetID ambiguity in lookup opcodes

File: pkg/arkade/asset_opcodes.go:384-400 (opcodeInspectOutAssetLookup) and :501-515 (opcodeInspectInAssetLookup)

Both lookup opcodes now iterate all groups and return the first match by canonical ID. If a packet contains two groups with the same canonical AssetID (same Txid + same Index), only the first group's amount is ever returned.

The old code filtered by packet position first (scriptNum(groupIdx) != gidx), making each group individually addressable. The new code makes duplicate-ID groups indistinguishable from the script's perspective.

Question: Does the Arkade V1 packet spec guarantee canonical AssetID uniqueness within a packet? If yes, this is fine (but deserves a comment). If no, this is a behavioral regression — scripts lose the ability to differentiate same-asset groups.


🟢 Code Quality — Good

resolveAssetID (line 588): Clean, correct. The uint16(k) cast is safe given the documented precondition that k ∈ [0, len(packet)) and packets can't exceed 65535 groups.

popAssetID (line 597): Good validation — rejects negative gidx, values > 65535, wrong-length txids, oversized ScriptNums, non-minimal encoding. All boundaries are tested.

assetIDEqual (line 621): Straightforward and correct.

opcodeInspectAssetGroupCtrl (line 76): The AssetRefByGroup branch now correctly resolves through resolveAssetID instead of duplicating the nil-check logic. The int() conversion on group.ControlAsset.GroupIndex (line 77) is correct since GroupIndex is uint16.

opcodeInspectInAssetAt (line 454): Correctly removes the intent_txid special case. Intent inputs now always emit the canonical issuance txid. This is a deliberate semantic change that's well-tested (OP_INSPECTINASSETAT_intent test, line 735).

opcodeInspectInAssetLookup (line 501): Correctly removes intent_txid matching. The OP_INSPECTINASSETLOOKUP_intent_txid_rejected test (line 799) verifies the old path now returns not-found.


🟢 Test Coverage — Thorough

New test cases I verified:

  • canonical_find_first_of_shared_txid / canonical_find_second_of_shared_txid — same txid, distinct canonical gidx ✅
  • canonical_out_at_emits_canonical_gidx — verifies gidx=20 emitted (not packet position 1) ✅
  • canonical_fresh_issuance_group_resolution_round_trip — INSPECTASSETGROUPASSETID → FINDASSETGROUPBYASSETID ✅
  • canonical_fresh_issuance_output_round_trip — OUTASSETAT → OUTASSETLOOKUP composability ✅
  • Operand validation (negative gidx, gidx>65535, oversized ScriptNum, non-minimal, wrong txid length) — all ✅
  • canonical_ctrl_by_group_fresh_issuance — control asset resolution through fresh issuance ✅
  • Intent txid rejection in both INSPECTINASSETAT and INSPECTINASSETLOOKUP ✅

Missing test: No test for the OP_INSPECTASSETGROUPCTRL AssetRefByID path with a canonical gidx ≠ 0. The existing OP_INSPECTASSETGROUPCTRL_found test uses ctrlTxid with Index: 7 but doesn't verify the gidx value is emitted as 7 (it just drops it). Consider adding explicit gidx verification.


🟢 Fuzz Generator Update — Correct

File: pkg/arkade/opcode_fuzz_test.go:338-359

The fuzz generator now derives canonical assetGidx from group.AssetId.Index (or k for fresh issuances) instead of always using the packet position. The mutation at line 356 (assetGidx += len(packet)) ensures the fuzzer explores out-of-range canonical gidx values. Correct.


🟢 Documentation — Good

README opcode table updates accurately reflect the new semantics. The new terminology section clearly distinguishes k (packet position) from asset_gidx (canonical issuance index). The note that intent input source txids are not AssetIDs is important and well-placed.


Summary

Finding Severity Status
Cross-repo breakage (solver, bancod, layerzero) 🔴 Critical Must fix before merge
Duplicate canonical AssetID ambiguity 🟡 Medium Needs clarification
Missing ctrl gidx verification test 🟢 Low Nice to have

⚠️ PROTOCOL-CRITICAL: This PR changes opcode semantics that affect VTXO script evaluation. Even though the code looks correct within the emulator, it changes how deployed scripts resolve assets. Requires human sign-off before merge.


🤖 Reviewed by Arkana · #97 · Co-authored review with Claude Opus 4.6

@msinkec

msinkec commented Jun 12, 2026

Copy link
Copy Markdown
Contributor Author

Question: Does the Arkade V1 packet spec guarantee canonical AssetID uniqueness within a packet? If yes, this is fine (but deserves a comment). If no, this is a behavioral regression — scripts lose the ability to differentiate same-asset groups.

This should be guaranteed by arkd.

@louisinger

Copy link
Copy Markdown
Collaborator

@msinkec merge with master to fix Trivy CI 🙏

@msinkec

msinkec commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

@msinkec merge with master to fix Trivy CI 🙏

Done.

@arkanaai arkanaai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arkana Follow-up Review — Merge commit 3bbc05b9

The new commit is a merge of origin/master into the branch. The only change it introduces to the PR's scope is a Go version bump (1.26.31.26.4 in go.mod). No conflicts, no changes to opcode logic, tests, or helpers. This merge is clean — no new issues.

Outstanding items from previous review

  1. 🔴 Cross-repo breakage (solver, bancod, layerzero-demo) — Still unresolved. Downstream scripts hardcode gidx=0 in OP_INSPECTOUTASSETLOOKUP calls. Companion PRs or a coordinated rollout plan needed before merge.

  2. 🟡 Duplicate canonical AssetID ambiguity@msinkec confirmed arkd guarantees uniqueness. ✅ Resolved. A brief code comment in the lookup opcodes noting this invariant would be nice but is not blocking.

Nothing else to flag. My request-changes from the first review remains in effect pending resolution of the cross-repo breakage.

🤖 Reviewed by Arkana · #97 · Iterative review of new commits

@louisinger louisinger merged commit 96f9b06 into master Jun 16, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants