Skip to content

Commit bab6f37

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 f97b850 commit bab6f37

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
@@ -60,14 +60,14 @@ export async function farmStored(
6060

6161
await ctx.store.save<Farm>(newFarm)
6262

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

6868
const newIP = new PublicIp()
6969

70-
newIP.id = item.event.id
70+
newIP.id = item.event.id + '-' + index
7171

7272
newIP.ip = validateString(ctx, ip.ip.toString())
7373
newIP.gateway = validateString(ctx, ip.gateway.toString())
@@ -167,26 +167,27 @@ export async function farmUpdated(
167167
savedFarm.certification = certification
168168

169169
let eventPublicIPs = farmUpdatedEventParsed.publicIps
170-
farmUpdatedEventParsed.publicIps.forEach(async ip => {
170+
for (let index = 0; index < farmUpdatedEventParsed.publicIps.length; index++) {
171+
const ip = farmUpdatedEventParsed.publicIps[index]
171172
if (!checkIPs(ctx, ip.ip.toString(), ip.gateway.toString())) {
172-
return
173+
continue
173174
}
174175
if (ip.ip.toString().indexOf('\x00') >= 0) {
175-
return
176+
continue
176177
}
177178
const savedIP = await ctx.store.get(PublicIp, { where: { ip: ip.ip.toString() }, relations: { farm: true } })
178-
// ip is already there in storage, don't save it again
179179
if (savedIP) {
180-
savedIP.ip = validateString(ctx, ip.ip.toString()) // not effective, but for since we already check for \x00
181-
savedIP.gateway = validateString(ctx, ip.gateway.toString())
182180
if (savedIP.farm.id !== savedFarm.id) {
183-
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}`);
181+
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}`);
182+
continue
184183
}
184+
// Same farm — update gateway
185+
savedIP.gateway = validateString(ctx, ip.gateway.toString())
186+
savedIP.contractId = ip.contractId
185187
await ctx.store.save<PublicIp>(savedIP)
186188
} else {
187-
188189
const newIP = new PublicIp()
189-
newIP.id = item.event.id
190+
newIP.id = item.event.id + '-' + index
190191
newIP.ip = validateString(ctx, ip.ip.toString())
191192
newIP.gateway = validateString(ctx, ip.gateway.toString())
192193
newIP.contractId = ip.contractId
@@ -198,21 +199,18 @@ export async function farmUpdated(
198199
savedFarm.publicIPs.push(newIP)
199200
ctx.log.debug({ eventName: item.name, ip: ip.ip.toString() }, `PublicIP: ${ip.ip.toString()} added with farm id: ${savedFarm.farmID}`);
200201
}
201-
})
202+
}
202203

203204
await ctx.store.save<Farm>(savedFarm)
204205

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

217215
let farm = item.event.args as Farm
218216
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)