Skip to content

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

Draft
Kukks wants to merge 7 commits intomasterfrom
feat/arkade-script
Draft

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

Conversation

@Kukks
Copy link
Copy Markdown
Collaborator

@Kukks Kukks commented May 3, 2026

Summary

Adds full client-side Arkade Script support to the .NET SDK as a new `NArk.Arkade` package, mirroring arkade-os/ts-sdk#319 and https://github.com/ArkLabsHQ/introspector. Consumers that don't use Arkade scripts carry no extra dependency.

321/321 unit tests green locally, including 48 Arkade-targeted tests (fixture-driven against `ArkLabsHQ/introspector pkg/arkade/testdata/`).

What's in the package

Surface Purpose
`ArkadeOpcode` + registry All 41 extension opcodes, byte-for-byte mirror of `ARKADE_OP`; combined Bitcoin + Arkade name↔value map for ASM
`ArkadeScriptNum` `BigInteger` sign-magnitude LE encode/decode (lifts the `long` ceiling so 32-byte EC scalars round-trip)
`ArkadeScriptHash` BIP-340 tagged hash with `"ArkScriptHash"` tag + `Tweak(introspectorPubKey, script)` returning the x-only tweaked key
`ArkadeScript` Encode/decode bytes ↔ `Op[]`; `ToAsm` / `FromAsm` / `AsmToBytes` / `BytesToAsm`
`IArkadeBoundScriptBuilder` Marker interface — type-driven detection of arkade-bound leaves (no metadata stamping)
`ArkadeNofNMultisigTapScript` Augments an N-of-N multisig with one tweaked pubkey per introspector; implements the marker interface
`IntrospectorEntry` + `IntrospectorPacket` TLV codec for the script+witness binding; implements `IExtensionPacket` so it rides in the same OP_RETURN envelope as the asset packet
`IIntrospectorProvider` + `IntrospectorClient` REST client for `GET /v1/info`, `POST /v1/tx`, `POST /v1/intent`, `POST /v1/finalization`
`ArkadePsbtExtensions` `RequiresIntrospectorCoSigning(coins)` / `BuildIntrospectorOutput(coins)` / `CoSignWithIntrospectorAsync(psbt, introspector)`
`IBatchSessionExtension` (NArk.Abstractions) Generic plug-point for batch-flow co-signing — `ShouldHandleAsync` + `CoSignAsync(phase, psbts, coins, ct)`
`ArkadeBatchSessionExtension` Engages only when ≥1 input is arkade-bound; routes each PSBT through `SubmitTxAsync`
DI `AddArkadeIntrospector(opts)` one-liner registers the REST client + the batch extension together

Test inventory (48 Arkade-targeted)

  • `ArkadeScriptNum`: known sign-magnitude vectors, 32-byte EC scalar round-trip, non-minimal rejection
  • `ArkadeScript` codec: Arkade opcodes survive round-trip, ASM mnemonic correctness, all 41 opcodes round-trip through ASM, bare-name parsing parity with ts-sdk
  • `ArkadeScriptHash`: deterministic compute, distinct scripts produce distinct digests, x-only tweak round-trip
  • `IntrospectorPacket` fixture-driven (introspector `testdata/introspector_packet.json`): every `valid` vector encodes to the exact bytes the Go reference produces, every `invalid` vector splits correctly between Validate-failures and Parse-failures
  • `IntrospectorPacket` Extension envelope: round-trips through `Extension.Serialize/FromScript`, `FromTransaction` finds it across multiple outputs, `PacketTypeId == 0x01` locked
  • `ArkadeNofNMultisigTapScript`: augments owner set with tweaked introspector key, emitted bytes match plain-multisig of augmented owners, rejects empty script / empty introspector list, implements `IArkadeBoundScriptBuilder`
  • `ArkadePsbtExtensions`: detection across mixed spends, `BuildIntrospectorOutput` round-trips through Extension parsing, `CoSignWithIntrospectorAsync` delegates correctly to `IIntrospectorProvider`
  • `ArkadeBatchSessionExtension`: engagement gate, passthrough when no arkade coin, dispatches per PSBT for both phases, propagates introspector failures

Out of scope for this PR (clearly tracked)

The package is complete and shippable; the remaining work is integration into existing protocol code, scoped to a follow-up:

  • `BatchSession` internal call sites — fire `IBatchSessionExtension.CoSignAsync` at `PostTreeSigning` (after `HandleAggregatedTreeNoncesEventAsync`) and `PreForfeitFinalization` (start of `HandleBatchFinalizationAsync`). Tightly scoped to two methods in `NArk.Core/Batches/BatchSession.cs`.
  • Intent-submit hook — `IIntrospectorProvider.SubmitIntentAsync` lives upstream of `BatchSession` (in `IntentGenerationService`); needs its own integration point, deferred until the corresponding shape is agreed.
  • `POST /v1/finalization` switch-over — `ArkadeBatchSessionExtension` currently uses `SubmitTxAsync` for both phases. Switching the `PreForfeitFinalization` path to `SubmitFinalizationAsync` requires threading the co-signed intent proof from intent-registration time; doc-commented swap-in point.
  • E2E test against an introspector docker stack — needs infrastructure additions (`docker-compose.introspector.yml` overlay, `SharedIntrospectorInfrastructure` setup fixture).

Test plan

  • `NArk.sln` builds clean
  • `dotnet test NArk.Tests` passes (321/321 incl. 48 new Arkade tests, fixtures from `ArkLabsHQ/introspector`)
  • CI green on the push
  • Follow-up PR: BatchSession call sites
  • Follow-up PR: E2E against introspector docker

Kukks added 7 commits May 3, 2026 19:11
…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.
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