Conversation
There was a problem hiding this comment.
Prelim machine review, ran it through two:
Opus 4.5
Details
Critical Bug: Missing Null Check
In the chelonia/private/deleteOrRotateRevokedKeys function, there's a potential TypeError when accessing a key that may not exist in activeForeignKeyIds:
const activeKeyId = activeForeignKeyIds[pkrKey.foreignKey]
const key = contractState._vm.authorizedKeys[activeKeyId]
if (affectedKeyIds.has(key.id)) return accIf pkrKey.foreignKey doesn't have a corresponding active key (e.g., because all keys with that foreign key have been revoked), activeKeyId will be undefined, making key undefined, and key.id will throw a TypeError.
Suggested fix:
const activeKeyId = activeForeignKeyIds[pkrKey.foreignKey]
if (!activeKeyId) return acc // No active key exists for this foreign key
const key = contractState._vm.authorizedKeys[activeKeyId]
if (affectedKeyIds.has(key.id)) return accPotential Issue: Null Safety in deleteKeyHelper
The helper function assumes state._volatile and state._volatile.pendingKeyRevocations exist:
for (const id of allIdsForNames[name]) {
if (has(state._volatile!.pendingKeyRevocations, id)) {
delete state._volatile!.pendingKeyRevocations![id]
}
}While the call sites currently initialize these properties beforehand, the function would be more robust with a guard:
const deleteKeyHelper = (state: ChelContractState, height: number, keyIds: string[]) => {
const allIdsForNames = Object.values(state._vm.authorizedKeys)
.reduce<Record<string, string[]>>((acc, { id, name }) => {
if (!acc[name]) {
acc[name] = [id]
} else {
acc[name].push(id)
}
return acc
}, Object.create(null))
for (const keyId of keyIds) {
const name = state._vm.authorizedKeys[keyId].name
// Clear pending revocations for all keys with the same name
// to handle key rotation scenarios where multiple keys exist
for (const id of allIdsForNames[name]) {
if (state._volatile?.pendingKeyRevocations && has(state._volatile.pendingKeyRevocations, id)) {
delete state._volatile.pendingKeyRevocations[id]
}
}
state._vm.authorizedKeys[keyId]._notAfterHeight = height
}
}Improvement: Type Annotation for Reduce Accumulator
The reduce accumulator in deleteKeyHelper lacks explicit typing:
.reduce((acc, { id, name }) => {
// ...
}, Object.create(null))Suggested improvement:
.reduce<Record<string, string[]>>((acc, { id, name }) => {
// ...
}, Object.create(null))Behavioral Change Clarification
The new deleteKeyHelper clears pending revocations for all keys with the same name, not just the specific key being deleted. The comment explains this is "to handle key rotation scenarios where multiple keys exist."
This is a significant behavioral change from the previous code which only cleared the specific keyId. Please confirm this broadened scope is intentional and won't cause unintended side effects when multiple keys share the same name but should be handled independently.
Gemini 3 Pro
Details
There are two critical bugs introduced in the chelonia/private/deleteOrRotateRevokedKeys selector. One causes a runtime crash (DoS), and the other causes logical errors where keys may fail to update if multiple local keys reference the same foreign key.
1. Critical: Runtime Crash (DoS) in deleteOrRotateRevokedKeys
In the reduce function within deleteOrRotateRevokedKeys, the code attempts to resolve an active key ID from a foreign key string. If the foreign key is not found in activeForeignKeyIds (which can happen if the key was deleted locally, or due to the overwrite bug detailed in point #2), activeKeyId becomes undefined.
Subsequently, accessing contractState._vm.authorizedKeys[undefined] returns undefined. Trying to access .id on that undefined result throws a TypeError, crashing the message processing.
Problematic Lines:
const activeKeyId = activeForeignKeyIds[pkrKey.foreignKey]
const key = contractState._vm.authorizedKeys[activeKeyId]
if (affectedKeyIds.has(key.id)) return accRecommended Fix:
You must check if activeKeyId (and consequently key) exists before accessing properties on it.
const activeKeyId = activeForeignKeyIds[pkrKey.foreignKey]
// FIX: Check if activeKeyId exists
if (!activeKeyId) return acc
const key = contractState._vm.authorizedKeys[activeKeyId]
// FIX: Ensure key lookup succeeded (sanity check)
if (!key) return acc
if (affectedKeyIds.has(key.id)) return acc2. Critical: Logic Error in activeForeignKeyIds Map Construction
The construction of activeForeignKeyIds uses Object.fromEntries with the foreign key URL as the key. If a contract has multiple local keys pointing to the same foreign key URL (e.g., different permissions or ring levels for the same foreign identity), Object.fromEntries will overwrite previous entries. Only the last processed key will be stored.
This results in the reduce loop failing to update/rotate the keys that were overwritten in the map.
Problematic Lines:
const activeForeignKeyIds = Object.fromEntries(
Object.values(contractState._vm.authorizedKeys)
.filter(({ foreignKey, _notAfterHeight }) =>
foreignKey != null && _notAfterHeight == null
)
.map(({ foreignKey, id }) => [foreignKey!, id])
)Recommended Fix:
Since deleteOrRotateRevokedKeys is trying to find the current active key that corresponds to a revoked key, and multiple keys might share a foreign key string, this logic is brittle.
However, to fix the immediate overwrite issue without refactoring the entire logic, you should map the Foreign Key string to an Array of IDs, and iterate over that array in the reducer.
Revised Code Suggestion (Fixing #1 and #2):
// 1. Create a map of ForeignKey -> Array<LocalKeyID>
const activeForeignKeyIds = Object.values(contractState._vm.authorizedKeys)
.filter(({ foreignKey, _notAfterHeight }) =>
foreignKey != null && _notAfterHeight == null
)
.reduce((acc, { foreignKey, id }) => {
const fk = foreignKey!
if (!acc[fk]) acc[fk] = []
acc[fk].push(id)
return acc
}, Object.create(null) as Record<string, string[]>)
// ... inside the reducer ...
const [, keyUpdateSigningKeyId, keyUpdateArgs] = keysToUpdate.reduce(
(acc, keyId) => {
const pkrKey = contractState._vm?.authorizedKeys?.[keyId]
if (!pkrKey || !pkrKey.foreignKey) return acc
// 2. Retrieve the list of active IDs for this foreign key
const activeKeyIds = activeForeignKeyIds[pkrKey.foreignKey]
if (!activeKeyIds || activeKeyIds.length === 0) return acc
// 3. Iterate over all potential active keys (handling the 1-to-many case)
let currentAcc = acc
for (const activeKeyId of activeKeyIds) {
const key = contractState._vm.authorizedKeys[activeKeyId]
if (!key) continue // Safety check
// Prevent processing the same key twice
if (affectedKeyIds.has(key.id)) continue
// ... existing logic regarding ringLevel and finding signingKeyId ...
// Note: You will need to adapt the accumulator logic here to handle
// potentially adding multiple updates in one pass of the reducer,
// or ensure the logic selects the specific correct key based on `name`.
// OPTIMIZATION: Usually, a key rotation replaces a key with the SAME name.
// It is safer to filter by name here:
if (key.name !== pkrKey.name) continue
// ... proceed with existing logic ...
}
return currentAcc
},
// ... initial value ...
)3. Improvement: Type Safety in deleteKeyHelper
In deleteKeyHelper, you access state._volatile! using the non-null assertion operator. While the callers (OP_KEY_DEL, OP_KEY_UPDATE) currently ensure _volatile exists, relying on the caller for null safety in a helper function can lead to future bugs if the helper is reused elsewhere.
Problematic Lines:
if (has(state._volatile!.pendingKeyRevocations, id)) {
delete state._volatile!.pendingKeyRevocations![id]
}Improvement:
Use optional chaining.
const deleteKeyHelper = (state: ChelContractState, height: number, keyIds: string[]) => {
// ... existing grouping logic ...
for (const keyId of keyIds) {
const key = state._vm.authorizedKeys[keyId]
if (!key) continue // Safety check in case keyId is invalid
const name = key.name
// Safe access
if (state._volatile?.pendingKeyRevocations) {
for (const id of allIdsForNames[name] || []) {
if (has(state._volatile.pendingKeyRevocations, id)) {
delete state._volatile.pendingKeyRevocations[id]
}
}
}
state._vm.authorizedKeys[keyId]._notAfterHeight = height
}
}4. Logic Check: deleteKeyHelper grouping
In deleteKeyHelper:
const allIdsForNames = Object.values(state._vm.authorizedKeys)
// ...
for (const id of allIdsForNames[name]) {If keyIds contains a key ID that does not exist in state._vm.authorizedKeys, the line const name = state._vm.authorizedKeys[keyId].name will throw.
While OP_KEY_DEL filters keys before calling this helper, OP_KEY_UPDATE passes keysToDelete derived from validateKeyUpdatePermissions. If that validation function returns an ID that isn't in authorizedKeys (unlikely, but possible during race conditions or state desyncs), this helper will crash.
Suggestion: Add a simple existence check inside the loop in deleteKeyHelper (as shown in the code block in point #3 above).
|
This issue: const activeKeyId = activeForeignKeyIds[pkrKey.foreignKey]
// [suggested] FIX: Check if activeKeyId exists
// ...
const key = contractState._vm.authorizedKeys[activeKeyId]
if (affectedKeyIds.has(key.id)) return accIsn't really possible due to how
It definitely is weird that a contract might have a foreign key defined as a key multiple time, and doing this will break
Dismissing adding a guard since it's redundant (a DRY argument can be made for moving the
This would IMHO be more verbose and confusing than the current implicit approach.
Dismissing
This check is redundant because all keys are supposed to exist, and their non-existence means that there's a bug elsewhere. |
|
I've been testing these changes, and there still is one race condition that needs to be handled. The situation is more or less as follows: Setup: A contract with a foreign key that's out of sync (but that has been rotated at least once) and syncing from scratch. Race condition:
Solution:
( |
|
Ran latest changes through Opus 4.5: DetailsI've analyzed the changes and found several issues worth addressing: 1. Potential Runtime Error - Missing Null CheckIn const activeKeyId = activeForeignKeyIds[pkrKey.foreignKey]
const key = contractState._vm.authorizedKeys[activeKeyId]
if (affectedKeyIds.has(key.id)) return acc // Will throw if key is undefinedSuggested fix: const activeKeyId = activeForeignKeyIds[pkrKey.foreignKey]
if (!activeKeyId) return acc // Add this check
const key = contractState._vm.authorizedKeys[activeKeyId]
if (!key || affectedKeyIds.has(key.id)) return acc2. Same Issue in Key Deletion LoopThe same pattern exists in the const [, keyDelSigningKeyId, keyIdsToDelete] = keysToDelete.reduce(
(acc, pkrKeyId) => {
const pkrKey = contractState._vm?.authorizedKeys?.[pkrKeyId]
if (!pkrKey || !pkrKey.foreignKey) return acc
const keyId = activeForeignKeyIds[pkrKey.foreignKey]
if (affectedKeyIds.has(keyId)) return acc // keyId could be undefined
// ... later uses keyId without null checkSuggested fix: const keyId = activeForeignKeyIds[pkrKey.foreignKey]
if (!keyId || affectedKeyIds.has(keyId)) return acc3. Good Improvement -
|
b30210a to
d96b9c6
Compare
|
/crush_fast AI review started. |
Advanced AI Review
Click to expand reviewI'll analyze this PR for bugs, security issues, and potential improvements. Let me start by examining some key areas.## Current State
Files & ChangesThe PR modifies multiple files:
Key areas examined so far:
Technical Context
Key behaviors discovered:
Strategy & Approach
Potential concerns identified but not yet documented as issues:
Exact Next Steps
Files & ChangesModified files in the PR:
Technical Context
Key function behaviors verified:
Strategy & Approach
Potential concerns identified so far (not yet formalized into issues):
Exact Next Steps
Review generated using |
|
/review_fast AI review started. |
Advanced AI Review
Click to expand reviewI'll analyze these changes for bugs, security issues, and potential improvements. Let me start by examining the codebase for context on how these functions are used. Code ReviewIssue 1: 🔴
|
|
/crush_fast AI review started. |
Advanced AI Review
Click to expand reviewReview generated using |
|
/crush_fast AI review started. |
Advanced AI Review
Click to expand reviewI'll review this PR for bugs, security issues, and improvements. Let me first examine some key areas in more detail. Review generated using |
|
/crush_fast (*oops, I forgot that crush has a bug that limits the GLM-5 context window to half its size. Fixed! re-running) AI review started. |
Advanced AI Review
Click to expand reviewBased on my review, here are the issues I've identified: Issue 1:
|
|
/crush_fast AI review started. |
Advanced AI Review
Click to expand reviewBased on my thorough review of the changes, here are the issues I've identified: Issue 1: Breaking change in operation hook naming convention🔴 High importance & high confidence
Location: The hook naming convention has changed from Before (main branch): const hook = `${manifestHash}/${contractName}/hook/${message.opType()}`
sbp(hook, { contractID, message, state })After (this PR): const hook = `${manifestHash}/${contractName}/_postOpHook/${op}`
sbp(hook, { contractID, message, state, atomic })This is a breaking change for any existing contracts that have defined hooks using the old naming convention. Contracts will need to rename their hook selectors from Additionally, the hook signature has changed to include an Suggestion: If backwards compatibility is needed, consider supporting both hook naming conventions temporarily, or document this as a breaking change clearly. Issue 2: Type assertion inconsistency in SPMessage.ts🟡 Medium importance & high confidence
Location: return maybeEncryptedIncomingData<ProtoSPOpKeyRequestV1 | SPOpKeyRequestV2>(
contractID,
state,
message as ProtoSPOpKeyRequestV1, // <-- Cast as V1 but type allows V1 | V2The generic type parameter is Suggestion: return maybeEncryptedIncomingData<ProtoSPOpKeyRequestV1 | SPOpKeyRequestV2>(
contractID,
state,
message as ProtoSPOpKeyRequestV1 | SPOpKeyRequestV2,Issue 3: Potential issue with
|
|
/crush_fast {{openrouter, google/gemini-3.1-pro-preview, google/gemini-3-flash-preview}} AI review started. |
Advanced AI Review
Click to expand reviewI have thoroughly reviewed the supplied changes and have identified a couple of issues ranging from high to medium severity. Please review my feedback below. 1. 🔴
|
|
/crush_fast {{openrouter, google/gemini-3.1-pro-preview, google/gemini-3-flash-preview}} (Sorry @corrideat - there was a small error in the "helpful context" run step of the previous job that i want to see if it's still there)
EDIT: looks like it's been fixed! AI review started. |
|
/crush_fast {{openrouter, google/gemini-3.1-pro-preview, google/gemini-3-flash-preview}} - cancelling the previous run since a commit was pushed AI review started. |
Advanced AI Review
Click to expand reviewOverall, the changes look extremely solid! The logic around foreign key tracking ( I found a couple of relatively minor observations, particularly one code pattern that might inhibit UI reactivity: 1. Array index assignment could break Vue 2 reactivity🟡 Medium Importance
In // src/internals.ts:1194-1196
const tuple = sharedKeyId
.foreignContractIDs.find(([id]) => id === v.foreignContractID)
if (tuple) {
tuple[2] = height
} else {
sharedKeyId.foreignContractIDs.push([v.foreignContractID, height])Details: Suggestion: const tuple = sharedKeyId
.foreignContractIDs.find(([id]) => id === v.foreignContractID)
if (tuple) {
tuple.splice(2, 1, height)
} else {2. Missing
|
This relates to okTurtles/group-income#2988
When debugging that issue, I found that sometimes key rotations for foreign keys (which should happen automatically) don't occur. This PR enhances how foreign key rotations are handled by:
pendingKeyRotationsif a more recent key with the same name is rotated. This prevents lingering keys.pendingKeyRotations, which addresses key rotations.This PR also adds
foreignContractIDsto key shares state, which is used to know whom a key was shared with. That didn't exist before, but it now does and it works in non-adversarial settings. (†)† The adversarial setting would be claiming that a key is shared with a contract but sending undecryptable or wrong information. (edited)