Skip to content

Commit b2e5f55

Browse files
sameh-faroukclaude
andcommitted
fix: resolve data loss bugs in farm IP, node interface, and contract mapping handlers
- B1: Replace async forEach with for...of in farmUpdated to fix race conditions on IP saves/removals - B2: Append IP string to PublicIp ID in farmStored/farmUpdated to prevent overwrites when multiple IPs exist - B3: Append interface name to Interfaces ID in nodeStored (matching nodeUpdated pattern) to prevent overwrites - B4: Add undefined guard for contractID in grace period handlers to prevent unpredictable queries - B5: Fix handlePublicConfigV105 fallthrough creating ghost PublicConfig with null fields when config is undefined - B6: Save inline PublicConfig in nodeStored for spec v9 nodes that carried publicConfig at creation time - Fix cross-farm duplicate IP handling: properly skip instead of silently overwriting gateway - Add '-' separator to composite IDs for readability (interfaces and public IPs) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 94fdaf4 commit b2e5f55

3 files changed

Lines changed: 41 additions & 29 deletions

File tree

src/mappings/contracts.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,11 @@ export async function contractGracePeriodStarted(
469469
contractID = contractGracePeriodStartedEvent.asV105.contractId
470470
}
471471

472+
if (contractID === undefined) {
473+
ctx.log.error({ eventName: item.name }, `found ContractGracePeriodStarted with unknown version! make sure types are updated`);
474+
return
475+
}
476+
472477
const savedNodeContract = await ctx.store.get(NodeContract, { where: { contractID } })
473478
if (savedNodeContract) {
474479
savedNodeContract.state = ContractState.GracePeriod
@@ -505,6 +510,11 @@ export async function contractGracePeriodEnded(
505510
contractID = contractGracePeriodEnded.asV105.contractId
506511
}
507512

513+
if (contractID === undefined) {
514+
ctx.log.error({ eventName: item.name }, `found ContractGracePeriodEnded with unknown version! make sure types are updated`);
515+
return
516+
}
517+
508518
const savedNodeContract = await ctx.store.get(NodeContract, { where: { contractID } })
509519
if (savedNodeContract) {
510520
savedNodeContract.state = ContractState.Created

src/mappings/farms.ts

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,14 @@ export async function farmStored(
6262

6363
await ctx.store.save<Farm>(newFarm)
6464

65-
const ipPromises = farmStoredEventParsed.publicIps.map(ip => {
65+
const ipPromises = farmStoredEventParsed.publicIps.map((ip, index) => {
6666
if (!checkIPs(ctx, ip.ip.toString(), ip.gateway.toString())) {
6767
return Promise.resolve()
6868
}
6969

7070
const newIP = new PublicIp()
7171

72-
newIP.id = item.event.id
72+
newIP.id = item.event.id + '-' + index
7373

7474
newIP.ip = validateString(ctx, ip.ip.toString())
7575
newIP.gateway = validateString(ctx, ip.gateway.toString())
@@ -170,26 +170,27 @@ export async function farmUpdated(
170170
savedFarm.certification = certification
171171

172172
let eventPublicIPs = farmUpdatedEventParsed.publicIps
173-
farmUpdatedEventParsed.publicIps.forEach(async ip => {
173+
for (let index = 0; index < farmUpdatedEventParsed.publicIps.length; index++) {
174+
const ip = farmUpdatedEventParsed.publicIps[index]
174175
if (!checkIPs(ctx, ip.ip.toString(), ip.gateway.toString())) {
175-
return
176+
continue
176177
}
177178
if (ip.ip.toString().indexOf('\x00') >= 0) {
178-
return
179+
continue
179180
}
180181
const savedIP = await ctx.store.get(PublicIp, { where: { ip: ip.ip.toString() }, relations: { farm: true } })
181-
// ip is already there in storage, don't save it again
182182
if (savedIP) {
183-
savedIP.ip = validateString(ctx, ip.ip.toString()) // not effective, but for since we already check for \x00
184-
savedIP.gateway = validateString(ctx, ip.gateway.toString())
185183
if (savedIP.farm.id !== savedFarm.id) {
186-
ctx.log.error({ eventName: item.name, ip: ip.ip.toString() }, `PublicIP: ${ip.ip.toString()} already exists on farm: ${savedIP.farm.farmID}, skiped adding it to farm with ID: ${savedFarm.farmID}`);
184+
ctx.log.error({ eventName: item.name, ip: ip.ip.toString() }, `PublicIP: ${ip.ip.toString()} already exists on farm: ${savedIP.farm.farmID}, skipped adding it to farm with ID: ${savedFarm.farmID}`);
185+
continue
187186
}
187+
// Same farm — update gateway
188+
savedIP.gateway = validateString(ctx, ip.gateway.toString())
189+
savedIP.contractId = ip.contractId
188190
await ctx.store.save<PublicIp>(savedIP)
189191
} else {
190-
191192
const newIP = new PublicIp()
192-
newIP.id = item.event.id
193+
newIP.id = item.event.id + '-' + index
193194
newIP.ip = validateString(ctx, ip.ip.toString())
194195
newIP.gateway = validateString(ctx, ip.gateway.toString())
195196
newIP.contractId = ip.contractId
@@ -201,21 +202,18 @@ export async function farmUpdated(
201202
savedFarm.publicIPs.push(newIP)
202203
ctx.log.debug({ eventName: item.name, ip: ip.ip.toString() }, `PublicIP: ${ip.ip.toString()} added with farm id: ${savedFarm.farmID}`);
203204
}
204-
})
205+
}
205206

206207
await ctx.store.save<Farm>(savedFarm)
207208

208209
const publicIPsOfFarm = await ctx.store.find<PublicIp>(PublicIp, { where: { farm: { id: savedFarm.id } }, relations: { farm: true } })
209-
publicIPsOfFarm.forEach(async ip => {
210+
for (const ip of publicIPsOfFarm) {
210211
if (eventPublicIPs.filter(eventIp => validateString(ctx, eventIp.ip.toString()) === ip.ip).length === 0) {
211212
// IP got removed from farm
212213
await ctx.store.remove<PublicIp>(ip)
213-
// remove ip from savedFarm.publicIPs
214-
// savedFarm.publicIPs = savedFarm.publicIPs.filter(savedIP => savedIP.id !== ip.id)
215-
// TODO: check if we need this code above? or Cascade delete involving here?
216214
ctx.log.debug({ eventName: item.name, ip: ip.ip.toString() }, `PublicIP: ${ip.ip.toString()} in farm: ${savedFarm.farmID} removed from publicIPs`);
217215
}
218-
})
216+
}
219217

220218
let farm = item.event.args as Farm
221219
if (farm.dedicatedFarm) {

src/mappings/nodes.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,16 @@ export async function nodeStored(
7979
await ctx.store.save<Node>(newNode)
8080

8181
const pubConfig = getNodePublicConfig(ctx, node)
82-
const newPubConfig = new PublicConfig()
83-
newPubConfig.id = item.event.id
84-
newPubConfig.ipv4 = pubConfig?.ip4
85-
newPubConfig.gw4 = pubConfig?.gw4
86-
newPubConfig.ipv6 = pubConfig?.ip6
87-
newPubConfig.gw6 = pubConfig?.gw6
88-
newPubConfig.node = newNode
82+
if (pubConfig) {
83+
const newPubConfig = new PublicConfig()
84+
newPubConfig.id = item.event.id
85+
newPubConfig.ipv4 = pubConfig.ip4
86+
newPubConfig.gw4 = pubConfig.gw4
87+
newPubConfig.ipv6 = pubConfig.ip6
88+
newPubConfig.gw6 = pubConfig.gw6
89+
newPubConfig.node = newNode
90+
await ctx.store.save<PublicConfig>(newPubConfig)
91+
}
8992

9093
if (node.isV43) {
9194
const nodeAsV43 = node.asV43
@@ -136,9 +139,9 @@ export async function nodeStored(
136139

137140
newNode.interfaces = []
138141

139-
const interfacesPromisses = nodeEvent.interfaces.map(async intf => {
142+
const interfacesPromisses = nodeEvent.interfaces.map(async (intf, index) => {
140143
const newInterface = new Interfaces()
141-
newInterface.id = item.event.id
144+
newInterface.id = item.event.id + '-' + index
142145
newInterface.node = newNode
143146
newInterface.name = validateString(ctx, intf.name.toString())
144147
newInterface.mac = validateString(ctx, intf.mac.toString())
@@ -325,9 +328,9 @@ export async function nodeUpdated(
325328
await ctx.store.remove(nodeIfs)
326329

327330
// Save ones from update event
328-
await Promise.all(nodeEvent.interfaces.map(async intf => {
331+
await Promise.all(nodeEvent.interfaces.map(async (intf, index) => {
329332
const newInterface = new Interfaces()
330-
newInterface.id = item.event.id + validateString(ctx, intf.name.toString())
333+
newInterface.id = item.event.id + '-' + index
331334
newInterface.name = validateString(ctx, intf.name.toString())
332335
newInterface.mac = validateString(ctx, intf.mac.toString())
333336
newInterface.ips = intf.ips.map(ip => validateString(ctx, ip.toString())).join(',')
@@ -462,8 +465,9 @@ async function handlePublicConfigV105(ctx: Ctx, eventID: string, nodeID: number,
462465
if (!config) {
463466
const pubConfig = await ctx.store.get(PublicConfig, { where: { node: { nodeID } }, relations: { node: true } })
464467
if (pubConfig) {
465-
return await ctx.store.remove(pubConfig)
468+
await ctx.store.remove(pubConfig)
466469
}
470+
return
467471
}
468472

469473
const savedNode = await ctx.store.get(Node, { where: { nodeID: nodeID }, relations: { location: true, interfaces: true } })

0 commit comments

Comments
 (0)