Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ hydentity/

```bash
# Clone the repository
git clone https://github.com/your-org/hydentity.git
git clone https://github.com/hydex-org/hydentity.git
cd hydentity

# Install dependencies
Expand Down
26 changes: 22 additions & 4 deletions packages/hydentity-sdk/src/client/hydentity-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ import {
} from '../instruction-builders/vault';
import { buildUpdatePolicyInstruction, createUpdatePolicyParams } from '../instruction-builders/policy';
import { buildAddDelegateInstruction, buildRevokeDelegateInstruction } from '../instruction-builders/delegate';
import { HYDENTITY_PROGRAM_ID } from '../constants';
import {
HYDENTITY_PROGRAM_ID,
POLICY_MASTER_SEED_SIGN_MESSAGE,
} from '../constants';
import { derivePolicyMasterSeedFromSignature } from '../utils/randomness';

/**
* Configuration options for HydentityClient
Expand Down Expand Up @@ -129,6 +133,21 @@ export class HydentityClient<T = SolanaTransactionSignature> {
return this.signer.getPublicKey();
}

/**
* Per-wallet entropy for PolicyEngine splits/delays (from signing POLICY_MASTER_SEED_SIGN_MESSAGE).
*/
private async derivePolicyMasterSeed(): Promise<Uint8Array> {
if (!this.signer) {
throw new Error('No signer configured. Call setSigner() first.');
}
const message = new TextEncoder().encode(POLICY_MASTER_SEED_SIGN_MESSAGE);
const [signature, pubkey] = await Promise.all([
this.signer.signMessage(message),
this.signer.getPublicKey(),
]);
return derivePolicyMasterSeedFromSignature(pubkey.toBytes(), signature);
}

/**
* Build, sign, and optionally send a transaction based on mode
*/
Expand Down Expand Up @@ -425,9 +444,8 @@ export class HydentityClient<T = SolanaTransactionSignature> {
throw new Error(`Insufficient vault balance: ${balance.sol} < ${amount}`);
}

// Create policy engine for this claim
const signer = await this.getSignerPublicKey();
const masterSeed = new Uint8Array(32); // TODO: Derive from signer
// Create policy engine for this claim (wallet-specific entropy via message sign)
const masterSeed = await this.derivePolicyMasterSeed();
const policyEngine = new PolicyEngine(masterSeed, policy.policyNonce);

// Generate execution plan
Expand Down
10 changes: 10 additions & 0 deletions packages/hydentity-sdk/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@ export const DUST_THRESHOLD_LAMPORTS = 10_000n; // 0.00001 SOL
export const KMAC_DOMAIN_RANDOM_SEED = 'Hydentity - Random Seed';
export const KMAC_DOMAIN_SPLIT_SEED = 'Hydentity - Split Seed';
export const KMAC_DOMAIN_DELAY_SEED = 'Hydentity - Delay Seed';
/** KMAC domain for hashing wallet proof into policy execution seed */
export const KMAC_DOMAIN_POLICY_MASTER_SEED = 'Hydentity - Policy Master Seed';

/**
* Message bytes users sign to derive per-wallet policy execution entropy for
* split and delay plans. Does not authorize transfers by itself.
*/
export const POLICY_MASTER_SEED_SIGN_MESSAGE =
'Sign to derive Hydentity private withdrawal split entropy. ' +
'This does not grant any transfer.';

/**
* SNS TLD key for .sol domains
Expand Down
34 changes: 34 additions & 0 deletions packages/hydentity-sdk/src/utils/randomness.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, expect, it } from 'vitest';
import { derivePolicyMasterSeedFromSignature } from './randomness';

describe('derivePolicyMasterSeedFromSignature', () => {
it('rejects wrong pubkey length', () => {
expect(() =>
derivePolicyMasterSeedFromSignature(new Uint8Array(31), new Uint8Array(64))
).toThrow('32 bytes');
});

it('rejects wrong signature length', () => {
expect(() =>
derivePolicyMasterSeedFromSignature(new Uint8Array(32), new Uint8Array(63))
).toThrow('64 bytes');
});

it('is deterministic for the same proof', () => {
const pk = new Uint8Array(32).fill(7);
const sig = new Uint8Array(64).fill(3);
const a = derivePolicyMasterSeedFromSignature(pk, sig);
const b = derivePolicyMasterSeedFromSignature(pk, sig);
expect(a).toEqual(b);
expect(a.length).toBe(32);
});

it('differs when signature bytes change', () => {
const pk = new Uint8Array(32).fill(1);
const sig1 = new Uint8Array(64).fill(2);
const sig2 = new Uint8Array(64).fill(9);
const s1 = derivePolicyMasterSeedFromSignature(pk, sig1);
const s2 = derivePolicyMasterSeedFromSignature(pk, sig2);
expect(Array.from(s1).join()).not.toBe(Array.from(s2).join());
});
});
25 changes: 25 additions & 0 deletions packages/hydentity-sdk/src/utils/randomness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
KMAC_DOMAIN_RANDOM_SEED,
KMAC_DOMAIN_SPLIT_SEED,
KMAC_DOMAIN_DELAY_SEED,
KMAC_DOMAIN_POLICY_MASTER_SEED,
DUST_THRESHOLD_LAMPORTS,
} from '../constants';
import type { Amount, U128 } from '../types/common';
Expand Down Expand Up @@ -49,6 +50,30 @@ export function deriveRandomSeed(masterSeed: Uint8Array, nonce: bigint): Uint8Ar
);
}

/**
* Derive 32-byte policy master seed from a wallet message-signing proof.
* Callers must pass the Ed25519 signature of POLICY_MASTER_SEED_SIGN_MESSAGE.
*/
export function derivePolicyMasterSeedFromSignature(
walletPublicKey32: Uint8Array,
messageSignature64: Uint8Array
): Uint8Array {
if (walletPublicKey32.length !== 32) {
throw new Error('wallet public key must be 32 bytes');
}
if (messageSignature64.length !== 64) {
throw new Error('Ed25519 signature must be 64 bytes');
}
const input = new Uint8Array(32 + 64);
input.set(walletPublicKey32, 0);
input.set(messageSignature64, 32);
return kmac256(
new TextEncoder().encode(KMAC_DOMAIN_POLICY_MASTER_SEED),
input,
{ dkLen: 32 }
);
}

/**
* Generate a deterministic random value from seed and index
*
Expand Down