@tapforce/pod-bridge-sdk 1.2.4 → 2.0.1
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/README.md +115 -156
- package/dist/clients/action/pod-to-source-chain-client.d.ts +20 -21
- package/dist/clients/action/pod-to-source-chain-client.js +25 -27
- package/dist/clients/action/source-chain-to-pod-client.d.ts +7 -6
- package/dist/clients/action/source-chain-to-pod-client.js +9 -8
- package/dist/clients/tracker/client.d.ts +5 -9
- package/dist/clients/tracker/client.js +12 -17
- package/dist/clients/tracker/pod-tracker.service.d.ts +4 -13
- package/dist/clients/tracker/pod-tracker.service.js +34 -31
- package/dist/clients/tracker/source-chain-tracker.service.d.ts +5 -10
- package/dist/clients/tracker/source-chain-tracker.service.js +25 -20
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -11
- package/dist/libs/abi/bridge.abi.d.ts +14 -6
- package/dist/libs/abi/bridge.abi.js +30 -20
- package/dist/libs/helpers/bridge-claim-proof.helper.d.ts +16 -0
- package/dist/libs/helpers/bridge-claim-proof.helper.js +27 -0
- package/dist/libs/helpers/convert-certified-log.helper.d.ts +2 -6
- package/dist/libs/helpers/convert-certified-log.helper.js +4 -8
- package/dist/libs/types/pod-bridge.types.d.ts +54 -17
- package/package.json +1 -1
- package/dist/libs/helpers/signature-recovery.helper.d.ts +0 -161
- package/dist/libs/helpers/signature-recovery.helper.js +0 -447
|
@@ -1,447 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.parseDerSignature = parseDerSignature;
|
|
4
|
-
exports.addressFromPublicKey = addressFromPublicKey;
|
|
5
|
-
exports.recoverSignature65B = recoverSignature65B;
|
|
6
|
-
exports.recoverAggregatedSignatures65B = recoverAggregatedSignatures65B;
|
|
7
|
-
exports.computeDepositTxHash = computeDepositTxHash;
|
|
8
|
-
exports.extractAggregatedSignatures = extractAggregatedSignatures;
|
|
9
|
-
exports.recoverSignatureWithoutPubkey = recoverSignatureWithoutPubkey;
|
|
10
|
-
exports.recoverSignatureWithValidators = recoverSignatureWithValidators;
|
|
11
|
-
exports.decompressPublicKey = decompressPublicKey;
|
|
12
|
-
exports.extractAggregatedSignaturesWithValidators = extractAggregatedSignaturesWithValidators;
|
|
13
|
-
exports.extractSignatureInfo = extractSignatureInfo;
|
|
14
|
-
const ethers_1 = require("ethers");
|
|
15
|
-
/**
|
|
16
|
-
* Parse a DER-encoded ECDSA signature to extract r and s components.
|
|
17
|
-
*
|
|
18
|
-
* DER format:
|
|
19
|
-
* 0x30 [total-length] 0x02 [r-length] [r] 0x02 [s-length] [s]
|
|
20
|
-
*
|
|
21
|
-
* Example: 3044022020749ca9...0220292484ed...
|
|
22
|
-
* - 30 = SEQUENCE tag
|
|
23
|
-
* - 44 = total length (68 bytes)
|
|
24
|
-
* - 02 = INTEGER tag for r
|
|
25
|
-
* - 20 = r length (32 bytes)
|
|
26
|
-
* - [32 bytes of r]
|
|
27
|
-
* - 02 = INTEGER tag for s
|
|
28
|
-
* - 20 = s length (32 bytes)
|
|
29
|
-
* - [32 bytes of s]
|
|
30
|
-
*
|
|
31
|
-
* @param derSignature The DER-encoded signature as hex string
|
|
32
|
-
* @returns Object with r and s as 32-byte hex strings (0x prefixed)
|
|
33
|
-
*/
|
|
34
|
-
function parseDerSignature(derSignature) {
|
|
35
|
-
const hex = derSignature.replace('0x', '');
|
|
36
|
-
const bytes = Uint8Array.from(Buffer.from(hex, 'hex'));
|
|
37
|
-
let offset = 0;
|
|
38
|
-
// Check SEQUENCE tag (0x30)
|
|
39
|
-
if (bytes[offset++] !== 0x30) {
|
|
40
|
-
throw new Error('Invalid DER signature: expected SEQUENCE tag (0x30)');
|
|
41
|
-
}
|
|
42
|
-
// Skip total length
|
|
43
|
-
const totalLength = bytes[offset++];
|
|
44
|
-
if (totalLength > 127) {
|
|
45
|
-
// Long form length (shouldn't happen for ECDSA sigs)
|
|
46
|
-
throw new Error('Invalid DER signature: unexpected long form length');
|
|
47
|
-
}
|
|
48
|
-
// Parse r
|
|
49
|
-
if (bytes[offset++] !== 0x02) {
|
|
50
|
-
throw new Error('Invalid DER signature: expected INTEGER tag (0x02) for r');
|
|
51
|
-
}
|
|
52
|
-
let rLength = bytes[offset++];
|
|
53
|
-
let rStart = offset;
|
|
54
|
-
offset += rLength;
|
|
55
|
-
// Parse s
|
|
56
|
-
if (bytes[offset++] !== 0x02) {
|
|
57
|
-
throw new Error('Invalid DER signature: expected INTEGER tag (0x02) for s');
|
|
58
|
-
}
|
|
59
|
-
let sLength = bytes[offset++];
|
|
60
|
-
let sStart = offset;
|
|
61
|
-
// Extract r and s, handling potential leading zero padding
|
|
62
|
-
let rBytes = bytes.slice(rStart, rStart + rLength);
|
|
63
|
-
let sBytes = bytes.slice(sStart, sStart + sLength);
|
|
64
|
-
// Remove leading zero if present (DER adds 0x00 prefix for values with high bit set)
|
|
65
|
-
if (rBytes.length === 33 && rBytes[0] === 0) {
|
|
66
|
-
rBytes = rBytes.slice(1);
|
|
67
|
-
}
|
|
68
|
-
if (sBytes.length === 33 && sBytes[0] === 0) {
|
|
69
|
-
sBytes = sBytes.slice(1);
|
|
70
|
-
}
|
|
71
|
-
// Pad to 32 bytes if necessary
|
|
72
|
-
const r = Buffer.from(rBytes).toString('hex').padStart(64, '0');
|
|
73
|
-
const s = Buffer.from(sBytes).toString('hex').padStart(64, '0');
|
|
74
|
-
return { r: '0x' + r, s: '0x' + s };
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Derive Ethereum address from a public key.
|
|
78
|
-
*
|
|
79
|
-
* @param publicKey The public key (uncompressed 65 bytes with 04 prefix, or 64 bytes x||y)
|
|
80
|
-
* @returns The Ethereum address
|
|
81
|
-
*/
|
|
82
|
-
function addressFromPublicKey(publicKey) {
|
|
83
|
-
const pubKeyHex = publicKey.replace('0x', '');
|
|
84
|
-
// Handle different public key formats
|
|
85
|
-
let xyBytes;
|
|
86
|
-
if (pubKeyHex.length === 130 && pubKeyHex.startsWith('04')) {
|
|
87
|
-
// Uncompressed: 04 + x (32 bytes) + y (32 bytes)
|
|
88
|
-
xyBytes = pubKeyHex.slice(2);
|
|
89
|
-
}
|
|
90
|
-
else if (pubKeyHex.length === 128) {
|
|
91
|
-
// Already x || y without prefix
|
|
92
|
-
xyBytes = pubKeyHex;
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
throw new Error(`Invalid public key length: ${pubKeyHex.length}. Expected 128 or 130 hex chars.`);
|
|
96
|
-
}
|
|
97
|
-
// Address = keccak256(x || y)[12:] = last 20 bytes
|
|
98
|
-
const hash = (0, ethers_1.keccak256)('0x' + xyBytes);
|
|
99
|
-
return '0x' + hash.slice(-40);
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Recover a 65-byte ECDSA signature (r,s,v) from a 64-byte compact signature.
|
|
103
|
-
*
|
|
104
|
-
* Exactly matches the Rust implementation:
|
|
105
|
-
* ```rust
|
|
106
|
-
* pub fn recover_65b_signature(
|
|
107
|
-
* sig: &Signature,
|
|
108
|
-
* msg_hash: [u8; 32],
|
|
109
|
-
* pubkey: &PublicKey,
|
|
110
|
-
* ) -> anyhow::Result<[u8; 65]> {
|
|
111
|
-
* let secp = Secp256k1::new();
|
|
112
|
-
* let msg = Message::from_digest(msg_hash);
|
|
113
|
-
* let sig_bytes = sig.serialize_compact();
|
|
114
|
-
*
|
|
115
|
-
* for recovery_id in [RecoveryId::Zero, RecoveryId::One] {
|
|
116
|
-
* let recoverable_sig = RecoverableSignature::from_compact(&sig_bytes, recovery_id)?;
|
|
117
|
-
* match secp.recover_ecdsa(msg, &recoverable_sig) {
|
|
118
|
-
* Ok(recovered_key) => {
|
|
119
|
-
* if recovered_key == *pubkey {
|
|
120
|
-
* return Ok(recoverable_sig.to_alloy().as_bytes());
|
|
121
|
-
* }
|
|
122
|
-
* }
|
|
123
|
-
* Err(e) => { ... }
|
|
124
|
-
* }
|
|
125
|
-
* }
|
|
126
|
-
* Err(anyhow!("Could not find valid recovery ID for signature"))
|
|
127
|
-
* }
|
|
128
|
-
* ```
|
|
129
|
-
*
|
|
130
|
-
* @param r The r component of the signature (32 bytes hex)
|
|
131
|
-
* @param s The s component of the signature (32 bytes hex)
|
|
132
|
-
* @param msgHash The 32-byte message hash that was signed
|
|
133
|
-
* @param publicKey The expected signer's public key
|
|
134
|
-
* @returns The 65-byte signature (r || s || v) or null if recovery fails
|
|
135
|
-
*/
|
|
136
|
-
function recoverSignature65B(r, s, msgHash, publicKey) {
|
|
137
|
-
// Normalize r and s
|
|
138
|
-
const rHex = r.replace('0x', '').padStart(64, '0');
|
|
139
|
-
const sHex = s.replace('0x', '').padStart(64, '0');
|
|
140
|
-
// Derive expected address from public key
|
|
141
|
-
let expectedAddress;
|
|
142
|
-
try {
|
|
143
|
-
expectedAddress = addressFromPublicKey(publicKey);
|
|
144
|
-
}
|
|
145
|
-
catch (e) {
|
|
146
|
-
console.error('Failed to derive address from public key:', e);
|
|
147
|
-
return null;
|
|
148
|
-
}
|
|
149
|
-
// Try both recovery IDs (27 and 28 in Ethereum, corresponding to RecoveryId::Zero and RecoveryId::One)
|
|
150
|
-
for (const v of [27, 28]) {
|
|
151
|
-
try {
|
|
152
|
-
const signature = { r: '0x' + rHex, s: '0x' + sHex, v };
|
|
153
|
-
const recoveredAddress = (0, ethers_1.recoverAddress)(msgHash, signature);
|
|
154
|
-
if (recoveredAddress.toLowerCase() === expectedAddress.toLowerCase()) {
|
|
155
|
-
// Found the correct v value - matches Rust: "if recovered_key == *pubkey"
|
|
156
|
-
const vHex = v.toString(16).padStart(2, '0');
|
|
157
|
-
return '0x' + rHex + sHex + vHex;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
catch (e) {
|
|
161
|
-
// Recovery failed with this v, try the other
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
return null;
|
|
166
|
-
}
|
|
167
|
-
/**
|
|
168
|
-
* Recover multiple 65-byte signatures from concatenated 64-byte signatures.
|
|
169
|
-
*
|
|
170
|
-
* @param aggregatedSig64 Concatenated 64-byte signatures (r || s for each)
|
|
171
|
-
* @param msgHash The message hash that was signed
|
|
172
|
-
* @param publicKeys Array of expected signer public keys in order
|
|
173
|
-
* @returns Concatenated 65-byte signatures or null if any recovery fails
|
|
174
|
-
*/
|
|
175
|
-
function recoverAggregatedSignatures65B(aggregatedSig64, msgHash, publicKeys) {
|
|
176
|
-
const sigBytes = typeof aggregatedSig64 === 'string'
|
|
177
|
-
? Uint8Array.from(Buffer.from(aggregatedSig64.replace('0x', ''), 'hex'))
|
|
178
|
-
: aggregatedSig64;
|
|
179
|
-
const sigCount = sigBytes.length / 64;
|
|
180
|
-
if (sigCount !== publicKeys.length) {
|
|
181
|
-
throw new Error(`Signature count (${sigCount}) doesn't match expected public keys count (${publicKeys.length})`);
|
|
182
|
-
}
|
|
183
|
-
const recovered65BSigs = [];
|
|
184
|
-
for (let i = 0; i < sigCount; i++) {
|
|
185
|
-
const offset = i * 64;
|
|
186
|
-
const r = '0x' + Buffer.from(sigBytes.slice(offset, offset + 32)).toString('hex');
|
|
187
|
-
const s = '0x' + Buffer.from(sigBytes.slice(offset + 32, offset + 64)).toString('hex');
|
|
188
|
-
const sig65 = recoverSignature65B(r, s, msgHash, publicKeys[i]);
|
|
189
|
-
if (!sig65) {
|
|
190
|
-
console.error(`Failed to recover signature ${i} for public key ${publicKeys[i]}`);
|
|
191
|
-
return null;
|
|
192
|
-
}
|
|
193
|
-
recovered65BSigs.push(sig65.replace('0x', ''));
|
|
194
|
-
}
|
|
195
|
-
return '0x' + recovered65BSigs.join('');
|
|
196
|
-
}
|
|
197
|
-
/**
|
|
198
|
-
* Compute the transaction hash for a bridge deposit that needs to be signed.
|
|
199
|
-
* This matches the depositTxHash function in the Bridge.sol contract.
|
|
200
|
-
*
|
|
201
|
-
* @param domainSeparator The domain separator from the bridge contract
|
|
202
|
-
* @param bridgeContract The address of the bridge contract on the other chain
|
|
203
|
-
* @param token The token address
|
|
204
|
-
* @param amount The amount
|
|
205
|
-
* @param to The recipient address
|
|
206
|
-
* @param proof The proof (deposit TX hash)
|
|
207
|
-
* @returns The transaction hash to be signed
|
|
208
|
-
*/
|
|
209
|
-
function computeDepositTxHash(domainSeparator, bridgeContract, token, amount, to, proof) {
|
|
210
|
-
const { AbiCoder, solidityPackedKeccak256 } = require('ethers');
|
|
211
|
-
const abiCoder = new AbiCoder();
|
|
212
|
-
// DEPOSIT_SELECTOR = keccak256("deposit(address,uint256,address)")[:4]
|
|
213
|
-
const DEPOSIT_SELECTOR = '0x47e7ef24';
|
|
214
|
-
// Compute dataHash = keccak256(selector || token || amount || to)
|
|
215
|
-
const dataHash = solidityPackedKeccak256(['bytes4', 'address', 'uint256', 'address'], [DEPOSIT_SELECTOR, token, amount, to]);
|
|
216
|
-
// Compute final hash = keccak256(domainSeparator || bridgeContract || dataHash || proof)
|
|
217
|
-
return (0, ethers_1.keccak256)(abiCoder.encode(['bytes32', 'address', 'bytes32', 'bytes'], [domainSeparator, bridgeContract, dataHash, proof]));
|
|
218
|
-
}
|
|
219
|
-
/**
|
|
220
|
-
* Extract aggregated 65-byte signatures from Pod transaction receipt.
|
|
221
|
-
*
|
|
222
|
-
* Receipt format:
|
|
223
|
-
* - attested_tx.hash: The transaction hash that was signed
|
|
224
|
-
* - signatures: Object with numeric keys containing DER-encoded signatures
|
|
225
|
-
*
|
|
226
|
-
* @param receipt The Pod transaction receipt
|
|
227
|
-
* @param msgHash Optional override for the message hash (defaults to attested_tx.hash)
|
|
228
|
-
* @param committeePublicKeys Optional array of validator public keys for v-value recovery
|
|
229
|
-
* @returns Concatenated 65-byte signatures (r || s || v for each signature)
|
|
230
|
-
*/
|
|
231
|
-
function extractAggregatedSignatures(receipt, msgHash, committeePublicKeys) {
|
|
232
|
-
// Use attested_tx.hash as the message that was signed
|
|
233
|
-
const hashToSign = msgHash || receipt.attested_tx.hash;
|
|
234
|
-
// Get signatures in order (0, 1, 2, ...)
|
|
235
|
-
const sigKeys = Object.keys(receipt.signatures).sort((a, b) => parseInt(a) - parseInt(b));
|
|
236
|
-
if (sigKeys.length === 0) {
|
|
237
|
-
throw new Error('No signatures found in receipt');
|
|
238
|
-
}
|
|
239
|
-
const signatures = [];
|
|
240
|
-
for (let i = 0; i < sigKeys.length; i++) {
|
|
241
|
-
const derSig = receipt.signatures[sigKeys[i]];
|
|
242
|
-
const { r, s } = parseDerSignature(derSig);
|
|
243
|
-
if (committeePublicKeys && committeePublicKeys[i]) {
|
|
244
|
-
// Use public key to recover correct v value
|
|
245
|
-
const sig65 = recoverSignature65B(r, s, hashToSign, committeePublicKeys[i]);
|
|
246
|
-
if (!sig65) {
|
|
247
|
-
throw new Error(`Failed to recover signature ${i} for public key ${committeePublicKeys[i]}`);
|
|
248
|
-
}
|
|
249
|
-
signatures.push(sig65.replace('0x', ''));
|
|
250
|
-
}
|
|
251
|
-
else {
|
|
252
|
-
// No public key available - try both v values and use the first valid one
|
|
253
|
-
const sig65 = recoverSignatureWithoutPubkey(r, s, hashToSign);
|
|
254
|
-
signatures.push(sig65.replace('0x', ''));
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
return '0x' + signatures.join('');
|
|
258
|
-
}
|
|
259
|
-
/**
|
|
260
|
-
* Recover a 65-byte signature without knowing the public key.
|
|
261
|
-
* Tries both v values (27 and 28) and returns the first valid recovery.
|
|
262
|
-
*
|
|
263
|
-
* WARNING: This is less secure as we can't verify the signer.
|
|
264
|
-
* Use recoverSignature65B with public key when possible.
|
|
265
|
-
*
|
|
266
|
-
* @param r The r component
|
|
267
|
-
* @param s The s component
|
|
268
|
-
* @param msgHash The message hash
|
|
269
|
-
* @returns The 65-byte signature with v=27 (tries 27 first, then 28)
|
|
270
|
-
*/
|
|
271
|
-
function recoverSignatureWithoutPubkey(r, s, msgHash) {
|
|
272
|
-
const rHex = r.replace('0x', '').padStart(64, '0');
|
|
273
|
-
const sHex = s.replace('0x', '').padStart(64, '0');
|
|
274
|
-
// Try v=27 first, then v=28
|
|
275
|
-
for (const v of [27, 28]) {
|
|
276
|
-
try {
|
|
277
|
-
const signature = { r: '0x' + rHex, s: '0x' + sHex, v };
|
|
278
|
-
const recoveredAddress = (0, ethers_1.recoverAddress)(msgHash, signature);
|
|
279
|
-
// If recovery succeeds, use this v value
|
|
280
|
-
if (recoveredAddress) {
|
|
281
|
-
const vHex = v.toString(16).padStart(2, '0');
|
|
282
|
-
return '0x' + rHex + sHex + vHex;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
catch (e) {
|
|
286
|
-
continue;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
// Default to v=27 if both fail
|
|
290
|
-
return '0x' + rHex + sHex + '1b';
|
|
291
|
-
}
|
|
292
|
-
/**
|
|
293
|
-
* Recover a 65-byte signature by checking which v-value recovers to a known validator address.
|
|
294
|
-
*
|
|
295
|
-
* @param r The r component
|
|
296
|
-
* @param s The s component
|
|
297
|
-
* @param msgHash The message hash
|
|
298
|
-
* @param validatorAddresses Set of known validator addresses (lowercase)
|
|
299
|
-
* @returns Object with signature and recovered address, or null if no validator match
|
|
300
|
-
*/
|
|
301
|
-
function recoverSignatureWithValidators(r, s, msgHash, validatorAddresses) {
|
|
302
|
-
const rHex = r.replace('0x', '').padStart(64, '0');
|
|
303
|
-
const sHex = s.replace('0x', '').padStart(64, '0');
|
|
304
|
-
// Try both v values and check if recovered address is a known validator
|
|
305
|
-
for (const v of [27, 28]) {
|
|
306
|
-
try {
|
|
307
|
-
const signature = { r: '0x' + rHex, s: '0x' + sHex, v };
|
|
308
|
-
const recoveredAddress = (0, ethers_1.recoverAddress)(msgHash, signature);
|
|
309
|
-
if (recoveredAddress && validatorAddresses.has(recoveredAddress.toLowerCase())) {
|
|
310
|
-
const vHex = v.toString(16).padStart(2, '0');
|
|
311
|
-
return {
|
|
312
|
-
signature: '0x' + rHex + sHex + vHex,
|
|
313
|
-
recoveredAddress: recoveredAddress.toLowerCase()
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
catch (e) {
|
|
318
|
-
continue;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
return null;
|
|
322
|
-
}
|
|
323
|
-
/**
|
|
324
|
-
* Decompress a compressed secp256k1 public key (33 bytes) to uncompressed format.
|
|
325
|
-
* Compressed format: 02/03 prefix + 32-byte x coordinate
|
|
326
|
-
* Uncompressed format: 04 prefix + 32-byte x + 32-byte y
|
|
327
|
-
*
|
|
328
|
-
* @param compressedPubKey The compressed public key (66 hex chars with 02/03 prefix)
|
|
329
|
-
* @returns The uncompressed public key (130 hex chars with 04 prefix)
|
|
330
|
-
*/
|
|
331
|
-
function decompressPublicKey(compressedPubKey) {
|
|
332
|
-
const hex = compressedPubKey.replace('0x', '');
|
|
333
|
-
if (hex.length !== 66) {
|
|
334
|
-
throw new Error(`Invalid compressed public key length: ${hex.length}. Expected 66 hex chars.`);
|
|
335
|
-
}
|
|
336
|
-
const prefix = hex.slice(0, 2);
|
|
337
|
-
if (prefix !== '02' && prefix !== '03') {
|
|
338
|
-
throw new Error(`Invalid compressed public key prefix: ${prefix}. Expected 02 or 03.`);
|
|
339
|
-
}
|
|
340
|
-
// secp256k1 curve parameters
|
|
341
|
-
const p = BigInt('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F');
|
|
342
|
-
const x = BigInt('0x' + hex.slice(2));
|
|
343
|
-
// y² = x³ + 7 (mod p)
|
|
344
|
-
const ySquared = (x ** 3n + 7n) % p;
|
|
345
|
-
// Calculate modular square root using Tonelli-Shanks for p ≡ 3 (mod 4)
|
|
346
|
-
// y = ySquared^((p+1)/4) mod p
|
|
347
|
-
const y = modPow(ySquared, (p + 1n) / 4n, p);
|
|
348
|
-
// Check if we need to negate y based on the prefix
|
|
349
|
-
const isYEven = y % 2n === 0n;
|
|
350
|
-
const needsEvenY = prefix === '02';
|
|
351
|
-
const finalY = isYEven === needsEvenY ? y : p - y;
|
|
352
|
-
// Convert to hex, pad to 64 chars
|
|
353
|
-
const xHex = x.toString(16).padStart(64, '0');
|
|
354
|
-
const yHex = finalY.toString(16).padStart(64, '0');
|
|
355
|
-
return '04' + xHex + yHex;
|
|
356
|
-
}
|
|
357
|
-
/**
|
|
358
|
-
* Modular exponentiation: base^exp mod mod
|
|
359
|
-
*/
|
|
360
|
-
function modPow(base, exp, mod) {
|
|
361
|
-
let result = 1n;
|
|
362
|
-
base = base % mod;
|
|
363
|
-
while (exp > 0n) {
|
|
364
|
-
if (exp % 2n === 1n) {
|
|
365
|
-
result = (result * base) % mod;
|
|
366
|
-
}
|
|
367
|
-
exp = exp / 2n;
|
|
368
|
-
base = (base * base) % mod;
|
|
369
|
-
}
|
|
370
|
-
return result;
|
|
371
|
-
}
|
|
372
|
-
/**
|
|
373
|
-
* Extract aggregated 65-byte signatures using validator public keys for v-value recovery.
|
|
374
|
-
* Committee format: Array of [index, compressed_public_key] tuples
|
|
375
|
-
*
|
|
376
|
-
* @param receipt The Pod transaction receipt
|
|
377
|
-
* @param validators Array of [index, compressedPubKey] tuples from pod_getCommittee
|
|
378
|
-
* @param msgHash Optional override for the message hash
|
|
379
|
-
* @returns Concatenated 65-byte signatures (r || s || v for each signature)
|
|
380
|
-
*/
|
|
381
|
-
function extractAggregatedSignaturesWithValidators(receipt, validators, msgHash) {
|
|
382
|
-
const hashToSign = msgHash || receipt.attested_tx.hash;
|
|
383
|
-
// Convert compressed public keys to addresses
|
|
384
|
-
const validatorAddresses = new Map();
|
|
385
|
-
for (const [index, compressedPubKey] of validators) {
|
|
386
|
-
try {
|
|
387
|
-
const uncompressed = decompressPublicKey(compressedPubKey);
|
|
388
|
-
const address = addressFromPublicKey(uncompressed);
|
|
389
|
-
validatorAddresses.set(index, address.toLowerCase());
|
|
390
|
-
}
|
|
391
|
-
catch (e) {
|
|
392
|
-
console.warn(`Failed to decompress public key for validator ${index}:`, e);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
console.log('Validator addresses:', Object.fromEntries(validatorAddresses));
|
|
396
|
-
// Create a set of all validator addresses for lookup
|
|
397
|
-
const validatorSet = new Set(validatorAddresses.values());
|
|
398
|
-
// Get signatures in order (0, 1, 2, ...)
|
|
399
|
-
const sigKeys = Object.keys(receipt.signatures).sort((a, b) => parseInt(a) - parseInt(b));
|
|
400
|
-
if (sigKeys.length === 0) {
|
|
401
|
-
throw new Error('No signatures found in receipt');
|
|
402
|
-
}
|
|
403
|
-
const signatures = [];
|
|
404
|
-
const recoveredValidators = [];
|
|
405
|
-
for (let i = 0; i < sigKeys.length; i++) {
|
|
406
|
-
const sigIndex = parseInt(sigKeys[i]);
|
|
407
|
-
const derSig = receipt.signatures[sigKeys[i]];
|
|
408
|
-
const { r, s } = parseDerSignature(derSig);
|
|
409
|
-
// Try to recover with validator address verification
|
|
410
|
-
const result = recoverSignatureWithValidators(r, s, hashToSign, validatorSet);
|
|
411
|
-
if (result) {
|
|
412
|
-
signatures.push(result.signature.replace('0x', ''));
|
|
413
|
-
recoveredValidators.push(result.recoveredAddress);
|
|
414
|
-
}
|
|
415
|
-
else {
|
|
416
|
-
// Fallback to without validator check (will likely fail on contract)
|
|
417
|
-
console.warn(`Signature ${sigIndex}: Could not match to any validator address`);
|
|
418
|
-
const sig65 = recoverSignatureWithoutPubkey(r, s, hashToSign);
|
|
419
|
-
signatures.push(sig65.replace('0x', ''));
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
console.log('Recovered validators:', recoveredValidators);
|
|
423
|
-
return '0x' + signatures.join('');
|
|
424
|
-
}
|
|
425
|
-
/**
|
|
426
|
-
* Extract signature info from Pod receipt for debugging/verification.
|
|
427
|
-
*
|
|
428
|
-
* @param receipt The Pod transaction receipt
|
|
429
|
-
* @param msgHash Optional override for the message hash
|
|
430
|
-
* @returns Array of signature info with recovered 65-byte signatures
|
|
431
|
-
*/
|
|
432
|
-
function extractSignatureInfo(receipt, msgHash) {
|
|
433
|
-
const hashToSign = msgHash || receipt.attested_tx.hash;
|
|
434
|
-
const sigKeys = Object.keys(receipt.signatures).sort((a, b) => parseInt(a) - parseInt(b));
|
|
435
|
-
return sigKeys.map((key, index) => {
|
|
436
|
-
const derSig = receipt.signatures[key];
|
|
437
|
-
const { r, s } = parseDerSignature(derSig);
|
|
438
|
-
const sig65 = recoverSignatureWithoutPubkey(r, s, hashToSign);
|
|
439
|
-
return {
|
|
440
|
-
index,
|
|
441
|
-
derSignature: derSig,
|
|
442
|
-
r,
|
|
443
|
-
s,
|
|
444
|
-
signature65: sig65
|
|
445
|
-
};
|
|
446
|
-
});
|
|
447
|
-
}
|