Skip to content

Commit

Permalink
feat: Add non-sequential multi-sig support (and fix legacy multi-sig …
Browse files Browse the repository at this point in the history
…bugs) (#1710)

* feat: add non-sequential multi-sig support (interface and defaults may change in the future)

* refactor: follow-up missing builders

* test: add test vector

* test: add explicit oversign tests

* refactor: improve signer state for cur-sig-hash

* test: harden multi-sig tests

* refactor: update address param handling

* test: update tests for address param

* fix: update P2WSH compression detection

* refactor: rename public key order helper

* refactor: remove optional param

---------

Co-authored-by: janniks <[email protected]>
  • Loading branch information
janniks and janniks authored Jun 30, 2024
1 parent 2c57ea4 commit 879263c
Show file tree
Hide file tree
Showing 13 changed files with 698 additions and 128 deletions.
1 change: 1 addition & 0 deletions packages/common/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export enum PeerNetworkID {

/** @ignore internal */
export const PRIVATE_KEY_COMPRESSED_LENGTH = 33;
// todo: `next` make length consts more consistent in naming

/** @ignore internal */
export const PRIVATE_KEY_UNCOMPRESSED_LENGTH = 32;
Expand Down
49 changes: 38 additions & 11 deletions packages/transactions/src/authorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,26 @@ export function createMultiSigSpendingCondition(
};
}

/** @internal */
export function isSingleSig(
condition: SpendingConditionOpts
): condition is SingleSigSpendingConditionOpts {
return 'signature' in condition;
}

/** @internal */
export function isSequentialMultiSig(hashMode: AddressHashMode): boolean {
return hashMode === AddressHashMode.SerializeP2SH || hashMode === AddressHashMode.SerializeP2WSH;
}

/** @internal */
export function isNonSequentialMultiSig(hashMode: AddressHashMode): boolean {
return (
hashMode === AddressHashMode.SerializeP2SHNonSequential ||
hashMode === AddressHashMode.SerializeP2WSHNonSequential
);
}

function clearCondition(condition: SpendingConditionOpts): SpendingCondition {
const cloned = cloneDeep(condition);
cloned.nonce = 0;
Expand Down Expand Up @@ -259,8 +273,13 @@ export function deserializeMultiSigSpendingCondition(
// Partially signed multi-sig tx can be serialized and deserialized without exception (Incorrect number of signatures)
// No need to check numSigs !== signaturesRequired to throw Incorrect number of signatures error

if (haveUncompressed && hashMode === AddressHashMode.SerializeP2SH)
if (
haveUncompressed &&
(hashMode === AddressHashMode.SerializeP2WSH ||
hashMode === AddressHashMode.SerializeP2WSHNonSequential)
) {
throw new VerificationError('Uncompressed keys are not allowed in this hash mode');
}

return {
hashMode,
Expand Down Expand Up @@ -448,17 +467,16 @@ function verifyMultiSig(
authType: AuthType
): string {
const publicKeys: StacksPublicKey[] = [];

let curSigHash = initialSigHash;
let haveUncompressed = false;
let numSigs = 0;

for (const field of condition.fields) {
let foundPubKey: StacksPublicKey;

switch (field.contents.type) {
case StacksMessageType.PublicKey:
if (!isCompressed(field.contents)) haveUncompressed = true;
foundPubKey = field.contents;
publicKeys.push(field.contents);
break;
case StacksMessageType.MessageSignature:
if (field.pubKeyEncoding === PubKeyEncoding.Uncompressed) haveUncompressed = true;
Expand All @@ -470,21 +488,30 @@ function verifyMultiSig(
field.pubKeyEncoding,
field.contents
);
curSigHash = nextSigHash;
foundPubKey = pubKey;

if (isSequentialMultiSig(condition.hashMode)) {
curSigHash = nextSigHash;
}

publicKeys.push(pubKey);

numSigs += 1;
if (numSigs === 65536) throw new VerificationError('Too many signatures');

break;
}
publicKeys.push(foundPubKey);
}

if (numSigs !== condition.signaturesRequired)
if (
(isSequentialMultiSig(condition.hashMode) && numSigs !== condition.signaturesRequired) ||
(isNonSequentialMultiSig(condition.hashMode) && numSigs < condition.signaturesRequired)
)
throw new VerificationError('Incorrect number of signatures');

if (haveUncompressed && condition.hashMode === AddressHashMode.SerializeP2SH)
if (
haveUncompressed &&
(condition.hashMode === AddressHashMode.SerializeP2WSH ||
condition.hashMode === AddressHashMode.SerializeP2WSHNonSequential)
)
throw new VerificationError('Uncompressed keys are not allowed in this hash mode');

const addrBytes = addressFromPublicKeys(
Expand Down Expand Up @@ -554,7 +581,7 @@ export function verifyOrigin(auth: Authorization, initialSigHash: string): strin
case AuthType.Standard:
return verify(auth.spendingCondition, initialSigHash, AuthType.Standard);
case AuthType.Sponsored:
return verify(auth.spendingCondition, initialSigHash, AuthType.Standard);
return verify(auth.spendingCondition, initialSigHash, AuthType.Standard); // todo: should this be .Sponsored?
default:
throw new SigningError('Invalid origin auth type');
}
Expand Down
Loading

0 comments on commit 879263c

Please sign in to comment.