Skip to content

Commit bdb62a2

Browse files
committed
ed updates complete for now, updated passing test suite, padded byte on sig for trx, built-in digest prep bytes depending on keytype
1 parent 2d2a7ab commit bdb62a2

File tree

15 files changed

+420
-155
lines changed

15 files changed

+420
-155
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@wireio/core",
33
"description": "Library for working with Wire powered blockchains.",
4-
"version": "0.1.8",
4+
"version": "0.2.0",
55
"homepage": "https://github.com/Wire-Network/sdk-core",
66
"license": "FSL-1.1-Apache-2.0",
77
"main": "lib/core.js",

src/api/client.ts

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -265,25 +265,22 @@ export class APIClient {
265265
async buildSignedTransaction(action: AnyAction | AnyAction[], opts?: TransactionExtraOptions): Promise<SignedTransaction> {
266266
if (!this.signer) throw new Error('No signer function provided in APIClient options');
267267

268-
const keyType = opts && opts.key_type ? opts.key_type : this.signer.keyType;
268+
const keyType = this.signer.keyType;
269269
const actions = await this.anyToAction(action);
270270
const info = await this.v1.chain.get_info();
271271
const header = info.getTransactionHeader();
272272
const transaction = Transaction.from({
273273
...header, actions,
274-
context_free_actions: (opts && opts.context_free_actions) ? opts.context_free_actions : [],
275-
transaction_extensions: [{ type: 1, data: [] }]
274+
context_free_actions: (opts && opts.context_free_actions) ? opts.context_free_actions : []
276275
});
277-
const msgDigest = transaction.signingDigest(info.chain_id);
278-
let msgBytes: Uint8Array = msgDigest.array;
279-
280-
// Handle keytype specific digest preparation
281-
switch (keyType) {
282-
case KeyType.EM: // Prefix with 0x and arrayify
283-
msgBytes = ethers.utils.arrayify('0x' + msgDigest.hexString);
284-
break;
276+
277+
if (keyType === KeyType.ED) {
278+
const pubKey = opts && opts.pub_key;
279+
if (!pubKey) throw new Error('ED signature requires a pubKey to be passed in TransactionExtraOptions');
280+
transaction.extPubKey(pubKey);
285281
}
286282

283+
const { msgBytes } = transaction.signingDigest(info.chain_id, keyType);
287284
const sigBytes = await this.signer.sign(msgBytes).catch(err => { throw new Error(err) });
288285
const signature = Signature.fromRaw(sigBytes, keyType);
289286
return SignedTransaction.from({ ...transaction, signatures: [signature] });

src/api/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
export * as v1 from './v1/types';
22
export * as v2 from './v2/types';
33

4-
import { ActionType, KeyType, NameType } from '../chain';
4+
import { ActionType, KeyType, NameType, PublicKeyType } from '../chain';
55
import { TableIndexType, TableIndexTypes } from './v1/types';
66
export interface GetRowsOptions {
77
contract: NameType;
@@ -26,5 +26,5 @@ export interface GetRowsOptions {
2626
export type TransactionExtraOptions = {
2727
wait_final?: boolean;
2828
context_free_actions?: ActionType[];
29-
key_type? : KeyType
29+
pub_key?: PublicKeyType
3030
};

src/chain/public-key.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,12 @@ export class PublicKey implements ABISerializableObject {
101101

102102
/** Return key in modern Antelope/EOSIO format (`PUB_<type>_<base58data>`) */
103103
toString() {
104+
// Ensure the key is compressed
105+
if ((this.type === KeyType.K1 || this.type === KeyType.R1 || this.type === KeyType.EM) &&
106+
this.data.array.length !== 33) {
107+
throw new Error(`Expected 33-byte compressed key for ${this.type}, got ${this.data.array.length}`);
108+
}
109+
104110
return `PUB_${this.type}_${Base58.encodeRipemd160Check(this.data, this.type)}`;
105111
}
106112

src/chain/signature.ts

Lines changed: 52 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,12 @@ export type SignatureParts = {
2727
export class Signature implements ABISerializableObject {
2828
static abiName = 'signature';
2929

30-
/** Type, e.g. `K1` or `ED` */
3130
type: KeyType;
32-
/** Signature data. */
3331
data: Bytes;
3432

3533
/** Create Signature object from representing types. */
3634
static from(value: SignatureType): Signature {
37-
if (isInstanceOf(value, Signature)) {
38-
return value;
39-
}
35+
if (isInstanceOf(value, Signature)) return value;
4036

4137
if (typeof value === 'object' && 'r' in value && 's' in value) {
4238
// ED25519 is pure 64-byte r||s
@@ -79,16 +75,14 @@ export class Signature implements ABISerializableObject {
7975
}
8076

8177
const type = KeyType.from(parts[1]);
82-
// 65 for all wire-format curves (we now pad ED to 65 bytes)
83-
const size =
84-
(type === KeyType.K1 ||
85-
type === KeyType.R1 ||
86-
type === KeyType.EM ||
87-
type === KeyType.ED)
78+
// 65 for ECDSA, 64 for ED
79+
const length =
80+
type === KeyType.K1 || type === KeyType.R1 || type === KeyType.EM
8881
? 65
89-
: undefined;
90-
91-
const data = Base58.decodeRipemd160Check(parts[2], size, type);
82+
: type === KeyType.ED
83+
? 64
84+
: undefined;
85+
const data = Base58.decodeRipemd160Check(parts[2], length, type);
9286
return new Signature(type, data);
9387
}
9488

@@ -107,8 +101,9 @@ export class Signature implements ABISerializableObject {
107101
return new Signature(KeyType.WA, data);
108102
}
109103

110-
const length = 65;
111-
return new Signature(type, new Bytes(decoder.readArray(length)));
104+
const length = (type === KeyType.ED) ? 64 : 65;
105+
const bytes = decoder.readArray(length);
106+
return new Signature(type, new Bytes(bytes));
112107
}
113108

114109
/**
@@ -133,11 +128,7 @@ export class Signature implements ABISerializableObject {
133128
// ED25519: the raw is already [r‖s]
134129
if (type === KeyType.ED) {
135130
if (raw.length !== 64) throw new Error(`ED raw sig must be 64 bytes, got ${raw.length}`);
136-
// ► pad to 65 bytes with a zero at the end:
137-
const wire = new Uint8Array(65);
138-
wire.set(raw, 0);
139-
wire[64] = 0;
140-
return new Signature(type, new Bytes(wire));
131+
return new Signature(type, new Bytes(raw));
141132
}
142133

143134
// ECDSA/EIP-191: raw should be 65 bytes [r‖s‖v]
@@ -177,35 +168,8 @@ export class Signature implements ABISerializableObject {
177168
* - ED: 64 bytes `[r(32)‖s(32)]`
178169
*/
179170
constructor(type: KeyType, data: Bytes | Uint8Array) {
180-
let wire: Uint8Array;
181-
182-
if (type === KeyType.ED) {
183-
const arr = data instanceof Bytes ? data.array : data;
184-
185-
if (arr.length === 64) {
186-
// pad to 65 so toString() and from() agree
187-
wire = new Uint8Array(65);
188-
wire.set(arr, 0);
189-
wire[64] = 0;
190-
} else if (arr.length === 65) {
191-
// already padded
192-
wire = arr;
193-
} else {
194-
throw new Error(`ED signature must be 64 or 65 bytes, got ${arr.length}`);
195-
}
196-
} else {
197-
// everything else: expect exactly 65 bytes already in wire-format
198-
const arr = data instanceof Bytes ? data.array : data;
199-
200-
if (arr.length !== 65) {
201-
throw new Error(`Expected 65-byte wire format for ${type}, got ${arr.length}`);
202-
}
203-
204-
wire = arr;
205-
}
206-
207171
this.type = type;
208-
this.data = new Bytes(wire);
172+
this.data = data instanceof Bytes ? data : new Bytes(data);
209173
}
210174

211175
equals(other: SignatureType): boolean {
@@ -251,11 +215,9 @@ export class Signature implements ABISerializableObject {
251215
const rawMsg = Bytes.from(message).array;
252216

253217
switch (this.type) {
254-
case KeyType.ED: {
255-
// ED25519: storage is [r||s||0], TweetNaCl needs exactly 64 bytes [r||s] - strip padded 0
256-
const sig64 = this.data.array.subarray(0, 64);
257-
return Crypto.verify(sig64, rawMsg, publicKey.data.array, this.type);
258-
}
218+
case KeyType.ED:
219+
// ED25519: raw `[r‖s]`
220+
return Crypto.verify(this.data.array, rawMsg, publicKey.data.array, this.type);
259221

260222
case KeyType.EM: {
261223
// 1) unwrap wire [vWire‖r‖s]
@@ -306,7 +268,9 @@ export class Signature implements ABISerializableObject {
306268
break;
307269
}
308270

309-
case KeyType.K1 || KeyType.R1: {
271+
case KeyType.K1:
272+
// eslint-disable-next-line padding-line-between-statements, no-fallthrough
273+
case KeyType.R1: {
310274
// wire[0] = k1V + 31
311275
const k1V = wire[0] - 31; // 0 or 1
312276
raw = new Uint8Array(65);
@@ -343,4 +307,37 @@ export class Signature implements ABISerializableObject {
343307
toJSON(): string {
344308
return this.toString();
345309
}
310+
}
311+
312+
/**
313+
* Pads an ED25519 signature for transaction wire-format.
314+
* @param sig - The presumed 64 byte r|s signature to pad.
315+
* @returns The padded 65 byte signature used for wire transactions.
316+
*/
317+
export function padEdForTx(sig: Signature): Signature {
318+
if (sig.type !== KeyType.ED) return sig;
319+
const arr = sig.data.array;
320+
if (arr.length === 65) return sig;
321+
if (arr.length !== 64) throw new Error(`ED sig must be 64 or 65, got ${arr.length}`);
322+
const padded = new Uint8Array(65);
323+
padded.set(arr, 0);
324+
padded[64] = 0;
325+
326+
return new Signature(KeyType.ED, new Bytes(padded));
327+
}
328+
329+
/**
330+
* Strips the ED25519 padding from a signature.
331+
* @param sig - The signature to strip.
332+
* @returns The stripped signature.
333+
*/
334+
export function stripEdPad(sig: Signature): Signature {
335+
if (sig.type !== KeyType.ED) return sig;
336+
const a = sig.data.array;
337+
338+
if (a.length === 65 && a[64] === 0) {
339+
return new Signature(KeyType.ED, a.subarray(0, 64));
340+
}
341+
342+
return sig;
346343
}

src/chain/transaction.ts

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import pako from 'pako';
22

33
import {abiEncode} from '../serializer/encoder';
4-
import {Signature, SignatureType} from './signature';
4+
import {padEdForTx, Signature, SignatureType, stripEdPad} from './signature';
55
import {abiDecode} from '../serializer/decoder';
66

77
import {
@@ -13,8 +13,11 @@ import {
1313
BytesType,
1414
Checksum256,
1515
Checksum256Type,
16+
KeyType,
1617
Name,
1718
NameType,
19+
PublicKey,
20+
PublicKeyType,
1821
Struct,
1922
TimePointSec,
2023
TimePointType,
@@ -27,13 +30,18 @@ import {
2730
VarUInt,
2831
VarUIntType,
2932
} from '../';
33+
import { ethers } from 'ethers';
3034

3135
@Struct.type('transaction_extension')
3236
export class TransactionExtension extends Struct {
3337
@Struct.field('uint16') declare type: UInt16;
3438
@Struct.field('bytes') declare data: Bytes;
3539
}
3640

41+
export enum TransactionExtensionType {
42+
PubKey = 0x8000,
43+
// Add other extension types here as needed
44+
}
3745
export interface TransactionHeaderFields {
3846
/** The time at which a transaction expires. */
3947
expiration: TimePointType;
@@ -96,6 +104,11 @@ export interface AnyTransaction extends TransactionHeaderFields {
96104

97105
export type TransactionType = Transaction | TransactionFields;
98106

107+
export interface SigningDigest {
108+
msgDigest : Checksum256;
109+
msgBytes : Uint8Array; // Optionally formatted based on key type
110+
}
111+
99112
@Struct.type('transaction')
100113
export class Transaction extends TransactionHeader {
101114
/** The context free actions in the transaction. */
@@ -151,9 +164,35 @@ export class Transaction extends TransactionHeader {
151164
return Checksum256.hash(abiEncode({object: this}));
152165
}
153166

154-
signingDigest(chainId: Checksum256Type): Checksum256 {
167+
/**
168+
* Computes the signing digest and message bytes for this transaction.
169+
*
170+
* @param chainId - The chain ID to use for the signing digest.
171+
* @param keyType - (Optional) The key type of the signer. If provided, the message bytes (`msgBytes`)
172+
* are formatted according to the expected format for the given key type.
173+
* - For `KeyType.EM`, the digest is prefixed with `0x` and arrayified.
174+
* - For `KeyType.ED`, the digest's hex string is encoded as UTF-8 bytes.
175+
* - If not provided, the default digest bytes are used.
176+
* @returns An object containing the signing digest (`msgDigest`) and the formatted message bytes (`msgBytes`).
177+
*/
178+
signingDigest(chainId: Checksum256Type, keyType?: KeyType): SigningDigest {
155179
const data = this.signingData(chainId);
156-
return Checksum256.hash(data);
180+
const msgDigest = Checksum256.hash(data);
181+
const msgDigestHex = msgDigest.hexString.toLowerCase();
182+
let msgBytes: Uint8Array = msgDigest.array;
183+
184+
// Prepare custom msgBytes based on key type
185+
switch (keyType) {
186+
case KeyType.EM: // Prefix with 0x and arrayify
187+
msgBytes = ethers.utils.arrayify('0x' + msgDigestHex);
188+
break;
189+
190+
case KeyType.ED: // Encode as UTF-8 bytes for Phantom signing
191+
msgBytes = new TextEncoder().encode(msgDigestHex);
192+
break;
193+
}
194+
195+
return { msgDigest, msgBytes };
157196
}
158197

159198
signingData(chainId: Checksum256Type): Bytes {
@@ -162,6 +201,24 @@ export class Transaction extends TransactionHeader {
162201
data = data.appending(new Uint8Array(32));
163202
return data;
164203
}
204+
205+
extPubKey(pubKeys: PublicKeyType | PublicKeyType[]) {
206+
if (!Array.isArray(pubKeys)) pubKeys = [pubKeys];
207+
208+
for (const pkey of pubKeys) {
209+
const pubKey = PublicKey.from(pkey);
210+
const rawKey = pubKey.data.array;
211+
const keyTypeTag = KeyType.indexFor(pubKey.type);
212+
const tag = Uint8Array.from([keyTypeTag]);
213+
214+
const ext = TransactionExtension.from({
215+
type: TransactionExtensionType.PubKey,
216+
data: Buffer.concat([tag, rawKey]),
217+
});
218+
219+
this.transaction_extensions.push(ext);
220+
}
221+
}
165222
}
166223

167224
export interface SignedTransactionFields extends TransactionFields {
@@ -234,6 +291,9 @@ export class PackedTransaction extends Struct {
234291
}
235292

236293
static fromSigned(signed: SignedTransaction, compression: CompressionType = 1) {
294+
// Pad ED sigs to 65 bytes, return unmodified K1/R1/EM sigs
295+
const wireSigs = signed.signatures.map(padEdForTx);
296+
237297
// Encode data
238298
let packed_trx: Bytes = abiEncode({object: Transaction.from(signed)});
239299
let packed_context_free_data: Bytes = abiEncode({
@@ -256,7 +316,7 @@ export class PackedTransaction extends Struct {
256316

257317
return this.from({
258318
compression,
259-
signatures: signed.signatures,
319+
signatures: wireSigs,
260320
packed_context_free_data,
261321
packed_trx,
262322
}) as PackedTransaction;
@@ -286,11 +346,11 @@ export class PackedTransaction extends Struct {
286346
// TODO: decode context free data
287347
return SignedTransaction.from({
288348
...transaction,
289-
signatures: this.signatures,
349+
signatures: this.signatures.map(stripEdPad),
290350
});
291351
}
292352
}
293-
353+
294354
@Struct.type('transaction_receipt')
295355
export class TransactionReceipt extends Struct {
296356
@Struct.field('string') declare status: string;

0 commit comments

Comments
 (0)