Create a Dependency that can load checkpoints from bundle

Closes #1315

This PR introduces small changes on each commit.
Things done:

rename Checkpoint+Constants to Checkpoint+helpers

Move `Checkpoint` from Model folder to Checkpoint folder

Remove unused function `ofLatestCheckpoint` from BlockHeight

Create a protocol called `CheckpointSource` that contains the
relevant functionality to get checkpoints from Bundle

Create a set of tests that check that functionality is maintained
when a `CheckpointSource` is used instead of Checkpoint helpers

Implement `BundleCheckpointSource` and add Tests

Code clean up: move `BundleCheckpointURLProvider` to its own file

Code clean up: `Checkpoint+helpers` match file header

Replace use of `Checkpoint.birthday(with:network)` with CheckpointSource

Revert "Remove unused function `ofLatestCheckpoint` from BlockHeight"

addresses PR comment from @daira

This reverts commit d0e154ded7, since it
modifies a public API and it was not the goal of this PR.

Update Sources/ZcashLightClientKit/Checkpoint/BundleCheckpointSource.swift

Use a decent Date Format

Co-authored-by: Daira Emma Hopwood <daira@jacaranda.org>

Improve documentation on BundleCheckpointURLProvider

Co-authored-by: Daira Emma Hopwood <daira@jacaranda.org>

Improve documentation on BundleCheckpointURLProvider

Co-authored-by: Daira Emma Hopwood <daira@jacaranda.org>

use YYYY-mm-dd on file header

author: @daira

Co-authored-by: Daira Emma Hopwood <daira@jacaranda.org>

Add test that verifies that the exact height is returned if available
This commit is contained in:
Francisco Gindre 2023-10-30 15:49:15 -03:00
parent 65cab30d64
commit 6625ffd711
No known key found for this signature in database
GPG Key ID: 6B61CD8DAA2862B4
12 changed files with 338 additions and 60 deletions

View File

@ -0,0 +1,38 @@
//
// BundleCheckpointSource.swift
//
//
// Created by Francisco Gindre on 2023-10-30.
//
import Foundation
struct BundleCheckpointSource: CheckpointSource {
var network: NetworkType
var saplingActivation: Checkpoint
init(network: NetworkType) {
self.network = network
self.saplingActivation = switch network {
case .mainnet:
Checkpoint.mainnetMin
case .testnet:
Checkpoint.testnetMin
}
}
func latestKnownCheckpoint() -> Checkpoint {
Checkpoint.birthday(
with: .max,
checkpointDirectory: BundleCheckpointURLProvider.default.url(self.network)
) ?? saplingActivation
}
func birthday(for height: BlockHeight) -> Checkpoint {
Checkpoint.birthday(
with: height,
checkpointDirectory: BundleCheckpointURLProvider.default.url(self.network)
) ?? saplingActivation
}
}

View File

@ -0,0 +1,59 @@
//
// BundleCheckpointURLProvider.swift
//
//
// Created by Francisco Gindre on 2023-10-30.
//
import Foundation
struct BundleCheckpointURLProvider {
var url: (NetworkType) -> URL
}
extension BundleCheckpointURLProvider {
/// Attempts to resolve the platform. `#if os(macOS)` implies that the build is for a macOS
/// target, otherwise we assume the build is for an iOS target.
static let `default` = BundleCheckpointURLProvider { networkType in
#if os(macOS)
Self.macOS.url(networkType)
#else
Self.iOS.url(networkType)
#endif
}
static let iOS = BundleCheckpointURLProvider(url: { networkType in
switch networkType {
case .mainnet:
return Checkpoint.mainnetCheckpointDirectory
case .testnet:
return Checkpoint.testnetCheckpointDirectory
}
})
/// This variant attempts to retrieve the saplingActivation checkpoint for the given network
/// type using `Bundle.module.url(forResource:withExtension:subdirectory:localization)`.
/// If not found it will return `WalletBirthday.mainnetCheckpointDirectory` or
/// `WalletBirthday.testnetCheckpointDirectory`. This responds to tests failing on a macOS
/// target because the checkpoint resources would not be found.
static let macOS = BundleCheckpointURLProvider(url: { networkType in
switch networkType {
case .mainnet:
return Bundle.module.url(
forResource: "419200",
withExtension: "json",
subdirectory: "checkpoints/mainnet/",
localization: nil
)?
.deletingLastPathComponent() ?? Checkpoint.mainnetCheckpointDirectory
case .testnet:
return Bundle.module.url(
forResource: "280000",
withExtension: "json",
subdirectory: "checkpoints/testnet/",
localization: nil
)?
.deletingLastPathComponent() ?? Checkpoint.testnetCheckpointDirectory
}
})
}

View File

@ -1,5 +1,5 @@
//
// WalletBirthday+Constants.swift
// Checkpoint+helpers.swift
// ZcashLightClientKit
//
// Created by Francisco Gindre on 7/28/21.
@ -64,54 +64,3 @@ extension Checkpoint {
}
}
}
struct BundleCheckpointURLProvider {
var url: (NetworkType) -> URL
}
extension BundleCheckpointURLProvider {
/// Attempts to resolve the platform by checking `#if os(macOS)` build corresponds to a MacOS target
/// `#else` branch of that condition will assume iOS is the target platform
static let `default` = BundleCheckpointURLProvider { networkType in
#if os(macOS)
Self.macOS.url(networkType)
#else
Self.iOS.url(networkType)
#endif
}
static let iOS = BundleCheckpointURLProvider(url: { networkType in
switch networkType {
case .mainnet:
return Checkpoint.mainnetCheckpointDirectory
case .testnet:
return Checkpoint.testnetCheckpointDirectory
}
})
/// This variant attempts to retrieve the saplingActivation checkpoint for the given network type
/// using `Bundle.module.url(forResource:withExtension:subdirectory:localization)`
/// if not found it will return `WalletBirthday.mainnetCheckpointDirectory` or
/// `WalletBirthday.testnetCheckpointDirectory`. This responds to tests
/// failing on MacOS target because the checkpoint resources would fail.
static let macOS = BundleCheckpointURLProvider(url: { networkType in
switch networkType {
case .mainnet:
return Bundle.module.url(
forResource: "419200",
withExtension: "json",
subdirectory: "checkpoints/mainnet/",
localization: nil
)?
.deletingLastPathComponent() ?? Checkpoint.mainnetCheckpointDirectory
case .testnet:
return Bundle.module.url(
forResource: "280000",
withExtension: "json",
subdirectory: "checkpoints/testnet/",
localization: nil
)?
.deletingLastPathComponent() ?? Checkpoint.testnetCheckpointDirectory
}
})
}

View File

@ -0,0 +1,34 @@
//
// CheckpointSource.swift
//
//
// Created by Francisco Gindre on 2023-10-30.
//
import Foundation
/// A protocol that abstracts the requirements around obtaining wallet checkpoints
/// (also known as TreeStates).
protocol CheckpointSource {
/// `NetworkType` of this Checkpoint source
var network: NetworkType { get }
/// The `Checkpoint` that represents the block in which Sapling was activated
var saplingActivation: Checkpoint { get }
/// Obtain the latest `Checkpoint` in terms of block height known by
/// this `CheckpointSource`. It is possible that the returned checkpoint
/// is not the latest checkpoint that exists in the blockchain.
/// - Returns a `Checkpoint` with the highest height known by this source
func latestKnownCheckpoint() -> Checkpoint
/// Obtain a `Checkpoint` in terms of a "wallet birthday". Wallet birthday
/// is estimated to be the latest height of the Zcash blockchain at the moment when the wallet was
/// created.
/// - Parameter height: Estimated or effective height known for the wallet birthday
/// - Returns: a `Checkpoint` that will allow the wallet to manage funds from the given `height`
/// onwards.
/// - Note: When the user knows the exact height of the first received funds for a wallet,
/// the effective birthday of that wallet is `transaction.height - 1`.
func birthday(for height: BlockHeight) -> Checkpoint
}

View File

@ -0,0 +1,14 @@
//
// CheckpointSourceFactory.swift
//
//
// Created by Francisco Gindre on 2023-10-30.
//
import Foundation
struct CheckpointSourceFactory {
static func fromBundle(for network: NetworkType) -> CheckpointSource {
BundleCheckpointSource(network: network)
}
}

View File

@ -281,7 +281,7 @@ public class Initializer {
self.storage = container.resolve(CompactBlockRepository.self)
self.blockDownloaderService = container.resolve(BlockDownloaderService.self)
self.network = network
self.walletBirthday = Checkpoint.birthday(with: 0, network: network).height
self.walletBirthday = container.resolve(CheckpointSource.self).saplingActivation.height
self.urlsParsingError = urlsParsingError
self.logger = container.resolve(Logger.self)
}
@ -416,7 +416,9 @@ public class Initializer {
return .seedRequired
}
let checkpoint = Checkpoint.birthday(with: walletBirthday, network: network)
let checkpointSource = container.resolve(CheckpointSource.self)
let checkpoint = checkpointSource.birthday(for: walletBirthday)
self.walletBirthday = checkpoint.height

View File

@ -17,6 +17,10 @@ enum Dependencies {
loggingPolicy: Initializer.LoggingPolicy = .default(.debug),
enableBackendTracing: Bool = false
) {
container.register(type: CheckpointSource.self, isSingleton: true) { _ in
return CheckpointSourceFactory.fromBundle(for: networkType)
}
container.register(type: Logger.self, isSingleton: true) { _ in
let logger: Logger
switch loggingPolicy {

View File

@ -97,8 +97,9 @@ class ReOrgTests: ZcashTestCase {
let mockLatestHeight = BlockHeight(663200)
let targetLatestHeight = BlockHeight(663202)
let reOrgHeight = BlockHeight(663195)
let walletBirthday = Checkpoint.birthday(with: 663150, network: network).height
let checkpointSource = CheckpointSourceFactory.fromBundle(for: network.networkType)
let walletBirthday = checkpointSource.birthday(for: 663150).height
try await basicReOrgTest(
baseDataset: .beforeReOrg,
reorgDataset: .afterSmallReorg,
@ -113,8 +114,9 @@ class ReOrgTests: ZcashTestCase {
let mockLatestHeight = BlockHeight(663200)
let targetLatestHeight = BlockHeight(663250)
let reOrgHeight = BlockHeight(663180)
let walletBirthday = Checkpoint.birthday(with: BlockHeight(663150), network: network).height
let checkpointSource = CheckpointSourceFactory.fromBundle(for: network.networkType)
let walletBirthday = checkpointSource.birthday(for: 663150).height
try await basicReOrgTest(
baseDataset: .beforeReOrg,
reorgDataset: .afterLargeReorg,

View File

@ -52,7 +52,8 @@ class TransactionEnhancementTests: ZcashTestCase {
waitExpectation = XCTestExpectation(description: "\(self.description) waitExpectation")
let birthday = Checkpoint.birthday(with: walletBirthday, network: network)
let checkpointSource = CheckpointSourceFactory.fromBundle(for: network.networkType)
let birthday = checkpointSource.birthday(for: walletBirthday)
let pathProvider = DefaultResourceProvider(network: network)
processorConfig = CompactBlockProcessor.Configuration(

View File

@ -0,0 +1,174 @@
//
// CheckpointSourceTests.swift
//
//
// Created by Francisco Gindre on 2023-10-30.
//
import XCTest
@testable import TestUtils
@testable import ZcashLightClientKit
class CheckpointSourceTests: XCTestCase {
func test_BirthdayGetsMostRecentCheckpointPrecedingTheGivenHeight_Testnet() throws {
let source = CheckpointSourceFactory.fromBundle(for: .testnet)
let birthday = source.birthday(for: 1530003)
let expected = Checkpoint(
height: 1530000,
hash: "0011f78082f26747e02f0ab3525dc34d8df8f69dde273f462fcbf08fe2aa14d6",
time: 1629030383,
saplingTree: """
0103ac57cbe96a0f86b78527aa69b21db02318e7e7a6995cbe497a107707825655001001623311941fc8cfac849331dca1ba89a60552eb9dbadd0019f8dfcb5f6ac6c906\
01b9a73d583be12b8e9c8a7616fe78a65469a2b91bdf02d411951fa261c9e1e64001e64e2365c8064f711643681da68b4fd626b28e5624abb9fb19d13208818b4d600133\
0c2415a69eddb56d7a0846f03f4c98936607d5c0e7f580748224bd2117e51200000149f61a12a3f8407f4f7bd3e4f619937fa1a09e984a5f7334fcd7734c4ba3e3690000\
0001bab80e68a5c63460d1e5c94ef540940792fa4703fa488b09fdfded97f8ec8a3d00013d2fd009bf8a22d68f720eac19c411c99014ed9c5f85d5942e15d1fc039e2868\
0001f08f39275112dd8905b854170b7f247cf2df18454d4fa94e6e4f9320cca05f24011f8322ef806eb2430dc4a7a41c1b344bea5be946efc7b4349c1c9edb14ff9d39
""",
orchardTree: nil
)
XCTAssertEqual(birthday, expected)
}
func test_BirthdayGetsMostRecentCheckpointPrecedingTheGivenHeight_Mainnet() throws {
let source = CheckpointSourceFactory.fromBundle(for: .mainnet)
let birthday = source.birthday(for: 1340004)
let expected = Checkpoint(
height: 1340000,
hash: "00000000031bc547da975ebd77d9113b178053f88fb6a1d8511b4f8962c21c4b",
time: 1627846248,
saplingTree: """
01ef55e131bf5e7d6737a6e353fe0ff246ba8938a264335457452db2c4023241590113f4e2a1f043d0a303c769d9aac5eeb8b6854d1a64d71b6b86cda2e0eeee07621301\
206a8d77952d4143cc5ba4d7943261e7145f0f138a81fe37c10e50a487487966012fb54cf3a70cccf01479fefc42e539c92a8215aead4179278cf1e8a302cb4868014574\
313eb9fd9ee592346fdf27752f698c1f629b044437853972e266e95b56020001be3f0fa5b20bbfa445293d588073dc27a856c92e9903831c6de4455f03d57a0401bb534b\
0af17c990f836204115aa17d4c2504fa0a675353ec7ae8a7d67510cc46012e2edeb7e5acb0d440dd5b500bec4a6efd6f53ba02c10e3883e23e53d7f91369000183c334e4\
55aeeeb82cceddbe832919324d7011418749fc9dea759cfa6c2cc21501f4a3504117d35efa15f57d5fdd19515b7fb1dd14c3b98b8a91685f0f788db330000000018846ec\
9170ad4e40a093cfb53162e5211d55377d8d22f826cde7783d30c1dd5f01b35fe4a943a47404f68db220c77b0573e13c3378a65c6f2396f93be7609d8f2a000125911f45\
24469c00ccb1ba69e64f0ee7380c8d17bbfc76ecd238421b86eb6e09000118f64df255c9c43db708255e7bf6bffd481e5c2f38fe9ed8f3d189f7f9cf2644
""",
orchardTree: nil
)
XCTAssertEqual(birthday, expected)
}
func test_BirthdayCheckpointGetsExactHeightIfAvailable_Testnet() throws {
let source = CheckpointSourceFactory.fromBundle(for: .testnet)
let birthday = source.birthday(for: 1520000)
let expected = Checkpoint(
height: 1520000,
hash: "0014a50344a6a43b02421286f6db15dad50cea54f3f0858f044ad0f1b845c395",
time: 1628358967,
saplingTree: "017d0620dbe96cb488e44dccfde260cf599c23c4ca689589d2e1ad743ec6770a6d00100000000000000000000001bab80e68a5c63460d1e5c94ef540940792fa4703fa488b09fdfded97f8ec8a3d00013d2fd009bf8a22d68f720eac19c411c99014ed9c5f85d5942e15d1fc039e28680001f08f39275112dd8905b854170b7f247cf2df18454d4fa94e6e4f9320cca05f24011f8322ef806eb2430dc4a7a41c1b344bea5be946efc7b4349c1c9edb14ff9d39",
orchardTree: nil
)
XCTAssertEqual(birthday, expected)
}
func test_BirthdayCheckpointGetsExactHeightIfAvailable_Mainnet() throws {
let source = CheckpointSourceFactory.fromBundle(for: .mainnet)
let birthday = source.birthday(for: 1340000)
let expected = Checkpoint(
height: 1340000,
hash: "00000000031bc547da975ebd77d9113b178053f88fb6a1d8511b4f8962c21c4b",
time: 1627846248,
saplingTree: """
01ef55e131bf5e7d6737a6e353fe0ff246ba8938a264335457452db2c4023241590113f4e2a1f043d0a303c769d9aac5eeb8b6854d1a64d71b6b86cda2e0eeee07621301\
206a8d77952d4143cc5ba4d7943261e7145f0f138a81fe37c10e50a487487966012fb54cf3a70cccf01479fefc42e539c92a8215aead4179278cf1e8a302cb4868014574\
313eb9fd9ee592346fdf27752f698c1f629b044437853972e266e95b56020001be3f0fa5b20bbfa445293d588073dc27a856c92e9903831c6de4455f03d57a0401bb534b\
0af17c990f836204115aa17d4c2504fa0a675353ec7ae8a7d67510cc46012e2edeb7e5acb0d440dd5b500bec4a6efd6f53ba02c10e3883e23e53d7f91369000183c334e4\
55aeeeb82cceddbe832919324d7011418749fc9dea759cfa6c2cc21501f4a3504117d35efa15f57d5fdd19515b7fb1dd14c3b98b8a91685f0f788db330000000018846ec\
9170ad4e40a093cfb53162e5211d55377d8d22f826cde7783d30c1dd5f01b35fe4a943a47404f68db220c77b0573e13c3378a65c6f2396f93be7609d8f2a000125911f45\
24469c00ccb1ba69e64f0ee7380c8d17bbfc76ecd238421b86eb6e09000118f64df255c9c43db708255e7bf6bffd481e5c2f38fe9ed8f3d189f7f9cf2644
""",
orchardTree: nil
)
XCTAssertEqual(birthday, expected)
}
func test_startBirthdayIsGivenIfTooLow_Testnet() throws {
let source = CheckpointSourceFactory.fromBundle(for: .testnet)
let birthday = source.birthday(for: 4)
let expected = Checkpoint(
height: 280000,
hash: "000420e7fcc3a49d729479fb0b560dd7b8617b178a08e9e389620a9d1dd6361a",
time: 1535262293,
saplingTree: "000000",
orchardTree: nil
)
XCTAssertEqual(birthday, expected)
}
func test_startBirthdayIsGivenIfTooLow_Mainnet() throws {
let source = CheckpointSourceFactory.fromBundle(for: .mainnet)
let birthday = source.birthday(for: 4)
let expected = Checkpoint(
height: 419200,
hash: "00000000025a57200d898ac7f21e26bf29028bbe96ec46e05b2c17cc9db9e4f3",
time: 1540779337,
saplingTree: "000000",
orchardTree: nil
)
XCTAssertEqual(birthday, expected)
}
func test_orchardTreeIsNotNilOnActivation_Mainnet() throws {
let activationHeight = 1_687_104
let source = CheckpointSourceFactory.fromBundle(for: .mainnet)
let birthday = source.birthday(for: activationHeight)
XCTAssertEqual(birthday.height, activationHeight)
XCTAssertEqual(birthday.orchardTree, "000000")
}
func test_orchardTreeIsNilBeforeActivation_Mainnet() throws {
let activationHeight = 1_687_104
let source = CheckpointSourceFactory.fromBundle(for: .mainnet)
let birthday = source.birthday(for: activationHeight - 1)
XCTAssertNil(birthday.orchardTree)
}
func test_orchardTreeIsNotNilOnActivation_Testnet() throws {
let activationHeight = 1_842_420
let source = CheckpointSourceFactory.fromBundle(for: .testnet)
let birthday = source.birthday(for: activationHeight)
XCTAssertEqual(birthday.height, activationHeight)
XCTAssertEqual(birthday.orchardTree, "000000")
}
func test_orchardTreeIsNilBeforeActivation_Testnet() throws {
let activationHeight = 1_687_104
let source = CheckpointSourceFactory.fromBundle(for: .testnet)
let birthday = source.birthday(for: activationHeight - 1)
XCTAssertNil(birthday.orchardTree)
}
}

View File

@ -107,7 +107,8 @@ class ZcashRustBackendTests: XCTestCase {
let initResult = try await rustBackend.initDataDb(seed: seed)
XCTAssertEqual(initResult, .success)
let treeState = Checkpoint.birthday(with: 1234567, network: ZcashMainnet()).treeState()
let checkpointSource = CheckpointSourceFactory.fromBundle(for: .mainnet)
let treeState = checkpointSource.birthday(for: 1234567).treeState()
let usk = try await rustBackend.createAccount(seed: seed, treeState: treeState, recoverUntil: nil)
XCTAssertEqual(usk.account, 0)