arkd-round-lifecycle
Round lifecycle management in arkd - stages, events, state machine, intent registration, finalization
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
Round lifecycle management in arkd - stages, events, state machine, intent registration, finalization
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
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.
| name | arkd-round-lifecycle |
| description | Round lifecycle management in arkd - stages, events, state machine, intent registration, finalization |
Use this skill when:
┌─────────────────────────────────────────────────────────────────┐
│ ROUND LIFECYCLE │
├─────────────────────────────────────────────────────────────────┤
│ │
│ [UNDEFINED] ─────────────────────────────────────────────────► │
│ │ │
│ ▼ StartRegistration() │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ REGISTRATION STAGE │ │
│ │ - Accept user intents (RegisterIntents) │ │
│ │ - Collect VTXOs to refresh/spend │ │
│ │ - Collect boarding inputs │ │
│ │ - Validate proofs of ownership │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ StartFinalization() │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ FINALIZATION STAGE │ │
│ │ - Build VTXO tree and commitment tx │ │
│ │ - MuSig2 tree signing with cosigners │ │
│ │ - Collect forfeit transactions │ │
│ │ - Sign and broadcast commitment tx │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ EndFinalization() │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ ROUND ENDED │ │
│ │ - Commitment tx broadcast │ │
│ │ - VTXOs created with expiration │ │
│ │ - Previous VTXOs marked spent │ │
│ │ - Batch output scheduled for sweeping │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ [FAILED] ◄───────── Fail() called at any stage │
│ │
└─────────────────────────────────────────────────────────────────┘
| Stage | Code | Description |
|---|---|---|
RoundUndefinedStage | 0 | Initial state, round not started |
RoundRegistrationStage | 1 | Accepting user intents |
RoundFinalizationStage | 2 | Building and signing trees |
| Event | Description |
|---|---|
RoundStarted | Round begins registration stage |
IntentsRegistered | Users' intents added to round |
RoundFinalizationStarted | Tree built, signing begins |
RoundFinalized | Round successfully completed |
RoundFailed | Round aborted with error |
BatchSwept | ASP reclaimed expired batch output |
type Round struct {
Id string // UUID of the round
StartingTimestamp int64 // When registration started
EndingTimestamp int64 // When round ended
Stage Stage // Current stage info
Intents map[string]Intent // User intents by ID
CommitmentTxid string // On-chain anchor txid
CommitmentTx string // Commitment transaction
ForfeitTxs []ForfeitTx // Collected forfeit txs
VtxoTree tree.FlatTxTree // VTXO transaction tree
Connectors tree.FlatTxTree // Connector tree
ConnectorAddress string // Address for connectors
VtxoTreeExpiration int64 // Seconds until VTXOs expire
Swept bool // Has batch been swept
SweepTxs map[string]string // Sweep transactions
FailReason string // Why round failed
Changes []Event // Domain events
}
Source: arkd/internal/core/domain/round.go:41-59
type Stage struct {
Code int // RoundStage value
Ended bool // Stage completed
Failed bool // Stage failed
}
type RoundStage int
const (
RoundUndefinedStage RoundStage = iota
RoundRegistrationStage
RoundFinalizationStage
)
func (s RoundStage) String() string {
switch s {
case RoundRegistrationStage:
return "REGISTRATION_STAGE"
case RoundFinalizationStage:
return "FINALIZATION_STAGE"
default:
return "UNDEFINED_STAGE"
}
}
Source: arkd/internal/core/domain/round.go:11-28
func NewRound() *Round {
return &Round{
Id: uuid.New().String(),
Intents: make(map[string]Intent),
Changes: make([]Event, 0),
}
}
// Reconstruct round from stored events
func NewRoundFromEvents(events []Event) *Round {
r := &Round{}
for _, event := range events {
r.on(event, true) // Apply events in order
}
r.Changes = append([]Event{}, events...)
return r
}
Source: arkd/internal/core/domain/round.go:61-79
func (r *Round) StartRegistration() ([]Event, error) {
empty := Stage{}
if r.Stage != empty {
return nil, fmt.Errorf("not in a valid stage to start intents registration")
}
event := RoundStarted{
RoundEvent: RoundEvent{
Id: r.Id,
Type: EventTypeRoundStarted,
},
Timestamp: time.Now().Unix(),
}
r.raise(event) // Apply event and add to Changes
return []Event{event}, nil
}
Source: arkd/internal/core/domain/round.go:85-101
func (r *Round) RegisterIntents(intents []Intent) ([]Event, error) {
if r.Stage.Code != int(RoundRegistrationStage) || r.IsFailed() {
return nil, fmt.Errorf("not in a valid stage to register intents")
}
if len(intents) <= 0 {
return nil, fmt.Errorf("missing intents to register")
}
// Validate each intent
for _, intent := range intents {
if err := intent.validate(false); err != nil {
return nil, err
}
}
event := IntentsRegistered{
RoundEvent: RoundEvent{
Id: r.Id,
Type: EventTypeIntentsRegistered,
},
Intents: intents,
}
r.raise(event)
return []Event{event}, nil
}
Source: arkd/internal/core/domain/round.go:103-126
func (r *Round) StartFinalization(
connectorAddress string, connectors tree.FlatTxTree, vtxoTree tree.FlatTxTree,
commitmentTxid, commitmentTx string, vtxoTreeExpiration int64,
) ([]Event, error) {
// Validate inputs
if len(commitmentTx) <= 0 {
return nil, fmt.Errorf("missing unsigned commitment tx")
}
if vtxoTreeExpiration <= 0 {
return nil, fmt.Errorf("missing vtxo tree expiration")
}
if r.Stage.Code != int(RoundRegistrationStage) || r.IsFailed() {
return nil, fmt.Errorf("not in a valid stage to start finalization")
}
if len(r.Intents) <= 0 {
return nil, fmt.Errorf("no intents registered")
}
event := RoundFinalizationStarted{
RoundEvent: RoundEvent{
Id: r.Id,
Type: EventTypeRoundFinalizationStarted,
},
VtxoTree: vtxoTree,
Connectors: connectors,
ConnectorAddress: connectorAddress,
CommitmentTxid: commitmentTxid,
CommitmentTx: commitmentTx,
VtxoTreeExpiration: vtxoTreeExpiration,
}
r.raise(event)
return []Event{event}, nil
}
Source: arkd/internal/core/domain/round.go:128-163
func (r *Round) EndFinalization(forfeitTxs []ForfeitTx, finalCommitmentTx string) ([]Event, error) {
// Check if forfeit txs are required
if len(forfeitTxs) <= 0 {
for _, intent := range r.Intents {
for _, in := range intent.Inputs {
if in.RequiresForfeit() {
return nil, fmt.Errorf("missing list of signed forfeit txs")
}
}
}
}
if r.Stage.Code != int(RoundFinalizationStage) || r.IsFailed() {
return nil, fmt.Errorf("not in a valid stage to end finalization")
}
if r.Stage.Ended {
return nil, fmt.Errorf("round already finalized")
}
if forfeitTxs == nil {
forfeitTxs = make([]ForfeitTx, 0)
}
event := RoundFinalized{
RoundEvent: RoundEvent{
Id: r.Id,
Type: EventTypeRoundFinalized,
},
ForfeitTxs: forfeitTxs,
FinalCommitmentTx: finalCommitmentTx,
Timestamp: time.Now().Unix(),
}
r.raise(event)
return []Event{event}, nil
}
Source: arkd/internal/core/domain/round.go:165-199
func (r *Round) Fail(err error) []Event {
if r.Stage.Failed {
return nil // Already failed
}
event := RoundFailed{
RoundEvent: RoundEvent{
Id: r.Id,
Type: EventTypeRoundFailed,
},
Reason: err.Error(),
Timestamp: time.Now().Unix(),
}
r.raise(event)
return []Event{event}
}
Source: arkd/internal/core/domain/round.go:232-247
func (r *Round) IsStarted() bool {
empty := Stage{}
return !r.IsFailed() && !r.IsEnded() && r.Stage != empty
}
func (r *Round) IsEnded() bool {
return !r.IsFailed() && r.Stage.Code == int(RoundFinalizationStage) && r.Stage.Ended
}
func (r *Round) IsFailed() bool {
return r.Stage.Failed
}
func (r *Round) ExpiryTimestamp() int64 {
if r.IsEnded() {
return time.Unix(r.EndingTimestamp, 0).Add(
time.Second * time.Duration(r.VtxoTreeExpiration),
).Unix()
}
return -1 // Round not ended yet
}
Source: arkd/internal/core/domain/round.go:249-269
func (r *Round) Sweep(
leafVtxos, preconfirmedVtxos []Outpoint, txid, tx string,
) ([]Event, error) {
if !r.IsEnded() {
return nil, fmt.Errorf("not in a valid stage to sweep")
}
if r.Swept {
return nil, nil // Already swept
}
// Calculate if fully swept
sweptVtxosCount := countSweptLeafVtxos(r.Changes)
leavesCount := len(tree.FlatTxTree(r.VtxoTree).Leaves())
fullySwept := len(leafVtxos)+sweptVtxosCount == leavesCount
event := BatchSwept{
RoundEvent: RoundEvent{
Id: r.Id,
Type: EventTypeBatchSwept,
},
LeafVtxos: leafVtxos,
PreconfirmedVtxos: preconfirmedVtxos,
Txid: txid,
Tx: tx,
FullySwept: fullySwept,
}
r.raise(event)
return []Event{event}, nil
}
Source: arkd/internal/core/domain/round.go:201-230
func (r *Round) on(event Event, replayed bool) {
switch e := event.(type) {
case RoundStarted:
r.Stage.Code = int(RoundRegistrationStage)
r.Id = e.Id
r.StartingTimestamp = e.Timestamp
case RoundFinalizationStarted:
r.Stage.Code = int(RoundFinalizationStage)
r.VtxoTree = e.VtxoTree
r.Connectors = e.Connectors
r.ConnectorAddress = e.ConnectorAddress
r.CommitmentTxid = e.CommitmentTxid
r.CommitmentTx = e.CommitmentTx
r.VtxoTreeExpiration = e.VtxoTreeExpiration
case RoundFinalized:
r.Stage.Ended = true
r.ForfeitTxs = append([]ForfeitTx{}, e.ForfeitTxs...)
r.EndingTimestamp = e.Timestamp
r.CommitmentTx = e.FinalCommitmentTx
case RoundFailed:
r.Stage.Failed = true
r.FailReason = e.Reason
r.EndingTimestamp = e.Timestamp
case IntentsRegistered:
if r.Intents == nil {
r.Intents = make(map[string]Intent)
}
for _, p := range e.Intents {
r.Intents[p.Id] = p
}
case BatchSwept:
if r.SweepTxs == nil {
r.SweepTxs = make(map[string]string)
}
r.Swept = e.FullySwept
r.SweepTxs[e.Txid] = e.Tx
}
if replayed {
r.Version++
}
}
Source: arkd/internal/core/domain/round.go:271-314
type Intent struct {
Id string // UUID
Inputs []Vtxo // VTXOs being spent
Receivers []Receiver // Output destinations
Proof string // Ownership proof (PSBT)
Message string // Signed message
}
func NewIntent(proof, message string, inputs []Vtxo) (*Intent, error) {
intent := &Intent{
Id: uuid.New().String(),
Inputs: inputs,
Proof: proof,
Message: message,
}
if err := intent.validate(true); err != nil {
return nil, err
}
return intent, nil
}
func (i Intent) TotalInputAmount() uint64 {
tot := uint64(0)
for _, in := range i.Inputs {
tot += in.Amount
}
return tot
}
func (i Intent) TotalOutputAmount() uint64 {
tot := uint64(0)
for _, r := range i.Receivers {
tot += r.Amount
}
return tot
}
Source: arkd/internal/core/domain/intent.go:9-58
func (s *service) startRound() {
defer s.wg.Done()
ctx := context.Background()
// Cleanup previous round
existingRound, _ := s.cache.CurrentRound().Get(ctx)
if existingRound != nil {
s.cache.ForfeitTxs().Reset(ctx)
s.cache.Intents().DeleteVtxos(ctx)
s.cache.ConfirmationSessions().Reset(ctx)
// ... more cleanup
}
// Create new round
round := domain.NewRound()
round.StartRegistration()
s.cache.CurrentRound().Upsert(ctx, func(_ *domain.Round) *domain.Round {
return round
})
log.Debugf("started registration stage for new round: %s", round.Id)
// Wait for registration duration
roundTiming := newRoundTiming(s.sessionDuration)
<-time.After(roundTiming.registrationDuration())
// Move to confirmation
s.wg.Add(1)
go s.startConfirmation(round.Id, roundTiming, s.roundMinParticipantsCount, s.roundMaxParticipantsCount)
}
Source: arkd/internal/core/application/service.go:2369-2466
func (s *service) startFinalization(
roundId string, roundTiming roundTiming, registeredIntents []ports.TimedIntent,
) {
ctx := context.Background()
round, _ := s.cache.CurrentRound().Get(ctx)
// Build commitment transaction and VTXO tree
commitmentTx, vtxoTree, connectorAddress, connectors, err := s.builder.BuildCommitmentTx(
s.forfeitPubkey, intents, boardingInputs, cosignersPublicKeys,
)
// Create MuSig2 coordinator session for tree signing
coordinator, _ := tree.NewTreeCoordinatorSession(
root.CloneBytes(), batchOutputAmount, vtxoTree,
)
// Generate operator's nonces
operatorSignerSession := tree.NewTreeSignerSession(s.operatorPrvkey)
operatorSignerSession.Init(root.CloneBytes(), batchOutputAmount, vtxoTree)
nonces, _ := operatorSignerSession.GetNonces()
coordinator.AddNonce(s.operatorPubkey, nonces)
// Wait for user nonces
// ... broadcast tree signing started event
// ... collect nonces from users
// Aggregate nonces and send to users
aggregatedNonces, _ := coordinator.AggregateNonces()
// ... broadcast aggregated nonces
// Wait for signatures
// ... collect partial signatures
signedTree, _ := coordinator.SignTree()
// Start round finalization
round.StartFinalization(
connectorAddress, flatConnectors, flatVtxoTree,
commitmentPtx.UnsignedTx.TxID(), commitmentTx, vtxoTreeExpiration,
)
}
Source: arkd/internal/core/application/service.go:2701-3062
func (s *service) finalizeRound(roundId string, roundTiming roundTiming) {
ctx := context.Background()
round, _ := s.cache.CurrentRound().Get(ctx)
// Wait for forfeit txs and boarding signatures
remainingTime := roundTiming.remainingDuration()
select {
case <-s.forfeitsBoardingSigsChan:
log.Debug("all forfeit txs and boarding inputs signatures have been sent")
case <-time.After(remainingTime):
log.Debug("timeout waiting for forfeit txs and boarding inputs signatures")
}
// Get collected forfeit txs
forfeitTxList, _ := s.cache.ForfeitTxs().Pop(ctx)
// Verify all required forfeits were received
allForfeitTxsSigned, _ := s.cache.ForfeitTxs().AllSigned(ctx)
if !allForfeitTxsSigned {
round.Fail(errors.INTERNAL_ERROR.New("missing forfeit transactions"))
return
}
// Verify signatures
if convictions := s.verifyForfeitTxsSigs(roundId, forfeitTxList); len(convictions) > 0 {
round.Fail(errors.INTERNAL_ERROR.New("invalid forfeit txs signature"))
return
}
// Sign and broadcast commitment tx
signedCommitmentTx, _ := s.wallet.SignCommitmentTx(ctx, round.CommitmentTx)
s.wallet.BroadcastTransaction(ctx, signedCommitmentTx)
// End finalization
changes, _ := round.EndFinalization(forfeitTxs, signedCommitmentTx)
s.saveEvents(ctx, roundId, changes)
// Schedule batch sweep
s.scheduleSweepBatchOutput(round)
// Start next round
go s.startRound()
}
Source: arkd/internal/core/application/service.go:3064-3335
| Purpose | File | Key Types/Functions |
|---|---|---|
| Round domain | arkd/internal/core/domain/round.go | Round, RoundStage, Stage |
| Round events | arkd/internal/core/domain/round_event.go | RoundStarted, RoundFinalized, etc. |
| Intent domain | arkd/internal/core/domain/intent.go | Intent, Receiver |
| Service layer | arkd/internal/core/application/service.go | startRound, startFinalization, finalizeRound |
| Event repo | arkd/internal/core/domain/events_repo.go | Event storage interfaces |
| Cache ports | arkd/internal/core/ports/cache.go | CurrentRound, ForfeitTxs, etc. |
func isVtxoValid(vtxo Vtxo, round *Round) bool {
return round.IsEnded() &&
vtxo.SettledBy == round.CommitmentTxid &&
vtxo.ExpiresAt > time.Now().Unix()
}
func getRoundExpiration(round *Round) time.Time {
if !round.IsEnded() {
return time.Time{} // Not yet expired
}
return time.Unix(round.ExpiryTimestamp(), 0)
}
func getCreatedVtxos(round *Round) []Vtxo {
vtxos := make([]Vtxo, 0)
for _, leaf := range round.VtxoTree.Leaves() {
// Parse VTXO from leaf transaction
vtxo := parseVtxoFromLeaf(leaf, round)
vtxos = append(vtxos, vtxo)
}
return vtxos
}
func getSpentVtxos(intents map[string]Intent) []Vtxo {
spent := make([]Vtxo, 0)
for _, intent := range intents {
for _, vtxo := range intent.Inputs {
if vtxo.RequiresForfeit() {
spent = append(spent, vtxo)
}
}
}
return spent
}
Stage Transitions: Transitions are one-way. You cannot go back from Finalization to Registration.
Event Sourcing: The Round is event-sourced. All state changes MUST go through events. Never mutate fields directly.
Failed Rounds: A failed round cannot be recovered. A new round must be started.
Forfeit Requirements: VTXOs that are not notes AND not swept require forfeit transactions. Check RequiresForfeit().
Expiration Calculation: VTXO expiration is EndingTimestamp + VtxoTreeExpiration. The expiration is only valid after the round ends.
Concurrent Rounds: Only ONE round can be active at a time. The service uses a cache to track the current round.
Sweep Tracking: A round can be swept incrementally. Track Swept flag and SweepTxs map for full sweep status.
Intent Validation: Intents are validated for proof validity, expiration timeranges, and non-duplicated inputs.
Boarding Inputs: Boarding inputs are treated differently - they don't require forfeit txs but need their own signatures.
Round Timing: The service uses a roundTiming helper to calculate registration, confirmation, and finalization durations.
Skill Owner: ark-developer Repos: arkd