Skip to content

Commit

Permalink
Periodic clock sync
Browse files Browse the repository at this point in the history
  • Loading branch information
corrideat committed Apr 2, 2024
1 parent b4735b1 commit 7fb549a
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 2 deletions.
4 changes: 4 additions & 0 deletions shared/domains/chelonia/chelonia.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type { EncryptedData } from './encryptedData.js'
import { isSignedData, signedIncomingData, signedOutgoingData, signedOutgoingDataWithRawKey } from './signedData.js'
import './internals.js'
import './files.js'
import './time-sync.js'
import { buildShelterAuthorizationHeader, eventsAfter, findForeignKeysByContractID, findKeyIdByName, findRevokedKeyIdsByName, findSuitableSecretKeyId, getContractIDfromKeyId } from './utils.js'

// TODO: define ChelContractType for /defineContract
Expand Down Expand Up @@ -328,6 +329,7 @@ export default (sbp('sbp/selectors/register', {
}
},
'chelonia/reset': async function (postCleanupFn) {
sbp('chelonia/private/stopClockSync')
// wait for any pending sync operations to finish before saving
await sbp('chelonia/contract/waitPublish')
await sbp('chelonia/contract/wait')
Expand All @@ -349,6 +351,7 @@ export default (sbp('sbp/selectors/register', {
this.subscriptionSet.clear()
sbp('chelonia/clearTransientSecretKeys')
sbp('okTurtles.events/emit', CONTRACTS_MODIFIED, this.subscriptionSet)
sbp('chelonia/private/startClockSync')
},
'chelonia/storeSecretKeys': function (keysFn: () => {key: Key, transient?: boolean}[]) {
const rootState = sbp(this.config.stateSelector)
Expand Down Expand Up @@ -526,6 +529,7 @@ export default (sbp('sbp/selectors/register', {
// its console output until we have a better solution. Do not use for auth.
pubsubURL += `?debugID=${randomHexString(6)}`
}
sbp('chelonia/private/startClockSync')
this.pubsub = createClient(pubsubURL, {
...this.config.connectionOptions,
messageHandlers: {
Expand Down
6 changes: 5 additions & 1 deletion shared/domains/chelonia/files.js
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,11 @@ export default (sbp('sbp/selectors/register', {
method: 'POST',
signal: this.abortController.signal,
headers: new Headers([
['authorization', token ? `bearer ${token}` : buildShelterAuthorizationHeader.call(this, billableContractID)]
['authorization',
token
? `bearer ${token}`
// $FlowFixMe[incompatible-call]
: buildShelterAuthorizationHeader.call(this, billableContractID)]
])
})
if (!response.ok) {
Expand Down
95 changes: 95 additions & 0 deletions shared/domains/chelonia/time-sync.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import sbp from '@sbp/sbp'

let wallBase = Date.now()
let monotonicBase = performance.now()
// `undefined` means the sync process has been stopped, `null` that the current
// request has finished
let resyncTimeout
let watchdog

const syncServerTime = async function () {
// Get our current monotonic time
const newMonotonicBase = performance.now()
// Now, ask the server for the time
const time = await fetch(`${this.config.connectionURL}/time`, { signal: this.abortController.signal })
const requestTimeElapsed = performance.now()
if (requestTimeElapsed - newMonotonicBase > 1000) {
throw new Error('Error fetching server time: request took too long')
}
// If the request didn't succeed, report it
if (!time.ok) throw new Error('Error fetching server time')
const serverTime = (new Date(await time.text())).valueOf()
// If the value could not be parsed, report that as well
if (Number.isNaN(serverTime)) throw new Error('Unable to parse server time')
wallBase = serverTime
monotonicBase = newMonotonicBase
}

export default (sbp('sbp/selectors/register', {
'chelonia/private/startClockSync': function () {
// Default re-sync every 5 minutes
const resync = (delay: number = 300000) => {
// If there's another time sync process in progress, don't do anything
if (resyncTimeout !== null) return
const timeout = setTimeout(() => {
// Get the server time
syncServerTime.call(this).then(() => {
// Mark the process as finished
if (resyncTimeout === timeout) resyncTimeout = null
// And then restart the listener
resync()
}).catch(e => {
// If there was an error, log it and possibly attempt again
if (resyncTimeout === timeout) {
// In this case, it was the current task that failed
resyncTimeout = null
console.error('Error re-syncing server time; will re-attempt in 5s', e)
// Call resync again, with a shorter delay
setTimeout(() => resync(0), 5000)
} else {
// If there is already another attempt, just log it
console.error('Error re-syncing server time; another attempt is in progress', e)
}
})
}, delay)
resyncTimeout = timeout
}

let wallLast = Date.now()
let monotonicLast = performance.now()

// Watchdog to ensure our time doesn't drift. Periodically check for
// differences between the elapsed wall time and the elapsed monotonic
// time
watchdog = setInterval(() => {
const wallNow = Date.now()
const monotonicNow = performance.now()
const difference = Math.abs(Math.abs((wallNow - wallLast)) - Math.abs((monotonicNow - monotonicLast)))
// Tolerate up to a 10ms difference
if (difference > 10) {
clearTimeout(resyncTimeout)
resyncTimeout = null
resync(0)
}
wallLast = wallNow
monotonicLast = monotonicNow
}, 10000)

// Start the sync process
resyncTimeout = null
resync(0)
},
'chelonia/private/stopClockSync': () => {
if (resyncTimeout !== undefined) {
clearInterval(watchdog)
clearTimeout(resyncTimeout)
watchdog = undefined
resyncTimeout = undefined
}
},
'chelonia/time': function () {
const monotonicNow = performance.now()
const wallNow = wallBase - monotonicBase + monotonicNow
return (wallNow / 1e3 | 0)
}
}): string[])
2 changes: 1 addition & 1 deletion shared/domains/chelonia/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -723,7 +723,7 @@ export function buildShelterAuthorizationHeader (contractID: string, state?: Obj
globalThis.crypto.getRandomValues(nonceBytes)

// <contractID> <UNIX time>.<nonce>
const data = `${contractID} ${Date.now() / 1e3 | 0}.${Buffer.from(nonceBytes).toString('base64')}`
const data = `${contractID} ${sbp('chelonia/time')}.${Buffer.from(nonceBytes).toString('base64')}`

// shelter <contractID> <UNIX time>.<nonce>.<signature>
return `shelter ${data}.${sign(deserializedSAK, data)}`
Expand Down

0 comments on commit 7fb549a

Please sign in to comment.