Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SoR History update #416

Merged
merged 28 commits into from Apr 15, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
9bc1dd8
first update to handle possible gaps in history
tarakby Mar 23, 2024
744408c
extend tests to cover single gap case
tarakby Mar 24, 2024
2f4b0ab
update getRandomSourceHistoryPage and add tests for backfilled pages
tarakby Mar 25, 2024
9e9e023
add test for non-contiguous gap edge case
tarakby Mar 26, 2024
10e8d20
enforce minimum source length
tarakby Mar 28, 2024
246804f
add RandomBeaconHistory.Backfiller and integrate with SoR commitment
sisyphusSmiling Mar 28, 2024
0372751
bump Flow CLI version in CI workflow
sisyphusSmiling Mar 28, 2024
f0c9b2e
Merge pull request #417 from onflow/giovanni/update-fix-sor-history
tarakby Mar 29, 2024
e2094e7
simplify the backfilling logic
tarakby Mar 29, 2024
b875b63
update gapStartIndex init value
tarakby Mar 29, 2024
322eff1
add event for missing and backfilled SoRs
tarakby Mar 29, 2024
fb153e2
update assets
tarakby Mar 29, 2024
c5b9d92
set and get for the backfiller limit
tarakby Apr 4, 2024
a9c658e
add a note about the hash output size
tarakby Apr 4, 2024
8b78cad
reduce calls to the large array length
tarakby Apr 4, 2024
178b5ef
go generate
tarakby Apr 4, 2024
bf7d1da
move all backfilling logic inside the backfiller resource
tarakby Apr 5, 2024
0580a8a
move correct index discover to Backfiller resource
sisyphusSmiling Apr 5, 2024
4e99430
add coverage.lcov to .gitignore
sisyphusSmiling Apr 5, 2024
c3d8cca
re-add early return to optimize common case
sisyphusSmiling Apr 5, 2024
ed762d8
rewrap contract comments
sisyphusSmiling Apr 5, 2024
466bed8
Merge pull request #418 from onflow/giovanni/fix-sor-history
tarakby Apr 8, 2024
a11baea
update .borrowBackfiller() access & add event test coverage
sisyphusSmiling Apr 9, 2024
bd2265f
tests: add non-continuous gaps events check and use backfilling constant
tarakby Apr 10, 2024
75d3500
PR review: use array length to compare with empty array and minor cha…
tarakby Apr 10, 2024
b181bc3
make generate
tarakby Apr 10, 2024
a14b5e2
optimize unnecessary function calls
tarakby Apr 12, 2024
e913f3a
minor optimization
tarakby Apr 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
106 changes: 101 additions & 5 deletions contracts/RandomBeaconHistory.cdc
Expand Up @@ -22,10 +22,58 @@ access(all) contract RandomBeaconHistory {
access(contract) var lowestHeight: UInt64?
/// Sequence of random sources recorded by the Heartbeat, stored as an array over a mapping to reduce storage
access(contract) let randomSourceHistory: [[UInt8]]
/// Start index of the first gap in the `randomSourceHistory` array where random sources were not recorded, because
/// of a heartbeat failure.
/// There may be non contiguous gaps in the history, `gapStartIndex` is the start index of the lowest-height
/// gap.
/// If no gap exists, `gapStartIndex` is equal to the `randomSourceHistory` array length.
access(contract) var gapStartIndex: UInt64
tarakby marked this conversation as resolved.
Show resolved Hide resolved

/// The path of the Heartbeat resource in the deployment account
access(all) let HeartbeatStoragePath: StoragePath


/// back fills entries in the history array starting from the stored `gapStartIndex`,
/// using `randomSource` as a seed for all entries.
//
/// all entries would use the same entropy. Each entry is extracted from `randomSource` using
/// successive hashing. This makes sure the entries are all distinct although they provide
/// the same entropy.
//
/// gaps only occur in the rare event of a system transaction failure. In this case, entries are still
/// filled using a source not known at the time of block execution, which guaranteed unpredicatability.
access(contract) fun backFill(randomSource: [UInt8]) {
// maximum number of entries to back fill per transaction to limit the computation cost.
let maxEntries = 100
let arrayLength = UInt64(self.randomSourceHistory.length)

var newEntry = randomSource
var index = self.gapStartIndex
var count = 0
while count < maxEntries {
// move to the next empty entry
while index < arrayLength && self.randomSourceHistory[index] != [] {
index = index + 1
}
// if we reach the end of the array then all existing gaps got filled
if index == arrayLength {
break
}
// back fill the empty entry
newEntry = HashAlgorithm.SHA3_256.hash(newEntry)
self.randomSourceHistory[index] = newEntry
index = index + 1
count = count + 1
}

// no more backfilling is possible but we need to update `gapStartIndex`
// to the next empty index if any still exists
while index < arrayLength && self.randomSourceHistory[index] != [] {
index = index + 1
}
self.gapStartIndex = index
}

/* --- Hearbeat --- */
//
/// The Heartbeat resource containing each block's source of randomness in sequence
Expand All @@ -36,14 +84,52 @@ access(all) contract RandomBeaconHistory {
///
/// @param randomSourceHistory The random source to record
///
access(all) fun heartbeat(randomSourceHistory: [UInt8]) {
/// The Flow protocol makes sure to call this function once per block as a system call. The transaction
/// comes at the end of each block so that the current block's entry becomes available only in the child
/// block.
///
access(all) fun heartbeat(randomSourceHistory: [UInt8]) {

let currentBlockHeight = getCurrentBlock().height
if RandomBeaconHistory.lowestHeight == nil {
RandomBeaconHistory.lowestHeight = currentBlockHeight
}

// next index to fill with the new random source
// so that evetually randomSourceHistory[nextIndex] = inputRandom
tarakby marked this conversation as resolved.
Show resolved Hide resolved
let nextIndex = currentBlockHeight - RandomBeaconHistory.lowestHeight!

// find out if `gapStartIndex` needs to be updated
if RandomBeaconHistory.gapStartIndex == UInt64(RandomBeaconHistory.randomSourceHistory.length) {
// enter only if no gap already exists in the past history.
// If a gap already exists, `gapStartIndex` should not be overwritten.
if nextIndex > UInt64(RandomBeaconHistory.randomSourceHistory.length) {
// enter if a new gap is detected in the current transaction,
// i.e some past height entries were not recorded.
// In this case, update `gapStartIndex`
RandomBeaconHistory.gapStartIndex = UInt64(RandomBeaconHistory.randomSourceHistory.length)
}
}

// regardless of whether `gapStartIndex` got updated or not,
// if a new gap is detected in the current transaction, fill the gap with empty entries.
while nextIndex > UInt64(RandomBeaconHistory.randomSourceHistory.length) {
// this happens in the rare case when a new gap occurs due to a system chunk failure
RandomBeaconHistory.randomSourceHistory.append([])
}

// we are now at the correct index to record the source of randomness
// created by the protocol for the current block
RandomBeaconHistory.randomSourceHistory.append(randomSourceHistory)

// check for any existing gap and backfill using the input random source if needed.
// If there are no gaps, `gapStartIndex` is equal to `RandomBeaconHistory`'s length.
if RandomBeaconHistory.gapStartIndex < UInt64(RandomBeaconHistory.randomSourceHistory.length) {
// backfilling happens in the rare case when a gap occurs due to a system chunk failure.
// backFilling is limited to a max entries only to limit the computation cost.
// This means a large gap may need a few transactions to get fully backfilled.
RandomBeaconHistory.backFill(randomSource: randomSourceHistory)
}
}
}

Expand Down Expand Up @@ -97,13 +183,17 @@ access(all) contract RandomBeaconHistory {
}
let index = blockHeight - self.lowestHeight!
assert(
index >= 0 && index < UInt64(self.randomSourceHistory.length),
index >= 0,
message: "Problem finding random source history index"
)
assert(
index < UInt64(self.randomSourceHistory.length) && self.randomSourceHistory[index] != [],
message: "Source of randomness is currently not available but will be available soon"
tarakby marked this conversation as resolved.
Show resolved Hide resolved
)
return RandomSource(blockHeight: blockHeight, value: self.randomSourceHistory[index])
}

/// Retrieves a page from the history of random sources, ordered chronologically
/// Retrieves a page from the history of random sources recorded so far, ordered chronologically
///
/// @param page: The page number to retrieve, 0-indexed
/// @param perPage: The number of random sources to include per page
Expand All @@ -126,18 +216,23 @@ access(all) contract RandomBeaconHistory {
if endIndex > totalLength {
endIndex = totalLength
}

// Return empty page if request exceeds last page
if startIndex == endIndex {
return RandomSourceHistoryPage(page: page, perPage: perPage, totalLength: totalLength, values: values)
}

// Iterate over history and construct page RandomSource values
let lowestHeight = self.lowestHeight!
for i, block in self.randomSourceHistory.slice(from: Int(startIndex), upTo: Int(endIndex)) {
for i, value in self.randomSourceHistory.slice(from: Int(startIndex), upTo: Int(endIndex)) {
assert(
value != [],
message: "Source of randomness is currently not available but will be available soon"
)
values.append(
RandomSource(
blockHeight: lowestHeight + startIndex + UInt64(i),
value: self.randomSourceHistory[startIndex + UInt64(i)]
value: value
)
)
}
Expand All @@ -161,6 +256,7 @@ access(all) contract RandomBeaconHistory {
init() {
self.lowestHeight = nil
self.randomSourceHistory = []
self.gapStartIndex = 0
self.HeartbeatStoragePath = /storage/FlowRandomBeaconHistoryHeartbeat

self.account.save(<-create Heartbeat(), to: self.HeartbeatStoragePath)
Expand Down