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 boxes/boxes/vanilla/app/embedded-wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ export class EmbeddedWallet extends EmbeddedWalletBase {
if (!address) {
return null;
}
const parsed = AztecAddress.fromString(address);
const parsed = AztecAddress.fromStringUnsafe(address);
this.connectedAccount = parsed;
return this.connectedAccount;
}
Expand Down
8 changes: 4 additions & 4 deletions boxes/boxes/vanilla/app/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ document.addEventListener('DOMContentLoaded', async () => {
const instance = await getContractInstanceFromInstantiationParams(
PrivateVotingContract.artifact,
{
deployer: AztecAddress.fromString(deployerAddress),
deployer: AztecAddress.fromStringUnsafe(deployerAddress),
salt: Fr.fromString(deploymentSalt),
constructorArgs: [AztecAddress.fromString(deployerAddress)],
constructorArgs: [AztecAddress.fromStringUnsafe(deployerAddress)],
}
);
await wallet.registerContract(instance, PrivateVotingContract.artifact);
Expand Down Expand Up @@ -155,7 +155,7 @@ voteButton.addEventListener('click', async (e) => {

// Prepare contract interaction
const votingContract = PrivateVotingContract.at(
AztecAddress.fromString(contractAddress),
AztecAddress.fromStringUnsafe(contractAddress),
wallet
);

Expand Down Expand Up @@ -188,7 +188,7 @@ async function updateVoteTally(wallet: Wallet, from: AztecAddress) {

// Prepare contract interaction
const votingContract = PrivateVotingContract.at(
AztecAddress.fromString(contractAddress),
AztecAddress.fromStringUnsafe(contractAddress),
wallet
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ When working with private state variables, many operations return a `NoteMessage
#### Delivery Methods

Private notes need to be communicated to their recipients so they know the note exists and can use it. The [`NoteMessage`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/note/struct.NoteMessage) wrapper forces you to make an explicit choice about how this happens:
- [`MessageDelivery::onchain_constrained()`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/messages/delivery/global.MessageDelivery): Verified in the circuit (most secure, but highest cost) - Use when the sender cannot be trusted to deliver correctly (e.g., protocol fees, multisig config updates). **Warning:** Currently [not fully constrained](https://github.com/AztecProtocol/aztec-packages/issues/14565) - the log's tag is unconstrained.
- [`MessageDelivery::onchain_constrained()`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/messages/delivery/global.MessageDelivery): Verified in the circuit (most secure, but highest cost) - Use when the sender cannot be trusted to deliver correctly (e.g., protocol fees, multisig config updates).
- [`MessageDelivery::onchain_unconstrained()`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/messages/delivery/global.MessageDelivery): Message stored onchain but no guarantees on content - Use when the sender is incentivized to deliver correctly but may not have an offchain channel to the recipient.
- [`MessageDelivery::offchain()`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/messages/delivery/global.MessageDelivery): Lowest cost, no onchain data - Use when the sender and recipient can communicate and the sender is incentivized to deliver correctly.

Expand Down
20 changes: 20 additions & 0 deletions docs/docs-developers/docs/resources/migration_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,26 @@ Aztec is in active development. Each version may introduce breaking changes that

## TBD

### [Aztec.js] Unchecked `AztecAddress` constructors renamed with an `Unsafe` suffix

The synchronous `AztecAddress` constructors that build an address from a raw value do not verify that the value is a valid address (the x-coordinate of a point on the Grumpkin curve, which is what allows it to be encrypted to). An invalid value is accepted silently and only fails later, when a transaction is sent. To make this obvious at the call site, they now carry an `Unsafe` suffix:

| Before | After |
| --- | --- |
| `AztecAddress.fromField` | `AztecAddress.fromFieldUnsafe` |
| `AztecAddress.fromBigInt` | `AztecAddress.fromBigIntUnsafe` |
| `AztecAddress.fromNumber` | `AztecAddress.fromNumberUnsafe` |
| `AztecAddress.fromString` | `AztecAddress.fromStringUnsafe` |

**Migration:**

```diff
- const address = AztecAddress.fromBigInt(123n);
+ const address = AztecAddress.fromBigIntUnsafe(123n);
```

For a random, genuinely valid address in tests use `AztecAddress.random()`, and to check an untrusted value use `address.isValid()`. The serialization constructors `fromBuffer` and `fromFields` keep their names (they are part of the (de)serialization interface and read addresses from already-validated data), but their docs now note that they perform no validation either.

### Cross-contract utility calls now have a `msg_sender`

A utility function called by another contract (utility to utility, or private to utility) can read the calling contract's address via `self.msg_sender()`, mirroring private and public functions. A top-level utility call (e.g. invoked directly by a wallet or dapp) has no caller: `self.msg_sender()` panics, and `self.context.maybe_msg_sender()` returns `Option::none()`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ case 'sendTx': {

// 1. Deserialize the payload
const payload = deserializeExecutionPayload(executionPayload);
const fromAddress = AztecAddress.fromString(from || options.from);
const fromAddress = AztecAddress.fromStringUnsafe(from || options.from);

// 2. Call wallet.sendTx (inherited from BaseWallet)
const result = await wallet.sendTx(payload, {
Expand Down Expand Up @@ -297,7 +297,7 @@ For gas estimation or validation, dApps use `simulateTx`:
case 'simulateTx': {
const { executionPayload, options } = args;
const payload = deserializeExecutionPayload(executionPayload);
const fromAddress = AztecAddress.fromString(from || options.from);
const fromAddress = AztecAddress.fromStringUnsafe(from || options.from);

const result = await wallet.simulateTx(payload, {
...options,
Expand Down Expand Up @@ -342,7 +342,7 @@ For delegated actions (like approving token spending), the wallet creates auth w
```typescript
case 'createAuthWit': {
const { from: authFrom, messageHashOrIntent } = args;
const fromAddress = AztecAddress.fromString(authFrom);
const fromAddress = AztecAddress.fromStringUnsafe(authFrom);

const authWit = await wallet.createAuthWit(fromAddress, messageHashOrIntent);

Expand Down
2 changes: 1 addition & 1 deletion docs/examples/webapp-tutorial/contracts/src/main.nr
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ pub contract PodRacing {
.at(game_id)
.at(player)
.insert(GameRoundNote::new(track1, track2, track3, track4, track5, round, player))
.deliver(MessageDelivery::onchain_constrained());
.deliver(MessageDelivery::onchain_unconstrained());

self.enqueue(PodRacing::at(self.context.this_address()).validate_and_play_round(
player,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export function GameLobby({ wallet, account, onGameJoined }: GameLobbyProps) {
return;
}

const contractAddr = AztecAddress.fromString(joinContractAddress);
const contractAddr = AztecAddress.fromStringUnsafe(joinContractAddress);
const contract = await attachToContract(
wallet,
contractAddr
Expand Down
5 changes: 3 additions & 2 deletions docs/examples/webapp-tutorial/src/embedded-wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,9 @@ export class EmbeddedWallet extends BaseEmbeddedWallet {
// docs:end:initialize

static async #getSponsoredFPCContract() {
const { SponsoredFPCContractArtifact } =
await import("@aztec/noir-contracts.js/SponsoredFPC");
const { SponsoredFPCContractArtifact } = await import(
"@aztec/noir-contracts.js/SponsoredFPC"
);
const instance = await getContractInstanceFromInstantiationParams(
SponsoredFPCContractArtifact,
{ salt: new Fr(SPONSORED_FPC_SALT) },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ use super::tag_secret_derivation::TagSecretDerivation;
/// ## Construction
///
/// The fields are private and there is no public constructor: a `MessageDelivery` can only be produced by a
/// [`MessageDeliveryBuilder`] that enforces valid configurations, so invalid field combinations cannot be
/// represented to the consumer.
/// [`MessageDeliveryBuilder`].
pub struct MessageDelivery {
mode: DeliveryMode,
tag_secret_derivation: TagSecretDerivation,
Expand Down Expand Up @@ -144,10 +143,6 @@ impl MessageDelivery {

/// Delivers the message on-chain, guaranteeing the recipient will receive the correct content.
///
/// >**WARNING**: this delivery mode is [currently NOT fully
/// constrained](https://github.com/AztecProtocol/aztec-packages/issues/14565). The log's tag is unconstrained,
/// meaning a malicious sender could manipulate it to prevent the recipient from finding the message.
///
/// ## Use Cases
///
/// This delivery method is suitable for all use cases, since it always works as expected. It is however the most
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
//! Sender-side helpers for constrained message delivery.
//!
//! Constrained messages form per-`(sender, recipient, secret)` sequences, each send anchored to the handshake
//! registry at an incrementing index. Two consequences shape the whole flow: sends on one sequence are strictly
//! ordered across transactions (parallel sends collide or fail the predecessor check, while distinct recipients are
//! distinct sequences and parallelize), and batching several sends onto one sequence within a transaction requires
//! an already-committed handshake.
//!
//! See [`constrain_secret`] for how a send is anchored to the registry,

use crate::context::PrivateContext;
use crate::nullifier::utils::compute_nullifier_existence_request;

use crate::protocol::{
abis::function_selector::FunctionSelector, address::AztecAddress, constants::DOM_SEP__CONSTRAINED_MSG_NULLIFIER,
hash::poseidon2_hash_with_separator, traits::ToField,
};

// The helper cannot import the handshake registry interface because the registry contract depends on aztec-nr. The
// registry's test suite compares this against its macro-generated `HandshakeRegistry::at(...).method(...).selector`
// value so signature drift fails in tests.
pub global VALIDATE_HANDSHAKE_SELECTOR: FunctionSelector =
comptime { FunctionSelector::from_signature("validate_handshake((Field),(Field),Field)") };

pub(crate) fn constrain_secret_and_emit_nullifier(
context: &mut PrivateContext,
registry: AztecAddress,
sender: AztecAddress,
recipient: AztecAddress,
secret: Field,
bootstrapped: bool,
index: u32,
) {
constrain_secret(
context,
registry,
sender,
recipient,
secret,
bootstrapped,
index,
);
context.push_nullifier_unsafe(compute_constrained_msg_nullifier(sender, recipient, secret, index));
}

/// Anchors an untrusted `(secret, index)` to the registry before its constrained tag is emitted.
///
/// - bootstrapped: the secret is the constrained `non_interactive_handshake` return (source of
/// truth), so only its `index == 0` start is asserted.
/// - reuse at index 0: `validate_handshake` binds the oracle-supplied secret to the stored handshake.
/// - reuse at index > 0: the prior sequence nullifier must exist, anchoring back to the index-0 check.
fn constrain_secret(
context: &mut PrivateContext,
registry: AztecAddress,
sender: AztecAddress,
recipient: AztecAddress,
secret: Field,
bootstrapped: bool,
index: u32,
) {
let caller = context.this_address();

if bootstrapped {
assert(index == 0, "freshly bootstrapped secret must start at index 0");
} else if index == 0 {
let _ = context.call_private_function(
registry,
VALIDATE_HANDSHAKE_SELECTOR,
[sender.to_field(), recipient.to_field(), secret],
);
} else {
let prev_nullifier = compute_constrained_msg_nullifier(sender, recipient, secret, index - 1);
context.assert_nullifier_exists(compute_nullifier_existence_request(prev_nullifier, caller));
}
}

/// Computes a constrained send's sequence nullifier.
///
/// Every constrained send emits this nullifier so the next send under the same `(sender, recipient, secret)` sequence
/// can prove its predecessor exists.
pub(crate) fn compute_constrained_msg_nullifier(
sender: AztecAddress,
recipient: AztecAddress,
secret: Field,
index: u32,
) -> Field {
poseidon2_hash_with_separator(
[sender.to_field(), recipient.to_field(), secret, index as Field],
DOM_SEP__CONSTRAINED_MSG_NULLIFIER,
)
}

mod test {
use crate::context::PrivateContext;
use crate::hash::hash_args;
use crate::protocol::{address::AztecAddress, hash::compute_siloed_nullifier, traits::{FromField, ToField}};
use crate::test::helpers::test_environment::TestEnvironment;
use super::{compute_constrained_msg_nullifier, constrain_secret_and_emit_nullifier, VALIDATE_HANDSHAKE_SELECTOR};
use std::test::OracleMock;

fn assert_current_nullifier_emitted(
context: &mut PrivateContext,
sender: AztecAddress,
recipient: AztecAddress,
secret: Field,
index: u32,
) {
assert_eq(context.nullifiers.len(), 1);
assert_eq(
context.nullifiers.get(0).inner.value,
compute_constrained_msg_nullifier(sender, recipient, secret, index),
);
}

#[test]
unconstrained fn constrained_helper_emits_current_nullifier() {
let env = TestEnvironment::new();
let registry = AztecAddress::from_field(1);
let sender = AztecAddress::from_field(2);
let recipient = AztecAddress::from_field(4);
let secret: Field = 1234;
let index: u32 = 0;

env.private_context(|context| {
constrain_secret_and_emit_nullifier(context, registry, sender, recipient, secret, true, index);

assert_current_nullifier_emitted(context, sender, recipient, secret, index);
assert_eq(context.private_call_requests.len(), 0);
assert_eq(context.nullifier_read_requests.len(), 0);
});
}

#[test(should_fail_with = "freshly bootstrapped secret must start at index 0")]
unconstrained fn bootstrapped_secret_must_start_at_index_zero() {
let env = TestEnvironment::new();
let registry = AztecAddress::from_field(1);
let sender = AztecAddress::from_field(2);
let recipient = AztecAddress::from_field(4);

env.private_context(|context| {
constrain_secret_and_emit_nullifier(context, registry, sender, recipient, 1234, true, 1);
});
}

#[test]
unconstrained fn reused_secret_at_index_zero_validates_registry_and_emits_nullifier() {
let env = TestEnvironment::new();
let registry = AztecAddress::from_field(1);
let sender = AztecAddress::from_field(2);
let recipient = AztecAddress::from_field(4);
let secret: Field = 1234;
let index: u32 = 0;

env.private_context(|context| {
// The real registry call is covered by integration tests; this unit test only needs a coherent child
// call result so `PrivateContext` records the request.
let child_call_end_counter = (context.side_effect_counter + 1) as Field;
let empty_returns_hash: Field = 0;
let _ = OracleMock::mock("aztec_prv_callPrivateFunction")
.returns([child_call_end_counter, empty_returns_hash])
.times(1);

constrain_secret_and_emit_nullifier(context, registry, sender, recipient, secret, false, index);

assert_current_nullifier_emitted(context, sender, recipient, secret, index);
assert_eq(context.nullifier_read_requests.len(), 0);
assert_eq(context.private_call_requests.len(), 1);

let request = context.private_call_requests.get(0);
assert_eq(request.call_context.msg_sender, context.this_address());
assert_eq(request.call_context.contract_address, registry);
assert_eq(request.call_context.function_selector, VALIDATE_HANDSHAKE_SELECTOR);
assert(!request.call_context.is_static_call);
assert_eq(request.args_hash, hash_args([sender.to_field(), recipient.to_field(), secret]));
});
}

#[test]
unconstrained fn reused_secret_above_index_zero_reads_previous_nullifier_and_emits_current_nullifier() {
let env = TestEnvironment::new();
let registry = AztecAddress::from_field(1);
let sender = AztecAddress::from_field(2);
let recipient = AztecAddress::from_field(4);
let secret: Field = 1234;
let index: u32 = 3;

env.private_context(|context| {
let _ = OracleMock::mock("aztec_prv_isNullifierPending").returns(false).times(1);

constrain_secret_and_emit_nullifier(context, registry, sender, recipient, secret, false, index);

assert_current_nullifier_emitted(context, sender, recipient, secret, index);
assert_eq(context.private_call_requests.len(), 0);
assert_eq(context.nullifier_read_requests.len(), 1);

let read_request = context.nullifier_read_requests.get(0);
assert_eq(read_request.contract_address, AztecAddress::zero());
assert_eq(
read_request.inner.inner,
compute_siloed_nullifier(
context.this_address(),
compute_constrained_msg_nullifier(sender, recipient, secret, index - 1),
),
);
});
}
}
Loading