Skip to content

Conversation

@agnxsh
Copy link
Collaborator

@agnxsh agnxsh commented Sep 6, 2025

Addressing #573
Consists tests for :

  • addition, subtraction, multiplication in KoalaBear and BabyBear
  • field io operations for the same.
  • sanity checking if modular squaring is consistent with multiplication

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary of Changes

Hello @agnxsh, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the test coverage for the BabyBear and KoalaBear finite fields. It introduces new tests for core arithmetic operations (addition, subtraction, multiplication), validates field I/O, and ensures the consistency of modular squaring. These additions improve the robustness and reliability of these critical cryptographic components.

Highlights

  • New Field Operation Tests: Comprehensive unit tests have been added for addition, subtraction, and multiplication operations for both BabyBear and KoalaBear finite fields.
  • Field I/O Operation Tests: Tests for field input/output (marshalling) operations have been introduced for BabyBear and KoalaBear.
  • Modular Squaring Consistency: Sanity checks have been extended to include BabyBear and KoalaBear, ensuring modular squaring consistency with multiplication.
  • Curve Configuration Update: The testingCurve flag has been set to true for both BabyBear and KoalaBear curves to enable their use in testing environments.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in issue comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@agnxsh agnxsh changed the title tests for koala and babybear field ops tests: koala and babybear field ops Sep 6, 2025
Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds a comprehensive set of tests for the KoalaBear and BabyBear fields, covering basic arithmetic operations, I/O, and sanity checks for modular squaring. The changes are well-structured and the test cases, including edge cases, are relevant. My feedback focuses on improving the maintainability of the test code by reducing duplication through helper functions and removing hardcoded magic numbers.

Comment on lines +169 to +402
test "Addition mod 2^31 - 2^27 + 1":
block:
var x, y, z: Fp[BabyBear]

x.fromUint(80'u32)
y.fromUint(10'u32)
z.fromUint(90'u32)

x += y

var x_bytes: array[4, byte]
x_bytes.marshal(x, cpuEndian)
let new_x = cast[uint32](x_bytes)

check:
# Check equality in the Montgomery domain
bool(z == x)
# Check equality when converting back to natural domain
new_x == 90'u32

block:
var x, y, z: Fp[BabyBear]

x.fromUint(0x78000000'u32) # p-1
y.fromUint(1'u32)
z.fromUint(0'u32)

x += y

var x_bytes: array[4, byte]
x_bytes.marshal(x, cpuEndian)
let new_x = cast[uint32](x_bytes)

check:
# Check equality in the Montgomery domain
bool(z == x)
# Check equality when converting back to natural domain
new_x == 0'u32

test "Substraction mod 2^31 - 2^27 + 1":
block:
var x, y, z: Fp[BabyBear]

x.fromUint(80'u32)
y.fromUint(10'u32)
z.fromUint(70'u32)

x -= y

var x_bytes: array[4, byte]
x_bytes.marshal(x, cpuEndian)
let new_x = cast[uint32](x_bytes)

check:
# Check equality in the Montgomery domain
bool(z == x)
# Check equality when converting back to natural domain
new_x == 70'u32

block:
var x, y, z: Fp[BabyBear]

x.fromUint(0'u32)
y.fromUint(1'u32)
z.fromUint(0x78000000'u32) # p-1

x -= y

var x_bytes: array[4, byte]
x_bytes.marshal(x, cpuEndian)
let new_x = cast[uint32](x_bytes)

check:
# Check equality in the Montgomery domain
bool(z == x)
# Check equality when converting back to natural domain
new_x == 0x78000000'u32

test "Multiplication mod 2^31 - 2^27 + 1":
block:
var x, y, z, r: Fp[BabyBear]

x.fromUint(10'u32)
y.fromUint(10'u32)
z.fromUint(100'u32)

r.prod(x, y)

var r_bytes: array[4, byte]
r_bytes.marshal(r, cpuEndian)
let new_r = cast[uint32](r_bytes)

check:
# Check equality in the Montgomery domain
bool(z == r)
# Check equality when converting back to natural domain
new_r == 100'u32

block:
var x, y, z, r: Fp[BabyBear]

x.fromUint(0x10000'u32)
y.fromUint(0x10000'u32)
z.fromUint(uint32((0x10000'u64 * 0x10000'u64) mod 0x78000001'u64))

r.prod(x, y)

var r_bytes: array[4, byte]
r_bytes.marshal(r, cpuEndian)
let new_r = cast[uint32](r_bytes)

check:
# Check equality in the Montgomery domain
bool(z == r)
# Check equality when converting back to natural domain
new_r == uint32((0x10000'u64 * 0x10000'u64) mod 0x78000001'u64)

test "Addition mod 2^31 - 2^24 + 1":
block:
var x, y, z: Fp[KoalaBear]

x.fromUint(50'u32)
y.fromUint(20'u32)
z.fromUint(70'u32)

x += y

var x_bytes: array[4, byte]
x_bytes.marshal(x, cpuEndian)
let new_x = cast[uint32](x_bytes)

check:
# Check equality in the Montgomery domain
bool(z == x)
# Check equality when converting back to natural domain
new_x == 70'u32

block:
var x, y, z: Fp[KoalaBear]

x.fromUint(0x7f000000'u32) # p-1
y.fromUint(1'u32)
z.fromUint(0'u32)

x += y

var x_bytes: array[4, byte]
x_bytes.marshal(x, cpuEndian)
let new_x = cast[uint32](x_bytes)

check:
# Check equality in the Montgomery domain
bool(z == x)
# Check equality when converting back to natural domain
new_x == 0'u32

test "Substraction mod 2^31 - 2^24 + 1":
block:
var x, y, z: Fp[KoalaBear]

x.fromUint(70'u32)
y.fromUint(20'u32)
z.fromUint(50'u32)

x -= y

var x_bytes: array[4, byte]
x_bytes.marshal(x, cpuEndian)
let new_x = cast[uint32](x_bytes)

check:
# Check equality in the Montgomery domain
bool(z == x)
# Check equality when converting back to natural domain
new_x == 50'u32

block:
var x, y, z: Fp[KoalaBear]

x.fromUint(0'u32)
y.fromUint(1'u32)
z.fromUint(0x7f000000'u32) # p-1

x -= y

var x_bytes: array[4, byte]
x_bytes.marshal(x, cpuEndian)
let new_x = cast[uint32](x_bytes)

check:
# Check equality in the Montgomery domain
bool(z == x)
# Check equality when converting back to natural domain
new_x == 0x7f000000'u32

test "Multiplication mod 2^31 - 2^24 + 1":
block:
var x, y, z, r: Fp[KoalaBear]

x.fromUint(12'u32)
y.fromUint(12'u32)
z.fromUint(144'u32)

r.prod(x, y)

var r_bytes: array[4, byte]
r_bytes.marshal(r, cpuEndian)
let new_r = cast[uint32](r_bytes)

check:
# Check equality in the Montgomery domain
bool(z == r)
# Check equality when converting back to natural domain
new_r == 144'u32

block:
var x, y, z, r: Fp[KoalaBear]

x.fromUint(0x10000'u32)
y.fromUint(0x20000'u32)
z.fromUint(uint32((0x10000'u64 * 0x20000'u64) mod 0x7f000001'u64))

r.prod(x, y)

var r_bytes: array[4, byte]
r_bytes.marshal(r, cpuEndian)
let new_r = cast[uint32](r_bytes)

check:
# Check equality in the Montgomery domain
bool(z == r)
# Check equality when converting back to natural domain
new_r == uint32((0x10000'u64 * 0x20000'u64) mod 0x7f000001'u64)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The tests for BabyBear and KoalaBear contain a lot of duplicated code for addition, subtraction, and multiplication. This can be refactored using helper procedures to make the tests more concise and maintainable.

Here are some example helper procedures you could add:

proc testAdd[F: static Algebra](x_val, y_val, z_val: uint32) =
  var x, y, z: Fp[F]
  x.fromUint(x_val)
  y.fromUint(y_val)
  z.fromUint(z_val)
  x += y
  var x_bytes: array[4, byte]
  x_bytes.marshal(x, cpuEndian)
  let new_x = cast[uint32](x_bytes)
  check:
    bool(z == x)
    new_x == z_val

proc testSub[F: static Algebra](x_val, y_val, z_val: uint32) =
  var x, y, z: Fp[F]
  x.fromUint(x_val)
  y.fromUint(y_val)
  z.fromUint(z_val)
  x -= y
  var x_bytes: array[4, byte]
  x_bytes.marshal(x, cpuEndian)
  let new_x = cast[uint32](x_bytes)
  check:
    bool(z == x)
    new_x == z_val

proc testMul[F: static Algebra](x_val, y_val, z_val: uint32) =
  var x, y, z, r: Fp[F]
  x.fromUint(x_val)
  y.fromUint(y_val)
  z.fromUint(z_val)
  r.prod(x, y)
  var r_bytes: array[4, byte]
  r_bytes.marshal(r, cpuEndian)
  let new_r = cast[uint32](r_bytes)
  check:
    bool(z == r)
    new_r == z_val

With these helpers, the tests become much simpler, for example:

test "Addition mod 2^31 - 2^27 + 1":
  testAdd[BabyBear](80'u32, 10'u32, 90'u32)
  testAdd[BabyBear](0x78000000'u32, 1'u32, 0'u32)

Copy link
Owner

@mratsim mratsim Sep 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a valid suggestion by Gemini

Comment on lines +49 to +138
# BabyBear ---------------------------------
block:
# "Little-endian" - 0
let x = 0'u64
let x_bytes = cast[array[8, byte]](x)
var f: Fp[BabyBear]
f.fromUint(x)

var r_bytes: array[8, byte]
marshal(r_bytes, f, littleEndian)
check: x_bytes == r_bytes

block:
# "Little-endian" - 1
let x = 1'u64
let x_bytes = cast[array[8, byte]](x)
var f: Fp[BabyBear]
f.fromUint(x)

var r_bytes: array[8, byte]
marshal(r_bytes, f, littleEndian)
check: x_bytes == r_bytes

block:
# "Little-endian" - 2^15
let x = 1'u64 shl 15
let x_bytes = cast[array[8, byte]](x)
var f: Fp[BabyBear]
f.fromUint(x)

var r_bytes: array[8, byte]
marshal(r_bytes, f, littleEndian)
check: x_bytes == r_bytes

block:
# "Little-endian" - p-1
let x = 2013265921'u64 - 1
let x_bytes = cast[array[8, byte]](x)
var f: Fp[BabyBear]
f.fromUint(x)

var r_bytes: array[8, byte]
marshal(r_bytes, f, littleEndian)
check: x_bytes == r_bytes

# KoalaBear ---------------------------------
block:
# "Little-endian" - 0
let x = 0'u64
let x_bytes = cast[array[8, byte]](x)
var f: Fp[KoalaBear]
f.fromUint(x)

var r_bytes: array[8, byte]
marshal(r_bytes, f, littleEndian)
check: x_bytes == r_bytes

block:
# "Little-endian" - 1
let x = 1'u64
let x_bytes = cast[array[8, byte]](x)
var f: Fp[KoalaBear]
f.fromUint(x)

var r_bytes: array[8, byte]
marshal(r_bytes, f, littleEndian)
check: x_bytes == r_bytes

block:
# "Little-endian" - 2^15
let x = 1'u64 shl 15
let x_bytes = cast[array[8, byte]](x)
var f: Fp[KoalaBear]
f.fromUint(x)

var r_bytes: array[8, byte]
marshal(r_bytes, f, littleEndian)
check: x_bytes == r_bytes

block:
# "Little-endian" - p-1
let x = 2130706433'u64 - 1
let x_bytes = cast[array[8, byte]](x)
var f: Fp[KoalaBear]
f.fromUint(x)

var r_bytes: array[8, byte]
marshal(r_bytes, f, littleEndian)
check: x_bytes == r_bytes

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There's a lot of repeated code in the IO tests for BabyBear and KoalaBear. You could reduce this duplication by creating a helper procedure. This would make the tests more concise and easier to maintain.

Here's an example of a helper procedure:

proc checkFieldIO[F: static Algebra](value: uint64) =
  let x_bytes = cast[array[8, byte]](value)
  var f: Fp[F]
  f.fromUint(value)

  var r_bytes: array[8, byte]
  marshal(r_bytes, f, littleEndian)
  check: x_bytes == r_bytes

Then you could refactor the test blocks like this:

# BabyBear ---------------------------------
block:
  # "Little-endian" - 0
  checkFieldIO[BabyBear](0'u64)

block:
  # "Little-endian" - 1
  checkFieldIO[BabyBear](1'u64)
# ... and so on for other test cases and KoalaBear.

Copy link
Owner

@mratsim mratsim Sep 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a valid suggestion by gemini

Copy link
Owner

@mratsim mratsim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just first pass

# -----------------------------------------------------------------------------

curve BabyBear: # BabyBear field used in SNARKs and zkVMs
testingCurve: true
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Testing curves is for fields/curves that aren't used in end-user applications like mod 101, it's unnecessary here

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixing

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Owner

@mratsim mratsim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The most important test files is t_finite_fields_vs_gmp.nim which enables comprehensive differential fuzzing vs GMP.

The other files are light sanity checks.

Comment on lines +169 to +402
test "Addition mod 2^31 - 2^27 + 1":
block:
var x, y, z: Fp[BabyBear]

x.fromUint(80'u32)
y.fromUint(10'u32)
z.fromUint(90'u32)

x += y

var x_bytes: array[4, byte]
x_bytes.marshal(x, cpuEndian)
let new_x = cast[uint32](x_bytes)

check:
# Check equality in the Montgomery domain
bool(z == x)
# Check equality when converting back to natural domain
new_x == 90'u32

block:
var x, y, z: Fp[BabyBear]

x.fromUint(0x78000000'u32) # p-1
y.fromUint(1'u32)
z.fromUint(0'u32)

x += y

var x_bytes: array[4, byte]
x_bytes.marshal(x, cpuEndian)
let new_x = cast[uint32](x_bytes)

check:
# Check equality in the Montgomery domain
bool(z == x)
# Check equality when converting back to natural domain
new_x == 0'u32

test "Substraction mod 2^31 - 2^27 + 1":
block:
var x, y, z: Fp[BabyBear]

x.fromUint(80'u32)
y.fromUint(10'u32)
z.fromUint(70'u32)

x -= y

var x_bytes: array[4, byte]
x_bytes.marshal(x, cpuEndian)
let new_x = cast[uint32](x_bytes)

check:
# Check equality in the Montgomery domain
bool(z == x)
# Check equality when converting back to natural domain
new_x == 70'u32

block:
var x, y, z: Fp[BabyBear]

x.fromUint(0'u32)
y.fromUint(1'u32)
z.fromUint(0x78000000'u32) # p-1

x -= y

var x_bytes: array[4, byte]
x_bytes.marshal(x, cpuEndian)
let new_x = cast[uint32](x_bytes)

check:
# Check equality in the Montgomery domain
bool(z == x)
# Check equality when converting back to natural domain
new_x == 0x78000000'u32

test "Multiplication mod 2^31 - 2^27 + 1":
block:
var x, y, z, r: Fp[BabyBear]

x.fromUint(10'u32)
y.fromUint(10'u32)
z.fromUint(100'u32)

r.prod(x, y)

var r_bytes: array[4, byte]
r_bytes.marshal(r, cpuEndian)
let new_r = cast[uint32](r_bytes)

check:
# Check equality in the Montgomery domain
bool(z == r)
# Check equality when converting back to natural domain
new_r == 100'u32

block:
var x, y, z, r: Fp[BabyBear]

x.fromUint(0x10000'u32)
y.fromUint(0x10000'u32)
z.fromUint(uint32((0x10000'u64 * 0x10000'u64) mod 0x78000001'u64))

r.prod(x, y)

var r_bytes: array[4, byte]
r_bytes.marshal(r, cpuEndian)
let new_r = cast[uint32](r_bytes)

check:
# Check equality in the Montgomery domain
bool(z == r)
# Check equality when converting back to natural domain
new_r == uint32((0x10000'u64 * 0x10000'u64) mod 0x78000001'u64)

test "Addition mod 2^31 - 2^24 + 1":
block:
var x, y, z: Fp[KoalaBear]

x.fromUint(50'u32)
y.fromUint(20'u32)
z.fromUint(70'u32)

x += y

var x_bytes: array[4, byte]
x_bytes.marshal(x, cpuEndian)
let new_x = cast[uint32](x_bytes)

check:
# Check equality in the Montgomery domain
bool(z == x)
# Check equality when converting back to natural domain
new_x == 70'u32

block:
var x, y, z: Fp[KoalaBear]

x.fromUint(0x7f000000'u32) # p-1
y.fromUint(1'u32)
z.fromUint(0'u32)

x += y

var x_bytes: array[4, byte]
x_bytes.marshal(x, cpuEndian)
let new_x = cast[uint32](x_bytes)

check:
# Check equality in the Montgomery domain
bool(z == x)
# Check equality when converting back to natural domain
new_x == 0'u32

test "Substraction mod 2^31 - 2^24 + 1":
block:
var x, y, z: Fp[KoalaBear]

x.fromUint(70'u32)
y.fromUint(20'u32)
z.fromUint(50'u32)

x -= y

var x_bytes: array[4, byte]
x_bytes.marshal(x, cpuEndian)
let new_x = cast[uint32](x_bytes)

check:
# Check equality in the Montgomery domain
bool(z == x)
# Check equality when converting back to natural domain
new_x == 50'u32

block:
var x, y, z: Fp[KoalaBear]

x.fromUint(0'u32)
y.fromUint(1'u32)
z.fromUint(0x7f000000'u32) # p-1

x -= y

var x_bytes: array[4, byte]
x_bytes.marshal(x, cpuEndian)
let new_x = cast[uint32](x_bytes)

check:
# Check equality in the Montgomery domain
bool(z == x)
# Check equality when converting back to natural domain
new_x == 0x7f000000'u32

test "Multiplication mod 2^31 - 2^24 + 1":
block:
var x, y, z, r: Fp[KoalaBear]

x.fromUint(12'u32)
y.fromUint(12'u32)
z.fromUint(144'u32)

r.prod(x, y)

var r_bytes: array[4, byte]
r_bytes.marshal(r, cpuEndian)
let new_r = cast[uint32](r_bytes)

check:
# Check equality in the Montgomery domain
bool(z == r)
# Check equality when converting back to natural domain
new_r == 144'u32

block:
var x, y, z, r: Fp[KoalaBear]

x.fromUint(0x10000'u32)
y.fromUint(0x20000'u32)
z.fromUint(uint32((0x10000'u64 * 0x20000'u64) mod 0x7f000001'u64))

r.prod(x, y)

var r_bytes: array[4, byte]
r_bytes.marshal(r, cpuEndian)
let new_r = cast[uint32](r_bytes)

check:
# Check equality in the Montgomery domain
bool(z == r)
# Check equality when converting back to natural domain
new_r == uint32((0x10000'u64 * 0x20000'u64) mod 0x7f000001'u64)

Copy link
Owner

@mratsim mratsim Sep 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a valid suggestion by Gemini

Comment on lines +49 to +138
# BabyBear ---------------------------------
block:
# "Little-endian" - 0
let x = 0'u64
let x_bytes = cast[array[8, byte]](x)
var f: Fp[BabyBear]
f.fromUint(x)

var r_bytes: array[8, byte]
marshal(r_bytes, f, littleEndian)
check: x_bytes == r_bytes

block:
# "Little-endian" - 1
let x = 1'u64
let x_bytes = cast[array[8, byte]](x)
var f: Fp[BabyBear]
f.fromUint(x)

var r_bytes: array[8, byte]
marshal(r_bytes, f, littleEndian)
check: x_bytes == r_bytes

block:
# "Little-endian" - 2^15
let x = 1'u64 shl 15
let x_bytes = cast[array[8, byte]](x)
var f: Fp[BabyBear]
f.fromUint(x)

var r_bytes: array[8, byte]
marshal(r_bytes, f, littleEndian)
check: x_bytes == r_bytes

block:
# "Little-endian" - p-1
let x = 2013265921'u64 - 1
let x_bytes = cast[array[8, byte]](x)
var f: Fp[BabyBear]
f.fromUint(x)

var r_bytes: array[8, byte]
marshal(r_bytes, f, littleEndian)
check: x_bytes == r_bytes

# KoalaBear ---------------------------------
block:
# "Little-endian" - 0
let x = 0'u64
let x_bytes = cast[array[8, byte]](x)
var f: Fp[KoalaBear]
f.fromUint(x)

var r_bytes: array[8, byte]
marshal(r_bytes, f, littleEndian)
check: x_bytes == r_bytes

block:
# "Little-endian" - 1
let x = 1'u64
let x_bytes = cast[array[8, byte]](x)
var f: Fp[KoalaBear]
f.fromUint(x)

var r_bytes: array[8, byte]
marshal(r_bytes, f, littleEndian)
check: x_bytes == r_bytes

block:
# "Little-endian" - 2^15
let x = 1'u64 shl 15
let x_bytes = cast[array[8, byte]](x)
var f: Fp[KoalaBear]
f.fromUint(x)

var r_bytes: array[8, byte]
marshal(r_bytes, f, littleEndian)
check: x_bytes == r_bytes

block:
# "Little-endian" - p-1
let x = 2130706433'u64 - 1
let x_bytes = cast[array[8, byte]](x)
var f: Fp[KoalaBear]
f.fromUint(x)

var r_bytes: array[8, byte]
marshal(r_bytes, f, littleEndian)
check: x_bytes == r_bytes

Copy link
Owner

@mratsim mratsim Sep 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a valid suggestion by gemini

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants