@btc-vision/transaction 1.8.7 → 1.8.9

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.
@@ -6,9 +6,31 @@ export declare class EpochValidator {
6
6
  */
7
7
  static sha1(data: Uint8Array): Uint8Array;
8
8
  /**
9
- * Calculate mining preimage
9
+ * Calculate mining preimage.
10
+ *
11
+ * When useConcatenatedPreimage is false the legacy XOR construction is used
12
+ * (checksumRoot ^ publicKey ^ salt). That construction is malleable: a
13
+ * miner can set salt == publicKey to force preimage == checksumRoot (max
14
+ * difficulty for free), or transform any solution to another key via
15
+ * salt' = publicKey' ^ publicKey ^ salt. When useConcatenatedPreimage is true
16
+ * the preimage is the 96-byte concatenation checksumRoot || publicKey || salt,
17
+ * which removes both (the SHA-1 proof-of-work over it is unchanged).
18
+ *
19
+ * The switch is a consensus rule: callers pass useConcatenatedPreimage =
20
+ * (epochBlockHeight >= activationHeight) using the activation height for the
21
+ * network they are on (opnet-node exposes it as
22
+ * consensusEpochPatches.PREIMAGE_CONCAT_PATCH_BLOCK_HEIGHT). The value MUST be
23
+ * identical across opnet-node, this library and the mining pool.
10
24
  */
11
- static calculatePreimage(checksumRoot: Uint8Array, publicKey: Uint8Array, salt: Uint8Array): Uint8Array;
25
+ static calculatePreimage(checksumRoot: Uint8Array, publicKey: Uint8Array, salt: Uint8Array, useConcatenatedPreimage?: boolean): Uint8Array;
26
+ /**
27
+ * Non-malleable mining preimage: the 96-byte concatenation
28
+ * checksumRoot || publicKey || salt. The public key sits in its own segment,
29
+ * so a miner-chosen salt can neither cancel it nor be transformed to
30
+ * re-attribute another miner's solution. The SHA-1 proof-of-work over it is
31
+ * unchanged.
32
+ */
33
+ static concatenatePreimage(checksumRoot: Uint8Array, publicKey: Uint8Array, salt: Uint8Array): Uint8Array;
12
34
  /**
13
35
  * Count matching bits between two hashes
14
36
  */
@@ -16,7 +38,7 @@ export declare class EpochValidator {
16
38
  /**
17
39
  * Verify an epoch solution using IPreimage
18
40
  */
19
- static verifySolution(challenge: IChallengeSolution, log?: boolean): boolean;
41
+ static verifySolution(challenge: IChallengeSolution, log?: boolean, useConcatenatedPreimage?: boolean): boolean;
20
42
  /**
21
43
  * Get the mining target block for an epoch
22
44
  */
@@ -24,11 +46,11 @@ export declare class EpochValidator {
24
46
  /**
25
47
  * Validate epoch winner from raw data
26
48
  */
27
- static validateEpochWinner(epochData: RawChallenge): boolean;
49
+ static validateEpochWinner(epochData: RawChallenge, useConcatenatedPreimage?: boolean): boolean;
28
50
  /**
29
51
  * Validate epoch winner from Preimage instance
30
52
  */
31
- static validateChallengeSolution(challenge: IChallengeSolution): boolean;
53
+ static validateChallengeSolution(challenge: IChallengeSolution, useConcatenatedPreimage?: boolean): boolean;
32
54
  /**
33
55
  * Calculate solution hash from preimage components
34
56
  * @param targetChecksum The target checksum (32 bytes)
@@ -36,7 +58,7 @@ export declare class EpochValidator {
36
58
  * @param salt The salt buffer (32 bytes)
37
59
  * @returns The SHA-1 hash of the preimage
38
60
  */
39
- static calculateSolution(targetChecksum: Uint8Array, publicKey: Uint8Array, salt: Uint8Array): Uint8Array;
61
+ static calculateSolution(targetChecksum: Uint8Array, publicKey: Uint8Array, salt: Uint8Array, useConcatenatedPreimage?: boolean): Uint8Array;
40
62
  /**
41
63
  * Check if a solution meets the minimum difficulty requirement
42
64
  */
@@ -10,13 +10,30 @@ export class EpochValidator {
10
10
  return crypto.sha1(data);
11
11
  }
12
12
  /**
13
- * Calculate mining preimage
13
+ * Calculate mining preimage.
14
+ *
15
+ * When useConcatenatedPreimage is false the legacy XOR construction is used
16
+ * (checksumRoot ^ publicKey ^ salt). That construction is malleable: a
17
+ * miner can set salt == publicKey to force preimage == checksumRoot (max
18
+ * difficulty for free), or transform any solution to another key via
19
+ * salt' = publicKey' ^ publicKey ^ salt. When useConcatenatedPreimage is true
20
+ * the preimage is the 96-byte concatenation checksumRoot || publicKey || salt,
21
+ * which removes both (the SHA-1 proof-of-work over it is unchanged).
22
+ *
23
+ * The switch is a consensus rule: callers pass useConcatenatedPreimage =
24
+ * (epochBlockHeight >= activationHeight) using the activation height for the
25
+ * network they are on (opnet-node exposes it as
26
+ * consensusEpochPatches.PREIMAGE_CONCAT_PATCH_BLOCK_HEIGHT). The value MUST be
27
+ * identical across opnet-node, this library and the mining pool.
14
28
  */
15
- static calculatePreimage(checksumRoot, publicKey, salt) {
29
+ static calculatePreimage(checksumRoot, publicKey, salt, useConcatenatedPreimage = false) {
16
30
  // Ensure all are 32 bytes
17
31
  if (checksumRoot.length !== 32 || publicKey.length !== 32 || salt.length !== 32) {
18
32
  throw new Error('All inputs must be 32 bytes');
19
33
  }
34
+ if (useConcatenatedPreimage) {
35
+ return this.concatenatePreimage(checksumRoot, publicKey, salt);
36
+ }
20
37
  const preimage = new Uint8Array(32);
21
38
  for (let i = 0; i < 32; i++) {
22
39
  preimage[i] =
@@ -24,6 +41,23 @@ export class EpochValidator {
24
41
  }
25
42
  return preimage;
26
43
  }
44
+ /**
45
+ * Non-malleable mining preimage: the 96-byte concatenation
46
+ * checksumRoot || publicKey || salt. The public key sits in its own segment,
47
+ * so a miner-chosen salt can neither cancel it nor be transformed to
48
+ * re-attribute another miner's solution. The SHA-1 proof-of-work over it is
49
+ * unchanged.
50
+ */
51
+ static concatenatePreimage(checksumRoot, publicKey, salt) {
52
+ if (checksumRoot.length !== 32 || publicKey.length !== 32 || salt.length !== 32) {
53
+ throw new Error('All inputs must be 32 bytes');
54
+ }
55
+ const message = new Uint8Array(96);
56
+ message.set(checksumRoot, 0);
57
+ message.set(publicKey, 32);
58
+ message.set(salt, 64);
59
+ return message;
60
+ }
27
61
  /**
28
62
  * Count matching bits between two hashes
29
63
  */
@@ -56,10 +90,10 @@ export class EpochValidator {
56
90
  /**
57
91
  * Verify an epoch solution using IPreimage
58
92
  */
59
- static verifySolution(challenge, log = false) {
93
+ static verifySolution(challenge, log = false, useConcatenatedPreimage = false) {
60
94
  try {
61
95
  const verification = challenge.verification;
62
- const calculatedPreimage = this.calculatePreimage(verification.targetChecksum, challenge.publicKey.toBuffer(), challenge.salt);
96
+ const calculatedPreimage = this.calculatePreimage(verification.targetChecksum, challenge.publicKey.toBuffer(), challenge.salt, useConcatenatedPreimage);
63
97
  const computedSolution = this.sha1(calculatedPreimage);
64
98
  if (!equals(computedSolution, challenge.solution)) {
65
99
  return false;
@@ -92,7 +126,7 @@ export class EpochValidator {
92
126
  /**
93
127
  * Validate epoch winner from raw data
94
128
  */
95
- static validateEpochWinner(epochData) {
129
+ static validateEpochWinner(epochData, useConcatenatedPreimage = false) {
96
130
  try {
97
131
  const epochNumber = BigInt(epochData.epochNumber);
98
132
  const publicKey = Address.fromString(epochData.mldsaPublicKey, epochData.legacyPublicKey);
@@ -108,7 +142,7 @@ export class EpochValidator {
108
142
  endBlock: BigInt(epochData.verification.endBlock),
109
143
  proofs: Object.freeze(epochData.verification.proofs.map((p) => stringToBuffer(p))),
110
144
  };
111
- const calculatedPreimage = this.calculatePreimage(verification.targetChecksum, publicKey.toBuffer(), salt);
145
+ const calculatedPreimage = this.calculatePreimage(verification.targetChecksum, publicKey.toBuffer(), salt, useConcatenatedPreimage);
112
146
  const computedSolution = this.sha1(calculatedPreimage);
113
147
  if (!equals(computedSolution, solution)) {
114
148
  return false;
@@ -129,8 +163,8 @@ export class EpochValidator {
129
163
  /**
130
164
  * Validate epoch winner from Preimage instance
131
165
  */
132
- static validateChallengeSolution(challenge) {
133
- return this.verifySolution(challenge);
166
+ static validateChallengeSolution(challenge, useConcatenatedPreimage = false) {
167
+ return this.verifySolution(challenge, false, useConcatenatedPreimage);
134
168
  }
135
169
  /**
136
170
  * Calculate solution hash from preimage components
@@ -139,8 +173,8 @@ export class EpochValidator {
139
173
  * @param salt The salt buffer (32 bytes)
140
174
  * @returns The SHA-1 hash of the preimage
141
175
  */
142
- static calculateSolution(targetChecksum, publicKey, salt) {
143
- const preimage = this.calculatePreimage(targetChecksum, publicKey, salt);
176
+ static calculateSolution(targetChecksum, publicKey, salt, useConcatenatedPreimage = false) {
177
+ const preimage = this.calculatePreimage(targetChecksum, publicKey, salt, useConcatenatedPreimage);
144
178
  return this.sha1(preimage);
145
179
  }
146
180
  /**
@@ -744,15 +744,31 @@ export class TransactionBuilder extends TweakedTransaction {
744
744
  generateMLDSALinkRequest(parameters, features) {
745
745
  const mldsaSigner = this.mldsaSigner;
746
746
  const legacySignature = this.generateLegacySignature();
747
+ // Reveal by default. The legacy Schnorr signature only proves ownership of
748
+ // the Bitcoin key being linked; it says nothing about the claimed
749
+ // hashedPublicKey. Without the ML-DSA public key and a signature over it,
750
+ // a link request carries no proof that the sender knows a preimage for
751
+ // that hash, so a node cannot safely create a NEW identity from it.
752
+ //
753
+ // Defaulting to false made every caller responsible for remembering, and
754
+ // callers do forget: opwallet never set this anywhere, and its deployment
755
+ // screen calls signDeployment() directly, bypassing any controller-level
756
+ // fix. Revealing when it was not needed only costs bytes (ML-DSA-44:
757
+ // 1312-byte key + 2420-byte signature); not revealing when it was needed
758
+ // gets the transaction rejected.
759
+ //
760
+ // Callers that KNOW the key is already linked on-chain should pass false
761
+ // explicitly to avoid paying for the reveal on every interaction.
762
+ const shouldReveal = parameters.revealMLDSAPublicKey ?? true;
747
763
  let mldsaSignature = null;
748
- if (parameters.revealMLDSAPublicKey) {
764
+ if (shouldReveal) {
749
765
  mldsaSignature = this.generateMLDSASignature();
750
766
  }
751
767
  const mldsaRequest = {
752
768
  priority: FeaturePriority.MLDSA_LINK_PUBKEY,
753
769
  opcode: Features.MLDSA_LINK_PUBKEY,
754
770
  data: {
755
- verifyRequest: !!parameters.revealMLDSAPublicKey,
771
+ verifyRequest: shouldReveal,
756
772
  publicKey: mldsaSigner.publicKey,
757
773
  hashedPublicKey: this.hashedPublicKey,
758
774
  level: getLevelFromPublicKeyLength(mldsaSigner.publicKey.length),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@btc-vision/transaction",
3
3
  "type": "module",
4
- "version": "1.8.7",
4
+ "version": "1.8.9",
5
5
  "author": "BlobMaster41",
6
6
  "description": "OPNet transaction library allows you to create and sign transactions for the OPNet network.",
7
7
  "engines": {
@@ -14,18 +14,37 @@ export class EpochValidator {
14
14
  }
15
15
 
16
16
  /**
17
- * Calculate mining preimage
17
+ * Calculate mining preimage.
18
+ *
19
+ * When useConcatenatedPreimage is false the legacy XOR construction is used
20
+ * (checksumRoot ^ publicKey ^ salt). That construction is malleable: a
21
+ * miner can set salt == publicKey to force preimage == checksumRoot (max
22
+ * difficulty for free), or transform any solution to another key via
23
+ * salt' = publicKey' ^ publicKey ^ salt. When useConcatenatedPreimage is true
24
+ * the preimage is the 96-byte concatenation checksumRoot || publicKey || salt,
25
+ * which removes both (the SHA-1 proof-of-work over it is unchanged).
26
+ *
27
+ * The switch is a consensus rule: callers pass useConcatenatedPreimage =
28
+ * (epochBlockHeight >= activationHeight) using the activation height for the
29
+ * network they are on (opnet-node exposes it as
30
+ * consensusEpochPatches.PREIMAGE_CONCAT_PATCH_BLOCK_HEIGHT). The value MUST be
31
+ * identical across opnet-node, this library and the mining pool.
18
32
  */
19
33
  public static calculatePreimage(
20
34
  checksumRoot: Uint8Array,
21
35
  publicKey: Uint8Array,
22
36
  salt: Uint8Array,
37
+ useConcatenatedPreimage: boolean = false,
23
38
  ): Uint8Array {
24
39
  // Ensure all are 32 bytes
25
40
  if (checksumRoot.length !== 32 || publicKey.length !== 32 || salt.length !== 32) {
26
41
  throw new Error('All inputs must be 32 bytes');
27
42
  }
28
43
 
44
+ if (useConcatenatedPreimage) {
45
+ return this.concatenatePreimage(checksumRoot, publicKey, salt);
46
+ }
47
+
29
48
  const preimage = new Uint8Array(32);
30
49
  for (let i = 0; i < 32; i++) {
31
50
  preimage[i] =
@@ -35,6 +54,30 @@ export class EpochValidator {
35
54
  return preimage;
36
55
  }
37
56
 
57
+ /**
58
+ * Non-malleable mining preimage: the 96-byte concatenation
59
+ * checksumRoot || publicKey || salt. The public key sits in its own segment,
60
+ * so a miner-chosen salt can neither cancel it nor be transformed to
61
+ * re-attribute another miner's solution. The SHA-1 proof-of-work over it is
62
+ * unchanged.
63
+ */
64
+ public static concatenatePreimage(
65
+ checksumRoot: Uint8Array,
66
+ publicKey: Uint8Array,
67
+ salt: Uint8Array,
68
+ ): Uint8Array {
69
+ if (checksumRoot.length !== 32 || publicKey.length !== 32 || salt.length !== 32) {
70
+ throw new Error('All inputs must be 32 bytes');
71
+ }
72
+
73
+ const message = new Uint8Array(96);
74
+ message.set(checksumRoot, 0);
75
+ message.set(publicKey, 32);
76
+ message.set(salt, 64);
77
+
78
+ return message;
79
+ }
80
+
38
81
  /**
39
82
  * Count matching bits between two hashes
40
83
  */
@@ -69,13 +112,18 @@ export class EpochValidator {
69
112
  /**
70
113
  * Verify an epoch solution using IPreimage
71
114
  */
72
- public static verifySolution(challenge: IChallengeSolution, log: boolean = false): boolean {
115
+ public static verifySolution(
116
+ challenge: IChallengeSolution,
117
+ log: boolean = false,
118
+ useConcatenatedPreimage: boolean = false,
119
+ ): boolean {
73
120
  try {
74
121
  const verification = challenge.verification;
75
122
  const calculatedPreimage = this.calculatePreimage(
76
123
  verification.targetChecksum,
77
124
  challenge.publicKey.toBuffer(),
78
125
  challenge.salt,
126
+ useConcatenatedPreimage,
79
127
  );
80
128
 
81
129
  const computedSolution = this.sha1(calculatedPreimage);
@@ -118,7 +166,10 @@ export class EpochValidator {
118
166
  /**
119
167
  * Validate epoch winner from raw data
120
168
  */
121
- public static validateEpochWinner(epochData: RawChallenge): boolean {
169
+ public static validateEpochWinner(
170
+ epochData: RawChallenge,
171
+ useConcatenatedPreimage: boolean = false,
172
+ ): boolean {
122
173
  try {
123
174
  const epochNumber = BigInt(epochData.epochNumber);
124
175
  const publicKey = Address.fromString(
@@ -143,6 +194,7 @@ export class EpochValidator {
143
194
  verification.targetChecksum,
144
195
  publicKey.toBuffer(),
145
196
  salt,
197
+ useConcatenatedPreimage,
146
198
  );
147
199
 
148
200
  const computedSolution = this.sha1(calculatedPreimage);
@@ -172,8 +224,11 @@ export class EpochValidator {
172
224
  /**
173
225
  * Validate epoch winner from Preimage instance
174
226
  */
175
- public static validateChallengeSolution(challenge: IChallengeSolution): boolean {
176
- return this.verifySolution(challenge);
227
+ public static validateChallengeSolution(
228
+ challenge: IChallengeSolution,
229
+ useConcatenatedPreimage: boolean = false,
230
+ ): boolean {
231
+ return this.verifySolution(challenge, false, useConcatenatedPreimage);
177
232
  }
178
233
 
179
234
  /**
@@ -187,8 +242,14 @@ export class EpochValidator {
187
242
  targetChecksum: Uint8Array,
188
243
  publicKey: Uint8Array,
189
244
  salt: Uint8Array,
245
+ useConcatenatedPreimage: boolean = false,
190
246
  ): Uint8Array {
191
- const preimage = this.calculatePreimage(targetChecksum, publicKey, salt);
247
+ const preimage = this.calculatePreimage(
248
+ targetChecksum,
249
+ publicKey,
250
+ salt,
251
+ useConcatenatedPreimage,
252
+ );
192
253
  return this.sha1(preimage);
193
254
  }
194
255
 
@@ -983,8 +983,25 @@ export abstract class TransactionBuilder<T extends TransactionType> extends Twea
983
983
  const mldsaSigner = this.mldsaSigner;
984
984
  const legacySignature = this.generateLegacySignature();
985
985
 
986
+ // Reveal by default. The legacy Schnorr signature only proves ownership of
987
+ // the Bitcoin key being linked; it says nothing about the claimed
988
+ // hashedPublicKey. Without the ML-DSA public key and a signature over it,
989
+ // a link request carries no proof that the sender knows a preimage for
990
+ // that hash, so a node cannot safely create a NEW identity from it.
991
+ //
992
+ // Defaulting to false made every caller responsible for remembering, and
993
+ // callers do forget: opwallet never set this anywhere, and its deployment
994
+ // screen calls signDeployment() directly, bypassing any controller-level
995
+ // fix. Revealing when it was not needed only costs bytes (ML-DSA-44:
996
+ // 1312-byte key + 2420-byte signature); not revealing when it was needed
997
+ // gets the transaction rejected.
998
+ //
999
+ // Callers that KNOW the key is already linked on-chain should pass false
1000
+ // explicitly to avoid paying for the reveal on every interaction.
1001
+ const shouldReveal = parameters.revealMLDSAPublicKey ?? true;
1002
+
986
1003
  let mldsaSignature: Uint8Array | null = null;
987
- if (parameters.revealMLDSAPublicKey) {
1004
+ if (shouldReveal) {
988
1005
  mldsaSignature = this.generateMLDSASignature();
989
1006
  }
990
1007
 
@@ -992,7 +1009,7 @@ export abstract class TransactionBuilder<T extends TransactionType> extends Twea
992
1009
  priority: FeaturePriority.MLDSA_LINK_PUBKEY,
993
1010
  opcode: Features.MLDSA_LINK_PUBKEY,
994
1011
  data: {
995
- verifyRequest: !!parameters.revealMLDSAPublicKey,
1012
+ verifyRequest: shouldReveal,
996
1013
  publicKey: mldsaSigner.publicKey,
997
1014
  hashedPublicKey: this.hashedPublicKey,
998
1015
  level: getLevelFromPublicKeyLength(mldsaSigner.publicKey.length),