Skip to content

feat: Arkade Script support (NArk.Arkade package)#81

Draft
Kukks wants to merge 9 commits into
masterfrom
feat/arkade-script
Draft

feat: Arkade Script support (NArk.Arkade package)#81
Kukks wants to merge 9 commits into
masterfrom
feat/arkade-script

Conversation

@Kukks

@Kukks Kukks commented May 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds full client-side Arkade Script support to the .NET SDK as the NArk.Arkade package, aligned with ts-sdk #319 and arkade-os/emulator — the introspection / co-signing service formerly named ArkLabsHQ/introspector. Consumers that don't use Arkade scripts carry no extra dependency.

485/485 unit tests green locally, including 63 emulator-targeted tests (60 in NArk.Tests.Arkade + 3 in PsbtHelpers), fixture-driven against arkade-os/emulator pkg/arkade/testdata/.

Update — aligned to current arkade-os/emulator + ts-sdk #319

Rebased onto master and brought up to date with both references:

  • Renamed Introspector → Emulator across the public surface (IEmulatorProvider, EmulatorClient, EmulatorPacket, EmulatorEntry, namespace NArk.Arkade.Emulator, DI AddArkadeEmulator), matching the upstream rename.
  • Opcodes — the deployed VM (opcode.go) is authoritative and diverges from ts-sdk #319 on 0xd70xe2. Remapped those to the VM's NUM2BIN / BIN2NUM / REVERSEBYTES / MODEXP (0xd70xda) and ECADD / ECMUL / ECPAIRING (0xe00xe2), dropping the ts-sdk-only 64-bit arithmetic / scriptnum-conversion ops, and added INSPECTPACKET 0xf4 / INSPECTINPUTPACKET 0xf5 / SIGHASH 0xf6. A 0xd7 script built against ts-sdk's table would otherwise execute as a different opcode on the emulator.
  • REST — added POST /v1/onchain-tx and the deprecatedSignerPubkeys field on /v1/info; signerPubkey is now treated as compressed (33-byte) hex, with a Tweak(ECPubKey, …) overload bridging it to the x-only tweak.
  • Packet — enforce the emulator decoder's explicit bounds (entries ≤ 1000, script 1–10000, witness blob ≤ 1,000,000) in both Validate and the wire parser.
  • PSBTprevarktx / prevouttx ark fields (key type 0xde) in PsbtHelpers, for the previous-tx data introspection opcodes read on /v1/onchain-tx.

What's in the package

Surface Purpose
ArkadeOpcode + registry 46 extension opcodes tracking arkade-os/emulator pkg/arkade/opcode.go; combined Bitcoin + Arkade name↔value map for ASM
ArkadeScriptNum BigInteger sign-magnitude LE encode/decode (32-byte EC scalars round-trip)
ArkadeScriptHash BIP-340 "ArkScriptHash" tagged hash + Tweak(TaprootPubKey / ECPubKey, script) → x-only tweaked key
ArkadeScript bytes ↔ Op[]; ToAsm / FromAsm / AsmToBytes / BytesToAsm
IArkadeBoundScriptBuilder marker interface — type-driven detection of arkade-bound leaves
ArkadeNofNMultisigTapScript augments an N-of-N multisig with one tweaked pubkey per emulator
EmulatorEntry + EmulatorPacket TLV codec (type 0x01) with the emulator's bounds; rides in the asset-packet OP_RETURN envelope
IEmulatorProvider + EmulatorClient REST client: GET /v1/info, POST /v1/tx, /v1/intent, /v1/finalization, /v1/onchain-tx
ArkadePsbtExtensions RequiresEmulatorCoSigning / BuildEmulatorOutput / CoSignWithEmulatorAsync
PsbtHelpers prev-tx fields SetArkFieldPrevArkTx / SetArkFieldPrevoutTx (+ getters), key type 0xde
IBatchSessionExtension (NArk.Abstractions) generic batch-flow co-signing plug-point
ArkadeBatchSessionExtension engages only when ≥ 1 input is arkade-bound
DI AddArkadeEmulator(opts) registers the REST client + the batch extension

Out of scope (unchanged — tracked for follow-up)

The package is complete and shippable; the remaining work is integration into existing protocol code:

  • BatchSession internal call sites — fire IBatchSessionExtension.CoSignAsync at PostTreeSigning (after HandleAggregatedTreeNoncesEventAsync) and PreForfeitFinalization (start of HandleBatchFinalizationAsync).
  • Intent-submit hookIEmulatorProvider.SubmitIntentAsync lives upstream of BatchSession (in IntentGenerationService).
  • POST /v1/finalization switch-overArkadeBatchSessionExtension currently uses SubmitTxAsync for both phases; switching PreForfeitFinalization to SubmitFinalizationAsync requires threading the co-signed intent proof from intent-registration time.
  • E2E against the emulator docker stack — the regtest emulator profile is already available in arkade-os/arkade-regtest.

Test plan

  • Solution builds clean
  • dotnet test NArk.Tests — 485/485 (incl. 63 emulator-targeted), fixtures vendored verbatim from arkade-os/emulator pkg/arkade/testdata/
  • CI green on the push
  • Follow-up: BatchSession call sites + /v1/finalization switch-over
  • Follow-up: E2E against the emulator docker stack

@Kukks Kukks force-pushed the feat/arkade-script branch from 17a617b to 0a570f4 Compare June 4, 2026 07:27
Kukks added 9 commits June 9, 2026 15:15
…eak primitives

First slice of Arkade Script support, ported from
arkade-os/ts-sdk#319 and the introspector reference
at https://github.com/ArkLabsHQ/introspector. Lays the foundation for the
script codec, ArkadeVtxoScript, PSBT field codecs, and the introspector REST
client that follow in subsequent commits on this branch.

- New NArk.Arkade package (net8.0, references NArk.Core).
- ArkadeOpcode enum mirrors the ts-sdk ARKADE_OP table 1:1 — values stay
  byte-compatible with the ts-sdk and the introspector's pkg/arkade/opcode.go
  so cross-SDK scripts round-trip.
- ArkadeOpcodeRegistry merges Arkade extension opcodes and NBitcoin's
  standard Bitcoin opcodes into a single name<->value map plus the
  OP_DATA_N pattern, matching the ts-sdk's combined OPCODE_NAMES/VALUES.
- ArkadeScriptNum encodes/decodes BigInteger using Bitcoin's sign-magnitude
  little-endian format — same wire shape as @scure/btc-signer's ScriptNum
  but without the int64 ceiling, so 32-byte EC scalars (consumed by
  OP_ECMULSCALARVERIFY / OP_TWEAKVERIFY) survive round-trip.
- ArkadeScriptHash computes the BIP-340 tagged hash with the literal
  "ArkScriptHash" tag and tweaks an introspector public key with it,
  producing the x-only TaprootPubKey the introspector will sign for any
  input whose attached ArkadeScript matches.

No tests yet — they land alongside the script codec in the next commit
so they can also exercise encode/decode round-trips against the ts-sdk
fixture vectors.
Sources introspector_packet.json and read_arkade_script.json verbatim from
ArkLabsHQ/introspector pkg/arkade/testdata/ so the .NET tests for the
upcoming script codec and packet TLV exercise the same vectors the Go
introspector and ts-sdk validate against. Cross-SDK drift will fail CI on
every side that consumes them.

Wires NArk.Tests against the new NArk.Arkade package and the fixture-copy
ItemGroup that mirrors the existing Assets/Fixtures pattern.
Continues the Arkade Script port from
arkade-os/ts-sdk#319 / ArkLabsHQ/introspector. Builds on the opcode /
scriptnum / tweak primitives committed earlier and adds:

- ArkadeScript: encode/decode bytes <-> Op[], plus ASM helpers using the
  combined Bitcoin+Arkade opcode registry. Round-trip is a thin pass-
  through over NBitcoin's Script/Op since NBitcoin already treats the
  Arkade extension byte range (0xb3, 0xc4-0xf3) as opaque single-byte
  ops — no custom serializer needed.
- IntrospectorPacket / IntrospectorEntry: TLV codec for the OP_RETURN
  payload that binds ArkadeScript to specific transaction inputs. Wire
  format is `compactSize(count) + [u16_le(vin) + compactSize(scriptLen)
  + script + compactSize(witnessLen) + witness]*`. Validates non-empty
  packet, non-empty script, unique vin (matching the introspector
  reference). Includes EncodePushList/DecodePushList for the inner
  list-of-pushes shape callers wrap into the witness slot.
- IIntrospectorProvider + IntrospectorClient: REST client for the four
  endpoints (GET /v1/info, POST /v1/tx, POST /v1/intent, POST
  /v1/finalization), DI-friendly via the new
  IntrospectorServiceCollectionExtensions.AddIntrospectorClient. RegisterIntentMessage
  is JSON-stringified into the wire envelope to match ts-sdk parity.

Tests:
- ArkadeScriptNum: known sign-magnitude vectors + 32-byte EC scalar
  round-trip + non-minimal rejection.
- ArkadeScriptCodec: opcode preservation, ASM mnemonic correctness,
  bare-name parsing parity with ts-sdk's fromASM, ALL 41 Arkade
  opcodes round-trip through ASM.
- ArkadeScriptHash: deterministic compute, distinct scripts produce
  distinct digests, x-only key tweak round-trip.
- IntrospectorPacket fixture-driven: every "valid" vector encodes to the
  exact bytes shipped in the introspector's testdata and parses back to
  the same entries. "Invalid" vectors split into Validate-failures
  (empty packet, empty script, duplicate vin) and Parse-failures
  (truncated, trailing bytes, length-fields exceeding the buffer).

302/302 unit tests green locally. Still pending on this branch:
ArkadeVtxoScript, PSBT ArkadeScript/ArkadeScriptWitness fields,
ArkadeBatchHandler, and the E2E docker test.
Wraps NArk.Core's NofNMultisigTapScript with an arkade-script binding —
appends one tweaked pubkey per introspector to the multisig owner set so
the resulting tapscript leaf can be co-signed by the introspector iff the
attached script body matches. This is the .NET-idiomatic equivalent of
ts-sdk's ArkadeVtxoScript.processScripts() pre-tweak step: existing
ArkContract subclasses already yield ScriptBuilder instances from
GetScriptBuilders(), so an arkade leaf drops in alongside the existing
CSV / collab-path leaves with no change to the contract abstraction.

Also adds the NArk.Arkade README section per the submodule CLAUDE.md doc
rule, with worked examples for the codec, the tweak, the multisig
wrapper, and the introspector REST client DI registration.

Tests:
- AugmentsOwnerSet_WithTweakedIntrospectorKey
- EmittedScript_MatchesPlainNofNWithAugmentedOwners
- RejectsEmptyArkadeScript / RejectsEmptyIntrospectorList

33/33 Arkade tests + 302/302 unit suite green locally.
…hape

Slice 1 of the introspector integration. Both layers (PSBT co-signing
helper and BatchSession hooks) need to detect "this contract has at least
one arkade-bound leaf" and "for the leaf being spent, what's its arkade
script body?". The cleanest answer is to put that knowledge on the
ScriptBuilder itself rather than smuggle it through ArkContractEntity
metadata strings — the builder already takes the script in its
constructor.

- IArkadeBoundScriptBuilder: marker interface exposing the ArkadeScript
  bytecode + the pre-tweak introspector pubkeys. Future arkade-bound
  flavours (CSV multisig, condition multisig, etc.) just implement it
  and the dispatch / packet-assembly code stays untouched.
- ArkadeNofNMultisigTapScript: implements the interface (one-line change
  — the data was already there).
- IntrospectorEntry.Witness: tightened from opaque byte[] to
  IReadOnlyList<byte[]> to match the Go reference's wire.TxWitness shape
  and avoid forcing every caller to pre-serialize the push list. The
  packet codec now does the inline EncodePushList() / DecodePushList()
  during Serialize / Parse.

Tests:
- Smoke test for the IArkadeBoundScriptBuilder cast on
  ArkadeNofNMultisigTapScript.
- Updated IntrospectorPacketFixtureTests for the new witness shape —
  vectors now compare witness lists element-wise (still byte-equal to
  introspector_packet.json).

34/34 Arkade tests + full suite green locally. Slice 2 (CoSign helper +
ArkadePsbtExtensions) and slice 3 (IBatchSessionExtension) follow.
Slice 2 of the introspector integration. The IntrospectorPacket now
shares the same OP_RETURN envelope the asset packet uses, and a small
extension method drives the post-sign co-signing round-trip end-to-end.

- IntrospectorPacket: refactored from a static-only class into an
  instance class implementing NArk.Core.Assets.IExtensionPacket
  (PacketType=0x01). Static helpers (Validate / EncodePushList /
  DecodePushList) are preserved for callers that need them. New
  FromBytes / FromExtension / FromTransaction factories let callers
  recover the introspector record from a parsed Extension or directly
  from a tx.
- ArkadePsbtExtensions:
    - RequiresIntrospectorCoSigning(coins): type-checks each ArkCoin's
      SpendingScriptBuilder against IArkadeBoundScriptBuilder. Cheap
      gate so spends with no arkade leaves skip the REST round-trip.
    - BuildIntrospectorOutput(coins): assembles the IntrospectorPacket
      from arkade-bound coins (one entry per arkade-bound vin, with the
      script body + the spending-condition witness pushes from the
      coin), wraps it in an Extension, returns the OP_RETURN TxOut
      ready to append to the unsigned tx.
    - CoSignWithIntrospectorAsync(psbt, introspector): submits the
      partially-signed PSBT to the introspector and returns the merged
      PSBT (the introspector returns the union of input + its own
      partial sigs, so this is a thin SubmitTxAsync wrapper).

Tests:
- IntrospectorPacketExtensionTests (4): packet-in-Extension roundtrip
  alone, FromTransaction across multiple outputs, no-extension-returns-
  null, packet type byte locked at 0x01.
- ArkadePsbtExtensionsTests (4): RequiresIntrospectorCoSigning detection
  with mixed spends, BuildIntrospectorOutput null when no arkade coin,
  emitted output round-trips through Extension parsing back to the
  expected vin/script/witness, CoSignWithIntrospectorAsync delegates to
  IIntrospectorProvider via NSubstitute and returns the parsed response.

42 Arkade-targeted tests, 315/315 across the full suite. Slice 3
(IBatchSessionExtension) follows after sanity-check.
Slice 3 of the introspector integration. Adds the plug-point that
BatchSession will fire at the two PSBT-emitting points of a batch flow,
plus the arkade implementation that routes those PSBTs through the
introspector for co-signing.

- IBatchSessionExtension (NArk.Abstractions.Batches): generic plug-point
  with two methods. ShouldHandleAsync(spendingCoins) is a cheap "is this
  batch relevant?" gate — BatchSession can short-circuit per-extension
  for the rest of the batch when it returns false. CoSignAsync is fired
  with the current PSBTs at a given BatchExtensionPhase
  (PostTreeSigning / PreForfeitFinalization) and returns the (possibly
  mutated) PSBTs.
- ArkadeBatchSessionExtension (NArk.Arkade.Introspector): engages only
  when at least one input's SpendingScriptBuilder implements
  IArkadeBoundScriptBuilder, then routes each PSBT through
  ArkadePsbtExtensions.CoSignWithIntrospectorAsync (POST /v1/tx). The
  dedicated POST /v1/finalization endpoint isn't used yet — that path
  needs the introspector-co-signed intent proof threaded through from
  intent registration time, which lives upstream of BatchSession; the
  doc-comment marks the swap-in point for when that wire-up exists.
- AddArkadeIntrospector: one-liner DI helper that registers the REST
  client + the IBatchSessionExtension together. AddIntrospectorClient
  is preserved for callers who want to inject manually.
- README example refreshed with the AddArkadeIntrospector one-liner and
  the inline CoSignWithIntrospectorAsync usage.

Tests (6 new):
- ShouldHandleAsync engagement gate (false when no arkade coins, true
  when at least one is arkade).
- CoSignAsync passthrough when no arkade coin present (no introspector
  call made at all).
- CoSignAsync dispatches each PSBT to the introspector for both
  BatchExtensionPhase values.
- Introspector failures propagate.

48 Arkade-targeted tests, 321/321 across the full suite.

The remaining work for the Arkade Script PR is the BatchSession
internal call sites that fire the two hooks — that's a tightly-scoped
follow-up (touching BatchSession.HandleAggregatedTreeNoncesEventAsync
and HandleBatchFinalizationAsync) once the package design is reviewed.
…mulator

The introspector co-signing service was renamed "emulator" upstream
(arkade-os/emulator; ts-sdk extension/emulator + EmulatorProvider). Align the
.NET public surface so callers write the same orchestration across SDKs:

- namespace NArk.Arkade.Introspector → NArk.Arkade.Emulator (folder moved too)
- IIntrospectorProvider→IEmulatorProvider, IntrospectorClient→EmulatorClient,
  IntrospectorPacket→EmulatorPacket, IntrospectorEntry→EmulatorEntry,
  IntrospectorInfo/SubmitTxResult/FinalizationResult→Emulator*
- IArkadeBoundScriptBuilder.IntrospectorKeys→EmulatorKeys;
  ArkadeNofNMultisigTapScript.TweakedIntrospectorKeys→TweakedEmulatorKeys
- ArkadePsbtExtensions Requires/Build/CoSignWith* → *Emulator*
- DI: AddIntrospectorClient→AddEmulatorClient, AddArkadeIntrospector→AddArkadeEmulator
- fixture introspector_packet.json→emulator_packet.json; docs/URLs → arkade-os/emulator

Pure rename — no behavior change. 48/48 Arkade tests green.
Updates NArk.Arkade against the current arkade-os/emulator + ts-sdk #319.

Opcodes: the emulator's pkg/arkade/opcode.go is authoritative (it is what
executes scripts) and diverges from ts-sdk #319 on 0xd7–0xe2. Remap those to
the VM's set — NUM2BIN/BIN2NUM/REVERSEBYTES/MODEXP (0xd7–0xda) and
ECADD/ECMUL/ECPAIRING (0xe0–0xe2) — dropping the ts-sdk-only 64-bit arithmetic
/ scriptnum-conversion ops, and add INSPECTPACKET 0xf4 / INSPECTINPUTPACKET
0xf5 / SIGHASH 0xf6.

REST: add POST /v1/onchain-tx (SubmitOnchainTxAsync) and the
deprecatedSignerPubkeys field on /v1/info; signerPubkey is documented as
compressed (33-byte) hex.

Packet: enforce the emulator decoder's explicit bounds — entries ≤1000,
script 1–10000, witness blob ≤1,000,000 — in both Validate and the wire parser.

Crypto: ArkadeScriptHash.Tweak overload taking the emulator's compressed
ECPubKey (drops parity → x-only → tweak).

PSBT: prevarktx / prevouttx ark fields (key type 0xde) in PsbtHelpers, for the
previous-tx data introspection opcodes read on /v1/onchain-tx.

Docs: refresh vendored read_arkade_script.json; README usage (onchain-tx,
deprecated signers, compressed-key tweak, prevout field) + opcode-authority note.

+15 unit tests; full suite (485) green.
@Kukks Kukks force-pushed the feat/arkade-script branch from 0a570f4 to a71d630 Compare June 9, 2026 13:17
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.

1 participant