@effectstream/sync 0.100.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +29 -0
- package/src/mod.ts +3 -0
- package/src/sync-protocols/avail/AvailClient.ts +120 -0
- package/src/sync-protocols/avail/fetcher.ts +174 -0
- package/src/sync-protocols/avail/state.ts +134 -0
- package/src/sync-protocols/avail/types.ts +92 -0
- package/src/sync-protocols/base/fetcher.ts +43 -0
- package/src/sync-protocols/base/page.ts +89 -0
- package/src/sync-protocols/base/primitive.ts +24 -0
- package/src/sync-protocols/base/state.ts +143 -0
- package/src/sync-protocols/bitcoin/fetcher.ts +549 -0
- package/src/sync-protocols/bitcoin/state.ts +144 -0
- package/src/sync-protocols/bitcoin/types.ts +92 -0
- package/src/sync-protocols/celestia/CelestiaClient.ts +139 -0
- package/src/sync-protocols/celestia/fetcher.ts +165 -0
- package/src/sync-protocols/celestia/state.ts +131 -0
- package/src/sync-protocols/celestia/types.ts +46 -0
- package/src/sync-protocols/common/page-helpers.ts +65 -0
- package/src/sync-protocols/common/root.ts +35 -0
- package/src/sync-protocols/common/utils.ts +29 -0
- package/src/sync-protocols/evm/fetcher.ts +290 -0
- package/src/sync-protocols/evm/state.ts +122 -0
- package/src/sync-protocols/evm/types.ts +53 -0
- package/src/sync-protocols/midnight/MidnightClient.ts +210 -0
- package/src/sync-protocols/midnight/fetcher.ts +252 -0
- package/src/sync-protocols/midnight/state.ts +147 -0
- package/src/sync-protocols/midnight/types.ts +128 -0
- package/src/sync-protocols/midnight/zswap-decoder.ts +123 -0
- package/src/sync-protocols/mod.ts +29 -0
- package/src/sync-protocols/near/NearClient.ts +178 -0
- package/src/sync-protocols/near/fetcher.ts +274 -0
- package/src/sync-protocols/near/state.ts +130 -0
- package/src/sync-protocols/near/types.ts +46 -0
- package/src/sync-protocols/ntp/fetcher.ts +199 -0
- package/src/sync-protocols/ntp/state.ts +118 -0
- package/src/sync-protocols/ntp/types.ts +40 -0
- package/src/sync-protocols/orchestration/merge.ts +215 -0
- package/src/sync-protocols/orchestration/sync.ts +74 -0
- package/src/sync-protocols/types.ts +32 -0
- package/src/sync-protocols/utxorpc/BufferedRpc.ts +169 -0
- package/src/sync-protocols/utxorpc/fetcher.ts +116 -0
- package/src/sync-protocols/utxorpc/state.ts +186 -0
- package/src/sync-protocols/utxorpc/types.ts +53 -0
- package/src/sync-protocols/utxorpc/utils.ts +80 -0
- package/src/syncProtocolFactory.ts +149 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { blockNumberRelation } from "../common/utils.ts";
|
|
2
|
+
import type { Operation } from "effection";
|
|
3
|
+
import { call } from "effection";
|
|
4
|
+
import { getPage } from "@effectstream/db";
|
|
5
|
+
import type { PoolClient } from "pg";
|
|
6
|
+
import { bound, type NtpBlockNumber } from "@effectstream/utils";
|
|
7
|
+
import { type LastPage, SyncState } from "../base/state.ts";
|
|
8
|
+
import type { RootOutput, RootPage } from "../types.ts";
|
|
9
|
+
import type { NtpFetcher } from "./fetcher.ts";
|
|
10
|
+
import { genInputRange } from "../common/page-helpers.ts";
|
|
11
|
+
import type { Input, Output, Page } from "./types.ts";
|
|
12
|
+
import { toMsTimestamp } from "./types.ts";
|
|
13
|
+
import type { ConfigNetworkType, SyncProtocolWithNetwork } from "@effectstream/config";
|
|
14
|
+
|
|
15
|
+
export class NtpSyncState extends SyncState<
|
|
16
|
+
Input,
|
|
17
|
+
Output,
|
|
18
|
+
Page,
|
|
19
|
+
RootOutput,
|
|
20
|
+
RootPage,
|
|
21
|
+
NtpFetcher
|
|
22
|
+
> {
|
|
23
|
+
constructor(
|
|
24
|
+
lastPage: LastPage<Page, RootPage>,
|
|
25
|
+
readonly config: Extract<
|
|
26
|
+
SyncProtocolWithNetwork,
|
|
27
|
+
{ networkType: ConfigNetworkType.NTP }
|
|
28
|
+
>,
|
|
29
|
+
fetcher: NtpFetcher,
|
|
30
|
+
dbConn: PoolClient,
|
|
31
|
+
) {
|
|
32
|
+
super(
|
|
33
|
+
config.syncProtocol.name,
|
|
34
|
+
lastPage,
|
|
35
|
+
fetcher,
|
|
36
|
+
blockNumberRelation,
|
|
37
|
+
dbConn,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@bound
|
|
42
|
+
override toRootOutput(data: Output): RootOutput {
|
|
43
|
+
return {
|
|
44
|
+
blockInfo: data.blockHashes.map((h) => ({
|
|
45
|
+
protocol_name: this.config.syncProtocol.name,
|
|
46
|
+
block_number: Number(data.raw.blockNumber),
|
|
47
|
+
blockHash: h,
|
|
48
|
+
})),
|
|
49
|
+
blockNumber: Number(data.raw.blockNumber),
|
|
50
|
+
timestamp: this.toRootPage(data),
|
|
51
|
+
primitives: [],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@bound
|
|
56
|
+
override toRootPage(data: Output): RootPage {
|
|
57
|
+
return toMsTimestamp(data.raw.timestamp);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@bound
|
|
61
|
+
override *stateToInput(): Operation<Input | undefined> {
|
|
62
|
+
return yield* genInputRange(
|
|
63
|
+
this as NtpSyncState,
|
|
64
|
+
1, // NTP Pages start from 1, and the fetcher offsets using the start page (date)
|
|
65
|
+
{
|
|
66
|
+
name: this.config.syncProtocol.name,
|
|
67
|
+
startPage: this.config.syncProtocol.startBlockHeight as NtpBlockNumber,
|
|
68
|
+
},
|
|
69
|
+
this.getNamespace(),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@bound
|
|
74
|
+
override toPage(
|
|
75
|
+
input: Input,
|
|
76
|
+
data: Output[],
|
|
77
|
+
): Page {
|
|
78
|
+
return Number(input.to);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@bound
|
|
82
|
+
override mergeDatum(
|
|
83
|
+
ourOutput: Output,
|
|
84
|
+
rootOutput: RootOutput,
|
|
85
|
+
): void {
|
|
86
|
+
throw new Error("Only parallel chains merge into root");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@bound
|
|
90
|
+
override getNamespace(): string[] {
|
|
91
|
+
return [this.config.network.name, this.config.syncProtocol.name];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
static *restoreState(
|
|
95
|
+
dbConn: PoolClient,
|
|
96
|
+
config: Extract<
|
|
97
|
+
SyncProtocolWithNetwork,
|
|
98
|
+
{ networkType: ConfigNetworkType.NTP }
|
|
99
|
+
>,
|
|
100
|
+
fetcher: NtpFetcher,
|
|
101
|
+
): Operation<NtpSyncState> {
|
|
102
|
+
// TODO: move this DB query into page-helpers?
|
|
103
|
+
const [result] = yield* call(async () =>
|
|
104
|
+
await getPage.run({
|
|
105
|
+
protocol_name: config.syncProtocol.name,
|
|
106
|
+
}, dbConn)
|
|
107
|
+
);
|
|
108
|
+
const page: any = result
|
|
109
|
+
? result.page as unknown as LastPage<Page, RootPage>
|
|
110
|
+
: undefined;
|
|
111
|
+
return new NtpSyncState(
|
|
112
|
+
page,
|
|
113
|
+
config,
|
|
114
|
+
fetcher,
|
|
115
|
+
dbConn,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BlockNumber,
|
|
3
|
+
NtpBlockHash,
|
|
4
|
+
NtpBlockNumber,
|
|
5
|
+
NtpPageJson,
|
|
6
|
+
TimestampMs,
|
|
7
|
+
} from "@effectstream/utils";
|
|
8
|
+
import { TypeboxHelpers } from "@effectstream/utils";
|
|
9
|
+
import type { PageSyncRange } from "../common/page-helpers.ts";
|
|
10
|
+
import { Type } from "@sinclair/typebox";
|
|
11
|
+
import type {
|
|
12
|
+
ConfigSyncProtocolType,
|
|
13
|
+
FlattenSyncProtocolIOFor,
|
|
14
|
+
} from "@effectstream/config";
|
|
15
|
+
|
|
16
|
+
export type Page = BlockNumber;
|
|
17
|
+
const PageJsonSchema = Type.Unsafe<
|
|
18
|
+
NtpPageJson
|
|
19
|
+
>(Type.String());
|
|
20
|
+
export const PageSchema = TypeboxHelpers.SerializeObjAsJson<
|
|
21
|
+
Page,
|
|
22
|
+
typeof PageJsonSchema
|
|
23
|
+
>();
|
|
24
|
+
|
|
25
|
+
export type PrimitiveType = FlattenSyncProtocolIOFor<
|
|
26
|
+
ConfigSyncProtocolType.NTP_MAIN
|
|
27
|
+
>;
|
|
28
|
+
export type Input = PageSyncRange<Page>;
|
|
29
|
+
export type Output = {
|
|
30
|
+
raw: {
|
|
31
|
+
timestamp: bigint;
|
|
32
|
+
hash: NtpBlockHash;
|
|
33
|
+
blockNumber: number;
|
|
34
|
+
};
|
|
35
|
+
blockHashes: NtpBlockHash[];
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export function toMsTimestamp(timestamp: bigint): TimestampMs {
|
|
39
|
+
return Number(timestamp);
|
|
40
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import type { Channel, Operation } from "effection";
|
|
2
|
+
import { ComponentNames, log, SeverityNumber } from "@effectstream/log";
|
|
3
|
+
import type {
|
|
4
|
+
AllSyncProtocols,
|
|
5
|
+
ISyncProtocol,
|
|
6
|
+
RootOutput,
|
|
7
|
+
RootPage,
|
|
8
|
+
} from "../types.ts";
|
|
9
|
+
import type { PageRelation } from "../base/page.ts";
|
|
10
|
+
import type Deque from "denque";
|
|
11
|
+
import type { ChainBlock } from "../common/root.ts";
|
|
12
|
+
import { chainPageRelation } from "../common/root.ts";
|
|
13
|
+
import type { CacheCleanup, OutputAndCleanup } from "../base/state.ts";
|
|
14
|
+
|
|
15
|
+
export function* startMerge(
|
|
16
|
+
syncProtocols: AllSyncProtocols[],
|
|
17
|
+
finalizedBlockStream: Channel<ChainBlock, void>,
|
|
18
|
+
): Operation<ChainBlock> {
|
|
19
|
+
while (true) {
|
|
20
|
+
let newBlock: ChainBlock | undefined = undefined;
|
|
21
|
+
|
|
22
|
+
const cleanup: CacheCleanup[] = [];
|
|
23
|
+
|
|
24
|
+
for (let i = 0; i < syncProtocols.length; i++) {
|
|
25
|
+
const mergeInfo: MergeResult<RootOutput> = yield* mergeIntoRoot(
|
|
26
|
+
syncProtocols[i],
|
|
27
|
+
{
|
|
28
|
+
value: newBlock,
|
|
29
|
+
toPage: (block) => block.timestamp,
|
|
30
|
+
comparePage: chainPageRelation,
|
|
31
|
+
},
|
|
32
|
+
);
|
|
33
|
+
cleanup.push(mergeInfo.updateCache);
|
|
34
|
+
if (mergeInfo.newRoot != null) {
|
|
35
|
+
newBlock = mergeInfo.newRoot;
|
|
36
|
+
}
|
|
37
|
+
if (i === 0) {
|
|
38
|
+
log.remote(
|
|
39
|
+
ComponentNames.EFFECTSTREAM_SYNC,
|
|
40
|
+
"block-merge",
|
|
41
|
+
SeverityNumber.DEBUG,
|
|
42
|
+
(log) => log(`new block ${mergeInfo.newRoot?.blockNumber}`),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (newBlock == null) continue;
|
|
47
|
+
log.remote(
|
|
48
|
+
ComponentNames.EFFECTSTREAM_SYNC,
|
|
49
|
+
"block-merge",
|
|
50
|
+
SeverityNumber.INFO,
|
|
51
|
+
(log) => log(`producing block ${newBlock?.blockNumber}`),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
yield* finalizedBlockStream.send(newBlock);
|
|
55
|
+
|
|
56
|
+
for (const fn of cleanup) {
|
|
57
|
+
fn();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type MergeResult<RootOutput> = {
|
|
63
|
+
updateCache: () => void;
|
|
64
|
+
/**
|
|
65
|
+
* undefined → replace whatever root might exist previously with this one.
|
|
66
|
+
* this is primarily useful for setting the initial root
|
|
67
|
+
*/
|
|
68
|
+
newRoot: undefined | RootOutput;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Merge as much of the data into the root as possible\
|
|
73
|
+
* Note: it does *not* modify the child data.
|
|
74
|
+
* Instead, it returns a callback to remove elements that were merged.
|
|
75
|
+
* This is to help with error handling
|
|
76
|
+
* since you can avoid removing elements until you're sure no errors have happened
|
|
77
|
+
* given there is no way to recover these elements if an error happens after they get removed.
|
|
78
|
+
*
|
|
79
|
+
* @param value the data being constructed. `undefined` if this is the first piece of data in the chain
|
|
80
|
+
*/
|
|
81
|
+
export function* mergeIntoRoot<SyncProtocol extends AllSyncProtocols>(
|
|
82
|
+
state: SyncProtocol,
|
|
83
|
+
rootInfo: {
|
|
84
|
+
value: undefined | RootOutput;
|
|
85
|
+
toPage: (data: RootOutput) => RootPage;
|
|
86
|
+
comparePage: PageRelation<RootPage>;
|
|
87
|
+
},
|
|
88
|
+
): Operation<MergeResult<RootOutput>> {
|
|
89
|
+
const iState = state as ISyncProtocol;
|
|
90
|
+
|
|
91
|
+
// 1) If we've never made a single query yet, wait
|
|
92
|
+
while (state.lastPage == null) {
|
|
93
|
+
log.remote(
|
|
94
|
+
ComponentNames.EFFECTSTREAM_SYNC,
|
|
95
|
+
state.getNamespace(),
|
|
96
|
+
SeverityNumber.DEBUG,
|
|
97
|
+
(log) => log("Merge blocked (first sync): waiting for page"),
|
|
98
|
+
);
|
|
99
|
+
yield* state.newPageCondVar.wait();
|
|
100
|
+
log.remote(
|
|
101
|
+
ComponentNames.EFFECTSTREAM_SYNC,
|
|
102
|
+
state.getNamespace(),
|
|
103
|
+
SeverityNumber.DEBUG,
|
|
104
|
+
(log) => log("Merge unblocked (first sync): got page"),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 2) Handle case where this is the first in the list of states to merge data from
|
|
109
|
+
if (rootInfo.value == null) {
|
|
110
|
+
while (state.bufferedData.size() === 0) {
|
|
111
|
+
log.remote(
|
|
112
|
+
ComponentNames.EFFECTSTREAM_SYNC,
|
|
113
|
+
state.getNamespace(),
|
|
114
|
+
SeverityNumber.DEBUG,
|
|
115
|
+
(log) => log("Merge blocked (missing main data): waiting for data"),
|
|
116
|
+
);
|
|
117
|
+
yield* state.newDataCondVar.wait();
|
|
118
|
+
log.remote(
|
|
119
|
+
ComponentNames.EFFECTSTREAM_SYNC,
|
|
120
|
+
state.getNamespace(),
|
|
121
|
+
SeverityNumber.DEBUG,
|
|
122
|
+
(log) => log("Merge unblocked (missing main data): got data"),
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
const output = iState.bufferedData.peekAt(0)!;
|
|
126
|
+
const newRoot = iState.toRootOutput(output.output);
|
|
127
|
+
return {
|
|
128
|
+
updateCache: () => {
|
|
129
|
+
output.cleanup();
|
|
130
|
+
state.bufferedData.remove(0, 1);
|
|
131
|
+
},
|
|
132
|
+
newRoot,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 3) If a previous state created data, keep merging data from this state into it
|
|
137
|
+
// until we've exceeded the time range
|
|
138
|
+
while (
|
|
139
|
+
rootInfo.comparePage.compare(
|
|
140
|
+
state.lastPage.root,
|
|
141
|
+
rootInfo.toPage(rootInfo.value),
|
|
142
|
+
) <= 0
|
|
143
|
+
) {
|
|
144
|
+
log.remote(
|
|
145
|
+
ComponentNames.EFFECTSTREAM_SYNC,
|
|
146
|
+
state.getNamespace(),
|
|
147
|
+
SeverityNumber.DEBUG,
|
|
148
|
+
(log) => log("Merge blocked (missing parallel data): waiting for data"),
|
|
149
|
+
);
|
|
150
|
+
yield* state.newPageCondVar.wait();
|
|
151
|
+
log.remote(
|
|
152
|
+
ComponentNames.EFFECTSTREAM_SYNC,
|
|
153
|
+
state.getNamespace(),
|
|
154
|
+
SeverityNumber.DEBUG,
|
|
155
|
+
(log) => log("Merge unblocked (missing parallel data): got data"),
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
const cleanups = getFromBuffer(
|
|
159
|
+
{
|
|
160
|
+
data: iState.bufferedData,
|
|
161
|
+
toPage: iState.toRootPage,
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
data: rootInfo.value,
|
|
165
|
+
toPage: rootInfo.toPage,
|
|
166
|
+
},
|
|
167
|
+
rootInfo.comparePage,
|
|
168
|
+
iState.mergeDatum,
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
updateCache: () => {
|
|
173
|
+
state.bufferedData.remove(0, cleanups.length);
|
|
174
|
+
for (const fn of cleanups) {
|
|
175
|
+
fn();
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
newRoot: undefined,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Merge as much of the data into the root as possible
|
|
184
|
+
* Note: it does *not* modify the child data
|
|
185
|
+
*
|
|
186
|
+
* @returns the amount of elements that can be added to the root data
|
|
187
|
+
*/
|
|
188
|
+
function getFromBuffer<Output, RootOutput, RootPage>(
|
|
189
|
+
child: {
|
|
190
|
+
data: Deque<OutputAndCleanup<Output>>;
|
|
191
|
+
toPage: (data: Output) => RootPage;
|
|
192
|
+
},
|
|
193
|
+
root: {
|
|
194
|
+
data: RootOutput;
|
|
195
|
+
toPage: (data: RootOutput) => RootPage;
|
|
196
|
+
},
|
|
197
|
+
pageRelation: PageRelation<RootPage>,
|
|
198
|
+
mergeData: (child: Output, root: RootOutput) => void,
|
|
199
|
+
): CacheCleanup[] {
|
|
200
|
+
const cleanups: CacheCleanup[] = [];
|
|
201
|
+
let nextElem = child.data.peekAt(cleanups.length);
|
|
202
|
+
|
|
203
|
+
while (
|
|
204
|
+
nextElem != null &&
|
|
205
|
+
pageRelation.compare(
|
|
206
|
+
child.toPage(nextElem.output),
|
|
207
|
+
root.toPage(root.data),
|
|
208
|
+
) <= 0
|
|
209
|
+
) {
|
|
210
|
+
mergeData(nextElem.output, root.data);
|
|
211
|
+
cleanups.push(nextElem.cleanup);
|
|
212
|
+
nextElem = child.data.peekAt(cleanups.length);
|
|
213
|
+
}
|
|
214
|
+
return cleanups;
|
|
215
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { Operation } from "effection";
|
|
2
|
+
import { sleep, spawn } from "effection";
|
|
3
|
+
import { ComponentNames, log, SeverityNumber } from "@effectstream/log";
|
|
4
|
+
import type { AllSyncProtocols, ISyncProtocol } from "../types.ts";
|
|
5
|
+
import { tryYield } from "@effectstream/utils";
|
|
6
|
+
|
|
7
|
+
export function* startSync(
|
|
8
|
+
state: AllSyncProtocols,
|
|
9
|
+
): Operation<void> {
|
|
10
|
+
const iState: ISyncProtocol = state as ISyncProtocol;
|
|
11
|
+
|
|
12
|
+
// spawn async task
|
|
13
|
+
yield* spawn(function* () {
|
|
14
|
+
yield* iState.startAsync();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// spawn polling task
|
|
18
|
+
yield* spawn(function* () {
|
|
19
|
+
while (true) {
|
|
20
|
+
const inputResult = yield* tryYield(iState.stateToInput());
|
|
21
|
+
if (inputResult.error != null) {
|
|
22
|
+
state.consecutiveErrors++;
|
|
23
|
+
state.lastErrorTimestamp = Date.now();
|
|
24
|
+
log.remote(
|
|
25
|
+
ComponentNames.EFFECTSTREAM_SYNC,
|
|
26
|
+
[...state.getNamespace(), "stateToInput"],
|
|
27
|
+
SeverityNumber.ERROR,
|
|
28
|
+
(log) => log(inputResult.error),
|
|
29
|
+
);
|
|
30
|
+
const { config } = state.fetcher;
|
|
31
|
+
if ("pollingInterval" in config.syncProtocol) {
|
|
32
|
+
yield* sleep(config.syncProtocol.pollingInterval);
|
|
33
|
+
}
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const input = inputResult.data;
|
|
37
|
+
const { config } = state.fetcher;
|
|
38
|
+
if (input == null) {
|
|
39
|
+
if ("pollingInterval" in config.syncProtocol) {
|
|
40
|
+
yield* sleep(config.syncProtocol.pollingInterval);
|
|
41
|
+
}
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const result = yield* tryYield(iState.fetcher.readData(input, iState));
|
|
46
|
+
if (result.error != null) {
|
|
47
|
+
state.consecutiveErrors++;
|
|
48
|
+
state.lastErrorTimestamp = Date.now();
|
|
49
|
+
log.remote(
|
|
50
|
+
ComponentNames.EFFECTSTREAM_SYNC,
|
|
51
|
+
[...state.getNamespace(), "readData"],
|
|
52
|
+
SeverityNumber.ERROR,
|
|
53
|
+
(l) => l(result.error),
|
|
54
|
+
);
|
|
55
|
+
if ("pollingInterval" in config.syncProtocol) {
|
|
56
|
+
yield* sleep(config.syncProtocol.pollingInterval);
|
|
57
|
+
}
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
state.consecutiveErrors = 0;
|
|
61
|
+
state.lastSuccessfulFetchMs = Date.now();
|
|
62
|
+
log.remote(
|
|
63
|
+
ComponentNames.EFFECTSTREAM_SYNC,
|
|
64
|
+
[...state.getNamespace(), "data"],
|
|
65
|
+
SeverityNumber.TRACE,
|
|
66
|
+
(log) => log(result.data),
|
|
67
|
+
);
|
|
68
|
+
yield* iState.updateState(input, result.data);
|
|
69
|
+
for (const datum of result.data.output) {
|
|
70
|
+
yield* iState.fetcher.producerChannel.send(datum.output);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ChainBlock, ChainPage } from "./common/root.ts";
|
|
2
|
+
import type { EvmSyncState } from "./evm/state.ts";
|
|
3
|
+
import type { SyncState } from "./base/state.ts";
|
|
4
|
+
import type { UtxoRpcSyncState } from "./utxorpc/state.ts";
|
|
5
|
+
import type { PaginatedFetcher } from "./base/fetcher.ts";
|
|
6
|
+
import type { UnionToIntersection } from "@effectstream/utils";
|
|
7
|
+
import type { MidnightSyncState } from "./midnight/state.ts";
|
|
8
|
+
import type { NtpSyncState } from "./ntp/state.ts";
|
|
9
|
+
import type { AvailSyncState } from "./avail/state.ts";
|
|
10
|
+
import type { BitcoinSyncState } from "./bitcoin/state.ts";
|
|
11
|
+
import type { CelestiaSyncState } from "./celestia/state.ts";
|
|
12
|
+
import type { NearSyncState } from "./near/state.ts";
|
|
13
|
+
|
|
14
|
+
// TODO: move folders
|
|
15
|
+
export type RootOutput = ChainBlock;
|
|
16
|
+
export type RootPage = ChainPage;
|
|
17
|
+
|
|
18
|
+
// TODO: map config types to sync protocols
|
|
19
|
+
export type AllSyncProtocols =
|
|
20
|
+
| NtpSyncState
|
|
21
|
+
| EvmSyncState
|
|
22
|
+
| UtxoRpcSyncState
|
|
23
|
+
| MidnightSyncState
|
|
24
|
+
| AvailSyncState
|
|
25
|
+
| BitcoinSyncState
|
|
26
|
+
| CelestiaSyncState
|
|
27
|
+
| NearSyncState;
|
|
28
|
+
export type ISyncProtocol = UnionToIntersection<AllSyncProtocols>;
|
|
29
|
+
|
|
30
|
+
type toPaginated<T> = T extends { fetcher: PaginatedFetcher<infer Page> } ? T
|
|
31
|
+
: never;
|
|
32
|
+
export type PaginatedSyncProtocols = toPaginated<AllSyncProtocols>;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { CardanoSyncClient } from "@utxorpc/sdk";
|
|
2
|
+
import type { cardano } from "@utxorpc/spec";
|
|
3
|
+
import type { OutputAndCleanup } from "../base/state.ts";
|
|
4
|
+
import Deque from "denque";
|
|
5
|
+
import type { BlockNumber } from "@effectstream/utils";
|
|
6
|
+
import type { Operation } from "effection";
|
|
7
|
+
import { conditionVariable } from "@effectstream/utils";
|
|
8
|
+
import type { ChainPoint, Page } from "./types.ts";
|
|
9
|
+
import { Buffer } from "node:buffer";
|
|
10
|
+
|
|
11
|
+
export class BufferedRpc {
|
|
12
|
+
private readonly buffer: Deque<OutputAndCleanup<cardano.Block>> =
|
|
13
|
+
new Deque();
|
|
14
|
+
private readonly newDataCondVar = conditionVariable<void>();
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
public readonly syncClient: CardanoSyncClient,
|
|
18
|
+
/**
|
|
19
|
+
* Finality should ensure that we never expose data that could be affected by `undo`
|
|
20
|
+
* recall: finality is in terms of blocks created (not in terms of slots)
|
|
21
|
+
*/
|
|
22
|
+
private readonly finality: BlockNumber,
|
|
23
|
+
) {}
|
|
24
|
+
|
|
25
|
+
static lastHeight: bigint | undefined = undefined;
|
|
26
|
+
static initialized = false;
|
|
27
|
+
|
|
28
|
+
public lastHeight(): bigint | undefined {
|
|
29
|
+
return BufferedRpc.lastHeight;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public async resolveOrigin(): Promise<ChainPoint> {
|
|
33
|
+
const blocks = await this.syncClient.fetchHistory(undefined, 1);
|
|
34
|
+
if (!blocks.length) throw new Error("No blocks found on chain");
|
|
35
|
+
const header = blocks[0].parsedBlock.header!;
|
|
36
|
+
return {
|
|
37
|
+
slot: Number(header.slot),
|
|
38
|
+
hash: Buffer.from(header.hash).toString("hex"),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public async start(point?: ChainPoint): Promise<void> {
|
|
43
|
+
if (BufferedRpc.initialized) {
|
|
44
|
+
// We do not want to follow the tip again.
|
|
45
|
+
throw new Error("BufferedRpc already initialized");
|
|
46
|
+
}
|
|
47
|
+
BufferedRpc.initialized = true;
|
|
48
|
+
const intersect = point ? [point] : [];
|
|
49
|
+
const blockEvents = this.syncClient.followTip(intersect);
|
|
50
|
+
|
|
51
|
+
let seenReset = false;
|
|
52
|
+
|
|
53
|
+
for await (const blockEvent of blockEvents) {
|
|
54
|
+
if (blockEvent.action === "apply") {
|
|
55
|
+
if (blockEvent.block.header?.height) {
|
|
56
|
+
BufferedRpc.lastHeight = blockEvent.block.header?.height;
|
|
57
|
+
}
|
|
58
|
+
// This works ONLY because the buffer is processed FIFO
|
|
59
|
+
this.buffer.push({
|
|
60
|
+
output: blockEvent.block,
|
|
61
|
+
cleanup: () => this.buffer.shift(),
|
|
62
|
+
});
|
|
63
|
+
this.newDataCondVar.wake();
|
|
64
|
+
} else if (blockEvent.action === "undo") {
|
|
65
|
+
if (blockEvent.block.header?.height) {
|
|
66
|
+
BufferedRpc.lastHeight = blockEvent.block.header?.height;
|
|
67
|
+
}
|
|
68
|
+
this.buffer.pop();
|
|
69
|
+
} else if (blockEvent.action === "reset") {
|
|
70
|
+
if (!seenReset) {
|
|
71
|
+
seenReset = true;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const lastBlock = this.buffer.peekBack();
|
|
76
|
+
const lastPoint = lastBlock ? JSON.stringify({
|
|
77
|
+
slot: Number(lastBlock.output.header!.slot),
|
|
78
|
+
hash: Buffer.from(lastBlock.output.header!.hash).toString("hex"),
|
|
79
|
+
}) : "<none>";
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Paima node stuck on fork. Currently at point ${lastPoint}`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public *storedBlocks(
|
|
88
|
+
height: undefined | BlockNumber,
|
|
89
|
+
): Operation<{ from: Page; to: Page }> {
|
|
90
|
+
let lastElement = this.buffer.peekBack();
|
|
91
|
+
|
|
92
|
+
if (height == null) {
|
|
93
|
+
height = 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// if we have nothing in our buffer yet, or if we're asking for a future height
|
|
97
|
+
while (
|
|
98
|
+
this.buffer.length == 0 ||
|
|
99
|
+
lastElement == null ||
|
|
100
|
+
height >= Number(lastElement.output.header!.height) - this.finality
|
|
101
|
+
) {
|
|
102
|
+
yield* this.newDataCondVar.wait();
|
|
103
|
+
lastElement = this.buffer.peekBack();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const endIndex = (() => {
|
|
107
|
+
let endIndex = this.buffer.length - 1;
|
|
108
|
+
const highestValid =
|
|
109
|
+
Number(this.buffer.peekBack()!.output.header!.height) -
|
|
110
|
+
this.finality;
|
|
111
|
+
while (
|
|
112
|
+
endIndex > 0 &&
|
|
113
|
+
Number(this.buffer.peekAt(endIndex)!.output.header!.height) >
|
|
114
|
+
highestValid
|
|
115
|
+
) {
|
|
116
|
+
endIndex--;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return endIndex;
|
|
120
|
+
})();
|
|
121
|
+
const startIndex = (() => {
|
|
122
|
+
let startIndex = endIndex;
|
|
123
|
+
while (
|
|
124
|
+
startIndex > 0 &&
|
|
125
|
+
Number(
|
|
126
|
+
this.buffer.peekAt(startIndex - 1)!.output.header!.height,
|
|
127
|
+
) >
|
|
128
|
+
height
|
|
129
|
+
) {
|
|
130
|
+
startIndex--;
|
|
131
|
+
}
|
|
132
|
+
return startIndex;
|
|
133
|
+
})();
|
|
134
|
+
return {
|
|
135
|
+
from: toPage(this.buffer.peekAt(startIndex)!.output),
|
|
136
|
+
to: toPage(this.buffer.peekAt(endIndex)!.output),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Return an array of blocks
|
|
142
|
+
* This is preferred over `fetchBlock` where there may be gaps in blockHeight
|
|
143
|
+
* Or when you don't want a blocking call
|
|
144
|
+
*/
|
|
145
|
+
public fetchBlocks(
|
|
146
|
+
from: BlockNumber, // (inclusive)
|
|
147
|
+
to: BlockNumber, // (inclusive)
|
|
148
|
+
): OutputAndCleanup<cardano.Block>[] {
|
|
149
|
+
const blocks: OutputAndCleanup<cardano.Block>[] = [];
|
|
150
|
+
for (let i = 0; i < this.buffer.length; i++) {
|
|
151
|
+
const block = this.buffer.peekAt(i)!;
|
|
152
|
+
if (Number(block.output.header!.height) >= from) {
|
|
153
|
+
blocks.push(block);
|
|
154
|
+
}
|
|
155
|
+
if (Number(block.output.header!.height) >= to) {
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return blocks;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function toPage(block: cardano.Block): Page {
|
|
164
|
+
return {
|
|
165
|
+
slot: Number(block.header!.slot),
|
|
166
|
+
hash: Buffer.from(block.header!.hash).toString("hex"),
|
|
167
|
+
height: Number(block.header!.height),
|
|
168
|
+
};
|
|
169
|
+
}
|