Skip to content

Commit a770d52

Browse files
committed
feat(core): extract SignerMultisigCkbBase for Omnilock multisig
Extract shared multisig logic into abstract SignerMultisigCkbBase with encodeWitnessLock/decodeWitnessLock template methods. CKB multisig and Omnilock multisig are thin subclasses differing only in witness encoding. MultisigCkbWitness entity moved to its own module. OmniLockWitnessLock molecule encoding added as standalone utility.
1 parent a137473 commit a770d52

8 files changed

Lines changed: 913 additions & 526 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1+
export * from "./multisigCkbWitness.js";
2+
export * from "./omniLockWitnessLock.js";
13
export * from "./secp256k1Signing.js";
24
export * from "./signerCkbPrivateKey.js";
35
export * from "./signerCkbPublicKey.js";
46
export * from "./signerCkbScriptReadonly.js";
7+
export * from "./signerMultisigCkbBase.js";
58
export * from "./signerMultisigCkbPrivateKey.js";
69
export * from "./signerMultisigCkbReadonly.js";
10+
export * from "./signerMultisigOmniLockPrivateKey.js";
11+
export * from "./signerMultisigOmniLockReadonly.js";
712
export * from "./verifyJoyId.js";
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import { Bytes, bytesConcat, bytesFrom } from "../../bytes/index.js";
2+
import { Since, SinceLike } from "../../ckb/index.js";
3+
import { codec, Entity } from "../../codec/index.js";
4+
import { HASH_CKB_SHORT_LENGTH, hashCkb } from "../../hasher/index.js";
5+
import { Hex, hexFrom, HexLike } from "../../hex/index.js";
6+
import { numFrom, NumLike, numToBytes } from "../../num/index.js";
7+
import { SECP256K1_SIGNATURE_LENGTH } from "./secp256k1Signing.js";
8+
9+
export type MultisigCkbWitnessLike = (
10+
| {
11+
publicKeyHashes: HexLike[];
12+
publicKeys?: undefined | null;
13+
}
14+
| {
15+
publicKeyHashes?: undefined | null;
16+
publicKeys: HexLike[];
17+
}
18+
) & {
19+
threshold: NumLike;
20+
mustMatch?: NumLike | null;
21+
signatures?: HexLike[] | null;
22+
};
23+
24+
/**
25+
* A class representing multisig information, holding information ingredients and containing utilities.
26+
* @public
27+
*/
28+
@codec({
29+
encode: (encodable: MultisigCkbWitness) => {
30+
const { publicKeyHashes, threshold, mustMatch, signatures } =
31+
MultisigCkbWitness.from(encodable);
32+
33+
if (
34+
signatures.some((s) => s.length !== SECP256K1_SIGNATURE_LENGTH * 2 + 2)
35+
) {
36+
throw Error("MultisigCkbWitness: invalid signature length");
37+
}
38+
if (
39+
publicKeyHashes.some((s) => s.length !== HASH_CKB_SHORT_LENGTH * 2 + 2)
40+
) {
41+
throw Error("MultisigCkbWitness: invalid public key hash length");
42+
}
43+
44+
return bytesConcat(
45+
"0x00",
46+
numToBytes(mustMatch ?? 0),
47+
numToBytes(threshold),
48+
numToBytes(publicKeyHashes.length),
49+
...publicKeyHashes,
50+
...signatures,
51+
);
52+
},
53+
decode: (raw: Bytes) => {
54+
const [
55+
_reserved,
56+
mustMatch,
57+
threshold,
58+
publicKeyHashesLength,
59+
...rawKeyAndSignatures
60+
] = raw;
61+
62+
if (
63+
rawKeyAndSignatures.length <
64+
publicKeyHashesLength * HASH_CKB_SHORT_LENGTH
65+
) {
66+
throw Error("MultisigCkbWitness: invalid public key hashes length");
67+
}
68+
69+
const signatures = rawKeyAndSignatures.slice(
70+
publicKeyHashesLength * HASH_CKB_SHORT_LENGTH,
71+
);
72+
73+
return MultisigCkbWitness.from({
74+
publicKeyHashes: Array.from(new Array(publicKeyHashesLength), (_, i) =>
75+
hexFrom(
76+
rawKeyAndSignatures.slice(
77+
i * HASH_CKB_SHORT_LENGTH,
78+
(i + 1) * HASH_CKB_SHORT_LENGTH,
79+
),
80+
),
81+
),
82+
threshold: numFrom(threshold),
83+
mustMatch: numFrom(mustMatch),
84+
signatures: Array.from(
85+
new Array(Math.floor(signatures.length / SECP256K1_SIGNATURE_LENGTH)),
86+
(_, i) =>
87+
hexFrom(
88+
signatures.slice(
89+
i * SECP256K1_SIGNATURE_LENGTH,
90+
(i + 1) * SECP256K1_SIGNATURE_LENGTH,
91+
),
92+
),
93+
),
94+
});
95+
},
96+
})
97+
export class MultisigCkbWitness extends Entity.Base<
98+
MultisigCkbWitnessLike,
99+
MultisigCkbWitness
100+
>() {
101+
/**
102+
* @param publicKeyHashes - The public key hashes.
103+
* @param threshold - The threshold.
104+
* @param mustMatch - The number of signatures that must match.
105+
* @param signatures - The signatures.
106+
*/
107+
constructor(
108+
public publicKeyHashes: Hex[],
109+
public threshold: number,
110+
public mustMatch: number,
111+
public signatures: Hex[],
112+
) {
113+
super();
114+
115+
const keysLength = publicKeyHashes.length;
116+
117+
if (threshold <= 0 || threshold > keysLength) {
118+
throw new Error(
119+
"threshold should be in range from 1 to public keys length",
120+
);
121+
}
122+
if (mustMatch < 0 || mustMatch > Math.min(keysLength, threshold)) {
123+
throw new Error(
124+
"mustMatch should be in range from 0 to min(public keys length, threshold)",
125+
);
126+
}
127+
if (keysLength > 255) {
128+
throw new Error("public keys length should be less than 256");
129+
}
130+
}
131+
132+
/**
133+
* Create a MultisigCkbWitness from a MultisigCkbWitnessLike.
134+
*
135+
* @param witness - The witness like object.
136+
* @returns The MultisigCkbWitness.
137+
*/
138+
static from(witness: MultisigCkbWitnessLike): MultisigCkbWitness {
139+
const publicKeyHashes = (() => {
140+
if (witness.publicKeyHashes) {
141+
return witness.publicKeyHashes;
142+
}
143+
return witness.publicKeys.map((k) => hashCkb(k).slice(0, 42));
144+
})();
145+
146+
return new MultisigCkbWitness(
147+
publicKeyHashes.map(hexFrom),
148+
Number(numFrom(witness.threshold)),
149+
Number(numFrom(witness.mustMatch ?? 0)),
150+
witness.signatures?.map(hexFrom) ?? [],
151+
);
152+
}
153+
154+
/**
155+
* Get the script args of the multisig script.
156+
*
157+
* @param since - The since value.
158+
* @returns The script args.
159+
*/
160+
scriptArgs(since?: SinceLike | null): Bytes {
161+
const hash = hashCkb(this.toBytes()).slice(0, 42);
162+
163+
if (since != null) {
164+
return bytesConcat(hash, Since.from(since).toBytes());
165+
}
166+
167+
return bytesFrom(hash);
168+
}
169+
170+
/**
171+
* Check if the multisig info is equal to another.
172+
*
173+
* @param otherLike - The other multisig info.
174+
* @returns True if the multisig info is equal, false otherwise.
175+
*/
176+
eqInfo(otherLike: MultisigCkbWitnessLike): boolean {
177+
const other = MultisigCkbWitness.from(otherLike);
178+
return (
179+
this.publicKeyHashes.length === other.publicKeyHashes.length &&
180+
this.publicKeyHashes.every((h, i) => h === other.publicKeyHashes[i]) &&
181+
this.threshold === other.threshold &&
182+
this.mustMatch === other.mustMatch
183+
);
184+
}
185+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/**
2+
* OmniLockWitnessLock molecule table encoding/decoding.
3+
*
4+
* table OmniLockWitnessLock {
5+
* signature: BytesOpt,
6+
* omni_identity: IdentityOpt,
7+
* preimage: BytesOpt,
8+
* }
9+
*
10+
* For bridge use (auth modes 0x00-0x06, 0xFC), only the signature field is
11+
* populated. omni_identity and preimage are None (zero-length).
12+
*/
13+
14+
import { Bytes, bytesConcat, bytesFrom } from "../../bytes/index.js";
15+
import { Hex, hexFrom } from "../../hex/index.js";
16+
import { numToBytes } from "../../num/index.js";
17+
18+
const NUM_FIELDS = 3;
19+
const HEADER_BYTES = (1 + NUM_FIELDS) * 4; // full_size + 3 offsets = 16
20+
21+
/**
22+
* Encode a signature into the OmniLockWitnessLock molecule table format.
23+
*
24+
* The result is suitable for WitnessArgs.lock when the Omnilock cell uses
25+
* a signature-based auth mode (secp256k1, Ethereum, Bitcoin, CKB multisig,
26+
* owner lock, etc.) and does not use the administrator (omni_identity) or
27+
* preimage fields.
28+
*
29+
* @param signature - The raw signature bytes.
30+
* @returns The encoded OmniLockWitnessLock bytes.
31+
* @public
32+
*/
33+
export function encodeOmniLockWitnessLock(signature: Bytes): Bytes {
34+
const fullSize = HEADER_BYTES + 4 + signature.length;
35+
return bytesFrom(
36+
bytesConcat(
37+
numToBytes(fullSize, 4), // full_size
38+
numToBytes(HEADER_BYTES, 4), // offset[0]: signature starts here
39+
numToBytes(fullSize, 4), // offset[1]: omni_identity (absent, at end)
40+
numToBytes(fullSize, 4), // offset[2]: preimage (absent, at end)
41+
numToBytes(signature.length, 4), // Bytes item count
42+
signature,
43+
),
44+
);
45+
}
46+
47+
/**
48+
* Encode a signature into OmniLockWitnessLock and return as Hex.
49+
*
50+
* @param signature - The raw signature bytes.
51+
* @returns The encoded OmniLockWitnessLock hex string.
52+
* @public
53+
*/
54+
export function encodeOmniLockWitnessLockToHex(signature: Bytes): Hex {
55+
return hexFrom(encodeOmniLockWitnessLock(signature));
56+
}
57+
58+
/**
59+
* Decode the signature from an OmniLockWitnessLock molecule table.
60+
*
61+
* @param data - The full OmniLockWitnessLock bytes.
62+
* @returns The decoded signature, or undefined if the signature field is absent.
63+
* @public
64+
*/
65+
export function decodeOmniLockWitnessLock(data: Bytes): Bytes | undefined {
66+
if (data.length < HEADER_BYTES) {
67+
throw new Error("OmniLockWitnessLock: data too short for header");
68+
}
69+
70+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
71+
const fullSize = view.getUint32(0, true);
72+
if (fullSize !== data.length) {
73+
throw new Error("OmniLockWitnessLock: full_size mismatch");
74+
}
75+
76+
const signatureOffset = view.getUint32(4, true);
77+
const omniIdentityOffset = view.getUint32(8, true);
78+
79+
// signature field is present if offset[0] < offset[1]
80+
if (signatureOffset >= omniIdentityOffset) {
81+
return undefined;
82+
}
83+
84+
const sigBytesCount = view.getUint32(signatureOffset, true);
85+
const sigStart = signatureOffset + 4;
86+
if (sigStart + sigBytesCount > data.length) {
87+
throw new Error("OmniLockWitnessLock: signature exceeds data bounds");
88+
}
89+
90+
return data.slice(sigStart, sigStart + sigBytesCount);
91+
}
92+
93+
/**
94+
* Compute the WitnessArgs.lock byte length for an OmniLockWitnessLock
95+
* containing a signature of the given length.
96+
*
97+
* Used to prepare the witness placeholder before computing sighash_all.
98+
*
99+
* @param signatureLength - The raw signature byte length.
100+
* @returns The total OmniLockWitnessLock byte length.
101+
* @public
102+
*/
103+
export function omniLockWitnessLockLength(signatureLength: number): number {
104+
return HEADER_BYTES + 4 + signatureLength;
105+
}

0 commit comments

Comments
 (0)