Skip to content

Commit c7ba90a

Browse files
janezpodhostniktarakbysisyphusSmiling
authored
RandomBeaconHistory contract (#375)
* Minimal source of randomness contract * Add call to entropy * Source of randomness history suggestions (#377) * change sor heartbeat signature * RandomBeaconHistory impl + transactions & scripts * simplify RandomBeaconHistory interface * rm RandomBeaconHistory.HeartbeatPublicPath * cleanup RandomBeaconHistory formatting & naming * update go assets * update RandomBeaconHistory events & err messages * update go assets * Update contracts/RandomBeaconHistory.cdc Co-authored-by: Tarak Ben Youssef <[email protected]> * update go assets * rename RandomBeaconHistory.initHeight -> .lowestHeight * add RandomBeaconHistory.getRandomSourceHistoryPage() * add RandomBeaconHistory scripts * update go assets * fix RandomBeaconHistory typo * update go assets * update RandomBeacon naming * remove unused transaction and rename * fix get_source_of_randomness script * Adress review comments * change signature of sourceOfRandomness * fix tidy --------- Co-authored-by: Tarak Ben Youssef <[email protected]> Co-authored-by: Giovanni Sanchez <[email protected]>
1 parent dac80c4 commit c7ba90a

File tree

9 files changed

+344
-50
lines changed

9 files changed

+344
-50
lines changed

contracts/RandomBeaconHistory.cdc

+168
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/// RandomBeaconHistory (FLIP 123)
2+
///
3+
/// This contract stores the history of random sources generated by the Flow network. The defined Heartbeat resource is
4+
/// updated by the Flow Service Account at the end of every block with that block's source of randomness.
5+
///
6+
/// While the source values are safely generated by the Random Beacon (non-predictable, unbiasable, verifiable) and transmitted into the execution
7+
/// environment via the committing transaction, using the raw values from this contract does not guarantee non-revertible
8+
/// randomness. The Hearbeat is intended to be used in conjunction with a
9+
/// commit-reveal mechanism to provide an onchain source of non-revertible randomness.
10+
// It is also recommended to use the source values with a pseudo-random number
11+
// generator (PRNG) to generate an arbitrary-long sequence of random values.
12+
//
13+
// For usage of randomness where result abortion is not an issue, it is recommended
14+
// to use the Cadence built-in function `revertibleRandom`, which is also based on
15+
// the safe Random Beacon.
16+
///
17+
/// Read the full FLIP here: https://github.com/onflow/flips/pull/123
18+
///
19+
access(all) contract RandomBeaconHistory {
20+
21+
/// The height at which the first source of randomness was recorded
22+
access(contract) var lowestHeight: UInt64?
23+
/// Sequence of random sources recorded by the Heartbeat, stored as an array over a mapping to reduce storage
24+
access(contract) let randomSourceHistory: [[UInt8]]
25+
26+
/// The path of the Heartbeat resource in the deployment account
27+
access(all) let HeartbeatStoragePath: StoragePath
28+
29+
/* --- Hearbeat --- */
30+
//
31+
/// The Heartbeat resource containing each block's source of randomness in sequence
32+
///
33+
access(all) resource Heartbeat {
34+
35+
/// Callable by owner of the Heartbeat resource, Flow Service Account, records the provided random source
36+
///
37+
/// @param randomSourceHistory The random source to record
38+
///
39+
access(all) fun heartbeat(randomSourceHistory: [UInt8]) {
40+
41+
let currentBlockHeight = getCurrentBlock().height
42+
if RandomBeaconHistory.lowestHeight == nil {
43+
RandomBeaconHistory.lowestHeight = currentBlockHeight
44+
}
45+
46+
RandomBeaconHistory.randomSourceHistory.append(randomSourceHistory)
47+
}
48+
}
49+
50+
/* --- RandomSourceHistory --- */
51+
//
52+
/// Represents a random source value for a given block height
53+
///
54+
access(all) struct RandomSource {
55+
access(all) let blockHeight: UInt64
56+
access(all) let value: [UInt8]
57+
58+
init(blockHeight: UInt64, value: [UInt8]) {
59+
self.blockHeight = blockHeight
60+
self.value = value
61+
}
62+
}
63+
64+
/* --- RandomSourceHistoryPage --- */
65+
//
66+
/// Contains RandomSource values ordered chronologically according to associated block height
67+
///
68+
access(all) struct RandomSourceHistoryPage {
69+
access(all) let page: UInt64
70+
access(all) let perPage: UInt64
71+
access(all) let totalLength: UInt64
72+
access(all) let values: [RandomSource]
73+
74+
init(page: UInt64, perPage: UInt64, totalLength: UInt64, values: [RandomSource]) {
75+
self.page = page
76+
self.perPage = perPage
77+
self.totalLength = totalLength
78+
self.values = values
79+
}
80+
}
81+
82+
/* --- Contract Methods --- */
83+
//
84+
/// Getter for the source of randomness at a given block height. Panics if the requested block height either
85+
/// precedes or exceeds the recorded history. Note that a source of randomness for block n will not be accessible
86+
/// until block n+1.
87+
///
88+
/// @param atBlockHeight The block height at which to retrieve the source of randomness
89+
///
90+
/// @return The source of randomness at the given block height as RandomSource struct
91+
///
92+
access(all) fun sourceOfRandomness(atBlockHeight blockHeight: UInt64): RandomSource {
93+
pre {
94+
self.lowestHeight != nil: "History has not yet been initialized"
95+
blockHeight >= self.lowestHeight!: "Requested block height precedes recorded history"
96+
blockHeight < getCurrentBlock().height: "Source of randomness not yet recorded"
97+
}
98+
let index = blockHeight - self.lowestHeight!
99+
assert(
100+
index >= 0 && index < UInt64(self.randomSourceHistory.length),
101+
message: "Problem finding random source history index"
102+
)
103+
return RandomSource(blockHeight: blockHeight, value: self.randomSourceHistory[index])
104+
}
105+
106+
/// Retrieves a page from the history of random sources, ordered chronologically
107+
///
108+
/// @param page: The page number to retrieve, 0-indexed
109+
/// @param perPage: The number of random sources to include per page
110+
///
111+
/// @return A RandomSourceHistoryPage containing RandomSource values in choronological order according to
112+
/// associated block height
113+
///
114+
access(all) view fun getRandomSourceHistoryPage(_ page: UInt64, perPage: UInt64): RandomSourceHistoryPage {
115+
pre {
116+
self.lowestHeight != nil: "History has not yet been initialized"
117+
}
118+
let values: [RandomSource] = []
119+
let totalLength = UInt64(self.randomSourceHistory.length)
120+
121+
var startIndex = page * perPage
122+
if startIndex > totalLength {
123+
startIndex = totalLength
124+
}
125+
var endIndex = startIndex + perPage
126+
if endIndex > totalLength {
127+
endIndex = totalLength
128+
}
129+
// Return empty page if request exceeds last page
130+
if startIndex == endIndex {
131+
return RandomSourceHistoryPage(page: page, perPage: perPage, totalLength: totalLength, values: values)
132+
}
133+
134+
// Iterate over history and construct page RandomSource values
135+
let lowestHeight = self.lowestHeight!
136+
for i, block in self.randomSourceHistory.slice(from: Int(startIndex), upTo: Int(endIndex)) {
137+
values.append(
138+
RandomSource(
139+
blockHeight: lowestHeight + startIndex + UInt64(i),
140+
value: self.randomSourceHistory[startIndex + UInt64(i)]
141+
)
142+
)
143+
}
144+
145+
return RandomSourceHistoryPage(
146+
page: page,
147+
perPage: perPage,
148+
totalLength: totalLength,
149+
values: values
150+
)
151+
}
152+
153+
/// Getter for the block height at which the first source of randomness was recorded
154+
///
155+
/// @return The block height at which the first source of randomness was recorded
156+
///
157+
access(all) view fun getLowestHeight(): UInt64 {
158+
return self.lowestHeight ?? panic("History has not yet been initialized")
159+
}
160+
161+
init() {
162+
self.lowestHeight = nil
163+
self.randomSourceHistory = []
164+
self.HeartbeatStoragePath = /storage/FlowRandomBeaconHistoryHeartbeat
165+
166+
self.account.save(<-create Heartbeat(), to: self.HeartbeatStoragePath)
167+
}
168+
}

flow.json

+5-11
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,9 @@
11
{
2-
"emulators": {
3-
"default": {
4-
"port": 3569,
5-
"serviceAccount": "emulator-account"
6-
}
7-
},
82
"contracts": {
93
"FlowClusterQC": "./contracts/epochs/FlowClusterQC.cdc",
10-
"FlowEpoch": "./contracts/epochs/FlowEpoch.cdc",
11-
"FlowDKG": "./contracts/epochs/FlowEpoch.cdc",
12-
"NodeVersionBeacon": "./contracts/NodeVersionBeacon.cdc",
134
"FlowContractAudits": "./contracts/FlowContractAudits.cdc",
5+
"FlowDKG": "./contracts/epochs/FlowEpoch.cdc",
6+
"FlowEpoch": "./contracts/epochs/FlowEpoch.cdc",
147
"FlowFees": "./contracts/FlowFees.cdc",
158
"FlowIDTableStaking": "./contracts/FlowIDTableStaking.cdc",
169
"FlowIDTableStaking_old": "./contracts/FlowIDTableStaking_old.cdc",
@@ -19,6 +12,8 @@
1912
"FlowStorageFees": "./contracts/FlowStorageFees.cdc",
2013
"FlowToken": "./contracts/FlowToken.cdc",
2114
"LockedTokens": "./contracts/LockedTokens.cdc",
15+
"NodeVersionBeacon": "./contracts/NodeVersionBeacon.cdc",
16+
"RandomBeaconHistory": "./contracts/RandomBeaconHistory.cdc",
2217
"StakingProxy": "./contracts/StakingProxy.cdc"
2318
},
2419
"networks": {
@@ -31,6 +26,5 @@
3126
"address": "f8d6e0586b0a20c7",
3227
"key": "7677f7c9410f8773b482737c778b5d7c6acfdbbae718d61e4727a07667f66004"
3328
}
34-
},
35-
"deployments": {}
29+
}
3630
}

lib/go/contracts/contracts.go

+20-13
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,20 @@ import (
2626
///
2727

2828
const (
29-
flowFeesFilename = "FlowFees.cdc"
30-
storageFeesFilename = "FlowStorageFees.cdc"
31-
flowServiceAccountFilename = "FlowServiceAccount.cdc"
32-
flowTokenFilename = "FlowToken.cdc"
33-
flowIdentityTableFilename = "FlowIDTableStaking.cdc"
34-
flowQCFilename = "epochs/FlowClusterQC.cdc"
35-
flowDKGFilename = "epochs/FlowDKG.cdc"
36-
flowEpochFilename = "epochs/FlowEpoch.cdc"
37-
flowLockedTokensFilename = "LockedTokens.cdc"
38-
flowStakingProxyFilename = "StakingProxy.cdc"
39-
flowStakingCollectionFilename = "FlowStakingCollection.cdc"
40-
flowContractAuditsFilename = "FlowContractAudits.cdc"
41-
flowNodeVersionBeaconFilename = "NodeVersionBeacon.cdc"
29+
flowFeesFilename = "FlowFees.cdc"
30+
storageFeesFilename = "FlowStorageFees.cdc"
31+
flowServiceAccountFilename = "FlowServiceAccount.cdc"
32+
flowTokenFilename = "FlowToken.cdc"
33+
flowIdentityTableFilename = "FlowIDTableStaking.cdc"
34+
flowQCFilename = "epochs/FlowClusterQC.cdc"
35+
flowDKGFilename = "epochs/FlowDKG.cdc"
36+
flowEpochFilename = "epochs/FlowEpoch.cdc"
37+
flowLockedTokensFilename = "LockedTokens.cdc"
38+
flowStakingProxyFilename = "StakingProxy.cdc"
39+
flowStakingCollectionFilename = "FlowStakingCollection.cdc"
40+
flowContractAuditsFilename = "FlowContractAudits.cdc"
41+
flowNodeVersionBeaconFilename = "NodeVersionBeacon.cdc"
42+
flowRandomBeaconHistoryFilename = "RandomBeaconHistory.cdc"
4243

4344
// Test contracts
4445
// only used for testing
@@ -342,6 +343,12 @@ func NodeVersionBeacon() []byte {
342343
return []byte(code)
343344
}
344345

346+
func RandomBeaconHistory() []byte {
347+
code := assets.MustAssetString(flowRandomBeaconHistoryFilename)
348+
349+
return []byte(code)
350+
}
351+
345352
// FlowContractAudits returns the deprecated FlowContractAudits contract.
346353
// This contract is no longer used on any network
347354
func FlowContractAudits() []byte {

0 commit comments

Comments
 (0)