arkd-offchain-tx
Off-chain transaction processing in arkd - Ark TX and checkpoint TX construction, submission, and finalization
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
Off-chain transaction processing in arkd - Ark TX and checkpoint TX construction, submission, and finalization
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
| name | arkd-offchain-tx |
| description | Off-chain transaction processing in arkd - Ark TX and checkpoint TX construction, submission, and finalization |
Use this skill when:
┌─────────────────────────────────────────────────────────────────┐
│ OFF-CHAIN TRANSACTION FLOW │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Client Server (arkd) │
│ ────── ───────────── │
│ │
│ 1. Build Ark TX + Checkpoint TXs │
│ │ │
│ ▼ │
│ 2. Sign Ark TX (forfeit path) │
│ │ │
│ └──────────────────────────────► SubmitOffchainTx() │
│ │ │
│ ▼ │
│ 3. Validate inputs │
│ 4. Co-sign Ark TX │
│ 5. Sign Checkpoint TXs │
│ ◄─────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 6. Verify server signatures │
│ 7. Sign checkpoint TXs (user side) │
│ │ │
│ └──────────────────────────────► FinalizeOffchainTx() │
│ │ │
│ ▼ │
│ 8. Store final TXs │
│ 9. Update VTXO state │
│ ◄─────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 10. Update local VTXO state │
│ │
└─────────────────────────────────────────────────────────────────┘
| Transaction | Purpose | Description |
|---|---|---|
| Ark TX | Off-chain payment | Spends checkpoint outputs to new VTXOs |
| Checkpoint TX | Intermediate step | Spends VTXO using forfeit path to checkpoint output |
| Stage | Code | Description |
|---|---|---|
OffchainTxUndefinedStage | 0 | Initial state |
OffchainTxRequestedStage | 1 | Client submitted, awaiting co-sign |
OffchainTxAcceptedStage | 2 | Server co-signed, awaiting finalization |
OffchainTxFinalizedStage | 3 | Fully signed and stored |
type VtxoInput struct {
Outpoint *wire.OutPoint
Amount int64
// Tapscript is the path used to spend the VTXO (forfeit closure)
Tapscript *waddrmgr.Tapscript
// RevealedTapscripts is the whole taproot tree of the VTXO
// Must be revealed to ASP to verify spend paths are valid
RevealedTapscripts []string
}
Source: arkd/pkg/ark-lib/offchain/tx.go:20-28
// BuildTxs builds the ark and checkpoint txs for given inputs and outputs.
func BuildTxs(
vtxos []VtxoInput, // VTXOs to spend
outputs []*wire.TxOut, // New VTXO outputs
signerUnrollScript []byte, // ASP's unroll script
) (*psbt.Packet, []*psbt.Packet, error) {
checkpointInputs := make([]VtxoInput, 0, len(vtxos))
checkpointTxs := make([]*psbt.Packet, 0, len(vtxos))
inputAmount := int64(0)
// Decode the signer unroll script
signerUnrollScriptClosure := &script.CSVMultisigClosure{}
valid, err := signerUnrollScriptClosure.Decode(signerUnrollScript)
if err != nil || !valid {
return nil, nil, fmt.Errorf("invalid signer unroll script")
}
// Build checkpoint tx for each VTXO
for _, vtxo := range vtxos {
checkpointPtx, checkpointInput, err := buildCheckpointTx(
vtxo, signerUnrollScriptClosure,
)
if err != nil {
return nil, nil, err
}
checkpointInputs = append(checkpointInputs, *checkpointInput)
checkpointTxs = append(checkpointTxs, checkpointPtx)
inputAmount += vtxo.Amount
}
// Verify input/output balance
outputAmount := int64(0)
for _, output := range outputs {
outputAmount += output.Value
}
if inputAmount != outputAmount {
return nil, nil, fmt.Errorf("input amount is not equal to output amount")
}
// Build the Ark TX spending checkpoint outputs
arkTx, err := buildArkTx(checkpointInputs, outputs)
if err != nil {
return nil, nil, err
}
return arkTx, checkpointTxs, nil
}
Source: arkd/pkg/ark-lib/offchain/tx.go:31-73
// buildCheckpointTx creates a virtual tx sending to a "checkpoint" VTXO script.
// Checkpoint = signer unroll script + owner's collaborative closure
func buildCheckpointTx(
vtxo VtxoInput, signerUnrollScript *script.CSVMultisigClosure,
) (*psbt.Packet, *VtxoInput, error) {
// Get owner's collaborative closure from the VTXO tapscript
collaborativeClosure, err := script.DecodeClosure(vtxo.Tapscript.RevealedScript)
if err != nil {
return nil, nil, err
}
// Create checkpoint VTXO script: [signer unroll, owner collaborative]
checkpointVtxoScript := script.TapscriptsVtxoScript{
Closures: []script.Closure{signerUnrollScript, collaborativeClosure},
}
tapKey, tapTree, err := checkpointVtxoScript.TapTree()
if err != nil {
return nil, nil, err
}
checkpointPkScript, err := script.P2TRScript(tapKey)
if err != nil {
return nil, nil, err
}
// Build the checkpoint virtual tx
checkpointPtx, err := buildArkTx(
[]VtxoInput{vtxo},
[]*wire.TxOut{{Value: vtxo.Amount, PkScript: checkpointPkScript}},
)
if err != nil {
return nil, nil, err
}
// Create input for Ark TX that spends this checkpoint output
tapLeafHash := txscript.NewBaseTapLeaf(vtxo.Tapscript.RevealedScript).TapHash()
collaborativeLeafProof, _ := tapTree.GetTaprootMerkleProof(tapLeafHash)
ctrlBlock, _ := txscript.ParseControlBlock(collaborativeLeafProof.ControlBlock)
revealedTapscripts, _ := checkpointVtxoScript.Encode()
checkpointInput := &VtxoInput{
Outpoint: &wire.OutPoint{
Hash: checkpointPtx.UnsignedTx.TxHash(),
Index: 0,
},
Amount: vtxo.Amount,
Tapscript: &waddrmgr.Tapscript{
ControlBlock: ctrlBlock,
RevealedScript: collaborativeLeafProof.Script,
},
RevealedTapscripts: revealedTapscripts,
}
return checkpointPtx, checkpointInput, nil
}
Source: arkd/pkg/ark-lib/offchain/tx.go:173-239
// buildArkTx builds an ark tx spending VTXOs via collaborative taproot path.
func buildArkTx(vtxos []VtxoInput, outputs []*wire.TxOut) (*psbt.Packet, error) {
if len(vtxos) <= 0 {
return nil, fmt.Errorf("missing vtxos")
}
ins := make([]*wire.OutPoint, 0, len(vtxos))
sequences := make([]uint32, 0, len(vtxos))
witnessUtxos := make(map[int]*wire.TxOut)
signingTapLeaves := make(map[int]*psbt.TaprootTapLeafScript)
tapscripts := make(map[int][]string)
txLocktime := common.AbsoluteLocktime(0)
for index, vtxo := range vtxos {
tapscripts[index] = vtxo.RevealedTapscripts
// Compute the VTXO output script
rootHash := vtxo.Tapscript.ControlBlock.RootHash(vtxo.Tapscript.RevealedScript)
taprootKey := txscript.ComputeTaprootOutputKey(script.UnspendableKey(), rootHash)
vtxoOutputScript, _ := script.P2TRScript(taprootKey)
witnessUtxos[index] = &wire.TxOut{
Value: vtxo.Amount,
PkScript: vtxoOutputScript,
}
ctrlBlockBytes, _ := vtxo.Tapscript.ControlBlock.ToBytes()
signingTapLeaves[index] = &psbt.TaprootTapLeafScript{
ControlBlock: ctrlBlockBytes,
Script: vtxo.Tapscript.RevealedScript,
LeafVersion: txscript.BaseLeafVersion,
}
// Handle CLTV multisig closure (updates tx locktime)
closure, _ := script.DecodeClosure(vtxo.Tapscript.RevealedScript)
if cltv, ok := closure.(*script.CLTVMultisigClosure); ok {
if cltv.Locktime > txLocktime {
txLocktime = cltv.Locktime
}
sequences = append(sequences, cltvSequence)
} else {
sequences = append(sequences, wire.MaxTxInSequenceNum)
}
ins = append(ins, vtxo.Outpoint)
}
// Create PSBT with anchor output
arkTx, _ := psbt.New(
ins, append(outputs, txutils.AnchorOutput()), 3, uint32(txLocktime), sequences,
)
// Set input fields
for i := range arkTx.Inputs {
arkTx.Inputs[i].WitnessUtxo = witnessUtxos[i]
arkTx.Inputs[i].TaprootLeafScript = []*psbt.TaprootTapLeafScript{
signingTapLeaves[i],
}
txutils.SetArkPsbtField(arkTx, i, txutils.VtxoTaprootTreeField, tapscripts[i])
}
return arkTx, nil
}
Source: arkd/pkg/ark-lib/offchain/tx.go:75-171
type OffchainTx struct {
Stage Stage // Current stage
StartingTimestamp int64 // When tx was submitted
EndingTimestamp int64 // When tx was finalized
ArkTxid string // Ark TX ID
ArkTx string // Ark TX (PSBT)
CheckpointTxs map[string]string // txid -> checkpoint tx
CommitmentTxids map[string]string // checkpoint txid -> commitment txid
RootCommitmentTxId string // Root commitment txid
ExpiryTimestamp int64 // When VTXOs expire
FailReason string // Failure reason
Version uint // Event version
changes []Event // Domain events
}
Source: arkd/internal/core/domain/offchain_tx.go:35-48
func (s *OffchainTx) Request(
arkTxid, arkTx string, unsignedCheckpointTxs map[string]string,
) (Event, error) {
if s.IsFailed() || s.Stage.Code != int(OffchainTxUndefinedStage) {
return nil, fmt.Errorf("not in a valid stage to request offchain tx")
}
if arkTxid == "" || arkTx == "" {
return nil, fmt.Errorf("missing ark tx")
}
if len(unsignedCheckpointTxs) == 0 {
return nil, fmt.Errorf("missing unsigned checkpoint txs")
}
event := OffchainTxRequested{
OffchainTxEvent: OffchainTxEvent{
Id: arkTxid,
Type: EventTypeOffchainTxRequested,
},
ArkTx: arkTx,
UnsignedCheckpointTxs: unsignedCheckpointTxs,
StartingTimestamp: time.Now().Unix(),
}
s.raise(event)
return event, nil
}
Source: arkd/internal/core/domain/offchain_tx.go:68-95
func (s *OffchainTx) Accept(
finalArkTx string, signedCheckpointTxs map[string]string,
commitmentTxsByCheckpointTxid map[string]string,
rootCommitmentTx string, expiryTimestamp int64,
) (Event, error) {
if finalArkTx == "" {
return nil, fmt.Errorf("missing final ark tx")
}
if len(signedCheckpointTxs) != len(s.CheckpointTxs) {
return nil, fmt.Errorf("invalid number of signed checkpoint txs")
}
if !s.IsRequested() {
return nil, fmt.Errorf("not in a valid stage to accept offchain tx")
}
event := OffchainTxAccepted{
OffchainTxEvent: OffchainTxEvent{
Id: s.ArkTxid,
Type: EventTypeOffchainTxAccepted,
},
FinalArkTx: finalArkTx,
SignedCheckpointTxs: signedCheckpointTxs,
CommitmentTxids: commitmentTxsByCheckpointTxid,
RootCommitmentTxid: rootCommitmentTx,
ExpiryTimestamp: expiryTimestamp,
}
s.raise(event)
return event, nil
}
Source: arkd/internal/core/domain/offchain_tx.go:97-138
func (s *OffchainTx) Finalize(finalCheckpointTxs map[string]string) (Event, error) {
if len(finalCheckpointTxs) != len(s.CheckpointTxs) {
return nil, fmt.Errorf("invalid number of final checkpoint txs")
}
// Verify all checkpoint txs are present
for txid := range s.CheckpointTxs {
if _, ok := finalCheckpointTxs[txid]; !ok {
return nil, fmt.Errorf("checkpoint tx %s not found", txid)
}
}
if !s.IsAccepted() {
return nil, fmt.Errorf("not in a valid stage to finalize offchain tx")
}
event := OffchainTxFinalized{
OffchainTxEvent: OffchainTxEvent{
Id: s.ArkTxid,
Type: EventTypeOffchainTxFinalized,
},
FinalCheckpointTxs: finalCheckpointTxs,
Timestamp: time.Now().Unix(),
}
s.raise(event)
return event, nil
}
Source: arkd/internal/core/domain/offchain_tx.go:140-172
func (s *service) SubmitOffchainTx(
ctx context.Context, unsignedCheckpointTxs []string, signedArkTx string,
) (acceptedTx *AcceptedOffchainTx, structErr errors.Error) {
arkPtx, _ := psbt.NewFromRawBytes(strings.NewReader(signedArkTx), true)
txid := arkPtx.UnsignedTx.TxID()
offchainTx := domain.NewOffchainTx()
var changes []domain.Event
defer func() {
if structErr != nil {
change := offchainTx.Fail(structErr)
changes = append(changes, change)
}
// Save events
s.repoManager.Events().Save(ctx, domain.OffchainTxTopic, txid, changes)
}()
// Validate and process each input VTXO
ins := make([]offchain.VtxoInput, 0)
for i, input := range arkPtx.UnsignedTx.TxIn {
vtxoOutpoint := domain.Outpoint{
Txid: input.PreviousOutPoint.Hash.String(),
VOut: input.PreviousOutPoint.Index,
}
// Look up VTXO commitment chain
vtxosResult, _ := s.repoManager.Vtxos().GetVtxos(ctx, []domain.Outpoint{vtxoOutpoint})
vtxo := vtxosResult[0]
// Verify VTXO is spendable
if vtxo.Spent {
return nil, errors.VTXO_ALREADY_SPENT.New("vtxo %s already spent", vtxoOutpoint)
}
// Build VtxoInput for validation
ins = append(ins, buildVtxoInput(vtxo, arkPtx.Inputs[i]))
}
// Validate checkpoint txs match the inputs
// ...
// Co-sign the Ark TX
signedArkTx, _ := s.signer.SignArkTx(ctx, signedArkTx)
// Sign checkpoint txs
signedCheckpointTxs := make(map[string]string)
for _, checkpointTx := range unsignedCheckpointTxs {
signed, _ := s.signer.SignCheckpointTx(ctx, checkpointTx)
signedCheckpointTxs[signed.TxID()] = signed
}
// Accept the offchain tx
event, _ := offchainTx.Accept(
signedArkTx, signedCheckpointTxs,
commitmentTxids, rootCommitmentTxid, expiryTimestamp,
)
changes = append(changes, event)
// Store in cache for finalization
s.cache.OffchainTxs().Add(ctx, offchainTx)
return &AcceptedOffchainTx{
Txid: txid,
FinalArkTx: signedArkTx,
SignedCheckpointTxs: signedCheckpointTxs,
}, nil
}
Source: arkd/internal/core/application/service.go:412-1114 (simplified)
func (s *service) FinalizeOffchainTx(
ctx context.Context, txid string, finalCheckpointTxs []string,
) (structErr errors.Error) {
var changes []domain.Event
// Get offchain tx from cache or repo
offchainTx, err := s.cache.OffchainTxs().Get(ctx, txid)
if err != nil || offchainTx == nil {
offchainTx, _ = s.repoManager.OffchainTxs().GetOffchainTx(ctx, txid)
}
defer func() {
if structErr != nil {
change := offchainTx.Fail(structErr)
changes = append(changes, change)
}
s.cache.OffchainTxs().Remove(ctx, txid)
s.repoManager.Events().Save(ctx, domain.OffchainTxTopic, txid, changes)
}()
// Build map of final checkpoint txs
finalCheckpointTxsMap := make(map[string]string)
for _, tx := range finalCheckpointTxs {
ptx, _ := psbt.NewFromRawBytes(strings.NewReader(tx), true)
finalCheckpointTxsMap[ptx.UnsignedTx.TxID()] = tx
}
// Verify all checkpoint signatures are valid
for _, tx := range finalCheckpointTxs {
if err := verifyCheckpointSignature(tx); err != nil {
return errors.INVALID_SIGNATURE.New("invalid checkpoint signature")
}
}
// Finalize the offchain tx
event, _ := offchainTx.Finalize(finalCheckpointTxsMap)
changes = append(changes, event)
// Mark input VTXOs as spent
for _, vtxo := range getInputVtxos(offchainTx) {
vtxo.Spent = true
vtxo.ArkTxid = offchainTx.ArkTxid
s.repoManager.Vtxos().UpdateVtxos(ctx, []domain.Vtxo{vtxo})
}
// Create new VTXOs from outputs
newVtxos := extractOutputVtxos(offchainTx)
s.repoManager.Vtxos().AddVtxos(ctx, newVtxos)
return nil
}
Source: arkd/internal/core/application/service.go:1116-1258 (simplified)
type OffchainTxRequested struct {
OffchainTxEvent
ArkTx string // Unsigned ark tx
UnsignedCheckpointTxs map[string]string // txid -> unsigned checkpoint
StartingTimestamp int64
}
type OffchainTxAccepted struct {
OffchainTxEvent
CommitmentTxids map[string]string // checkpoint txid -> commitment txid
RootCommitmentTxid string // Root commitment txid
FinalArkTx string // Co-signed ark tx
SignedCheckpointTxs map[string]string // txid -> server-signed checkpoint
ExpiryTimestamp int64
}
type OffchainTxFinalized struct {
OffchainTxEvent
FinalCheckpointTxs map[string]string // txid -> fully signed checkpoint
Timestamp int64
}
type OffchainTxFailed struct {
OffchainTxEvent
Reason string
Timestamp int64
}
Source: arkd/internal/core/domain/offchain_tx_event.go:13-39
func (s *OffchainTx) on(event Event, replayed bool) {
switch e := event.(type) {
case OffchainTxRequested:
if s.Stage.Code != int(OffchainTxUndefinedStage) || s.Stage.Failed {
return
}
s.Stage.Code = int(OffchainTxRequestedStage)
s.ArkTxid = e.Id
s.ArkTx = e.ArkTx
s.CheckpointTxs = e.UnsignedCheckpointTxs
s.StartingTimestamp = e.StartingTimestamp
case OffchainTxAccepted:
if s.Stage.Code != int(OffchainTxRequestedStage) || s.Stage.Failed {
return
}
s.Stage.Code = int(OffchainTxAcceptedStage)
s.ArkTx = e.FinalArkTx
s.CheckpointTxs = e.SignedCheckpointTxs
s.CommitmentTxids = e.CommitmentTxids
s.RootCommitmentTxId = e.RootCommitmentTxid
s.ExpiryTimestamp = e.ExpiryTimestamp
case OffchainTxFinalized:
if s.Stage.Code != int(OffchainTxAcceptedStage) {
return
}
s.Stage.Code = int(OffchainTxFinalizedStage)
s.Stage.Ended = true
s.CheckpointTxs = e.FinalCheckpointTxs
s.EndingTimestamp = e.Timestamp
case OffchainTxFailed:
if s.Stage.Code == int(OffchainTxFinalizedStage) || s.Stage.Failed {
return
}
s.Stage.Failed = true
s.FailReason = e.Reason
}
if replayed {
s.Version++
}
}
Source: arkd/internal/core/domain/offchain_tx.go:227-269
| Purpose | File | Key Types/Functions |
|---|---|---|
| Off-chain tx building | arkd/pkg/ark-lib/offchain/tx.go | BuildTxs, VtxoInput, buildArkTx, buildCheckpointTx |
| Domain model | arkd/internal/core/domain/offchain_tx.go | OffchainTx, Request, Accept, Finalize |
| Domain events | arkd/internal/core/domain/offchain_tx_event.go | OffchainTxRequested, OffchainTxAccepted, etc. |
| Service layer | arkd/internal/core/application/service.go | SubmitOffchainTx, FinalizeOffchainTx |
| Repository | arkd/internal/core/domain/offchain_tx_repo.go | OffchainTxRepository interface |
// In go-sdk client.go (SendOffChain)
func buildOffchainTx(inputs []coin, receivers []Receiver, checkpointExitPath []byte) (*psbt.Packet, []*psbt.Packet, error) {
vtxos := make([]offchain.VtxoInput, 0)
for _, in := range inputs {
vtxos = append(vtxos, offchain.VtxoInput{
Outpoint: in.Outpoint,
Amount: int64(in.Amount),
Tapscript: in.Tapscript,
RevealedTapscripts: in.RevealedTapscripts,
})
}
outputs := make([]*wire.TxOut, 0)
for _, r := range receivers {
outputs = append(outputs, &wire.TxOut{
Value: int64(r.Amount),
PkScript: r.Script,
})
}
return offchain.BuildTxs(vtxos, outputs, checkpointExitPath)
}
func isPending(offchainTx *OffchainTx) bool {
return offchainTx.IsRequested() || offchainTx.IsAccepted()
}
func getCommitmentChain(offchainTx *OffchainTx, checkpointTxid string) []string {
commitmentTxid := offchainTx.CommitmentTxids[checkpointTxid]
return []string{offchainTx.RootCommitmentTxId, commitmentTxid}
}
Checkpoint TX Purpose: Checkpoint TXs add an intermediate step that allows the ASP to unroll if the user misbehaves. They're virtual transactions (never broadcast).
Input/Output Balance: The sum of inputs MUST equal sum of outputs (no fees within Ark TX, fees handled by anchors).
CLTV Handling: If any input uses CLTV closure, the tx locktime must be set appropriately and sequence must be cltvSequence.
Revealed Tapscripts: The full taproot tree must be revealed to the ASP for validation. Don't skip this.
Concurrent Spending: Check s.cache.OffchainTxs().Includes() before accepting to prevent double-spending.
Expiry Inheritance: New VTXOs inherit the earliest expiry from input VTXOs (or input commitment chain).
Event Sourcing: OffchainTx is event-sourced. All state changes through events, replay reconstructs state.
Version 3 Transactions: All Ark and checkpoint transactions must be version 3 for TRUC/v3 relay.
Server Co-signing: The ASP signs AFTER the user. Verify user signatures before co-signing.
Finalization Window: Off-chain TXs should be finalized promptly. The cache holds pending TXs temporarily.
Skill Owner: ark-developer Repos: arkd, go-sdk
Analyze a project repository and add it to the Arkadian documentation registry with standardized structure. Use when: user provides a path to a new project to onboard.
Analyse an Arkadian orchestrator session - read transcripts, logs, and artifacts to understand what happened and suggest improvements. Use when: user wants to debug or review a past session.
Generate validated operational SOPs for Ark ecosystem projects by testing commands before documenting them. Use when: user wants to create or audit SOPs for a project.
Remove a project from the Arkadian documentation registry and delete all associated documentation files. Use when: user wants to deregister a project.
Update project documentation based on new commits and changes in the repository. Use when: user wants to sync docs after project changes.
Self-improvement loop for arkadian. Launches arkadian -d with a GitHub issue, polls until done, evaluates per-agent criteria, reads transcripts on failure, improves prompts, deletes worktree, and relaunches. Loops until passing or max iterations.