Skip to content

Commit fbdf6b0

Browse files
authored
feat!: add AztecAddress.isValid and make random be valid (#10081)
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.
1 parent adae143 commit fbdf6b0

File tree

13 files changed

+158
-61
lines changed

13 files changed

+158
-61
lines changed

docs/docs/migration_notes.md

+6
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ keywords: [sandbox, aztec, notes, migration, updating, upgrading]
66

77
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.
88

9+
## TBD
10+
11+
### [aztec.js] Random addresses are now valid
12+
13+
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.
14+
915
## 0.63.0
1016
### [PXE] Note tagging and discovery
1117

noir/noir-repo/Cargo.lock

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

yarn-project/circuit-types/src/logs/l1_payload/encrypted_log_payload.test.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {
66
PRIVATE_LOG_SIZE_IN_BYTES,
77
computeAddressSecret,
88
computeOvskApp,
9-
computePoint,
109
deriveKeys,
1110
derivePublicKeyFromSecretKey,
1211
} from '@aztec/circuits.js';
@@ -83,7 +82,7 @@ describe('EncryptedLogPayload', () => {
8382
ephSk.hi,
8483
ephSk.lo,
8584
recipient,
86-
computePoint(recipient).toCompressedBuffer(),
85+
recipient.toAddressPoint().toCompressedBuffer(),
8786
);
8887
const outgoingBodyCiphertext = encrypt(
8988
outgoingBodyPlaintext,

yarn-project/circuit-types/src/logs/l1_payload/encrypted_log_payload.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
Point,
99
type PublicKey,
1010
computeOvskApp,
11-
computePoint,
1211
derivePublicKeyFromSecretKey,
1312
} from '@aztec/circuits.js';
1413
import { randomBytes } from '@aztec/foundation/crypto';
@@ -59,7 +58,7 @@ export class EncryptedLogPayload {
5958
ovKeys: KeyValidationRequest,
6059
rand: (len: number) => Buffer = randomBytes,
6160
): Buffer {
62-
const addressPoint = computePoint(recipient);
61+
const addressPoint = recipient.toAddressPoint();
6362

6463
const ephPk = derivePublicKeyFromSecretKey(ephSk);
6564
const incomingHeaderCiphertext = encrypt(this.contractAddress.toBuffer(), ephSk, addressPoint);

yarn-project/circuits.js/src/keys/derivation.ts

+2-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { AztecAddress } from '@aztec/foundation/aztec-address';
22
import { poseidon2HashWithSeparator, sha512ToGrumpkinScalar } from '@aztec/foundation/crypto';
3-
import { Fq, Fr, GrumpkinScalar, Point } from '@aztec/foundation/fields';
3+
import { Fq, Fr, GrumpkinScalar } from '@aztec/foundation/fields';
44

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

85-
export function computePoint(address: AztecAddress) {
86-
return Point.fromXAndSign(address.toField(), true);
87-
}
88-
8985
export function derivePublicKeyFromSecretKey(secretKey: Fq) {
9086
const curve = new Grumpkin();
9187
return curve.mul(curve.generator(), secretKey);
@@ -130,7 +126,7 @@ export function deriveKeys(secretKey: Fr) {
130126
export function computeTaggingSecret(knownAddress: CompleteAddress, ivsk: Fq, externalAddress: AztecAddress) {
131127
const knownPreaddress = computePreaddress(knownAddress.publicKeys.hash(), knownAddress.partialAddress);
132128
// TODO: #8970 - Computation of address point from x coordinate might fail
133-
const externalAddressPoint = computePoint(externalAddress);
129+
const externalAddressPoint = externalAddress.toAddressPoint();
134130
const curve = new Grumpkin();
135131
// Given A (known complete address) -> B (external address) and h == preaddress
136132
// Compute shared secret as S = (h_A + ivsk_A) * Addr_Point_B
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Fr } from '../fields/fields.js';
2+
import { AztecAddress } from './index.js';
3+
4+
describe('aztec-address', () => {
5+
describe('isValid', () => {
6+
it('returns true for a valid address', () => {
7+
// The point (5, 21888242871839275195798879923479812031525119486506890092185616889232283231735) is on the
8+
// Grumpkin curve.
9+
const address = new AztecAddress(new Fr(5));
10+
expect(address.isValid()).toEqual(true);
11+
});
12+
13+
it('returns false for an invalid address', () => {
14+
// No point on the Grumpkin curve has an x coordinate equal to 6.
15+
const address = new AztecAddress(new Fr(6));
16+
expect(address.isValid()).toEqual(false);
17+
});
18+
});
19+
20+
describe('random', () => {
21+
it('always returns a valid address', () => {
22+
for (let i = 0; i < 100; ++i) {
23+
const address = AztecAddress.random();
24+
expect(address.isValid()).toEqual(true);
25+
}
26+
});
27+
28+
it('returns a different address on each call', () => {
29+
const set = new Set();
30+
for (let i = 0; i < 100; ++i) {
31+
set.add(AztecAddress.random());
32+
}
33+
34+
expect(set.size).toEqual(100);
35+
});
36+
});
37+
38+
describe('toAddressPoint', () => {
39+
it("reconstructs an address's point", () => {
40+
const address = AztecAddress.random();
41+
const point = address.toAddressPoint();
42+
expect(point.isOnGrumpkin()).toEqual(true);
43+
});
44+
45+
it('throws for an invalid address', () => {
46+
const address = new AztecAddress(new Fr(6));
47+
expect(() => address.toAddressPoint()).toThrow('The given x-coordinate is not on the Grumpkin curve');
48+
});
49+
});
50+
});

yarn-project/foundation/src/aztec-address/index.ts

+47-16
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
22
import { inspect } from 'util';
33

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

2426
constructor(buffer: Buffer | Fr) {
2527
if ('length' in buffer && buffer.length !== 32) {
2628
throw new Error(`Invalid AztecAddress length ${buffer.length}.`);
2729
}
28-
this.value = new Fr(buffer);
30+
this.xCoord = new Fr(buffer);
2931
}
3032

3133
[inspect.custom]() {
@@ -69,36 +71,65 @@ export class AztecAddress {
6971
return new AztecAddress(hexToBuffer(buf));
7072
}
7173

74+
/**
75+
* @returns a random valid address (i.e. one that can be encrypted to).
76+
*/
7277
static random() {
73-
return new AztecAddress(Fr.random());
78+
// About half of random field elements result in invalid addresses, so we loop until we get a valid one.
79+
while (true) {
80+
const candidate = new AztecAddress(Fr.random());
81+
if (candidate.isValid()) {
82+
return candidate;
83+
}
84+
}
7485
}
7586

7687
get size() {
77-
return this.value.size;
88+
return this.xCoord.size;
7889
}
7990

8091
equals(other: AztecAddress) {
81-
return this.value.equals(other.value);
92+
return this.xCoord.equals(other.xCoord);
8293
}
8394

8495
isZero() {
85-
return this.value.isZero();
96+
return this.xCoord.isZero();
97+
}
98+
99+
/**
100+
* @returns true if the address is valid. Invalid addresses cannot receive encrypted messages.
101+
*/
102+
isValid() {
103+
// An address is a field value (Fr), which for some purposes is assumed to be the x coordinate of a point in the
104+
// Grumpkin curve (notably in order to encrypt to it). An address that is not the x coordinate of such a point is
105+
// called an 'invalid' address.
106+
//
107+
// For Grumpkin, y^2 = x^3 − 17 . There exist values x ∈ Fr for which no y satisfies this equation. This means that
108+
// given such an x and t = x^3 − 17, then sqrt(t) does not exist in Fr.
109+
return Point.YFromX(this.xCoord) !== null;
110+
}
111+
112+
/**
113+
* @returns the Point from which the address is derived. Throws if the address is invalid.
114+
*/
115+
toAddressPoint() {
116+
return Point.fromXAndSign(this.xCoord, true);
86117
}
87118

88119
toBuffer() {
89-
return this.value.toBuffer();
120+
return this.xCoord.toBuffer();
90121
}
91122

92123
toBigInt() {
93-
return this.value.toBigInt();
124+
return this.xCoord.toBigInt();
94125
}
95126

96127
toField() {
97-
return this.value;
128+
return this.xCoord;
98129
}
99130

100131
toString() {
101-
return this.value.toString();
132+
return this.xCoord.toString();
102133
}
103134

104135
toJSON() {

yarn-project/foundation/src/fields/point.test.ts

+18-4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,24 @@ import { Fr } from './fields.js';
44
import { Point } from './point.js';
55

66
describe('Point', () => {
7+
describe('random', () => {
8+
it('always returns a valid point', () => {
9+
for (let i = 0; i < 100; ++i) {
10+
const point = Point.random();
11+
expect(point.isOnGrumpkin()).toEqual(true);
12+
}
13+
});
14+
15+
it('returns a different points on each call', () => {
16+
const set = new Set();
17+
for (let i = 0; i < 100; ++i) {
18+
set.add(Point.random());
19+
}
20+
21+
expect(set.size).toEqual(100);
22+
});
23+
});
24+
725
it('converts to and from x and sign of y coordinate', () => {
826
const p = new Point(
927
new Fr(0x30426e64aee30e998c13c8ceecda3a77807dbead52bc2f3bf0eae851b4b710c1n),
@@ -17,10 +35,6 @@ describe('Point', () => {
1735
expect(p.equals(p2)).toBeTruthy();
1836
});
1937

20-
it('creates a valid random point', () => {
21-
expect(Point.random().isOnGrumpkin()).toBeTruthy();
22-
});
23-
2438
it('converts to and from buffer', () => {
2539
const p = Point.random();
2640
const p2 = Point.fromBuffer(p.toBuffer());

yarn-project/foundation/src/fields/point.ts

+17-11
Original file line numberDiff line numberDiff line change
@@ -117,14 +117,8 @@ export class Point {
117117
* @returns The point as an array of 2 fields
118118
*/
119119
static fromXAndSign(x: Fr, sign: boolean) {
120-
// Calculate y^2 = x^3 - 17
121-
const ySquared = x.square().mul(x).sub(new Fr(17));
122-
123-
// Calculate the square root of ySquared
124-
const y = ySquared.sqrt();
125-
126-
// If y is null, the x-coordinate is not on the curve
127-
if (y === null) {
120+
const y = Point.YFromX(x);
121+
if (y == null) {
128122
throw new NotOnCurveError(x);
129123
}
130124

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

135+
/**
136+
* @returns
137+
*/
138+
static YFromX(x: Fr): Fr | null {
139+
// Calculate y^2 = x^3 - 17 (i.e. the Grumpkin curve equation)
140+
const ySquared = x.square().mul(x).sub(new Fr(17));
141+
142+
// y is then simply the square root. Note however that not all square roots exist in the field: if sqrt returns null
143+
// then there is no point in the curve with this x coordinate.
144+
return ySquared.sqrt();
145+
}
146+
141147
/**
142148
* Returns the x coordinate and the sign of the y coordinate.
143149
* @dev The y sign can be determined by checking if the y coordinate is greater than half of the modulus.
@@ -267,10 +273,10 @@ export class Point {
267273
return true;
268274
}
269275

270-
// p.y * p.y == p.x * p.x * p.x - 17
271-
const A = new Fr(17);
276+
// The Grumpkin equation is y^2 = x^3 - 17. We could use `YFromX` and then compare to `this.y`, but this would
277+
// involve computing the square root of y, of which there are two possible valid values. This method is also faster.
272278
const lhs = this.y.square();
273-
const rhs = this.x.square().mul(this.x).sub(A);
279+
const rhs = this.x.mul(this.x).mul(this.x).sub(new Fr(17));
274280
return lhs.equals(rhs);
275281
}
276282
}

yarn-project/pxe/src/database/kv_pxe_database.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
type IndexedTaggingSecret,
1414
type PublicKey,
1515
SerializableContractInstance,
16-
computePoint,
1716
} from '@aztec/circuits.js';
1817
import { type ContractArtifact, FunctionSelector, FunctionType } from '@aztec/foundation/abi';
1918
import { toBufferBE } from '@aztec/foundation/bigint-buffer';
@@ -327,7 +326,7 @@ export class KVPxeDatabase implements PxeDatabase {
327326
}
328327

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

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

0 commit comments

Comments
 (0)