Skip to content

Commit c6507b6

Browse files
authored
fix state trie (#366)
* fix fuzz test * add new traces * fix state trie hash calulation * swift 6.2 fix * update docker for swift 6.2 * misc
1 parent d3baee2 commit c6507b6

File tree

119 files changed

+25798
-207
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

119 files changed

+25798
-207
lines changed

.swift-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
6.1
1+
6.2

Blockchain/Sources/Blockchain/State/State.swift

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -238,19 +238,7 @@ public struct State: Sendable {
238238

239239
public var stateRoot: Data32 {
240240
get async {
241-
// TODO: should use backend.rootHash after StateTrie is fixed
242-
do {
243-
let allKeys = try await backend.getKeys(nil, nil, nil)
244-
var kv: [Data31: Data] = [:]
245-
for (key, value) in allKeys {
246-
if let key31 = Data31(key) {
247-
kv[key31] = value
248-
}
249-
}
250-
return try stateMerklize(kv: kv)
251-
} catch {
252-
return await backend.rootHash
253-
}
241+
await backend.rootHash
254242
}
255243
}
256244

Blockchain/Sources/Blockchain/State/StateBackend.swift

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,18 +55,18 @@ public final class StateBackend: Sendable {
5555
break
5656
}
5757

58-
guard trieNodeData.count == 64 else {
58+
guard trieNodeData.count == 65 else {
5959
continue
6060
}
6161

6262
let firstByte = trieNodeData[relative: 0]
63-
let isLeaf = (firstByte & 0b1100_0000) == 0b1000_0000 || (firstByte & 0b1100_0000) == 0b1100_0000
63+
let isLeaf = firstByte == 1 || firstByte == 2
6464

6565
guard isLeaf else {
6666
continue
6767
}
6868

69-
let stateKey = Data(trieNodeData[relative: 1 ..< 32])
69+
let stateKey = Data(trieNodeData[relative: 2 ..< 33])
7070

7171
if !prefixData.isEmpty, !stateKey.starts(with: prefixData) {
7272
continue
@@ -94,7 +94,9 @@ public final class StateBackend: Sendable {
9494
}
9595

9696
public func write(_ values: any Sequence<(key: Data31, value: (Codable & Sendable)?)>) async throws {
97-
try await trie.update(values.map { try (key: $0.key, value: $0.value.map { try JamEncoder.encode($0) }) })
97+
let updates: [(key: Data31, value: Data?)] = try values.map { try (key: $0.key, value: $0.value.map { try JamEncoder.encode($0) }) }
98+
99+
try await trie.update(updates)
98100
try await trie.save()
99101
}
100102

@@ -109,13 +111,13 @@ public final class StateBackend: Sendable {
109111

110112
public func gc() async throws {
111113
try await impl.gc { data in
112-
guard data.count == 64 else {
114+
guard data.count == 65 else {
113115
// unexpected data size
114116
return nil
115117
}
116-
let isRegularLeaf = data[0] & 0b1100_0000 == 0b1100_0000
118+
let isRegularLeaf = data[0] == 2 // type byte for regularLeaf
117119
if isRegularLeaf {
118-
return Data32(data.suffix(from: 32))!
120+
return Data32(data.suffix(from: 33))! // right child starts at byte 33
119121
}
120122
return nil
121123
}

Blockchain/Sources/Blockchain/State/StateBackendProtocol.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public protocol StateBackendIterator: Sendable {
1313
}
1414

1515
/// key: trie node hash (31 bytes)
16-
/// value: trie node data (64 bytes)
16+
/// value: trie node data (65 bytes - includes node type + original child data)
1717
/// ref counting requirements:
1818
/// - write do not increment ref count, only explicit ref increment do
1919
/// - lazy prune is used. e.g. when ref count is reduced to zero, the value will only be removed

Blockchain/Sources/Blockchain/State/StateTrie.swift

Lines changed: 95 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -12,39 +12,77 @@ private enum TrieNodeType {
1212

1313
private struct TrieNode {
1414
let hash: Data32
15-
let left: Data32
16-
let right: Data32
15+
let left: Data32 // Original child hash/data
16+
let right: Data32 // Original child hash/data
1717
let type: TrieNodeType
1818
let isNew: Bool
1919
let rawValue: Data?
2020

21-
init(hash: Data32, data: Data64, isNew: Bool = false) {
21+
// Constructor for loading from storage (65-byte format: [type][left-32][right-32])
22+
init(hash: Data32, data: Data, isNew: Bool = false) {
2223
self.hash = hash
23-
left = Data32(data.data.prefix(32))!
24-
right = Data32(data.data.suffix(32))!
2524
self.isNew = isNew
2625
rawValue = nil
27-
switch data.data.first! & 0b1100_0000 {
28-
case 0b1000_0000:
26+
27+
let typeByte = data[relative: 0]
28+
switch typeByte {
29+
case 0:
30+
type = .branch
31+
case 1:
2932
type = .embeddedLeaf
30-
case 0b1100_0000:
33+
case 2:
3134
type = .regularLeaf
3235
default:
3336
type = .branch
3437
}
38+
39+
left = Data32(data[relative: 1 ..< 33])! // bytes 1-32
40+
right = Data32(data[relative: 33 ..< 65])! // bytes 33-64
3541
}
3642

43+
// Constructor for pure trie operations
3744
private init(left: Data32, right: Data32, type: TrieNodeType, isNew: Bool, rawValue: Data?) {
38-
hash = Blake2b256.hash(left.data, right.data)
39-
self.left = left
40-
self.right = right
45+
hash = Self.calculateHash(left: left, right: right, type: type)
46+
self.left = left // Store original data
47+
self.right = right // Store original data
4148
self.type = type
4249
self.isNew = isNew
4350
self.rawValue = rawValue
4451
}
4552

46-
var encodedData: Data64 {
47-
Data64(left.data + right.data)!
53+
// JAM spec compliant hash calculation
54+
private static func calculateHash(left: Data32, right: Data32, type: TrieNodeType) -> Data32 {
55+
switch type {
56+
case .branch:
57+
var leftForHashing = left.data
58+
leftForHashing[leftForHashing.startIndex] = leftForHashing[leftForHashing.startIndex] & 0b0111_1111
59+
return Blake2b256.hash(leftForHashing, right.data)
60+
case .embeddedLeaf:
61+
var leftForHashing = left.data
62+
let valueLength = leftForHashing[leftForHashing.startIndex]
63+
leftForHashing[leftForHashing.startIndex] = 0b1000_0000 | valueLength
64+
return Blake2b256.hash(leftForHashing, right.data)
65+
case .regularLeaf:
66+
var leftForHashing = left.data
67+
leftForHashing[leftForHashing.startIndex] = 0b1100_0000
68+
return Blake2b256.hash(leftForHashing, right.data)
69+
}
70+
}
71+
72+
// New 65-byte storage format: [type:1][left:32][right:32]
73+
var storageData: Data {
74+
var data = Data(capacity: 65)
75+
76+
switch type {
77+
case .branch: data.append(0)
78+
case .embeddedLeaf: data.append(1)
79+
case .regularLeaf: data.append(2)
80+
}
81+
82+
data.append(left.data)
83+
data.append(right.data)
84+
85+
return data
4886
}
4987

5088
var isBranch: Bool {
@@ -66,27 +104,30 @@ private struct TrieNode {
66104
guard type == .embeddedLeaf else {
67105
return nil
68106
}
69-
let len = left.data.first! & 0b0011_1111
107+
// For embedded leaves: length is stored in first byte
108+
let len = left.data[relative: 0]
70109
return right.data[relative: 0 ..< Int(len)]
71110
}
72111

73112
static func leaf(key: Data31, value: Data) -> TrieNode {
74-
var newKey = Data(capacity: 32)
75113
if value.count <= 32 {
76-
newKey.append(0b1000_0000 | UInt8(value.count))
77-
newKey += key.data
78-
let newValue = value + Data(repeating: 0, count: 32 - value.count)
79-
return .init(left: Data32(newKey)!, right: Data32(newValue)!, type: .embeddedLeaf, isNew: true, rawValue: value)
114+
// Embedded leaf: store length + key, padded value
115+
var keyData = Data(capacity: 32)
116+
keyData.append(UInt8(value.count)) // Store length in first byte
117+
keyData += key.data
118+
let paddedValue = value + Data(repeating: 0, count: 32 - value.count)
119+
return .init(left: Data32(keyData)!, right: Data32(paddedValue)!, type: .embeddedLeaf, isNew: true, rawValue: value)
120+
} else {
121+
// Regular leaf: store key, value hash
122+
var keyData = Data(capacity: 32)
123+
keyData.append(0x00) // Placeholder for first byte
124+
keyData += key.data
125+
return .init(left: Data32(keyData)!, right: value.blake2b256hash(), type: .regularLeaf, isNew: true, rawValue: value)
80126
}
81-
newKey.append(0b1100_0000)
82-
newKey += key.data
83-
return .init(left: Data32(newKey)!, right: value.blake2b256hash(), type: .regularLeaf, isNew: true, rawValue: value)
84127
}
85128

86129
static func branch(left: Data32, right: Data32) -> TrieNode {
87-
var left = left.data
88-
left[left.startIndex] = left[left.startIndex] & 0b0111_1111 // clear the highest bit
89-
return .init(left: Data32(left)!, right: right, type: .branch, isNew: true, rawValue: nil)
130+
.init(left: left, right: right, type: .branch, isNew: true, rawValue: nil)
90131
}
91132
}
92133

@@ -148,12 +189,10 @@ public actor StateTrie {
148189
guard let data = try await backend.read(key: id) else {
149190
return nil
150191
}
151-
152-
guard let data64 = Data64(data) else {
192+
guard data.count == 65 else {
153193
throw StateTrieError.invalidData
154194
}
155-
156-
let node = TrieNode(hash: hash, data: data64)
195+
let node = TrieNode(hash: hash, data: data)
157196
saveNode(node: node)
158197
return node
159198
}
@@ -189,7 +228,7 @@ public actor StateTrie {
189228
deleted.removeAll()
190229

191230
for node in nodes.values where node.isNew {
192-
ops.append(.write(key: node.hash.data.suffix(31), value: node.encodedData.data))
231+
ops.append(.write(key: node.hash.data.suffix(31), value: node.storageData))
193232
if node.type == .regularLeaf {
194233
try ops.append(.writeRawValue(key: node.right, value: node.rawValue.unwrap()))
195234
}
@@ -256,7 +295,7 @@ public actor StateTrie {
256295
return newLeaf.hash
257296
}
258297

259-
let existingKeyBit = bitAt(existing.left.data[1...], position: depth)
298+
let existingKeyBit = bitAt(existing.left.data[relative: 1...], position: depth)
260299
let newKeyBit = bitAt(newKey.data, position: depth)
261300

262301
if existingKeyBit == newKeyBit {
@@ -306,6 +345,30 @@ public actor StateTrie {
306345
if left == Data32(), right == Data32() {
307346
// this branch is empty
308347
return Data32()
348+
} else if left == Data32() {
349+
// only right child remains - check if we can collapse
350+
let rightNode = try await get(hash: right)
351+
if let rightNode, rightNode.isLeaf {
352+
// Can collapse: right child is a leaf
353+
return right
354+
} else {
355+
// Cannot collapse: right child is a branch that needs to maintain its depth
356+
let newBranch = TrieNode.branch(left: left, right: right)
357+
saveNode(node: newBranch)
358+
return newBranch.hash
359+
}
360+
} else if right == Data32() {
361+
// only left child remains - check if we can collapse
362+
let leftNode = try await get(hash: left)
363+
if let leftNode, leftNode.isLeaf {
364+
// Can collapse: left child is a leaf
365+
return left
366+
} else {
367+
// Cannot collapse: left child is a branch that needs to maintain its depth
368+
let newBranch = TrieNode.branch(left: left, right: right)
369+
saveNode(node: newBranch)
370+
return newBranch.hash
371+
}
309372
}
310373

311374
let newBranch = TrieNode.branch(left: left, right: right)
@@ -331,7 +394,7 @@ public actor StateTrie {
331394
private func saveNode(node: TrieNode) {
332395
let id = node.hash.data.suffix(31)
333396
nodes[id] = node
334-
deleted.remove(id) // TODO: maybe this is not needed
397+
deleted.remove(id)
335398
}
336399

337400
public func debugPrint() async throws {

Blockchain/Sources/Blockchain/VMInvocations/HostCall/HostCalls.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -925,6 +925,7 @@ public class Checkpoint: HostCall {
925925
y.nextAccountIndex = x.nextAccountIndex
926926
y.transfers = x.transfers
927927
y.yield = x.yield
928+
y.provide = x.provide
928929
}
929930
}
930931

Blockchain/Tests/BlockchainTests/StateTrieTests.swift

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,5 +301,98 @@ struct StateTrieTests {
301301
}
302302
}
303303

304-
// TODO: test for gc, ref counting & pruning, raw value ref counting & cleaning
304+
@Test
305+
func testNodeDeletion() async throws {
306+
let trie = StateTrie(rootHash: Data32(), backend: backend)
307+
308+
// Create a scenario that will cause node deletion and tree restructuring
309+
let keys = [
310+
Data31(Data([0x00] + Data(repeating: 0, count: 30)))!, // 00000000...
311+
Data31(Data([0x40] + Data(repeating: 0, count: 30)))!, // 01000000...
312+
Data31(Data([0x80] + Data(repeating: 0, count: 30)))!, // 10000000...
313+
Data31(Data([0xC0] + Data(repeating: 0, count: 30)))!, // 11000000...
314+
]
315+
let values = [Data([0x01]), Data([0x02]), Data([0x03]), Data([0x04])]
316+
317+
// Insert all keys
318+
try await trie.update(Array(zip(keys, values.map { $0 as Data? })))
319+
try await trie.save()
320+
321+
// Verify all are readable
322+
for (i, (key, expectedValue)) in zip(keys, values).enumerated() {
323+
let readValue = try await trie.read(key: key)
324+
#expect(readValue == expectedValue, "Key \(i) should be readable after batch insert")
325+
}
326+
327+
// Delete some keys to trigger tree restructuring
328+
try await trie.update([
329+
(key: keys[1], value: nil), // Delete 01000000...
330+
(key: keys[3], value: nil), // Delete 11000000...
331+
])
332+
try await trie.save()
333+
334+
// Verify remaining keys are still readable and deleted keys return nil
335+
let remainingKeys = [keys[0], keys[2]]
336+
let remainingValues = [values[0], values[2]]
337+
338+
for (i, (key, expectedValue)) in zip(remainingKeys, remainingValues).enumerated() {
339+
let readValue = try await trie.read(key: key)
340+
#expect(readValue == expectedValue, "Remaining key \(i) should still be readable after deletions")
341+
}
342+
343+
let deletedValue1 = try await trie.read(key: keys[1])
344+
let deletedValue3 = try await trie.read(key: keys[3])
345+
#expect(deletedValue1 == nil, "Deleted key should return nil")
346+
#expect(deletedValue3 == nil, "Deleted key should return nil")
347+
348+
// Test re-adding a deleted key
349+
try await trie.update([(key: keys[1], value: Data([0xFF]))])
350+
try await trie.save()
351+
352+
let readdedValue = try await trie.read(key: keys[1])
353+
#expect(readdedValue == Data([0xFF]), "Re-added key should be readable")
354+
355+
// Ensure other keys are still intact
356+
for (i, (key, expectedValue)) in zip(remainingKeys, remainingValues).enumerated() {
357+
let readValue = try await trie.read(key: key)
358+
#expect(readValue == expectedValue, "Key \(i) should remain readable after re-adding deleted key")
359+
}
360+
}
361+
362+
@Test
363+
func testConsecutiveSaveAndLoad() async throws {
364+
let trie = StateTrie(rootHash: Data32(), backend: backend)
365+
366+
let key = Data31.random()
367+
let value1 = Data("value1".utf8)
368+
let value2 = Data("value2".utf8)
369+
370+
// Insert, save, read
371+
try await trie.update([(key: key, value: value1)])
372+
try await trie.save()
373+
374+
let read1 = try await trie.read(key: key)
375+
#expect(read1 == value1, "Value should be readable after first save")
376+
377+
// Update same key, save, read
378+
try await trie.update([(key: key, value: value2)])
379+
try await trie.save()
380+
381+
let read2 = try await trie.read(key: key)
382+
#expect(read2 == value2, "Updated value should be readable after second save")
383+
384+
// Multiple saves shouldn't change anything
385+
try await trie.save()
386+
try await trie.save()
387+
388+
let read3 = try await trie.read(key: key)
389+
#expect(read3 == value2, "Value should remain after multiple saves")
390+
391+
let rootHash1 = await trie.rootHash
392+
393+
// Another save shouldn't change root hash
394+
try await trie.save()
395+
let rootHash2 = await trie.rootHash
396+
#expect(rootHash1 == rootHash2, "Root hash should be stable across saves")
397+
}
305398
}

0 commit comments

Comments
 (0)