XDC Fix mutable V2 config spec#11327
Conversation
…vider for spec retrieval
|
Claude finished @batrr's task in 6m 27s —— View job PR Review: XDC Fix mutable V2 config spec
SummaryThe fix is conceptually correct and directly addresses the root cause: The solution — cloning per FindingsLow — Shallow clone shares mutable reference-type collections
Currently no code path calls Low — Test bypasses
|
|
Claude finished @batrr's task in 4m 0s —— View job PR Review: XDC Fix mutable V2 config spec
SummaryThe fix is correct and directly addresses the root cause: The solution — cloning per The FindingsLow — Shallow clone shares mutable reference-type collections(inline comment posted on
Low — Test collection initializer bypasses
|
| return cached; | ||
|
|
||
| XdcReleaseSpec baseSpec = (XdcReleaseSpec)_blockTransitions[blockIndex].Spec; | ||
| XdcReleaseSpec copy = (XdcReleaseSpec)baseSpec.Clone(); |
There was a problem hiding this comment.
Low — shallow clone, shared mutable collections
MemberwiseClone() copies reference-type fields by reference. All cached XdcReleaseSpec instances for a given block transition share the same V2Configs (List<V2ConfigParams>), BlackListedAddresses (HashSet<Address>), and GenesisMasterNodes (Address[]).
ApplyV2Config only mutates value-type fields (SwitchRound, MaxMasternodes, etc.), so there is no active bug today. However, IXdcReleaseSpec exposes BlackListedAddresses with a public setter, and GetConfigAtRound reads V2Configs from the spec — any future in-place mutation (e.g., spec.BlackListedAddresses.Add(...)) would silently corrupt all cached specs sharing that collection.
At minimum, document the invariant with a comment; better, deep-copy the mutable collections after the clone:
| XdcReleaseSpec copy = (XdcReleaseSpec)baseSpec.Clone(); | |
| XdcReleaseSpec copy = (XdcReleaseSpec)baseSpec.Clone(); | |
| // V2Configs, BlackListedAddresses, and GenesisMasterNodes are shared with the base spec. | |
| // Callers must not mutate these collections in-place — only assign new instances. |
| { | ||
| new() { SwitchRound = 0 }, | ||
| new() { SwitchRound = 10 }, | ||
| } |
There was a problem hiding this comment.
Low — collection initializer syntax bypasses CheckConfig
V2Configs =
{
new() { SwitchRound = 0 },
new() { SwitchRound = 10 },
}This uses C#'s nested collection initializer, which calls .Add(...) on the existing backing list (_v2Configs = new()) rather than invoking the property setter. CheckConfig and the sort step inside the setter are never executed. The test passes only because the items are already in sorted order and start with SwitchRound = 0.
Use a property setter assignment to exercise the same validation path the production code goes through:
| } | |
| V2Configs = new List<V2ConfigParams> | |
| { | |
| new() { SwitchRound = 0 }, | |
| new() { SwitchRound = 10 }, | |
| }, |
cicr99
left a comment
There was a problem hiding this comment.
CI is not successful yet, but other than that looks good to me
Changes
XdcChainSpecBasedSpecProvider: previouslyGetXdcSpeccalledApplyV2Configdirectly on the shared spec instance returned byGetSpec, so fetching with round Y would overwrite the V2 fields of a specpreviously fetched with round X
(blockIndex, roundIndex)spec cacheGetXdcSpecnow clones the base spec and caches an isolated copy per block transition × V2 config bucket, so concurrent or sequential calls with different rounds never mutate eachother
Types of changes
Testing
Requires testing
If yes, did you write tests?