-
Notifications
You must be signed in to change notification settings - Fork 1
/
advertisement.js
201 lines (189 loc) · 8.09 KB
/
advertisement.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
import * as Block from 'multiformats/block'
import * as DagCbor from '@ipld/dag-cbor'
import { CID } from 'multiformats/cid'
import { sha256 } from 'multiformats/hashes/sha2'
import { concat } from 'uint8arrays/concat'
import { RecordEnvelope } from '@libp2p/peer-record'
/**
* @typedef {import('./schema').Link } Link
* @typedef {import('./provider').Provider } Provider
* @typedef {import('@libp2p/interface-peer-id').PeerId} PeerId
* @typedef {import('@multiformats/multiaddr').Multiaddr} Multiaddr
* @typedef {Uint8Array} Bytes
* @typedef {Uint8Array} Metadata
*/
// libp2p signed Envelope details
// see: https://github.com/ipni/go-libipni/blob/afe2d8ea45b86c2a22f756ee521741c8f99675e5/ingest/schema/envelope.go#L20-L22
// see: https://github.com/libp2p/js-libp2p-peer-record/blob/master/README.md#envelope
export const AD_SIG_CODEC = new TextEncoder().encode('/indexer/ingest/adSignature')
export const EP_SIG_CODEC = new TextEncoder().encode('/indexer/ingest/extendedProviderSignature')
export const SIG_DOMAIN = 'indexer'
// instead of making Entries optional there is a magic CID, a stubby 16 byte sha256 of empty bytes
// https://github.com/ipni/go-libipni/blob/81286e4b32baed09e6151ce4f8e763f449b81331/ingest/schema/schema.go#L64-L69
export const NO_ENTRIES = CID.parse('bafkreehdwdcefgh4dqkjv67uzcmw7oje')
// an empty byte array signifies no context should be applied
export const NO_CONTEXT = new Uint8Array()
// maximum number of bytes accepted as Advertisement.ContextID.
export const MAX_CONTEXT_ID_LENGTH = 64
/**
* Sign the serialized form of an Advertisement or a Provider
* @param {PeerId} peerId
* @param {Uint8Array} bytes - bytes to sign
* @param {AD_SIG_CODEC|EP_SIG_CODEC} codec - envelope record codec
*/
export async function sign (peerId, bytes, codec) {
const payload = await hashSignableBytes(bytes)
const record = {
codec,
domain: SIG_DOMAIN,
marshal: () => payload,
equals: () => { throw new Error('Not implemented') }
}
const sealed = await RecordEnvelope.seal(record, peerId)
return sealed.marshal()
}
/**
* `sign` must take the sha-256 multihash of the signable bytes and sign that.
* see: https://github.com/ipni/go-libipni/blob/81286e4b32baed09e6151ce4f8e763f449b81331/ingest/schema/envelope.go#L119
* @param {Uint8Array} bytes
*/
export async function hashSignableBytes (bytes) {
const digest = await sha256.digest(bytes)
return digest.bytes
}
/**
* Encode and Sign IPNI Advertisement data.
*/
export class Advertisement {
/**
* @param {object} config
* @param {Link | null} config.previous - CID of previous Advertisement
* @param {Provider[]|Provider} config.providers - Array of Provider info where entries are available
* @param {Link | null} config.entries - CID for an EntryBatch, an array of content multihashes you're providing
* @param {Bytes | null} config.context - A custom id used to group subsets of advertisements
* @param {boolean} [config.remove] - true if this represents entries that are no longer retrievable.
* @param {boolean} [config.override] - true if the extended providers specified should be used instead of any previously announced without a context.
*/
constructor ({ previous, providers, context, entries, remove = false, override = false }) {
if (!providers) {
throw new Error('providers are required')
}
if (entries === undefined) {
throw new Error('entries must be set. To specify no entries pass null')
}
if (entries !== null && CID.asCID(entries) === null) {
throw new Error('entries must be an instance of CID')
}
if (context === undefined) {
throw new Error('context must be set. To specify no context pass null')
}
if (previous === undefined) {
throw new Error('previous must be set. If this is your first advertisement pass null')
}
if (previous !== null && CID.asCID(previous) === null) {
throw new Error('previous must be an instance of CID')
}
if (context !== null && context.byteLength > MAX_CONTEXT_ID_LENGTH) {
throw new Error(`context must be less than ${MAX_CONTEXT_ID_LENGTH} bytes`)
}
this.providers = Array.isArray(providers) ? providers : [providers]
this.previous = CID.asCID(previous)
this.entries = CID.asCID(entries) ?? NO_ENTRIES
this.context = context ?? NO_CONTEXT
this.remove = remove
this.override = override
if (this.remove && this.providers.length > 1) {
// see: https://github.com/ipni/go-libipni/blob/afe2d8ea45b86c2a22f756ee521741c8f99675e5/ingest/schema/envelope.go#L126-L127
throw new Error('remove may only be true when there is a single provider. IsRm is not supported for ExtendedProvider advertisements')
}
if (this.override && (this.context.byteLength === 0 || this.providers.length < 2)) {
throw new Error('override may only be true when a context is set and more than 1 provider')
}
}
/**
* Convert to IPLD shape and sign
*/
async encodeAndSign () {
const ad = this
const provider = ad.providers[0]
/** @type {import('./schema').AdvertisementOutput} AdvertisementOutput */
const value = {
Provider: provider.peerId.toString(),
Addresses: provider.addresses.map(a => a.toString()),
Signature: await sign(provider.peerId, ad.signableBytes(), AD_SIG_CODEC),
Entries: ad.entries,
ContextID: ad.context,
Metadata: provider.encodeMetadata(),
IsRm: ad.remove
}
if (ad.previous) {
value.PreviousID = ad.previous
}
// ExtendedProvider mode!
if (ad.providers.length > 1) {
const Providers = []
for (const p of ad.providers) {
Providers.push({
ID: p.peerId.toString(),
Addresses: p.addresses.map(a => a.toString()),
Metadata: p.encodeMetadata(),
Signature: await sign(p.peerId, p.signableBytes(ad), EP_SIG_CODEC)
})
}
value.ExtendedProvider = {
Providers,
Override: ad.override
}
}
return value
}
/**
* Serialize the fields use for signing the Advertisement
* note: peerId and multiaddr string bytes are signed rather than using their byte encodings!
* impl: https://github.com/ipni/go-libipni/blob/afe2d8ea45b86c2a22f756ee521741c8f99675e5/ingest/schema/envelope.go#L84
*/
signableBytes () {
const text = new TextEncoder()
const ad = this
const provider = this.providers[0]
const IsRm = ad.remove ? 1 : 0
return concat([
ad.previous?.bytes ?? new Uint8Array(),
ad.entries.bytes,
text.encode(provider.peerId.toString()),
text.encode(provider.addresses.map(a => a.toString()).join('')),
provider.encodeMetadata(),
new Uint8Array([IsRm])
])
}
/**
* the dag-json encoded IPLD Block
*/
async export () {
const value = await this.encodeAndSign()
return Block.encode({ codec: DagCbor, hasher: sha256, value })
}
}
/**
* Advertise that **all** past and future entries in this chain are now
* available from a new, additional provider by specifying the root provider
* and the additional providers along with no context id and no entries cid.
*
* To advertise that subset of entries are available from additional providers
* specify the relevant context id to identify that group.
*
* Note: it is not yet possible to unannounce an extended provider once announced.
* see: https://github.com/ipni/storetheindex/issues/1745
*
* @param {object} config
* @param {Link | null} config.previous - CID of previous Advertisement
* @param {Provider[]} config.providers - Two or more Provider objects where entries are available
* @param {Bytes | null} [config.context] - A custom id used to group subsets of advertisements
* @param {boolean} [config.override] - true if the providers should be used instead of any previously announced without a context.
*/
export function createExtendedProviderAd ({ previous, providers, context = null, override = false }) {
if (!providers || !Array.isArray(providers) || providers.length < 2) {
throw new Error('at least 2 providers are required, the root provider and the new extended provider')
}
return new Advertisement({ previous, providers, entries: null, context, override })
}