Skip to content

Commit

Permalink
Handle forked chains
Browse files Browse the repository at this point in the history
  • Loading branch information
corrideat committed Jan 25, 2025
1 parent 76b6a9c commit b6e8a9d
Show file tree
Hide file tree
Showing 6 changed files with 51 additions and 10 deletions.
13 changes: 12 additions & 1 deletion frontend/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,19 @@ async function startApp () {
Vue.set(reverseCache, value, name)
})

sbp('okTurtles.events/on', SERIOUS_ERROR, (error) => {
sbp('okTurtles.events/on', SERIOUS_ERROR, (error, { contractID }) => {
sbp('gi.ui/seriousErrorBanner', error)
if (error?.name === 'ChelErrorForkedChain') {
const rootState = sbp('state/vuex/state')
const type = rootState.contracts[contractID].type || '(unknown)'
const retry = confirm(L('There was a serious issue when trying to sync the contract with ID {contractID} of type {type}. The data we received doesn\'t match what we have on file, which could mean there has been tampering or possibly a loss of data on the server. Unfortunately, we can\'t recover the original data. However, if you think this might be an error, you can try deleting your local records and syncing the contract again.\n\nDo you want to attempt this?', { contractID, type }))

if (retry) {
sbp('chelonia/contract/sync', contractID, { resync: true }).catch((e) => {
console.error('Error during re-sync', contractID, e)
})
}
}
if (process.env.CI) {
Promise.reject(error)
}
Expand Down
13 changes: 10 additions & 3 deletions frontend/setupChelonia.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,15 @@ const setupChelonia = async (): Promise<*> => {
}
},
hooks: {
syncContractError: (e: Error, contractID: string) => {
if (['ChelErrorUnrecoverable', 'ChelErrorForkedChain'].includes(e?.name)) {
sbp('okTurtles.events/emit', SERIOUS_ERROR, e, { contractID })
}
},
handleEventError: (e: Error, message: GIMessage) => {
if (e.name === 'ChelErrorUnrecoverable') {
sbp('okTurtles.events/emit', SERIOUS_ERROR, e)
if (['ChelErrorUnrecoverable', 'ChelErrorForkedChain'].includes(e?.name)) {
const contractID = message.contractID()
sbp('okTurtles.events/emit', SERIOUS_ERROR, e, { contractID, message })
}
if (sbp('okTurtles.data/get', 'sideEffectError') !== message.hash()) {
// Avoid duplicate notifications for the same message.
Expand All @@ -161,7 +167,8 @@ const setupChelonia = async (): Promise<*> => {
errorNotification('process', e, message)
},
sideEffectError: (e: Error, message: GIMessage) => {
sbp('okTurtles.events/emit', SERIOUS_ERROR, e)
const contractID = message.contractID()
sbp('okTurtles.events/emit', SERIOUS_ERROR, e, { contractID, message })
sbp('okTurtles.data/set', 'sideEffectError', message.hash())
errorNotification('sideEffect', e, message)
}
Expand Down
1 change: 1 addition & 0 deletions shared/domains/chelonia/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const ChelErrorDBBadPreviousHEAD: typeof Error = ChelErrorGenerator('Chel
export const ChelErrorDBConnection: typeof Error = ChelErrorGenerator('ChelErrorDBConnection')
export const ChelErrorUnexpected: typeof Error = ChelErrorGenerator('ChelErrorUnexpected')
export const ChelErrorUnrecoverable: typeof Error = ChelErrorGenerator('ChelErrorUnrecoverable')
export const ChelErrorForkedChain: typeof Error = ChelErrorGenerator('ChelErrorForkedChain')
export const ChelErrorDecryptionError: typeof Error = ChelErrorGenerator('ChelErrorDecryptionError')
export const ChelErrorDecryptionKeyNotFound: typeof Error = ChelErrorGenerator('ChelErrorDecryptionKeyNotFound', ChelErrorDecryptionError)
export const ChelErrorSignatureError: typeof Error = ChelErrorGenerator('ChelErrorSignatureError')
Expand Down
12 changes: 9 additions & 3 deletions shared/domains/chelonia/internals.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { deserializeKey, keyId, verifySignature } from './crypto.js'
import './db.js'
import { encryptedIncomingData, encryptedOutgoingData, unwrapMaybeEncryptedData } from './encryptedData.js'
import type { EncryptedData } from './encryptedData.js'
import { ChelErrorUnrecoverable, ChelErrorWarning, ChelErrorDBBadPreviousHEAD, ChelErrorAlreadyProcessed, ChelErrorFetchServerTimeFailed } from './errors.js'
import { ChelErrorUnrecoverable, ChelErrorWarning, ChelErrorDBBadPreviousHEAD, ChelErrorAlreadyProcessed, ChelErrorFetchServerTimeFailed, ChelErrorForkedChain } from './errors.js'
import { CONTRACTS_MODIFIED, CONTRACT_HAS_RECEIVED_KEYS, CONTRACT_IS_SYNCING, EVENT_HANDLED, EVENT_PUBLISHED, EVENT_PUBLISHING_ERROR } from './events.js'
import { buildShelterAuthorizationHeader, findKeyIdByName, findSuitablePublicKeyIds, findSuitableSecretKeyId, getContractIDfromKeyId, keyAdditionProcessor, recreateEvent, validateKeyPermissions, validateKeyAddPermissions, validateKeyDelPermissions, validateKeyUpdatePermissions } from './utils.js'
import { isSignedData, signedIncomingData } from './signedData.js'
Expand Down Expand Up @@ -1286,7 +1286,7 @@ export default (sbp('sbp/selectors/register', {
const { done, value: event } = await eventReader.read()
if (done) {
if (!latestHashFound) {
throw new ChelErrorUnrecoverable(`expected hash ${latestHEAD} in list of events for contract ${contractID}`)
throw new ChelErrorForkedChain(`expected hash ${latestHEAD} in list of events for contract ${contractID}`)
}
break
}
Expand Down Expand Up @@ -1864,7 +1864,13 @@ 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' || message.isFirstMessage()) throw e
if (
e.name === 'ChelErrorUnrecoverable' ||
e.name === 'ChelErrorForkedChain' ||
message.isFirstMessage()
) {
throw e
}
}

// process any side-effects (these must never result in any mutation to the contract state!)
Expand Down
8 changes: 6 additions & 2 deletions shared/domains/chelonia/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { INVITE_STATUS } from './constants.js'
import { deserializeKey, serializeKey, sign, verifySignature } from './crypto.js'
import type { EncryptedData } from './encryptedData.js'
import { unwrapMaybeEncryptedData } from './encryptedData.js'
import { ChelErrorWarning } from './errors.js'
import { ChelErrorForkedChain, ChelErrorWarning } from './errors.js'
import { CONTRACT_IS_PENDING_KEY_REQUESTS } from './events.js'
import type { SignedData } from './signedData.js'
import { isSignedData } from './signedData.js'
Expand Down Expand Up @@ -688,7 +688,11 @@ export function eventsAfter (contractID: string, sinceHeight: number, limit?: nu
const hash = GIMessage.deserializeHEAD(currentEvent).hash
const height = GIMessage.deserializeHEAD(currentEvent).head.height
if (height !== sinceHeight || (sinceHash && sinceHash !== hash)) {
controller.error(new Error('hash() !== since'))
if (height === sinceHeight && sinceHash && sinceHash !== hash) {
controller.error(new ChelErrorForkedChain('Forked chain: hash() !== since'))
} else {
controller.error(new Error('hash() !== since'))
}
return
}
}
Expand Down
14 changes: 13 additions & 1 deletion shared/serdes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,17 @@ export const serializer = (data: any): any => {
}
// Error, Blob, File, etc. are supported by structuredClone but not by JSON
// We mark these as 'refs', so that the reviver can undo this transformation
if (value instanceof Error || value instanceof Blob || value instanceof File) {
if (value instanceof Blob || value instanceof File) {
const pos = verbatim.length
verbatim[verbatim.length] = value
return rawResult(['_', '_ref', pos])
}
// However, Error cloning doesn't preserve `.name`
if (value instanceof Error) {
const pos = verbatim.length
verbatim[verbatim.length] = value
return rawResult(['_', '_err', rawResult(['_', '_ref', pos]), value.name])
}
// Same for other types supported by structuredClone but not JSON
if (value instanceof MessagePort || value instanceof ReadableStream || value instanceof WritableStream || value instanceof ArrayBuffer) {
const pos = verbatim.length
Expand Down Expand Up @@ -167,6 +173,12 @@ export const deserializer = (data: any): any => {
// These are literal values, return them
case '_ref':
return verbatim[value[2]]
case '_err': {
if (value[2].name !== value[3]) {
value[2].name = value[3]
}
return value[2]
}
// These were functions converted to a MessagePort. Convert them on this
// end back into functions using that port.
case '_fn': {
Expand Down

0 comments on commit b6e8a9d

Please sign in to comment.