Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Edmund/continue poseidon #1313

Merged
merged 13 commits into from
Nov 13, 2023
Merged

Edmund/continue poseidon #1313

merged 13 commits into from
Nov 13, 2023

Conversation

edmundnoble
Copy link
Contributor

@edmundnoble edmundnoble commented Oct 25, 2023

This PR supersedes #1274.

Remaining work: check test vectors, check gas.
Edit: both done.

The performance improvements in this PR go from this:

defaultMain [bench "poseidon" $ whnf poseidon [1,2,3,4,5,6,7,8]]
benchmarking poseidon
time                 1.696 ms   (1.686 ms .. 1.710 ms)
                     0.999 R²   (0.999 R² .. 1.000 R²)
mean                 1.704 ms   (1.695 ms .. 1.722 ms)
std dev              41.47 μs   (25.65 μs .. 74.70 μs)
variance introduced by outliers: 13% (moderately inflated)

to this:

defaultMain [bench "poseidon" $ whnf poseidon [1,2,3,4,5,6,7,8]]
benchmarking poseidon
time                 1.222 ms   (1.215 ms .. 1.230 ms)
                     0.999 R²   (0.999 R² .. 1.000 R²)
mean                 1.222 ms   (1.212 ms .. 1.244 ms)
std dev              49.37 μs   (21.97 μs .. 99.33 μs)

and from this:

defaultMain [bench "poseidon" $ whnf poseidon [1,2]]
benchmarking poseidon
time                 182.7 μs   (182.1 μs .. 183.3 μs)
                     1.000 R²   (1.000 R² .. 1.000 R²)
mean                 182.0 μs   (181.2 μs .. 182.7 μs)
std dev              2.628 μs   (2.202 μs .. 3.368 μs)

to this:

defaultMain [bench "poseidon" $ whnf poseidon [1,2]]
benchmarking poseidon
time                 159.5 μs   (155.2 μs .. 164.8 μs)
                     0.996 R²   (0.993 R² .. 0.999 R²)
mean                 155.5 μs   (153.7 μs .. 157.8 μs)
std dev              6.763 μs   (5.227 μs .. 10.27 μs)
variance introduced by outliers: 43% (moderately inflated)

PR checklist:

  • Test coverage for the proposed changes
  • PR description contains example output from repl interaction or a snippet from unit test output
  • Documentation has been updated if new natives or FV properties have been added. To generate new documentation, issue cabal run tests. If they pass locally, docs are generated.
  • Any changes that could be relevant to users have been recorded in the changelog
  • In case of changes to the Pact trace output (pact -t), make sure pact-lsp is in sync.

Additionally, please justify why you should or should not do the following:

  • Confirm replay/back compat
  • Benchmark regressions
  • (For Kadena engineers) Run integration-tests against a Chainweb built with this version of Pact

@edmundnoble
Copy link
Contributor Author

Gas graph:
2023-10-26-172425_666x447_scrot
The actual inputs don't affect the time taken to compute the hash from what I can tell, only the number of them, so that's the X axis. The Y axis is micros * 2.5, i.e. gas as a unit of time.

Copy link
Member

@emilypi emilypi left a comment

Choose a reason for hiding this comment

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

Looking good so far

src/Crypto/Hash/PoseidonNative.hs Show resolved Hide resolved
, "The input consists of 1 to 8 integers."
, "The output is the hash result as an integer."
, "Note: This is a reference version of the Poseidon hash function used by Hack-a-Chain."
])
Copy link
Member

Choose a reason for hiding this comment

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

This is non-idiomatic of the way we write native docs. There's no need to intro this now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let me know if the new version is idiomatic.

tests/PactTests.hs Outdated Show resolved Hide resolved
Comment on lines +25 to +27
let in2 = mulmod inVal inVal
in4 = mulmod in2 in2
in mulmod in4 inVal
Copy link
Contributor

Choose a reason for hiding this comment

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

My group theory is rusty, but can you do mod just once after all the multiplications?

Copy link
Contributor

Choose a reason for hiding this comment

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

I tried some basic tests and that seems to hold true:

module Main where

modulus :: Integer
modulus = 53

mulmod :: Integer -> Integer -> Integer
mulmod a b = (a * b) `mod` modulus

sig :: Integer -> Integer
sig inVal =
  let in2 = mulmod inVal inVal
      in4 = mulmod in2 in2
  in mulmod in4 inVal

mulmod2:: Integer -> Integer -> Integer
mulmod2 a b = (a * b)

sig2 :: Integer -> Integer
sig2 inVal =
  let in2 = mulmod2 inVal inVal
      in4 = mulmod2 in2 in2
  in mulmod2 in4 inVal `mod` modulus

main :: IO ()
main = do
    print $ sig 123
    print $ sig 456
    print $ sig 789
    print $ sig2 123
    print $ sig2 456
    print $ sig2 789

Copy link
Contributor

Choose a reason for hiding this comment

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

Alright, I grabbed a pen and paper today! So the question effectively is whether ab mod n = (a mod n) × (b mod n) mod n. Let's use brute force and assume a = k₁n + r₁, b = k₂n + r₂ (with the usual conditions of r₁, r₂ < n). Then:

ab mod n = (...) × n + r₁r₂ mod n = r₁r₂ mod n

while for the rhs

(a mod n) × (b mod n) mod n = r₁r₂ mod n

so that clearly holds, and mod can happen just once. That is assuming nothing funny happens wrt negative signs here and haskell mod behaves like math mod (which IIRC it does).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks Georg, worth a try.

Just tried it, it's not any faster according to my benchmark to do

(inVal ^ 5) `mod` modulus

than to do what we're doing now. I think that's what you were suggesting.

Copy link
Contributor

@0xd34df00d 0xd34df00d Nov 1, 2023

Choose a reason for hiding this comment

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

Yep, that was it.

Well, perhaps it's not that much of a hotspot then! Up to you what expression to keep, although I'd probably vote for inVal ^ 5 — this way it's more obvious what's happening.

Copy link
Member

Choose a reason for hiding this comment

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

I think sig is just strangely named (due a the paper reference impl maybe?).

That said (inVal ^ 5) if the ^ implementation isn't applying modulo may be significantly slower because of the size the integers can grow. The modulus is quite a large prime, so there are numbers quite large there.

@edmundnoble
Copy link
Contributor Author

Here's where exactly the constants came from: https://github.com/iden3/circomlibjs/blob/main/src/poseidon_constants.js. I've patched the PR to use the exact same constants (in hex, just like they do it) so it's easier to compare. I have verified by hand that the output for these test vectors matches the output from https://github.com/iden3/circomlibjs/blob/main/src/poseidon_reference.js.

Copy link
Contributor

@imalsogreg imalsogreg left a comment

Choose a reason for hiding this comment

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

Looks fantastic!

  • Ran it locally in repl, outputs look reasonable
  • Rejects some bad arguments as expected
  • Unit tests look good
  • Gas tests look good
  • Gas analysis matches the benchmarks
  • Repl complains when DisablePact410

import qualified Data.Primitive.SmallArray as SmallArray

modulus :: Integer
modulus = 21888242871839275222246405745257275088548364400416034343698204186575808495617
Copy link
Member

Choose a reason for hiding this comment

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

This constant is in our ZK impl as well. I wonder whether we want a common place for it.

summod :: Integer -> Integer -> Integer
summod a b = (a + b) `mod` modulus

sig :: Integer -> Integer
Copy link
Member

Choose a reason for hiding this comment

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

sig is just a^5 mod n, is the name due to the reference impl?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes I think.

in mulmod in4 inVal

ark :: Integer -> Integer -> Integer
ark inVal cc = summod inVal cc
Copy link
Member

Choose a reason for hiding this comment

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

ark = summod ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I kept the names just in case this makes it easier to compare to the ref. impl.

Comment on lines +25 to +27
let in2 = mulmod inVal inVal
in4 = mulmod in2 in2
in mulmod in4 inVal
Copy link
Member

Choose a reason for hiding this comment

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

I think sig is just strangely named (due a the paper reference impl maybe?).

That said (inVal ^ 5) if the ^ implementation isn't applying modulo may be significantly slower because of the size the integers can grow. The modulus is quite a large prime, so there are numbers quite large there.

ark inVal cc = summod inVal cc

data NineRings =
NineRings !Integer !Integer !Integer !Integer !Integer !Integer !Integer !Integer !Integer
Copy link
Member

Choose a reason for hiding this comment

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

Same amount Kobe and Shaq have combined.

poseidon :: [Integer] -> Integer
poseidon inputs =
if nInputs > 8 || nInputs == 0
then error $ "poseidon: number of inputs not between 0 and 8: " <> show nInputs
Copy link
Member

Choose a reason for hiding this comment

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

error here worries me ever so slightly, only since the function spits out package hashes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We never get to this point anyway, because of the case in poseidonDef. But I can replace this with a bespoke exception type if you'd prefer.

Copy link
Member

Choose a reason for hiding this comment

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

Should be okay 👍

@edmundnoble edmundnoble merged commit 422cec4 into master Nov 13, 2023
8 checks passed
@edmundnoble edmundnoble mentioned this pull request Nov 14, 2023
8 tasks
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.

None yet

8 participants