Skip to content

Commit a0f3171

Browse files
authored
community STF tests (#304)
* add submodules * move files * setup * fix state merklize * decode tests * update submodule * update decode * fix decoder * pass safrole * fix block author
1 parent ea1b6ca commit a0f3171

37 files changed

+400
-38
lines changed

.gitmodules

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
11
[submodule "JAMTests/jamtestvectors"]
22
path = JAMTests/jamtestvectors
33
url = https://github.com/open-web3-stack/jamtestvectors.git
4+
[submodule "JAMTests/jamixir"]
5+
path = JAMTests/jamixir
6+
url = https://github.com/jamixir/jamtestnet.git
7+
[submodule "JAMTests/javajam"]
8+
path = JAMTests/javajam
9+
url = https://github.com/javajamio/javajam-trace.git
10+
[submodule "JAMTests/jamduna"]
11+
path = JAMTests/jamduna
12+
url = https://github.com/jam-duna/jamtestnet.git

Blockchain/Sources/Blockchain/RuntimeProtocols/Accumulation.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ extension Accumulation {
244244
let parallelOutput = try await parallelizedAccumulate(
245245
config: config,
246246
state: state,
247-
workReports: Array(workReports[0 ..< i]),
247+
workReports: Array(workReports[0 ..< min(i, workReports.count)]),
248248
privilegedGas: privilegedGas,
249249
entropy: entropy,
250250
timeslot: timeslot

Blockchain/Sources/Blockchain/RuntimeProtocols/Runtime.swift

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -75,18 +75,20 @@ public final class Runtime {
7575
// offendersMarkers is validated at apply time by Disputes
7676
}
7777

78-
public func validateHeaderSeal(block: BlockRef, state: inout State, prevState: StateRef) throws(Error) {
78+
public func validateHeaderSeal(block: BlockRef, state: State) throws(Error) {
7979
let vrfOutput: Data32
80+
// H_a
8081
let blockAuthorKey = try Result {
8182
try Bandersnatch.PublicKey(data: state.currentValidators[Int(block.header.authorIndex)].bandersnatch)
8283
}.mapError(Error.invalidBlockSeal).get()
84+
8385
let index = block.header.timeslot % UInt32(config.value.epochLength)
8486
let encodedHeader = try Result { try JamEncoder.encode(block.header.unsigned) }.mapError(Error.invalidBlockSeal).get()
8587
let entropyVRFInputData: Data
8688
switch state.safroleState.ticketsOrKeys {
8789
case let .left(tickets):
8890
let ticket = tickets[Int(index)]
89-
let vrfInputData = SigningContext.safroleTicketInputData(entropy: prevState.value.entropyPool.t3, attempt: ticket.attempt)
91+
let vrfInputData = SigningContext.safroleTicketInputData(entropy: state.entropyPool.t3, attempt: ticket.attempt)
9092
vrfOutput = try Result {
9193
try blockAuthorKey.ietfVRFVerify(
9294
vrfInputData: vrfInputData,
@@ -106,7 +108,7 @@ public final class Runtime {
106108
logger.debug("expected key: \(key.toHexString()), got key: \(blockAuthorKey.data.toHexString())")
107109
throw Error.invalidAuthorKey
108110
}
109-
let vrfInputData = SigningContext.fallbackSealInputData(entropy: prevState.value.entropyPool.t3)
111+
let vrfInputData = SigningContext.fallbackSealInputData(entropy: state.entropyPool.t3)
110112
vrfOutput = try Result {
111113
logger.trace("verifying ticket", metadata: ["key": "\(blockAuthorKey.data.toHexString())"])
112114
return try blockAuthorKey.ietfVRFVerify(
@@ -116,7 +118,7 @@ public final class Runtime {
116118
)
117119
}.mapError(Error.invalidBlockSeal).get()
118120

119-
entropyVRFInputData = SigningContext.fallbackSealInputData(entropy: prevState.value.entropyPool.t3)
121+
entropyVRFInputData = SigningContext.entropyInputData(entropy: vrfOutput)
120122
}
121123

122124
_ = try Result {
@@ -144,13 +146,13 @@ public final class Runtime {
144146
.mapError(Error.validateError)
145147
.get()
146148

147-
return try await apply(block: validatedBlock, state: prevState, context: context)
149+
try validate(block: validatedBlock, state: prevState, context: context)
150+
151+
return try await apply(block: validatedBlock, state: prevState)
148152
}
149153

150-
public func apply(block: Validated<BlockRef>, state prevState: StateRef, context: ApplyContext) async throws(Error) -> StateRef {
151-
try validate(block: block, state: prevState, context: context)
154+
public func apply(block: Validated<BlockRef>, state prevState: StateRef) async throws(Error) -> StateRef {
152155
let block = block.value
153-
154156
var newState = prevState.value
155157

156158
do {
@@ -163,7 +165,7 @@ public final class Runtime {
163165
])
164166
}
165167

166-
try validateHeaderSeal(block: block, state: &newState, prevState: prevState)
168+
try validateHeaderSeal(block: block, state: newState)
167169

168170
try updateDisputes(block: block, state: &newState)
169171

@@ -233,7 +235,7 @@ public final class Runtime {
233235
let safroleResult = try newState.updateSafrole(
234236
config: config,
235237
slot: block.header.timeslot,
236-
entropy: newState.entropyPool.t0,
238+
entropy: Bandersnatch.getIetfSignatureOutput(signature: block.header.vrfSignature),
237239
offenders: newState.judgements.punishSet,
238240
extrinsics: block.extrinsic.tickets
239241
)

Blockchain/Sources/Blockchain/State/StateKeys.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ public enum StateKeys {
297297
}
298298

299299
public func encode() -> Data32 {
300-
constructKey(index, UInt32.max - 1, hash.data[1...])
300+
constructKey(index, UInt32.max - 1, hash.data[relative: 1...])
301301
}
302302
}
303303

Blockchain/Sources/Blockchain/State/StateLayer.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ private enum StateLayerValue: Sendable {
2121
}
2222
}
2323

24-
// @unchecked because AnyHashable is not Sendable
2524
public struct StateLayer: Sendable {
2625
private var changes: [Data32: StateLayerValue] = [:]
2726

Blockchain/Sources/Blockchain/Validator/BlockAuthor.swift

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ public final class BlockAuthor: ServiceBase2, @unchecked Sendable {
5656
let state = try await dataProvider.getState(hash: parentHash)
5757
let stateRoot = await state.value.stateRoot
5858
let epoch = timeslot.timeslotToEpochIndex(config: config)
59+
let parentEpoch = state.value.timeslot.timeslotToEpochIndex(config: config)
60+
let isEpochChange = epoch > parentEpoch
5961

6062
let pendingTickets = await safroleTicketPool.getPendingTickets(epoch: epoch)
6163
let existingTickets = SortedArray(sortedUnchecked: state.value.safroleState.ticketsAccumulator.array.map(\.id))
@@ -92,19 +94,17 @@ public final class BlockAuthor: ServiceBase2, @unchecked Sendable {
9294
try throwUnreachable("no secret key for public key")
9395
}
9496

97+
let sealEntropy = isEpochChange ? state.value.entropyPool.t2 : state.value.entropyPool.t3
98+
9599
let vrfOutput: Data32
96100
if let ticket {
97101
vrfOutput = ticket.output
98102
} else {
99-
let inputData = SigningContext.fallbackSealInputData(entropy: state.value.entropyPool.t3)
103+
let inputData = SigningContext.fallbackSealInputData(entropy: sealEntropy)
100104
vrfOutput = try secretKey.getOutput(vrfInputData: inputData)
101105
}
102106

103-
let vrfSignature = if ticket != nil {
104-
try secretKey.ietfVRFSign(vrfInputData: SigningContext.entropyInputData(entropy: vrfOutput))
105-
} else {
106-
try secretKey.ietfVRFSign(vrfInputData: SigningContext.fallbackSealInputData(entropy: state.value.entropyPool.t3))
107-
}
107+
let vrfSignature = try secretKey.ietfVRFSign(vrfInputData: SigningContext.entropyInputData(entropy: vrfOutput))
108108

109109
let authorIndex = state.value.currentValidators.firstIndex { publicKey.data == $0.bandersnatch }
110110
guard let authorIndex else {
@@ -114,7 +114,7 @@ public final class BlockAuthor: ServiceBase2, @unchecked Sendable {
114114
let safroleResult = try state.value.updateSafrole(
115115
config: config,
116116
slot: timeslot,
117-
entropy: state.value.entropyPool.t0,
117+
entropy: Bandersnatch.getIetfSignatureOutput(signature: vrfSignature),
118118
offenders: state.value.judgements.punishSet,
119119
extrinsics: extrinsic.tickets
120120
)
@@ -136,14 +136,14 @@ public final class BlockAuthor: ServiceBase2, @unchecked Sendable {
136136
let seal = if let ticket {
137137
try secretKey.ietfVRFSign(
138138
vrfInputData: SigningContext.safroleTicketInputData(
139-
entropy: state.value.entropyPool.t3,
139+
entropy: sealEntropy,
140140
attempt: ticket.ticket.attempt
141141
),
142142
auxData: encodedHeader
143143
)
144144
} else {
145145
try secretKey.ietfVRFSign(
146-
vrfInputData: SigningContext.fallbackSealInputData(entropy: state.value.entropyPool.t3),
146+
vrfInputData: SigningContext.fallbackSealInputData(entropy: sealEntropy),
147147
auxData: encodedHeader
148148
)
149149
}

Blockchain/Tests/BlockchainTests/ValidatorServiceTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ struct ValidatorServiceTests {
109109

110110
await storeMiddleware.wait()
111111

112-
for _ in 0 ..< 25 {
112+
for _ in 0 ..< 50 {
113113
await scheduler.advance(by: TimeInterval(config.value.slotPeriodSeconds))
114114
await storeMiddleware.wait() // let events to be processed
115115
}
@@ -118,7 +118,7 @@ struct ValidatorServiceTests {
118118

119119
let blockAuthoredEvents = events.filter { $0 is RuntimeEvents.BlockAuthored }
120120

121-
#expect(blockAuthoredEvents.count == 25)
121+
#expect(blockAuthoredEvents.count == 50)
122122
}
123123

124124
@Test

Codec/Sources/Codec/JamDecoder.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,17 @@ public class JamDecoder {
1717
return res
1818
}
1919

20-
public static func decode<T: Decodable>(_ type: T.Type, from data: DataInput, withConfig config: Any? = nil) throws -> T {
20+
public static func decode<T: Decodable>(
21+
_ type: T.Type,
22+
from data: DataInput,
23+
withConfig config: Any? = nil,
24+
allowTrailingBytes: Bool = false
25+
) throws -> T {
2126
let decoder = JamDecoder(data: data, config: config)
2227
let val = try decoder.decode(type)
23-
try decoder.finalize()
28+
if !allowTrailingBytes {
29+
try decoder.finalize()
30+
}
2431
return val
2532
}
2633

JAMTests/Package.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,12 @@ let package = Package(
3131
dependencies: [
3232
"Blockchain",
3333
],
34-
resources: [.copy("../../jamtestvectors")]
34+
resources: [
35+
.copy("../../jamtestvectors"),
36+
.copy("../../jamduna"),
37+
.copy("../../javajam"),
38+
.copy("../../jamixir"),
39+
]
3540
),
3641
.testTarget(
3742
name: "JAMTestsTests",
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import Blockchain
2+
import Codec
3+
import Foundation
4+
import Utils
5+
6+
struct JamTestnetTestcase: Codable {
7+
var preState: TestState
8+
var block: Block
9+
var postState: TestState
10+
}
11+
12+
extension ProtocolConfig {
13+
public enum Int4: ReadInt {
14+
public typealias TConfig = ProtocolConfigRef
15+
public static func read(config _: ProtocolConfigRef) -> Int {
16+
4
17+
}
18+
}
19+
}
20+
21+
struct TestState: Codable {
22+
var root: Data32
23+
var keyvals: [ConfigFixedSizeArray<Data, ProtocolConfig.Int4>]
24+
25+
func toDict() -> [Data32: Data] {
26+
// keyvals are array of 4 item arrays, we only need first 2 item of each array
27+
let tuples = keyvals.map { (Data32($0[0])!, $0[1]) }
28+
return tuples.reduce(into: [:]) { $0[$1.0] = $1.1 }
29+
}
30+
31+
// extract from string like "s=0|h=0x8c30f2c101674af1da31769e96ce72e81a4a44c89526d7d3ff0a1a511d5f3c9f l=25|t=[0] tlen=1"
32+
func extractDetails(from input: String) -> [String: String] {
33+
var result = [String: String]()
34+
let components = input.split(separator: "|")
35+
for component in components {
36+
let subComponents = component.split(separator: " ")
37+
for subComponent in subComponents {
38+
let pair = subComponent.split(separator: "=", maxSplits: 1).map(String.init)
39+
if pair.count == 2 {
40+
result[pair[0]] = pair[1]
41+
}
42+
}
43+
}
44+
return result
45+
}
46+
47+
func toState(config: ProtocolConfigRef = TestVariants.tiny.config) throws -> State {
48+
var changes: [(key: any StateKey, value: Codable & Sendable)] = []
49+
50+
for arr in keyvals {
51+
let key: any StateKey
52+
let value: Codable & Sendable
53+
54+
guard let desc = String(data: arr[2], encoding: .utf8) else {
55+
fatalError("invalid description")
56+
}
57+
guard let detail = String(data: arr[3], encoding: .utf8) else {
58+
fatalError("invalid detail")
59+
}
60+
61+
switch desc {
62+
case "c1":
63+
key = StateKeys.CoreAuthorizationPoolKey()
64+
value = try JamDecoder.decode(StateKeys.CoreAuthorizationPoolKey.Value.self, from: arr[1], withConfig: config)
65+
case "c2":
66+
key = StateKeys.AuthorizationQueueKey()
67+
value = try JamDecoder.decode(StateKeys.AuthorizationQueueKey.Value.self, from: arr[1], withConfig: config)
68+
case "c3":
69+
key = StateKeys.RecentHistoryKey()
70+
value = try JamDecoder.decode(StateKeys.RecentHistoryKey.Value.self, from: arr[1], withConfig: config)
71+
case "c4":
72+
key = StateKeys.SafroleStateKey()
73+
value = try JamDecoder.decode(StateKeys.SafroleStateKey.Value.self, from: arr[1], withConfig: config)
74+
case "c5":
75+
key = StateKeys.JudgementsKey()
76+
value = try JamDecoder.decode(StateKeys.JudgementsKey.Value.self, from: arr[1], withConfig: config)
77+
case "c6":
78+
key = StateKeys.EntropyPoolKey()
79+
value = try JamDecoder.decode(StateKeys.EntropyPoolKey.Value.self, from: arr[1], withConfig: config)
80+
case "c7":
81+
key = StateKeys.ValidatorQueueKey()
82+
value = try JamDecoder.decode(StateKeys.ValidatorQueueKey.Value.self, from: arr[1], withConfig: config)
83+
case "c8":
84+
key = StateKeys.CurrentValidatorsKey()
85+
value = try JamDecoder.decode(StateKeys.CurrentValidatorsKey.Value.self, from: arr[1], withConfig: config)
86+
case "c9":
87+
key = StateKeys.PreviousValidatorsKey()
88+
value = try JamDecoder.decode(StateKeys.PreviousValidatorsKey.Value.self, from: arr[1], withConfig: config)
89+
case "c10":
90+
key = StateKeys.ReportsKey()
91+
value = try JamDecoder.decode(StateKeys.ReportsKey.Value.self, from: arr[1], withConfig: config)
92+
case "c11":
93+
key = StateKeys.TimeslotKey()
94+
value = try JamDecoder.decode(StateKeys.TimeslotKey.Value.self, from: arr[1], withConfig: config)
95+
case "c12":
96+
key = StateKeys.PrivilegedServicesKey()
97+
value = try JamDecoder.decode(StateKeys.PrivilegedServicesKey.Value.self, from: arr[1], withConfig: config)
98+
case "c13":
99+
key = StateKeys.ActivityStatisticsKey()
100+
value = try JamDecoder.decode(StateKeys.ActivityStatisticsKey.Value.self, from: arr[1], withConfig: config)
101+
case "c14":
102+
key = StateKeys.AccumulationQueueKey()
103+
value = try JamDecoder.decode(StateKeys.AccumulationQueueKey.Value.self, from: arr[1], withConfig: config)
104+
case "c15":
105+
key = StateKeys.AccumulationHistoryKey()
106+
value = try JamDecoder.decode(StateKeys.AccumulationHistoryKey.Value.self, from: arr[1], withConfig: config)
107+
case "service_account":
108+
let details = extractDetails(from: detail)
109+
key = StateKeys.ServiceAccountKey(index: UInt32(details["s"]!)!)
110+
value = try JamDecoder.decode(StateKeys.ServiceAccountKey.Value.self, from: arr[1], withConfig: config)
111+
case "account_storage":
112+
let details = extractDetails(from: detail)
113+
key = StateKeys.ServiceAccountStorageKey(
114+
index: UInt32(details["s"]!)!,
115+
key: Data32(fromHexString: String(details["k"]!.suffix(64)))!
116+
)
117+
value = try JamDecoder.decode(StateKeys.ServiceAccountStorageKey.Value.self, from: arr[1], withConfig: config)
118+
case "account_preimage":
119+
let details = extractDetails(from: detail)
120+
key = StateKeys.ServiceAccountPreimagesKey(
121+
index: UInt32(details["s"]!)!,
122+
hash: Data32(fromHexString: String(details["h"]!.suffix(64)))!
123+
)
124+
value = arr[1]
125+
case "account_lookup":
126+
let details = extractDetails(from: detail)
127+
key = StateKeys.ServiceAccountPreimageInfoKey(
128+
index: UInt32(details["s"]!)!,
129+
hash: Data32(fromHexString: String(details["h"]!.suffix(64)))!,
130+
length: UInt32(details["l"]!)!
131+
)
132+
value = try JamDecoder.decode(StateKeys.ServiceAccountPreimageInfoKey.Value.self, from: arr[1], withConfig: config)
133+
default:
134+
fatalError("invalid key")
135+
}
136+
changes.append((key, value))
137+
}
138+
139+
let backend = StateBackend(InMemoryBackend(), config: config, rootHash: root)
140+
141+
let layer = StateLayer(changes: changes)
142+
143+
return State(backend: backend, layer: layer)
144+
}
145+
}
146+
147+
enum JamTestnet {
148+
static func loadTests(path: String, src: TestsSource) throws -> [Testcase] {
149+
try TestLoader.getTestcases(path: path, extension: "bin", src: src)
150+
}
151+
152+
static func decodeTestcase(_ input: Testcase, config: ProtocolConfigRef = TestVariants.tiny.config) throws -> JamTestnetTestcase {
153+
// NOTE: some tests have trailing bytes
154+
try JamDecoder.decode(JamTestnetTestcase.self, from: input.data, withConfig: config, allowTrailingBytes: true)
155+
}
156+
157+
static func runSTF(
158+
_ testcase: JamTestnetTestcase,
159+
config: ProtocolConfigRef = TestVariants.tiny.config
160+
) async throws -> Result<StateRef, Error> {
161+
let runtime = Runtime(config: config)
162+
163+
let result = await Result {
164+
// NOTE: skip block validate first, some tests does not ensure this
165+
let blockRef = try testcase.block.asRef().toValidated(config: config)
166+
let stateRef = try testcase.preState.toState(config: config).asRef()
167+
return try await runtime.apply(block: blockRef, state: stateRef)
168+
}
169+
170+
return result
171+
}
172+
}

0 commit comments

Comments
 (0)