Skip to content

Commit 944c8b1

Browse files
authored
Merge pull request #2322 from o1-labs/2025-07-foreign-field-from-field
Support Foreign Field Initialization from Field
2 parents 2860241 + 2d0936f commit 944c8b1

File tree

5 files changed

+112
-7
lines changed

5 files changed

+112
-7
lines changed

.husky/pre-commit

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ fi
99
echo "⚠️ Husky pre-commit hook enabled. Disable it with: git config --unset husky.optin"
1010

1111
# Filter for diffed (excluding deleted) js and ts files, and format them according to oxlint and prettier rules
12-
git diff --cached --name-only --diff-filter=d | grep -E '\.(ts|js)$' | xargs npm run lint:fix
13-
git diff --cached --name-only --diff-filter=d | grep -E '\.(ts|js)$' | xargs npm run format
12+
git diff --cached --name-only --diff-filter=d | grep -E '\.(ts|js|md)$' | xargs npm run lint:fix
13+
git diff --cached --name-only --diff-filter=d | grep -E '\.(ts|js|md)$' | xargs npm run format

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ This project adheres to
4747
- Sourcemaps properly work with the .cjs distribution of `o1js` now.
4848
https://github.com/o1-labs/o1js/pull/2285
4949

50+
### Added
51+
52+
- Support for `Field` as an input type to `ForeignField.from`
53+
https://github.com/o1-labs/o1js/pull/2322
54+
5055
## [2.7.0](https://github.com/o1-labs/o1js/compare/6b6d8b9...045b1ab) - 2025-07-23
5156

5257
### Added

flake.nix

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,8 @@
103103
((pkgs.rustChannelOf
104104
{
105105
channel = "nightly";
106-
date = "2024-09-05";
107-
sha256 = "sha256-3aoA7PuH09g8F+60uTUQhnHrb/ARDLueSOD08ZVsWe0=";
106+
date = "2024-06-13";
107+
sha256 = "sha256-s5nlYcYG9EuO2HK2BU3PkI928DZBKCTJ4U9bz3RX1t4=";
108108
}).rust.override
109109
{
110110
targets = [
@@ -205,7 +205,7 @@
205205
checkPhase = if pkgs.stdenv.isDarwin then "" else null;
206206
text =
207207
''
208-
if [ "$1" = run ] && { [ "$2" = nightly-2024-09-05 ] || [[ "$2" =~ 1.79-x86_64* ]]; }
208+
if [ "$1" = run ] && { [ "$2" = nightly-2024-06-13 ] || [[ "$2" =~ 1.79-x86_64* ]]; }
209209
then
210210
echo using nix toolchain
211211
${rustup}/bin/rustup run nix "''${@:3}"

src/lib/provable/foreign-field.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Tuple, TupleMap, TupleN } from '../util/types.js';
66
import { Gadgets } from './gadgets/gadgets.js';
77
import { ForeignField as FF, Field3 } from './gadgets/foreign-field.js';
88
import { assert } from './gadgets/common.js';
9+
import { fieldToField3 } from './gadgets/comparison.js';
910
import { l3, l } from './gadgets/range-check.js';
1011
import { ProvablePureExtended } from './types/struct.js';
1112

@@ -42,6 +43,36 @@ class ForeignField {
4243
return this.constructor as typeof ForeignField;
4344
}
4445

46+
/**
47+
* Unsafe constructor methods for advanced usage.
48+
*/
49+
static get Unsafe() {
50+
const Constructor = this;
51+
return {
52+
/**
53+
* Converts a {@link Field} into a {@link ForeignField}. This is an **unsafe** operation
54+
* as the native Field size may be larger than the Foreign Field size, and changes in modulus
55+
* can have unintended consequences.
56+
*
57+
* Only use this if you have already constrained the Field element to be within the foreign field modulus.
58+
*
59+
* @param x a {@link Field}
60+
*/
61+
fromField(x: Field) {
62+
const p = Constructor.modulus;
63+
if (x.isConstant()) {
64+
let value = x.toBigInt();
65+
if (value >= p) {
66+
throw new Error(
67+
`Field value ${value} exceeds foreign field modulus ${p}. Please ensure the value is reduced to the modulus.`
68+
);
69+
}
70+
}
71+
return new Constructor.Canonical(fieldToField3(x));
72+
},
73+
};
74+
}
75+
4576
/**
4677
* Sibling classes that represent different ranges of field elements.
4778
*/
@@ -76,10 +107,11 @@ class ForeignField {
76107
}
77108

78109
/**
79-
* Create a new {@link ForeignField} from a bigint, number, string or another ForeignField.
110+
* Create a new {@link ForeignField} from a bigint, number, string, or another ForeignField.
80111
* @example
81112
* ```ts
82113
* let x = new ForeignField(5);
114+
* let y = ForeignField.from(10n);
83115
* ```
84116
*
85117
* Note: Inputs must be range checked if they originate from a different field with a different modulus or if they are not constants.
@@ -110,6 +142,7 @@ class ForeignField {
110142

111143
/**
112144
* Coerce the input to a {@link ForeignField}.
145+
* @param x - The value to convert. Can be a {@link ForeignField}, bigint, number, or string.
113146
*/
114147
static from(x: bigint | number | string): CanonicalForeignField;
115148
static from(x: ForeignField | bigint | number | string): ForeignField;
@@ -689,7 +722,7 @@ type Constructor<T> = new (...args: any[]) => T;
689722
function provable<
690723
F extends ForeignField & {
691724
type: 'Unreduced' | 'AlmostReduced' | 'FullyReduced';
692-
},
725+
}
693726
>(
694727
Class: Constructor<F> & { check(x: ForeignField): void }
695728
): ProvablePureExtended<F, bigint, string> {

src/lib/provable/test/foreign-field.unit-test.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { expect } from 'expect';
55
import {
66
bool,
77
equivalentProvable as equivalent,
8+
field,
89
first,
910
spec,
1011
throwError,
@@ -100,3 +101,69 @@ equivalent({ from: [f], to: f })(
100101
return ForeignScalar.fromBits(bits);
101102
}
102103
);
104+
105+
// Field to ForeignField conversion tests
106+
107+
console.log('Testing Field to ForeignField conversion...');
108+
109+
// Test 1: Constant Field conversion
110+
{
111+
let field = Field(123);
112+
let foreignField = ForeignScalar.Unsafe.fromField(field);
113+
expect(foreignField.toBigInt()).toBe(123n);
114+
expect(foreignField.isConstant()).toBe(true);
115+
console.log('✓ Constant Field conversion');
116+
}
117+
118+
// Test 2: Constructor with Field
119+
{
120+
let field = Field(456);
121+
let foreignField = ForeignScalar.Unsafe.fromField(field);
122+
expect(foreignField.toBigInt()).toBe(456n);
123+
expect(foreignField.isConstant()).toBe(true);
124+
console.log('✓ Constructor with Field');
125+
}
126+
127+
// Test 3: Boundary values
128+
{
129+
// Test zero
130+
let zero = ForeignScalar.Unsafe.fromField(Field(0));
131+
expect(zero.toBigInt()).toBe(0n);
132+
133+
// Test large valid value within native Field range
134+
// Use a large value that fits in native Field (~2^254) rather than foreign field modulus (~2^259)
135+
let largeValue = (1n << 254n) - 1n; // Safe value within native Field range
136+
let large = Field(largeValue);
137+
let foreignLarge = ForeignScalar.Unsafe.fromField(large);
138+
expect(foreignLarge.toBigInt()).toBe(largeValue);
139+
console.log('✓ Boundary values');
140+
}
141+
142+
// Test 4: Small field modulus overflow error
143+
{
144+
// Using SmallField (modulus 17) to easily test overflow
145+
expect(() => {
146+
SmallField.Unsafe.fromField(Field(18)); // 18 > 17 (modulus)
147+
}).toThrow('exceeds foreign field modulus');
148+
console.log('✓ Modulus overflow error');
149+
}
150+
151+
// Test 5: Round-trip conversion
152+
{
153+
let original = Field(12345);
154+
let foreign = ForeignScalar.Unsafe.fromField(original);
155+
let reconstructed = Field(foreign.toBigInt());
156+
expect(reconstructed.toBigInt()).toBe(original.toBigInt());
157+
console.log('✓ Round-trip conversion');
158+
}
159+
160+
// Test 6: Variable Field conversion in provable context
161+
equivalent({
162+
from: [field],
163+
to: f,
164+
})(
165+
(x) => x,
166+
(field) => ForeignScalar.Unsafe.fromField(field)
167+
);
168+
169+
console.log('✓ All Field conversion tests passed!');

0 commit comments

Comments
 (0)