feat: Arkade Script support (NArk.Arkade package)#81
Draft
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
Test inventory (48 Arkade-targeted)
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:
Test plan