@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,144 @@
|
|
|
1
|
+
import { applyDelay, 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 } from "@effectstream/utils";
|
|
7
|
+
import {
|
|
8
|
+
type LastPage,
|
|
9
|
+
SyncState,
|
|
10
|
+
} from "../base/state.ts";
|
|
11
|
+
import type { RootOutput, RootPage } from "../types.ts";
|
|
12
|
+
import type { BitcoinFetcher } from "./fetcher.ts";
|
|
13
|
+
import {
|
|
14
|
+
toMsTimestamp,
|
|
15
|
+
type Input,
|
|
16
|
+
type Output,
|
|
17
|
+
type Page,
|
|
18
|
+
} from "./types.ts";
|
|
19
|
+
import { genInputRange } from "../common/page-helpers.ts";
|
|
20
|
+
import type {
|
|
21
|
+
ConfigNetworkType,
|
|
22
|
+
SyncProtocolWithNetwork,
|
|
23
|
+
} from "@effectstream/config";
|
|
24
|
+
|
|
25
|
+
export class BitcoinSyncState extends SyncState<
|
|
26
|
+
Input,
|
|
27
|
+
Output,
|
|
28
|
+
Page,
|
|
29
|
+
RootOutput,
|
|
30
|
+
RootPage,
|
|
31
|
+
BitcoinFetcher
|
|
32
|
+
> {
|
|
33
|
+
constructor(
|
|
34
|
+
lastPage: LastPage<Page, RootPage> | undefined,
|
|
35
|
+
readonly config: Extract<
|
|
36
|
+
SyncProtocolWithNetwork,
|
|
37
|
+
{ networkType: ConfigNetworkType.BITCOIN }
|
|
38
|
+
>,
|
|
39
|
+
fetcher: BitcoinFetcher,
|
|
40
|
+
dbConn: PoolClient,
|
|
41
|
+
) {
|
|
42
|
+
super(
|
|
43
|
+
config.syncProtocol.name,
|
|
44
|
+
lastPage,
|
|
45
|
+
fetcher,
|
|
46
|
+
blockNumberRelation,
|
|
47
|
+
dbConn,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@bound
|
|
52
|
+
override toRootOutput(data: Output): RootOutput {
|
|
53
|
+
return {
|
|
54
|
+
blockNumber: Number(data.raw.height),
|
|
55
|
+
timestamp: this.toRootPage(data),
|
|
56
|
+
blockInfo: data.blockHashes.map((hash) => ({
|
|
57
|
+
protocol_name: this.config.syncProtocol.name,
|
|
58
|
+
block_number: Number(data.raw.height),
|
|
59
|
+
blockHash: hash,
|
|
60
|
+
})),
|
|
61
|
+
primitives: data.primitives.map((primitive) => ({
|
|
62
|
+
...primitive,
|
|
63
|
+
source: this.config.syncProtocol.name,
|
|
64
|
+
})),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@bound
|
|
69
|
+
override toRootPage(data: Output): RootPage {
|
|
70
|
+
return applyDelay(
|
|
71
|
+
toMsTimestamp(data.raw.time),
|
|
72
|
+
this.config.syncProtocol.delayMs ?? 0,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@bound
|
|
77
|
+
override *stateToInput(): Operation<Input | undefined> {
|
|
78
|
+
return yield* genInputRange(
|
|
79
|
+
this as BitcoinSyncState,
|
|
80
|
+
this.config.syncProtocol.startBlockHeight as Page,
|
|
81
|
+
{
|
|
82
|
+
name: this.config.syncProtocol.name,
|
|
83
|
+
startPage: this.config.syncProtocol.startBlockHeight as Page,
|
|
84
|
+
},
|
|
85
|
+
this.getNamespace(),
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@bound
|
|
90
|
+
override toPage(
|
|
91
|
+
input: Input,
|
|
92
|
+
_data: Output[],
|
|
93
|
+
): Page {
|
|
94
|
+
return input.to;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@bound
|
|
98
|
+
override mergeDatum(
|
|
99
|
+
ourOutput: Output,
|
|
100
|
+
rootOutput: RootOutput,
|
|
101
|
+
): void {
|
|
102
|
+
const primitives = ourOutput.primitives.map((primitive) => ({
|
|
103
|
+
...primitive,
|
|
104
|
+
source: this.config.syncProtocol.name,
|
|
105
|
+
}));
|
|
106
|
+
const blockInfo = ourOutput.blockHashes.map((hash) => ({
|
|
107
|
+
protocol_name: this.config.syncProtocol.name,
|
|
108
|
+
block_number: Number(ourOutput.raw.height),
|
|
109
|
+
blockHash: hash,
|
|
110
|
+
}));
|
|
111
|
+
rootOutput.primitives.push(...primitives);
|
|
112
|
+
rootOutput.blockInfo.push(...blockInfo);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@bound
|
|
116
|
+
override getNamespace(): string[] {
|
|
117
|
+
return [this.config.network.name, this.config.syncProtocol.name];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
static *restoreState(
|
|
121
|
+
dbConn: PoolClient,
|
|
122
|
+
config: Extract<
|
|
123
|
+
SyncProtocolWithNetwork,
|
|
124
|
+
{ networkType: ConfigNetworkType.BITCOIN }
|
|
125
|
+
>,
|
|
126
|
+
fetcher: BitcoinFetcher,
|
|
127
|
+
): Operation<BitcoinSyncState> {
|
|
128
|
+
const [result] = yield* call(async () =>
|
|
129
|
+
await getPage.run({
|
|
130
|
+
protocol_name: config.syncProtocol.name,
|
|
131
|
+
}, dbConn)
|
|
132
|
+
);
|
|
133
|
+
const page = result
|
|
134
|
+
? result.page as unknown as LastPage<Page, RootPage>
|
|
135
|
+
: undefined;
|
|
136
|
+
return new BitcoinSyncState(
|
|
137
|
+
page,
|
|
138
|
+
config,
|
|
139
|
+
fetcher,
|
|
140
|
+
dbConn,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BitcoinBlockHash,
|
|
3
|
+
BitcoinTxId,
|
|
4
|
+
BlockNumber,
|
|
5
|
+
TimestampMs,
|
|
6
|
+
} from "@effectstream/utils";
|
|
7
|
+
import type { PageSyncRange } from "../common/page-helpers.ts";
|
|
8
|
+
import type {
|
|
9
|
+
ConfigNetworkType,
|
|
10
|
+
ConfigSyncProtocolType,
|
|
11
|
+
FlattenSyncProtocolIOFor,
|
|
12
|
+
PrimitiveEntry,
|
|
13
|
+
SyncProtocolWithNetwork,
|
|
14
|
+
} from "@effectstream/config";
|
|
15
|
+
|
|
16
|
+
export type Page = BlockNumber;
|
|
17
|
+
|
|
18
|
+
export type ConfigType = Extract<
|
|
19
|
+
SyncProtocolWithNetwork,
|
|
20
|
+
{ networkType: ConfigNetworkType.BITCOIN }
|
|
21
|
+
>;
|
|
22
|
+
|
|
23
|
+
export type PrimitiveEntryType = Extract<
|
|
24
|
+
PrimitiveEntry,
|
|
25
|
+
{ syncProtocol: ConfigSyncProtocolType.BITCOIN_RPC_PARALLEL }
|
|
26
|
+
>;
|
|
27
|
+
|
|
28
|
+
export type PrimitiveType = FlattenSyncProtocolIOFor<
|
|
29
|
+
ConfigSyncProtocolType.BITCOIN_RPC_PARALLEL
|
|
30
|
+
>;
|
|
31
|
+
|
|
32
|
+
export type Input = PageSyncRange<Page>;
|
|
33
|
+
|
|
34
|
+
export type BitcoinScriptPubKey = {
|
|
35
|
+
asm: string;
|
|
36
|
+
hex: string;
|
|
37
|
+
type?: string;
|
|
38
|
+
address?: string;
|
|
39
|
+
addresses?: string[];
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type BitcoinVout = {
|
|
43
|
+
value: number;
|
|
44
|
+
n: number;
|
|
45
|
+
scriptPubKey: BitcoinScriptPubKey;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type BitcoinVin = {
|
|
49
|
+
txid?: BitcoinTxId;
|
|
50
|
+
vout?: number;
|
|
51
|
+
coinbase?: string;
|
|
52
|
+
sequence: number;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type BitcoinTransaction = {
|
|
56
|
+
txid: BitcoinTxId;
|
|
57
|
+
hash: BitcoinTxId;
|
|
58
|
+
version: number;
|
|
59
|
+
size: number;
|
|
60
|
+
vsize: number;
|
|
61
|
+
weight: number;
|
|
62
|
+
locktime: number;
|
|
63
|
+
vin: BitcoinVin[];
|
|
64
|
+
vout: BitcoinVout[];
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export type BitcoinBlock = {
|
|
68
|
+
hash: BitcoinBlockHash;
|
|
69
|
+
confirmations: number;
|
|
70
|
+
size: number;
|
|
71
|
+
height: Page;
|
|
72
|
+
version: number;
|
|
73
|
+
time: number;
|
|
74
|
+
mediantime: number;
|
|
75
|
+
nonce: number;
|
|
76
|
+
bits: string;
|
|
77
|
+
difficulty: number;
|
|
78
|
+
previousblockhash?: BitcoinBlockHash;
|
|
79
|
+
nextblockhash?: BitcoinBlockHash;
|
|
80
|
+
tx: BitcoinTransaction[];
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export type Output = {
|
|
84
|
+
raw: BitcoinBlock;
|
|
85
|
+
primitives: PrimitiveType[];
|
|
86
|
+
blockHashes: BitcoinBlockHash[];
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export function toMsTimestamp(timestamp: number): TimestampMs {
|
|
90
|
+
return timestamp * 1000 as TimestampMs;
|
|
91
|
+
}
|
|
92
|
+
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
export type CelestiaBlob = {
|
|
2
|
+
/** Base64-encoded 29-byte namespace */
|
|
3
|
+
namespace: string;
|
|
4
|
+
/** Base64-encoded blob data */
|
|
5
|
+
data: string;
|
|
6
|
+
share_version: number;
|
|
7
|
+
/** Base64-encoded blob commitment */
|
|
8
|
+
commitment: string;
|
|
9
|
+
signer: string;
|
|
10
|
+
index: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type CelestiaHeader = {
|
|
14
|
+
version: { block: string; app: string };
|
|
15
|
+
chain_id: string;
|
|
16
|
+
/** String-encoded block height */
|
|
17
|
+
height: string;
|
|
18
|
+
/** ISO 8601 timestamp */
|
|
19
|
+
time: string;
|
|
20
|
+
last_block_id: {
|
|
21
|
+
hash: string;
|
|
22
|
+
parts: { total: number; hash: string };
|
|
23
|
+
};
|
|
24
|
+
last_commit_hash: string;
|
|
25
|
+
data_hash: string;
|
|
26
|
+
validators_hash: string;
|
|
27
|
+
next_validators_hash: string;
|
|
28
|
+
consensus_hash: string;
|
|
29
|
+
app_hash: string;
|
|
30
|
+
last_results_hash: string;
|
|
31
|
+
evidence_hash: string;
|
|
32
|
+
proposer_address: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type CelestiaCommit = {
|
|
36
|
+
height: string;
|
|
37
|
+
block_id: {
|
|
38
|
+
hash: string;
|
|
39
|
+
parts: { total: number; hash: string };
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type CelestiaExtendedHeader = {
|
|
44
|
+
header: CelestiaHeader;
|
|
45
|
+
commit: CelestiaCommit;
|
|
46
|
+
validator_set: unknown;
|
|
47
|
+
dah: unknown;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
import { ENV } from "@effectstream/utils/node-env";
|
|
51
|
+
|
|
52
|
+
const CELESTIA_NODE_URL = ENV.getString(
|
|
53
|
+
"CELESTIA_NODE_URL",
|
|
54
|
+
"http://localhost:26658",
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Converts a hex-encoded Celestia namespace to a base64-encoded 29-byte array.
|
|
59
|
+
* Format: 1 byte version (0x00) + 28 bytes namespace ID, right-aligned.
|
|
60
|
+
*/
|
|
61
|
+
export function celestiaNamespaceToBase64(hex: string): string {
|
|
62
|
+
const cleanHex = hex.replace(/^0x/, "").padStart(56, "0"); // 28 bytes = 56 hex chars
|
|
63
|
+
const bytes = new Uint8Array(29); // version byte (0) + 28-byte ID
|
|
64
|
+
for (let i = 0; i < 28; i++) {
|
|
65
|
+
bytes[i + 1] = parseInt(cleanHex.slice(i * 2, i * 2 + 2), 16);
|
|
66
|
+
}
|
|
67
|
+
let binary = "";
|
|
68
|
+
for (const byte of bytes) {
|
|
69
|
+
binary += String.fromCharCode(byte);
|
|
70
|
+
}
|
|
71
|
+
return btoa(binary);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export class CelestiaClient {
|
|
75
|
+
private readonly rpcUrl: string;
|
|
76
|
+
|
|
77
|
+
constructor(rpcUrl: string = CELESTIA_NODE_URL) {
|
|
78
|
+
this.rpcUrl = rpcUrl;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private async rpc<T>(method: string, params: unknown[]): Promise<T | null> {
|
|
82
|
+
let res: Response;
|
|
83
|
+
try {
|
|
84
|
+
res = await fetch(this.rpcUrl, {
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers: { "Content-Type": "application/json" },
|
|
87
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
|
|
88
|
+
});
|
|
89
|
+
} catch (err: unknown) {
|
|
90
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
91
|
+
console.error(`[Celestia] RPC fetch error [${method}]: ${msg}`);
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const json = await res.json();
|
|
96
|
+
if (json.error) {
|
|
97
|
+
const errMsg: string = json.error.message ?? String(json.error);
|
|
98
|
+
if (errMsg.includes("blob: not found")) return null;
|
|
99
|
+
console.error(`[Celestia] RPC error [${method}]:`, errMsg);
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
return json.result as T;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async getLatestBlockHeight(): Promise<number> {
|
|
106
|
+
const head = await this.rpc<CelestiaExtendedHeader>(
|
|
107
|
+
"header.LocalHead",
|
|
108
|
+
[],
|
|
109
|
+
);
|
|
110
|
+
if (!head) {
|
|
111
|
+
throw new Error("[Celestia] Could not retrieve latest block header");
|
|
112
|
+
}
|
|
113
|
+
return parseInt(head.header.height, 10);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async getHeaderAtHeight(height: number): Promise<CelestiaExtendedHeader> {
|
|
117
|
+
const header = await this.rpc<CelestiaExtendedHeader>(
|
|
118
|
+
"header.GetByHeight",
|
|
119
|
+
[height],
|
|
120
|
+
);
|
|
121
|
+
if (!header) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`[Celestia] Could not retrieve header at height ${height}`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return header;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async getBlobsAtHeight(
|
|
130
|
+
height: number,
|
|
131
|
+
namespaceB64: string,
|
|
132
|
+
): Promise<CelestiaBlob[]> {
|
|
133
|
+
const blobs = await this.rpc<CelestiaBlob[]>("blob.GetAll", [
|
|
134
|
+
height,
|
|
135
|
+
[namespaceB64],
|
|
136
|
+
]);
|
|
137
|
+
return blobs ?? [];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ConfigSyncProtocolType,
|
|
3
|
+
type PrimitiveEntry,
|
|
4
|
+
} from "@effectstream/config";
|
|
5
|
+
import { BaseDataFetcher } from "../base/fetcher.ts";
|
|
6
|
+
import type { DataFetched } from "../base/fetcher.ts";
|
|
7
|
+
import type {
|
|
8
|
+
LastPage,
|
|
9
|
+
OutputAndCleanup,
|
|
10
|
+
RootConversion,
|
|
11
|
+
} from "../base/state.ts";
|
|
12
|
+
import type { RootOutput, RootPage } from "../types.ts";
|
|
13
|
+
import type {
|
|
14
|
+
ConfigType,
|
|
15
|
+
Input,
|
|
16
|
+
Output,
|
|
17
|
+
Page,
|
|
18
|
+
PrimitiveType,
|
|
19
|
+
} from "./types.ts";
|
|
20
|
+
import {
|
|
21
|
+
CelestiaClient,
|
|
22
|
+
celestiaNamespaceToBase64,
|
|
23
|
+
} from "./CelestiaClient.ts";
|
|
24
|
+
import { all, call, type Operation } from "effection";
|
|
25
|
+
import { bound } from "@effectstream/utils";
|
|
26
|
+
|
|
27
|
+
export class CelestiaFetcher extends BaseDataFetcher<
|
|
28
|
+
Input,
|
|
29
|
+
Output,
|
|
30
|
+
RootOutput,
|
|
31
|
+
Page,
|
|
32
|
+
RootPage
|
|
33
|
+
> {
|
|
34
|
+
readonly client: CelestiaClient;
|
|
35
|
+
|
|
36
|
+
constructor(
|
|
37
|
+
readonly config: ConfigType,
|
|
38
|
+
) {
|
|
39
|
+
super(config.syncProtocol.name);
|
|
40
|
+
this.client = new CelestiaClient(config.network.rpcUrl);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@bound
|
|
44
|
+
override *readData(
|
|
45
|
+
data: Input,
|
|
46
|
+
rootConversion: RootConversion<Output, RootOutput, RootPage>,
|
|
47
|
+
lastPage: LastPage<Page, RootPage> | undefined,
|
|
48
|
+
): Operation<DataFetched<Output, Page, RootPage>> {
|
|
49
|
+
const outputs: OutputAndCleanup<Output>[] = [];
|
|
50
|
+
|
|
51
|
+
console.log(
|
|
52
|
+
`[Celestia] Fetching blocks from ${data.from} to ${data.to}.${
|
|
53
|
+
data.isPresync ? " [presync]" : ""
|
|
54
|
+
}`,
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
for (let height = data.from; height <= data.to; height++) {
|
|
58
|
+
let extHeader;
|
|
59
|
+
try {
|
|
60
|
+
extHeader = yield* call(() =>
|
|
61
|
+
this.client.getHeaderAtHeight(height)
|
|
62
|
+
);
|
|
63
|
+
} catch (_e) {
|
|
64
|
+
// Likely reached the chain tip; stop here
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const blockHash = extHeader.commit.block_id.hash;
|
|
69
|
+
|
|
70
|
+
const primitives = yield* this.readPrimitives(
|
|
71
|
+
height,
|
|
72
|
+
blockHash,
|
|
73
|
+
this.config.primitives,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
outputs.push({
|
|
77
|
+
output: {
|
|
78
|
+
timestamp: extHeader.header.time,
|
|
79
|
+
height,
|
|
80
|
+
hash: blockHash,
|
|
81
|
+
primitives,
|
|
82
|
+
},
|
|
83
|
+
cleanup: () => {},
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (outputs.length === 0) {
|
|
88
|
+
if (!lastPage) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`[Celestia] Could not fetch any blocks from ${data.from} to ${data.to} and no previous page was found.`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
output: [],
|
|
95
|
+
lastPage,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const lastOutput = outputs[outputs.length - 1].output;
|
|
100
|
+
return {
|
|
101
|
+
output: outputs,
|
|
102
|
+
lastPage: {
|
|
103
|
+
ownBlockNumber: lastOutput.height,
|
|
104
|
+
own: {
|
|
105
|
+
height: lastOutput.height,
|
|
106
|
+
hash: lastOutput.hash,
|
|
107
|
+
},
|
|
108
|
+
root: rootConversion.toRootPage(lastOutput),
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@bound
|
|
114
|
+
*readPrimitives(
|
|
115
|
+
height: number,
|
|
116
|
+
blockHash: string,
|
|
117
|
+
primitiveEntries: Extract<
|
|
118
|
+
PrimitiveEntry,
|
|
119
|
+
{ syncProtocol: ConfigSyncProtocolType.CELESTIA_PARALLEL }
|
|
120
|
+
>[],
|
|
121
|
+
): Operation<PrimitiveType[]> {
|
|
122
|
+
const allOperations: Operation<PrimitiveType[]>[] = primitiveEntries.map(
|
|
123
|
+
(entry) => this.fetchBlobsForPrimitive(height, blockHash, entry),
|
|
124
|
+
);
|
|
125
|
+
const results = yield* all(allOperations);
|
|
126
|
+
return results.flat();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@bound
|
|
130
|
+
*fetchBlobsForPrimitive(
|
|
131
|
+
height: number,
|
|
132
|
+
blockHash: string,
|
|
133
|
+
primitiveEntry: Extract<
|
|
134
|
+
PrimitiveEntry,
|
|
135
|
+
{ syncProtocol: ConfigSyncProtocolType.CELESTIA_PARALLEL }
|
|
136
|
+
>,
|
|
137
|
+
): Operation<PrimitiveType[]> {
|
|
138
|
+
const namespace = primitiveEntry.primitive.namespace;
|
|
139
|
+
const namespaceB64 = celestiaNamespaceToBase64(namespace);
|
|
140
|
+
|
|
141
|
+
const blobs = yield* call(() =>
|
|
142
|
+
this.client.getBlobsAtHeight(height, namespaceB64)
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
return blobs.map((blob): PrimitiveType => ({
|
|
146
|
+
syncProtocol: {
|
|
147
|
+
name: primitiveEntry.syncProtocol,
|
|
148
|
+
blockNumber: height,
|
|
149
|
+
transactionHash: blockHash,
|
|
150
|
+
contractAddress: namespace,
|
|
151
|
+
logIndex: blob.index,
|
|
152
|
+
},
|
|
153
|
+
primitive: primitiveEntry.primitive.name,
|
|
154
|
+
output: {
|
|
155
|
+
payloadType: "celestia-blob",
|
|
156
|
+
payload: {
|
|
157
|
+
suppliedValue: atob(blob.data),
|
|
158
|
+
namespace,
|
|
159
|
+
commitment: blob.commitment,
|
|
160
|
+
blobIndex: blob.index,
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { call, type Operation } from "effection";
|
|
2
|
+
import { bound } from "@effectstream/utils";
|
|
3
|
+
import type { PoolClient } from "pg";
|
|
4
|
+
import { type LastPage, SyncState } from "../base/state.ts";
|
|
5
|
+
import type { RootOutput, RootPage } from "../types.ts";
|
|
6
|
+
import type { Input, Output, Page } from "./types.ts";
|
|
7
|
+
import { pageRelation } from "./types.ts";
|
|
8
|
+
import type { CelestiaFetcher } from "./fetcher.ts";
|
|
9
|
+
import type { ConfigNetworkType, SyncProtocolWithNetwork } from "@effectstream/config";
|
|
10
|
+
import { getPage } from "@effectstream/db";
|
|
11
|
+
import { CelestiaClient } from "./CelestiaClient.ts";
|
|
12
|
+
import { applyDelay } from "../common/utils.ts";
|
|
13
|
+
|
|
14
|
+
export class CelestiaSyncState extends SyncState<
|
|
15
|
+
Input,
|
|
16
|
+
Output,
|
|
17
|
+
Page,
|
|
18
|
+
RootOutput,
|
|
19
|
+
RootPage,
|
|
20
|
+
CelestiaFetcher
|
|
21
|
+
> {
|
|
22
|
+
constructor(
|
|
23
|
+
lastPage: LastPage<Page, RootPage> | undefined,
|
|
24
|
+
readonly config: Extract<
|
|
25
|
+
SyncProtocolWithNetwork,
|
|
26
|
+
{ networkType: ConfigNetworkType.CELESTIA }
|
|
27
|
+
>,
|
|
28
|
+
fetcher: CelestiaFetcher,
|
|
29
|
+
public readonly client: CelestiaClient,
|
|
30
|
+
dbConn: PoolClient,
|
|
31
|
+
) {
|
|
32
|
+
super(
|
|
33
|
+
config.syncProtocol.name,
|
|
34
|
+
lastPage,
|
|
35
|
+
fetcher,
|
|
36
|
+
pageRelation,
|
|
37
|
+
dbConn,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@bound
|
|
42
|
+
override toPage(_input: Input, data: Output[]): Page {
|
|
43
|
+
const lastBlock = data[data.length - 1];
|
|
44
|
+
return {
|
|
45
|
+
height: lastBlock.height,
|
|
46
|
+
hash: lastBlock.hash,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@bound
|
|
51
|
+
override toRootPage(data: Output): RootPage {
|
|
52
|
+
// Celestia headers use ISO 8601 timestamps; convert to ms.
|
|
53
|
+
const timestampMs = new Date(data.timestamp).getTime();
|
|
54
|
+
return applyDelay(timestampMs, this.config.syncProtocol.delayMs);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@bound
|
|
58
|
+
override toRootOutput(_data: Output): RootOutput {
|
|
59
|
+
throw new Error("Only main chains create root outputs");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@bound
|
|
63
|
+
override *stateToInput(): Operation<Input | undefined> {
|
|
64
|
+
const latestHeight = yield* call(() =>
|
|
65
|
+
this.client.getLatestBlockHeight()
|
|
66
|
+
);
|
|
67
|
+
const startHeight = this.lastPage?.own.height ??
|
|
68
|
+
this.config.syncProtocol.startBlockHeight - 1;
|
|
69
|
+
|
|
70
|
+
if (startHeight >= latestHeight) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const from = startHeight + 1;
|
|
75
|
+
const to = Math.min(
|
|
76
|
+
from + this.config.syncProtocol.stepSize - 1,
|
|
77
|
+
latestHeight,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
from,
|
|
82
|
+
to,
|
|
83
|
+
isPresync: false,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@bound
|
|
88
|
+
override mergeDatum(ourOutput: Output, rootOutput: RootOutput): void {
|
|
89
|
+
const primitives = ourOutput.primitives.map((p) => ({
|
|
90
|
+
...p,
|
|
91
|
+
source: this.config.syncProtocol.name,
|
|
92
|
+
}));
|
|
93
|
+
const blockInfo = [{
|
|
94
|
+
protocol_name: this.config.syncProtocol.name,
|
|
95
|
+
block_number: ourOutput.height,
|
|
96
|
+
blockHash: ourOutput.hash,
|
|
97
|
+
}];
|
|
98
|
+
rootOutput.blockInfo.push(...blockInfo);
|
|
99
|
+
rootOutput.primitives.push(...primitives);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@bound
|
|
103
|
+
override getNamespace(): string[] {
|
|
104
|
+
return [this.config.network.name, this.config.syncProtocol.name];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
static *restoreState(
|
|
108
|
+
dbConn: PoolClient,
|
|
109
|
+
config: Extract<
|
|
110
|
+
SyncProtocolWithNetwork,
|
|
111
|
+
{ networkType: ConfigNetworkType.CELESTIA }
|
|
112
|
+
>,
|
|
113
|
+
fetcher: CelestiaFetcher,
|
|
114
|
+
): Operation<CelestiaSyncState> {
|
|
115
|
+
const [result] = yield* call(async () =>
|
|
116
|
+
await getPage.run({
|
|
117
|
+
protocol_name: config.syncProtocol.name,
|
|
118
|
+
}, dbConn)
|
|
119
|
+
);
|
|
120
|
+
const page = result
|
|
121
|
+
? result.page as unknown as LastPage<Page, RootPage>
|
|
122
|
+
: undefined;
|
|
123
|
+
return new CelestiaSyncState(
|
|
124
|
+
page,
|
|
125
|
+
config,
|
|
126
|
+
fetcher,
|
|
127
|
+
new CelestiaClient(config.network.rpcUrl),
|
|
128
|
+
dbConn,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ConfigSyncProtocolType,
|
|
3
|
+
FlattenSyncProtocolIOFor,
|
|
4
|
+
SyncProtocolWithNetwork,
|
|
5
|
+
} from "@effectstream/config";
|
|
6
|
+
import type { BlockNumber } from "@effectstream/utils";
|
|
7
|
+
import type { PageSyncRange } from "../common/page-helpers.ts";
|
|
8
|
+
import type { PageRelation } from "../base/page.ts";
|
|
9
|
+
|
|
10
|
+
// =====================
|
|
11
|
+
// Sync protocol typings
|
|
12
|
+
// =====================
|
|
13
|
+
|
|
14
|
+
/** The page identifier for Celestia: a block height + hash pair */
|
|
15
|
+
export type Page = {
|
|
16
|
+
height: BlockNumber;
|
|
17
|
+
hash: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type PrimitiveType = FlattenSyncProtocolIOFor<
|
|
21
|
+
ConfigSyncProtocolType.CELESTIA_PARALLEL
|
|
22
|
+
>;
|
|
23
|
+
|
|
24
|
+
export type Input = PageSyncRange<BlockNumber>;
|
|
25
|
+
|
|
26
|
+
export type Output = {
|
|
27
|
+
/** ISO 8601 timestamp from the block header */
|
|
28
|
+
timestamp: string;
|
|
29
|
+
/** Block height */
|
|
30
|
+
height: number;
|
|
31
|
+
/** Hex block hash from last_block_id */
|
|
32
|
+
hash: string;
|
|
33
|
+
primitives: PrimitiveType[];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const pageRelation: PageRelation<Page> = {
|
|
37
|
+
compare: (p1, p2) => p1.height - p2.height,
|
|
38
|
+
equals: (p1, p2) => p1.height === p2.height,
|
|
39
|
+
min: (p1, p2) => (p1.height < p2.height ? p1 : p2),
|
|
40
|
+
max: (p1, p2) => (p1.height > p2.height ? p1 : p2),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type ConfigType = Extract<
|
|
44
|
+
SyncProtocolWithNetwork,
|
|
45
|
+
{ syncProtocolType: ConfigSyncProtocolType.CELESTIA_PARALLEL }
|
|
46
|
+
>;
|