Skip to content

Commit

Permalink
Fix #2103
Browse files Browse the repository at this point in the history
* Fix manifest loading
* Prevent processing events if OP_CONTRACT failed
* Prevent processing subsequent events after an error
  due to missing state
* Use reference counting for syncContractAndWatchKeys
  • Loading branch information
corrideat committed Jun 24, 2024
1 parent 9a64672 commit 1ffad5a
Show file tree
Hide file tree
Showing 2 changed files with 64 additions and 41 deletions.
4 changes: 4 additions & 0 deletions frontend/model/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,10 @@ const getters = {
Object.keys(state.contracts)
.filter(contractID => state.contracts[contractID].type === 'gi.contracts/identity')
.forEach(contractID => {
if (!state[contractID]) {
console.warn('[ourContactProfilesById] Missing state', contractID)
return
}
const attributes = state[contractID].attributes
if (attributes) { // NOTE: this is for fixing the error while syncing the identity contracts
const username = checkedUsername(state, attributes.username, contractID)
Expand Down
101 changes: 60 additions & 41 deletions shared/domains/chelonia/internals.js
Original file line number Diff line number Diff line change
Expand Up @@ -1059,7 +1059,10 @@ export default (sbp('sbp/selectors/register', {
}
if (!this.config.skipActionProcessing && !this.manifestToContract[manifestHash]) {
const rootState = sbp(this.config.stateSelector)
const contractName = has(rootState.contracts, contractID)
// Having rootState.contracts[contractID] is not enough to determine we
// have previously synced this contract, as reference counts are also
// stored there. Hence, we check for the presence of 'type'
const contractName = has(rootState.contracts, contractID) && has(rootState.contracts[contractID], 'type')
? rootState.contracts[contractID].type
: opT === GIMessage.OP_CONTRACT
? ((opV: any): GIOpContract).type
Expand Down Expand Up @@ -1293,53 +1296,69 @@ export default (sbp('sbp/selectors/register', {
return
}

// We check this.subscriptionSet to see if we're already
// subscribed to the contract; if not, we call sync.
if (!this.subscriptionSet.has(contractID)) {
await sbp('chelonia/private/in/syncContract', contractID)
}
try {
// Since we require a contract state for the duration of this operation,
// we call retain on this contract ephemerally and release it when we're
// done. This prevent the contract form being removed while this function
// is executing. We need to do reference counting manually here because
// we can't block on the contract queue
if (!this.ephemeralReferenceCount[contractID]) {
this.ephemeralReferenceCount[contractID] = 1
} else {
this.ephemeralReferenceCount[contractID] = this.ephemeralReferenceCount[contractID] + 1
}
// We check this.subscriptionSet to see if we're already
// subscribed to the contract; if not, we call sync.
if (!this.subscriptionSet.has(contractID)) {
await sbp('chelonia/private/in/syncContract', contractID)
}

const contractState = rootState[contractID]
const keysToDelete = []
const keysToUpdate = []
const contractState = rootState[contractID]
const keysToDelete = []
const keysToUpdate = []

pendingWatch.forEach(([keyName, externalId]) => {
pendingWatch.forEach(([keyName, externalId]) => {
// Does the key exist? If not, it has probably been removed and instead
// of waiting, we need to remove it ourselves
const keyId = findKeyIdByName(contractState, keyName)
if (!keyId) {
keysToDelete.push(externalId)
return
} else if (keyId !== externalId) {
const keyId = findKeyIdByName(contractState, keyName)
if (!keyId) {
keysToDelete.push(externalId)
return
} else if (keyId !== externalId) {
// Or, the key has been updated and we need to update it in the external
// contract as well
keysToUpdate.push(externalId)
}
keysToUpdate.push(externalId)
}

// Add keys to watchlist as another contract is waiting on these
// operations
if (!contractState._volatile) {
this.config.reactiveSet(contractState, '_volatile', Object.create(null, { watch: { value: [[keyName, externalContractID]], configurable: true, enumerable: true, writable: true } }))
} else {
if (!contractState._volatile.watch) this.config.reactiveSet(contractState._volatile, 'watch', [[keyName, externalContractID]])
if (Array.isArray(contractState._volatile.watch) && !contractState._volatile.watch.find((v) => v[0] === keyName && v[1] === externalContractID)) contractState._volatile.watch.push([keyName, externalContractID])
}
})
// Add keys to watchlist as another contract is waiting on these
// operations
if (!contractState._volatile) {
this.config.reactiveSet(contractState, '_volatile', Object.create(null, { watch: { value: [[keyName, externalContractID]], configurable: true, enumerable: true, writable: true } }))
} else {
if (!contractState._volatile.watch) this.config.reactiveSet(contractState._volatile, 'watch', [[keyName, externalContractID]])
if (Array.isArray(contractState._volatile.watch) && !contractState._volatile.watch.find((v) => v[0] === keyName && v[1] === externalContractID)) contractState._volatile.watch.push([keyName, externalContractID])
}
})

// If there are keys that need to be revoked, queue an event to handle the
// deletion
if (keysToDelete.length || keysToUpdate.length) {
if (!externalContractState._volatile) {
this.config.reactiveSet(externalContractState, '_volatile', Object.create(null))
}
if (!externalContractState._volatile.pendingKeyRevocations) {
this.config.reactiveSet(externalContractState._volatile, 'pendingKeyRevocations', Object.create(null))
}
keysToDelete.forEach((id) => this.config.reactiveSet(externalContractState._volatile.pendingKeyRevocations, id, 'del'))
keysToUpdate.forEach((id) => this.config.reactiveSet(externalContractState._volatile.pendingKeyRevocations, id, true))
// If there are keys that need to be revoked, queue an event to handle the
// deletion
if (keysToDelete.length || keysToUpdate.length) {
if (!externalContractState._volatile) {
this.config.reactiveSet(externalContractState, '_volatile', Object.create(null))
}
if (!externalContractState._volatile.pendingKeyRevocations) {
this.config.reactiveSet(externalContractState._volatile, 'pendingKeyRevocations', Object.create(null))
}
keysToDelete.forEach((id) => this.config.reactiveSet(externalContractState._volatile.pendingKeyRevocations, id, 'del'))
keysToUpdate.forEach((id) => this.config.reactiveSet(externalContractState._volatile.pendingKeyRevocations, id, true))

sbp('chelonia/private/queueEvent', externalContractID, ['chelonia/private/deleteOrRotateRevokedKeys', externalContractID]).catch((e) => {
console.error(`Error at deleteOrRotateRevokedKeys for contractID ${contractID} and externalContractID ${externalContractID}`, e)
sbp('chelonia/private/queueEvent', externalContractID, ['chelonia/private/deleteOrRotateRevokedKeys', externalContractID]).catch((e) => {
console.error(`Error at deleteOrRotateRevokedKeys for contractID ${contractID} and externalContractID ${externalContractID}`, e)
})
}
} finally {
sbp('chelonia/contract/release', contractID, { ephemeral: true }).catch((e) => {
console.warn('[chelonia/private/in/syncContractAndWatchKeys] Error calling release', contractID, e)
})
}
},
Expand Down Expand Up @@ -1696,7 +1715,7 @@ export default (sbp('sbp/selectors/register', {
throw new Error(`[chelonia] Wrong contract ID. Expected ${contractID} but got ${message.contractID()}`)
}
if (!message.isFirstMessage() && (!has(state.contracts, contractID) || !has(state, contractID))) {
throw new Error('The event is not for a first message but the contract state is missing')
throw new ChelErrorUnrecoverable('The event is not for a first message but the contract state is missing')
}
preHandleEvent?.(message)
// the order the following actions are done is critically important!
Expand Down Expand Up @@ -1740,7 +1759,7 @@ export default (sbp('sbp/selectors/register', {
processingErrored = e?.name !== 'ChelErrorWarning'
this.config.hooks.processError?.(e, message, getMsgMeta(message, contractID, state))
// special error that prevents the head from being updated, effectively killing the contract
if (e.name === 'ChelErrorUnrecoverable') throw e
if (e.name === 'ChelErrorUnrecoverable' || message.isFirstMessage()) throw e
}

// process any side-effects (these must never result in any mutation to the contract state!)
Expand Down

0 comments on commit 1ffad5a

Please sign in to comment.