Skip to content

Commit 854df38

Browse files
committed
Compute moneyFlew from bob log events for transaction status
Fetch all logs for a tick via GET /log/{epoch}/{logIdStart}/{logIdEnd}, group QU_TRANSFER events by transaction hash, and determine moneyFlew by comparing the transaction fields against its first QU_TRANSFER event: moneyFlew = (tx.amount == event.amount) && (tx.src == event.src) && (tx.dst == event.dst) && (tx.tick == event.tick) Only transactions with amount > 0 are candidates; others default to false. - Add computeMoneyFlew with log fetching and matching logic - Add transaction caching in BobDataFetcher for use by GetTxStatus - Add bobLogEvent/bobLogBody types for log deserialization - Replace GetTxStatus stub with real moneyFlew computation - Add tests covering matching, mismatched, zero-amount, and empty cases
1 parent b74e55d commit 854df38

5 files changed

Lines changed: 550 additions & 14 deletions

File tree

BOB_GAPS.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,20 +50,21 @@ response.
5050

5151
---
5252

53-
## 4. Transaction status (moneyFlew)
53+
## 4. Transaction status (moneyFlew) - RESOLVED
5454

5555
**What**: Bob has per-transaction `executed` status from log events, but does NOT
5656
expose the `MoneyFlew` bit array that the node connector provides.
5757

58-
**Current behavior**: The archiver returns empty transaction status (all moneyFlew
59-
bits set to false) when using bob. The `txstatus` validation is effectively skipped.
58+
**Resolution**: Implemented moneyFlew computation from bob's log events. The archiver
59+
fetches all logs for a tick via `GET /log/{epoch}/{logIdStart}/{logIdEnd}`, groups
60+
QU_TRANSFER events by transaction hash, and applies the algorithm:
61+
```
62+
moneyFlew = (tx.amount == first_event.amount) && (tx.src == first_event.src)
63+
&& (tx.dst == first_event.dst) && (tx.tick == first_event.tick)
64+
```
65+
Only transactions with `amount > 0` are candidates; others default to false.
6066

61-
**Impact**: Medium. The `moneyFlew` field is used by API consumers to determine if
62-
a transaction's side effects were executed. Currently all transactions appear as
63-
"not executed" when using bob.
64-
65-
**Future fix**: Use `qubic_getTransactionReceipt` to fetch per-transaction execution
66-
status, then reconstruct the MoneyFlew bit array from the `executed` field.
67+
See: `network/bob/moneyflew.go`
6768

6869
---
6970

network/bob/fetcher.go

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ type BobDataFetcher struct {
1919
mu sync.Mutex
2020
cachedTick uint32
2121
cachedTickResp *bobTickResponse
22+
23+
// Per-cycle cache for transactions (from GetTickTransactions).
24+
// Used by GetTxStatus to compute moneyFlew without re-fetching.
25+
cachedTxsTick uint32
26+
cachedTxs types.Transactions
2227
}
2328

2429
// NewBobDataFetcher creates a new BobDataFetcher.
@@ -108,6 +113,12 @@ func (b *BobDataFetcher) GetTickTransactions(ctx context.Context, tickNumber uin
108113
txs = append(txs, tx)
109114
}
110115

116+
// Cache for use by GetTxStatus
117+
b.mu.Lock()
118+
b.cachedTxsTick = tickNumber
119+
b.cachedTxs = txs
120+
b.mu.Unlock()
121+
111122
return txs, nil
112123
}
113124

@@ -125,17 +136,37 @@ func (b *BobDataFetcher) GetComputors(ctx context.Context) (types.Computors, err
125136
return convertComputors(compsResp.Computors, compsResp.Epoch)
126137
}
127138

128-
func (b *BobDataFetcher) GetTxStatus(_ context.Context, _ uint32) (types.TransactionStatus, bool, error) {
129-
// TODO: Compute moneyFlew from bob's log events using qubic_getTransactionReceipt.
130-
// For now, return empty status indicating data is not available.
131-
return types.TransactionStatus{}, false, nil
139+
func (b *BobDataFetcher) GetTxStatus(ctx context.Context, tick uint32) (types.TransactionStatus, bool, error) {
140+
tickResp, err := b.getTickResponse(ctx, tick)
141+
if err != nil {
142+
return types.TransactionStatus{}, false, fmt.Errorf("getting tick response for tx status: %w", err)
143+
}
144+
145+
// Get cached transactions (populated by GetTickTransactions which runs before GetTxStatus)
146+
b.mu.Lock()
147+
txs := b.cachedTxs
148+
txsTick := b.cachedTxsTick
149+
b.mu.Unlock()
150+
151+
if txsTick != tick || txs == nil {
152+
return types.TransactionStatus{}, false, fmt.Errorf("transactions for tick %d not cached (have tick %d)", tick, txsTick)
153+
}
154+
155+
status, err := computeMoneyFlew(ctx, b.client, tickResp, txs, tick)
156+
if err != nil {
157+
return types.TransactionStatus{}, false, fmt.Errorf("computing moneyFlew: %w", err)
158+
}
159+
160+
return status, true, nil
132161
}
133162

134163
func (b *BobDataFetcher) Release(_ error) {
135-
// Clear the per-cycle cache
164+
// Clear per-cycle caches
136165
b.mu.Lock()
137166
b.cachedTick = 0
138167
b.cachedTickResp = nil
168+
b.cachedTxsTick = 0
169+
b.cachedTxs = nil
139170
b.mu.Unlock()
140171
}
141172

network/bob/moneyflew.go

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
package bob
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"log"
8+
"strings"
9+
10+
"github.com/qubic/go-node-connector/types"
11+
)
12+
13+
const quTransferLogType = 0
14+
15+
// firstQUTransfer holds the fields from the first QU_TRANSFER event for a transaction.
16+
type firstQUTransfer struct {
17+
Source string // uppercase identity
18+
Destination string // uppercase identity
19+
Amount int64
20+
Tick uint32
21+
}
22+
23+
// computeMoneyFlew fetches logs for the given tick and computes the moneyFlew
24+
// status for each transaction by comparing the transaction's fields against its
25+
// first QU_TRANSFER log event.
26+
//
27+
// Algorithm (from bob maintainer):
28+
//
29+
// moneyFlew = (tx.amount == first_event.amount)
30+
// && (tx.src == first_event.src)
31+
// && (tx.dst == first_event.dst)
32+
// && (tx.tick == first_event.tick)
33+
//
34+
// Only transactions with amount > 0 are candidates; others default to false.
35+
func computeMoneyFlew(ctx context.Context, client *Client, tickResp *bobTickResponse, txs types.Transactions, tickNumber uint32) (types.TransactionStatus, error) {
36+
epoch := tickResp.TickData.Epoch
37+
logIdStart := tickResp.TickData.LogIdStart
38+
logIdEnd := tickResp.TickData.LogIdEnd
39+
40+
// Build transaction lookup: txHash -> Transaction
41+
txByHash := make(map[string]*types.Transaction, len(txs))
42+
for i := range txs {
43+
txID, err := txs[i].ID()
44+
if err != nil {
45+
return types.TransactionStatus{}, fmt.Errorf("computing tx ID for index %d: %w", i, err)
46+
}
47+
txByHash[txID] = &txs[i]
48+
}
49+
50+
// Collect non-zero transaction digests and build the digest list
51+
var digestList [][32]byte
52+
for i, digestStr := range tickResp.TickData.TransactionDigests {
53+
if digestStr == "" {
54+
continue
55+
}
56+
digest, err := qubicHashToBytes32(digestStr)
57+
if err != nil {
58+
return types.TransactionStatus{}, fmt.Errorf("converting digest[%d]: %w", i, err)
59+
}
60+
digestList = append(digestList, digest)
61+
}
62+
63+
// Build the MoneyFlew bit array
64+
var moneyFlew [128]byte
65+
66+
// If there are no logs, return with all-zero moneyFlew
67+
if logIdStart < 0 || logIdEnd < logIdStart {
68+
return types.TransactionStatus{
69+
CurrentTickOfNode: tickNumber,
70+
Tick: tickNumber,
71+
TxCount: uint32(len(digestList)),
72+
MoneyFlew: moneyFlew,
73+
TransactionDigests: digestList,
74+
}, nil
75+
}
76+
77+
// Fetch all logs for this tick
78+
firstTransfers, err := fetchFirstQUTransfers(ctx, client, epoch, logIdStart, logIdEnd)
79+
if err != nil {
80+
return types.TransactionStatus{}, fmt.Errorf("fetching logs for tick %d: %w", tickNumber, err)
81+
}
82+
83+
// For each transaction digest, determine moneyFlew
84+
for i, digest := range digestList {
85+
// Convert digest to txHash (60-char lowercase)
86+
var id types.Identity
87+
id, err := id.FromPubKey(digest, true)
88+
if err != nil {
89+
log.Printf("[WARN] failed to convert digest[%d] to identity: %v", i, err)
90+
continue
91+
}
92+
txHash := id.String()
93+
94+
// Look up the actual transaction
95+
tx, hasTx := txByHash[txHash]
96+
if !hasTx || tx.Amount <= 0 {
97+
continue // moneyFlew stays false
98+
}
99+
100+
// Look up the first QU_TRANSFER for this tx
101+
transfer, hasLog := firstTransfers[txHash]
102+
if !hasLog {
103+
continue // no QU_TRANSFER event -> moneyFlew = false
104+
}
105+
106+
// Convert tx public keys to uppercase identities for comparison
107+
var srcID types.Identity
108+
srcID, err = srcID.FromPubKey(tx.SourcePublicKey, false)
109+
if err != nil {
110+
log.Printf("[WARN] failed to convert source pubkey for tx %s: %v", txHash, err)
111+
continue
112+
}
113+
var dstID types.Identity
114+
dstID, err = dstID.FromPubKey(tx.DestinationPublicKey, false)
115+
if err != nil {
116+
log.Printf("[WARN] failed to convert dest pubkey for tx %s: %v", txHash, err)
117+
continue
118+
}
119+
120+
// Apply the moneyFlew algorithm
121+
if tx.Amount == transfer.Amount &&
122+
string(srcID) == transfer.Source &&
123+
string(dstID) == transfer.Destination &&
124+
tx.Tick == transfer.Tick {
125+
setMoneyFlewBit(&moneyFlew, i)
126+
}
127+
}
128+
129+
return types.TransactionStatus{
130+
CurrentTickOfNode: tickNumber,
131+
Tick: tickNumber,
132+
TxCount: uint32(len(digestList)),
133+
MoneyFlew: moneyFlew,
134+
TransactionDigests: digestList,
135+
}, nil
136+
}
137+
138+
// fetchFirstQUTransfers fetches all logs for the tick and returns a map of
139+
// txHash -> first QU_TRANSFER event for that transaction.
140+
func fetchFirstQUTransfers(ctx context.Context, client *Client, epoch uint16, logIdStart, logIdEnd int64) (map[string]firstQUTransfer, error) {
141+
path := fmt.Sprintf("/log/%d/%d/%d", epoch, logIdStart, logIdEnd)
142+
body, err := client.RESTGet(ctx, path)
143+
if err != nil {
144+
return nil, fmt.Errorf("GET %s: %w", path, err)
145+
}
146+
147+
var logs []bobLogEvent
148+
if err := json.Unmarshal(body, &logs); err != nil {
149+
return nil, fmt.Errorf("unmarshalling logs: %w", err)
150+
}
151+
152+
result := make(map[string]firstQUTransfer)
153+
for _, logEvent := range logs {
154+
if !logEvent.OK {
155+
continue
156+
}
157+
if logEvent.Type != quTransferLogType {
158+
continue
159+
}
160+
txHash := strings.ToLower(logEvent.TxHash)
161+
if txHash == "" {
162+
continue
163+
}
164+
// Only keep the first QU_TRANSFER per transaction
165+
if _, exists := result[txHash]; exists {
166+
continue
167+
}
168+
result[txHash] = firstQUTransfer{
169+
Source: logEvent.Body.From,
170+
Destination: logEvent.Body.To,
171+
Amount: logEvent.Body.Amount,
172+
Tick: logEvent.Tick,
173+
}
174+
}
175+
176+
return result, nil
177+
}
178+
179+
// setMoneyFlewBit sets bit at position index in the MoneyFlew byte array.
180+
func setMoneyFlewBit(moneyFlew *[128]byte, index int) {
181+
bytePos := index / 8
182+
bitPos := index % 8
183+
moneyFlew[bytePos] |= 1 << bitPos
184+
}

0 commit comments

Comments
 (0)