diff --git a/src/Cairo/Felt.php b/src/Cairo/Felt.php index 6ac80d3..35a7055 100644 --- a/src/Cairo/Felt.php +++ b/src/Cairo/Felt.php @@ -31,7 +31,7 @@ public static function assertInCairoVMRange($value) throw new InvalidArgumentException('The value to assertInCairoVMRange function in not support.'); } if ( - !($value->compare(Constants::ZERO()) >= 0 && $value->compare(Utils::toBn(Constants::FIELD_PRIME)) < 0) + !($value->compare(Constants::ZERO()) >= 0 && $value->compare(Utils::toBn('0x' . Constants::FIELD_PRIME)) < 0) ) { throw new InvalidArgumentException('The value in expected to be in the range [0,' . Constants::FIELD_PRIME . '].'); } diff --git a/src/Crypto/PoseidonHash.php b/src/Crypto/PoseidonHash.php new file mode 100644 index 0000000..c2ad550 --- /dev/null +++ b/src/Crypto/PoseidonHash.php @@ -0,0 +1,171 @@ + + * + * @author Peter Lai + * @license MIT + */ + +namespace StarkNet\Crypto; + +use StarkNet\Utils; +use StarkNet\Constants; + +class PoseidonHash +{ + const RATE = 2; + const CAPACITY = 1; + const ROUNDS_FULL = 8; + const ROUNDS_PARTIAL = 83; + + private static $fieldPrime = null; + private static $roundConstants = null; + private static $mds = null; + + private static function fieldPrime() + { + if (self::$fieldPrime === null) { + self::$fieldPrime = Utils::toBn('0x' . Constants::FIELD_PRIME, 16); + } + return self::$fieldPrime; + } + + private static function field($value) + { + if (!($value instanceof BigInteger)) { + $value = Utils::toBn($value); + } + + list(, $remainder) = $value->divide(self::fieldPrime()); + + if ($remainder->is_negative) { + $remainder = $remainder->add(self::fieldPrime()); + } + + return $remainder; + } + + private static function roundConstant($name, $index) + { + return self::field(Utils::toBn('0x' . hash('sha256', $name . (string) $index))); + } + + private static function roundConstants() + { + if (self::$roundConstants === null) { + self::$roundConstants = array(); + $rounds = self::ROUNDS_FULL + self::ROUNDS_PARTIAL; + $width = self::RATE + self::CAPACITY; + for ($i = 0; $i < $rounds; $i++) { + $row = array(); + for ($j = 0; $j < $width; $j++) { + $row[] = self::roundConstant('Hades', $width * $i + $j); + } + self::$roundConstants[] = $row; + } + } + return self::$roundConstants; + } + + private static function mds() + { + if (self::$mds === null) { + self::$mds = array( + array(self::field(3), self::field(1), self::field(1)), + array(self::field(1), self::field(-1), self::field(1)), + array(self::field(1), self::field(1), self::field(-2)), + ); + } + return self::$mds; + } + + private static function sbox($value) + { + $cube = $value->multiply($value)->multiply($value); + return self::field($cube); + } + + private static function poseidonRound($values, $isFull, $index) + { + $roundConstants = self::roundConstants(); + for ($i = 0; $i < count($values); $i++) { + $values[$i] = self::field($values[$i]->add($roundConstants[$index][$i])); + } + if ($isFull) { + for ($i = 0; $i < count($values); $i++) { + $values[$i] = self::sbox($values[$i]); + } + } else { + $last = count($values) - 1; + $values[$last] = self::sbox($values[$last]); + } + $result = array(); + $mds = self::mds(); + foreach ($mds as $row) { + $acc = Constants::ZERO(); + for ($i = 0; $i < count($values); $i++) { + $product = $row[$i]->multiply($values[$i]); + $acc = $acc->add($product); + } + $result[] = self::field($acc); + } + return $result; + } + + public static function poseidonHash($values) + { + $width = self::RATE + self::CAPACITY; + if (count($values) !== $width) { + throw new \InvalidArgumentException('Poseidon: wrong values length'); + } + for ($i = 0; $i < count($values); $i++) { + $values[$i] = self::field($values[$i]); + } + $roundIndex = 0; + $halfRoundsFull = intval(self::ROUNDS_FULL / 2); + for ($i = 0; $i < $halfRoundsFull; $i++) { + $values = self::poseidonRound($values, true, $roundIndex); + $roundIndex++; + } + for ($i = 0; $i < self::ROUNDS_PARTIAL; $i++) { + $values = self::poseidonRound($values, false, $roundIndex); + $roundIndex++; + } + for ($i = 0; $i < $halfRoundsFull; $i++) { + $values = self::poseidonRound($values, true, $roundIndex); + $roundIndex++; + } + return $values; + } + + public static function hashMany($values) + { + $padded = array_values($values); + $padded[] = 1; + while ((count($padded) % self::RATE) !== 0) { + $padded[] = 0; + } + + $state = array( + Constants::ZERO(), + Constants::ZERO(), + Constants::ZERO() + ); + + for ($i = 0; $i < count($padded); $i += self::RATE) { + for ($j = 0; $j < self::RATE; $j++) { + $state[$j] = $state[$j]->add(Utils::toBn($padded[$i + $j])); + } + $state = self::poseidonHash($state); + } + return $state[0]; + } + + public static function computeHashOnElements($data) + { + return '0x' . Utils::removeLeadingZero(Poseidon::hashMany($data)->toHex()); + } +} \ No newline at end of file diff --git a/test/unit/PedersenHashTest.php b/test/unit/PedersenHashTest.php index a5aac8c..4329468 100644 --- a/test/unit/PedersenHashTest.php +++ b/test/unit/PedersenHashTest.php @@ -14,25 +14,6 @@ */ class PedersenHashTest extends TestCase { - /** - * $fixture - * - * @var array - */ - // protected $fixture; - - /** - * setUp - * - * @return void - */ - // public function setUp(): void - // { - // parent::setUp(); - // $f = file_get_contents( dirname(__DIR__) . '/fixtures/issue2.json'); - // $this->fixture = json_decode($f); - // } - /** * testHash * diff --git a/test/unit/PoseidonHashTest.php b/test/unit/PoseidonHashTest.php new file mode 100644 index 0000000..80a7f57 --- /dev/null +++ b/test/unit/PoseidonHashTest.php @@ -0,0 +1,29 @@ +assertEquals('2457757238178986673695038558497063891521456354791980183317105434323761563347', $result[0]->toString()); + } +} \ No newline at end of file