Skip to content

Commit d067c5d

Browse files
committed
simulation: added adversarial behaviour field to nodes in topology
Only behaviour supported is `UnboundedIbs`.
1 parent 19e8786 commit d067c5d

File tree

9 files changed

+122
-11
lines changed

9 files changed

+122
-11
lines changed

data/simulation/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ trace.rust.schema.json: trace.rust.d.ts
1717
npx prettier -w trace.rust.schema.json
1818

1919
.PHONY: validate
20-
validate:
20+
validate: all
2121
@echo "Validating config..."
2222
@npx pajv -m schema.schema.json -s config.schema.json -d config.default.yaml > /dev/null && echo "config.default.yaml valid" || exit 1
2323
@echo "Validating topology..."

data/simulation/topology.d.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ export interface Node<Location> {
2323
"cpu-core-count"?: bigint | null;
2424
location: Location;
2525
producers: { [producer: NodeName]: LinkInfo };
26+
/** If not null, the node will behave according to the given Behaviour.
27+
*
28+
* Only supported by Haskell simulation.
29+
*/
30+
adversarial?: Behaviour | null;
2631
}
2732

2833
/** Link information. */
@@ -38,3 +43,20 @@ export interface Cluster {
3843
}
3944

4045
export type Coord2D = [number, number];
46+
47+
export type Behaviour =
48+
| UnboundedIbs
49+
;
50+
51+
/** A node that after some time stops respecting IB sortition and
52+
instead starts generating old IBs every slot.
53+
54+
Only supported by Haskell simulation.
55+
*/
56+
export interface UnboundedIbs {
57+
behaviour: "unbounded-ibs";
58+
/* When to start the adversarial generation. */
59+
"starting-at-slot": number;
60+
"slot-of-generated-ibs": number;
61+
"ibs-per-slot": number;
62+
}

data/simulation/topology.schema.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@
2626
"Node<Cluster>": {
2727
"description": "A node.",
2828
"properties": {
29+
"adversarial": {
30+
"$ref": "#/definitions/UnboundedIbs",
31+
"description": "If not null, the node will behave according to the given Behaviour.\n\nOnly supported by Haskell simulation."
32+
},
2933
"cpu-core-count": {
3034
"additionalProperties": false,
3135
"properties": {},
@@ -51,6 +55,10 @@
5155
"Node<Coord2D>": {
5256
"description": "A node.",
5357
"properties": {
58+
"adversarial": {
59+
"$ref": "#/definitions/UnboundedIbs",
60+
"description": "If not null, the node will behave according to the given Behaviour.\n\nOnly supported by Haskell simulation."
61+
},
5462
"cpu-core-count": {
5563
"additionalProperties": false,
5664
"properties": {},
@@ -82,6 +90,25 @@
8290
}
8391
},
8492
"type": "object"
93+
},
94+
"UnboundedIbs": {
95+
"description": "A node that after some time stops respecting IB sortition and\ninstead starts generating old IBs every slot.\n\nOnly supported by Haskell simulation.",
96+
"properties": {
97+
"behaviour": {
98+
"const": "unbounded-ibs",
99+
"type": "string"
100+
},
101+
"ibs-per-slot": {
102+
"type": "number"
103+
},
104+
"slot-of-generated-ibs": {
105+
"type": "number"
106+
},
107+
"starting-at-slot": {
108+
"type": "number"
109+
}
110+
},
111+
"type": "object"
85112
}
86113
},
87114
"description": "The topology for a Leios simulation.\n\nThe nodes in a topology may either specify their location as cluster names,\nwhich may be omitted, or as coordinates, but all nodes in the topology must\nuse the same kind of location.",

leios-trace-hs/src/LeiosTopology.hs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
{-# LANGUAGE TypeFamilies #-}
2222
{-# LANGUAGE UndecidableInstances #-}
2323
{-# LANGUAGE ViewPatterns #-}
24+
{-# OPTIONS_GHC -Wno-incomplete-record-updates #-}
2425

2526
module LeiosTopology where
2627

@@ -37,7 +38,7 @@ import Data.Text (Text)
3738
import qualified Data.Vector as V
3839
import GHC.Generics (Generic)
3940
import GHC.Records (HasField (..))
40-
import JSONCompat (Getter, always, get, omitDefault, parseField, parseFieldOrDefault)
41+
import JSONCompat (Getter, always, camelToKebab, get, omitDefault, parseField, parseFieldOrDefault)
4142
import LeiosTypes (Point (..))
4243
import Text.Printf (PrintfArg)
4344

@@ -202,6 +203,10 @@ instance HasField "coord2D" (Node 'COORD2D) Point where
202203
getField :: Node 'COORD2D -> Point
203204
getField node = node.nodeInfo.location.coord2D
204205

206+
instance HasField "adversarial" (Node lk) (Maybe Behaviour) where
207+
getField :: Node lk -> Maybe Behaviour
208+
getField node = node.nodeInfo.adversarial
209+
205210
instance Default (Node 'CLUSTER) where
206211
def :: Node 'CLUSTER
207212
def = Node{nodeInfo = def, producers = mempty}
@@ -210,9 +215,13 @@ data NodeInfo (lk :: LocationKind) = NodeInfo
210215
{ stake :: {-# UNPACK #-} !Word
211216
, cpuCoreCount :: {-# UNPACK #-} !CpuCoreCount
212217
, location :: !(Location lk)
218+
, adversarial :: !(Maybe Behaviour)
213219
}
214220
deriving stock (Show, Eq, Generic)
215221

222+
data Behaviour = UnboundedIbs {startingAtSlot :: Word, slotOfGeneratedIbs :: Word, ibsPerSlot :: Word}
223+
deriving (Show, Eq, Generic)
224+
216225
instance HasField "coord2D" (NodeInfo 'COORD2D) Point where
217226
getField :: NodeInfo 'COORD2D -> Point
218227
getField nodeInfo = nodeInfo.location.coord2D
@@ -224,6 +233,7 @@ instance Default (NodeInfo 'CLUSTER) where
224233
{ stake = 0
225234
, cpuCoreCount = Unbounded
226235
, location = LocCluster Nothing
236+
, adversarial = Nothing
227237
}
228238

229239
data LinkInfo = LinkInfo
@@ -250,6 +260,7 @@ nodeToKVs getter node =
250260
, get @"cpuCoreCount" getter node
251261
, get @"location" getter node
252262
, get @"producers" getter node
263+
, get @"adversarial" getter node
253264
]
254265

255266
instance ToJSON (Node 'CLUSTER) where
@@ -273,6 +284,7 @@ instance FromJSON (Node 'CLUSTER) where
273284
cpuCoreCount <- parseFieldOrDefault @(Node 'CLUSTER) @"cpuCoreCount" obj
274285
location <- parseFieldOrDefault @(Node 'CLUSTER) @"location" obj
275286
producers <- parseFieldOrDefault @(Node 'CLUSTER) @"producers" obj
287+
adversarial <- parseFieldOrDefault @(Node 'CLUSTER) @"adversarial" obj
276288
pure Node{nodeInfo = NodeInfo{..}, ..}
277289

278290
instance FromJSON (Node 'COORD2D) where
@@ -285,8 +297,25 @@ instance FromJSON (Node 'COORD2D) where
285297
cpuCoreCount <- parseFieldOrDefault @(Node 'CLUSTER) @"cpuCoreCount" obj
286298
location <- parseField @(Node 'COORD2D) @"location" obj
287299
producers <- parseFieldOrDefault @(Node 'CLUSTER) @"producers" obj
300+
adversarial <- parseFieldOrDefault @(Node 'CLUSTER) @"adversarial" obj
288301
pure Node{nodeInfo = NodeInfo{..}, ..}
289302

303+
behaviourJSONOptions :: Options
304+
behaviourJSONOptions =
305+
defaultOptions
306+
{ sumEncoding = Json.defaultTaggedObject{Json.tagFieldName = "behaviour"}
307+
, fieldLabelModifier = camelToKebab
308+
, constructorTagModifier = camelToKebab
309+
, tagSingleConstructors = True
310+
}
311+
312+
instance FromJSON Behaviour where
313+
parseJSON = genericParseJSON behaviourJSONOptions
314+
315+
instance ToJSON Behaviour where
316+
toJSON = Json.genericToJSON behaviourJSONOptions
317+
toEncoding = Json.genericToEncoding behaviourJSONOptions
318+
290319
linkInfoToKVs :: KeyValue e kv => Getter LinkInfo -> LinkInfo -> [kv]
291320
linkInfoToKVs getter link =
292321
catMaybes

simulation/src/LeiosProtocol/Short/Generate.hs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ data BuffersView m = BuffersView
3838

3939
data Role :: Type -> Type where
4040
Base :: Role RankingBlock
41-
Propose :: Role InputBlock
41+
Propose :: {ibSlot :: Maybe SlotNo, delay :: Maybe DiffTime} -> Role InputBlock
4242
Endorse :: Role EndorseBlock
4343
Vote :: Role VoteMsg
4444

@@ -81,13 +81,13 @@ leiosBlockGenerator LeiosGeneratorConfig{..} =
8181
let !rb = mkPartialBlock slot body
8282
let !task = leios.praos.blockGenerationDelay rb
8383
return [(task, rb)]
84-
execute' slot Propose wins = do
84+
execute' slot Propose{ibSlot, delay} wins = do
8585
ibData <- lift $ atomically buffers.newIBData
8686
forM [toEnum $ fromIntegral sub | sub <- [0 .. wins - 1]] $ \sub -> do
8787
i <- nextBlkId InputBlockId
88-
let header = mkInputBlockHeader leios i slot sub nodeId ibData.referenceRankingBlock
88+
let header = mkInputBlockHeader leios i (fromMaybe slot ibSlot) sub nodeId ibData.referenceRankingBlock
8989
let !ib = mkInputBlock leios header ibData.txsPayload
90-
let !task = leios.delays.inputBlockGeneration ib
90+
let !task = fromMaybe (leios.delays.inputBlockGeneration ib) delay
9191
return (task, ib)
9292
execute' slot Endorse _wins = do
9393
i <- nextBlkId EndorseBlockId

simulation/src/LeiosProtocol/Short/Node.hs

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,14 @@ data LeiosNodeEvent
8484
---- Node Config
8585
--------------------------------------------------------------
8686

87+
data BlockGeneration
88+
= Honest
89+
| UnboundedIbs
90+
{ startingAtSlot :: SlotNo
91+
, slotOfGeneratedIbs :: SlotNo
92+
, ibsPerSlot :: Word64
93+
}
94+
8795
data LeiosNodeConfig = LeiosNodeConfig
8896
{ leios :: !LeiosConfig
8997
, slotConfig :: !SlotConfig
@@ -93,7 +101,8 @@ data LeiosNodeConfig = LeiosNodeConfig
93101
-- ^ for block generation
94102
, baseChain :: !(Chain RankingBlock)
95103
, processingQueueBound :: !Natural
96-
, processingCores :: NumCores
104+
, processingCores :: !NumCores
105+
, blockGeneration :: !BlockGeneration
97106
}
98107

99108
--------------------------------------------------------------
@@ -812,7 +821,7 @@ generator tracer cfg st = do
812821
return (rb, chain :> rb)
813822
traceWith tracer (PraosNodeEvent (PraosNodeEventGenerate rb))
814823
traceWith tracer (PraosNodeEvent (PraosNodeEventNewTip newChain))
815-
SomeAction Generate.Propose ib -> (GenIB,) $ do
824+
SomeAction Generate.Propose{} ib -> (GenIB,) $ do
816825
now <- getCurrentTime
817826
atomically $ do
818827
modifyTVar' st.relayIBState.relayBufferVar (RB.snocIfNew ib.header.id (ib.header, ib.body))
@@ -929,9 +938,14 @@ mkSchedule cfg = do
929938
calcWins rate = Just $ \sample ->
930939
if sample <= coerce (nodeRate cfg.stake rate) then 1 else 0
931940
voteRate = votingRatePerPipeline cfg.leios cfg.stake
941+
honestIBRate = inputBlockRate cfg.leios cfg.stake
942+
ibRate Honest slot = (SomeRole (Generate.Propose Nothing Nothing), honestIBRate slot)
943+
ibRate (UnboundedIbs{..}) slot =
944+
if slot < startingAtSlot
945+
then (SomeRole (Generate.Propose Nothing Nothing), honestIBRate slot)
946+
else (SomeRole (Generate.Propose (Just slotOfGeneratedIbs) (Just 0)), Just (const $ ibsPerSlot))
932947
pureRates =
933-
[ (SomeRole Generate.Propose, inputBlockRate cfg.leios cfg.stake)
934-
, (SomeRole Generate.Endorse, endorseBlockRate cfg.leios cfg.stake)
948+
[ (SomeRole Generate.Endorse, endorseBlockRate cfg.leios cfg.stake)
935949
, (SomeRole Generate.Base, const $ calcWins (NetworkRate cfg.leios.praos.blockFrequencyPerSlot))
936950
]
937951
rates votingSlots slot = do
@@ -943,7 +957,11 @@ mkSchedule cfg = do
943957
writeTVar votingSlots sls
944958
pure (Just voteRate)
945959
_ -> pure Nothing
946-
pure $ (SomeRole Generate.Vote, vote) : map (fmap ($ slot)) pureRates
960+
pure $
961+
[ (SomeRole Generate.Vote, vote)
962+
, ibRate cfg.blockGeneration slot
963+
]
964+
++ map (fmap ($ slot)) pureRates
947965
pickFromRanges :: StdGen -> [(SlotNo, SlotNo)] -> [SlotNo]
948966
pickFromRanges rng0 rs = snd $ mapAccumL f rng0 rs
949967
where

simulation/src/LeiosProtocol/Short/Sim.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,7 @@ traceRelayLink1 connectionOptions =
449449
, -- \^ overall size of txs to include in IBs
450450
processingQueueBound = 100
451451
, processingCores = Infinite
452+
, blockGeneration = Honest
452453
, ..
453454
}
454455
(pA, cB) <- newConnectionBundle (leiosTracer nodeA nodeB) (uncurry configureConnection connectionOptions)

simulation/src/LeiosProtocol/Short/SimP2P.hs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import SimTCPLinks (labelDirToLabelLink, selectTimedEvents, simTracer)
2525
import SimTypes
2626
import System.Random (StdGen, split)
2727
import Topology (P2PNetwork (..))
28+
import qualified Topology
2829

2930
traceLeiosP2P ::
3031
StdGen ->
@@ -109,6 +110,14 @@ exampleTrace2' rng0 leios@LeiosConfig{praos = PraosConfig{configureConnection}}
109110
, processingCores
110111
, nodeId
111112
, rng
113+
, blockGeneration = case Map.lookup nodeId =<< p2pAdversaries of
114+
Nothing -> Honest
115+
Just Topology.UnboundedIbs{..} ->
116+
UnboundedIbs
117+
{ startingAtSlot = SlotNo $ fromIntegral startingAtSlot
118+
, slotOfGeneratedIbs = SlotNo $ fromIntegral slotOfGeneratedIbs
119+
, ibsPerSlot = fromIntegral ibsPerSlot
120+
}
112121
}
113122
where
114123
processingCores = fromMaybe undefined $ Map.lookup nodeId p2pNodeCores

simulation/src/Topology.hs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ benchTopologyToTopology benchTopology latencies stakeShareSize =
106106
{ stake = maybe 0 (stakeShareSize *) benchTopologyNode.pools
107107
, cpuCoreCount = Unbounded
108108
, location = LocCluster (regionNameToClusterName <$> benchTopologyNode.region)
109+
, adversarial = Nothing
109110
}
110111
, producers =
111112
M.fromList
@@ -336,6 +337,7 @@ data P2PNetwork = P2PNetwork
336337
, p2pNodeNames :: !(Map NodeId Text)
337338
, p2pNodeCores :: !(Map NodeId NumCores)
338339
, p2pNodeStakes :: !(Map NodeId StakeFraction)
340+
, p2pAdversaries :: !(Maybe (Map NodeId Behaviour))
339341
, p2pLinks :: !(Map Link (Latency, Maybe BandwidthBytesPerSecond))
340342
, p2pWorld :: !World
341343
}
@@ -381,6 +383,7 @@ grToP2PNetwork p2pWorld gr = P2PNetwork{..}
381383
totalStake = fromIntegral . sum $ map (fromIntegral @_ @Integer . (.stake) . snd) $ Map.elems nodeInfoMap
382384
p2pLinks = flip Map.map edgeInfoMap $ \link ->
383385
(link.latencyS, fromIntegral <$> unBandwidthBps link.bandwidthBytesPerSecond)
386+
p2pAdversaries = Just $ Map.fromList [(nid, b) | (nid, (_, NodeInfo{adversarial = Just b})) <- Map.toList nodeInfoMap]
384387

385388
p2pNetworkToGr :: Word -> P2PNetwork -> Gr (NodeName, NodeInfo COORD2D) LinkInfo
386389
p2pNetworkToGr totalStake P2PNetwork{..} = G.mkGraph grNodes grLinks
@@ -394,6 +397,7 @@ p2pNetworkToGr totalStake P2PNetwork{..} = G.mkGraph grNodes grLinks
394397
Infinite -> Nothing
395398
Finite n -> Just $ fromIntegral n
396399
, let location = LocCoord2D point
400+
, let adversarial = Map.lookup nId =<< p2pAdversaries
397401
]
398402
grLinks =
399403
[ linkToEdge link linkInfo
@@ -420,6 +424,7 @@ topologyToNetwork P2PTopography{..} = P2PNetwork{p2pLinks = fmap (,defaultBandwi
420424
numNodes = fromIntegral $ Map.size p2pNodes
421425
-- TODO: unrestricted bandwidth is unsupported
422426
defaultBandwidthBps = Just (kilobytes 1000)
427+
p2pAdversaries = Nothing
423428

424429
overrideUnlimitedBandwidth :: Bytes -> P2PNetwork -> P2PNetwork
425430
overrideUnlimitedBandwidth x P2PNetwork{..} =

0 commit comments

Comments
 (0)