-
Notifications
You must be signed in to change notification settings - Fork 285
/
Copy pathblock.ts
292 lines (227 loc) · 10.5 KB
/
block.ts
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
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
import { Hash, HashAlgorithms, Slots } from "../crypto";
import { BlockSchemaError } from "../errors";
import { IBlock, IBlockData, IBlockJson, IBlockVerification, ITransaction, ITransactionData } from "../interfaces";
import { configManager } from "../managers/config";
import { BigNumber, isException } from "../utils";
import { validator } from "../validation";
import { Serializer } from "./serializer";
import { Managers } from "..";
export class Block implements IBlock {
// @ts-ignore - todo: this is public but not initialised on creation, either make it private or declare it as undefined
public serialized: string;
public data: IBlockData;
public transactions: ITransaction[];
public verification: IBlockVerification;
public constructor({ data, transactions, id }: { data: IBlockData; transactions: ITransaction[]; id?: string }) {
this.data = data;
// TODO genesis block calculated id is wrong for some reason
if (this.data.height === 1) {
if (id) {
this.applyGenesisBlockFix(id);
} else if (data.id) {
this.applyGenesisBlockFix(data.id);
}
}
// fix on real timestamp, this is overloading transaction
// timestamp with block timestamp for storage only
// also add sequence to keep database sequence
this.transactions = transactions.map((transaction, index) => {
transaction.data.blockId = this.data.id;
transaction.data.blockHeight = this.data.height;
transaction.data.sequence = index;
transaction.timestamp = this.data.timestamp;
return transaction;
});
delete this.data.transactions;
this.verification = this.verify();
// Order of transactions messed up in mainnet V1
const { wrongTransactionOrder } = configManager.get("exceptions");
if (this.data.id && wrongTransactionOrder && wrongTransactionOrder[this.data.id]) {
const fixedOrderIds = wrongTransactionOrder[this.data.id];
this.transactions = fixedOrderIds.map((id: string) =>
this.transactions.find((transaction) => transaction.id === id),
);
}
}
public static applySchema(data: IBlockData): IBlockData | undefined {
let result = validator.validate("block", data);
if (!result.error) {
return result.value;
}
result = validator.validateException("block", data);
if (!result.errors) {
return result.value;
}
for (const err of result.errors) {
let fatal = false;
const match = err.dataPath.match(/\.transactions\[([0-9]+)\]/);
if (match === null) {
if (!isException(data)) {
fatal = true;
}
} else {
const txIndex = match[1];
if (data.transactions) {
const tx = data.transactions[txIndex];
if (tx.id === undefined || !isException(tx)) {
fatal = true;
}
}
}
if (fatal) {
throw new BlockSchemaError(
data.height,
`Invalid data${err.dataPath ? " at " + err.dataPath : ""}: ` +
`${err.message}: ${JSON.stringify(err.data)}`,
);
}
}
return result.value;
}
public static getIdHex(data: IBlockData): string {
const constants = configManager.getMilestone(data.height);
const payloadHash: Buffer = Serializer.serialize(data);
const hash: Buffer = HashAlgorithms.sha256(payloadHash);
if (constants.block.idFullSha256) {
return hash.toString("hex");
}
const temp: Buffer = Buffer.alloc(8);
for (let i = 0; i < 8; i++) {
temp[i] = hash[7 - i];
}
return temp.toString("hex");
}
public static toBytesHex(data): string {
const temp: string = data ? BigNumber.make(data).toString(16) : "";
return "0".repeat(16 - temp.length) + temp;
}
public static getId(data: IBlockData): string {
const constants = configManager.getMilestone(data.height);
const idHex: string = Block.getIdHex(data);
return constants.block.idFullSha256 ? idHex : BigNumber.make(`0x${idHex}`).toString();
}
public getHeader(): IBlockData {
const header: IBlockData = Object.assign({}, this.data);
delete header.transactions;
return header;
}
public verifySignature(): boolean {
const bytes: Buffer = Serializer.serialize(this.data, false);
const hash: Buffer = HashAlgorithms.sha256(bytes);
if (!this.data.blockSignature) {
throw new Error();
}
return Hash.verifyECDSA(hash, this.data.blockSignature, this.data.generatorPublicKey);
}
public toJson(): IBlockJson {
const data: IBlockJson = JSON.parse(JSON.stringify(this.data));
data.reward = this.data.reward.toString();
data.totalAmount = this.data.totalAmount.toString();
data.totalFee = this.data.totalFee.toString();
data.transactions = this.transactions.map((transaction) => transaction.toJson());
return data;
}
public verify(): IBlockVerification {
const block: IBlockData = this.data;
const result: IBlockVerification = {
verified: false,
containsMultiSignatures: false,
errors: [],
};
try {
const constants = configManager.getMilestone(block.height);
if (block.height !== 1) {
if (!block.previousBlock) {
result.errors.push("Invalid previous block");
}
}
if (!block.reward.isEqualTo(constants.reward)) {
result.errors.push(["Invalid block reward:", block.reward, "expected:", constants.reward].join(" "));
}
const valid = this.verifySignature();
if (!valid) {
result.errors.push("Failed to verify block signature");
}
if (block.version !== constants.block.version) {
result.errors.push("Invalid block version");
}
if (block.timestamp > Slots.getTime() + Managers.configManager.getMilestone(block.height).blocktime) {
result.errors.push("Invalid block timestamp");
}
const size: number = Serializer.size(this);
if (size > constants.block.maxPayload) {
result.errors.push(`Payload is too large: ${size} > ${constants.block.maxPayload}`);
}
const invalidTransactions: ITransaction[] = this.transactions.filter((tx) => !tx.verified);
if (invalidTransactions.length > 0) {
result.errors.push("One or more transactions are not verified:");
for (const invalidTransaction of invalidTransactions) {
result.errors.push(`=> ${invalidTransaction.serialized.toString("hex")}`);
}
result.containsMultiSignatures = invalidTransactions.some((tx) => !!tx.data.signatures);
}
if (this.transactions.length !== block.numberOfTransactions) {
result.errors.push("Invalid number of transactions");
}
if (this.transactions.length > constants.block.maxTransactions) {
if (block.height > 1) {
result.errors.push("Transactions length is too high");
}
}
// Checking if transactions of the block adds up to block values.
const appliedTransactions: Record<string, ITransactionData> = {};
let totalAmount: BigNumber = BigNumber.ZERO;
let totalFee: BigNumber = BigNumber.ZERO;
const payloadBuffers: Buffer[] = [];
for (const transaction of this.transactions) {
if (!transaction.data || !transaction.data.id) {
throw new Error();
}
const bytes: Buffer = Buffer.from(transaction.data.id, "hex");
if (appliedTransactions[transaction.data.id]) {
result.errors.push(`Encountered duplicate transaction: ${transaction.data.id}`);
}
if (
transaction.data.expiration &&
transaction.data.expiration > 0 &&
transaction.data.expiration <= this.data.height
) {
const isException =
configManager.get("network.name") === "devnet" && constants.ignoreExpiredTransactions;
if (!isException) {
result.errors.push(`Encountered expired transaction: ${transaction.data.id}`);
}
}
if (transaction.data.version === 1 && !constants.block.acceptExpiredTransactionTimestamps) {
const now: number = block.timestamp;
if (transaction.data.timestamp > now + 3600 + constants.blocktime) {
result.errors.push(`Encountered future transaction: ${transaction.data.id}`);
} else if (now - transaction.data.timestamp > 21600) {
result.errors.push(`Encountered expired transaction: ${transaction.data.id}`);
}
}
appliedTransactions[transaction.data.id] = transaction.data;
totalAmount = totalAmount.plus(transaction.data.amount);
totalFee = totalFee.plus(transaction.data.fee);
payloadBuffers.push(bytes);
}
if (!totalAmount.isEqualTo(block.totalAmount)) {
result.errors.push("Invalid total amount");
}
if (!totalFee.isEqualTo(block.totalFee)) {
result.errors.push("Invalid total fee");
}
if (HashAlgorithms.sha256(payloadBuffers).toString("hex") !== block.payloadHash) {
result.errors.push("Invalid payload hash");
}
} catch (error) {
result.errors.push(error);
}
result.verified = result.errors.length === 0;
return result;
}
private applyGenesisBlockFix(id: string): void {
this.data.id = id;
this.data.idHex = id.length === 64 ? id : Block.toBytesHex(id); // if id.length is 64 it's already hex
}
}