Skip to content

Commit

Permalink
feat!: add AztecAddress.isValid and make random be valid (#10081)
Browse files Browse the repository at this point in the history
Closes #10039

Since #8970 is not
yet implemented, invalid addresses cannot be the recipient of e.g. token
transfers, since we end up producing points not in the curve and causing
MSM to fail. Making `random()` return valid addresses seems like very
reasonable behavior.
  • Loading branch information
nventuro authored Nov 22, 2024
1 parent adae143 commit fbdf6b0
Show file tree
Hide file tree
Showing 13 changed files with 158 additions and 61 deletions.
6 changes: 6 additions & 0 deletions docs/docs/migration_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ keywords: [sandbox, aztec, notes, migration, updating, upgrading]

Aztec is in full-speed development. Literally every version breaks compatibility with the previous ones. This page attempts to target errors and difficulties you might encounter when upgrading, and how to resolve them.

## TBD

### [aztec.js] Random addresses are now valid

The `AztecAddress.random()` function now returns valid addresses, i.e. addresses that can receive encrypted messages and therefore have notes be sent to them. `AztecAddress.isValid()` was also added to check for validity of an address.

## 0.63.0
### [PXE] Note tagging and discovery

Expand Down
4 changes: 2 additions & 2 deletions noir/noir-repo/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
PRIVATE_LOG_SIZE_IN_BYTES,
computeAddressSecret,
computeOvskApp,
computePoint,
deriveKeys,
derivePublicKeyFromSecretKey,
} from '@aztec/circuits.js';
Expand Down Expand Up @@ -83,7 +82,7 @@ describe('EncryptedLogPayload', () => {
ephSk.hi,
ephSk.lo,
recipient,
computePoint(recipient).toCompressedBuffer(),
recipient.toAddressPoint().toCompressedBuffer(),
);
const outgoingBodyCiphertext = encrypt(
outgoingBodyPlaintext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
Point,
type PublicKey,
computeOvskApp,
computePoint,
derivePublicKeyFromSecretKey,
} from '@aztec/circuits.js';
import { randomBytes } from '@aztec/foundation/crypto';
Expand Down Expand Up @@ -59,7 +58,7 @@ export class EncryptedLogPayload {
ovKeys: KeyValidationRequest,
rand: (len: number) => Buffer = randomBytes,
): Buffer {
const addressPoint = computePoint(recipient);
const addressPoint = recipient.toAddressPoint();

const ephPk = derivePublicKeyFromSecretKey(ephSk);
const incomingHeaderCiphertext = encrypt(this.contractAddress.toBuffer(), ephSk, addressPoint);
Expand Down
8 changes: 2 additions & 6 deletions yarn-project/circuits.js/src/keys/derivation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AztecAddress } from '@aztec/foundation/aztec-address';
import { poseidon2HashWithSeparator, sha512ToGrumpkinScalar } from '@aztec/foundation/crypto';
import { Fq, Fr, GrumpkinScalar, Point } from '@aztec/foundation/fields';
import { Fq, Fr, GrumpkinScalar } from '@aztec/foundation/fields';

import { Grumpkin } from '../barretenberg/crypto/grumpkin/index.js';
import { GeneratorIndex } from '../constants.gen.js';
Expand Down Expand Up @@ -82,10 +82,6 @@ export function computeAddressSecret(preaddress: Fr, ivsk: Fq) {
return addressSecretCandidate;
}

export function computePoint(address: AztecAddress) {
return Point.fromXAndSign(address.toField(), true);
}

export function derivePublicKeyFromSecretKey(secretKey: Fq) {
const curve = new Grumpkin();
return curve.mul(curve.generator(), secretKey);
Expand Down Expand Up @@ -130,7 +126,7 @@ export function deriveKeys(secretKey: Fr) {
export function computeTaggingSecret(knownAddress: CompleteAddress, ivsk: Fq, externalAddress: AztecAddress) {
const knownPreaddress = computePreaddress(knownAddress.publicKeys.hash(), knownAddress.partialAddress);
// TODO: #8970 - Computation of address point from x coordinate might fail
const externalAddressPoint = computePoint(externalAddress);
const externalAddressPoint = externalAddress.toAddressPoint();
const curve = new Grumpkin();
// Given A (known complete address) -> B (external address) and h == preaddress
// Compute shared secret as S = (h_A + ivsk_A) * Addr_Point_B
Expand Down
50 changes: 50 additions & 0 deletions yarn-project/foundation/src/aztec-address/aztec-address.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Fr } from '../fields/fields.js';
import { AztecAddress } from './index.js';

describe('aztec-address', () => {
describe('isValid', () => {
it('returns true for a valid address', () => {
// The point (5, 21888242871839275195798879923479812031525119486506890092185616889232283231735) is on the
// Grumpkin curve.
const address = new AztecAddress(new Fr(5));
expect(address.isValid()).toEqual(true);
});

it('returns false for an invalid address', () => {
// No point on the Grumpkin curve has an x coordinate equal to 6.
const address = new AztecAddress(new Fr(6));
expect(address.isValid()).toEqual(false);
});
});

describe('random', () => {
it('always returns a valid address', () => {
for (let i = 0; i < 100; ++i) {
const address = AztecAddress.random();
expect(address.isValid()).toEqual(true);
}
});

it('returns a different address on each call', () => {
const set = new Set();
for (let i = 0; i < 100; ++i) {
set.add(AztecAddress.random());
}

expect(set.size).toEqual(100);
});
});

describe('toAddressPoint', () => {
it("reconstructs an address's point", () => {
const address = AztecAddress.random();
const point = address.toAddressPoint();
expect(point.isOnGrumpkin()).toEqual(true);
});

it('throws for an invalid address', () => {
const address = new AztecAddress(new Fr(6));
expect(() => address.toAddressPoint()).toThrow('The given x-coordinate is not on the Grumpkin curve');
});
});
});
63 changes: 47 additions & 16 deletions yarn-project/foundation/src/aztec-address/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
import { inspect } from 'util';

import { Fr, fromBuffer } from '../fields/index.js';
import { Fr, Point, fromBuffer } from '../fields/index.js';
import { type BufferReader, FieldReader } from '../serialize/index.js';
import { TypeRegistry } from '../serialize/type_registry.js';
import { hexToBuffer } from '../string/index.js';
Expand All @@ -12,20 +12,22 @@ export interface AztecAddress {
_branding: 'AztecAddress';
}
/**
* AztecAddress represents a 32-byte address in the Aztec Protocol.
* It provides methods to create, manipulate, and compare addresses.
* The maximum value of an address is determined by the field modulus and all instances of AztecAddress.
* It should have a value less than or equal to this max value.
* This class also provides helper functions to convert addresses from strings, buffers, and other formats.
* AztecAddress represents a 32-byte address in the Aztec Protocol. It provides methods to create, manipulate, and
* compare addresses, as well as conversion to and from strings, buffers, and other formats.
* Addresses are the x coordinate of a point in the Grumpkin curve, and therefore their maximum is determined by the
* field modulus. An address with a value that is not the x coordinate of a point in the curve is a called an 'invalid
* address'. These addresses have a greatly reduced feature set, as they cannot own secrets nor have messages encrypted
* to them, making them quite useless. We need to be able to represent them however as they can be encountered in the
* wild.
*/
export class AztecAddress {
private value: Fr;
private xCoord: Fr;

constructor(buffer: Buffer | Fr) {
if ('length' in buffer && buffer.length !== 32) {
throw new Error(`Invalid AztecAddress length ${buffer.length}.`);
}
this.value = new Fr(buffer);
this.xCoord = new Fr(buffer);
}

[inspect.custom]() {
Expand Down Expand Up @@ -69,36 +71,65 @@ export class AztecAddress {
return new AztecAddress(hexToBuffer(buf));
}

/**
* @returns a random valid address (i.e. one that can be encrypted to).
*/
static random() {
return new AztecAddress(Fr.random());
// About half of random field elements result in invalid addresses, so we loop until we get a valid one.
while (true) {
const candidate = new AztecAddress(Fr.random());
if (candidate.isValid()) {
return candidate;
}
}
}

get size() {
return this.value.size;
return this.xCoord.size;
}

equals(other: AztecAddress) {
return this.value.equals(other.value);
return this.xCoord.equals(other.xCoord);
}

isZero() {
return this.value.isZero();
return this.xCoord.isZero();
}

/**
* @returns true if the address is valid. Invalid addresses cannot receive encrypted messages.
*/
isValid() {
// An address is a field value (Fr), which for some purposes is assumed to be the x coordinate of a point in the
// Grumpkin curve (notably in order to encrypt to it). An address that is not the x coordinate of such a point is
// called an 'invalid' address.
//
// For Grumpkin, y^2 = x^3 − 17 . There exist values x ∈ Fr for which no y satisfies this equation. This means that
// given such an x and t = x^3 − 17, then sqrt(t) does not exist in Fr.
return Point.YFromX(this.xCoord) !== null;
}

/**
* @returns the Point from which the address is derived. Throws if the address is invalid.
*/
toAddressPoint() {
return Point.fromXAndSign(this.xCoord, true);
}

toBuffer() {
return this.value.toBuffer();
return this.xCoord.toBuffer();
}

toBigInt() {
return this.value.toBigInt();
return this.xCoord.toBigInt();
}

toField() {
return this.value;
return this.xCoord;
}

toString() {
return this.value.toString();
return this.xCoord.toString();
}

toJSON() {
Expand Down
22 changes: 18 additions & 4 deletions yarn-project/foundation/src/fields/point.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,24 @@ import { Fr } from './fields.js';
import { Point } from './point.js';

describe('Point', () => {
describe('random', () => {
it('always returns a valid point', () => {
for (let i = 0; i < 100; ++i) {
const point = Point.random();
expect(point.isOnGrumpkin()).toEqual(true);
}
});

it('returns a different points on each call', () => {
const set = new Set();
for (let i = 0; i < 100; ++i) {
set.add(Point.random());
}

expect(set.size).toEqual(100);
});
});

it('converts to and from x and sign of y coordinate', () => {
const p = new Point(
new Fr(0x30426e64aee30e998c13c8ceecda3a77807dbead52bc2f3bf0eae851b4b710c1n),
Expand All @@ -17,10 +35,6 @@ describe('Point', () => {
expect(p.equals(p2)).toBeTruthy();
});

it('creates a valid random point', () => {
expect(Point.random().isOnGrumpkin()).toBeTruthy();
});

it('converts to and from buffer', () => {
const p = Point.random();
const p2 = Point.fromBuffer(p.toBuffer());
Expand Down
28 changes: 17 additions & 11 deletions yarn-project/foundation/src/fields/point.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,14 +117,8 @@ export class Point {
* @returns The point as an array of 2 fields
*/
static fromXAndSign(x: Fr, sign: boolean) {
// Calculate y^2 = x^3 - 17
const ySquared = x.square().mul(x).sub(new Fr(17));

// Calculate the square root of ySquared
const y = ySquared.sqrt();

// If y is null, the x-coordinate is not on the curve
if (y === null) {
const y = Point.YFromX(x);
if (y == null) {
throw new NotOnCurveError(x);
}

Expand All @@ -138,6 +132,18 @@ export class Point {
return new this(x, finalY, false);
}

/**
* @returns
*/
static YFromX(x: Fr): Fr | null {
// Calculate y^2 = x^3 - 17 (i.e. the Grumpkin curve equation)
const ySquared = x.square().mul(x).sub(new Fr(17));

// y is then simply the square root. Note however that not all square roots exist in the field: if sqrt returns null
// then there is no point in the curve with this x coordinate.
return ySquared.sqrt();
}

/**
* Returns the x coordinate and the sign of the y coordinate.
* @dev The y sign can be determined by checking if the y coordinate is greater than half of the modulus.
Expand Down Expand Up @@ -267,10 +273,10 @@ export class Point {
return true;
}

// p.y * p.y == p.x * p.x * p.x - 17
const A = new Fr(17);
// The Grumpkin equation is y^2 = x^3 - 17. We could use `YFromX` and then compare to `this.y`, but this would
// involve computing the square root of y, of which there are two possible valid values. This method is also faster.
const lhs = this.y.square();
const rhs = this.x.square().mul(this.x).sub(A);
const rhs = this.x.mul(this.x).mul(this.x).sub(new Fr(17));
return lhs.equals(rhs);
}
}
Expand Down
3 changes: 1 addition & 2 deletions yarn-project/pxe/src/database/kv_pxe_database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
type IndexedTaggingSecret,
type PublicKey,
SerializableContractInstance,
computePoint,
} from '@aztec/circuits.js';
import { type ContractArtifact, FunctionSelector, FunctionType } from '@aztec/foundation/abi';
import { toBufferBE } from '@aztec/foundation/bigint-buffer';
Expand Down Expand Up @@ -327,7 +326,7 @@ export class KVPxeDatabase implements PxeDatabase {
}

getIncomingNotes(filter: IncomingNotesFilter): Promise<IncomingNoteDao[]> {
const publicKey: PublicKey | undefined = filter.owner ? computePoint(filter.owner) : undefined;
const publicKey: PublicKey | undefined = filter.owner ? filter.owner.toAddressPoint() : undefined;

filter.status = filter.status ?? NoteStatus.ACTIVE;

Expand Down
Loading

0 comments on commit fbdf6b0

Please sign in to comment.