From 40c92eadc4df01f497c93d82b4da17c259b76a37 Mon Sep 17 00:00:00 2001 From: Anton Filonenko Date: Fri, 12 Jun 2026 18:18:48 +0300 Subject: [PATCH 01/15] feat: add core protocol packages Initial Go module with the protocol primitives: - pkg/core: wire types (blocks, ops, receipts, URIs) with generated tuple-mode CBOR codecs - pkg/decimal: arbitrary-precision decimal with tag-2 CBOR encoding - pkg/cborx: canonical envelope/frame/version codec - pkg/abiutil: shared ABI type singletons - pkg/eip712: EIP-712 digest and signer recovery - pkg/bls: BN254 keygen/sign/aggregate/verify, cluster signature verification, vault withdrawal-ID derivation - pkg/receipt: burn/mint receipt digests and verifier - preimage freeze: byte-exact golden tests for signing preimages, enforced by scripts/ci/check-preimage-goldens.sh --- go.mod | 32 + go.sum | 69 + pkg/abiutil/types.go | 48 + pkg/bls/bls.go | 315 ++ pkg/bls/bls_property_test.go | 195 + pkg/bls/bls_test.go | 428 +++ pkg/bls/preimage_golden_test.go | 371 ++ pkg/bls/verify.go | 187 + pkg/cborx/bigint.go | 154 + pkg/cborx/bigint_test.go | 199 + pkg/cborx/decimal.go | 125 + pkg/cborx/decimal_test.go | 165 + pkg/cborx/doc.go | 74 + pkg/cborx/envelope.go | 133 + pkg/cborx/envelope_test.go | 130 + pkg/cborx/frame.go | 143 + pkg/cborx/frame_test.go | 75 + pkg/cborx/goldens_test.go | 563 +++ pkg/cborx/hash.go | 98 + pkg/cborx/hash_test.go | 151 + pkg/cborx/maybe.go | 58 + pkg/cborx/maybe_test.go | 84 + pkg/cborx/rfc_determinism_test.go | 102 + pkg/cborx/time.go | 82 + pkg/cborx/time_test.go | 92 + pkg/cborx/version.go | 38 + pkg/core/block.go | 395 ++ pkg/core/bloom.go | 9 + pkg/core/cbor_gen.go | 3386 +++++++++++++++++ pkg/core/entry_type.go | 89 + pkg/core/gen/main.go | 65 + pkg/core/generate.go | 11 + pkg/core/operations.go | 172 + pkg/core/payload_codec.go | 211 + pkg/core/preimage_golden_test.go | 499 +++ pkg/core/types.go | 90 + pkg/core/uri.go | 280 ++ pkg/decimal/cbor.go | 231 ++ pkg/decimal/const.go | 63 + pkg/decimal/const_test.go | 39 + pkg/decimal/decimal-go.go | 415 ++ pkg/decimal/decimal.go | 1808 +++++++++ pkg/decimal/decimal_bench_test.go | 295 ++ pkg/decimal/decimal_test.go | 3331 ++++++++++++++++ pkg/decimal/functional_test.go | 446 +++ pkg/decimal/rounding.go | 160 + pkg/eip712/eip712.go | 233 ++ pkg/eip712/eip712_property_test.go | 357 ++ pkg/receipt/receipt_verifier.go | 300 ++ pkg/receipt/receipt_verifier_test.go | 399 ++ pkg/receipt/static_signer_source.go | 51 + scripts/ci/check-preimage-goldens.sh | 84 + .../primitives/addr20_vitalik_like.golden.hex | 1 + .../primitives/addr20_vitalik_like.input.json | 3 + .../cbor/primitives/addr20_zero.golden.hex | 1 + .../cbor/primitives/addr20_zero.input.json | 3 + .../primitives/bigint_boundary_255.golden.hex | 1 + .../primitives/bigint_boundary_255.input.json | 4 + .../primitives/bigint_boundary_256.golden.hex | 1 + .../primitives/bigint_boundary_256.input.json | 4 + .../primitives/bigint_minus_25.golden.hex | 1 + .../primitives/bigint_minus_25.input.json | 4 + .../primitives/bigint_minus_one.golden.hex | 1 + .../primitives/bigint_minus_one.input.json | 4 + .../bigint_neg_two_pow_128.golden.hex | 1 + .../bigint_neg_two_pow_128.input.json | 4 + .../cbor/primitives/bigint_one.golden.hex | 1 + .../cbor/primitives/bigint_one.input.json | 4 + .../primitives/bigint_two_pow_128.golden.hex | 1 + .../primitives/bigint_two_pow_128.input.json | 4 + .../cbor/primitives/bigint_u32_max.golden.hex | 1 + .../cbor/primitives/bigint_u32_max.input.json | 4 + .../bigint_u32_max_plus_one.golden.hex | 1 + .../bigint_u32_max_plus_one.input.json | 4 + .../cbor/primitives/bigint_u64_max.golden.hex | 1 + .../cbor/primitives/bigint_u64_max.input.json | 4 + .../cbor/primitives/bigint_zero.golden.hex | 1 + .../cbor/primitives/bigint_zero.input.json | 4 + .../primitives/decimal_mega_unit.golden.hex | 1 + .../primitives/decimal_mega_unit.input.json | 4 + .../primitives/decimal_neg_half.golden.hex | 1 + .../primitives/decimal_neg_half.input.json | 4 + .../cbor/primitives/decimal_one.golden.hex | 1 + .../cbor/primitives/decimal_one.input.json | 4 + .../decimal_one_point_five.golden.hex | 1 + .../decimal_one_point_five.input.json | 4 + .../decimal_ten_pow_40_at_-18.golden.hex | 1 + .../decimal_ten_pow_40_at_-18.input.json | 4 + .../decimal_wei_precision.golden.hex | 1 + .../decimal_wei_precision.input.json | 4 + .../cbor/primitives/decimal_zero.golden.hex | 1 + .../cbor/primitives/decimal_zero.input.json | 4 + .../envelope_v1_bigint_one.golden.hex | 1 + .../envelope_v1_bigint_one.input.json | 4 + .../envelope_v1_bigint_zero.golden.hex | 1 + .../envelope_v1_bigint_zero.input.json | 4 + .../primitives/hash32_incrementing.golden.hex | 1 + .../primitives/hash32_incrementing.input.json | 3 + .../cbor/primitives/hash32_max.golden.hex | 1 + .../cbor/primitives/hash32_max.input.json | 3 + .../cbor/primitives/hash32_one.golden.hex | 1 + .../cbor/primitives/hash32_one.input.json | 3 + .../cbor/primitives/hash32_zero.golden.hex | 1 + .../cbor/primitives/hash32_zero.input.json | 3 + .../primitives/maybe_bigint_nil.golden.hex | 1 + .../primitives/maybe_bigint_nil.input.json | 3 + .../maybe_bigint_present_negative.golden.hex | 1 + .../maybe_bigint_present_negative.input.json | 4 + .../maybe_bigint_present_positive.golden.hex | 1 + .../maybe_bigint_present_positive.input.json | 4 + .../primitives/maybe_bigint_zero.golden.hex | 1 + .../primitives/maybe_bigint_zero.input.json | 4 + .../cbor/primitives/time_epoch.golden.hex | 1 + .../cbor/primitives/time_epoch.input.json | 3 + .../primitives/time_minus_one_ns.golden.hex | 1 + .../primitives/time_minus_one_ns.input.json | 3 + .../cbor/primitives/time_modern.golden.hex | 1 + .../cbor/primitives/time_modern.input.json | 3 + .../cbor/primitives/time_one_ns.golden.hex | 1 + .../cbor/primitives/time_one_ns.input.json | 3 + .../block_header/empty_accounts.golden.hex | 1 + .../block_header/empty_accounts.input.json | 12 + .../block_header/single_account.golden.hex | 1 + .../block_header/single_account.input.json | 18 + .../preimages/entry_hash/transfer.golden.hex | 1 + .../preimages/entry_hash/transfer.input.json | 11 + .../finalized_withdrawal/header.golden.hex | 1 + .../finalized_withdrawal/header.input.json | 10 + .../preimages/op_payload/repeg.golden.hex | 1 + .../preimages/op_payload/repeg.input.json | 15 + .../op_payload/session_challenge.golden.hex | 1 + .../op_payload/session_challenge.input.json | 12 + .../op_payload/session_close.golden.hex | 1 + .../op_payload/session_close.input.json | 13 + .../preimages/op_payload/swap.golden.hex | 1 + .../preimages/op_payload/swap.input.json | 18 + .../transfer_single_asset.golden.hex | 1 + .../transfer_single_asset.input.json | 16 + .../op_payload/withdrawal.golden.hex | 1 + .../op_payload/withdrawal.input.json | 14 + .../bls/aggregate_sig.golden.hex | 1 + .../bls/aggregate_sig.input.json | 12 + .../bls/g1_generator.golden.hex | 1 + .../bls/g1_generator.input.json | 4 + .../bls/g1_identity.golden.hex | 1 + .../bls/g1_identity.input.json | 4 + .../bls/g1_random.golden.hex | 1 + .../bls/g1_random.input.json | 7 + .../bls/g2_generator.golden.hex | 1 + .../bls/g2_generator.input.json | 4 + .../bls/g2_identity.golden.hex | 1 + .../bls/g2_identity.input.json | 4 + .../bls/g2_random.golden.hex | 1 + .../bls/g2_random.input.json | 9 + 154 files changed, 17889 insertions(+) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 pkg/abiutil/types.go create mode 100644 pkg/bls/bls.go create mode 100644 pkg/bls/bls_property_test.go create mode 100644 pkg/bls/bls_test.go create mode 100644 pkg/bls/preimage_golden_test.go create mode 100644 pkg/bls/verify.go create mode 100644 pkg/cborx/bigint.go create mode 100644 pkg/cborx/bigint_test.go create mode 100644 pkg/cborx/decimal.go create mode 100644 pkg/cborx/decimal_test.go create mode 100644 pkg/cborx/doc.go create mode 100644 pkg/cborx/envelope.go create mode 100644 pkg/cborx/envelope_test.go create mode 100644 pkg/cborx/frame.go create mode 100644 pkg/cborx/frame_test.go create mode 100644 pkg/cborx/goldens_test.go create mode 100644 pkg/cborx/hash.go create mode 100644 pkg/cborx/hash_test.go create mode 100644 pkg/cborx/maybe.go create mode 100644 pkg/cborx/maybe_test.go create mode 100644 pkg/cborx/rfc_determinism_test.go create mode 100644 pkg/cborx/time.go create mode 100644 pkg/cborx/time_test.go create mode 100644 pkg/cborx/version.go create mode 100644 pkg/core/block.go create mode 100644 pkg/core/bloom.go create mode 100644 pkg/core/cbor_gen.go create mode 100644 pkg/core/entry_type.go create mode 100644 pkg/core/gen/main.go create mode 100644 pkg/core/generate.go create mode 100644 pkg/core/operations.go create mode 100644 pkg/core/payload_codec.go create mode 100644 pkg/core/preimage_golden_test.go create mode 100644 pkg/core/types.go create mode 100644 pkg/core/uri.go create mode 100644 pkg/decimal/cbor.go create mode 100644 pkg/decimal/const.go create mode 100644 pkg/decimal/const_test.go create mode 100644 pkg/decimal/decimal-go.go create mode 100644 pkg/decimal/decimal.go create mode 100644 pkg/decimal/decimal_bench_test.go create mode 100644 pkg/decimal/decimal_test.go create mode 100644 pkg/decimal/functional_test.go create mode 100644 pkg/decimal/rounding.go create mode 100644 pkg/eip712/eip712.go create mode 100644 pkg/eip712/eip712_property_test.go create mode 100644 pkg/receipt/receipt_verifier.go create mode 100644 pkg/receipt/receipt_verifier_test.go create mode 100644 pkg/receipt/static_signer_source.go create mode 100755 scripts/ci/check-preimage-goldens.sh create mode 100644 testdata/cbor/primitives/addr20_vitalik_like.golden.hex create mode 100644 testdata/cbor/primitives/addr20_vitalik_like.input.json create mode 100644 testdata/cbor/primitives/addr20_zero.golden.hex create mode 100644 testdata/cbor/primitives/addr20_zero.input.json create mode 100644 testdata/cbor/primitives/bigint_boundary_255.golden.hex create mode 100644 testdata/cbor/primitives/bigint_boundary_255.input.json create mode 100644 testdata/cbor/primitives/bigint_boundary_256.golden.hex create mode 100644 testdata/cbor/primitives/bigint_boundary_256.input.json create mode 100644 testdata/cbor/primitives/bigint_minus_25.golden.hex create mode 100644 testdata/cbor/primitives/bigint_minus_25.input.json create mode 100644 testdata/cbor/primitives/bigint_minus_one.golden.hex create mode 100644 testdata/cbor/primitives/bigint_minus_one.input.json create mode 100644 testdata/cbor/primitives/bigint_neg_two_pow_128.golden.hex create mode 100644 testdata/cbor/primitives/bigint_neg_two_pow_128.input.json create mode 100644 testdata/cbor/primitives/bigint_one.golden.hex create mode 100644 testdata/cbor/primitives/bigint_one.input.json create mode 100644 testdata/cbor/primitives/bigint_two_pow_128.golden.hex create mode 100644 testdata/cbor/primitives/bigint_two_pow_128.input.json create mode 100644 testdata/cbor/primitives/bigint_u32_max.golden.hex create mode 100644 testdata/cbor/primitives/bigint_u32_max.input.json create mode 100644 testdata/cbor/primitives/bigint_u32_max_plus_one.golden.hex create mode 100644 testdata/cbor/primitives/bigint_u32_max_plus_one.input.json create mode 100644 testdata/cbor/primitives/bigint_u64_max.golden.hex create mode 100644 testdata/cbor/primitives/bigint_u64_max.input.json create mode 100644 testdata/cbor/primitives/bigint_zero.golden.hex create mode 100644 testdata/cbor/primitives/bigint_zero.input.json create mode 100644 testdata/cbor/primitives/decimal_mega_unit.golden.hex create mode 100644 testdata/cbor/primitives/decimal_mega_unit.input.json create mode 100644 testdata/cbor/primitives/decimal_neg_half.golden.hex create mode 100644 testdata/cbor/primitives/decimal_neg_half.input.json create mode 100644 testdata/cbor/primitives/decimal_one.golden.hex create mode 100644 testdata/cbor/primitives/decimal_one.input.json create mode 100644 testdata/cbor/primitives/decimal_one_point_five.golden.hex create mode 100644 testdata/cbor/primitives/decimal_one_point_five.input.json create mode 100644 testdata/cbor/primitives/decimal_ten_pow_40_at_-18.golden.hex create mode 100644 testdata/cbor/primitives/decimal_ten_pow_40_at_-18.input.json create mode 100644 testdata/cbor/primitives/decimal_wei_precision.golden.hex create mode 100644 testdata/cbor/primitives/decimal_wei_precision.input.json create mode 100644 testdata/cbor/primitives/decimal_zero.golden.hex create mode 100644 testdata/cbor/primitives/decimal_zero.input.json create mode 100644 testdata/cbor/primitives/envelope_v1_bigint_one.golden.hex create mode 100644 testdata/cbor/primitives/envelope_v1_bigint_one.input.json create mode 100644 testdata/cbor/primitives/envelope_v1_bigint_zero.golden.hex create mode 100644 testdata/cbor/primitives/envelope_v1_bigint_zero.input.json create mode 100644 testdata/cbor/primitives/hash32_incrementing.golden.hex create mode 100644 testdata/cbor/primitives/hash32_incrementing.input.json create mode 100644 testdata/cbor/primitives/hash32_max.golden.hex create mode 100644 testdata/cbor/primitives/hash32_max.input.json create mode 100644 testdata/cbor/primitives/hash32_one.golden.hex create mode 100644 testdata/cbor/primitives/hash32_one.input.json create mode 100644 testdata/cbor/primitives/hash32_zero.golden.hex create mode 100644 testdata/cbor/primitives/hash32_zero.input.json create mode 100644 testdata/cbor/primitives/maybe_bigint_nil.golden.hex create mode 100644 testdata/cbor/primitives/maybe_bigint_nil.input.json create mode 100644 testdata/cbor/primitives/maybe_bigint_present_negative.golden.hex create mode 100644 testdata/cbor/primitives/maybe_bigint_present_negative.input.json create mode 100644 testdata/cbor/primitives/maybe_bigint_present_positive.golden.hex create mode 100644 testdata/cbor/primitives/maybe_bigint_present_positive.input.json create mode 100644 testdata/cbor/primitives/maybe_bigint_zero.golden.hex create mode 100644 testdata/cbor/primitives/maybe_bigint_zero.input.json create mode 100644 testdata/cbor/primitives/time_epoch.golden.hex create mode 100644 testdata/cbor/primitives/time_epoch.input.json create mode 100644 testdata/cbor/primitives/time_minus_one_ns.golden.hex create mode 100644 testdata/cbor/primitives/time_minus_one_ns.input.json create mode 100644 testdata/cbor/primitives/time_modern.golden.hex create mode 100644 testdata/cbor/primitives/time_modern.input.json create mode 100644 testdata/cbor/primitives/time_one_ns.golden.hex create mode 100644 testdata/cbor/primitives/time_one_ns.input.json create mode 100644 testdata/goldens/preimages/block_header/empty_accounts.golden.hex create mode 100644 testdata/goldens/preimages/block_header/empty_accounts.input.json create mode 100644 testdata/goldens/preimages/block_header/single_account.golden.hex create mode 100644 testdata/goldens/preimages/block_header/single_account.input.json create mode 100644 testdata/goldens/preimages/entry_hash/transfer.golden.hex create mode 100644 testdata/goldens/preimages/entry_hash/transfer.input.json create mode 100644 testdata/goldens/preimages/finalized_withdrawal/header.golden.hex create mode 100644 testdata/goldens/preimages/finalized_withdrawal/header.input.json create mode 100644 testdata/goldens/preimages/op_payload/repeg.golden.hex create mode 100644 testdata/goldens/preimages/op_payload/repeg.input.json create mode 100644 testdata/goldens/preimages/op_payload/session_challenge.golden.hex create mode 100644 testdata/goldens/preimages/op_payload/session_challenge.input.json create mode 100644 testdata/goldens/preimages/op_payload/session_close.golden.hex create mode 100644 testdata/goldens/preimages/op_payload/session_close.input.json create mode 100644 testdata/goldens/preimages/op_payload/swap.golden.hex create mode 100644 testdata/goldens/preimages/op_payload/swap.input.json create mode 100644 testdata/goldens/preimages/op_payload/transfer_single_asset.golden.hex create mode 100644 testdata/goldens/preimages/op_payload/transfer_single_asset.input.json create mode 100644 testdata/goldens/preimages/op_payload/withdrawal.golden.hex create mode 100644 testdata/goldens/preimages/op_payload/withdrawal.input.json create mode 100644 testdata/goldens/solidity-preimages/bls/aggregate_sig.golden.hex create mode 100644 testdata/goldens/solidity-preimages/bls/aggregate_sig.input.json create mode 100644 testdata/goldens/solidity-preimages/bls/g1_generator.golden.hex create mode 100644 testdata/goldens/solidity-preimages/bls/g1_generator.input.json create mode 100644 testdata/goldens/solidity-preimages/bls/g1_identity.golden.hex create mode 100644 testdata/goldens/solidity-preimages/bls/g1_identity.input.json create mode 100644 testdata/goldens/solidity-preimages/bls/g1_random.golden.hex create mode 100644 testdata/goldens/solidity-preimages/bls/g1_random.input.json create mode 100644 testdata/goldens/solidity-preimages/bls/g2_generator.golden.hex create mode 100644 testdata/goldens/solidity-preimages/bls/g2_generator.input.json create mode 100644 testdata/goldens/solidity-preimages/bls/g2_identity.golden.hex create mode 100644 testdata/goldens/solidity-preimages/bls/g2_identity.input.json create mode 100644 testdata/goldens/solidity-preimages/bls/g2_random.golden.hex create mode 100644 testdata/goldens/solidity-preimages/bls/g2_random.input.json diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3e4720c --- /dev/null +++ b/go.mod @@ -0,0 +1,32 @@ +module github.com/layer-3/clearnet-sdk + +go 1.26 + +require ( + github.com/consensys/gnark-crypto v0.20.1 + github.com/ethereum/go-ethereum v1.17.3 + github.com/fxamacker/cbor/v2 v2.9.1 + github.com/ipfs/go-cid v0.0.6 + github.com/whyrusleeping/cbor-gen v0.3.1 + golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 +) + +require ( + github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect + github.com/bits-and-blooms/bitset v1.24.4 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect + github.com/holiman/uint256 v1.3.2 // indirect + github.com/klauspost/cpuid/v2 v2.0.9 // indirect + github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 // indirect + github.com/minio/sha256-simd v1.0.0 // indirect + github.com/mr-tron/base58 v1.1.3 // indirect + github.com/multiformats/go-base32 v0.0.3 // indirect + github.com/multiformats/go-base36 v0.1.0 // indirect + github.com/multiformats/go-multibase v0.0.3 // indirect + github.com/multiformats/go-multihash v0.0.13 // indirect + github.com/multiformats/go-varint v0.0.5 // indirect + github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/x448/float16 v0.8.4 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/sys v0.41.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6911993 --- /dev/null +++ b/go.sum @@ -0,0 +1,69 @@ +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= +github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE= +github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/consensys/gnark-crypto v0.20.1 h1:PXDUBvk8AzhvWowHLWBEAfUQcV1/aZgWIqD6eMpXmDg= +github.com/consensys/gnark-crypto v0.20.1/go.mod h1:RBWrSgy+IDbGR69RRV313th3M/aZU1ubk2om+qHuTSc= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/ethereum/go-ethereum v1.17.3 h1:Ev/sQHH+UdKZHWjuVzhu2pxhi/sXaPZl23Q+Q5LDd4Q= +github.com/ethereum/go-ethereum v1.17.3/go.mod h1:f2EhRwqewIZkGoQekywI2Y2RZAMTSavLNkD9qItFy1A= +github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ= +github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= +github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/ipfs/go-cid v0.0.6 h1:go0y+GcDOGeJIV01FeBsta4FHngoA4Wz7KMeLkXAhMs= +github.com/ipfs/go-cid v0.0.6/go.mod h1:6Ux9z5e+HpkQdckYoX1PG/6xqKspzlEIR5SDmgqgC/I= +github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= +github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= +github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 h1:lYpkrQH5ajf0OXOcUbGjvZxxijuBwbbmlSxLiuofa+g= +github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ= +github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/mr-tron/base58 v1.1.0/go.mod h1:xcD2VGqlgYjBdcBLw+TuYLr8afG+Hj8g2eTVqeSzSU8= +github.com/mr-tron/base58 v1.1.3 h1:v+sk57XuaCKGXpWtVBX8YJzO7hMGx4Aajh4TQbdEFdc= +github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/multiformats/go-base32 v0.0.3 h1:tw5+NhuwaOjJCC5Pp82QuXbrmLzWg7uxlMFp8Nq/kkI= +github.com/multiformats/go-base32 v0.0.3/go.mod h1:pLiuGC8y0QR3Ue4Zug5UzK9LjgbkL8NSQj0zQ5Nz/AA= +github.com/multiformats/go-base36 v0.1.0 h1:JR6TyF7JjGd3m6FbLU2cOxhC0Li8z8dLNGQ89tUg4F4= +github.com/multiformats/go-base36 v0.1.0/go.mod h1:kFGE83c6s80PklsHO9sRn2NCoffoRdUUOENyW/Vv6sM= +github.com/multiformats/go-multibase v0.0.3 h1:l/B6bJDQjvQ5G52jw4QGSYeOTZoAwIO77RblWplfIqk= +github.com/multiformats/go-multibase v0.0.3/go.mod h1:5+1R4eQrT3PkYZ24C3W2Ue2tPwIdYQD509ZjSb5y9Oc= +github.com/multiformats/go-multihash v0.0.13 h1:06x+mk/zj1FoMsgNejLpy6QTvJqlSt/BhLEy87zidlc= +github.com/multiformats/go-multihash v0.0.13/go.mod h1:VdAWLKTwram9oKAatUcLxBNUjdtcVwxObEQBtRfuyjc= +github.com/multiformats/go-varint v0.0.5 h1:XVZwSo04Cs3j/jS0uAEPpT3JY6DzMcVLLoWOSnCxOjg= +github.com/multiformats/go-varint v0.0.5/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/whyrusleeping/cbor-gen v0.3.1 h1:82ioxmhEYut7LBVGhGq8xoRkXPLElVuh5mV67AFfdv0= +github.com/whyrusleeping/cbor-gen v0.3.1/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/abiutil/types.go b/pkg/abiutil/types.go new file mode 100644 index 0000000..01fcf22 --- /dev/null +++ b/pkg/abiutil/types.go @@ -0,0 +1,48 @@ +// Package abiutil provides shared ABI type singletons parsed once at init. +// Seven packages previously declared their own abi.NewType("uint256", ...) etc. +// at package scope, silently discarding the error return 30+ times. This +// package centralises those declarations behind a must() wrapper. +package abiutil + +import "github.com/ethereum/go-ethereum/accounts/abi" + +// Scalar types used across the protocol. +var ( + Uint256 abi.Type + Uint64 abi.Type + Uint16 abi.Type + Uint8 abi.Type + Int64 abi.Type + Bytes32 abi.Type + Address abi.Type + String abi.Type + Bool abi.Type + Bytes abi.Type + + // Fixed-size array types used by the BLS signature ABI encoding. + Uint256Arr2 abi.Type + Uint256Arr4 abi.Type +) + +func init() { + Uint256 = must("uint256") + Uint64 = must("uint64") + Uint16 = must("uint16") + Uint8 = must("uint8") + Int64 = must("int64") + Bytes32 = must("bytes32") + Address = must("address") + String = must("string") + Bool = must("bool") + Bytes = must("bytes") + Uint256Arr2 = must("uint256[2]") + Uint256Arr4 = must("uint256[4]") +} + +func must(t string) abi.Type { + ty, err := abi.NewType(t, "", nil) + if err != nil { + panic("abiutil: bad ABI type " + t + ": " + err.Error()) + } + return ty +} diff --git a/pkg/bls/bls.go b/pkg/bls/bls.go new file mode 100644 index 0000000..f4d951e --- /dev/null +++ b/pkg/bls/bls.go @@ -0,0 +1,315 @@ +package bls + +import ( + "encoding/hex" + "errors" + "math/big" + "strings" + + "github.com/consensys/gnark-crypto/ecc/bn254" + "github.com/consensys/gnark-crypto/ecc/bn254/fr" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/layer-3/clearnet-sdk/pkg/abiutil" +) + +// BN254 field prime P. +var fieldP, _ = new(big.Int).SetString("21888242871839275222246405745257275088696311157297823662689037894645226208583", 10) + +// (P + 1) / 4 — exponent for modular square root when P ≡ 3 mod 4. +var sqrtExp = new(big.Int).Rsh(new(big.Int).Add(fieldP, big.NewInt(1)), 2) + +// KeyPair holds a BLS key pair on BN254. +type KeyPair struct { + Secret fr.Element + PublicG1 bn254.G1Affine + PublicG2 bn254.G2Affine +} + +// GenerateKeyPair creates a random BLS key pair. +func GenerateKeyPair() (*KeyPair, error) { + var sk fr.Element + _, err := sk.SetRandom() + if err != nil { + return nil, err + } + return keyPairFromScalar(&sk), nil +} + +// KeyPairFromSeed derives a deterministic key pair from a seed (for tests). +func KeyPairFromSeed(seed []byte) *KeyPair { + // Hash seed to get a scalar. + h := crypto.Keccak256(seed) + var sk fr.Element + sk.SetBytes(h) + return keyPairFromScalar(&sk) +} + +// MarshalHex serializes the secret scalar as a 64-character hex string +// (pure, in-memory). File persistence belongs to the caller — see +// `cmd/clearnode` and `client/chain.go` for production I/O. +func (kp *KeyPair) MarshalHex() string { + var skBytes [32]byte + sk := kp.Secret.Bytes() + copy(skBytes[:], sk[:]) + return hex.EncodeToString(skBytes[:]) +} + +// UnmarshalHexKeyPair parses a hex-encoded 32-byte secret scalar and +// reconstructs the full key pair. +func UnmarshalHexKeyPair(hexStr string) (*KeyPair, error) { + trimmed := strings.TrimSpace(hexStr) + skBytes, err := hex.DecodeString(trimmed) + if err != nil { + return nil, err + } + var sk fr.Element + sk.SetBytes(skBytes) + return keyPairFromScalar(&sk), nil +} + +func keyPairFromScalar(sk *fr.Element) *KeyPair { + kp := &KeyPair{Secret: *sk} + + // pk_g1 = sk * G1_generator + var skBigInt big.Int + sk.BigInt(&skBigInt) + + var g1Gen bn254.G1Affine + g1Gen.X.SetOne() + g1Gen.Y.SetUint64(2) + + kp.PublicG1.ScalarMultiplication(&g1Gen, &skBigInt) + + // pk_g2 = sk * G2_generator + _, _, _, g2Gen := bn254.Generators() + kp.PublicG2.ScalarMultiplication(&g2Gen, &skBigInt) + + return kp +} + +// HashToG1 hashes a 32-byte message to a BN254 G1 point using try-and-increment. +// This MUST match the Solidity BLS.hashToG1 exactly. +func HashToG1(msgHash [32]byte) (bn254.G1Affine, error) { + h := new(big.Int).SetBytes(msgHash[:]) + h.Mod(h, fieldP) + + three := big.NewInt(3) + + for i := 0; i < 256; i++ { + // x = (h + i) % P + x := new(big.Int).Add(h, big.NewInt(int64(i))) + x.Mod(x, fieldP) + + // y² = x³ + 3 + x2 := new(big.Int).Mul(x, x) + x2.Mod(x2, fieldP) + x3 := new(big.Int).Mul(x2, x) + x3.Mod(x3, fieldP) + y2 := new(big.Int).Add(x3, three) + y2.Mod(y2, fieldP) + + // y = y2^((P+1)/4) mod P + y := new(big.Int).Exp(y2, sqrtExp, fieldP) + + // Verify: y² == y2 mod P + ySquared := new(big.Int).Mul(y, y) + ySquared.Mod(ySquared, fieldP) + if ySquared.Cmp(y2) == 0 { + var pt bn254.G1Affine + pt.X.SetBigInt(x) + pt.Y.SetBigInt(y) + return pt, nil + } + } + + return bn254.G1Affine{}, errors.New("hashToG1: no valid point found in 256 iterations") +} + +// Sign produces a BLS signature: sigma = sk * HashToG1(msgHash). +func Sign(sk *fr.Element, msgHash [32]byte) (bn254.G1Affine, error) { + hm, err := HashToG1(msgHash) + if err != nil { + return bn254.G1Affine{}, err + } + + var skBigInt big.Int + sk.BigInt(&skBigInt) + + var sigma bn254.G1Affine + sigma.ScalarMultiplication(&hm, &skBigInt) + return sigma, nil +} + +// ErrEmptyAggregation is returned when AggregateG1 or AggregateG2 is called +// with an empty slice. An empty set of signatures/keys must not produce a +// valid aggregate — doing so would allow an empty cluster to "sign" any message. +var ErrEmptyAggregation = errors.New("cannot aggregate empty point set") + +// AggregateG1 sums a slice of G1 points. Returns ErrEmptyAggregation if the +// input is empty — an empty set of signatures must not produce a valid aggregate. +func AggregateG1(points []bn254.G1Affine) (bn254.G1Affine, error) { + if len(points) == 0 { + return bn254.G1Affine{}, ErrEmptyAggregation + } + var agg bn254.G1Jac + agg.FromAffine(&points[0]) + for i := 1; i < len(points); i++ { + var pJac bn254.G1Jac + pJac.FromAffine(&points[i]) + agg.AddAssign(&pJac) + } + var result bn254.G1Affine + result.FromJacobian(&agg) + return result, nil +} + +// AggregateG2 sums a slice of G2 points. Returns ErrEmptyAggregation if the +// input is empty — an empty set of public keys must not produce a valid aggregate. +func AggregateG2(points []bn254.G2Affine) (bn254.G2Affine, error) { + if len(points) == 0 { + return bn254.G2Affine{}, ErrEmptyAggregation + } + var agg bn254.G2Jac + agg.FromAffine(&points[0]) + for i := 1; i < len(points); i++ { + var pJac bn254.G2Jac + pJac.FromAffine(&points[i]) + agg.AddAssign(&pJac) + } + var result bn254.G2Affine + result.FromJacobian(&agg) + return result, nil +} + +// Verify checks a BLS signature via pairing: e(sigma, G2gen) == e(H(m), pubG2). +// Equivalently: e(sigma, G2gen) * e(-H(m), pubG2) == 1. +func Verify(sigma bn254.G1Affine, pubG2 bn254.G2Affine, msgHash [32]byte) (bool, error) { + hm, err := HashToG1(msgHash) + if err != nil { + return false, err + } + + // Negate H(m) + var negHm bn254.G1Affine + negHm.Neg(&hm) + + _, _, _, g2Gen := bn254.Generators() + + // PairingCheck verifies: e(g1s[0], g2s[0]) * e(g1s[1], g2s[1]) == 1 + ok, err := bn254.PairingCheck( + []bn254.G1Affine{sigma, negHm}, + []bn254.G2Affine{g2Gen, pubG2}, + ) + if err != nil { + return false, err + } + return ok, nil +} + +// EncodeSignatureForContract ABI-encodes a BLS cluster signature for the Solidity contract. +// The Solidity contract decodes: abi.decode(signature, (uint256, uint256[2], uint256[4])) +// where the format is (bitmask, [sigma.X, sigma.Y], [apkG2.X.im, apkG2.X.re, apkG2.Y.im, apkG2.Y.re]). +func EncodeSignatureForContract(bitmask *big.Int, sigma bn254.G1Affine, apkG2 bn254.G2Affine) ([]byte, error) { + // Extract sigma G1 coordinates + var sigX, sigY big.Int + sigma.X.BigInt(&sigX) + sigma.Y.BigInt(&sigY) + + // Extract apkG2 coordinates. + // gnark-crypto E2: A0 = real, A1 = imaginary. + // Solidity expects: [x_im, x_re, y_im, y_re]. + var apkXIm, apkXRe, apkYIm, apkYRe big.Int + apkG2.X.A1.BigInt(&apkXIm) // imaginary + apkG2.X.A0.BigInt(&apkXRe) // real + apkG2.Y.A1.BigInt(&apkYIm) // imaginary + apkG2.Y.A0.BigInt(&apkYRe) // real + + // ABI encode as (uint256, uint256[2], uint256[4]) + args := abi.Arguments{ + {Type: abiutil.Uint256}, + {Type: abiutil.Uint256Arr2}, + {Type: abiutil.Uint256Arr4}, + } + + return args.Pack( + bitmask, + [2]*big.Int{&sigX, &sigY}, + [4]*big.Int{&apkXIm, &apkXRe, &apkYIm, &apkYRe}, + ) +} + +// G1ToCoords extracts the X, Y coordinates of a G1 point as big.Ints. +func G1ToCoords(p bn254.G1Affine) [2]*big.Int { + var x, y big.Int + p.X.BigInt(&x) + p.Y.BigInt(&y) + return [2]*big.Int{&x, &y} +} + +// SerializeG1 serializes a G1 point as 64 bytes (X || Y, big-endian). +func SerializeG1(p bn254.G1Affine) []byte { + var x, y big.Int + p.X.BigInt(&x) + p.Y.BigInt(&y) + buf := make([]byte, 64) + x.FillBytes(buf[:32]) + y.FillBytes(buf[32:]) + return buf +} + +// DeserializeG1 reads a G1 point from 64 bytes (X || Y, big-endian). +func DeserializeG1(data []byte) (bn254.G1Affine, error) { + if len(data) != 64 { + return bn254.G1Affine{}, errors.New("invalid G1 data length: expected 64 bytes") + } + var pt bn254.G1Affine + pt.X.SetBigInt(new(big.Int).SetBytes(data[:32])) + pt.Y.SetBigInt(new(big.Int).SetBytes(data[32:])) + return pt, nil +} + +// G2ToCoords extracts the BN254 G2 coordinates in Solidity order: [x_im, x_re, y_im, y_re]. +func G2ToCoords(p bn254.G2Affine) [4]*big.Int { + var xIm, xRe, yIm, yRe big.Int + p.X.A1.BigInt(&xIm) // imaginary + p.X.A0.BigInt(&xRe) // real + p.Y.A1.BigInt(&yIm) // imaginary + p.Y.A0.BigInt(&yRe) // real + return [4]*big.Int{&xIm, &xRe, &yIm, &yRe} +} + +// SerializeG2 serializes a G2 point as 128 bytes (X.A1 || X.A0 || Y.A1 || Y.A0, big-endian). +func SerializeG2(p bn254.G2Affine) []byte { + buf := make([]byte, 128) + var xi, xr, yi, yr big.Int + p.X.A1.BigInt(&xi) // imaginary + p.X.A0.BigInt(&xr) // real + p.Y.A1.BigInt(&yi) // imaginary + p.Y.A0.BigInt(&yr) // real + xi.FillBytes(buf[0:32]) + xr.FillBytes(buf[32:64]) + yi.FillBytes(buf[64:96]) + yr.FillBytes(buf[96:128]) + return buf +} + +// DeserializeG2 reads a G2 point from 128 bytes. +func DeserializeG2(data []byte) (bn254.G2Affine, error) { + if len(data) != 128 { + return bn254.G2Affine{}, errors.New("invalid G2 data length: expected 128 bytes") + } + var pt bn254.G2Affine + pt.X.A1.SetBigInt(new(big.Int).SetBytes(data[0:32])) + pt.X.A0.SetBigInt(new(big.Int).SetBytes(data[32:64])) + pt.Y.A1.SetBigInt(new(big.Int).SetBytes(data[64:96])) + pt.Y.A0.SetBigInt(new(big.Int).SetBytes(data[96:128])) + return pt, nil +} + +// ComputePopHash computes the proof-of-possession message hash for a given operator address. +// Matches the Solidity contract: keccak256(abi.encodePacked(msg.sender)). +func ComputePopHash(operator common.Address) [32]byte { + return crypto.Keccak256Hash(operator.Bytes()) +} diff --git a/pkg/bls/bls_property_test.go b/pkg/bls/bls_property_test.go new file mode 100644 index 0000000..5a8a5a0 --- /dev/null +++ b/pkg/bls/bls_property_test.go @@ -0,0 +1,195 @@ +package bls + +import ( + "bytes" + "errors" + "math/rand" + "testing" + + bn254 "github.com/consensys/gnark-crypto/ecc/bn254" +) + +func randBytes32Det(rng *rand.Rand) [32]byte { + var b [32]byte + rng.Read(b[:]) + return b +} + +// TestProperty_BLS_SignVerifyRoundTrip asserts invariant B1: for any +// keypair and any 32-byte message, Sign + Verify with the matching +// public G2 key returns true, and with a wrong key returns false. +// +// Mutation-check 2026-04-18: negated the pairing check in Verify +// (swapped the sign of negHm) — test failed as signatures no longer +// verified; restored. +func TestProperty_BLS_SignVerifyRoundTrip(t *testing.T) { + rng := rand.New(rand.NewSource(0x4_0B01)) + for trial := 0; trial < 20; trial++ { + // BLS operations are slow — trial count kept modest. + seed := randBytes32Det(rng) + kp := KeyPairFromSeed(seed[:]) + msg := randBytes32Det(rng) + + sig, err := Sign(&kp.Secret, msg) + if err != nil { + t.Fatalf("trial=%d: sign: %v", trial, err) + } + + ok, err := Verify(sig, kp.PublicG2, msg) + if err != nil { + t.Fatalf("trial=%d: verify: %v", trial, err) + } + if !ok { + t.Fatalf("trial=%d: valid signature did not verify", trial) + } + + // Different message must not verify under same sig + key. + other := randBytes32Det(rng) + if other == msg { + other[0] ^= 1 + } + ok, err = Verify(sig, kp.PublicG2, other) + if err != nil { + t.Fatalf("trial=%d: verify wrong msg: %v", trial, err) + } + if ok { + t.Fatalf("trial=%d: signature verified against wrong message", trial) + } + + // Different key must not verify under same sig + msg. + otherSeed := randBytes32Det(rng) + if otherSeed == seed { + otherSeed[0] ^= 1 + } + otherKp := KeyPairFromSeed(otherSeed[:]) + ok, err = Verify(sig, otherKp.PublicG2, msg) + if err != nil { + t.Fatalf("trial=%d: verify wrong key: %v", trial, err) + } + if ok { + t.Fatalf("trial=%d: signature verified against wrong public key", trial) + } + } +} + +// TestProperty_BLS_KeyPairFromSeed_Deterministic asserts invariant B2: +// same seed → same keypair (scalar, G1, G2 all equal). +func TestProperty_BLS_KeyPairFromSeed_Deterministic(t *testing.T) { + rng := rand.New(rand.NewSource(0x4_0B02)) + for trial := 0; trial < 50; trial++ { + n := 1 + rng.Intn(128) + seed := make([]byte, n) + rng.Read(seed) + + a := KeyPairFromSeed(seed) + b := KeyPairFromSeed(seed) + + if !a.Secret.Equal(&b.Secret) { + t.Fatalf("trial=%d: secret scalars differ", trial) + } + if !a.PublicG1.Equal(&b.PublicG1) { + t.Fatalf("trial=%d: G1 pubkeys differ", trial) + } + if !a.PublicG2.Equal(&b.PublicG2) { + t.Fatalf("trial=%d: G2 pubkeys differ", trial) + } + } +} + +// TestProperty_BLS_KeyPairFromSeed_Sensitivity asserts B3: different +// seeds produce different keypairs. Cross-check: serialised G1 points +// are distinct across random seeds. +func TestProperty_BLS_KeyPairFromSeed_Sensitivity(t *testing.T) { + rng := rand.New(rand.NewSource(0x4_0B03)) + seenG1 := map[[64]byte]int{} // G1 is 64 bytes uncompressed via SerializeG1 + for trial := 0; trial < 100; trial++ { + var seed [32]byte + rng.Read(seed[:]) + kp := KeyPairFromSeed(seed[:]) + raw := SerializeG1(kp.PublicG1) + var key [64]byte + if len(raw) != 64 { + // If serialization changes shape, fail loudly rather than truncate. + t.Fatalf("trial=%d: unexpected G1 serialisation length %d", trial, len(raw)) + } + copy(key[:], raw) + if prior, ok := seenG1[key]; ok && prior != trial { + t.Fatalf("trial=%d: seed collision — same G1 as trial %d", trial, prior) + } + seenG1[key] = trial + } +} + +// TestProperty_BLS_AggregateEmpty_Rejects asserts invariant B4: both +// AggregateG1 and AggregateG2 reject empty input with +// ErrEmptyAggregation. Also tests nil input (separate from empty slice). +func TestProperty_BLS_AggregateEmpty_Rejects(t *testing.T) { + // G1 + if _, err := AggregateG1(nil); !errors.Is(err, ErrEmptyAggregation) { + t.Fatalf("AggregateG1(nil): got %v, want ErrEmptyAggregation", err) + } + if _, err := AggregateG1([]bn254.G1Affine{}); !errors.Is(err, ErrEmptyAggregation) { + t.Fatalf("AggregateG1([]): got %v, want ErrEmptyAggregation", err) + } + // G2 + if _, err := AggregateG2(nil); !errors.Is(err, ErrEmptyAggregation) { + t.Fatalf("AggregateG2(nil): got %v, want ErrEmptyAggregation", err) + } + if _, err := AggregateG2([]bn254.G2Affine{}); !errors.Is(err, ErrEmptyAggregation) { + t.Fatalf("AggregateG2([]): got %v, want ErrEmptyAggregation", err) + } + + // Single-point aggregations succeed (not empty). + rng := rand.New(rand.NewSource(0x4_0B04)) + var seed [32]byte + rng.Read(seed[:]) + kp := KeyPairFromSeed(seed[:]) + if _, err := AggregateG1([]bn254.G1Affine{kp.PublicG1}); err != nil { + t.Fatalf("single-point G1 aggregate: %v", err) + } + if _, err := AggregateG2([]bn254.G2Affine{kp.PublicG2}); err != nil { + t.Fatalf("single-point G2 aggregate: %v", err) + } +} + +// TestProperty_BLS_SerializeG1G2_RoundTrip asserts invariant B7: +// DeserializeG1(SerializeG1(p)) == p across random G1 points derived +// from random scalars. Same for G2. +func TestProperty_BLS_SerializeG1G2_RoundTrip(t *testing.T) { + rng := rand.New(rand.NewSource(0x4_0B07)) + for trial := 0; trial < 50; trial++ { + var seed [32]byte + rng.Read(seed[:]) + kp := KeyPairFromSeed(seed[:]) + + // G1 round-trip + raw := SerializeG1(kp.PublicG1) + back, err := DeserializeG1(raw) + if err != nil { + t.Fatalf("trial=%d: DeserializeG1: %v", trial, err) + } + if !back.Equal(&kp.PublicG1) { + t.Fatalf("trial=%d: G1 round-trip mismatch", trial) + } + + // Re-serialise and check byte-equality. + raw2 := SerializeG1(back) + if !bytes.Equal(raw, raw2) { + t.Fatalf("trial=%d: G1 serialisation not idempotent", trial) + } + + // G2 round-trip + rawG2 := SerializeG2(kp.PublicG2) + backG2, err := DeserializeG2(rawG2) + if err != nil { + t.Fatalf("trial=%d: DeserializeG2: %v", trial, err) + } + if !backG2.Equal(&kp.PublicG2) { + t.Fatalf("trial=%d: G2 round-trip mismatch", trial) + } + rawG2_2 := SerializeG2(backG2) + if !bytes.Equal(rawG2, rawG2_2) { + t.Fatalf("trial=%d: G2 serialisation not idempotent", trial) + } + } +} diff --git a/pkg/bls/bls_test.go b/pkg/bls/bls_test.go new file mode 100644 index 0000000..2615882 --- /dev/null +++ b/pkg/bls/bls_test.go @@ -0,0 +1,428 @@ +package bls + +import ( + "encoding/hex" + "errors" + "math/big" + "testing" + + "github.com/consensys/gnark-crypto/ecc/bn254" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +// --------------------------------------------------------------------------- +// Key Generation +// --------------------------------------------------------------------------- + +func TestGenerateKeyPair(t *testing.T) { + kp, err := GenerateKeyPair() + if err != nil { + t.Fatalf("GenerateKeyPair: %v", err) + } + if kp.Secret.IsZero() { + t.Fatal("secret key should not be zero") + } + // Public keys must be on curve. + if !kp.PublicG1.IsOnCurve() { + t.Fatal("G1 public key not on curve") + } + if !kp.PublicG2.IsOnCurve() { + t.Fatal("G2 public key not on curve") + } +} + +func TestGenerateKeyPairUnique(t *testing.T) { + kp1, _ := GenerateKeyPair() + kp2, _ := GenerateKeyPair() + if kp1.Secret.Equal(&kp2.Secret) { + t.Fatal("two random key pairs should have different secrets") + } +} + +func TestKeyPairFromSeedDeterministic(t *testing.T) { + seed := []byte("test-seed-12345") + kp1 := KeyPairFromSeed(seed) + kp2 := KeyPairFromSeed(seed) + if !kp1.Secret.Equal(&kp2.Secret) { + t.Fatal("same seed should produce same secret") + } + if kp1.PublicG1 != kp2.PublicG1 { + t.Fatal("same seed should produce same G1 pubkey") + } +} + +func TestKeyPairFromSeedDifferent(t *testing.T) { + kp1 := KeyPairFromSeed([]byte("seed-a")) + kp2 := KeyPairFromSeed([]byte("seed-b")) + if kp1.Secret.Equal(&kp2.Secret) { + t.Fatal("different seeds should produce different secrets") + } +} + +// --------------------------------------------------------------------------- +// HashToG1 +// --------------------------------------------------------------------------- + +func TestHashToG1OnCurve(t *testing.T) { + msg := crypto.Keccak256Hash([]byte("test message")) + pt, err := HashToG1(msg) + if err != nil { + t.Fatalf("HashToG1: %v", err) + } + if !pt.IsOnCurve() { + t.Fatal("hashed point not on curve") + } +} + +func TestHashToG1Deterministic(t *testing.T) { + msg := crypto.Keccak256Hash([]byte("deterministic")) + p1, _ := HashToG1(msg) + p2, _ := HashToG1(msg) + if p1 != p2 { + t.Fatal("HashToG1 should be deterministic") + } +} + +func TestHashToG1DifferentMessages(t *testing.T) { + m1 := crypto.Keccak256Hash([]byte("message-1")) + m2 := crypto.Keccak256Hash([]byte("message-2")) + p1, _ := HashToG1(m1) + p2, _ := HashToG1(m2) + if p1 == p2 { + t.Fatal("different messages should hash to different points") + } +} + +// --------------------------------------------------------------------------- +// Sign / Verify +// --------------------------------------------------------------------------- + +func TestSignAndVerify(t *testing.T) { + kp, _ := GenerateKeyPair() + msg := crypto.Keccak256Hash([]byte("hello world")) + + sig, err := Sign(&kp.Secret, msg) + if err != nil { + t.Fatalf("Sign: %v", err) + } + if !sig.IsOnCurve() { + t.Fatal("signature point not on curve") + } + + ok, err := Verify(sig, kp.PublicG2, msg) + if err != nil { + t.Fatalf("Verify: %v", err) + } + if !ok { + t.Fatal("valid signature should verify") + } +} + +func TestVerifyWrongMessage(t *testing.T) { + kp, _ := GenerateKeyPair() + msg := crypto.Keccak256Hash([]byte("correct")) + wrong := crypto.Keccak256Hash([]byte("wrong")) + + sig, _ := Sign(&kp.Secret, msg) + ok, err := Verify(sig, kp.PublicG2, wrong) + if err != nil { + t.Fatalf("Verify: %v", err) + } + if ok { + t.Fatal("signature should not verify with wrong message") + } +} + +func TestVerifyWrongKey(t *testing.T) { + kp1, _ := GenerateKeyPair() + kp2, _ := GenerateKeyPair() + msg := crypto.Keccak256Hash([]byte("test")) + + sig, _ := Sign(&kp1.Secret, msg) + ok, err := Verify(sig, kp2.PublicG2, msg) + if err != nil { + t.Fatalf("Verify: %v", err) + } + if ok { + t.Fatal("signature should not verify with wrong public key") + } +} + +// --------------------------------------------------------------------------- +// Aggregation +// --------------------------------------------------------------------------- + +func TestAggregateG1SinglePoint(t *testing.T) { + kp, _ := GenerateKeyPair() + msg := crypto.Keccak256Hash([]byte("aggregate-test")) + sig, _ := Sign(&kp.Secret, msg) + + agg, err := AggregateG1([]bn254.G1Affine{sig}) + if err != nil { + t.Fatalf("AggregateG1: %v", err) + } + if agg != sig { + t.Fatal("aggregating a single signature should return the same signature") + } +} + +func TestAggregateAndVerifyMultipleSigners(t *testing.T) { + msg := crypto.Keccak256Hash([]byte("multi-signer")) + n := 5 + + keys := make([]*KeyPair, n) + sigs := make([]bn254.G1Affine, n) + g2Pubs := make([]bn254.G2Affine, n) + + for i := 0; i < n; i++ { + kp, _ := GenerateKeyPair() + keys[i] = kp + sig, _ := Sign(&kp.Secret, msg) + sigs[i] = sig + g2Pubs[i] = kp.PublicG2 + } + + aggSig, err := AggregateG1(sigs) + if err != nil { + t.Fatalf("AggregateG1: %v", err) + } + aggPub, err := AggregateG2(g2Pubs) + if err != nil { + t.Fatalf("AggregateG2: %v", err) + } + + ok, verr := Verify(aggSig, aggPub, msg) + if verr != nil { + t.Fatalf("Verify: %v", verr) + } + if !ok { + t.Fatal("aggregated signature should verify against aggregated public key") + } +} + +func TestAggregateG1Empty(t *testing.T) { + _, err := AggregateG1(nil) + if err == nil { + t.Fatal("expected error for empty G1 aggregation") + } + if !errors.Is(err, ErrEmptyAggregation) { + t.Fatalf("expected ErrEmptyAggregation, got %v", err) + } +} + +func TestAggregateG2Empty(t *testing.T) { + _, err := AggregateG2(nil) + if err == nil { + t.Fatal("expected error for empty G2 aggregation") + } + if !errors.Is(err, ErrEmptyAggregation) { + t.Fatalf("expected ErrEmptyAggregation, got %v", err) + } +} + +// --------------------------------------------------------------------------- +// Serialization +// --------------------------------------------------------------------------- + +func TestSerializeDeserializeG1(t *testing.T) { + kp, _ := GenerateKeyPair() + msg := crypto.Keccak256Hash([]byte("serialize-test")) + sig, _ := Sign(&kp.Secret, msg) + + serialized := SerializeG1(sig) + if len(serialized) != 64 { + t.Fatalf("expected 64 bytes, got %d", len(serialized)) + } + + deserialized, err := DeserializeG1(serialized) + if err != nil { + t.Fatalf("DeserializeG1: %v", err) + } + if deserialized != sig { + t.Fatal("round-trip serialization should preserve the point") + } +} + +func TestDeserializeG1InvalidLength(t *testing.T) { + _, err := DeserializeG1([]byte{1, 2, 3}) + if err == nil { + t.Fatal("expected error for invalid length") + } +} + +func TestG1ToCoords(t *testing.T) { + kp, _ := GenerateKeyPair() + coords := G1ToCoords(kp.PublicG1) + if coords[0] == nil || coords[1] == nil { + t.Fatal("coordinates should not be nil") + } + if coords[0].Sign() == 0 && coords[1].Sign() == 0 { + t.Fatal("at least one coordinate should be non-zero for a valid key") + } +} + +func TestG2ToCoords(t *testing.T) { + kp, _ := GenerateKeyPair() + coords := G2ToCoords(kp.PublicG2) + for i, c := range coords { + if c == nil { + t.Fatalf("G2 coordinate[%d] should not be nil", i) + } + } +} + +// --------------------------------------------------------------------------- +// ComputePopHash +// --------------------------------------------------------------------------- + +func TestComputePopHash(t *testing.T) { + addr := common.HexToAddress("0xAbCdEf0123456789AbCdEf0123456789AbCdEf01") + h := ComputePopHash(addr) + expected := crypto.Keccak256Hash(addr.Bytes()) + if h != expected { + t.Fatalf("ComputePopHash mismatch: got %x, want %x", h, expected) + } +} + +// TestComputePopHash_GoldenVectors pins B8's on-chain byte layout +// against literal expected hashes. Spec: ADR-008 §WS-1 + +// contracts/evm/src/Registry.sol:531 compute +// keccak256(abi.encodePacked(msg.sender)) — which for a single address +// is 20 raw address bytes, no 32-byte left-padding. A mutation from +// addr.Bytes() to abi.encode(addr) (32-byte padded), to +// []byte(addr.Hex()) (ASCII), or to keccak over addr+salt, all produce +// different 32-byte outputs that this test catches. +func TestComputePopHash_GoldenVectors(t *testing.T) { + cases := []struct { + addr string + expect string // hex, no 0x + }{ + // keccak256 of 20 raw bytes 0x00… + {"0x0000000000000000000000000000000000000000", + "5380c7b7ae81a58eb98d9c78de4a1fd7fd9535fc953ed2be602daaa41767312a"}, + // keccak256 of 20 raw bytes 0x1234…5678 + {"0x1234567890abcdef1234567890abcdef12345678", + "5f6174255b44b7ca652c5289d2546de65e4394eb6aa52a40045e01237736d023"}, + // keccak256 of the existing test address, as a literal this time. + {"0xAbCdEf0123456789AbCdEf0123456789AbCdEf01", + "90a01d80e0e12c0b6acd5e4a69eec3dafeb108be3340405e3264b567330b5ba0"}, + } + for _, tc := range cases { + addr := common.HexToAddress(tc.addr) + got := ComputePopHash(addr) + wantBytes, err := hex.DecodeString(tc.expect) + if err != nil { + t.Fatalf("bad fixture hex %s: %v", tc.expect, err) + } + var want [32]byte + copy(want[:], wantBytes) + if got != want { + t.Fatalf("ComputePopHash(%s) layout drift:\n got %x\n want %x\n"+ + "This indicates an abi.encode vs abi.encodePacked divergence from "+ + "Registry.sol:531 — coordinate any change with the on-chain verifier.", + tc.addr, got, want) + } + } +} + +func TestComputePopHashDifferentAddresses(t *testing.T) { + h1 := ComputePopHash(common.HexToAddress("0x0000000000000000000000000000000000000001")) + h2 := ComputePopHash(common.HexToAddress("0x0000000000000000000000000000000000000002")) + if h1 == h2 { + t.Fatal("different addresses should produce different PoP hashes") + } +} + +// --------------------------------------------------------------------------- +// EncodeSignatureForContract — round-trip +// --------------------------------------------------------------------------- + +func TestEncodeSignatureForContractRoundTrip(t *testing.T) { + kp, _ := GenerateKeyPair() + msg := crypto.Keccak256Hash([]byte("contract-encoding")) + sig, _ := Sign(&kp.Secret, msg) + + bitmask := big.NewInt(0xFF) + encoded, err := EncodeSignatureForContract(bitmask, sig, kp.PublicG2) + if err != nil { + t.Fatalf("EncodeSignatureForContract: %v", err) + } + if len(encoded) == 0 { + t.Fatal("encoded signature should not be empty") + } + // 7 * 32 bytes = 224 bytes (1 uint256 + 2 uint256 + 4 uint256) + if len(encoded) != 7*32 { + t.Fatalf("expected %d bytes, got %d", 7*32, len(encoded)) + } +} + +// --------------------------------------------------------------------------- +// End-to-end: sign, aggregate, verify — simulating a cluster +// --------------------------------------------------------------------------- + +func TestClusterSignatureWorkflow(t *testing.T) { + const clusterSize = 8 + msg := crypto.Keccak256Hash([]byte("cluster-test-withdrawal")) + + keys := make([]*KeyPair, clusterSize) + sigs := make([]bn254.G1Affine, clusterSize) + g2Pubs := make([]bn254.G2Affine, clusterSize) + + for i := 0; i < clusterSize; i++ { + kp, _ := GenerateKeyPair() + keys[i] = kp + sig, err := Sign(&kp.Secret, msg) + if err != nil { + t.Fatalf("Sign[%d]: %v", i, err) + } + sigs[i] = sig + g2Pubs[i] = kp.PublicG2 + + // Each individual signature should verify. + ok, err := Verify(sig, kp.PublicG2, msg) + if err != nil { + t.Fatalf("individual Verify[%d]: %v", i, err) + } + if !ok { + t.Fatalf("individual signature[%d] failed to verify", i) + } + } + + // Aggregate all signatures and public keys. + aggSig, err := AggregateG1(sigs) + if err != nil { + t.Fatalf("AggregateG1: %v", err) + } + aggPub, err := AggregateG2(g2Pubs) + if err != nil { + t.Fatalf("AggregateG2: %v", err) + } + + ok, err := Verify(aggSig, aggPub, msg) + if err != nil { + t.Fatalf("aggregated Verify: %v", err) + } + if !ok { + t.Fatal("aggregated cluster signature should verify") + } + + // Verify with subset (threshold = 2/3 + 1 = 6). + threshold := (clusterSize*2)/3 + 1 + subSigs, err := AggregateG1(sigs[:threshold]) + if err != nil { + t.Fatalf("AggregateG1 subset: %v", err) + } + subPubs, err := AggregateG2(g2Pubs[:threshold]) + if err != nil { + t.Fatalf("AggregateG2 subset: %v", err) + } + + ok, err = Verify(subSigs, subPubs, msg) + if err != nil { + t.Fatalf("threshold Verify: %v", err) + } + if !ok { + t.Fatal("threshold subset signature should verify") + } +} diff --git a/pkg/bls/preimage_golden_test.go b/pkg/bls/preimage_golden_test.go new file mode 100644 index 0000000..e45f809 --- /dev/null +++ b/pkg/bls/preimage_golden_test.go @@ -0,0 +1,371 @@ +package bls + +// Byte-exact golden vectors for the BLS G1/G2 preimage surfaces that are pinned +// to Solidity verifiers (contracts/evm/src/BLS.sol and Slasher.sol). See +// docs/plans/cbor-encoding.md §8.4 and ADR-009 (Q8): these bytes MUST NOT drift +// without a coordinated on-chain upgrade. +// +// Layout captured here: +// - G1 serialization: 64 bytes = X(32) || Y(32), big-endian. +// - G2 serialization: 128 bytes = X.A1(im,32) || X.A0(re,32) || Y.A1(im,32) || Y.A0(re,32). +// Matches BLS.sol's pairing-precompile input order (EIP-197, imaginary first). +// - aggregate_sig: two partial signatures aggregated on G1 plus the matching +// aggregate G2 pubkey, packed as G1(64) || G2(128) = 192 bytes. +// +// To regenerate fixtures after a legitimate (coordinated) change, run: +// go test ./pkg/bls/ -run TestGoldens_Preimages -update +// Then inspect & commit the diff. + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "flag" + "math/big" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/consensys/gnark-crypto/ecc/bn254" +) + +var updateGoldens = flag.Bool("update", false, "regenerate golden fixtures from current Go output") + +// fixtureRoot returns the repo-root testdata directory. Go runs tests with the +// package dir as CWD, so we walk up until we find the repo root (directory +// containing testdata/goldens/). +func fixtureRoot(t *testing.T) string { + t.Helper() + cwd, err := os.Getwd() + if err != nil { + t.Fatalf("getwd: %v", err) + } + dir := cwd + for i := 0; i < 8; i++ { + cand := filepath.Join(dir, "testdata", "goldens", "solidity-preimages") + if st, err := os.Stat(cand); err == nil && st.IsDir() { + return cand + } + parent := filepath.Dir(dir) + if parent == dir { + break + } + dir = parent + } + t.Fatalf("could not locate testdata/goldens/solidity-preimages from %s", cwd) + return "" +} + +// writeOrCompare writes (when -update) or compares (default) a golden hex +// fixture plus its human-readable input JSON. +func writeOrCompare(t *testing.T, base string, inputJSON []byte, goldenBytes []byte) { + t.Helper() + hexPath := base + ".golden.hex" + jsonPath := base + ".input.json" + wantHex := strings.ToLower(hex.EncodeToString(goldenBytes)) + + if *updateGoldens { + if err := os.WriteFile(jsonPath, append(inputJSON, '\n'), 0o644); err != nil { + t.Fatalf("write input: %v", err) + } + if err := os.WriteFile(hexPath, []byte(wantHex+"\n"), 0o644); err != nil { + t.Fatalf("write golden: %v", err) + } + return + } + + raw, err := os.ReadFile(hexPath) + if err != nil { + t.Fatalf("read golden %s: %v (re-run with -update to create)", hexPath, err) + } + got := strings.TrimSpace(string(raw)) + if got != wantHex { + t.Fatalf("preimage drift at %s:\n want (current Go): %s\n have (on disk): %s\n"+ + "If this change is intentional and coordinated with Solidity verifiers, "+ + "re-run with -update and commit the diff.", hexPath, wantHex, got) + } + + // Sanity: input.json must also exist so future readers see the literal inputs. + if _, err := os.Stat(jsonPath); err != nil { + t.Fatalf("missing input fixture %s: %v", jsonPath, err) + } +} + +// --- helpers to build the three canonical points --- + +// g1Identity is the BN254 G1 point at infinity. Our SerializeG1 writes (0,0) for it. +func g1Identity() bn254.G1Affine { + var p bn254.G1Affine // zero value = point at infinity + return p +} + +// g2Identity is the BN254 G2 point at infinity. +func g2Identity() bn254.G2Affine { + var p bn254.G2Affine + return p +} + +// g1Generator returns the fixed G1 generator (1, 2) — matches BLS.sol G1_X/G1_Y. +func g1Generator() bn254.G1Affine { + _, _, g1Gen, _ := bn254.Generators() + return g1Gen +} + +// g2Generator returns the standard BN254 G2 generator — matches BLS.sol constants. +func g2Generator() bn254.G2Affine { + _, _, _, g2Gen := bn254.Generators() + return g2Gen +} + +// keypairFromSeed returns a deterministic BLS key pair from a literal byte seed. +// Uses the production KeyPairFromSeed so the scalar derivation is exactly the +// same as clearnode uses at runtime. +func keypairFromSeed(seed string) *KeyPair { + return KeyPairFromSeed([]byte(seed)) +} + +// --- test --- + +type g1Vector struct { + Name string `json:"name"` + Kind string `json:"kind"` // identity | generator | scalarMult + Seed string `json:"seed,omitempty"` + // When Kind == scalarMult, we derive scalar from KeyPairFromSeed(seed).Secret + // and multiply the fixed G1 generator. We record the resulting X/Y in the + // JSON for human inspection; they are *outputs*, not inputs. + DerivedX string `json:"derivedX,omitempty"` + DerivedY string `json:"derivedY,omitempty"` +} + +type g2Vector struct { + Name string `json:"name"` + Kind string `json:"kind"` // identity | generator | scalarMult + Seed string `json:"seed,omitempty"` + DerivedXI string `json:"derivedXIm,omitempty"` + DerivedXR string `json:"derivedXRe,omitempty"` + DerivedYI string `json:"derivedYIm,omitempty"` + DerivedYR string `json:"derivedYRe,omitempty"` +} + +type aggVector struct { + Name string `json:"name"` + Description string `json:"description"` + MsgHashHex string `json:"msgHashHex"` + SeedA string `json:"signerASeed"` + SeedB string `json:"signerBSeed"` + SigmaAHex string `json:"partialSigmaAHex"` + SigmaBHex string `json:"partialSigmaBHex"` + AggSigHex string `json:"aggregateSigmaHex"` + AggApkG2Hex string `json:"aggregateApkG2Hex"` + PayloadOrder string `json:"payloadOrder"` +} + +func TestGoldens_Preimages(t *testing.T) { + root := fixtureRoot(t) + blsDir := filepath.Join(root, "bls") + + // --- G1 points --- + g1Cases := []struct { + name string + pt bn254.G1Affine + meta g1Vector + }{ + { + name: "g1_identity", + pt: g1Identity(), + meta: g1Vector{Name: "g1_identity", Kind: "identity"}, + }, + { + name: "g1_generator", + pt: g1Generator(), + meta: g1Vector{Name: "g1_generator", Kind: "generator"}, + }, + } + // Random (deterministic) G1: scalar-mult of G1 generator by KeyPairFromSeed. + randKp := keypairFromSeed("clearnet/cbor-w0/bls/g1_random") + var randX, randY big.Int + randKp.PublicG1.X.BigInt(&randX) + randKp.PublicG1.Y.BigInt(&randY) + g1Cases = append(g1Cases, struct { + name string + pt bn254.G1Affine + meta g1Vector + }{ + name: "g1_random", + pt: randKp.PublicG1, + meta: g1Vector{ + Name: "g1_random", + Kind: "scalarMult", + Seed: "clearnet/cbor-w0/bls/g1_random", + DerivedX: randX.String(), + DerivedY: randY.String(), + }, + }) + + for _, c := range g1Cases { + t.Run(c.name, func(t *testing.T) { + b := SerializeG1(c.pt) + if len(b) != 64 { + t.Fatalf("SerializeG1 produced %d bytes, want 64", len(b)) + } + js, _ := json.MarshalIndent(c.meta, "", " ") + writeOrCompare(t, filepath.Join(blsDir, c.name), js, b) + }) + } + + // --- G2 points --- + g2Cases := []struct { + name string + pt bn254.G2Affine + meta g2Vector + }{ + { + name: "g2_identity", + pt: g2Identity(), + meta: g2Vector{Name: "g2_identity", Kind: "identity"}, + }, + { + name: "g2_generator", + pt: g2Generator(), + meta: g2Vector{Name: "g2_generator", Kind: "generator"}, + }, + } + randKp2 := keypairFromSeed("clearnet/cbor-w0/bls/g2_random") + var g2XI, g2XR, g2YI, g2YR big.Int + randKp2.PublicG2.X.A1.BigInt(&g2XI) + randKp2.PublicG2.X.A0.BigInt(&g2XR) + randKp2.PublicG2.Y.A1.BigInt(&g2YI) + randKp2.PublicG2.Y.A0.BigInt(&g2YR) + g2Cases = append(g2Cases, struct { + name string + pt bn254.G2Affine + meta g2Vector + }{ + name: "g2_random", + pt: randKp2.PublicG2, + meta: g2Vector{ + Name: "g2_random", + Kind: "scalarMult", + Seed: "clearnet/cbor-w0/bls/g2_random", + DerivedXI: g2XI.String(), + DerivedXR: g2XR.String(), + DerivedYI: g2YI.String(), + DerivedYR: g2YR.String(), + }, + }) + + for _, c := range g2Cases { + t.Run(c.name, func(t *testing.T) { + b := SerializeG2(c.pt) + if len(b) != 128 { + t.Fatalf("SerializeG2 produced %d bytes, want 128", len(b)) + } + js, _ := json.MarshalIndent(c.meta, "", " ") + writeOrCompare(t, filepath.Join(blsDir, c.name), js, b) + }) + } + + // --- Aggregate signature over a deterministic message --- + t.Run("aggregate_sig", func(t *testing.T) { + var msgHash [32]byte + // Literal 32-byte message: keccak of "clearnet/cbor-w0/bls/agg". + // We use a hand-picked hex string so the input is fully literal. + msgHex := "6d91a2b8e2f9c0d1a3b4c5d6e7f8091a2b3c4d5e6f70819a2b3c4d5e6f708192" + raw, err := hex.DecodeString(msgHex) + if err != nil || len(raw) != 32 { + t.Fatalf("bad msg hex") + } + copy(msgHash[:], raw) + + kpA := keypairFromSeed("clearnet/cbor-w0/bls/agg/A") + kpB := keypairFromSeed("clearnet/cbor-w0/bls/agg/B") + + sigA, err := Sign(&kpA.Secret, msgHash) + if err != nil { + t.Fatalf("sign A: %v", err) + } + sigB, err := Sign(&kpB.Secret, msgHash) + if err != nil { + t.Fatalf("sign B: %v", err) + } + aggSig, err := AggregateG1([]bn254.G1Affine{sigA, sigB}) + if err != nil { + t.Fatalf("aggregate G1: %v", err) + } + aggApk, err := AggregateG2([]bn254.G2Affine{kpA.PublicG2, kpB.PublicG2}) + if err != nil { + t.Fatalf("aggregate G2: %v", err) + } + + // Verify the aggregate just to catch accidental bit-rot in production code + // that a pure byte comparison would miss. + ok, err := Verify(aggSig, aggApk, msgHash) + if err != nil || !ok { + t.Fatalf("aggregate verify failed: ok=%v err=%v", ok, err) + } + + sigBytes := SerializeG1(aggSig) + apkBytes := SerializeG2(aggApk) + payload := append(append([]byte{}, sigBytes...), apkBytes...) + if len(payload) != 192 { + t.Fatalf("payload len = %d, want 192", len(payload)) + } + + meta := aggVector{ + Name: "aggregate_sig", + Description: "Two partial signatures over the literal 32-byte msgHash, aggregated via AggregateG1; aggregate G2 pubkey via AggregateG2. Layout: sigma(G1,64B) || apkG2(128B).", + MsgHashHex: msgHex, + SeedA: "clearnet/cbor-w0/bls/agg/A", + SeedB: "clearnet/cbor-w0/bls/agg/B", + SigmaAHex: hex.EncodeToString(SerializeG1(sigA)), + SigmaBHex: hex.EncodeToString(SerializeG1(sigB)), + AggSigHex: hex.EncodeToString(sigBytes), + AggApkG2Hex: hex.EncodeToString(apkBytes), + PayloadOrder: "aggSigmaG1(64) || aggApkG2(128)", + } + js, _ := json.MarshalIndent(meta, "", " ") + writeOrCompare(t, filepath.Join(blsDir, "aggregate_sig"), js, payload) + }) +} + +// Round-trip sanity: every G1/G2 golden must deserialize back to the exact +// point the generator produced. This catches the case where someone changes +// SerializeG1/G2 byte order but updates the fixtures without noticing. +func TestGoldens_Preimages_RoundTrip(t *testing.T) { + root := fixtureRoot(t) + for _, name := range []string{"g1_identity", "g1_generator", "g1_random"} { + hexBytes, err := os.ReadFile(filepath.Join(root, "bls", name+".golden.hex")) + if err != nil { + t.Fatalf("read %s: %v", name, err) + } + raw, err := hex.DecodeString(strings.TrimSpace(string(hexBytes))) + if err != nil { + t.Fatalf("decode hex: %v", err) + } + pt, err := DeserializeG1(raw) + if err != nil { + t.Fatalf("DeserializeG1(%s): %v", name, err) + } + if !bytes.Equal(SerializeG1(pt), raw) { + t.Fatalf("%s: round-trip serialize mismatch", name) + } + } + for _, name := range []string{"g2_identity", "g2_generator", "g2_random"} { + hexBytes, err := os.ReadFile(filepath.Join(root, "bls", name+".golden.hex")) + if err != nil { + t.Fatalf("read %s: %v", name, err) + } + raw, err := hex.DecodeString(strings.TrimSpace(string(hexBytes))) + if err != nil { + t.Fatalf("decode hex: %v", err) + } + pt, err := DeserializeG2(raw) + if err != nil { + t.Fatalf("DeserializeG2(%s): %v", name, err) + } + if !bytes.Equal(SerializeG2(pt), raw) { + t.Fatalf("%s: round-trip serialize mismatch", name) + } + } +} diff --git a/pkg/bls/verify.go b/pkg/bls/verify.go new file mode 100644 index 0000000..af10758 --- /dev/null +++ b/pkg/bls/verify.go @@ -0,0 +1,187 @@ +package bls + +import ( + "errors" + "fmt" + "math/big" + + "github.com/consensys/gnark-crypto/ecc/bn254" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/layer-3/clearnet-sdk/pkg/abiutil" + "github.com/layer-3/clearnet-sdk/pkg/core" +) + +// VerifyClusterSignature verifies an aggregated BLS threshold signature +// against the set of validators indicated by the bitmask. +// The signature is expected in ABI-encoded format: (uint256 bitmask, uint256[2] sigma, uint256[4] apkG2). +// +// k is the DQE-computed signing quorum for this block (block.K). The +// threshold is floor(2k/3)+1, not floor(2r/3)+1 where r=len(validators) is +// the shard replication factor — in multi-Slot shards r > k, so a block +// sealed by exactly k validators must not be rejected for under-signing +// against r (ISSUE-035 WS-1). +// +// §7 coordination: validators is [][]byte (serialized BN254 G2 pubkeys, +// exactly 128 bytes each — ADR-008). The on-chain Slasher reconstructs +// apkG2 from the bitmask by summing those pubkeys; off-chain verification +// performs the same reconstruction and rejects entries of any other length. +// k=0 is rejected: every caller must supply the explicit signing quorum +// (F-CONSENSUS-001). +func VerifyClusterSignature(data []byte, signature []byte, bitmask [32]byte, k uint16, validators [][]byte) (bool, error) { + if k == 0 { + return false, errors.New("verify: k=0 not allowed; signing quorum must be explicit") + } + // Count signers via popcount on the full bitmask. We can't bound the + // loop by len(validators) because some sealers (P2PBlockSealer) append + // validators in partial-receipt order while bitmask bits track the + // original cluster-member index — so a high-index signer can fall + // outside a len(validators)-bounded iteration. Popcount is index-agnostic + // and matches the on-chain Slasher's signer count. + signerCount := core.BitmaskOnesCount(bitmask) + threshold := (int(k)*2)/3 + 1 + if signerCount < threshold { + return false, nil + } + + // ABI-encoded format: (uint256 bitmask, uint256[2] sigma, uint256[4] apkG2). + args := abi.Arguments{ + {Type: abiutil.Uint256}, + {Type: abiutil.Uint256Arr2}, + {Type: abiutil.Uint256Arr4}, + } + + values, err := args.Unpack(signature) + if err != nil { + return false, fmt.Errorf("decode signature: %w", err) + } + if len(validators) == 0 { + return false, nil + } + + // ISSUE-043-01: reject tampered tuple-internal bitmask. AggregateSignatures + // packs values[0]=0 today (it lives in Attestation.Bitmask instead), so the + // invariant is zero OR equal to the outer bitmask — anything else is a + // post-seal edit in a region the BLS signature does not cover. + tupleBitmask := values[0].(*big.Int) + if tupleBitmask.Sign() != 0 && tupleBitmask.Cmp(core.BitmaskToBigInt(bitmask)) != 0 { + return false, nil + } + + sigCoords := values[1].([2]*big.Int) + apkCoords := values[2].([4]*big.Int) + + var sigma bn254.G1Affine + sigma.X.SetBigInt(sigCoords[0]) + sigma.Y.SetBigInt(sigCoords[1]) + + var apkG2 bn254.G2Affine + apkG2.X.A1.SetBigInt(apkCoords[0]) // imaginary + apkG2.X.A0.SetBigInt(apkCoords[1]) // real + apkG2.Y.A1.SetBigInt(apkCoords[2]) // imaginary + apkG2.Y.A0.SetBigInt(apkCoords[3]) // real + + // ISSUE-043-02: bind the outer bitmask to the aggregated apkG2. + // + // Spec invariant (ADR-008 §11): every set bit of the bitmask references + // a valid index into Validators, i.e. + // highest_set_bit(bitmask) < len(Validators) + // AND apkG2 == sum(Validators[i] for bit i set). + // + // Two sealer paths co-exist and produce compatible blocks under this + // invariant: + // - cluster.SigningCoordinator.SealBlock writes ALL r shard + // members into Validators and a signers-only bitmask; typically + // popcount(bitmask) < len(Validators). This is the production + // path (`cmd/clearnode/main.go`). + // - node/service/block_sealer.go P2PBlockSealer writes only + // signers into Validators with popcount == len(Validators). + // + // Previously this function rejected when popcount != len(Validators), + // which wrongly rejected every block sealed via SigningCoordinator + // with threshold < r. The fix below aligns off-chain verification + // with the on-chain Slasher reconstruction: range-check the bitmask + // against the validator roster, then sum only the validators at + // set bit positions. + if len(validators) > 0 { + // Bitmask is [32]byte; len(validators) is guarded by this check. + // If any set bit i has i >= len(validators), the reconstruction + // would dereference out of range. + if core.BitmaskBitLen(bitmask) > len(validators) { + return false, nil + } + } + + // ADR-008 §11 / F-CONSENSUS-001: every validator entry MUST be a full + // 128-byte BN254 G2 pubkey. NodeID placeholders or any other length are + // rejected. The sum of the bitmask-selected entries MUST match the + // embedded apkG2 — mirrors the on-chain Slasher reconstruction. + for i, v := range validators { + if len(v) != core.BLSPubKeyG2Len { + return false, fmt.Errorf("verify: validator %d has wrong length: got %d, want %d", i, len(v), core.BLSPubKeyG2Len) + } + } + var expected bn254.G2Affine + for i := 0; i < len(validators) && i < 256; i++ { + if !core.GetBitmaskBit(bitmask, i) { + continue + } + pub, dErr := DeserializeG2(validators[i]) + if dErr != nil { + return false, fmt.Errorf("validator pubkey decode: %w", dErr) + } + expected.Add(&expected, &pub) + } + if !expected.Equal(&apkG2) { + return false, nil + } + + // A.1: hash the full input so verification matches Engine.Sign. + msgHash := crypto.Keccak256Hash(data) + + return Verify(sigma, apkG2, msgHash) +} + +// ComputeVaultWithdrawalID computes the frozen withdrawal ID used at the vault +// level after a withdrawal entry has been sealed into a block. +// +// WithdrawalID = keccak256(accountId || blockHash || entryIndex || chainId || recipient || asset || amount || nonce) +// +// All integer fields use fixed-width big-endian encoding; amount is uint256. +// Including account identity and block location prevents economic-tuple +// collisions while still giving the vault a stable replay key for the exact +// finalized withdrawal. +func ComputeVaultWithdrawalID(accountID, blockHash [32]byte, entryIndex, chainID uint64, recipient, asset common.Address, amount *big.Int, nonce uint64) [32]byte { + buf := make([]byte, 0, 32+32+8+8+20+20+32+8) + buf = append(buf, accountID[:]...) + buf = append(buf, blockHash[:]...) + buf = appendUint64BE(buf, entryIndex) + buf = appendUint64BE(buf, chainID) + buf = append(buf, recipient.Bytes()...) + buf = append(buf, asset.Bytes()...) + buf = append(buf, uint256Bytes(amount)...) + buf = appendUint64BE(buf, nonce) + return crypto.Keccak256Hash(buf) +} + +func appendUint64BE(buf []byte, v uint64) []byte { + return append(buf, + byte(v>>56), byte(v>>48), byte(v>>40), byte(v>>32), + byte(v>>24), byte(v>>16), byte(v>>8), byte(v), + ) +} + +func uint256Bytes(v *big.Int) []byte { + var out [32]byte + if v == nil || v.Sign() < 0 { + return out[:] + } + b := v.Bytes() + if len(b) > len(out) { + b = b[len(b)-len(out):] + } + copy(out[len(out)-len(b):], b) + return out[:] +} diff --git a/pkg/cborx/bigint.go b/pkg/cborx/bigint.go new file mode 100644 index 0000000..982607b --- /dev/null +++ b/pkg/cborx/bigint.go @@ -0,0 +1,154 @@ +package cborx + +import ( + "errors" + "fmt" + "io" + "math/big" + + cbg "github.com/whyrusleeping/cbor-gen" // layer-guard: allow +) + +// Bignum tag numbers from RFC 8949 §3.4.3. +const ( + tagUnsignedBignum uint64 = 2 // positive *big.Int, value = big-endian byte string + tagNegativeBignum uint64 = 3 // negative *big.Int, value = big-endian byte string of (-1 - n) +) + +// BigInt is a CBOR adapter for math/big.Int. +// +// Encoding follows RFC 8949 §3.4.3: +// +// - Non-negative n is written as CBOR tag 2 wrapping a byte-string +// of big-endian magnitude bytes, with leading zero bytes trimmed. +// - Negative n is written as CBOR tag 3 wrapping the big-endian +// magnitude of (-1 - n) with leading zeros trimmed. +// - Zero is the single canonical form "tag 2 + empty byte string" +// (length = 0). This keeps zero unambiguous: the negative-bignum +// form of zero (-1 - 0 = -1, magnitude 1, byte 0x01) is structurally +// a different encoding of -1, not of 0, and is rejected as malformed +// when a byte-string of length 0 appears under tag 3. +// - A nil *big.Int is illegal inside a BigInt — use MaybeBigInt when +// absence is a legitimate state. +// +// The adapter rejects nil on marshal; use Value only when callers need a +// read-side zero default for an absent pointer. +type BigInt struct { + // V is the wrapped integer. Callers pass and read the pointer; the + // adapter never mutates it in-place when marshaling. + V *big.Int +} + +// Value returns a non-nil *big.Int (zero if b.V was nil). +func (b BigInt) Value() *big.Int { + if b.V == nil { + return new(big.Int) + } + return b.V +} + +// MarshalCBOR writes the RFC 8949 tag-2 / tag-3 bignum encoding. +// +// A nil inner value is rejected; callers must wrap a nilable amount in +// MaybeBigInt (which encodes nil as CBOR null). +func (b BigInt) MarshalCBOR(w io.Writer) error { + if b.V == nil { + return errors.New("cborx: BigInt: nil *big.Int (use MaybeBigInt for nilable fields)") + } + + var ( + tag uint64 + mag []byte + sign = b.V.Sign() + ) + switch { + case sign >= 0: + tag = tagUnsignedBignum + mag = b.V.Bytes() // big-endian, no leading zeros, empty for zero + default: + tag = tagNegativeBignum + // CBOR encodes the negative value n as the magnitude of (-1 - n). + // Allocate a new big.Int so we never mutate b.V. + neg := new(big.Int).Neg(b.V) + neg.Sub(neg, big.NewInt(1)) + mag = neg.Bytes() + } + + if err := cbg.WriteMajorTypeHeader(w, cbg.MajTag, tag); err != nil { + return fmt.Errorf("cborx: BigInt: write tag: %w", err) + } + if err := cbg.WriteMajorTypeHeader(w, cbg.MajByteString, uint64(len(mag))); err != nil { + return fmt.Errorf("cborx: BigInt: write length: %w", err) + } + if len(mag) > 0 { + if _, err := w.Write(mag); err != nil { + return fmt.Errorf("cborx: BigInt: write bytes: %w", err) + } + } + return nil +} + +// UnmarshalCBOR decodes a tag-2 / tag-3 bignum into b.V, enforcing the +// canonical-zero and canonical-leading-byte rules of RFC 8949 §4.2. +func (b *BigInt) UnmarshalCBOR(r io.Reader) error { + maj, val, err := cbg.CborReadHeader(r) + if err != nil { + return fmt.Errorf("cborx: BigInt: read tag: %w", err) + } + if maj != cbg.MajTag { + return fmt.Errorf("cborx: BigInt: expected tag (major 6), got major %d", maj) + } + tag := val + + maj, val, err = cbg.CborReadHeader(r) + if err != nil { + return fmt.Errorf("cborx: BigInt: read length: %w", err) + } + if maj != cbg.MajByteString { + return fmt.Errorf("cborx: BigInt: expected byte string (major 2), got major %d", maj) + } + length := val + + // Deterministic-encoding rule: a byte string backing a bignum is + // big-endian with no leading zero padding, except for zero itself + // which is the empty byte string under tag 2. + if length > 1<<20 { + return fmt.Errorf("cborx: BigInt: refusing length %d (> 1 MiB)", length) + } + + var buf []byte + if length > 0 { + buf = make([]byte, length) + if _, err := io.ReadFull(r, buf); err != nil { + return fmt.Errorf("cborx: BigInt: read bytes: %w", err) + } + if buf[0] == 0 { + return errors.New("cborx: BigInt: non-canonical leading zero byte") + } + } + + switch tag { + case tagUnsignedBignum: + n := new(big.Int) + if length > 0 { + n.SetBytes(buf) + } + b.V = n + return nil + case tagNegativeBignum: + // RFC 8949 §3.4.3: tag 3 wraps the big-endian magnitude m of + // (-1 - n). For n = -1, m = 0, which is the empty byte string. + // That is the canonical encoding of -1 under tag 3. + if length == 0 { + b.V = big.NewInt(-1) + return nil + } + mag := new(big.Int).SetBytes(buf) + n := new(big.Int).Neg(mag) + n.Sub(n, big.NewInt(1)) + b.V = n + return nil + default: + return fmt.Errorf("cborx: BigInt: expected tag 2 or 3, got tag %d", tag) + } +} diff --git a/pkg/cborx/bigint_test.go b/pkg/cborx/bigint_test.go new file mode 100644 index 0000000..0f949fd --- /dev/null +++ b/pkg/cborx/bigint_test.go @@ -0,0 +1,199 @@ +package cborx_test + +import ( + "bytes" + "encoding/hex" + "math/big" + "strings" + "testing" + + "github.com/layer-3/clearnet-sdk/pkg/cborx" +) + +// encodeBigInt is the shared helper every table-driven test goes +// through. Returns the byte output of a BigInt{V: n}.MarshalCBOR. +func encodeBigInt(t *testing.T, n *big.Int) []byte { + t.Helper() + var buf bytes.Buffer + if err := (cborx.BigInt{V: n}).MarshalCBOR(&buf); err != nil { + t.Fatalf("MarshalCBOR(%s): %v", n, err) + } + return buf.Bytes() +} + +func TestBigInt_ZeroCanonicalForm(t *testing.T) { + want := []byte{0xc2, 0x40} // tag 2, byte string length 0 + got := encodeBigInt(t, big.NewInt(0)) + if !bytes.Equal(got, want) { + t.Fatalf("zero encoding = %x, want %x", got, want) + } + // Default-constructed big.Int is also zero. + got2 := encodeBigInt(t, new(big.Int)) + if !bytes.Equal(got2, want) { + t.Fatalf("new(big.Int) encoding = %x, want %x", got2, want) + } +} + +func TestBigInt_ShortestForm(t *testing.T) { + cases := []struct { + n *big.Int + want string // hex + }{ + // Positive tag 2 with big-endian magnitude. + {big.NewInt(1), "c241" + "01"}, + {big.NewInt(23), "c241" + "17"}, + {big.NewInt(255), "c241" + "ff"}, + {big.NewInt(256), "c242" + "0100"}, + {big.NewInt(65535), "c242" + "ffff"}, + {big.NewInt(65536), "c243" + "010000"}, + {big.NewInt(1<<32 - 1), "c244" + "ffffffff"}, + {big.NewInt(1 << 32), "c245" + "0100000000"}, + {big.NewInt(int64(1<<63 - 1)), "c248" + "7fffffffffffffff"}, + {new(big.Int).SetUint64(^uint64(0)), "c248" + "ffffffffffffffff"}, + // Negative tag 3: magnitude = -1 - n. + {big.NewInt(-1), "c340"}, // mag 0, empty byte string + {big.NewInt(-2), "c341" + "01"}, // mag 1 + {big.NewInt(-24), "c341" + "17"}, // mag 23 + {big.NewInt(-25), "c341" + "18"}, // mag 24 + {big.NewInt(-256), "c341" + "ff"}, // mag 255 + {big.NewInt(-257), "c342" + "0100"}, + } + for _, c := range cases { + got := encodeBigInt(t, c.n) + if hex.EncodeToString(got) != c.want { + t.Errorf("encode(%s) = %x, want %s", c.n, got, c.want) + } + } +} + +func TestBigInt_LargeBignum(t *testing.T) { + // 2^128 is canonical tag-2 with 17 big-endian bytes (0x01 + 16 zeros). + n := new(big.Int).Lsh(big.NewInt(1), 128) + got := encodeBigInt(t, n) + wantHex := "c251" + "0100000000000000000000000000000000" + if hex.EncodeToString(got) != wantHex { + t.Fatalf("2^128 encoded = %x, want %s", got, wantHex) + } +} + +func TestBigInt_MarshalDoesNotMutateNegativeInput(t *testing.T) { + n := big.NewInt(-257) + want := new(big.Int).Set(n) + + _ = encodeBigInt(t, n) + + if n.Cmp(want) != 0 { + t.Fatalf("MarshalCBOR mutated input: got %s, want %s", n, want) + } +} + +func TestBigInt_RoundTrip(t *testing.T) { + cases := []*big.Int{ + big.NewInt(0), + big.NewInt(1), + big.NewInt(-1), + big.NewInt(23), + big.NewInt(24), + big.NewInt(255), + big.NewInt(256), + big.NewInt(1<<32 - 1), + big.NewInt(1 << 32), + big.NewInt(int64(1<<63 - 1)), + new(big.Int).SetUint64(^uint64(0)), + new(big.Int).Lsh(big.NewInt(1), 128), + new(big.Int).Neg(new(big.Int).Lsh(big.NewInt(1), 128)), + big.NewInt(-24), + big.NewInt(-25), + } + for _, n := range cases { + bz := encodeBigInt(t, n) + var got cborx.BigInt + if err := got.UnmarshalCBOR(bytes.NewReader(bz)); err != nil { + t.Fatalf("UnmarshalCBOR(%s): %v", n, err) + } + if got.V.Cmp(n) != 0 { + t.Errorf("round-trip: got %s, want %s", got.V, n) + } + + // Idempotence: re-encode and compare bytes. + var buf2 bytes.Buffer + if err := got.MarshalCBOR(&buf2); err != nil { + t.Fatalf("re-marshal: %v", err) + } + if !bytes.Equal(buf2.Bytes(), bz) { + t.Errorf("idempotence for %s: %x vs %x", n, buf2.Bytes(), bz) + } + } +} + +func TestBigInt_RejectNil(t *testing.T) { + var buf bytes.Buffer + err := (cborx.BigInt{V: nil}).MarshalCBOR(&buf) + if err == nil || !strings.Contains(err.Error(), "nil") { + t.Fatalf("expected nil-rejection error, got %v", err) + } +} + +func TestBigInt_RejectNonCanonicalLeadingZero(t *testing.T) { + // tag 2, length 1, byte 0x00 — value decodes to 0, but canonical + // zero is the empty byte string. Must reject. + raw := []byte{0xc2, 0x41, 0x00} + var got cborx.BigInt + err := got.UnmarshalCBOR(bytes.NewReader(raw)) + if err == nil { + t.Fatalf("expected rejection of tag-2 with leading zero, decoded %v", got.V) + } +} + +func TestBigInt_RejectNegativeNonCanonicalLeadingZero(t *testing.T) { + // tag 3, length 1, byte 0x00 — value decodes to -1, but canonical + // -1 is tag 3 with an empty byte string. Must reject. + raw := []byte{0xc3, 0x41, 0x00} + var got cborx.BigInt + err := got.UnmarshalCBOR(bytes.NewReader(raw)) + if err == nil { + t.Fatalf("expected rejection of tag-3 with leading zero, decoded %v", got.V) + } +} + +func TestBigInt_RejectWrongTag(t *testing.T) { + // tag 5 is reserved; bignum decoder must reject. + raw := []byte{0xc5, 0x40} + var got cborx.BigInt + if err := got.UnmarshalCBOR(bytes.NewReader(raw)); err == nil { + t.Fatalf("expected rejection of tag 5") + } +} + +func TestBigInt_RejectIndefiniteLengthByteString(t *testing.T) { + // tag 2 + indefinite-length byte string (0x5f ... 0xff). + // cbor-gen's reader rejects indefinite by hitting the default + // "invalid header" branch; we surface that as a decode error. + raw := []byte{0xc2, 0x5f, 0x41, 0x01, 0xff} + var got cborx.BigInt + if err := got.UnmarshalCBOR(bytes.NewReader(raw)); err == nil { + t.Fatalf("expected rejection of indefinite byte string") + } +} + +func TestBigInt_NonShortestHeaderRejected(t *testing.T) { + // tag 2, byte-string length encoded as 2-byte value 0x0001 (low = 25). + // Canonical would be length 1 (low = 0x41). cbor-gen's header reader + // rejects this as non-canonical on decode. + raw := []byte{0xc2, 0x59, 0x00, 0x01, 0x42} + var got cborx.BigInt + if err := got.UnmarshalCBOR(bytes.NewReader(raw)); err == nil { + t.Fatalf("expected rejection of non-shortest length header") + } +} + +func TestBigInt_RejectsOversizeMagnitudeBeforeAllocation(t *testing.T) { + // tag 2, byte-string length = 1 MiB + 1, no body. The decoder must + // reject on the declared length before allocating or attempting ReadFull. + raw := []byte{0xc2, 0x5a, 0x00, 0x10, 0x00, 0x01} + var got cborx.BigInt + err := got.UnmarshalCBOR(bytes.NewReader(raw)) + if err == nil || !strings.Contains(err.Error(), "1 MiB") { + t.Fatalf("expected oversize magnitude rejection, got %v", err) + } +} diff --git a/pkg/cborx/decimal.go b/pkg/cborx/decimal.go new file mode 100644 index 0000000..37d670e --- /dev/null +++ b/pkg/cborx/decimal.go @@ -0,0 +1,125 @@ +package cborx + +import ( + "errors" + "fmt" + "io" + "math" + + cbg "github.com/whyrusleeping/cbor-gen" // layer-guard: allow + + "github.com/layer-3/clearnet-sdk/pkg/decimal" // layer-guard: allow +) + +// tagDecimalFraction is RFC 8949 §3.4.4: major type 6, tag 4, wrapping +// a two-element array [exponent, mantissa]. +const tagDecimalFraction uint64 = 4 + +// Decimal is a CBOR adapter for internal/decimal.Decimal (the project's +// vendored fixed-point decimal; API: Exponent() int32, Coefficient() +// *big.Int, NewFromBigInt). +// +// Encoding follows RFC 8949 §3.4.4: +// +// - major type 6 (tag) with value 4, +// - wrapping a definite-length array of exactly two elements, +// - element 0: exponent, written as the shortest-form +// MajUnsignedInt / MajNegativeInt (docs/specs/cbor.md §2 disallows +// wider encodings), +// - element 1: mantissa, written as a BigInt (RFC 8949 tag 2/3) +// so the sign is carried without an extra prefix. +// +// internal/decimal exposes only int32 exponents, so the exponent fits +// comfortably into CBOR's native integer range and no bignum is ever +// needed for it. +type Decimal struct { + V decimal.Decimal +} + +// MarshalCBOR writes the tag-4 two-element array. +func (d Decimal) MarshalCBOR(w io.Writer) error { + if err := cbg.WriteMajorTypeHeader(w, cbg.MajTag, tagDecimalFraction); err != nil { + return fmt.Errorf("cborx: Decimal: write tag: %w", err) + } + if err := cbg.WriteMajorTypeHeader(w, cbg.MajArray, 2); err != nil { + return fmt.Errorf("cborx: Decimal: write array header: %w", err) + } + + exp := int64(d.V.Exponent()) + if exp >= 0 { + if err := cbg.WriteMajorTypeHeader(w, cbg.MajUnsignedInt, uint64(exp)); err != nil { + return fmt.Errorf("cborx: Decimal: write exponent: %w", err) + } + } else { + // Canonical negative int: value = -1 - n, stored as uint64. + if err := cbg.WriteMajorTypeHeader(w, cbg.MajNegativeInt, uint64(-exp)-1); err != nil { + return fmt.Errorf("cborx: Decimal: write negative exponent: %w", err) + } + } + + mantissa := d.V.Coefficient() + if err := (BigInt{V: mantissa}).MarshalCBOR(w); err != nil { + return fmt.Errorf("cborx: Decimal: write mantissa: %w", err) + } + return nil +} + +// UnmarshalCBOR decodes the tag-4 two-element array. +func (d *Decimal) UnmarshalCBOR(r io.Reader) error { + maj, val, err := cbg.CborReadHeader(r) + if err != nil { + return fmt.Errorf("cborx: Decimal: read tag: %w", err) + } + if maj != cbg.MajTag { + return fmt.Errorf("cborx: Decimal: expected tag (major 6), got major %d", maj) + } + if val != tagDecimalFraction { + return fmt.Errorf("cborx: Decimal: expected tag 4, got tag %d", val) + } + + maj, val, err = cbg.CborReadHeader(r) + if err != nil { + return fmt.Errorf("cborx: Decimal: read array header: %w", err) + } + if maj != cbg.MajArray { + return fmt.Errorf("cborx: Decimal: expected array (major 4), got major %d", maj) + } + if val != 2 { + return fmt.Errorf("cborx: Decimal: expected 2 elements, got %d", val) + } + + var expI int64 + maj, val, err = cbg.CborReadHeader(r) + if err != nil { + return fmt.Errorf("cborx: Decimal: read exponent: %w", err) + } + switch maj { + case cbg.MajUnsignedInt: + if val > math.MaxInt32 { + return fmt.Errorf("cborx: Decimal: exponent %d overflows int32", val) + } + expI = int64(val) + case cbg.MajNegativeInt: + // CBOR negative = -1 - val; int32 range check. + if val > math.MaxInt32 { + return fmt.Errorf("cborx: Decimal: negative exponent overflows int32") + } + expI = -int64(val) - 1 + default: + return fmt.Errorf("cborx: Decimal: exponent must be integer, got major %d", maj) + } + if expI < math.MinInt32 || expI > math.MaxInt32 { + return fmt.Errorf("cborx: Decimal: exponent %d out of int32 range", expI) + } + + var mantissa BigInt + if err := mantissa.UnmarshalCBOR(r); err != nil { + return fmt.Errorf("cborx: Decimal: mantissa: %w", err) + } + if mantissa.V == nil { + return errors.New("cborx: Decimal: mantissa decoded to nil big.Int") + } + + d.V = decimal.NewFromBigInt(mantissa.V, int32(expI)) + return nil +} diff --git a/pkg/cborx/decimal_test.go b/pkg/cborx/decimal_test.go new file mode 100644 index 0000000..38b11fc --- /dev/null +++ b/pkg/cborx/decimal_test.go @@ -0,0 +1,165 @@ +package cborx_test + +import ( + "bytes" + "encoding/hex" + "math/big" + "testing" + + "github.com/layer-3/clearnet-sdk/pkg/cborx" + "github.com/layer-3/clearnet-sdk/pkg/decimal" +) + +func mustDec(t *testing.T, s string) decimal.Decimal { + t.Helper() + d, err := decimal.NewFromString(s) + if err != nil { + t.Fatalf("decimal parse %q: %v", s, err) + } + return d +} + +func TestDecimal_TagStructure(t *testing.T) { + // 1.5 = mantissa 15, exponent -1. + d := mustDec(t, "1.5") + var buf bytes.Buffer + if err := (cborx.Decimal{V: d}).MarshalCBOR(&buf); err != nil { + t.Fatalf("marshal: %v", err) + } + // Expected: tag 4 (0xc4), array length 2 (0x82), exponent -1 (0x20), + // mantissa BigInt tag 2 length 1 value 0x0f. + want := "c482" + "20" + "c2410f" + if hex.EncodeToString(buf.Bytes()) != want { + t.Fatalf("1.5 encoding = %x, want %s", buf.Bytes(), want) + } +} + +func TestDecimal_RoundTripAtVariousExponents(t *testing.T) { + cases := []struct { + mantissa int64 + exp int32 + }{ + {0, 0}, + {1, 0}, + {-1, 0}, + {123456, 6}, + {123456, -6}, + {1, -18}, + {1, 18}, + {-999999999999, -18}, + {1, -1_000_000}, // far-negative exponent inside int32 + } + for _, c := range cases { + d := decimal.NewFromBigInt(big.NewInt(c.mantissa), c.exp) + var buf bytes.Buffer + if err := (cborx.Decimal{V: d}).MarshalCBOR(&buf); err != nil { + t.Fatalf("marshal %d*10^%d: %v", c.mantissa, c.exp, err) + } + + var got cborx.Decimal + if err := got.UnmarshalCBOR(bytes.NewReader(buf.Bytes())); err != nil { + t.Fatalf("unmarshal %d*10^%d: %v", c.mantissa, c.exp, err) + } + if got.V.Exponent() != c.exp { + t.Errorf("exp for %d*10^%d: got %d, want %d", c.mantissa, c.exp, got.V.Exponent(), c.exp) + } + if got.V.Coefficient().Cmp(big.NewInt(c.mantissa)) != 0 { + t.Errorf("mantissa for %d*10^%d: got %s", c.mantissa, c.exp, got.V.Coefficient()) + } + + // Idempotence. + var buf2 bytes.Buffer + if err := got.MarshalCBOR(&buf2); err != nil { + t.Fatalf("re-marshal: %v", err) + } + if !bytes.Equal(buf.Bytes(), buf2.Bytes()) { + t.Errorf("idempotence %d*10^%d: %x vs %x", c.mantissa, c.exp, buf.Bytes(), buf2.Bytes()) + } + } +} + +func TestDecimal_LargeMantissaRoundTrip(t *testing.T) { + // 10^40 * 10^-18 — mantissa doesn't fit in int64. + mantissa := new(big.Int).Exp(big.NewInt(10), big.NewInt(40), nil) + d := decimal.NewFromBigInt(mantissa, -18) + + var buf bytes.Buffer + if err := (cborx.Decimal{V: d}).MarshalCBOR(&buf); err != nil { + t.Fatalf("marshal: %v", err) + } + + var got cborx.Decimal + if err := got.UnmarshalCBOR(bytes.NewReader(buf.Bytes())); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if got.V.Coefficient().Cmp(mantissa) != 0 { + t.Errorf("mantissa = %s, want %s", got.V.Coefficient(), mantissa) + } + if got.V.Exponent() != -18 { + t.Errorf("exp = %d, want -18", got.V.Exponent()) + } +} + +func TestDecimal_RejectWrongTag(t *testing.T) { + // tag 5 instead of 4. + raw := []byte{0xc5, 0x82, 0x00, 0xc2, 0x40} + var got cborx.Decimal + if err := got.UnmarshalCBOR(bytes.NewReader(raw)); err == nil { + t.Fatalf("expected rejection of tag 5") + } +} + +func TestDecimal_RejectWrongArity(t *testing.T) { + // tag 4, array length 1 instead of 2. + raw := []byte{0xc4, 0x81, 0x00} + var got cborx.Decimal + if err := got.UnmarshalCBOR(bytes.NewReader(raw)); err == nil { + t.Fatalf("expected rejection of 1-element array") + } +} + +func TestDecimal_RejectNonIntegerExponent(t *testing.T) { + // tag 4, [exponent = text "0", mantissa = 0]. Exponents must be + // bare signed integers, never strings or floats. + raw := []byte{0xc4, 0x82, 0x61, '0', 0xc2, 0x40} + var got cborx.Decimal + if err := got.UnmarshalCBOR(bytes.NewReader(raw)); err == nil { + t.Fatalf("expected rejection of non-integer exponent") + } +} + +func TestDecimal_RejectExponentOverflow(t *testing.T) { + tests := []struct { + name string + raw []byte + }{ + { + name: "positive over int32", + // tag 4, [2147483648, 0] + raw: []byte{0xc4, 0x82, 0x1a, 0x80, 0x00, 0x00, 0x00, 0xc2, 0x40}, + }, + { + name: "negative below int32", + // tag 4, [-2147483649, 0]. CBOR negative stores -1-n = 2147483648. + raw: []byte{0xc4, 0x82, 0x3a, 0x80, 0x00, 0x00, 0x00, 0xc2, 0x40}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got cborx.Decimal + if err := got.UnmarshalCBOR(bytes.NewReader(tt.raw)); err == nil { + t.Fatalf("expected exponent overflow rejection") + } + }) + } +} + +func TestDecimal_RejectFloatMantissa(t *testing.T) { + // tag 4, [exp 0, float 1.0 as half-precision 0xf9 0x3c 0x00]. + raw := []byte{0xc4, 0x82, 0x00, 0xf9, 0x3c, 0x00} + var got cborx.Decimal + if err := got.UnmarshalCBOR(bytes.NewReader(raw)); err == nil { + t.Fatalf("expected rejection of float mantissa") + } +} diff --git a/pkg/cborx/doc.go b/pkg/cborx/doc.go new file mode 100644 index 0000000..aa2de2f --- /dev/null +++ b/pkg/cborx/doc.go @@ -0,0 +1,74 @@ +// Package cborx provides shared primitive-adapter codecs for the +// canonical deterministic CBOR (RFC 8949 §4.2) encoding used across the +// Yellow Network protocol. +// +// This is the foundation package for the canonical CBOR rules in +// docs/specs/cbor.md and ADR-009 (docs/decisions/009-cbor-encoding.md §3 — +// the adapter table, §5 — the version byte envelope). +// +// # Scope +// +// cborx does NOT generate per-type struct codecs. It only wraps +// whyrusleeping/cbor-gen primitives with adapters so that generated +// codecs (owned by later waves under each package's gen/ subfolder) +// can delegate the encoding of complex primitive types — big integers, +// fixed-point decimals, fixed-length hashes and addresses, and timestamps +// — to one canonical implementation that satisfies RFC 8949 §4.2 +// deterministic-encoding rules. +// +// # Adapter set (ADR-009 §3) +// +// - BigInt — *big.Int as RFC 8949 tag 2 (unsigned bignum) or +// tag 3 (negative bignum); zero is tag 2 with empty +// byte string; nil is invalid inside a plain BigInt. +// - MaybeBigInt — nilable *big.Int; nil encodes as CBOR null. +// - Decimal — internal/decimal.Decimal as RFC 8949 tag 4 +// (decimal fraction) = [exponent int32, mantissa bigint]. +// - Hash32 — [32]byte as major type 2, definite length 32. +// - Addr20 — common.Address / [20]byte as major type 2, +// definite length 20. +// - Time — time.Time as major type 0/1, Unix nanoseconds int64. +// +// # Envelope (ADR-009 §5) +// +// Every CBOR frame on the wire or in a BLOB is prefixed with one byte +// (the global schema-family version). WriteEnvelope / ReadEnvelope +// handle the prefix around a cbor-gen CBORMarshaler / CBORUnmarshaler +// body. V1 (0x01) is the only version emitted by this migration; +// ReadEnvelope rejects 0x00 (reserved) and anything above V1 with a +// typed ErrUnsupportedVersion so callers can distinguish framing errors +// from payload errors. +// +// # Determinism (RFC 8949 §4.2) +// +// Adapters enforce — and tests prove — the deterministic-encoding rules in +// docs/specs/cbor.md §2: +// +// 1. Integers in shortest form (no over-wide length encoding). +// 2. Definite-length containers only; indefinite-length input is rejected. +// 3. Map keys sorted lexicographically by their CBOR-encoded bytes +// (RFC 8949 §4.2.1). Adapters themselves hold no maps; the envelope +// helpers preserve sort order produced by cbor-gen. +// 4. No floating-point types at all. NaN, infinities, and negative +// zero are never encoded and are rejected on decode. +// 5. Tagged integers only where declared (tag 2/3 for bignums, tag 4 +// for decimals). +// +// # cbor-gen mode +// +// ADR-009 §2 mandates **struct-as-array (positional, no keys on wire)** +// for every generated type in the network. Wave 2's per-package +// gen/main.go must invoke cbor-gen with the tuple-encoding entry point +// (typegen.WriteTupleEncodersToFile) rather than the map entry point. +// Fields in generated structs may only be appended at the end; insert, +// reorder, rename, or remove requires a version-byte bump and a re-seed +// of every CBOR-encoded BLOB (ADR-009 §5). +// +// # Imports +// +// This package imports whyrusleeping/cbor-gen and fxamacker/cbor/v2. +// internal/ is stdlib-only under the repo's architecture rules; the +// relevant import lines carry the `layer-guard: allow` escape hatch +// because ADR-009 and the migration plan explicitly sanction cborx as +// the single package that wraps these libraries. +package cborx diff --git a/pkg/cborx/envelope.go b/pkg/cborx/envelope.go new file mode 100644 index 0000000..80e3bb1 --- /dev/null +++ b/pkg/cborx/envelope.go @@ -0,0 +1,133 @@ +package cborx + +import ( + "bytes" + "errors" + "fmt" + "io" + + cbg "github.com/whyrusleeping/cbor-gen" // layer-guard: allow +) + +// ErrEnvelopeTrailingBytes reports an envelope decoded from a bounded +// reader (byte slice or io.LimitReader) that contains bytes after the +// canonical body. Canonical CBOR (ADR-009 §1, RFC 8949 §4.2) requires +// exactly one logical value per encoded byte string; trailing bytes +// would admit multiple wire encodings for the same logical value. +var ErrEnvelopeTrailingBytes = errors.New("cborx: trailing bytes after envelope body") + +// WriteEnvelope writes the one-byte schema-family version followed by +// the canonical CBOR body produced by body.MarshalCBOR. It is the sole +// sanctioned way to stamp a frame before it hits a stream or a BLOB +// (ADR-009 §5). +// +// v MUST be V1 for this migration; any other value returns a wrapped +// ErrUnsupportedVersion or ErrReservedVersion so callers cannot +// accidentally write a frame with a version this build doesn't know +// how to read back. +func WriteEnvelope(w io.Writer, v Version, body cbg.CBORMarshaler) error { + if v == VersionReserved { + return fmt.Errorf("%w: cannot write 0x00", ErrReservedVersion) + } + if v != V1 { + return fmt.Errorf("%w: write refused for v=0x%02x (this build writes V1 only)", ErrUnsupportedVersion, uint8(v)) + } + if body == nil { + return errors.New("cborx: WriteEnvelope: nil body") + } + if _, err := w.Write([]byte{byte(v)}); err != nil { + return fmt.Errorf("cborx: write version byte: %w", err) + } + if err := body.MarshalCBOR(w); err != nil { + return fmt.Errorf("cborx: marshal body: %w", err) + } + return nil +} + +// ReadEnvelope reads the one-byte schema-family version, rejects +// reserved / unsupported values with a typed error, and decodes the +// remainder of the stream into body via body.UnmarshalCBOR. +// +// On return: +// - *v is set to the observed version byte on success, or the +// rejected byte when the error is one of ErrReservedVersion or +// ErrUnsupportedVersion (so callers can log the raw byte). +// - A body-level decode failure is returned unwrapped from either +// version sentinel, which lets callers distinguish framing errors +// from payload errors via errors.Is. +func ReadEnvelope(r io.Reader, v *Version, body cbg.CBORUnmarshaler) error { + if v == nil { + return errors.New("cborx: ReadEnvelope: nil *Version") + } + if body == nil { + return errors.New("cborx: ReadEnvelope: nil body") + } + + var buf [1]byte + n, err := io.ReadFull(r, buf[:]) + if err != nil { + if (err == io.EOF || err == io.ErrUnexpectedEOF) && n == 0 { + return fmt.Errorf("%w", ErrEmptyEnvelope) + } + return fmt.Errorf("cborx: read version byte: %w", err) + } + + ver := Version(buf[0]) + *v = ver + + switch { + case ver == VersionReserved: + return fmt.Errorf("%w", ErrReservedVersion) + case ver == V1: + // ok + case ver > V1: + return fmt.Errorf("%w: 0x%02x", ErrUnsupportedVersion, uint8(ver)) + } + + if err := body.UnmarshalCBOR(r); err != nil { + return fmt.Errorf("cborx: unmarshal body: %w", err) + } + return nil +} + +// ReadEnvelopeStrict reads an envelope and additionally requires that +// the underlying reader is exhausted after the body decodes. Use this +// at every byte-slice and bounded-stream (io.LimitReader) boundary to +// reject non-canonical inputs that would otherwise round-trip to a +// shorter canonical encoding. +// +// Mirrors the trailing-byte check in ReadFrame (see frame.go). Stream- +// based callers that intentionally pipeline multiple envelopes on one +// connection should keep using ReadEnvelope; ReadEnvelopeStrict is for +// the canonical-single-value boundary. +func ReadEnvelopeStrict(r io.Reader, v *Version, body cbg.CBORUnmarshaler) error { + if err := ReadEnvelope(r, v, body); err != nil { + return err + } + var extra [1]byte + n, err := r.Read(extra[:]) + if n > 0 { + return fmt.Errorf("%w", ErrEnvelopeTrailingBytes) + } + if err == nil || err == io.EOF { + return nil + } + return fmt.Errorf("cborx: check envelope trailing bytes: %w", err) +} + +// UnmarshalExact decodes a single canonical CBOR value from data into +// body and rejects any trailing bytes. Use for raw-CBOR boundaries that +// do not carry a version envelope (e.g. BlockEntry.Payload). +func UnmarshalExact(data []byte, body cbg.CBORUnmarshaler) error { + if body == nil { + return errors.New("cborx: UnmarshalExact: nil body") + } + r := bytes.NewReader(data) + if err := body.UnmarshalCBOR(r); err != nil { + return fmt.Errorf("cborx: unmarshal body: %w", err) + } + if r.Len() != 0 { + return fmt.Errorf("%w: %d bytes remaining", ErrEnvelopeTrailingBytes, r.Len()) + } + return nil +} diff --git a/pkg/cborx/envelope_test.go b/pkg/cborx/envelope_test.go new file mode 100644 index 0000000..a8c5b02 --- /dev/null +++ b/pkg/cborx/envelope_test.go @@ -0,0 +1,130 @@ +package cborx_test + +import ( + "bytes" + "errors" + "io" + "math/big" + "testing" + + "github.com/layer-3/clearnet-sdk/pkg/cborx" +) + +func TestEnvelope_RoundTripV1(t *testing.T) { + body := cborx.BigInt{V: big.NewInt(1234567890)} + var buf bytes.Buffer + if err := cborx.WriteEnvelope(&buf, cborx.V1, body); err != nil { + t.Fatalf("WriteEnvelope: %v", err) + } + if buf.Bytes()[0] != byte(cborx.V1) { + t.Fatalf("first byte = 0x%02x, want 0x%02x", buf.Bytes()[0], byte(cborx.V1)) + } + + var got cborx.BigInt + var ver cborx.Version + if err := cborx.ReadEnvelope(&buf, &ver, &got); err != nil { + t.Fatalf("ReadEnvelope: %v", err) + } + if ver != cborx.V1 { + t.Fatalf("version = 0x%02x, want V1", uint8(ver)) + } + if got.V.Cmp(body.V) != 0 { + t.Fatalf("round-trip value = %s, want %s", got.V, body.V) + } +} + +func TestEnvelope_RejectReservedByte(t *testing.T) { + buf := bytes.NewReader([]byte{0x00, 0xc2, 0x40}) // 0x00 then a valid tag-2 zero + var got cborx.BigInt + var ver cborx.Version + err := cborx.ReadEnvelope(buf, &ver, &got) + if err == nil { + t.Fatalf("expected error reading reserved 0x00") + } + if !errors.Is(err, cborx.ErrReservedVersion) { + t.Fatalf("expected ErrReservedVersion, got %v", err) + } + if ver != cborx.VersionReserved { + t.Fatalf("ver should record the rejected byte (0x00), got 0x%02x", uint8(ver)) + } +} + +func TestEnvelope_RejectFutureVersion(t *testing.T) { + for _, b := range []byte{0x02, 0x03, 0x7f, 0x80, 0xff} { + buf := bytes.NewReader([]byte{b, 0xc2, 0x40}) + var got cborx.BigInt + var ver cborx.Version + err := cborx.ReadEnvelope(buf, &ver, &got) + if err == nil { + t.Fatalf("expected error reading 0x%02x", b) + } + if !errors.Is(err, cborx.ErrUnsupportedVersion) { + t.Fatalf("for 0x%02x: expected ErrUnsupportedVersion, got %v", b, err) + } + if uint8(ver) != b { + t.Fatalf("for 0x%02x: ver = 0x%02x, want 0x%02x", b, uint8(ver), b) + } + } +} + +func TestEnvelope_WriteRejectsReservedAndFuture(t *testing.T) { + body := cborx.BigInt{V: big.NewInt(0)} + + var buf bytes.Buffer + if err := cborx.WriteEnvelope(&buf, cborx.VersionReserved, body); !errors.Is(err, cborx.ErrReservedVersion) { + t.Fatalf("WriteEnvelope(0x00): expected ErrReservedVersion, got %v", err) + } + for _, v := range []cborx.Version{0x02, 0x7f, 0x80, 0xff} { + var tmp bytes.Buffer + if err := cborx.WriteEnvelope(&tmp, v, body); !errors.Is(err, cborx.ErrUnsupportedVersion) { + t.Fatalf("WriteEnvelope(0x%02x): expected ErrUnsupportedVersion, got %v", uint8(v), err) + } + } +} + +func TestEnvelope_RejectNilArguments(t *testing.T) { + var buf bytes.Buffer + if err := cborx.WriteEnvelope(&buf, cborx.V1, nil); err == nil { + t.Fatal("WriteEnvelope nil body: got nil error") + } + + var got cborx.BigInt + if err := cborx.ReadEnvelope(bytes.NewReader([]byte{0x01, 0xc2, 0x40}), nil, &got); err == nil { + t.Fatal("ReadEnvelope nil version pointer: got nil error") + } + var ver cborx.Version + if err := cborx.ReadEnvelope(bytes.NewReader([]byte{0x01, 0xc2, 0x40}), &ver, nil); err == nil { + t.Fatal("ReadEnvelope nil body: got nil error") + } +} + +func TestEnvelope_EmptyInput(t *testing.T) { + var got cborx.BigInt + var ver cborx.Version + err := cborx.ReadEnvelope(bytes.NewReader(nil), &ver, &got) + if !errors.Is(err, cborx.ErrEmptyEnvelope) { + t.Fatalf("empty input: expected ErrEmptyEnvelope, got %v", err) + } +} + +// brokenBody is a CBORUnmarshaler that always fails, used to prove +// body errors are distinct from version-byte errors. +type brokenBody struct{} + +func (brokenBody) UnmarshalCBOR(r io.Reader) error { return errors.New("body broken") } + +func TestEnvelope_BodyErrorDistinguishable(t *testing.T) { + // Valid V1 envelope followed by bytes the broken body will reject. + buf := bytes.NewReader([]byte{0x01, 0x00}) + var ver cborx.Version + err := cborx.ReadEnvelope(buf, &ver, brokenBody{}) + if err == nil { + t.Fatal("expected body error") + } + if errors.Is(err, cborx.ErrReservedVersion) || errors.Is(err, cborx.ErrUnsupportedVersion) || errors.Is(err, cborx.ErrEmptyEnvelope) { + t.Fatalf("body error should not match version sentinels; got %v", err) + } + if ver != cborx.V1 { + t.Fatalf("ver should be V1 once the version byte parses; got 0x%02x", uint8(ver)) + } +} diff --git a/pkg/cborx/frame.go b/pkg/cborx/frame.go new file mode 100644 index 0000000..3917f71 --- /dev/null +++ b/pkg/cborx/frame.go @@ -0,0 +1,143 @@ +package cborx + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + + cbg "github.com/whyrusleeping/cbor-gen" // layer-guard: allow +) + +// Frame size caps per docs/specs/cbor.md §5.2 / ADR-009 §8. Callers choose which cap +// applies to their stream: backfill (blocks/logs) is bulk, every other +// libp2p stream, the clearnet TCP listener, and every gossipsub +// message is control. +const ( + // MaxBulkFrame bounds a single length-prefixed frame for + // block / log backfill streams (1 MB). + MaxBulkFrame uint64 = 1 << 20 + + // MaxControlFrame bounds a single length-prefixed frame for every + // non-bulk libp2p stream, the clearnet TCP listener, and every + // gossipsub message (64 KB). + MaxControlFrame uint64 = 64 << 10 +) + +// ErrFrameTooLarge reports a declared frame length above the caller's +// cap. Callers should close the stream when this is returned — the +// remote is either mis-framed or attempting a DoS. +var ErrFrameTooLarge = errors.New("cborx: frame exceeds size cap") + +// ErrFrameTrailingBytes reports a frame whose declared length contains bytes +// after the envelope body has decoded. ADR-009 requires strict decode at the +// frame boundary rather than silently accepting schema drift or concatenation. +var ErrFrameTrailingBytes = errors.New("cborx: trailing bytes in frame") + +// WriteFrame writes a length-prefixed envelope to w: +// +// [uvarint body-length][1 byte version][CBOR body] +// +// where body-length covers the version byte + CBOR body (i.e. the +// envelope that WriteEnvelope would produce). Used on streams that +// carry more than one frame per connection: the clearnet TCP listener +// (docs/specs/cbor.md §5.2), libp2p backfill protocols, and anywhere a caller wants +// explicit per-frame bounds before allocation. +// +// Single-message libp2p request/response streams use this framing so +// receivers get an explicit message boundary without waiting for stream EOF. +func WriteFrame(w io.Writer, v Version, body cbg.CBORMarshaler) error { + var buf bytes.Buffer + if err := WriteEnvelope(&buf, v, body); err != nil { + return err + } + var hdr [binary.MaxVarintLen64]byte + n := binary.PutUvarint(hdr[:], uint64(buf.Len())) + if _, err := w.Write(hdr[:n]); err != nil { + return fmt.Errorf("cborx: write frame length: %w", err) + } + if _, err := w.Write(buf.Bytes()); err != nil { + return fmt.Errorf("cborx: write frame body: %w", err) + } + return nil +} + +// ReadFrame reads a length-prefixed envelope produced by WriteFrame. +// max is the largest body length this call will tolerate before +// returning ErrFrameTooLarge; callers pass MaxBulkFrame or +// MaxControlFrame depending on the stream. +// +// On success *v carries the observed version byte and body is filled +// via UnmarshalCBOR. A clean EOF before any bytes were read is +// reported as io.EOF (not wrapped) so callers can terminate backfill +// loops without special-casing ErrEmptyEnvelope. +func ReadFrame(r io.Reader, max uint64, v *Version, body cbg.CBORUnmarshaler) error { + if v == nil { + return errors.New("cborx: ReadFrame: nil *Version") + } + if body == nil { + return errors.New("cborx: ReadFrame: nil body") + } + + br := asByteReader(r) + length, err := binary.ReadUvarint(br) + if err != nil { + if err == io.EOF { + return io.EOF + } + return fmt.Errorf("cborx: read frame length: %w", err) + } + if length == 0 { + return fmt.Errorf("%w", ErrEmptyEnvelope) + } + if length > max { + return fmt.Errorf("%w: %d > %d", ErrFrameTooLarge, length, max) + } + + // Bound the envelope reader to exactly `length` bytes so a mis-sized + // frame can't bleed into the next frame's length prefix. + lr := io.LimitReader(br, int64(length)) + if err := ReadEnvelope(lr, v, body); err != nil { + return err + } + var extra [1]byte + if n, err := lr.Read(extra[:]); n > 0 || err == nil { + return fmt.Errorf("%w", ErrFrameTrailingBytes) + } else if err != io.EOF { + return fmt.Errorf("cborx: check frame trailing bytes: %w", err) + } + return nil +} + +// byteReaderAdapter wraps an io.Reader as an io.ByteReader for +// binary.ReadUvarint. Reads one byte at a time — acceptable for the +// varint header (at most 10 bytes). +type byteReaderAdapter struct{ r io.Reader } + +func (b byteReaderAdapter) ReadByte() (byte, error) { + var buf [1]byte + if _, err := io.ReadFull(b.r, buf[:]); err != nil { + return 0, err + } + return buf[0], nil +} + +// Read satisfies io.Reader so downstream callers (ReadFrame's +// io.LimitReader) can share one adapter across header + body. +func (b byteReaderAdapter) Read(p []byte) (int, error) { return b.r.Read(p) } + +// asByteReader returns r itself when it already implements io.ByteReader, +// or wraps it in a one-byte-at-a-time adapter otherwise. +func asByteReader(r io.Reader) interface { + io.Reader + io.ByteReader +} { + if br, ok := r.(interface { + io.Reader + io.ByteReader + }); ok { + return br + } + return byteReaderAdapter{r: r} +} diff --git a/pkg/cborx/frame_test.go b/pkg/cborx/frame_test.go new file mode 100644 index 0000000..281cd95 --- /dev/null +++ b/pkg/cborx/frame_test.go @@ -0,0 +1,75 @@ +package cborx_test + +import ( + "bytes" + "encoding/binary" + "errors" + "io" + "math/big" + "testing" + + "github.com/layer-3/clearnet-sdk/pkg/cborx" +) + +func TestFrameRoundTrip(t *testing.T) { + body := cborx.BigInt{V: big.NewInt(42)} + var buf bytes.Buffer + if err := cborx.WriteFrame(&buf, cborx.V1, body); err != nil { + t.Fatalf("WriteFrame: %v", err) + } + + var got cborx.BigInt + var ver cborx.Version + if err := cborx.ReadFrame(&buf, cborx.MaxControlFrame, &ver, &got); err != nil { + t.Fatalf("ReadFrame: %v", err) + } + if ver != cborx.V1 { + t.Fatalf("version = 0x%02x, want V1", uint8(ver)) + } + if got.V.Cmp(body.V) != 0 { + t.Fatalf("round-trip value = %s, want %s", got.V, body.V) + } +} + +func TestFrameRejectsOversizeBeforeBodyAllocation(t *testing.T) { + var hdr [binary.MaxVarintLen64]byte + n := binary.PutUvarint(hdr[:], cborx.MaxControlFrame+1) + buf := bytes.NewReader(hdr[:n]) + + var got cborx.BigInt + var ver cborx.Version + err := cborx.ReadFrame(buf, cborx.MaxControlFrame, &ver, &got) + if !errors.Is(err, cborx.ErrFrameTooLarge) { + t.Fatalf("expected ErrFrameTooLarge, got %v", err) + } +} + +func TestFrameCleanEOF(t *testing.T) { + var got cborx.BigInt + var ver cborx.Version + err := cborx.ReadFrame(bytes.NewReader(nil), cborx.MaxControlFrame, &ver, &got) + if !errors.Is(err, io.EOF) { + t.Fatalf("empty stream = %v, want io.EOF", err) + } +} + +func TestFrameRejectsTrailingBytesInsideDeclaredFrame(t *testing.T) { + var body bytes.Buffer + if err := cborx.WriteEnvelope(&body, cborx.V1, cborx.BigInt{V: big.NewInt(42)}); err != nil { + t.Fatalf("WriteEnvelope: %v", err) + } + body.WriteByte(0xff) + + var frame bytes.Buffer + var hdr [binary.MaxVarintLen64]byte + n := binary.PutUvarint(hdr[:], uint64(body.Len())) + frame.Write(hdr[:n]) + frame.Write(body.Bytes()) + + var got cborx.BigInt + var ver cborx.Version + err := cborx.ReadFrame(&frame, cborx.MaxControlFrame, &ver, &got) + if !errors.Is(err, cborx.ErrFrameTrailingBytes) { + t.Fatalf("expected ErrFrameTrailingBytes, got %v", err) + } +} diff --git a/pkg/cborx/goldens_test.go b/pkg/cborx/goldens_test.go new file mode 100644 index 0000000..04d8edb --- /dev/null +++ b/pkg/cborx/goldens_test.go @@ -0,0 +1,563 @@ +package cborx_test + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "flag" + "fmt" + "math/big" + "math/rand" + "os" + "path/filepath" + "runtime" + "sort" + "testing" + "time" + + cbor "github.com/fxamacker/cbor/v2" + + "github.com/layer-3/clearnet-sdk/pkg/cborx" + "github.com/layer-3/clearnet-sdk/pkg/decimal" +) + +// goldensUpdate writes new fixture files under testdata/cbor/primitives/. +// The flag is kept out of `go test` by default — CI treats a missing +// fixture as a failure, not a reason to re-seed. Run +// +// go test ./internal/cborx/... -update -run TestGoldenFixtures +// +// after intentionally changing an adapter to regenerate. +var goldensUpdate = flag.Bool("update", false, "regenerate testdata/cbor/primitives/* fixtures") + +// The fuzz seed is committed. A drift in this seed would re-seed every +// fixture and is a schema-level change, not a routine edit. +const fuzzSeed int64 = 0x0c801e115ca11baa + +// Iteration counts for the determinism fuzz. 200 instances × 1000 +// encodes per instance = 200,000 encode ops per adapter type, run in +// a randomized order to catch any map-iteration or pool-derived byte +// drift. CI bumps -count=10 for another 10× factor. +const ( + fuzzInstances = 200 + fuzzIterations = 1000 +) + +// testdataDir returns the absolute path of testdata/cbor/primitives +// regardless of cwd (go test may run from the package dir or a symlink). +func testdataDir(t *testing.T) string { + t.Helper() + _, file, _, ok := runtime.Caller(0) + if !ok { + t.Fatal("runtime.Caller failed") + } + // file = .../internal/cborx/goldens_test.go + repoRoot := filepath.Join(filepath.Dir(file), "..", "..") + return filepath.Join(repoRoot, "testdata", "cbor", "primitives") +} + +// fixture is one golden: a typed description of the input plus its +// expected canonical-CBOR bytes. The bytes are committed separately as +// `_.golden.hex` and the description as +// `_.input.json` so a human can eyeball what each byte +// string represents. +type fixture struct { + Case string `json:"case"` + Notes string `json:"notes,omitempty"` + Input any `json:"input"` + Golden string `json:"-"` +} + +// ----- BigInt fixtures -------------------------------------------------- + +type bigIntInput struct { + Hex string `json:"hex"` + Sign int `json:"sign"` +} + +func biFx(name, notes string, n *big.Int) fixture { + var buf bytes.Buffer + if err := (cborx.BigInt{V: n}).MarshalCBOR(&buf); err != nil { + panic(err) + } + return fixture{ + Case: name, + Notes: notes, + Input: bigIntInput{Hex: n.Text(16), Sign: n.Sign()}, + Golden: hex.EncodeToString(buf.Bytes()), + } +} + +func bigIntFixtures() []fixture { + return []fixture{ + biFx("zero", "canonical tag 2 + empty byte string", big.NewInt(0)), + biFx("one", "smallest unsigned bignum", big.NewInt(1)), + biFx("minus_one", "canonical tag 3 + empty byte string", big.NewInt(-1)), + biFx("boundary_255", "last single-byte magnitude", big.NewInt(255)), + biFx("boundary_256", "first 2-byte magnitude", big.NewInt(256)), + biFx("u32_max", "2^32 - 1", big.NewInt(1<<32-1)), + biFx("u32_max_plus_one", "2^32", big.NewInt(1<<32)), + biFx("u64_max", "2^64 - 1", new(big.Int).SetUint64(^uint64(0))), + biFx("two_pow_128", "17-byte magnitude", new(big.Int).Lsh(big.NewInt(1), 128)), + biFx("minus_25", "negative-int encoding boundary", big.NewInt(-25)), + biFx("neg_two_pow_128", "17-byte negative magnitude", new(big.Int).Neg(new(big.Int).Lsh(big.NewInt(1), 128))), + } +} + +// ----- Decimal fixtures ------------------------------------------------- + +type decimalInput struct { + Mantissa string `json:"mantissa"` + Exponent int32 `json:"exponent"` +} + +func decFx(name, notes string, mantissa *big.Int, exp int32) fixture { + d := decimal.NewFromBigInt(mantissa, exp) + var buf bytes.Buffer + if err := (cborx.Decimal{V: d}).MarshalCBOR(&buf); err != nil { + panic(err) + } + return fixture{ + Case: name, + Notes: notes, + Input: decimalInput{Mantissa: mantissa.Text(10), Exponent: exp}, + Golden: hex.EncodeToString(buf.Bytes()), + } +} + +func decimalFixtures() []fixture { + return []fixture{ + decFx("zero", "mantissa 0, exp 0", big.NewInt(0), 0), + decFx("one", "mantissa 1, exp 0", big.NewInt(1), 0), + decFx("one_point_five", "mantissa 15, exp -1", big.NewInt(15), -1), + decFx("neg_half", "mantissa -5, exp -1", big.NewInt(-5), -1), + decFx("wei_precision", "1 at exp -18 (wei)", big.NewInt(1), -18), + decFx("mega_unit", "1 at exp 6", big.NewInt(1), 6), + decFx("ten_pow_40_at_-18", "mantissa >int64, exp -18", new(big.Int).Exp(big.NewInt(10), big.NewInt(40), nil), -18), + } +} + +// ----- Hash32 / Addr20 fixtures ----------------------------------------- + +type hashInput struct { + Hex string `json:"hex"` +} + +func hashFx(name string, b []byte) fixture { + if len(b) != 32 { + panic("bad hash length") + } + var h cborx.Hash32 + copy(h.V[:], b) + var buf bytes.Buffer + if err := h.MarshalCBOR(&buf); err != nil { + panic(err) + } + return fixture{ + Case: name, + Notes: "major 2, definite length 32", + Input: hashInput{Hex: hex.EncodeToString(b)}, + Golden: hex.EncodeToString(buf.Bytes()), + } +} + +func addrFx(name string, b []byte) fixture { + if len(b) != 20 { + panic("bad addr length") + } + var a cborx.Addr20 + copy(a.V[:], b) + var buf bytes.Buffer + if err := a.MarshalCBOR(&buf); err != nil { + panic(err) + } + return fixture{ + Case: name, + Notes: "major 2, definite length 20", + Input: hashInput{Hex: hex.EncodeToString(b)}, + Golden: hex.EncodeToString(buf.Bytes()), + } +} + +func hashFixtures() []fixture { + return []fixture{ + hashFx("zero", bytes.Repeat([]byte{0}, 32)), + hashFx("one", bytes.Repeat([]byte{1}, 32)), + hashFx("max", bytes.Repeat([]byte{0xff}, 32)), + hashFx("incrementing", func() []byte { + b := make([]byte, 32) + for i := range b { + b[i] = byte(i) + } + return b + }()), + } +} + +func addrFixtures() []fixture { + return []fixture{ + addrFx("zero", bytes.Repeat([]byte{0}, 20)), + addrFx("vitalik_like", func() []byte { + b, _ := hex.DecodeString("d8da6bf26964af9d7eed9e03e53415d37aa96045") + return b + }()), + } +} + +// ----- Time fixtures ---------------------------------------------------- + +type timeInput struct { + UnixNano int64 `json:"unix_nano"` +} + +func timeFx(name, notes string, ns int64) fixture { + tm := time.Unix(0, ns).UTC() + var buf bytes.Buffer + if err := (cborx.Time{V: tm}).MarshalCBOR(&buf); err != nil { + panic(err) + } + return fixture{ + Case: name, + Notes: notes, + Input: timeInput{UnixNano: ns}, + Golden: hex.EncodeToString(buf.Bytes()), + } +} + +func timeFixtures() []fixture { + return []fixture{ + timeFx("epoch", "unix 0 → major 0, value 0", 0), + timeFx("one_ns", "smallest positive ns", 1), + timeFx("minus_one_ns", "smallest negative ns", -1), + timeFx("modern", "a specific block-producing moment", 1_700_000_000_123_456_789), + } +} + +// ----- MaybeBigInt fixtures --------------------------------------------- + +type maybeInput struct { + Present bool `json:"present"` + Value string `json:"value,omitempty"` +} + +func maybeFx(name, notes string, n *big.Int) fixture { + var buf bytes.Buffer + if err := (cborx.MaybeBigInt{V: n}).MarshalCBOR(&buf); err != nil { + panic(err) + } + input := maybeInput{Present: n != nil} + if n != nil { + input.Value = n.Text(10) + } + return fixture{ + Case: name, + Notes: notes, + Input: input, + Golden: hex.EncodeToString(buf.Bytes()), + } +} + +func maybeFixtures() []fixture { + return []fixture{ + maybeFx("nil", "nil encodes as CBOR null (0xf6)", nil), + maybeFx("zero", "zero is encoded as BigInt zero (non-nil)", big.NewInt(0)), + maybeFx("present_positive", "", big.NewInt(42)), + maybeFx("present_negative", "", big.NewInt(-42)), + } +} + +// ----- Envelope fixtures ------------------------------------------------ + +type envelopeInput struct { + Version string `json:"version"` + Body string `json:"body_description"` +} + +func envelopeFixtures() []fixture { + var out []fixture + for _, inner := range []struct { + name string + n *big.Int + }{ + {"v1_bigint_zero", big.NewInt(0)}, + {"v1_bigint_one", big.NewInt(1)}, + } { + var buf bytes.Buffer + if err := cborx.WriteEnvelope(&buf, cborx.V1, cborx.BigInt{V: inner.n}); err != nil { + panic(err) + } + out = append(out, fixture{ + Case: inner.name, + Notes: "V1 envelope wrapping a BigInt", + Input: envelopeInput{Version: "V1 (0x01)", Body: "BigInt " + inner.n.String()}, + Golden: hex.EncodeToString(buf.Bytes()), + }) + } + return out +} + +// ----- Fixture driver --------------------------------------------------- + +type fixtureGroup struct { + name string + fxs []fixture +} + +func allFixtureGroups() []fixtureGroup { + return []fixtureGroup{ + {"bigint", bigIntFixtures()}, + {"maybe_bigint", maybeFixtures()}, + {"decimal", decimalFixtures()}, + {"hash32", hashFixtures()}, + {"addr20", addrFixtures()}, + {"time", timeFixtures()}, + {"envelope", envelopeFixtures()}, + } +} + +func TestGoldenFixtures(t *testing.T) { + dir := testdataDir(t) + if *goldensUpdate { + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatalf("mkdir %s: %v", dir, err) + } + } + + groups := allFixtureGroups() + for _, g := range groups { + for _, fx := range g.fxs { + stem := filepath.Join(dir, fmt.Sprintf("%s_%s", g.name, fx.Case)) + inputPath := stem + ".input.json" + goldenPath := stem + ".golden.hex" + + inputJSON, err := json.MarshalIndent(fx.Input, "", " ") + if err != nil { + t.Fatalf("marshal fixture description %s: %v", fx.Case, err) + } + inputJSON = append(inputJSON, '\n') + + goldenBytes := []byte(fx.Golden + "\n") + + if *goldensUpdate { + if err := os.WriteFile(inputPath, inputJSON, 0o644); err != nil { + t.Fatalf("write %s: %v", inputPath, err) + } + if err := os.WriteFile(goldenPath, goldenBytes, 0o644); err != nil { + t.Fatalf("write %s: %v", goldenPath, err) + } + continue + } + + gotGolden, err := os.ReadFile(goldenPath) + if err != nil { + t.Errorf("read %s: %v — run `go test -update` to regenerate", goldenPath, err) + continue + } + if string(gotGolden) != string(goldenBytes) { + t.Errorf("%s: golden drift\n file: %s\n current: %s", + stem, string(gotGolden), string(goldenBytes)) + } + gotInput, err := os.ReadFile(inputPath) + if err != nil { + t.Errorf("read %s: %v", inputPath, err) + continue + } + if string(gotInput) != string(inputJSON) { + t.Errorf("%s: input.json drift\n file: %s\n current: %s", + stem, string(gotInput), string(inputJSON)) + } + + // Cross-validate with an independent CBOR decoder so we + // know the bytes parse as RFC 8949 (not just + // round-trip through cbor-gen). The envelope group + // prepends a non-CBOR version byte, so skip the body + // check there — the adapter-level fixtures on their + // own already prove the body bytes are valid CBOR. + if g.name == "envelope" { + continue + } + raw, err := hex.DecodeString(fx.Golden) + if err != nil { + t.Fatalf("decode %s golden hex: %v", stem, err) + } + var any interface{} + if err := cbor.Unmarshal(raw, &any); err != nil { + t.Errorf("%s: fxamacker rejects our bytes: %v (hex=%s)", stem, err, fx.Golden) + } + } + } +} + +// TestDeterminism is the wave-critical test: generate a fixed-seed +// population of each adapter type and encode each one fuzzInstances × +// fuzzIterations times in a shuffled order. Every encoding must match +// the first byte-for-byte. +func TestDeterminism(t *testing.T) { + rng := rand.New(rand.NewSource(fuzzSeed)) + + instances := buildFuzzInstances(rng) + if len(instances) != fuzzInstances*7 { + t.Fatalf("expected %d instances, got %d", fuzzInstances*7, len(instances)) + } + + // First-pass canonical encoding. + canon := make(map[int][]byte, len(instances)) + for i, inst := range instances { + canon[i] = inst.encode() + } + + // Iterate over the whole population, in shuffled order each round, + // re-encoding and asserting byte-equality against canon[i]. + for round := 0; round < fuzzIterations; round++ { + order := rng.Perm(len(instances)) + for _, i := range order { + got := instances[i].encode() + if !bytes.Equal(got, canon[i]) { + t.Fatalf("round %d, inst #%d (%s): byte drift\n canon: %x\n got: %x", + round, i, instances[i].label, canon[i], got) + } + } + } +} + +// fuzzInstance is one typed encode target. +type fuzzInstance struct { + label string + encode func() []byte +} + +func buildFuzzInstances(rng *rand.Rand) []fuzzInstance { + var all []fuzzInstance + + // BigInt + for i := 0; i < fuzzInstances; i++ { + n := randomBigInt(rng) + all = append(all, fuzzInstance{ + label: "BigInt", + encode: func() []byte { + var buf bytes.Buffer + if err := (cborx.BigInt{V: n}).MarshalCBOR(&buf); err != nil { + panic(err) + } + return buf.Bytes() + }, + }) + } + + // MaybeBigInt + for i := 0; i < fuzzInstances; i++ { + var n *big.Int + if rng.Intn(4) == 0 { + n = nil + } else { + n = randomBigInt(rng) + } + all = append(all, fuzzInstance{ + label: "MaybeBigInt", + encode: func() []byte { + var buf bytes.Buffer + if err := (cborx.MaybeBigInt{V: n}).MarshalCBOR(&buf); err != nil { + panic(err) + } + return buf.Bytes() + }, + }) + } + + // Decimal + for i := 0; i < fuzzInstances; i++ { + mant := randomBigInt(rng) + exp := int32(rng.Intn(37) - 18) + d := decimal.NewFromBigInt(mant, exp) + all = append(all, fuzzInstance{ + label: "Decimal", + encode: func() []byte { + var buf bytes.Buffer + if err := (cborx.Decimal{V: d}).MarshalCBOR(&buf); err != nil { + panic(err) + } + return buf.Bytes() + }, + }) + } + + // Hash32 + for i := 0; i < fuzzInstances; i++ { + var h cborx.Hash32 + rng.Read(h.V[:]) + all = append(all, fuzzInstance{ + label: "Hash32", + encode: func() []byte { + var buf bytes.Buffer + if err := h.MarshalCBOR(&buf); err != nil { + panic(err) + } + return buf.Bytes() + }, + }) + } + + // Addr20 + for i := 0; i < fuzzInstances; i++ { + var a cborx.Addr20 + rng.Read(a.V[:]) + all = append(all, fuzzInstance{ + label: "Addr20", + encode: func() []byte { + var buf bytes.Buffer + if err := a.MarshalCBOR(&buf); err != nil { + panic(err) + } + return buf.Bytes() + }, + }) + } + + // Time + for i := 0; i < fuzzInstances; i++ { + ns := rng.Int63n(2_000_000_000_000_000_000) - 1_000_000_000_000_000_000 + tm := time.Unix(0, ns).UTC() + all = append(all, fuzzInstance{ + label: "Time", + encode: func() []byte { + var buf bytes.Buffer + if err := (cborx.Time{V: tm}).MarshalCBOR(&buf); err != nil { + panic(err) + } + return buf.Bytes() + }, + }) + } + + // Envelope-wrapped BigInt + for i := 0; i < fuzzInstances; i++ { + n := randomBigInt(rng) + all = append(all, fuzzInstance{ + label: "Envelope(V1, BigInt)", + encode: func() []byte { + var buf bytes.Buffer + if err := cborx.WriteEnvelope(&buf, cborx.V1, cborx.BigInt{V: n}); err != nil { + panic(err) + } + return buf.Bytes() + }, + }) + } + + // Stable ordering for reproducibility across test invocations. + sort.SliceStable(all, func(i, j int) bool { return all[i].label < all[j].label }) + return all +} + +// randomBigInt produces a uniformly-distributed signed big integer with +// a random byte length between 0 and 32. +func randomBigInt(rng *rand.Rand) *big.Int { + width := rng.Intn(33) // 0..32 bytes + b := make([]byte, width) + rng.Read(b) + n := new(big.Int).SetBytes(b) + if rng.Intn(2) == 0 { + n.Neg(n) + } + return n +} diff --git a/pkg/cborx/hash.go b/pkg/cborx/hash.go new file mode 100644 index 0000000..3b3d40a --- /dev/null +++ b/pkg/cborx/hash.go @@ -0,0 +1,98 @@ +package cborx + +import ( + "fmt" + "io" + + cbg "github.com/whyrusleeping/cbor-gen" // layer-guard: allow +) + +// Hash32Len / Addr20Len fix the declared definite lengths for the +// hash / address adapters. The length is part of the encoding +// contract, not a bound — a decoder that reads a different length +// rejects. +const ( + Hash32Len = 32 + Addr20Len = 20 +) + +// Hash32 wraps a 32-byte digest (keccak256 output, block hash, entry +// hash, state root, etc.). Encoded as CBOR major type 2 with a definite +// length of 32. +type Hash32 struct { + V [Hash32Len]byte +} + +// MarshalCBOR writes the 32-byte byte string with the canonical +// shortest-form length (a single byte in this case). +func (h Hash32) MarshalCBOR(w io.Writer) error { + if err := cbg.WriteMajorTypeHeader(w, cbg.MajByteString, Hash32Len); err != nil { + return fmt.Errorf("cborx: Hash32: write header: %w", err) + } + if _, err := w.Write(h.V[:]); err != nil { + return fmt.Errorf("cborx: Hash32: write body: %w", err) + } + return nil +} + +// UnmarshalCBOR reads exactly 32 bytes into h.V; any other length is +// a hard reject. +func (h *Hash32) UnmarshalCBOR(r io.Reader) error { + maj, val, err := cbg.CborReadHeader(r) + if err != nil { + return fmt.Errorf("cborx: Hash32: read header: %w", err) + } + if maj != cbg.MajByteString { + return fmt.Errorf("cborx: Hash32: expected byte string (major 2), got major %d", maj) + } + if val != Hash32Len { + return fmt.Errorf("cborx: Hash32: expected length %d, got %d", Hash32Len, val) + } + if _, err := io.ReadFull(r, h.V[:]); err != nil { + return fmt.Errorf("cborx: Hash32: read body: %w", err) + } + return nil +} + +// Addr20 wraps a 20-byte Ethereum-style address as a bare byte array +// so internal/cborx can stay free of outer-layer dependencies. Wave 2 +// generated codecs pass a go-ethereum common.Address in via an explicit +// [20]byte conversion (Address is a named [20]byte type). +// +// Encoded as CBOR major type 2 with a definite length of 20. Round-trips +// bit-identically to the ABI encoding of a bare 20-byte address slice +// so later boundaries (e.g. custody/evm/) can splice the bytes without +// extra conversion. +type Addr20 struct { + V [Addr20Len]byte +} + +// MarshalCBOR writes the 20-byte byte string. +func (a Addr20) MarshalCBOR(w io.Writer) error { + if err := cbg.WriteMajorTypeHeader(w, cbg.MajByteString, Addr20Len); err != nil { + return fmt.Errorf("cborx: Addr20: write header: %w", err) + } + if _, err := w.Write(a.V[:]); err != nil { + return fmt.Errorf("cborx: Addr20: write body: %w", err) + } + return nil +} + +// UnmarshalCBOR reads exactly 20 bytes into a.V; any other length is +// a hard reject. +func (a *Addr20) UnmarshalCBOR(r io.Reader) error { + maj, val, err := cbg.CborReadHeader(r) + if err != nil { + return fmt.Errorf("cborx: Addr20: read header: %w", err) + } + if maj != cbg.MajByteString { + return fmt.Errorf("cborx: Addr20: expected byte string (major 2), got major %d", maj) + } + if val != Addr20Len { + return fmt.Errorf("cborx: Addr20: expected length %d, got %d", Addr20Len, val) + } + if _, err := io.ReadFull(r, a.V[:]); err != nil { + return fmt.Errorf("cborx: Addr20: read body: %w", err) + } + return nil +} diff --git a/pkg/cborx/hash_test.go b/pkg/cborx/hash_test.go new file mode 100644 index 0000000..23ad6c1 --- /dev/null +++ b/pkg/cborx/hash_test.go @@ -0,0 +1,151 @@ +package cborx_test + +import ( + "bytes" + "encoding/hex" + "strings" + "testing" + + "github.com/layer-3/clearnet-sdk/pkg/cborx" +) + +func TestHash32_EncodesWithDefiniteLength32(t *testing.T) { + var h cborx.Hash32 + for i := range h.V { + h.V[i] = byte(i) + } + var buf bytes.Buffer + if err := h.MarshalCBOR(&buf); err != nil { + t.Fatalf("marshal: %v", err) + } + // major type 2 with length 32 = 0x58 0x20, then the 32 bytes. + wantPrefix := "5820" + got := hex.EncodeToString(buf.Bytes()) + if !strings.HasPrefix(got, wantPrefix) { + t.Fatalf("prefix = %s, want %s", got[:len(wantPrefix)], wantPrefix) + } + if len(buf.Bytes()) != 2+32 { + t.Fatalf("total length = %d, want 34", len(buf.Bytes())) + } +} + +func TestHash32_RoundTrip(t *testing.T) { + var h cborx.Hash32 + for i := range h.V { + h.V[i] = byte(0xA0 ^ i) + } + var buf bytes.Buffer + if err := h.MarshalCBOR(&buf); err != nil { + t.Fatalf("marshal: %v", err) + } + + var got cborx.Hash32 + if err := got.UnmarshalCBOR(bytes.NewReader(buf.Bytes())); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if got.V != h.V { + t.Fatalf("round-trip mismatch: %x vs %x", got.V, h.V) + } + + var buf2 bytes.Buffer + if err := got.MarshalCBOR(&buf2); err != nil { + t.Fatalf("re-marshal: %v", err) + } + if !bytes.Equal(buf.Bytes(), buf2.Bytes()) { + t.Fatalf("idempotence: %x vs %x", buf.Bytes(), buf2.Bytes()) + } +} + +func TestHash32_RejectWrongLength(t *testing.T) { + cases := [][]byte{ + // length 0: 0x40 + {0x40}, + // length 31: 0x58 0x1f + 31 zero bytes + append([]byte{0x58, 0x1f}, bytes.Repeat([]byte{0}, 31)...), + // length 33: 0x58 0x21 + 33 zero bytes + append([]byte{0x58, 0x21}, bytes.Repeat([]byte{0}, 33)...), + } + for i, c := range cases { + var got cborx.Hash32 + if err := got.UnmarshalCBOR(bytes.NewReader(c)); err == nil { + t.Errorf("case %d: expected rejection", i) + } + } +} + +func TestHash32_RejectWrongMajor(t *testing.T) { + // Text string (major 3) of length 32 — wrong major. + raw := append([]byte{0x78, 0x20}, bytes.Repeat([]byte{'a'}, 32)...) + var got cborx.Hash32 + if err := got.UnmarshalCBOR(bytes.NewReader(raw)); err == nil { + t.Fatalf("expected rejection of major 3") + } +} + +func TestAddr20_EncodesWithDefiniteLength20(t *testing.T) { + var a cborx.Addr20 + for i := range a.V { + a.V[i] = byte(i + 1) + } + var buf bytes.Buffer + if err := a.MarshalCBOR(&buf); err != nil { + t.Fatalf("marshal: %v", err) + } + // major 2, length 20 = 0x54. + if buf.Bytes()[0] != 0x54 { + t.Fatalf("first byte = 0x%02x, want 0x54", buf.Bytes()[0]) + } + if len(buf.Bytes()) != 1+20 { + t.Fatalf("total length = %d, want 21", len(buf.Bytes())) + } +} + +func TestAddr20_RoundTrip(t *testing.T) { + var a cborx.Addr20 + for i := range a.V { + a.V[i] = byte(i) + } + var buf bytes.Buffer + if err := a.MarshalCBOR(&buf); err != nil { + t.Fatalf("marshal: %v", err) + } + + var got cborx.Addr20 + if err := got.UnmarshalCBOR(bytes.NewReader(buf.Bytes())); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if got.V != a.V { + t.Fatalf("round-trip mismatch: %x vs %x", got.V, a.V) + } + + var buf2 bytes.Buffer + if err := got.MarshalCBOR(&buf2); err != nil { + t.Fatalf("re-marshal: %v", err) + } + if !bytes.Equal(buf.Bytes(), buf2.Bytes()) { + t.Fatalf("idempotence: %x vs %x", buf.Bytes(), buf2.Bytes()) + } +} + +func TestAddr20_RejectWrongLength(t *testing.T) { + cases := [][]byte{ + {0x40}, // 0 bytes + append([]byte{0x53}, bytes.Repeat([]byte{0}, 19)...), // 19 bytes + append([]byte{0x55}, bytes.Repeat([]byte{0}, 21)...), // 21 bytes + } + for i, c := range cases { + var got cborx.Addr20 + if err := got.UnmarshalCBOR(bytes.NewReader(c)); err == nil { + t.Errorf("case %d: expected rejection", i) + } + } +} + +func TestAddr20_RejectWrongMajor(t *testing.T) { + // Text string (major 3) of length 20 — wrong major. + raw := append([]byte{0x74}, bytes.Repeat([]byte{'a'}, 20)...) + var got cborx.Addr20 + if err := got.UnmarshalCBOR(bytes.NewReader(raw)); err == nil { + t.Fatalf("expected rejection of major 3") + } +} diff --git a/pkg/cborx/maybe.go b/pkg/cborx/maybe.go new file mode 100644 index 0000000..b8865a0 --- /dev/null +++ b/pkg/cborx/maybe.go @@ -0,0 +1,58 @@ +package cborx + +import ( + "fmt" + "io" + "math/big" + + cbg "github.com/whyrusleeping/cbor-gen" // layer-guard: allow +) + +// cborNullByte is CBOR's null encoding: major type 7, simple value 22 +// → 0xf6. +const cborNullByte byte = 0xf6 + +// MaybeBigInt wraps a nilable *big.Int. CBOR null decodes to a +// MaybeBigInt with V == nil; any other input is delegated to BigInt. +// Useful for optional-amount fields (e.g. a MinAmountOut that may be +// absent rather than zero). +type MaybeBigInt struct { + V *big.Int +} + +// MarshalCBOR writes CBOR null when V == nil, otherwise the BigInt form. +func (m MaybeBigInt) MarshalCBOR(w io.Writer) error { + if m.V == nil { + if _, err := w.Write([]byte{cborNullByte}); err != nil { + return fmt.Errorf("cborx: MaybeBigInt: write null: %w", err) + } + return nil + } + return BigInt{V: m.V}.MarshalCBOR(w) +} + +// UnmarshalCBOR peeks the first byte to distinguish CBOR null from a +// tagged bignum. The cbg.CborReader wrapper provides the unread +// capability we need regardless of the underlying reader type. +func (m *MaybeBigInt) UnmarshalCBOR(r io.Reader) error { + cr := cbg.NewCborReader(r) + + first, err := cr.ReadByte() + if err != nil { + return fmt.Errorf("cborx: MaybeBigInt: peek: %w", err) + } + if first == cborNullByte { + m.V = nil + return nil + } + if err := cr.UnreadByte(); err != nil { + return fmt.Errorf("cborx: MaybeBigInt: unread: %w", err) + } + + var inner BigInt + if err := inner.UnmarshalCBOR(cr); err != nil { + return err + } + m.V = inner.V + return nil +} diff --git a/pkg/cborx/maybe_test.go b/pkg/cborx/maybe_test.go new file mode 100644 index 0000000..792fd91 --- /dev/null +++ b/pkg/cborx/maybe_test.go @@ -0,0 +1,84 @@ +package cborx_test + +import ( + "bytes" + "math/big" + "testing" + + "github.com/layer-3/clearnet-sdk/pkg/cborx" +) + +func TestMaybeBigInt_NilEncodesAsNull(t *testing.T) { + var buf bytes.Buffer + if err := (cborx.MaybeBigInt{V: nil}).MarshalCBOR(&buf); err != nil { + t.Fatalf("marshal: %v", err) + } + want := []byte{0xf6} // CBOR null + if !bytes.Equal(buf.Bytes(), want) { + t.Fatalf("nil MaybeBigInt = %x, want %x", buf.Bytes(), want) + } +} + +func TestMaybeBigInt_NullDecodesToNil(t *testing.T) { + m := cborx.MaybeBigInt{V: big.NewInt(42)} // pre-populated, expect override + if err := m.UnmarshalCBOR(bytes.NewReader([]byte{0xf6})); err != nil { + t.Fatalf("unmarshal null: %v", err) + } + if m.V != nil { + t.Fatalf("expected nil V, got %v", m.V) + } +} + +func TestMaybeBigInt_ValueRoundTrips(t *testing.T) { + for _, n := range []*big.Int{ + big.NewInt(0), + big.NewInt(1), + big.NewInt(-1), + big.NewInt(1 << 62), + new(big.Int).Lsh(big.NewInt(1), 100), + } { + var buf bytes.Buffer + if err := (cborx.MaybeBigInt{V: n}).MarshalCBOR(&buf); err != nil { + t.Fatalf("marshal %s: %v", n, err) + } + var got cborx.MaybeBigInt + if err := got.UnmarshalCBOR(bytes.NewReader(buf.Bytes())); err != nil { + t.Fatalf("unmarshal %s: %v", n, err) + } + if got.V == nil || got.V.Cmp(n) != 0 { + t.Fatalf("round-trip %s: got %v", n, got.V) + } + + // Idempotence. + var buf2 bytes.Buffer + if err := got.MarshalCBOR(&buf2); err != nil { + t.Fatalf("re-marshal: %v", err) + } + if !bytes.Equal(buf.Bytes(), buf2.Bytes()) { + t.Fatalf("idempotence %s: %x vs %x", n, buf.Bytes(), buf2.Bytes()) + } + } +} + +func TestMaybeBigInt_RejectMalformedNonNull(t *testing.T) { + // Anything other than CBOR null must be a canonical BigInt tag 2/3 value. + // CBOR false is a valid simple value, but it is not a MaybeBigInt encoding. + var got cborx.MaybeBigInt + if err := got.UnmarshalCBOR(bytes.NewReader([]byte{0xf4})); err == nil { + t.Fatal("expected malformed non-null MaybeBigInt to reject") + } +} + +func TestMaybeBigInt_NilIdempotence(t *testing.T) { + var buf bytes.Buffer + _ = (cborx.MaybeBigInt{V: nil}).MarshalCBOR(&buf) + + var got cborx.MaybeBigInt + _ = got.UnmarshalCBOR(bytes.NewReader(buf.Bytes())) + + var buf2 bytes.Buffer + _ = got.MarshalCBOR(&buf2) + if !bytes.Equal(buf.Bytes(), buf2.Bytes()) { + t.Fatalf("nil idempotence: %x vs %x", buf.Bytes(), buf2.Bytes()) + } +} diff --git a/pkg/cborx/rfc_determinism_test.go b/pkg/cborx/rfc_determinism_test.go new file mode 100644 index 0000000..3d23ab2 --- /dev/null +++ b/pkg/cborx/rfc_determinism_test.go @@ -0,0 +1,102 @@ +package cborx_test + +import ( + "bytes" + "testing" + + "github.com/layer-3/clearnet-sdk/pkg/cborx" +) + +// Hand-assembled wire bytes from RFC 8949 §4.2 violations. Every entry +// below is a byte string a malicious or buggy peer could send that +// MUST be rejected by our adapter pipeline. +var determinismRejects = map[string][]byte{ + // BigInt: tag 2 with byte-string length encoded 2-byte-wide for a + // value that fits in 1 byte. cbor-gen's reader catches + // "lval 25 with value <= MaxUint8". + "bigint_non_shortest_len_2byte": {0xc2, 0x59, 0x00, 0x01, 0x01}, + // BigInt: tag 2 encoded with a one-byte payload instead of the direct + // additional-info form. Canonical tag 2 is 0xc2, not 0xd8 0x02. + "bigint_non_shortest_tag": {0xd8, 0x02, 0x40}, + // BigInt: tag 2 with byte-string length encoded 4-byte-wide for a + // value that fits in 2 bytes. + "bigint_non_shortest_len_4byte": {0xc2, 0x5a, 0x00, 0x00, 0x01, 0x00, 0x01, 0x02}, + // BigInt: tag 2 indefinite-length byte string. + "bigint_indefinite_bytestring": {0xc2, 0x5f, 0x41, 0x01, 0xff}, + // Time: half-precision NaN (0xf9 0x7e 0x00). + "time_float_nan": {0xf9, 0x7e, 0x00}, + // Time: half-precision +Inf (0xf9 0x7c 0x00). + "time_float_inf": {0xf9, 0x7c, 0x00}, + // Time: single-precision negative zero (0xfa 0x80 0x00 0x00 0x00). + "time_float_neg_zero": {0xfa, 0x80, 0x00, 0x00, 0x00}, + // Decimal: tag 4, array-length encoded 2-byte-wide instead of 1-byte. + "decimal_non_shortest_array": {0xc4, 0x98, 0x02, 0x00, 0xc2, 0x40}, + // Decimal: tag 4 encoded with a one-byte payload instead of direct tag 4. + "decimal_non_shortest_tag": {0xd8, 0x04, 0x82, 0x00, 0xc2, 0x40}, + // Hash32: indefinite-length byte string of 32 bytes total. + "hash32_indefinite": append([]byte{0x5f, 0x58, 0x20}, append(bytes.Repeat([]byte{0}, 32), 0xff)...), + // Hash32: fixed byte-string length 32 encoded 2-byte-wide instead of + // the shortest one-byte length payload. + "hash32_non_shortest_len": append([]byte{0x59, 0x00, 0x20}, bytes.Repeat([]byte{0}, 32)...), + // Addr20: length 20 fits directly in the additional-info bits and must + // not be encoded using an extra one-byte payload. + "addr20_non_shortest_len": append([]byte{0x58, 0x14}, bytes.Repeat([]byte{0}, 20)...), +} + +func TestDeterminismRejects_BigInt(t *testing.T) { + for name, raw := range map[string][]byte{ + "bigint_non_shortest_len_2byte": determinismRejects["bigint_non_shortest_len_2byte"], + "bigint_non_shortest_tag": determinismRejects["bigint_non_shortest_tag"], + "bigint_non_shortest_len_4byte": determinismRejects["bigint_non_shortest_len_4byte"], + "bigint_indefinite_bytestring": determinismRejects["bigint_indefinite_bytestring"], + } { + var got cborx.BigInt + if err := got.UnmarshalCBOR(bytes.NewReader(raw)); err == nil { + t.Errorf("%s: expected rejection, decoded %s", name, got.V) + } + } +} + +func TestDeterminismRejects_Time(t *testing.T) { + for name, raw := range map[string][]byte{ + "time_float_nan": determinismRejects["time_float_nan"], + "time_float_inf": determinismRejects["time_float_inf"], + "time_float_neg_zero": determinismRejects["time_float_neg_zero"], + } { + var got cborx.Time + if err := got.UnmarshalCBOR(bytes.NewReader(raw)); err == nil { + t.Errorf("%s: expected rejection of float, decoded %v", name, got.V) + } + } +} + +func TestDeterminismRejects_Decimal(t *testing.T) { + for name, raw := range map[string][]byte{ + "decimal_non_shortest_array": determinismRejects["decimal_non_shortest_array"], + "decimal_non_shortest_tag": determinismRejects["decimal_non_shortest_tag"], + } { + var got cborx.Decimal + if err := got.UnmarshalCBOR(bytes.NewReader(raw)); err == nil { + t.Errorf("%s: expected rejection", name) + } + } +} + +func TestDeterminismRejects_Hash32(t *testing.T) { + for name, raw := range map[string][]byte{ + "hash32_indefinite": determinismRejects["hash32_indefinite"], + "hash32_non_shortest_len": determinismRejects["hash32_non_shortest_len"], + } { + var got cborx.Hash32 + if err := got.UnmarshalCBOR(bytes.NewReader(raw)); err == nil { + t.Errorf("%s: expected rejection", name) + } + } +} + +func TestDeterminismRejects_Addr20(t *testing.T) { + var got cborx.Addr20 + if err := got.UnmarshalCBOR(bytes.NewReader(determinismRejects["addr20_non_shortest_len"])); err == nil { + t.Errorf("addr20_non_shortest_len: expected rejection") + } +} diff --git a/pkg/cborx/time.go b/pkg/cborx/time.go new file mode 100644 index 0000000..a9ed7e2 --- /dev/null +++ b/pkg/cborx/time.go @@ -0,0 +1,82 @@ +package cborx + +import ( + "fmt" + "io" + "math" + "time" + + cbg "github.com/whyrusleeping/cbor-gen" // layer-guard: allow +) + +// Time is a CBOR adapter for time.Time. Encoding is Unix nanoseconds +// as a signed CBOR integer: +// +// - positive (or zero) → major type 0 with shortest-form width. +// - negative (pre-epoch) → major type 1 with shortest-form width. +// +// This is the same encoding used by cbor-gen's CborTime helper, chosen +// over RFC 8949 tag 0/1 because: +// +// - Our timestamps are already Unix-normalized (no sub-second-range +// tricks, no text form). +// - Positional struct-as-array codegen is friendlier to bare integers +// than to tagged wrappers. +// - int64 nanoseconds give a range of ±292 years around 1970 +// (roughly 1678-09-21..2262-04-11), which comfortably covers every +// network timestamp. Times outside that range fail closed rather than +// relying on time.Time.UnixNano(), which silently overflows. +type Time struct { + V time.Time +} + +var ( + minUnixNanoTime = time.Unix(0, math.MinInt64).UTC() + maxUnixNanoTime = time.Unix(0, math.MaxInt64).UTC() +) + +// MarshalCBOR writes the Unix-nanosecond integer. +func (t Time) MarshalCBOR(w io.Writer) error { + if t.V.Before(minUnixNanoTime) || t.V.After(maxUnixNanoTime) { + return fmt.Errorf("cborx: Time: %s outside int64 Unix-nanosecond range", t.V) + } + nsecs := t.V.UnixNano() + if nsecs >= 0 { + if err := cbg.WriteMajorTypeHeader(w, cbg.MajUnsignedInt, uint64(nsecs)); err != nil { + return fmt.Errorf("cborx: Time: write positive: %w", err) + } + return nil + } + if err := cbg.WriteMajorTypeHeader(w, cbg.MajNegativeInt, uint64(-nsecs)-1); err != nil { + return fmt.Errorf("cborx: Time: write negative: %w", err) + } + return nil +} + +// UnmarshalCBOR decodes a shortest-form signed int as Unix nanoseconds. +// A major-type mismatch (e.g. float, tagged value, byte string) is a +// hard reject — ADR-009 §3 forbids float-encoded timestamps anywhere +// in the protocol. +func (t *Time) UnmarshalCBOR(r io.Reader) error { + maj, val, err := cbg.CborReadHeader(r) + if err != nil { + return fmt.Errorf("cborx: Time: read header: %w", err) + } + var nsecs int64 + switch maj { + case cbg.MajUnsignedInt: + if val > math.MaxInt64 { + return fmt.Errorf("cborx: Time: positive ns %d overflows int64", val) + } + nsecs = int64(val) + case cbg.MajNegativeInt: + if val > math.MaxInt64 { + return fmt.Errorf("cborx: Time: negative ns overflows int64") + } + nsecs = -int64(val) - 1 + default: + return fmt.Errorf("cborx: Time: expected integer, got major %d", maj) + } + t.V = time.Unix(0, nsecs).UTC() + return nil +} diff --git a/pkg/cborx/time_test.go b/pkg/cborx/time_test.go new file mode 100644 index 0000000..42db89c --- /dev/null +++ b/pkg/cborx/time_test.go @@ -0,0 +1,92 @@ +package cborx_test + +import ( + "bytes" + "encoding/hex" + "math" + "testing" + "time" + + "github.com/layer-3/clearnet-sdk/pkg/cborx" +) + +func TestTime_ZeroIsPositiveIntZero(t *testing.T) { + tm := cborx.Time{V: time.Unix(0, 0).UTC()} + var buf bytes.Buffer + if err := tm.MarshalCBOR(&buf); err != nil { + t.Fatalf("marshal: %v", err) + } + if hex.EncodeToString(buf.Bytes()) != "00" { + t.Fatalf("zero time = %x, want 00", buf.Bytes()) + } +} + +func TestTime_RoundTrip(t *testing.T) { + cases := []time.Time{ + time.Unix(0, 0).UTC(), // epoch + time.Unix(1_700_000_000, 123_456_789).UTC(), // ~2023 + time.Unix(0, -1).UTC(), // 1ns before epoch + time.Unix(-1_000_000, 0).UTC(), // pre-1970 + time.Unix(7_000_000_000, 999_999_999).UTC(), // year ~2191 (within int64 ns) + time.Unix(-7_000_000_000, -999_999_999).UTC(), // year ~1748 + time.Unix(0, math.MaxInt64).UTC(), // highest representable Unix ns + time.Unix(0, math.MinInt64).UTC(), // lowest representable Unix ns + } + for _, tm := range cases { + var buf bytes.Buffer + if err := (cborx.Time{V: tm}).MarshalCBOR(&buf); err != nil { + t.Fatalf("marshal %s: %v", tm, err) + } + + var got cborx.Time + if err := got.UnmarshalCBOR(bytes.NewReader(buf.Bytes())); err != nil { + t.Fatalf("unmarshal %s: %v", tm, err) + } + if !got.V.Equal(tm) { + t.Errorf("round-trip %s: got %s", tm, got.V) + } + + // Idempotence. + var buf2 bytes.Buffer + if err := got.MarshalCBOR(&buf2); err != nil { + t.Fatalf("re-marshal: %v", err) + } + if !bytes.Equal(buf.Bytes(), buf2.Bytes()) { + t.Errorf("idempotence %s: %x vs %x", tm, buf.Bytes(), buf2.Bytes()) + } + } +} + +func TestTime_RejectMarshalOutsideUnixNanoRange(t *testing.T) { + cases := map[string]time.Time{ + "before_min": time.Unix(0, math.MinInt64).Add(-time.Nanosecond).UTC(), + "after_max": time.Unix(0, math.MaxInt64).Add(time.Nanosecond).UTC(), + } + for name, tm := range cases { + var buf bytes.Buffer + if err := (cborx.Time{V: tm}).MarshalCBOR(&buf); err == nil { + t.Fatalf("%s: expected marshal rejection for %s", name, tm) + } + } +} + +func TestTime_RejectFloat(t *testing.T) { + // half-precision 1.0 = 0xf9 0x3c 0x00 + raw := []byte{0xf9, 0x3c, 0x00} + var got cborx.Time + if err := got.UnmarshalCBOR(bytes.NewReader(raw)); err == nil { + t.Fatalf("expected rejection of float") + } +} + +func TestTime_RejectTag(t *testing.T) { + // tag 0 (text date-time) wrapping "1970-01-01T00:00:00Z" — RFC 8949 + // allows this, but the cborx.Time adapter is bare-int only per + // ADR-009 §3 and docs/specs/cbor.md §2. + raw := []byte{0xc0, 0x74, '1', '9', '7', '0', '-', '0', '1', '-', '0', '1', + 'T', '0', '0', ':', '0', '0', ':', '0', '0', 'Z'} + var got cborx.Time + if err := got.UnmarshalCBOR(bytes.NewReader(raw)); err == nil { + t.Fatalf("expected rejection of tag-0 time string") + } +} diff --git a/pkg/cborx/version.go b/pkg/cborx/version.go new file mode 100644 index 0000000..c6e377a --- /dev/null +++ b/pkg/cborx/version.go @@ -0,0 +1,38 @@ +package cborx + +import "errors" + +// Version is the one-byte schema-family prefix carried by every CBOR +// frame on the wire or in a BLOB. ADR-009 §5. +// +// Allocation: +// +// 0x00 reserved — never written; rejected on decode. +// 0x01 V1 — the initial CBOR migration shipped by this repo. +// 0x02..0x7F reserved for future CBOR schema-family versions. +// 0x80..0xFF reserved for future non-CBOR codecs (e.g. a later +// migration away from CBOR). +// +// A wire-incompatible change bumps the byte globally; there is no +// per-codec version and no backwards-compatible reader. +type Version uint8 + +const ( + // V1 is the CBOR schema-family version shipped by ADR-009. + V1 Version = 0x01 + + // VersionReserved is the guard value; readers reject it explicitly + // so a zero-filled buffer can never be mistaken for a valid frame. + VersionReserved Version = 0x00 +) + +// ErrReservedVersion reports a 0x00 version byte on decode. +var ErrReservedVersion = errors.New("cborx: reserved version byte 0x00") + +// ErrUnsupportedVersion reports a version byte above V1 (or otherwise +// unknown to this binary). Callers can distinguish version-envelope +// errors from payload errors by unwrapping against this sentinel. +var ErrUnsupportedVersion = errors.New("cborx: unsupported schema version") + +// ErrEmptyEnvelope reports that the version byte could not be read. +var ErrEmptyEnvelope = errors.New("cborx: empty envelope") diff --git a/pkg/core/block.go b/pkg/core/block.go new file mode 100644 index 0000000..5f4f7f6 --- /dev/null +++ b/pkg/core/block.go @@ -0,0 +1,395 @@ +package core + +import ( + "errors" + "bytes" + "fmt" + "math/big" + "sort" + + "github.com/ethereum/go-ethereum/crypto" +) + +// BlockHeader is the signing-preimage projection of a sealed Block +// (docs/specs/protocol/data-structures.md §5.3). It pulls out the fields the +// BLS signature actually commits to — the six-tuple that becomes the +// canonical CBOR preimage now that Wave 2b has rewritten +// `Block.SigningMessage()` around the cbor-gen codec of this struct. +// +// `Accounts` is newly included in the preimage (Q6 in the CBOR migration +// plan / ADR-009 §4) so per-account chain continuity (PrevBlockRef / +// PostNonce) commits under the BLS signature — closing the latent forgery +// surface where the legacy 106-byte preimage did not bind chain-continuity +// metadata. `K` is `uint64` on the Go side for cbor-gen compatibility +// (W2-pre widen); the semantic bound is still 256 (DQE cap). +// +// Field order on this struct IS the wire encoding (cbor-gen tuple mode, +// ADR-009 §2). Append-only evolution: any insert/reorder/rename bumps the +// global schema-family version byte (ADR-009 §5). +type BlockHeader struct { + Anchor [32]byte // AccountID of the first transaction (DHT center) + SealedAt int64 // Unix timestamp when block was sealed + StateRoot [32]byte // Flat SMT root after applying ALL entries in order + EntriesDigest [32]byte // Flat hash binding entries to BLS signature (§2.2) + K uint64 // DQE-computed cluster size; semantic bound 256 + Accounts []AccountSnapshot // Per-account chain continuity metadata (Q6) +} + +// Block is an ordered batch of operations from accounts within a DHT neighborhood, +// attested by a single BLS threshold signature (ADR-005 §2). +type Block struct { + // --- Header (covered by BLS signature) --- + Anchor [32]byte `json:"Anchor"` // AccountID of the first transaction (DHT center) + SealedAt int64 `json:"SealedAt"` // Unix timestamp when block was sealed + Entries []BlockEntry `json:"Entries"` // Ordered operations (canonical sort, §2.3) + StateRoot [32]byte `json:"StateRoot"` // Flat SMT root after applying ALL entries in order + EntriesDigest [32]byte `json:"EntriesDigest"` // Flat hash binding entries to BLS signature (§2.2) + K uint64 `json:"K"` // DQE-computed cluster size for aggregate VaR (§5.2). Semantic bound of 256; wire type widened to uint64 for cbor-gen compatibility. + Attestation Attestation `json:"Attestation"` // Single BLS threshold signature for the block (un-embedded so cbor-gen tuple emitter produces stable output) + Accounts []AccountSnapshot `json:"Accounts"` // Per-account chain continuity metadata + EventBloom [BloomByteLength]byte `json:"event_bloom,omitempty"` // Per-block event bloom filter (data_layer.md §5.2) + AggregateVaR *big.Int `json:"aggregate_var,omitempty"` // Advisory VaR projection; validators recompute (§6.6) + SealReason string `json:"seal_reason,omitempty"` // "idle" | "window" | "entries" | "value" +} + +// BlockEntry is an individual operation within a Block (ADR-005 §2). +type BlockEntry struct { + Type EntryType `json:"Type"` // Transfer | Swap | Withdrawal | Mint | Burn | Repeg (LP ops use Transfer per §8.3) + Account string `json:"Account"` // Account whose state is mutated + Nonce uint64 `json:"Nonce"` // Account nonce consumed by this entry + Payload []byte `json:"Payload"` // Canonical CBOR of the typed op (§5.3) +} + +// AccountSnapshot preserves per-account chain continuity within a Block (ADR-005 §2.4). +type AccountSnapshot struct { + AccountID [32]byte `json:"AccountID"` // keccak256(Address) + PrevBlockRef [32]byte `json:"PrevBlockRef"` // Account.LastBlockHash before this block (back-pointer) + PostNonce uint64 `json:"PostNonce"` // Account.Nonce after all entries for this account +} + +// BlockState represents the lifecycle phase of a pending block (ADR-005 §4.4). +type BlockState uint8 + +const ( + BlockOpen BlockState = iota // Proposer received first tx, block initialized + BlockFilling // Accepting additional transactions + BlockSealed // Sealed, BLS signing complete +) + +// SigningMessage returns the canonical CBOR preimage covered by the BLS +// signature (ADR-009 §4, CBOR migration plan §7 Wave 2b). +// +// Preimage shape: canonical CBOR of `BlockHeader{Anchor, SealedAt, +// StateRoot, EntriesDigest, K, Accounts}`. `Accounts` (per-account +// `PrevBlockRef` + `PostNonce`) is newly bound under the signature (Q6). +// +// Byte output is frozen by goldens under `testdata/goldens/preimages/` +// (see `scripts/ci/check-preimage-goldens.sh`); any change to the field +// set or encoding rules trips the CI guard and requires a version-byte +// bump per ADR-009 §5. +func (b *Block) SigningMessage() []byte { + header := BlockHeader{ + Anchor: b.Anchor, + SealedAt: b.SealedAt, + StateRoot: b.StateRoot, + EntriesDigest: b.EntriesDigest, + K: b.K, + Accounts: b.Accounts, + } + var buf bytes.Buffer + if err := header.MarshalCBOR(&buf); err != nil { + // BlockHeader's generated CBOR codec writes to a bytes.Buffer, + // which cannot fail; a panic here means a programmer error + // (e.g. a field type the codec cannot serialize was introduced). + panic(fmt.Errorf("core: BlockHeader.MarshalCBOR: %w", err)) + } + return buf.Bytes() +} + +// Hash returns keccak256(SigningMessage) — the block identifier (ADR-005 §2.1). +// After sealing, Account.LastBlockHash = Hash() for each account in Accounts. +// +// Formula unchanged under Wave 2b; only the preimage layout moved from +// the 106-byte hand-rolled concat to canonical CBOR of `BlockHeader`. +func (b *Block) Hash() [32]byte { + return crypto.Keccak256Hash(b.SigningMessage()) +} + +// ComputeEntriesDigest computes the flat hash over all entry hashes (ADR-005 §2.2). +// +// EntriesDigest = keccak256(EntryHash_1 || EntryHash_2 || ... || EntryHash_n) +func ComputeEntriesDigest(entries []BlockEntry) [32]byte { + if len(entries) == 0 { + return [32]byte{} + } + buf := make([]byte, 0, len(entries)*32) + for _, e := range entries { + h := ComputeEntryHash(e) + buf = append(buf, h[:]...) + } + return crypto.Keccak256Hash(buf) +} + +// ComputeEntryHash computes the hash of a single entry (ADR-005 §2.2). +// +// EntryHash = keccak256(canonical-CBOR(BlockEntry)) +// +// The preimage is the cbor-gen tuple encoding of the entry struct +// (ADR-009 §4, CBOR migration plan §7 Wave 2b). `EntriesDigest` +// continues to be the flat keccak-concat of these per-entry hashes — +// only the per-entry preimage layout changed in Wave 2b. +func ComputeEntryHash(e BlockEntry) [32]byte { + var buf bytes.Buffer + if err := e.MarshalCBOR(&buf); err != nil { + // See SigningMessage: a bytes.Buffer write cannot fail, so a + // non-nil error here is a structural regression. + panic(fmt.Errorf("core: BlockEntry.MarshalCBOR: %w", err)) + } + return crypto.Keccak256Hash(buf.Bytes()) +} + +// CanonicalSort sorts entries in deterministic order (ADR-005 §2.3, +// data-structures.md §5.3.3). +// Primary: AccountID (= keccak256(URI)) by XOR distance from Anchor, ascending. +// Secondary: Nonce ascending within the same account. +func CanonicalSort(anchor [32]byte, entries []BlockEntry) { + sort.SliceStable(entries, func(i, j int) bool { + ai := ComputeAccountID(entries[i].Account) + aj := ComputeAccountID(entries[j].Account) + + // XOR distance from anchor + for k := 0; k < 32; k++ { + di := ai[k] ^ anchor[k] + dj := aj[k] ^ anchor[k] + if di != dj { + return di < dj + } + } + // Same account — sort by nonce ascending + return entries[i].Nonce < entries[j].Nonce + }) +} + +// ValidateEntryOrder checks that entries are in canonical order (ADR-005 §2.3). +// Returns nil if ordering is valid. +func ValidateEntryOrder(anchor [32]byte, entries []BlockEntry) error { + for i := 1; i < len(entries); i++ { + ai := ComputeAccountID(entries[i-1].Account) + aj := ComputeAccountID(entries[i].Account) + + for k := 0; k < 32; k++ { + di := ai[k] ^ anchor[k] + dj := aj[k] ^ anchor[k] + if di < dj { + break // correct order + } + if di > dj { + return ErrNonCanonicalOrder + } + // equal byte — continue to next byte + if k == 31 { + // Same account — nonces must be strictly consecutive (ADR-005 §6). + if entries[i-1].Nonce+1 != entries[i].Nonce { + return ErrNonConsecutiveNonce + } + } + } + } + return nil +} + +// ValidateEntriesDigest recomputes EntriesDigest and checks it matches the block header. +func (b *Block) ValidateEntriesDigest() error { + computed := ComputeEntriesDigest(b.Entries) + if computed != b.EntriesDigest { + return ErrEntriesDigestMismatch + } + return nil +} + +// --------------------------------------------------------------------------- +// BlockEntry convenience methods +// --------------------------------------------------------------------------- + +// Hash returns keccak256(Type || Account || Nonce || Payload) for this entry. +func (e *BlockEntry) Hash() [32]byte { + return ComputeEntryHash(*e) +} + +// DecodeOp decodes the entry payload into its typed operation struct. +// Returns one of *TransferOp, *SwapOp, *WithdrawalOp, *RepegOp, +// *SessionCloseOp, or *SessionChallengeOp. +func (e *BlockEntry) DecodeOp() (interface{}, error) { + return DecodePayload(*e) +} + +// DecodeTransferOp decodes the payload as a TransferOp. +// Returns an error if the entry type is not OpTransfer or the payload is malformed. +func (e *BlockEntry) DecodeTransferOp() (*TransferOp, error) { + if e.Type != OpTransfer { + return nil, fmt.Errorf("DecodeTransferOp: entry type is %d, want %d (OpTransfer)", e.Type, OpTransfer) + } + op := &TransferOp{} + if err := op.Decode(e.Payload); err != nil { + return nil, err + } + return op, nil +} + +// DecodeSwapOp decodes the payload as a SwapOp. +// Returns an error if the entry type is not OpSwap or the payload is malformed. +func (e *BlockEntry) DecodeSwapOp() (*SwapOp, error) { + if e.Type != OpSwap { + return nil, fmt.Errorf("DecodeSwapOp: entry type is %d, want %d (OpSwap)", e.Type, OpSwap) + } + op := &SwapOp{} + if err := op.Decode(e.Payload); err != nil { + return nil, err + } + return op, nil +} + +// DecodeWithdrawalOp decodes the payload as a WithdrawalOp. +// Returns an error if the entry type is not OpWithdrawal or the payload is malformed. +func (e *BlockEntry) DecodeWithdrawalOp() (*WithdrawalOp, error) { + if e.Type != OpWithdrawal { + return nil, fmt.Errorf("DecodeWithdrawalOp: entry type is %d, want %d (OpWithdrawal)", e.Type, OpWithdrawal) + } + op := &WithdrawalOp{} + if err := op.Decode(e.Payload); err != nil { + return nil, err + } + return op, nil +} + +// DecodeBurnReceipt decodes the payload as a BurnReceipt (the on-block +// payload for OpBurn entries — the receipt IS the operation). +func (e *BlockEntry) DecodeBurnReceipt() (*BurnReceipt, error) { + if e.Type != OpBurn { + return nil, fmt.Errorf("DecodeBurnReceipt: entry type is %d, want %d (OpBurn)", e.Type, OpBurn) + } + v := &BurnReceipt{} + if err := unmarshalPayload(e.Payload, v); err != nil { + return nil, err + } + return v, nil +} + +// DecodeMintReceipt decodes the payload as a MintReceipt (the on-block +// payload for OpMint entries — the receipt IS the operation). +func (e *BlockEntry) DecodeMintReceipt() (*MintReceipt, error) { + if e.Type != OpMint { + return nil, fmt.Errorf("DecodeMintReceipt: entry type is %d, want %d (OpMint)", e.Type, OpMint) + } + v := &MintReceipt{} + if err := unmarshalPayload(e.Payload, v); err != nil { + return nil, err + } + return v, nil +} + +// DecodeRepegOp decodes the payload as a RepegOp. +// Returns an error if the entry type is not OpRepeg or the payload is malformed. +func (e *BlockEntry) DecodeRepegOp() (*RepegOp, error) { + if e.Type != OpRepeg { + return nil, fmt.Errorf("DecodeRepegOp: entry type is %d, want %d (OpRepeg)", e.Type, OpRepeg) + } + op := &RepegOp{} + if err := op.Decode(e.Payload); err != nil { + return nil, err + } + return op, nil +} + +// ConsumesNonce reports whether applying this entry advances the account +// nonce. Receipt-driven entries (OpMint deposit credits, OpBurn +// withdrawal-execution burns) leave the account nonce unchanged — the +// authoritative trigger is the custody-signed receipt, not a user-signed +// transaction. Used by block validation to compute the correct pre-block +// nonce from snap.PostNonce when verifying entry order. +func (e *BlockEntry) ConsumesNonce() bool { + return e.Type != OpMint && e.Type != OpBurn +} + +// IsTransfer returns true if the entry type is OpTransfer. +func (e *BlockEntry) IsTransfer() bool { return e.Type == OpTransfer } + +// IsSwap returns true if the entry type is OpSwap. +func (e *BlockEntry) IsSwap() bool { return e.Type == OpSwap } + +// IsWithdrawal returns true if the entry type is OpWithdrawal. +func (e *BlockEntry) IsWithdrawal() bool { return e.Type == OpWithdrawal } + +// AccountID returns keccak256(e.Account). +func (e *BlockEntry) AccountID() [32]byte { + return ComputeAccountID(e.Account) +} + +// Block validation errors (extracted from clearnet block_params.go — used by +// Block entry/digest verification above). +var ( + ErrNonCanonicalOrder = errors.New("block: entries not in canonical order") + ErrNonConsecutiveNonce = errors.New("block: same-account entries must have consecutive nonces") + ErrEntriesDigestMismatch = errors.New("block: entries digest does not match header") +) + +// BlockEntryRef identifies a specific entry within a sealed block. +// This is the composite key used throughout the escrow lifecycle: +// recording, challenge, finalization, and BurnReceipt handling. +type BlockEntryRef struct { + BlockHash [32]byte // keccak256(SigningMessage) of the sealed block + EntryIndex uint64 // Position of the entry in Block.Entries (widened from uint16 for cbor-gen compat, Wave 2 pre) +} + +// String returns a compact hex:index representation for logging. +func (r BlockEntryRef) String() string { + return fmt.Sprintf("%x:%d", r.BlockHash[:8], r.EntryIndex) +} + +// Key returns a full-hex deduplication key for map lookups. +func (r BlockEntryRef) Key() string { + return fmt.Sprintf("%x:%d", r.BlockHash, r.EntryIndex) +} + +// RefFromBlock creates a BlockEntryRef from a sealed block and entry index. +func RefFromBlock(block *Block, entryIndex uint64) BlockEntryRef { + return BlockEntryRef{ + BlockHash: block.Hash(), + EntryIndex: entryIndex, + } +} + +// BurnReceipt is returned by the custody layer after L1 execution of a +// withdrawal (ADR-005 §9.1, receipt-model amendment 2026-05-12). Provider +// ECDSA signatures (k-of-n against the configured custody signer directory +// — manifest custody.signers/threshold; future Registry-backed source) +// confirm the withdrawal was executed on-chain; the cluster validates the +// receipt and applies the second leg of the burn (DR 2010 / CR 1020). +type BurnReceipt struct { + WithdrawalID [32]byte // keccak256(accountId, blockHash, entryIndex, chainId, recipient, asset, amount, nonce) + BlockEntryRef // Block hash + entry index of the escrow entry + L1TxHash [32]byte // Transaction hash on the target chain + Signatures [][]byte // k-of-n provider ECDSA signatures over the receipt digest +} + +// MintReceipt is issued by the custody layer after an L1 deposit confirms +// (ADR-005 §9.1, receipt-model amendment 2026-05-12). Provider ECDSA +// signatures (k-of-n against the configured custody signer directory — +// manifest custody.signers/threshold; future Registry-backed source) attest +// that the deposit landed and reached the configured confirmation depth; +// the cluster validates the receipt and credits the user account +// (DR 1010 / CR 2010). +// +// Idempotency is keyed by (ChainID, L1TxHash, LogIndex) so a re-issued +// receipt cannot produce a second credit. Clearnet does not watch the +// chain — receipts are the only deposit ingress. +type MintReceipt struct { + ChainID uint64 // EIP-155 chain id where the deposit landed + L1TxHash [32]byte // Transaction hash on the source chain + LogIndex uint64 // Log index of the Deposited event within the tx + Account string // Clearnet account URI to credit + Asset string // Asset symbol (matches on-chain symbol) + Amount *big.Int // Deposit amount (base units, must be > 0) + BlockNumber uint64 // L1 block number for diagnostics / receipts + Signatures [][]byte // k-of-n provider ECDSA signatures over the receipt digest +} diff --git a/pkg/core/bloom.go b/pkg/core/bloom.go new file mode 100644 index 0000000..0daebd5 --- /dev/null +++ b/pkg/core/bloom.go @@ -0,0 +1,9 @@ +package core + +// Event bloom filter for per-block log filtering (data_layer.md §5.2). +// 2048-bit (256-byte) bloom with 3 hash functions, matching Ethereum's log bloom. +// +// Only the length constant lives in the SDK — Block.EventBloom is +// [BloomByteLength]byte. The add/test helpers (BloomAdd, BloomTest, +// BlockEventBloom) stay in clearnet; they operate on clearnet-internal Events. +const BloomByteLength = 256 diff --git a/pkg/core/cbor_gen.go b/pkg/core/cbor_gen.go new file mode 100644 index 0000000..9663adf --- /dev/null +++ b/pkg/core/cbor_gen.go @@ -0,0 +1,3386 @@ +// Code generated by github.com/whyrusleeping/cbor-gen. DO NOT EDIT. + +package core + +import ( + "fmt" + "io" + "math" + "sort" + + cid "github.com/ipfs/go-cid" + cbg "github.com/whyrusleeping/cbor-gen" + xerrors "golang.org/x/xerrors" + big "math/big" +) + +var _ = xerrors.Errorf +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort + +var lengthBufBlockHeader = []byte{134} + +func (t *BlockHeader) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write(lengthBufBlockHeader); err != nil { + return err + } + + // t.Anchor ([32]uint8) (array) + if len(t.Anchor) > 2097152 { + return xerrors.Errorf("Byte array in field t.Anchor was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.Anchor))); err != nil { + return err + } + + if _, err := cw.Write(t.Anchor[:]); err != nil { + return err + } + + // t.SealedAt (int64) (int64) + if t.SealedAt >= 0 { + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.SealedAt)); err != nil { + return err + } + } else { + if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-t.SealedAt-1)); err != nil { + return err + } + } + + // t.StateRoot ([32]uint8) (array) + if len(t.StateRoot) > 2097152 { + return xerrors.Errorf("Byte array in field t.StateRoot was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.StateRoot))); err != nil { + return err + } + + if _, err := cw.Write(t.StateRoot[:]); err != nil { + return err + } + + // t.EntriesDigest ([32]uint8) (array) + if len(t.EntriesDigest) > 2097152 { + return xerrors.Errorf("Byte array in field t.EntriesDigest was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.EntriesDigest))); err != nil { + return err + } + + if _, err := cw.Write(t.EntriesDigest[:]); err != nil { + return err + } + + // t.K (uint64) (uint64) + + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.K)); err != nil { + return err + } + + // t.Accounts ([]core.AccountSnapshot) (slice) + if len(t.Accounts) > 8192 { + return xerrors.Errorf("Slice value in field t.Accounts was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Accounts))); err != nil { + return err + } + for _, v := range t.Accounts { + if err := v.MarshalCBOR(cw); err != nil { + return err + } + + } + return nil +} + +func (t *BlockHeader) UnmarshalCBOR(r io.Reader) (err error) { + *t = BlockHeader{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajArray { + return fmt.Errorf("cbor input should be of type array") + } + + if extra != 6 { + return fmt.Errorf("cbor input had wrong number of fields") + } + + // t.Anchor ([32]uint8) (array) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.Anchor: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + if extra != 32 { + return fmt.Errorf("expected array to have 32 elements") + } + + t.Anchor = [32]uint8{} + if _, err := io.ReadFull(cr, t.Anchor[:]); err != nil { + return err + } + // t.SealedAt (int64) (int64) + { + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + var extraI int64 + switch maj { + case cbg.MajUnsignedInt: + extraI = int64(extra) + if extraI < 0 { + return fmt.Errorf("int64 positive overflow") + } + case cbg.MajNegativeInt: + extraI = int64(extra) + if extraI < 0 { + return fmt.Errorf("int64 negative overflow") + } + extraI = -1 - extraI + default: + return fmt.Errorf("wrong type for int64 field: %d", maj) + } + + t.SealedAt = int64(extraI) + } + // t.StateRoot ([32]uint8) (array) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.StateRoot: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + if extra != 32 { + return fmt.Errorf("expected array to have 32 elements") + } + + t.StateRoot = [32]uint8{} + if _, err := io.ReadFull(cr, t.StateRoot[:]); err != nil { + return err + } + // t.EntriesDigest ([32]uint8) (array) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.EntriesDigest: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + if extra != 32 { + return fmt.Errorf("expected array to have 32 elements") + } + + t.EntriesDigest = [32]uint8{} + if _, err := io.ReadFull(cr, t.EntriesDigest[:]); err != nil { + return err + } + // t.K (uint64) (uint64) + + { + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajUnsignedInt { + return fmt.Errorf("wrong type for uint64 field") + } + t.K = uint64(extra) + + } + // t.Accounts ([]core.AccountSnapshot) (slice) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Accounts: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Accounts = make([]AccountSnapshot, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + + if err := t.Accounts[i].UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Accounts[i]: %w", err) + } + + } + + } + } + return nil +} + +var lengthBufBlock = []byte{139} + +func (t *Block) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write(lengthBufBlock); err != nil { + return err + } + + // t.Anchor ([32]uint8) (array) + if len(t.Anchor) > 2097152 { + return xerrors.Errorf("Byte array in field t.Anchor was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.Anchor))); err != nil { + return err + } + + if _, err := cw.Write(t.Anchor[:]); err != nil { + return err + } + + // t.SealedAt (int64) (int64) + if t.SealedAt >= 0 { + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.SealedAt)); err != nil { + return err + } + } else { + if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-t.SealedAt-1)); err != nil { + return err + } + } + + // t.Entries ([]core.BlockEntry) (slice) + if len(t.Entries) > 8192 { + return xerrors.Errorf("Slice value in field t.Entries was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Entries))); err != nil { + return err + } + for _, v := range t.Entries { + if err := v.MarshalCBOR(cw); err != nil { + return err + } + + } + + // t.StateRoot ([32]uint8) (array) + if len(t.StateRoot) > 2097152 { + return xerrors.Errorf("Byte array in field t.StateRoot was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.StateRoot))); err != nil { + return err + } + + if _, err := cw.Write(t.StateRoot[:]); err != nil { + return err + } + + // t.EntriesDigest ([32]uint8) (array) + if len(t.EntriesDigest) > 2097152 { + return xerrors.Errorf("Byte array in field t.EntriesDigest was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.EntriesDigest))); err != nil { + return err + } + + if _, err := cw.Write(t.EntriesDigest[:]); err != nil { + return err + } + + // t.K (uint64) (uint64) + + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.K)); err != nil { + return err + } + + // t.Attestation (core.Attestation) (struct) + if err := t.Attestation.MarshalCBOR(cw); err != nil { + return err + } + + // t.Accounts ([]core.AccountSnapshot) (slice) + if len(t.Accounts) > 8192 { + return xerrors.Errorf("Slice value in field t.Accounts was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Accounts))); err != nil { + return err + } + for _, v := range t.Accounts { + if err := v.MarshalCBOR(cw); err != nil { + return err + } + + } + + // t.EventBloom ([256]uint8) (array) + if len(t.EventBloom) > 2097152 { + return xerrors.Errorf("Byte array in field t.EventBloom was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.EventBloom))); err != nil { + return err + } + + if _, err := cw.Write(t.EventBloom[:]); err != nil { + return err + } + + // t.AggregateVaR (big.Int) (struct) + { + if t.AggregateVaR != nil && t.AggregateVaR.Sign() < 0 { + return xerrors.Errorf("Value in field t.AggregateVaR was a negative big-integer (not supported)") + } + if err := cw.CborWriteHeader(cbg.MajTag, 2); err != nil { + return err + } + var b []byte + if t.AggregateVaR != nil { + b = t.AggregateVaR.Bytes() + } + + if err := cw.CborWriteHeader(cbg.MajByteString, uint64(len(b))); err != nil { + return err + } + if _, err := cw.Write(b); err != nil { + return err + } + } + + // t.SealReason (string) (string) + if len(t.SealReason) > 8192 { + return xerrors.Errorf("Value in field t.SealReason was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.SealReason))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.SealReason)); err != nil { + return err + } + return nil +} + +func (t *Block) UnmarshalCBOR(r io.Reader) (err error) { + *t = Block{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajArray { + return fmt.Errorf("cbor input should be of type array") + } + + if extra != 11 { + return fmt.Errorf("cbor input had wrong number of fields") + } + + // t.Anchor ([32]uint8) (array) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.Anchor: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + if extra != 32 { + return fmt.Errorf("expected array to have 32 elements") + } + + t.Anchor = [32]uint8{} + if _, err := io.ReadFull(cr, t.Anchor[:]); err != nil { + return err + } + // t.SealedAt (int64) (int64) + { + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + var extraI int64 + switch maj { + case cbg.MajUnsignedInt: + extraI = int64(extra) + if extraI < 0 { + return fmt.Errorf("int64 positive overflow") + } + case cbg.MajNegativeInt: + extraI = int64(extra) + if extraI < 0 { + return fmt.Errorf("int64 negative overflow") + } + extraI = -1 - extraI + default: + return fmt.Errorf("wrong type for int64 field: %d", maj) + } + + t.SealedAt = int64(extraI) + } + // t.Entries ([]core.BlockEntry) (slice) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Entries: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Entries = make([]BlockEntry, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + + if err := t.Entries[i].UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Entries[i]: %w", err) + } + + } + + } + } + // t.StateRoot ([32]uint8) (array) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.StateRoot: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + if extra != 32 { + return fmt.Errorf("expected array to have 32 elements") + } + + t.StateRoot = [32]uint8{} + if _, err := io.ReadFull(cr, t.StateRoot[:]); err != nil { + return err + } + // t.EntriesDigest ([32]uint8) (array) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.EntriesDigest: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + if extra != 32 { + return fmt.Errorf("expected array to have 32 elements") + } + + t.EntriesDigest = [32]uint8{} + if _, err := io.ReadFull(cr, t.EntriesDigest[:]); err != nil { + return err + } + // t.K (uint64) (uint64) + + { + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajUnsignedInt { + return fmt.Errorf("wrong type for uint64 field") + } + t.K = uint64(extra) + + } + // t.Attestation (core.Attestation) (struct) + + { + + if err := t.Attestation.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Attestation: %w", err) + } + + } + // t.Accounts ([]core.AccountSnapshot) (slice) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Accounts: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Accounts = make([]AccountSnapshot, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + + if err := t.Accounts[i].UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Accounts[i]: %w", err) + } + + } + + } + } + // t.EventBloom ([256]uint8) (array) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.EventBloom: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + if extra != 256 { + return fmt.Errorf("expected array to have 256 elements") + } + + t.EventBloom = [256]uint8{} + if _, err := io.ReadFull(cr, t.EventBloom[:]); err != nil { + return err + } + // t.AggregateVaR (big.Int) (struct) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if maj != cbg.MajTag || extra != 2 { + return fmt.Errorf("big ints should be cbor bignums") + } + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if maj != cbg.MajByteString { + return fmt.Errorf("big ints should be tagged cbor byte strings") + } + + if extra > 256 { + return fmt.Errorf("t.AggregateVaR: cbor bignum was too large") + } + + if extra > 0 { + buf := make([]byte, extra) + if _, err := io.ReadFull(cr, buf); err != nil { + return err + } + t.AggregateVaR = big.NewInt(0).SetBytes(buf) + } else { + t.AggregateVaR = big.NewInt(0) + } + // t.SealReason (string) (string) + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.SealReason = string(sval) + } + return nil +} + +var lengthBufBlockEntry = []byte{132} + +func (t *BlockEntry) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write(lengthBufBlockEntry); err != nil { + return err + } + + // t.Type (core.EntryType) (uint8) + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.Type)); err != nil { + return err + } + + // t.Account (string) (string) + if len(t.Account) > 8192 { + return xerrors.Errorf("Value in field t.Account was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Account))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Account)); err != nil { + return err + } + + // t.Nonce (uint64) (uint64) + + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.Nonce)); err != nil { + return err + } + + // t.Payload ([]uint8) (slice) + if len(t.Payload) > 2097152 { + return xerrors.Errorf("Byte array in field t.Payload was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.Payload))); err != nil { + return err + } + + if _, err := cw.Write(t.Payload); err != nil { + return err + } + + return nil +} + +func (t *BlockEntry) UnmarshalCBOR(r io.Reader) (err error) { + *t = BlockEntry{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajArray { + return fmt.Errorf("cbor input should be of type array") + } + + if extra != 4 { + return fmt.Errorf("cbor input had wrong number of fields") + } + + // t.Type (core.EntryType) (uint8) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajUnsignedInt { + return fmt.Errorf("wrong type for uint8 field") + } + if extra > math.MaxUint8 { + return fmt.Errorf("integer in input was too large for uint8 field") + } + t.Type = EntryType(extra) + // t.Account (string) (string) + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.Account = string(sval) + } + // t.Nonce (uint64) (uint64) + + { + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajUnsignedInt { + return fmt.Errorf("wrong type for uint64 field") + } + t.Nonce = uint64(extra) + + } + // t.Payload ([]uint8) (slice) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.Payload: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + + if extra > 0 { + t.Payload = make([]uint8, extra) + } + + if _, err := io.ReadFull(cr, t.Payload); err != nil { + return err + } + + return nil +} + +var lengthBufAccountSnapshot = []byte{131} + +func (t *AccountSnapshot) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write(lengthBufAccountSnapshot); err != nil { + return err + } + + // t.AccountID ([32]uint8) (array) + if len(t.AccountID) > 2097152 { + return xerrors.Errorf("Byte array in field t.AccountID was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.AccountID))); err != nil { + return err + } + + if _, err := cw.Write(t.AccountID[:]); err != nil { + return err + } + + // t.PrevBlockRef ([32]uint8) (array) + if len(t.PrevBlockRef) > 2097152 { + return xerrors.Errorf("Byte array in field t.PrevBlockRef was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.PrevBlockRef))); err != nil { + return err + } + + if _, err := cw.Write(t.PrevBlockRef[:]); err != nil { + return err + } + + // t.PostNonce (uint64) (uint64) + + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.PostNonce)); err != nil { + return err + } + + return nil +} + +func (t *AccountSnapshot) UnmarshalCBOR(r io.Reader) (err error) { + *t = AccountSnapshot{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajArray { + return fmt.Errorf("cbor input should be of type array") + } + + if extra != 3 { + return fmt.Errorf("cbor input had wrong number of fields") + } + + // t.AccountID ([32]uint8) (array) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.AccountID: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + if extra != 32 { + return fmt.Errorf("expected array to have 32 elements") + } + + t.AccountID = [32]uint8{} + if _, err := io.ReadFull(cr, t.AccountID[:]); err != nil { + return err + } + // t.PrevBlockRef ([32]uint8) (array) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.PrevBlockRef: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + if extra != 32 { + return fmt.Errorf("expected array to have 32 elements") + } + + t.PrevBlockRef = [32]uint8{} + if _, err := io.ReadFull(cr, t.PrevBlockRef[:]); err != nil { + return err + } + // t.PostNonce (uint64) (uint64) + + { + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajUnsignedInt { + return fmt.Errorf("wrong type for uint64 field") + } + t.PostNonce = uint64(extra) + + } + return nil +} + +var lengthBufAttestation = []byte{131} + +func (t *Attestation) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write(lengthBufAttestation); err != nil { + return err + } + + // t.ThresholdSig ([]uint8) (slice) + if len(t.ThresholdSig) > 2097152 { + return xerrors.Errorf("Byte array in field t.ThresholdSig was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.ThresholdSig))); err != nil { + return err + } + + if _, err := cw.Write(t.ThresholdSig); err != nil { + return err + } + + // t.Bitmask ([32]uint8) (array) + if len(t.Bitmask) > 2097152 { + return xerrors.Errorf("Byte array in field t.Bitmask was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.Bitmask))); err != nil { + return err + } + + if _, err := cw.Write(t.Bitmask[:]); err != nil { + return err + } + + // t.Validators ([][]uint8) (slice) + if len(t.Validators) > 8192 { + return xerrors.Errorf("Slice value in field t.Validators was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Validators))); err != nil { + return err + } + for _, v := range t.Validators { + if len(v) > 2097152 { + return xerrors.Errorf("Byte array in field v was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(v))); err != nil { + return err + } + + if _, err := cw.Write(v); err != nil { + return err + } + + } + return nil +} + +func (t *Attestation) UnmarshalCBOR(r io.Reader) (err error) { + *t = Attestation{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajArray { + return fmt.Errorf("cbor input should be of type array") + } + + if extra != 3 { + return fmt.Errorf("cbor input had wrong number of fields") + } + + // t.ThresholdSig ([]uint8) (slice) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.ThresholdSig: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + + if extra > 0 { + t.ThresholdSig = make([]uint8, extra) + } + + if _, err := io.ReadFull(cr, t.ThresholdSig); err != nil { + return err + } + + // t.Bitmask ([32]uint8) (array) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.Bitmask: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + if extra != 32 { + return fmt.Errorf("expected array to have 32 elements") + } + + t.Bitmask = [32]uint8{} + if _, err := io.ReadFull(cr, t.Bitmask[:]); err != nil { + return err + } + // t.Validators ([][]uint8) (slice) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Validators: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Validators = make([][]uint8, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.Validators[i]: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + + if extra > 0 { + t.Validators[i] = make([]uint8, extra) + } + + if _, err := io.ReadFull(cr, t.Validators[i]); err != nil { + return err + } + + } + } + return nil +} + +var lengthBufAssetTransfer = []byte{130} + +func (t *AssetTransfer) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write(lengthBufAssetTransfer); err != nil { + return err + } + + // t.Asset (core.AssetID) (string) + if len(t.Asset) > 8192 { + return xerrors.Errorf("Value in field t.Asset was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Asset))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Asset)); err != nil { + return err + } + + // t.Amount (decimal.Decimal) (struct) + if err := t.Amount.MarshalCBOR(cw); err != nil { + return err + } + return nil +} + +func (t *AssetTransfer) UnmarshalCBOR(r io.Reader) (err error) { + *t = AssetTransfer{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajArray { + return fmt.Errorf("cbor input should be of type array") + } + + if extra != 2 { + return fmt.Errorf("cbor input had wrong number of fields") + } + + // t.Asset (core.AssetID) (string) + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.Asset = AssetID(sval) + } + // t.Amount (decimal.Decimal) (struct) + + { + + if err := t.Amount.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Amount: %w", err) + } + + } + return nil +} + +var lengthBufTransferOp = []byte{131} + +func (t *TransferOp) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write(lengthBufTransferOp); err != nil { + return err + } + + // t.TxID (string) (string) + if len(t.TxID) > 8192 { + return xerrors.Errorf("Value in field t.TxID was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.TxID))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.TxID)); err != nil { + return err + } + + // t.To (string) (string) + if len(t.To) > 8192 { + return xerrors.Errorf("Value in field t.To was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.To))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.To)); err != nil { + return err + } + + // t.Assets ([]core.AssetTransfer) (slice) + if len(t.Assets) > 8192 { + return xerrors.Errorf("Slice value in field t.Assets was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Assets))); err != nil { + return err + } + for _, v := range t.Assets { + if err := v.MarshalCBOR(cw); err != nil { + return err + } + + } + return nil +} + +func (t *TransferOp) UnmarshalCBOR(r io.Reader) (err error) { + *t = TransferOp{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajArray { + return fmt.Errorf("cbor input should be of type array") + } + + if extra != 3 { + return fmt.Errorf("cbor input had wrong number of fields") + } + + // t.TxID (string) (string) + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.TxID = string(sval) + } + // t.To (string) (string) + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.To = string(sval) + } + // t.Assets ([]core.AssetTransfer) (slice) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Assets: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Assets = make([]AssetTransfer, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + + if err := t.Assets[i].UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Assets[i]: %w", err) + } + + } + + } + } + return nil +} + +var lengthBufSwapOp = []byte{138} + +func (t *SwapOp) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write(lengthBufSwapOp); err != nil { + return err + } + + // t.TxID (string) (string) + if len(t.TxID) > 8192 { + return xerrors.Errorf("Value in field t.TxID was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.TxID))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.TxID)); err != nil { + return err + } + + // t.AssetIn (core.AssetID) (string) + if len(t.AssetIn) > 8192 { + return xerrors.Errorf("Value in field t.AssetIn was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.AssetIn))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.AssetIn)); err != nil { + return err + } + + // t.AssetOut (core.AssetID) (string) + if len(t.AssetOut) > 8192 { + return xerrors.Errorf("Value in field t.AssetOut was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.AssetOut))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.AssetOut)); err != nil { + return err + } + + // t.AmountIn (decimal.Decimal) (struct) + if err := t.AmountIn.MarshalCBOR(cw); err != nil { + return err + } + + // t.AmountOut (decimal.Decimal) (struct) + if err := t.AmountOut.MarshalCBOR(cw); err != nil { + return err + } + + // t.PoolID (string) (string) + if len(t.PoolID) > 8192 { + return xerrors.Errorf("Value in field t.PoolID was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.PoolID))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.PoolID)); err != nil { + return err + } + + // t.Fee (decimal.Decimal) (struct) + if err := t.Fee.MarshalCBOR(cw); err != nil { + return err + } + + // t.FeeRate (uint64) (uint64) + + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.FeeRate)); err != nil { + return err + } + + // t.PriceEMA (decimal.Decimal) (struct) + if err := t.PriceEMA.MarshalCBOR(cw); err != nil { + return err + } + + // t.SpotPrice (decimal.Decimal) (struct) + if err := t.SpotPrice.MarshalCBOR(cw); err != nil { + return err + } + return nil +} + +func (t *SwapOp) UnmarshalCBOR(r io.Reader) (err error) { + *t = SwapOp{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajArray { + return fmt.Errorf("cbor input should be of type array") + } + + if extra != 10 { + return fmt.Errorf("cbor input had wrong number of fields") + } + + // t.TxID (string) (string) + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.TxID = string(sval) + } + // t.AssetIn (core.AssetID) (string) + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.AssetIn = AssetID(sval) + } + // t.AssetOut (core.AssetID) (string) + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.AssetOut = AssetID(sval) + } + // t.AmountIn (decimal.Decimal) (struct) + + { + + if err := t.AmountIn.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.AmountIn: %w", err) + } + + } + // t.AmountOut (decimal.Decimal) (struct) + + { + + if err := t.AmountOut.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.AmountOut: %w", err) + } + + } + // t.PoolID (string) (string) + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.PoolID = string(sval) + } + // t.Fee (decimal.Decimal) (struct) + + { + + if err := t.Fee.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Fee: %w", err) + } + + } + // t.FeeRate (uint64) (uint64) + + { + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajUnsignedInt { + return fmt.Errorf("wrong type for uint64 field") + } + t.FeeRate = uint64(extra) + + } + // t.PriceEMA (decimal.Decimal) (struct) + + { + + if err := t.PriceEMA.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.PriceEMA: %w", err) + } + + } + // t.SpotPrice (decimal.Decimal) (struct) + + { + + if err := t.SpotPrice.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.SpotPrice: %w", err) + } + + } + return nil +} + +var lengthBufWithdrawalOp = []byte{134} + +func (t *WithdrawalOp) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write(lengthBufWithdrawalOp); err != nil { + return err + } + + // t.Asset (core.AssetID) (string) + if len(t.Asset) > 8192 { + return xerrors.Errorf("Value in field t.Asset was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Asset))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Asset)); err != nil { + return err + } + + // t.L1Asset (string) (string) + if len(t.L1Asset) > 8192 { + return xerrors.Errorf("Value in field t.L1Asset was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.L1Asset))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.L1Asset)); err != nil { + return err + } + + // t.Amount (decimal.Decimal) (struct) + if err := t.Amount.MarshalCBOR(cw); err != nil { + return err + } + + // t.ChainID (uint64) (uint64) + + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.ChainID)); err != nil { + return err + } + + // t.Recipient (string) (string) + if len(t.Recipient) > 8192 { + return xerrors.Errorf("Value in field t.Recipient was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Recipient))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Recipient)); err != nil { + return err + } + + // t.UserSignature ([]uint8) (slice) + if len(t.UserSignature) > 2097152 { + return xerrors.Errorf("Byte array in field t.UserSignature was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.UserSignature))); err != nil { + return err + } + + if _, err := cw.Write(t.UserSignature); err != nil { + return err + } + + return nil +} + +func (t *WithdrawalOp) UnmarshalCBOR(r io.Reader) (err error) { + *t = WithdrawalOp{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajArray { + return fmt.Errorf("cbor input should be of type array") + } + + if extra != 6 { + return fmt.Errorf("cbor input had wrong number of fields") + } + + // t.Asset (core.AssetID) (string) + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.Asset = AssetID(sval) + } + // t.L1Asset (string) (string) + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.L1Asset = string(sval) + } + // t.Amount (decimal.Decimal) (struct) + + { + + if err := t.Amount.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Amount: %w", err) + } + + } + // t.ChainID (uint64) (uint64) + + { + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajUnsignedInt { + return fmt.Errorf("wrong type for uint64 field") + } + t.ChainID = uint64(extra) + + } + // t.Recipient (string) (string) + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.Recipient = string(sval) + } + // t.UserSignature ([]uint8) (slice) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.UserSignature: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + + if extra > 0 { + t.UserSignature = make([]uint8, extra) + } + + if _, err := io.ReadFull(cr, t.UserSignature); err != nil { + return err + } + + return nil +} + +var lengthBufRepegOp = []byte{135} + +func (t *RepegOp) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write(lengthBufRepegOp); err != nil { + return err + } + + // t.PoolID (string) (string) + if len(t.PoolID) > 8192 { + return xerrors.Errorf("Value in field t.PoolID was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.PoolID))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.PoolID)); err != nil { + return err + } + + // t.OldPriceScale (big.Int) (struct) + { + if t.OldPriceScale != nil && t.OldPriceScale.Sign() < 0 { + return xerrors.Errorf("Value in field t.OldPriceScale was a negative big-integer (not supported)") + } + if err := cw.CborWriteHeader(cbg.MajTag, 2); err != nil { + return err + } + var b []byte + if t.OldPriceScale != nil { + b = t.OldPriceScale.Bytes() + } + + if err := cw.CborWriteHeader(cbg.MajByteString, uint64(len(b))); err != nil { + return err + } + if _, err := cw.Write(b); err != nil { + return err + } + } + + // t.NewPriceScale (big.Int) (struct) + { + if t.NewPriceScale != nil && t.NewPriceScale.Sign() < 0 { + return xerrors.Errorf("Value in field t.NewPriceScale was a negative big-integer (not supported)") + } + if err := cw.CborWriteHeader(cbg.MajTag, 2); err != nil { + return err + } + var b []byte + if t.NewPriceScale != nil { + b = t.NewPriceScale.Bytes() + } + + if err := cw.CborWriteHeader(cbg.MajByteString, uint64(len(b))); err != nil { + return err + } + if _, err := cw.Write(b); err != nil { + return err + } + } + + // t.OldVirtPrice (big.Int) (struct) + { + if t.OldVirtPrice != nil && t.OldVirtPrice.Sign() < 0 { + return xerrors.Errorf("Value in field t.OldVirtPrice was a negative big-integer (not supported)") + } + if err := cw.CborWriteHeader(cbg.MajTag, 2); err != nil { + return err + } + var b []byte + if t.OldVirtPrice != nil { + b = t.OldVirtPrice.Bytes() + } + + if err := cw.CborWriteHeader(cbg.MajByteString, uint64(len(b))); err != nil { + return err + } + if _, err := cw.Write(b); err != nil { + return err + } + } + + // t.NewVirtPrice (big.Int) (struct) + { + if t.NewVirtPrice != nil && t.NewVirtPrice.Sign() < 0 { + return xerrors.Errorf("Value in field t.NewVirtPrice was a negative big-integer (not supported)") + } + if err := cw.CborWriteHeader(cbg.MajTag, 2); err != nil { + return err + } + var b []byte + if t.NewVirtPrice != nil { + b = t.NewVirtPrice.Bytes() + } + + if err := cw.CborWriteHeader(cbg.MajByteString, uint64(len(b))); err != nil { + return err + } + if _, err := cw.Write(b); err != nil { + return err + } + } + + // t.Epoch (uint64) (uint64) + + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.Epoch)); err != nil { + return err + } + + // t.PriceEMA (big.Int) (struct) + { + if t.PriceEMA != nil && t.PriceEMA.Sign() < 0 { + return xerrors.Errorf("Value in field t.PriceEMA was a negative big-integer (not supported)") + } + if err := cw.CborWriteHeader(cbg.MajTag, 2); err != nil { + return err + } + var b []byte + if t.PriceEMA != nil { + b = t.PriceEMA.Bytes() + } + + if err := cw.CborWriteHeader(cbg.MajByteString, uint64(len(b))); err != nil { + return err + } + if _, err := cw.Write(b); err != nil { + return err + } + } + return nil +} + +func (t *RepegOp) UnmarshalCBOR(r io.Reader) (err error) { + *t = RepegOp{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajArray { + return fmt.Errorf("cbor input should be of type array") + } + + if extra != 7 { + return fmt.Errorf("cbor input had wrong number of fields") + } + + // t.PoolID (string) (string) + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.PoolID = string(sval) + } + // t.OldPriceScale (big.Int) (struct) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if maj != cbg.MajTag || extra != 2 { + return fmt.Errorf("big ints should be cbor bignums") + } + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if maj != cbg.MajByteString { + return fmt.Errorf("big ints should be tagged cbor byte strings") + } + + if extra > 256 { + return fmt.Errorf("t.OldPriceScale: cbor bignum was too large") + } + + if extra > 0 { + buf := make([]byte, extra) + if _, err := io.ReadFull(cr, buf); err != nil { + return err + } + t.OldPriceScale = big.NewInt(0).SetBytes(buf) + } else { + t.OldPriceScale = big.NewInt(0) + } + // t.NewPriceScale (big.Int) (struct) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if maj != cbg.MajTag || extra != 2 { + return fmt.Errorf("big ints should be cbor bignums") + } + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if maj != cbg.MajByteString { + return fmt.Errorf("big ints should be tagged cbor byte strings") + } + + if extra > 256 { + return fmt.Errorf("t.NewPriceScale: cbor bignum was too large") + } + + if extra > 0 { + buf := make([]byte, extra) + if _, err := io.ReadFull(cr, buf); err != nil { + return err + } + t.NewPriceScale = big.NewInt(0).SetBytes(buf) + } else { + t.NewPriceScale = big.NewInt(0) + } + // t.OldVirtPrice (big.Int) (struct) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if maj != cbg.MajTag || extra != 2 { + return fmt.Errorf("big ints should be cbor bignums") + } + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if maj != cbg.MajByteString { + return fmt.Errorf("big ints should be tagged cbor byte strings") + } + + if extra > 256 { + return fmt.Errorf("t.OldVirtPrice: cbor bignum was too large") + } + + if extra > 0 { + buf := make([]byte, extra) + if _, err := io.ReadFull(cr, buf); err != nil { + return err + } + t.OldVirtPrice = big.NewInt(0).SetBytes(buf) + } else { + t.OldVirtPrice = big.NewInt(0) + } + // t.NewVirtPrice (big.Int) (struct) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if maj != cbg.MajTag || extra != 2 { + return fmt.Errorf("big ints should be cbor bignums") + } + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if maj != cbg.MajByteString { + return fmt.Errorf("big ints should be tagged cbor byte strings") + } + + if extra > 256 { + return fmt.Errorf("t.NewVirtPrice: cbor bignum was too large") + } + + if extra > 0 { + buf := make([]byte, extra) + if _, err := io.ReadFull(cr, buf); err != nil { + return err + } + t.NewVirtPrice = big.NewInt(0).SetBytes(buf) + } else { + t.NewVirtPrice = big.NewInt(0) + } + // t.Epoch (uint64) (uint64) + + { + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajUnsignedInt { + return fmt.Errorf("wrong type for uint64 field") + } + t.Epoch = uint64(extra) + + } + // t.PriceEMA (big.Int) (struct) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if maj != cbg.MajTag || extra != 2 { + return fmt.Errorf("big ints should be cbor bignums") + } + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if maj != cbg.MajByteString { + return fmt.Errorf("big ints should be tagged cbor byte strings") + } + + if extra > 256 { + return fmt.Errorf("t.PriceEMA: cbor bignum was too large") + } + + if extra > 0 { + buf := make([]byte, extra) + if _, err := io.ReadFull(cr, buf); err != nil { + return err + } + t.PriceEMA = big.NewInt(0).SetBytes(buf) + } else { + t.PriceEMA = big.NewInt(0) + } + return nil +} + +var lengthBufSessionCloseOp = []byte{133} + +func (t *SessionCloseOp) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write(lengthBufSessionCloseOp); err != nil { + return err + } + + // t.SessionID ([32]uint8) (array) + if len(t.SessionID) > 2097152 { + return xerrors.Errorf("Byte array in field t.SessionID was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.SessionID))); err != nil { + return err + } + + if _, err := cw.Write(t.SessionID[:]); err != nil { + return err + } + + // t.Version (uint64) (uint64) + + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.Version)); err != nil { + return err + } + + // t.UserAmount (decimal.Decimal) (struct) + if err := t.UserAmount.MarshalCBOR(cw); err != nil { + return err + } + + // t.ServiceAmount (decimal.Decimal) (struct) + if err := t.ServiceAmount.MarshalCBOR(cw); err != nil { + return err + } + + // t.Cooperative (bool) (bool) + if err := cbg.WriteBool(w, t.Cooperative); err != nil { + return err + } + return nil +} + +func (t *SessionCloseOp) UnmarshalCBOR(r io.Reader) (err error) { + *t = SessionCloseOp{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajArray { + return fmt.Errorf("cbor input should be of type array") + } + + if extra != 5 { + return fmt.Errorf("cbor input had wrong number of fields") + } + + // t.SessionID ([32]uint8) (array) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.SessionID: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + if extra != 32 { + return fmt.Errorf("expected array to have 32 elements") + } + + t.SessionID = [32]uint8{} + if _, err := io.ReadFull(cr, t.SessionID[:]); err != nil { + return err + } + // t.Version (uint64) (uint64) + + { + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajUnsignedInt { + return fmt.Errorf("wrong type for uint64 field") + } + t.Version = uint64(extra) + + } + // t.UserAmount (decimal.Decimal) (struct) + + { + + if err := t.UserAmount.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.UserAmount: %w", err) + } + + } + // t.ServiceAmount (decimal.Decimal) (struct) + + { + + if err := t.ServiceAmount.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.ServiceAmount: %w", err) + } + + } + // t.Cooperative (bool) (bool) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajOther { + return fmt.Errorf("booleans must be major type 7") + } + switch extra { + case 20: + t.Cooperative = false + case 21: + t.Cooperative = true + default: + return fmt.Errorf("booleans are either major type 7, value 20 or 21 (got %d)", extra) + } + return nil +} + +var lengthBufSessionChallengeOp = []byte{132} + +func (t *SessionChallengeOp) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write(lengthBufSessionChallengeOp); err != nil { + return err + } + + // t.SessionID ([32]uint8) (array) + if len(t.SessionID) > 2097152 { + return xerrors.Errorf("Byte array in field t.SessionID was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.SessionID))); err != nil { + return err + } + + if _, err := cw.Write(t.SessionID[:]); err != nil { + return err + } + + // t.PreviousVersion (uint64) (uint64) + + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.PreviousVersion)); err != nil { + return err + } + + // t.NewVersion (uint64) (uint64) + + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.NewVersion)); err != nil { + return err + } + + // t.NewDeadline (int64) (int64) + if t.NewDeadline >= 0 { + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.NewDeadline)); err != nil { + return err + } + } else { + if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-t.NewDeadline-1)); err != nil { + return err + } + } + + return nil +} + +func (t *SessionChallengeOp) UnmarshalCBOR(r io.Reader) (err error) { + *t = SessionChallengeOp{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajArray { + return fmt.Errorf("cbor input should be of type array") + } + + if extra != 4 { + return fmt.Errorf("cbor input had wrong number of fields") + } + + // t.SessionID ([32]uint8) (array) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.SessionID: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + if extra != 32 { + return fmt.Errorf("expected array to have 32 elements") + } + + t.SessionID = [32]uint8{} + if _, err := io.ReadFull(cr, t.SessionID[:]); err != nil { + return err + } + // t.PreviousVersion (uint64) (uint64) + + { + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajUnsignedInt { + return fmt.Errorf("wrong type for uint64 field") + } + t.PreviousVersion = uint64(extra) + + } + // t.NewVersion (uint64) (uint64) + + { + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajUnsignedInt { + return fmt.Errorf("wrong type for uint64 field") + } + t.NewVersion = uint64(extra) + + } + // t.NewDeadline (int64) (int64) + { + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + var extraI int64 + switch maj { + case cbg.MajUnsignedInt: + extraI = int64(extra) + if extraI < 0 { + return fmt.Errorf("int64 positive overflow") + } + case cbg.MajNegativeInt: + extraI = int64(extra) + if extraI < 0 { + return fmt.Errorf("int64 negative overflow") + } + extraI = -1 - extraI + default: + return fmt.Errorf("wrong type for int64 field: %d", maj) + } + + t.NewDeadline = int64(extraI) + } + return nil +} + +var lengthBufBlockEntryRef = []byte{130} + +func (t *BlockEntryRef) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write(lengthBufBlockEntryRef); err != nil { + return err + } + + // t.BlockHash ([32]uint8) (array) + if len(t.BlockHash) > 2097152 { + return xerrors.Errorf("Byte array in field t.BlockHash was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.BlockHash))); err != nil { + return err + } + + if _, err := cw.Write(t.BlockHash[:]); err != nil { + return err + } + + // t.EntryIndex (uint64) (uint64) + + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.EntryIndex)); err != nil { + return err + } + + return nil +} + +func (t *BlockEntryRef) UnmarshalCBOR(r io.Reader) (err error) { + *t = BlockEntryRef{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajArray { + return fmt.Errorf("cbor input should be of type array") + } + + if extra != 2 { + return fmt.Errorf("cbor input had wrong number of fields") + } + + // t.BlockHash ([32]uint8) (array) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.BlockHash: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + if extra != 32 { + return fmt.Errorf("expected array to have 32 elements") + } + + t.BlockHash = [32]uint8{} + if _, err := io.ReadFull(cr, t.BlockHash[:]); err != nil { + return err + } + // t.EntryIndex (uint64) (uint64) + + { + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajUnsignedInt { + return fmt.Errorf("wrong type for uint64 field") + } + t.EntryIndex = uint64(extra) + + } + return nil +} + +var lengthBufBurnReceipt = []byte{132} + +func (t *BurnReceipt) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write(lengthBufBurnReceipt); err != nil { + return err + } + + // t.WithdrawalID ([32]uint8) (array) + if len(t.WithdrawalID) > 2097152 { + return xerrors.Errorf("Byte array in field t.WithdrawalID was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.WithdrawalID))); err != nil { + return err + } + + if _, err := cw.Write(t.WithdrawalID[:]); err != nil { + return err + } + + // t.BlockEntryRef (core.BlockEntryRef) (struct) + if err := t.BlockEntryRef.MarshalCBOR(cw); err != nil { + return err + } + + // t.L1TxHash ([32]uint8) (array) + if len(t.L1TxHash) > 2097152 { + return xerrors.Errorf("Byte array in field t.L1TxHash was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.L1TxHash))); err != nil { + return err + } + + if _, err := cw.Write(t.L1TxHash[:]); err != nil { + return err + } + + // t.Signatures ([][]uint8) (slice) + if len(t.Signatures) > 8192 { + return xerrors.Errorf("Slice value in field t.Signatures was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Signatures))); err != nil { + return err + } + for _, v := range t.Signatures { + if len(v) > 2097152 { + return xerrors.Errorf("Byte array in field v was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(v))); err != nil { + return err + } + + if _, err := cw.Write(v); err != nil { + return err + } + + } + return nil +} + +func (t *BurnReceipt) UnmarshalCBOR(r io.Reader) (err error) { + *t = BurnReceipt{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajArray { + return fmt.Errorf("cbor input should be of type array") + } + + if extra != 4 { + return fmt.Errorf("cbor input had wrong number of fields") + } + + // t.WithdrawalID ([32]uint8) (array) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.WithdrawalID: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + if extra != 32 { + return fmt.Errorf("expected array to have 32 elements") + } + + t.WithdrawalID = [32]uint8{} + if _, err := io.ReadFull(cr, t.WithdrawalID[:]); err != nil { + return err + } + // t.BlockEntryRef (core.BlockEntryRef) (struct) + + { + + if err := t.BlockEntryRef.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.BlockEntryRef: %w", err) + } + + } + // t.L1TxHash ([32]uint8) (array) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.L1TxHash: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + if extra != 32 { + return fmt.Errorf("expected array to have 32 elements") + } + + t.L1TxHash = [32]uint8{} + if _, err := io.ReadFull(cr, t.L1TxHash[:]); err != nil { + return err + } + // t.Signatures ([][]uint8) (slice) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Signatures: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Signatures = make([][]uint8, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.Signatures[i]: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + + if extra > 0 { + t.Signatures[i] = make([]uint8, extra) + } + + if _, err := io.ReadFull(cr, t.Signatures[i]); err != nil { + return err + } + + } + } + return nil +} + +var lengthBufMintReceipt = []byte{136} + +func (t *MintReceipt) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write(lengthBufMintReceipt); err != nil { + return err + } + + // t.ChainID (uint64) (uint64) + + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.ChainID)); err != nil { + return err + } + + // t.L1TxHash ([32]uint8) (array) + if len(t.L1TxHash) > 2097152 { + return xerrors.Errorf("Byte array in field t.L1TxHash was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.L1TxHash))); err != nil { + return err + } + + if _, err := cw.Write(t.L1TxHash[:]); err != nil { + return err + } + + // t.LogIndex (uint64) (uint64) + + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.LogIndex)); err != nil { + return err + } + + // t.Account (string) (string) + if len(t.Account) > 8192 { + return xerrors.Errorf("Value in field t.Account was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Account))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Account)); err != nil { + return err + } + + // t.Asset (string) (string) + if len(t.Asset) > 8192 { + return xerrors.Errorf("Value in field t.Asset was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Asset))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Asset)); err != nil { + return err + } + + // t.Amount (big.Int) (struct) + { + if t.Amount != nil && t.Amount.Sign() < 0 { + return xerrors.Errorf("Value in field t.Amount was a negative big-integer (not supported)") + } + if err := cw.CborWriteHeader(cbg.MajTag, 2); err != nil { + return err + } + var b []byte + if t.Amount != nil { + b = t.Amount.Bytes() + } + + if err := cw.CborWriteHeader(cbg.MajByteString, uint64(len(b))); err != nil { + return err + } + if _, err := cw.Write(b); err != nil { + return err + } + } + + // t.BlockNumber (uint64) (uint64) + + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.BlockNumber)); err != nil { + return err + } + + // t.Signatures ([][]uint8) (slice) + if len(t.Signatures) > 8192 { + return xerrors.Errorf("Slice value in field t.Signatures was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Signatures))); err != nil { + return err + } + for _, v := range t.Signatures { + if len(v) > 2097152 { + return xerrors.Errorf("Byte array in field v was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(v))); err != nil { + return err + } + + if _, err := cw.Write(v); err != nil { + return err + } + + } + return nil +} + +func (t *MintReceipt) UnmarshalCBOR(r io.Reader) (err error) { + *t = MintReceipt{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajArray { + return fmt.Errorf("cbor input should be of type array") + } + + if extra != 8 { + return fmt.Errorf("cbor input had wrong number of fields") + } + + // t.ChainID (uint64) (uint64) + + { + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajUnsignedInt { + return fmt.Errorf("wrong type for uint64 field") + } + t.ChainID = uint64(extra) + + } + // t.L1TxHash ([32]uint8) (array) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.L1TxHash: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + if extra != 32 { + return fmt.Errorf("expected array to have 32 elements") + } + + t.L1TxHash = [32]uint8{} + if _, err := io.ReadFull(cr, t.L1TxHash[:]); err != nil { + return err + } + // t.LogIndex (uint64) (uint64) + + { + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajUnsignedInt { + return fmt.Errorf("wrong type for uint64 field") + } + t.LogIndex = uint64(extra) + + } + // t.Account (string) (string) + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.Account = string(sval) + } + // t.Asset (string) (string) + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.Asset = string(sval) + } + // t.Amount (big.Int) (struct) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if maj != cbg.MajTag || extra != 2 { + return fmt.Errorf("big ints should be cbor bignums") + } + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if maj != cbg.MajByteString { + return fmt.Errorf("big ints should be tagged cbor byte strings") + } + + if extra > 256 { + return fmt.Errorf("t.Amount: cbor bignum was too large") + } + + if extra > 0 { + buf := make([]byte, extra) + if _, err := io.ReadFull(cr, buf); err != nil { + return err + } + t.Amount = big.NewInt(0).SetBytes(buf) + } else { + t.Amount = big.NewInt(0) + } + // t.BlockNumber (uint64) (uint64) + + { + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajUnsignedInt { + return fmt.Errorf("wrong type for uint64 field") + } + t.BlockNumber = uint64(extra) + + } + // t.Signatures ([][]uint8) (slice) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Signatures: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Signatures = make([][]uint8, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.Signatures[i]: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + + if extra > 0 { + t.Signatures[i] = make([]uint8, extra) + } + + if _, err := io.ReadFull(cr, t.Signatures[i]); err != nil { + return err + } + + } + } + return nil +} + +var lengthBufFinalizedWithdrawal = []byte{134} + +func (t *FinalizedWithdrawal) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write(lengthBufFinalizedWithdrawal); err != nil { + return err + } + + // t.WithdrawalID ([32]uint8) (array) + if len(t.WithdrawalID) > 2097152 { + return xerrors.Errorf("Byte array in field t.WithdrawalID was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.WithdrawalID))); err != nil { + return err + } + + if _, err := cw.Write(t.WithdrawalID[:]); err != nil { + return err + } + + // t.BlockHash ([32]uint8) (array) + if len(t.BlockHash) > 2097152 { + return xerrors.Errorf("Byte array in field t.BlockHash was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.BlockHash))); err != nil { + return err + } + + if _, err := cw.Write(t.BlockHash[:]); err != nil { + return err + } + + // t.EntryIndex (uint64) (uint64) + + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.EntryIndex)); err != nil { + return err + } + + // t.FinalizedAt (int64) (int64) + if t.FinalizedAt >= 0 { + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.FinalizedAt)); err != nil { + return err + } + } else { + if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-t.FinalizedAt-1)); err != nil { + return err + } + } + + // t.Attestation (core.Attestation) (struct) + if err := t.Attestation.MarshalCBOR(cw); err != nil { + return err + } + + // t.Block (core.Block) (struct) + if err := t.Block.MarshalCBOR(cw); err != nil { + return err + } + return nil +} + +func (t *FinalizedWithdrawal) UnmarshalCBOR(r io.Reader) (err error) { + *t = FinalizedWithdrawal{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajArray { + return fmt.Errorf("cbor input should be of type array") + } + + if extra != 6 { + return fmt.Errorf("cbor input had wrong number of fields") + } + + // t.WithdrawalID ([32]uint8) (array) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.WithdrawalID: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + if extra != 32 { + return fmt.Errorf("expected array to have 32 elements") + } + + t.WithdrawalID = [32]uint8{} + if _, err := io.ReadFull(cr, t.WithdrawalID[:]); err != nil { + return err + } + // t.BlockHash ([32]uint8) (array) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.BlockHash: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + if extra != 32 { + return fmt.Errorf("expected array to have 32 elements") + } + + t.BlockHash = [32]uint8{} + if _, err := io.ReadFull(cr, t.BlockHash[:]); err != nil { + return err + } + // t.EntryIndex (uint64) (uint64) + + { + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajUnsignedInt { + return fmt.Errorf("wrong type for uint64 field") + } + t.EntryIndex = uint64(extra) + + } + // t.FinalizedAt (int64) (int64) + { + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + var extraI int64 + switch maj { + case cbg.MajUnsignedInt: + extraI = int64(extra) + if extraI < 0 { + return fmt.Errorf("int64 positive overflow") + } + case cbg.MajNegativeInt: + extraI = int64(extra) + if extraI < 0 { + return fmt.Errorf("int64 negative overflow") + } + extraI = -1 - extraI + default: + return fmt.Errorf("wrong type for int64 field: %d", maj) + } + + t.FinalizedAt = int64(extraI) + } + // t.Attestation (core.Attestation) (struct) + + { + + if err := t.Attestation.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Attestation: %w", err) + } + + } + // t.Block (core.Block) (struct) + + { + + if err := t.Block.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Block: %w", err) + } + + } + return nil +} + +var lengthBufFinalizedWithdrawalHeader = []byte{132} + +func (t *FinalizedWithdrawalHeader) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write(lengthBufFinalizedWithdrawalHeader); err != nil { + return err + } + + // t.WithdrawalID ([32]uint8) (array) + if len(t.WithdrawalID) > 2097152 { + return xerrors.Errorf("Byte array in field t.WithdrawalID was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.WithdrawalID))); err != nil { + return err + } + + if _, err := cw.Write(t.WithdrawalID[:]); err != nil { + return err + } + + // t.BlockHash ([32]uint8) (array) + if len(t.BlockHash) > 2097152 { + return xerrors.Errorf("Byte array in field t.BlockHash was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.BlockHash))); err != nil { + return err + } + + if _, err := cw.Write(t.BlockHash[:]); err != nil { + return err + } + + // t.EntryIndex (uint64) (uint64) + + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.EntryIndex)); err != nil { + return err + } + + // t.FinalizedAt (int64) (int64) + if t.FinalizedAt >= 0 { + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.FinalizedAt)); err != nil { + return err + } + } else { + if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-t.FinalizedAt-1)); err != nil { + return err + } + } + + return nil +} + +func (t *FinalizedWithdrawalHeader) UnmarshalCBOR(r io.Reader) (err error) { + *t = FinalizedWithdrawalHeader{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajArray { + return fmt.Errorf("cbor input should be of type array") + } + + if extra != 4 { + return fmt.Errorf("cbor input had wrong number of fields") + } + + // t.WithdrawalID ([32]uint8) (array) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.WithdrawalID: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + if extra != 32 { + return fmt.Errorf("expected array to have 32 elements") + } + + t.WithdrawalID = [32]uint8{} + if _, err := io.ReadFull(cr, t.WithdrawalID[:]); err != nil { + return err + } + // t.BlockHash ([32]uint8) (array) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.BlockHash: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + if extra != 32 { + return fmt.Errorf("expected array to have 32 elements") + } + + t.BlockHash = [32]uint8{} + if _, err := io.ReadFull(cr, t.BlockHash[:]); err != nil { + return err + } + // t.EntryIndex (uint64) (uint64) + + { + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajUnsignedInt { + return fmt.Errorf("wrong type for uint64 field") + } + t.EntryIndex = uint64(extra) + + } + // t.FinalizedAt (int64) (int64) + { + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + var extraI int64 + switch maj { + case cbg.MajUnsignedInt: + extraI = int64(extra) + if extraI < 0 { + return fmt.Errorf("int64 positive overflow") + } + case cbg.MajNegativeInt: + extraI = int64(extra) + if extraI < 0 { + return fmt.Errorf("int64 negative overflow") + } + extraI = -1 - extraI + default: + return fmt.Errorf("wrong type for int64 field: %d", maj) + } + + t.FinalizedAt = int64(extraI) + } + return nil +} diff --git a/pkg/core/entry_type.go b/pkg/core/entry_type.go new file mode 100644 index 0000000..2473003 --- /dev/null +++ b/pkg/core/entry_type.go @@ -0,0 +1,89 @@ +package core + +import ( + "math/big" + "math/bits" +) + +// EntryType identifies the kind of state operation in a BlockEntry (ADR-005 §5.3.5). +type EntryType uint8 + +const ( + OpTransfer EntryType = iota + 1 // SS5.3.5 — Transfer: debit sender, credit receiver (also LP operations per SS8.3) + OpSwap // SS8.1 — Swap: AMM execution result + OpWithdrawal // SS7.3.1 — Withdrawal: fund lock for L1 + OpBurn // ADR-005 §9.1 receipt-model — Withdrawal execution burn: DR 1020 / CR 2010 driven by a custody-signed BurnReceipt. Payload = canonical CBOR of the BurnReceipt. + OpMint // ADR-005 §9.1 receipt-model — Deposit credit: DR 1010 / CR 2010 driven by a custody-signed MintReceipt. Payload = canonical CBOR of the MintReceipt. + OpRepeg // SS8.1.2 — PriceScale adjustment + OpSessionClose // service_sessions.md §4 — Cooperative or unilateral session close + OpSessionChallenge // service_sessions.md §4 — Session dispute challenge +) + +// Attestation holds the BLS threshold signature proving cluster consensus (ADR-005 §5.3). +// Embedded in every Block. Verified off-chain by clearing layer and custody layer. +// +// §7 coordination: Validators is []BLSPubKey (variable-length byte slices) so entries can +// hold full 128-byte BN254 G2 public keys (WS-A.2). Legacy NodeID-only entries remain +// parseable as 32-byte slices; WS-A.2 populates real G2 keys from core.Slot.BLSPubKey. +type Attestation struct { + ThresholdSig []byte `json:"ThresholdSig"` // Aggregated BLS signature (G1 point) + Bitmask [32]byte `json:"Bitmask"` // Bitmask indicating which validators signed (up to 256 bits, SS5.3). + Validators [][]byte `json:"Validators"` // BLS public keys (G2) of signing validators — 128 bytes each once WS-A.2 lands. +} + +// SetBitmaskBit sets bit i (0-indexed, little-endian bit order) in a [32]byte bitmask. +// Bit i is stored at byte i/8, bit position i%8 within that byte. +func SetBitmaskBit(bm *[32]byte, i int) { + if i < 0 || i >= 256 { + return + } + bm[i/8] |= 1 << uint(i%8) +} + +// GetBitmaskBit returns true if bit i is set in the bitmask. +func GetBitmaskBit(bm [32]byte, i int) bool { + if i < 0 || i >= 256 { + return false + } + return bm[i/8]&(1<= 0; i-- { + if bm[i] != 0 { + b := bm[i] + bit := 7 + for b&(1< 0", asset.Asset) + } + out[i] = asset + } + sort.Slice(out, func(i, j int) bool { return out[i].Asset < out[j].Asset }) + for i := 1; i < len(out); i++ { + if out[i-1].Asset == out[i].Asset { + return nil, fmt.Errorf("duplicate transfer asset %s", out[i].Asset) + } + } + return out, nil +} + +// TransferOp is the payload for a transfer block entry (§5.3.5). +// Issued by the sender's cluster after debiting the sender. +// Consumed by the recipient's cluster to credit the recipient. +type TransferOp struct { + TxID string // Original transaction ID + To string // Recipient address + Assets []AssetTransfer // Assets transferred, sorted by AssetID (§7.2) +} + +// NewSingleAssetTransferOp creates a TransferOp with a single asset (common case). +func NewSingleAssetTransferOp(txID, to string, asset AssetID, amount decimal.Decimal) *TransferOp { + return &TransferOp{ + TxID: txID, + To: to, + Assets: []AssetTransfer{{Asset: asset, Amount: amount}}, + } +} + +// --------------------------------------------------------------------------- +// Swap TransferOp TxID convention (§7.1) +// +// liquidity.md §8.3: liquidity operations are expressed as ordinary multi-asset +// TransferOps to pool URIs and require no synthetic prefix; the pool cluster +// dispatches by asset shape (2 assets ⇒ AddLiquidity, 1 LP-share asset ⇒ +// RemoveLiquidity). Only swap retains a TxID prefix because the ordinary +// Transfer typed-data carries no AssetOut / MinAmountOut field — the prefix +// re-attaches that user-authorized data to the user→pool handoff op. +// --------------------------------------------------------------------------- + +const ( + swapPrefix = "swap:" +) + +// MakeSwapTxID creates the TransferOp TxID used for the user->pool swap +// handoff. TransferOp has no AssetOut field, so the pool cluster recovers the +// user-authorized output asset and slippage floor from this stable prefix after +// validating the sealed user block. +func MakeSwapTxID(baseTxID string, assetOut AssetID, minAmountOut *big.Int) string { + min := "0" + if minAmountOut != nil && minAmountOut.Sign() > 0 { + min = minAmountOut.String() + } + return swapPrefix + string(assetOut) + ":" + min + ":" + baseTxID +} + +// ParseSwapTxID extracts the fields encoded by MakeSwapTxID. +func ParseSwapTxID(txID string) (baseTxID string, assetOut AssetID, minAmountOut *big.Int, ok bool) { + if !strings.HasPrefix(txID, swapPrefix) { + return "", "", nil, false + } + parts := strings.SplitN(strings.TrimPrefix(txID, swapPrefix), ":", 3) + if len(parts) != 3 || parts[0] == "" || parts[2] == "" { + return "", "", nil, false + } + min, parsed := new(big.Int).SetString(parts[1], 10) + if !parsed || min.Sign() < 0 { + return "", "", nil, false + } + return parts[2], AssetID(parts[0]), min, true +} + +// IsSwapTxID returns true if the TxID represents a swap handoff TransferOp. +func IsSwapTxID(txID string) bool { + return strings.HasPrefix(txID, swapPrefix) +} + +// SwapOp is the payload for a swap execution block entry (§7.1). +// Issued by the pool cluster after executing the AMM swap. +// Consumed by the user's cluster to credit the output asset. +type SwapOp struct { + TxID string // Original transaction ID + AssetIn AssetID // Input asset (what the user paid) + AssetOut AssetID // Output asset (what the user receives) + AmountIn decimal.Decimal // Amount of input asset consumed + AmountOut decimal.Decimal // Amount of output asset produced (after fee) + PoolID string // Pool that executed the swap + Fee decimal.Decimal // Fee deducted (in output asset) + FeeRate uint64 // Dynamic fee rate applied (bps) — §5.3 + PriceEMA decimal.Decimal // Post-swap EMA price (1e18 fixed-point, data_layer.md §4.2) + SpotPrice decimal.Decimal // Post-swap spot price (1e18 fixed-point, data_layer.md §4.2) +} + +// RepegOp is the payload for a PriceScale repegging block entry (SS8.1.2). +// Issued by the pool cluster after a profit-gated PriceScale adjustment. +type RepegOp struct { + PoolID string // Target pool + OldPriceScale *big.Int // PriceScale before the repeg + NewPriceScale *big.Int // PriceScale after the repeg + OldVirtPrice *big.Int // VirtualPrice before the repeg + NewVirtPrice *big.Int // VirtualPrice after the repeg + Epoch uint64 // Epoch number when the repeg occurred + PriceEMA *big.Int // Post-repeg PriceEMA (data_layer.md §4.2) +} + +// WithdrawalOp is the payload for a withdrawal block entry (SS7.3.1). +// Issued by the user's cluster after locking funds for L1 withdrawal. +// Delivered to the custody layer after the clearing-side challenge window. +// +// Removed fields (now in block header or derived at vault level): +// - WithdrawalID: derived at vault level as keccak256(accountId, blockHash, entryIndex, chainId, recipient, asset, amount, nonce) +// - Nonce: in BlockEntry.Nonce +// - K: in Block.K +// - SignedAt: in Block.SealedAt +// - SignerNodeIDs: recoverable from Block.Attestation.Bitmask + cluster lookup +type WithdrawalOp struct { + Asset AssetID // Protocol-level asset name (e.g. "ETH") — needed for ledger finalization + L1Asset string // On-chain asset address (e.g., "0xA0b8...USDC") + Amount decimal.Decimal // Withdrawal amount in token units + ChainID uint64 // Target L1 chain + Recipient string // L1 recipient address (chain-native format) + UserSignature []byte // User's ECDSA authorization of the withdrawal +} + +// SessionCloseOp is the payload for a session close block entry (service_sessions.md §3.3). +// Emitted during cooperative or unilateral session settlement. +type SessionCloseOp struct { + SessionID [32]byte // keccak256(userAccountID || serviceAccountID || nonce) + Version uint64 // Latest co-signed version + UserAmount decimal.Decimal // Final user allocation + ServiceAmount decimal.Decimal // Final service allocation + Cooperative bool // True if both parties signed +} + +// SessionChallengeOp is the payload for a session dispute challenge (service_sessions.md §5.1). +type SessionChallengeOp struct { + SessionID [32]byte // Session being challenged + PreviousVersion uint64 // Version being replaced + NewVersion uint64 // Challenger's version (must be higher) + NewDeadline int64 // Unix timestamp when challenge window expires +} diff --git a/pkg/core/payload_codec.go b/pkg/core/payload_codec.go new file mode 100644 index 0000000..f288ab1 --- /dev/null +++ b/pkg/core/payload_codec.go @@ -0,0 +1,211 @@ +package core + +// Block-entry payload encoding. Per ADR-009 §4, `BlockEntry.Payload` +// bytes are canonical CBOR (cbor-gen tuple codec of the op struct) — +// no version-byte envelope, since BlockEntry.Payload is a nested byte +// string inside the already-enveloped Block. + +import ( + "bytes" + "fmt" + "io" + + "github.com/layer-3/clearnet-sdk/pkg/cborx" +) + +// cborPayloadMarshaler / cborPayloadUnmarshaler narrow the interface +// exactly to what cbor-gen's tuple emitter produces. Kept file-local so +// they do not leak into the exported surface. +type ( + cborPayloadMarshaler interface { + MarshalCBOR(w io.Writer) error + } + cborPayloadUnmarshaler interface { + UnmarshalCBOR(r io.Reader) error + } +) + +// marshalPayload encodes a codec target into a fresh byte slice using +// the generated CBOR tuple codec. The signature mirrors the old +// hand-rolled `op.Encode() []byte` so call-sites need no update. +func marshalPayload(op cborPayloadMarshaler) []byte { + var buf bytes.Buffer + if err := op.MarshalCBOR(&buf); err != nil { + // Writing to bytes.Buffer cannot fail; a non-nil error means the + // codec itself refused to serialize a required field — a + // programmer error we surface loudly. + panic(fmt.Errorf("core: payload MarshalCBOR: %w", err)) + } + return buf.Bytes() +} + +// unmarshalPayload decodes payload bytes into the given op using the +// generated CBOR tuple codec. Returns a wrapped error on any decode +// failure (truncated input, wrong type, non-canonical bytes). +// +// ADR-009 §1 requires a single canonical CBOR value per encoded byte +// string; cborx.UnmarshalExact rejects trailing bytes so a non-canonical +// payload cannot round-trip to a shorter canonical encoding. +func unmarshalPayload(payload []byte, op cborPayloadUnmarshaler) error { + if err := cborx.UnmarshalExact(payload, op); err != nil { + return fmt.Errorf("payload UnmarshalCBOR: %w", err) + } + return nil +} + +// DecodePayload decodes a BlockEntry's binary payload into the typed +// operation struct based on entry.Type. Returns one of: +// +// *TransferOp, *SwapOp, *WithdrawalOp, *RepegOp, +// *SessionCloseOp, *SessionChallengeOp +// +// Returns an error for unknown entry types or malformed payloads. +func DecodePayload(entry BlockEntry) (interface{}, error) { + switch entry.Type { + case OpTransfer: + op := &TransferOp{} + if err := op.Decode(entry.Payload); err != nil { + return nil, fmt.Errorf("decode transfer payload: %w", err) + } + return op, nil + case OpSwap: + op := &SwapOp{} + if err := op.Decode(entry.Payload); err != nil { + return nil, fmt.Errorf("decode swap payload: %w", err) + } + return op, nil + case OpWithdrawal: + op := &WithdrawalOp{} + if err := op.Decode(entry.Payload); err != nil { + return nil, fmt.Errorf("decode withdrawal payload: %w", err) + } + return op, nil + case OpRepeg: + op := &RepegOp{} + if err := op.Decode(entry.Payload); err != nil { + return nil, fmt.Errorf("decode repeg payload: %w", err) + } + return op, nil + case OpBurn: + v := &BurnReceipt{} + if err := unmarshalPayload(entry.Payload, v); err != nil { + return nil, fmt.Errorf("decode burn receipt payload: %w", err) + } + return v, nil + case OpMint: + v := &MintReceipt{} + if err := unmarshalPayload(entry.Payload, v); err != nil { + return nil, fmt.Errorf("decode mint receipt payload: %w", err) + } + return v, nil + case OpSessionClose: + op := &SessionCloseOp{} + if err := op.Decode(entry.Payload); err != nil { + return nil, fmt.Errorf("decode session close payload: %w", err) + } + return op, nil + case OpSessionChallenge: + op := &SessionChallengeOp{} + if err := op.Decode(entry.Payload); err != nil { + return nil, fmt.Errorf("decode session challenge payload: %w", err) + } + return op, nil + default: + return nil, fmt.Errorf("unknown entry type: %d", entry.Type) + } +} + +// --------------------------------------------------------------------------- +// TransferOp +// --------------------------------------------------------------------------- + +// Encode serializes the TransferOp into a block entry payload +// (canonical CBOR, ADR-009 §4). +func (op *TransferOp) Encode() []byte { return marshalPayload(op) } + +// Decode deserializes a TransferOp from a block entry payload. +func (op *TransferOp) Decode(payload []byte) error { return unmarshalPayload(payload, op) } + +// --------------------------------------------------------------------------- +// SwapOp +// --------------------------------------------------------------------------- + +// Encode serializes the SwapOp into a block entry payload. +func (op *SwapOp) Encode() []byte { return marshalPayload(op) } + +// Decode deserializes a SwapOp from a block entry payload. +func (op *SwapOp) Decode(payload []byte) error { return unmarshalPayload(payload, op) } + +// --------------------------------------------------------------------------- +// WithdrawalOp +// --------------------------------------------------------------------------- + +// Encode serializes the WithdrawalOp into a block entry payload. +func (op *WithdrawalOp) Encode() []byte { return marshalPayload(op) } + +// Decode deserializes a WithdrawalOp from a block entry payload. +func (op *WithdrawalOp) Decode(payload []byte) error { return unmarshalPayload(payload, op) } + +// --------------------------------------------------------------------------- +// RepegOp +// --------------------------------------------------------------------------- + +// Encode serializes the RepegOp into a block entry payload. +func (op *RepegOp) Encode() []byte { return marshalPayload(op) } + +// Decode deserializes a RepegOp from a block entry payload. +func (op *RepegOp) Decode(payload []byte) error { return unmarshalPayload(payload, op) } + +// --------------------------------------------------------------------------- +// MintReceipt / BurnReceipt) +// --------------------------------------------------------------------------- + +// EncodePayload serializes a MintReceipt into a BlockEntry payload. +func (v *MintReceipt) EncodePayload() []byte { return marshalPayload(v) } + +// EncodePayload serializes a BurnReceipt into a BlockEntry payload. +func (v *BurnReceipt) EncodePayload() []byte { return marshalPayload(v) } + +// --------------------------------------------------------------------------- +// SessionCloseOp +// --------------------------------------------------------------------------- + +// Encode serializes the SessionCloseOp into a block entry payload. +func (op *SessionCloseOp) Encode() []byte { return marshalPayload(op) } + +// Decode deserializes a SessionCloseOp from a block entry payload. +func (op *SessionCloseOp) Decode(payload []byte) error { return unmarshalPayload(payload, op) } + +// EncodeSessionCloseOp serializes a SessionCloseOp. +func EncodeSessionCloseOp(op *SessionCloseOp) []byte { return op.Encode() } + +// DecodeSessionCloseOp deserializes a SessionCloseOp. +func DecodeSessionCloseOp(data []byte) (*SessionCloseOp, error) { + op := &SessionCloseOp{} + if err := op.Decode(data); err != nil { + return nil, err + } + return op, nil +} + +// --------------------------------------------------------------------------- +// SessionChallengeOp +// --------------------------------------------------------------------------- + +// Encode serializes the SessionChallengeOp into a block entry payload. +func (op *SessionChallengeOp) Encode() []byte { return marshalPayload(op) } + +// Decode deserializes a SessionChallengeOp from a block entry payload. +func (op *SessionChallengeOp) Decode(payload []byte) error { return unmarshalPayload(payload, op) } + +// EncodeSessionChallengeOp serializes a SessionChallengeOp. +func EncodeSessionChallengeOp(op *SessionChallengeOp) []byte { return op.Encode() } + +// DecodeSessionChallengeOp deserializes a SessionChallengeOp. +func DecodeSessionChallengeOp(data []byte) (*SessionChallengeOp, error) { + op := &SessionChallengeOp{} + if err := op.Decode(data); err != nil { + return nil, err + } + return op, nil +} diff --git a/pkg/core/preimage_golden_test.go b/pkg/core/preimage_golden_test.go new file mode 100644 index 0000000..0fb7311 --- /dev/null +++ b/pkg/core/preimage_golden_test.go @@ -0,0 +1,499 @@ +package core + +// Byte-exact golden vectors for the CBOR-based signing preimages +// introduced by Wave 2b of the CBOR migration (docs/plans/cbor-encoding.md +// §7 Wave 2b, ADR-009 §4). Complements the Solidity-pinned preimages +// captured under `testdata/goldens/solidity-preimages/` in Wave 0. +// +// Fixtures pinned by this test: +// +// - Block.SigningMessage = canonical CBOR of BlockHeader{Anchor, +// SealedAt, StateRoot, EntriesDigest, K, Accounts}. +// - BlockEntry.Hash preimage = canonical CBOR of BlockEntry{Type, +// Account, Nonce, Payload}, hashed by keccak256 at the call site. +// - BlockEntry.Payload bytes = canonical CBOR of one typed op per +// entry kind (TransferOp, SwapOp, WithdrawalOp, RepegOp, +// SessionCloseOp, SessionChallengeOp). +// +// Regenerate fixtures after a coordinated change: +// +// go test ./pkg/core/ -run TestGoldens_Preimages -update +// +// A change that alters any golden without a corresponding version-byte +// bump (ADR-009 §5) is a schema-family break and must be reviewed as +// such. The CI guard `scripts/ci/check-preimage-goldens.sh` enforces +// byte equality against committed goldens. + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "flag" + "io" + "math/big" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/crypto" + "github.com/layer-3/clearnet-sdk/pkg/decimal" +) + +var updatePreimageGoldens = flag.Bool("update", false, "regenerate testdata/goldens/preimages/* fixtures") + +// cborMarshaler narrows the codec surface to the generated MarshalCBOR +// method. (In clearnet this lived in cbor_roundtrip_test.go, which did +// not move to the SDK.) +type cborMarshaler interface { + MarshalCBOR(w io.Writer) error +} + +// preimageGoldenRoot locates `testdata/goldens/preimages/` regardless of +// cwd. Mirrors the pattern used by clearing/anchor/smt_golden_test.go so +// the two fixture families coexist without surprise. +func preimageGoldenRoot(t *testing.T) string { + t.Helper() + _, file, _, ok := runtime.Caller(0) + if !ok { + t.Fatal("runtime.Caller failed") + } + // file = /pkg/core/preimage_golden_test.go + repoRoot := filepath.Join(filepath.Dir(file), "..", "..") + return filepath.Join(repoRoot, "testdata", "goldens", "preimages") +} + +// writeOrComparePreimage pairs with the Wave-0 solidity-preimage helper +// but lives in the `core` package because these preimages are authored +// by core. +func writeOrComparePreimage(t *testing.T, base string, inputJSON []byte, goldenBytes []byte) { + t.Helper() + hexPath := base + ".golden.hex" + jsonPath := base + ".input.json" + wantHex := strings.ToLower(hex.EncodeToString(goldenBytes)) + + if *updatePreimageGoldens { + if err := os.MkdirAll(filepath.Dir(hexPath), 0o755); err != nil { + t.Fatalf("mkdir %s: %v", filepath.Dir(hexPath), err) + } + if err := os.WriteFile(jsonPath, append(inputJSON, '\n'), 0o644); err != nil { + t.Fatalf("write input: %v", err) + } + if err := os.WriteFile(hexPath, []byte(wantHex+"\n"), 0o644); err != nil { + t.Fatalf("write golden: %v", err) + } + return + } + raw, err := os.ReadFile(hexPath) + if err != nil { + t.Fatalf("read golden %s: %v (re-run with -update to create)", hexPath, err) + } + got := strings.TrimSpace(string(raw)) + if got != wantHex { + t.Fatalf("preimage drift at %s:\n want (current Go): %s\n have (on disk): %s", + hexPath, wantHex, got) + } + if _, err := os.Stat(jsonPath); err != nil { + t.Fatalf("missing input fixture %s: %v", jsonPath, err) + } +} + +// cborBytes marshals a codec target to a fresh byte slice. +func cborBytes(t *testing.T, m cborMarshaler) []byte { + t.Helper() + var buf bytes.Buffer + if err := m.MarshalCBOR(&buf); err != nil { + t.Fatalf("MarshalCBOR: %v", err) + } + return buf.Bytes() +} + +// parseHex32Preimage decodes a 64-char hex string into a 32-byte array. +func parseHex32Preimage(t *testing.T, s string) [32]byte { + t.Helper() + b, err := hex.DecodeString(s) + if err != nil || len(b) != 32 { + t.Fatalf("bad 32-byte hex %q: %v (len=%d)", s, err, len(b)) + } + var out [32]byte + copy(out[:], b) + return out +} + +// --------------------------------------------------------------------------- +// Fixture shapes (embedded in .input.json files for human traceability) +// --------------------------------------------------------------------------- + +type blockHeaderVec struct { + Name string `json:"name"` + Description string `json:"description"` + Anchor string `json:"anchorHex"` + SealedAt int64 `json:"sealedAt"` + StateRoot string `json:"stateRootHex"` + EntriesDigest string `json:"entriesDigestHex"` + K uint64 `json:"k"` + Accounts []accountSnapshotVecRow `json:"accounts"` + CBORBytes int `json:"cborBytes"` + DerivedHash string `json:"derivedBlockHashHex"` +} + +type accountSnapshotVecRow struct { + AccountID string `json:"accountIDHex"` + PrevBlockRef string `json:"prevBlockRefHex"` + PostNonce uint64 `json:"postNonce"` +} + +type entryHashVec struct { + Name string `json:"name"` + Description string `json:"description"` + EntryType uint8 `json:"entryType"` + EntryTypeStr string `json:"entryTypeStr"` + Account string `json:"account"` + Nonce uint64 `json:"nonce"` + PayloadHex string `json:"payloadHex"` + CBORBytes int `json:"cborBytes"` + DerivedHash string `json:"derivedEntryHashHex"` +} + +type opPayloadVec struct { + Name string `json:"name"` + Description string `json:"description"` + OpType string `json:"opType"` + Op interface{} `json:"op"` + CBORBytes int `json:"cborBytes"` +} + +type finalizedWithdrawalHeaderVec struct { + Name string `json:"name"` + Description string `json:"description"` + WithdrawalID string `json:"withdrawalIDHex"` + BlockHash string `json:"blockHashHex"` + EntryIndex uint64 `json:"entryIndex"` + FinalizedAt int64 `json:"finalizedAt"` + CBORBytes int `json:"cborBytes"` + DerivedHash string `json:"derivedFinalizedHashHex"` +} + +// snapshotRows converts []AccountSnapshot to the serializable vec shape. +func snapshotRows(accs []AccountSnapshot) []accountSnapshotVecRow { + out := make([]accountSnapshotVecRow, len(accs)) + for i, a := range accs { + out[i] = accountSnapshotVecRow{ + AccountID: hex.EncodeToString(a.AccountID[:]), + PrevBlockRef: hex.EncodeToString(a.PrevBlockRef[:]), + PostNonce: a.PostNonce, + } + } + return out +} + +// --------------------------------------------------------------------------- +// TestGoldens_Preimages — the single top-level entry point. +// --------------------------------------------------------------------------- + +func TestGoldens_Preimages(t *testing.T) { + root := preimageGoldenRoot(t) + + t.Run("block_header", func(t *testing.T) { + dir := filepath.Join(root, "block_header") + + // Case 1: empty Accounts — pure header bind. + t.Run("empty_accounts", func(t *testing.T) { + bh := BlockHeader{ + Anchor: parseHex32Preimage(t, "1111111111111111111111111111111111111111111111111111111111111111"), + SealedAt: 1700000000, + StateRoot: parseHex32Preimage(t, "2222222222222222222222222222222222222222222222222222222222222222"), + EntriesDigest: parseHex32Preimage(t, "3333333333333333333333333333333333333333333333333333333333333333"), + K: 7, + Accounts: nil, + } + got := cborBytes(t, &bh) + blk := Block{ + Anchor: bh.Anchor, + SealedAt: bh.SealedAt, + StateRoot: bh.StateRoot, + EntriesDigest: bh.EntriesDigest, + K: bh.K, + Accounts: bh.Accounts, + } + blkHash := blk.Hash() + + meta := blockHeaderVec{ + Name: "empty_accounts", + Description: "BlockHeader preimage (ADR-009 §4) with no AccountSnapshots. canonical-CBOR([]). Block.Hash = keccak256(preimage).", + Anchor: hex.EncodeToString(bh.Anchor[:]), + SealedAt: bh.SealedAt, + StateRoot: hex.EncodeToString(bh.StateRoot[:]), + EntriesDigest: hex.EncodeToString(bh.EntriesDigest[:]), + K: bh.K, + Accounts: snapshotRows(bh.Accounts), + CBORBytes: len(got), + DerivedHash: hex.EncodeToString(blkHash[:]), + } + js, _ := json.MarshalIndent(meta, "", " ") + writeOrComparePreimage(t, filepath.Join(dir, "empty_accounts"), js, got) + }) + + // Case 2: single AccountSnapshot — binds chain continuity (Q6). + t.Run("single_account", func(t *testing.T) { + bh := BlockHeader{ + Anchor: parseHex32Preimage(t, "1111111111111111111111111111111111111111111111111111111111111111"), + SealedAt: 1700000000, + StateRoot: parseHex32Preimage(t, "2222222222222222222222222222222222222222222222222222222222222222"), + EntriesDigest: parseHex32Preimage(t, "3333333333333333333333333333333333333333333333333333333333333333"), + K: 7, + Accounts: []AccountSnapshot{ + { + AccountID: parseHex32Preimage(t, "4444444444444444444444444444444444444444444444444444444444444444"), + PrevBlockRef: parseHex32Preimage(t, "5555555555555555555555555555555555555555555555555555555555555555"), + PostNonce: 42, + }, + }, + } + got := cborBytes(t, &bh) + blk := Block{ + Anchor: bh.Anchor, + SealedAt: bh.SealedAt, + StateRoot: bh.StateRoot, + EntriesDigest: bh.EntriesDigest, + K: bh.K, + Accounts: bh.Accounts, + } + blkHash := blk.Hash() + + meta := blockHeaderVec{ + Name: "single_account", + Description: "BlockHeader preimage with one AccountSnapshot. Q6 binds per-account chain continuity (PrevBlockRef + PostNonce) under the BLS signature.", + Anchor: hex.EncodeToString(bh.Anchor[:]), + SealedAt: bh.SealedAt, + StateRoot: hex.EncodeToString(bh.StateRoot[:]), + EntriesDigest: hex.EncodeToString(bh.EntriesDigest[:]), + K: bh.K, + Accounts: snapshotRows(bh.Accounts), + CBORBytes: len(got), + DerivedHash: hex.EncodeToString(blkHash[:]), + } + js, _ := json.MarshalIndent(meta, "", " ") + writeOrComparePreimage(t, filepath.Join(dir, "single_account"), js, got) + }) + }) + + t.Run("entry_hash", func(t *testing.T) { + dir := filepath.Join(root, "entry_hash") + // Transfer entry: encode a known TransferOp, then pin EntryHash preimage. + t.Run("transfer", func(t *testing.T) { + op := NewSingleAssetTransferOp("tx-golden-transfer", "0xBob", "USDT", decimal.NewFromBigInt(big.NewInt(500_000), 0)) + e := BlockEntry{ + Type: OpTransfer, + Account: "0xAlice", + Nonce: 1, + Payload: op.Encode(), + } + preimage := cborBytes(t, &e) + entryHash := crypto.Keccak256Hash(preimage) + + meta := entryHashVec{ + Name: "transfer", + Description: "EntryHash preimage = canonical CBOR of BlockEntry tuple (ADR-009 §4). EntryHash = keccak256(preimage). Payload is the CBOR-encoded TransferOp.", + EntryType: uint8(e.Type), + EntryTypeStr: "OpTransfer", + Account: e.Account, + Nonce: e.Nonce, + PayloadHex: hex.EncodeToString(e.Payload), + CBORBytes: len(preimage), + DerivedHash: hex.EncodeToString(entryHash[:]), + } + js, _ := json.MarshalIndent(meta, "", " ") + writeOrComparePreimage(t, filepath.Join(dir, "transfer"), js, preimage) + }) + }) + + t.Run("op_payload", func(t *testing.T) { + dir := filepath.Join(root, "op_payload") + + // TransferOp + t.Run("transfer_single_asset", func(t *testing.T) { + op := NewSingleAssetTransferOp("tx-golden-xfer-1", "0xBob", "USDT", decimal.NewFromBigInt(big.NewInt(1_000), 0)) + got := cborBytes(t, op) + meta := opPayloadVec{ + Name: "transfer_single_asset", + Description: "TransferOp payload = canonical CBOR of TransferOp tuple (ADR-009 §4); replaces the deleted hand-rolled op_encoding.go layout.", + OpType: "TransferOp", + Op: map[string]interface{}{ + "txID": op.TxID, "to": op.To, + "assets": []map[string]interface{}{{"asset": string(op.Assets[0].Asset), "amountDec": op.Assets[0].Amount.String()}}, + }, + CBORBytes: len(got), + } + js, _ := json.MarshalIndent(meta, "", " ") + writeOrComparePreimage(t, filepath.Join(dir, "transfer_single_asset"), js, got) + }) + + // SwapOp + t.Run("swap", func(t *testing.T) { + op := &SwapOp{ + TxID: "tx-golden-swap", + AssetIn: "ETH", + AssetOut: "USDC", + AmountIn: decimal.NewFromBigInt(big.NewInt(1_000), 0), + AmountOut: decimal.NewFromBigInt(big.NewInt(2_000), 0), + PoolID: "pool:ETH-USDC", + Fee: decimal.NewFromBigInt(big.NewInt(3), 0), + FeeRate: 30, + PriceEMA: decimal.NewFromBigInt(big.NewInt(2_000), 0), + SpotPrice: decimal.NewFromBigInt(big.NewInt(2_001), 0), + } + got := cborBytes(t, op) + meta := opPayloadVec{ + Name: "swap", + Description: "SwapOp payload = canonical CBOR of SwapOp tuple.", + OpType: "SwapOp", + Op: map[string]interface{}{ + "txID": op.TxID, "assetIn": string(op.AssetIn), "assetOut": string(op.AssetOut), + "amountInDec": op.AmountIn.String(), "amountOutDec": op.AmountOut.String(), + "poolID": op.PoolID, "feeDec": op.Fee.String(), "feeRate": op.FeeRate, + "priceEMADec": op.PriceEMA.String(), "spotPriceDec": op.SpotPrice.String(), + }, + CBORBytes: len(got), + } + js, _ := json.MarshalIndent(meta, "", " ") + writeOrComparePreimage(t, filepath.Join(dir, "swap"), js, got) + }) + + // WithdrawalOp + t.Run("withdrawal", func(t *testing.T) { + op := &WithdrawalOp{ + Asset: "USDT", + L1Asset: "0xA0b8000000000000000000000000000000000001", + Amount: decimal.NewFromBigInt(big.NewInt(10_000_000), 0), + ChainID: 1, + Recipient: "0xRecipient", + UserSignature: []byte{0xAA, 0xBB, 0xCC}, + } + got := cborBytes(t, op) + meta := opPayloadVec{ + Name: "withdrawal", + Description: "WithdrawalOp payload = canonical CBOR of WithdrawalOp tuple.", + OpType: "WithdrawalOp", + Op: map[string]interface{}{ + "asset": string(op.Asset), "l1Asset": op.L1Asset, + "amountDec": op.Amount.String(), "chainID": op.ChainID, "recipient": op.Recipient, + "userSignatureHex": hex.EncodeToString(op.UserSignature), + }, + CBORBytes: len(got), + } + js, _ := json.MarshalIndent(meta, "", " ") + writeOrComparePreimage(t, filepath.Join(dir, "withdrawal"), js, got) + }) + + // RepegOp + t.Run("repeg", func(t *testing.T) { + op := &RepegOp{ + PoolID: "pool:ETH-USDC", + OldPriceScale: big.NewInt(2_000), + NewPriceScale: big.NewInt(2_010), + OldVirtPrice: big.NewInt(1_000), + NewVirtPrice: big.NewInt(1_001), + Epoch: 42, + PriceEMA: big.NewInt(2_005), + } + got := cborBytes(t, op) + meta := opPayloadVec{ + Name: "repeg", + Description: "RepegOp payload = canonical CBOR of RepegOp tuple.", + OpType: "RepegOp", + Op: map[string]interface{}{ + "poolID": op.PoolID, "epoch": op.Epoch, + "oldPriceScaleDec": op.OldPriceScale.String(), "newPriceScaleDec": op.NewPriceScale.String(), + "oldVirtPriceDec": op.OldVirtPrice.String(), "newVirtPriceDec": op.NewVirtPrice.String(), + "priceEMADec": op.PriceEMA.String(), + }, + CBORBytes: len(got), + } + js, _ := json.MarshalIndent(meta, "", " ") + writeOrComparePreimage(t, filepath.Join(dir, "repeg"), js, got) + }) + + // SessionCloseOp + t.Run("session_close", func(t *testing.T) { + op := &SessionCloseOp{ + SessionID: parseHex32Preimage(t, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + Version: 42, + UserAmount: decimal.NewFromBigInt(big.NewInt(1_000_000), 0), + ServiceAmount: decimal.NewFromBigInt(big.NewInt(500_000), 0), + Cooperative: true, + } + got := cborBytes(t, op) + meta := opPayloadVec{ + Name: "session_close", + Description: "SessionCloseOp payload = canonical CBOR of SessionCloseOp tuple.", + OpType: "SessionCloseOp", + Op: map[string]interface{}{ + "sessionIDHex": hex.EncodeToString(op.SessionID[:]), + "version": op.Version, + "userAmountDec": op.UserAmount.String(), "serviceAmountDec": op.ServiceAmount.String(), + "cooperative": op.Cooperative, + }, + CBORBytes: len(got), + } + js, _ := json.MarshalIndent(meta, "", " ") + writeOrComparePreimage(t, filepath.Join(dir, "session_close"), js, got) + }) + + // SessionChallengeOp + t.Run("session_challenge", func(t *testing.T) { + op := &SessionChallengeOp{ + SessionID: parseHex32Preimage(t, "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), + PreviousVersion: 5, + NewVersion: 6, + NewDeadline: 1700003600, + } + got := cborBytes(t, op) + meta := opPayloadVec{ + Name: "session_challenge", + Description: "SessionChallengeOp payload = canonical CBOR of SessionChallengeOp tuple.", + OpType: "SessionChallengeOp", + Op: map[string]interface{}{ + "sessionIDHex": hex.EncodeToString(op.SessionID[:]), + "previousVersion": op.PreviousVersion, "newVersion": op.NewVersion, + "newDeadline": op.NewDeadline, + }, + CBORBytes: len(got), + } + js, _ := json.MarshalIndent(meta, "", " ") + writeOrComparePreimage(t, filepath.Join(dir, "session_challenge"), js, got) + }) + }) + + // FinalizedWithdrawal.SigningMessage() — the BLS finality preimage. + // Per ADR-009 §4 this is the canonical-CBOR encoding of the four-field + // header projection (Attestation deliberately excluded so the signature + // is not self-referential). + t.Run("finalized_withdrawal", func(t *testing.T) { + dir := filepath.Join(root, "finalized_withdrawal") + t.Run("header", func(t *testing.T) { + fw := &FinalizedWithdrawal{ + WithdrawalID: parseHex32Preimage(t, "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"), + BlockHash: parseHex32Preimage(t, "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"), + EntryIndex: 3, + FinalizedAt: 1700004200, + } + got := fw.SigningMessage() + finalizedHash := crypto.Keccak256Hash(got) + + meta := finalizedWithdrawalHeaderVec{ + Name: "header", + Description: "FinalizedWithdrawal.SigningMessage() = canonical CBOR of FinalizedWithdrawalHeader{WithdrawalID, BlockHash, EntryIndex, FinalizedAt} (ADR-009 §4). Finality hash = keccak256(preimage).", + WithdrawalID: hex.EncodeToString(fw.WithdrawalID[:]), + BlockHash: hex.EncodeToString(fw.BlockHash[:]), + EntryIndex: fw.EntryIndex, + FinalizedAt: fw.FinalizedAt, + CBORBytes: len(got), + DerivedHash: hex.EncodeToString(finalizedHash[:]), + } + js, _ := json.MarshalIndent(meta, "", " ") + writeOrComparePreimage(t, filepath.Join(dir, "header"), js, got) + }) + }) +} diff --git a/pkg/core/types.go b/pkg/core/types.go new file mode 100644 index 0000000..c40a9e8 --- /dev/null +++ b/pkg/core/types.go @@ -0,0 +1,90 @@ +package core + +import ( + "bytes" + "fmt" +) + +// AssetID is a unique identifier for a token (§2). +// Symbols (e.g. "USDT", "ETH") are mapped to L1 (ChainID, TokenAddress) pairs +// via the AssetResolver interface at runtime. +type AssetID string + +// FinalizedWithdrawal is the positive clearing-layer finality object consumed +// by custody after the challenge window expires. Attestation signs +// SigningMessage(); Block is carried so custody providers can bind the ID to +// the exact withdrawal payload without maintaining clearing state. +type FinalizedWithdrawal struct { + WithdrawalID [32]byte `json:"WithdrawalID"` + BlockHash [32]byte `json:"BlockHash"` + EntryIndex uint64 `json:"EntryIndex"` + FinalizedAt int64 `json:"FinalizedAt"` + Attestation Attestation `json:"Attestation"` + Block Block `json:"Block"` +} + +// FinalizedWithdrawalHeader is the deterministic preimage projection for a +// FinalizedWithdrawal. It intentionally excludes Attestation and Block while +// binding the block hash and entry location that custody must verify. +type FinalizedWithdrawalHeader struct { + WithdrawalID [32]byte + BlockHash [32]byte + EntryIndex uint64 + FinalizedAt int64 +} + +func (fw *FinalizedWithdrawal) Header() FinalizedWithdrawalHeader { + if fw == nil { + return FinalizedWithdrawalHeader{} + } + return FinalizedWithdrawalHeader{ + WithdrawalID: fw.WithdrawalID, + BlockHash: fw.BlockHash, + EntryIndex: fw.EntryIndex, + FinalizedAt: fw.FinalizedAt, + } +} + +// SigningMessage returns the canonical CBOR preimage covered by the BLS +// finality signature (ADR-009 §4). It is not the same preimage as Block and +// excludes Attestation so the signature cannot be self-referential. +// Returns nil for a nil receiver so callers can distinguish "no message" from +// an all-zero preimage. +// +// Preimage shape: canonical CBOR of FinalizedWithdrawalHeader{WithdrawalID, +// BlockHash, EntryIndex, FinalizedAt}. Frozen by the FinalizedWithdrawalHeader +// case in TestGoldens_Preimages; any change is a schema-family bump. +func (fw *FinalizedWithdrawal) SigningMessage() []byte { + if fw == nil { + return nil + } + header := fw.Header() + var buf bytes.Buffer + if err := (&header).MarshalCBOR(&buf); err != nil { + // FinalizedWithdrawalHeader's generated codec writes to a bytes.Buffer, + // which cannot fail; a non-nil error here is a structural regression. + panic(fmt.Errorf("core: FinalizedWithdrawalHeader.MarshalCBOR: %w", err)) + } + return buf.Bytes() +} + +// BLSPubKeyG2Len is the serialized length of a BN254 G2 pubkey (4×32 bytes). +const BLSPubKeyG2Len = 128 + +// NodeID is a 256-bit identity in the Kademlia space (§3.1). +// A Node MAY operate multiple NodeIDs, each with its own collateral and BLS key pair. +type NodeID [32]byte + +// First8Hex returns the first 8 hex characters of id. Used to synthesize +// human-readable Slot labels when a peer did not supply one +// (docs/plans/logical_node.md §5.4). +func First8Hex(id NodeID) string { + const hex = "0123456789abcdef" + var out [8]byte + for i := 0; i < 4; i++ { + b := id[i] + out[i*2] = hex[b>>4] + out[i*2+1] = hex[b&0x0f] + } + return string(out[:]) +} diff --git a/pkg/core/uri.go b/pkg/core/uri.go new file mode 100644 index 0000000..4b48587 --- /dev/null +++ b/pkg/core/uri.go @@ -0,0 +1,280 @@ +package core + +import ( + "encoding/hex" + "fmt" + "strings" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +// URI scheme and default network for Yellow Network resources (ADR-007 §4). +const ( + URIScheme = "yellow" + DefaultNetwork = "ynet" +) + +// AccountType identifies the entity kind (ADR-007 §3). +type AccountType uint8 + +const ( + AccountTypeUnknown AccountType = 0x00 // Sentinel: URI kind not recognised (D-003, 2026-04-21). + AccountTypeUser AccountType = 0x01 // Externally owned + AccountTypeNative AccountType = 0x02 // On-cluster deterministic handlers (pools, vaults) + AccountTypeVirtual AccountType = 0x03 // Off-chain operator, NFT-identified + AccountTypeContract AccountType = 0x04 // User-deployed bytecode (future) +) + +// String returns the human-readable name of the AccountType. +func (t AccountType) String() string { + switch t { + case AccountTypeUnknown: + return "unknown" + case AccountTypeUser: + return "user" + case AccountTypeNative: + return "native" + case AccountTypeVirtual: + return "virtual" + case AccountTypeContract: + return "contract" + default: + return fmt.Sprintf("unknown(%d)", t) + } +} + +// --------------------------------------------------------------------------- +// URI constructors — build canonical yellow:// URIs +// +// ADR-007 §4: "All URI components are lowercase." Every constructor below +// normalises its inputs via strings.ToLower so the same logical address +// produces a byte-identical URI — and therefore a byte-identical +// AccountID — regardless of the case the caller supplied (EIP-55 +// checksummed, user wallet, etc.). D-002 (2026-04-21): resolution lives +// inside the constructor, not at the caller. +// --------------------------------------------------------------------------- + +// UserURI returns the canonical URI for a user account. +// +// yellow://ynet/user/0xd8da6bf26964af9d7eed9e03e53415d37aa96045 +func UserURI(address string) string { + return URIScheme + "://" + DefaultNetwork + "/user/" + strings.ToLower(address) +} + +// PoolURI returns the canonical URI for a liquidity pool (ADR-007 §4). +// All pools are YELLOW-quoted (ADR-006), so the URI contains only the +// lowercase base asset. Example: yellow://ynet/pool/eth +func PoolURI(base string) string { + return URIScheme + "://" + DefaultNetwork + "/pool/" + strings.ToLower(base) +} + +// TreasuryURI returns the canonical URI for a protocol treasury (ADR-010 §1). +// The name is drawn from the compile-time ValidTreasuries allowlist. Initial +// set: `emission`. Example: yellow://ynet/treasury/emission. +func TreasuryURI(name string) string { + return URIScheme + "://" + DefaultNetwork + "/treasury/" + strings.ToLower(name) +} + +// NodeURI returns the canonical URI for a validator node account. +// +// yellow://ynet/node/<64-char lowercase hex nodeid> +func NodeURI(id NodeID) string { + return URIScheme + "://" + DefaultNetwork + "/node/" + hex.EncodeToString(id[:]) +} + +// URIToAddress maps a canonical yellow:// URI into the 20-byte address slot +// used by EIP-712 schemas that cannot carry a string recipient. The mapping is +// the first 20 bytes of keccak256(uri), paralleling the pool-anchor preimage +// rule while fitting the Transfer(address to, ...) type. +func URIToAddress(uri string) (common.Address, error) { + if err := ValidateURI(uri); err != nil { + return common.Address{}, err + } + hash := crypto.Keccak256([]byte(uri)) + return common.BytesToAddress(hash[:20]), nil +} + +// ServiceURI returns the canonical URI for a named service. +// +// yellow://ynet// +func ServiceURI(kind, path string) string { + return URIScheme + "://" + DefaultNetwork + "/" + strings.ToLower(kind) + "/" + strings.ToLower(path) +} + +// --------------------------------------------------------------------------- +// URI parser +// --------------------------------------------------------------------------- + +// ParseURI splits a canonical URI into its components. +// +// "yellow://ynet/pool/eth" → ("ynet", "pool", "eth", nil) +func ParseURI(uri string) (network, kind, path string, err error) { + prefix := URIScheme + "://" + if !strings.HasPrefix(uri, prefix) { + return "", "", "", fmt.Errorf("invalid URI scheme: %q (expected %s://)", uri, URIScheme) + } + rest := uri[len(prefix):] + + // Split: network/kind/path... + parts := strings.SplitN(rest, "/", 3) + if len(parts) < 2 { + return "", "", "", fmt.Errorf("invalid URI: %q (expected yellow://network/kind/path)", uri) + } + + network = parts[0] + kind = parts[1] + if len(parts) > 2 { + path = parts[2] + } + + if network == "" || kind == "" { + return "", "", "", fmt.Errorf("invalid URI: %q (empty network or kind)", uri) + } + + return network, kind, path, nil +} + +// KindToAccountType maps a URI kind segment to its AccountType (ADR-007 §3). +// Returns an error on unknown kinds — this was previously a silent +// User-default which masked malformed URIs (D-003 resolution, +// 2026-04-21). +// +// The returned AccountType on error is AccountTypeUnknown (0), so code +// that ignores the error still surfaces an obviously-bogus type rather +// than a plausible User account. +func KindToAccountType(kind string) (AccountType, error) { + switch kind { + case "user": + return AccountTypeUser, nil + case "pool", "treasury", "node": + return AccountTypeNative, nil + case "dax", "game", "relay": + return AccountTypeVirtual, nil + case "contract": + return AccountTypeContract, nil + default: + return AccountTypeUnknown, fmt.Errorf("unknown URI kind %q (ADR-007 §3 as amended by ADR-010 §8 enumerates: user, pool, treasury, node, dax, game, relay, contract)", kind) + } +} + +// ValidateURI enforces ADR-007 §4 URI conventions: +// +// 1. The URI uses the yellow:// scheme. +// 2. Parsing yields a non-empty network and kind. +// 3. The network matches DefaultNetwork ("ynet") — multi-network +// support is a future extension; every URI in-flight today lives +// on ynet. +// 4. The kind is one of the enumerated ADR-007 §3 values. +// 5. Every URI component is lowercase (ADR-007 §4: "All URI +// components are lowercase"). +// +// Returns a descriptive error on the first violation; nil on success. +// Callers at system boundaries (gateway request parsing, store loads, +// manifest reads) should call ValidateURI on any URI they receive +// from an external source before trusting it. Internal constructors +// (UserURI, PoolURI, etc.) produce canonical URIs by construction +// and do not need re-validation at their call sites. +func ValidateURI(uri string) error { + if !IsURI(uri) { + return fmt.Errorf("URI missing %s:// scheme: %q", URIScheme, uri) + } + if uri != strings.ToLower(uri) { + return fmt.Errorf("URI contains uppercase characters (ADR-007 §4 requires lowercase): %q", uri) + } + network, kind, path, err := ParseURI(uri) + if err != nil { + return fmt.Errorf("URI parse: %w", err) + } + if network != DefaultNetwork { + return fmt.Errorf("URI network %q unsupported (expected %q): %q", network, DefaultNetwork, uri) + } + if _, err := KindToAccountType(kind); err != nil { + return fmt.Errorf("URI kind check: %w", err) + } + // ADR-010 §1: treasury names are drawn from a compile-time allowlist. + // A URI parsing as kind=treasury but with a name outside ValidTreasuries + // MUST be rejected by every account-materialization path. + if kind == "treasury" { + if !IsValidTreasury(path) { + return fmt.Errorf("URI treasury name %q not in compile-time allowlist (ADR-010 §1): %q", path, uri) + } + } + if kind == "node" && !isCanonicalNodeIDPath(path) { + return fmt.Errorf("URI node id must be exactly 64 lowercase hex characters without 0x prefix: %q", uri) + } + return nil +} + +func isCanonicalNodeIDPath(path string) bool { + if len(path) != 64 { + return false + } + for _, c := range path { + if (c < '0' || c > '9') && (c < 'a' || c > 'f') { + return false + } + } + return true +} + +// IsURI returns true if the string looks like a yellow:// URI. +func IsURI(s string) bool { + return strings.HasPrefix(s, URIScheme+"://") +} + +// URIKind extracts the kind segment from a URI or address. +// For non-URI inputs (raw hex addresses), returns "user". +// Returns "" if parsing fails on an actual URI. +func URIKind(uriOrAddress string) string { + if !IsURI(uriOrAddress) { + return "user" + } + _, kind, _, err := ParseURI(uriOrAddress) + if err != nil { + return "" + } + return kind +} + +// ComputeAccountID derives the canonical 32-byte AccountID from an account +// URI or raw address (ADR-007 §4). The input is normalized to a canonical +// lowercase before hashing, so a caller that accidentally hands in a +// non-canonical form still produces the canonical AccountID. That +// covers two footguns at once: +// +// 1. Non-URI heterogeneous protocol fields such as BlockEntry.Account +// (which may carry either a canonical URI for service/pool accounts +// or a raw user address for user-owned accounts). Non-URI input is +// auto-wrapped in UserURI() first. +// 2. Mixed-case URI input (e.g. a URI produced by an external tool +// that didn't enforce the rule). Post-lowercasing eliminates the +// divergence before the hash is taken. +func ComputeAccountID(uri string) [32]byte { + if !IsURI(uri) { + uri = UserURI(uri) + } + return crypto.Keccak256Hash([]byte(strings.ToLower(uri))) +} + +// ValidTreasuries is the compile-time allowlist of protocol-treasury names +// (ADR-010 §1). Treasuries are consensus-critical state-machine inputs, not +// operator configuration: declaring them in the network manifest would diverge +// SMT roots across operators the first time distribution fires. The set is +// therefore hardcoded here. Adding a new treasury requires a follow-up ADR, +// an entry in this list, a registered minimum balance (ADR-010 §6), and an +// authorized ledger helper for its outbound flow (ADR-010 §3). +var ValidTreasuries = []string{"emission"} + +// IsValidTreasury reports whether a treasury name is in the compile-time +// allowlist. A URI that parses as kind=`treasury` but whose name is absent +// from this list MUST be rejected by every account-materialization path +// (ADR-010 §1). +func IsValidTreasury(name string) bool { + for _, t := range ValidTreasuries { + if t == name { + return true + } + } + return false +} diff --git a/pkg/decimal/cbor.go b/pkg/decimal/cbor.go new file mode 100644 index 0000000..add5939 --- /dev/null +++ b/pkg/decimal/cbor.go @@ -0,0 +1,231 @@ +// Package decimal — CBOR adapter methods for decimal.Decimal. +// +// This file attaches MarshalCBOR / UnmarshalCBOR to decimal.Decimal so that +// cbor-gen (which emits `v.MarshalCBOR(cw)` for every field of a struct whose +// type implements cbg.CBORMarshaler / cbg.CBORUnmarshaler) can serialize +// Decimal fields through the canonical tag-4 encoding pinned by ADR-009 §3. +// +// Why not delegate to internal/cborx.Decimal? +// +// internal/cborx.Decimal already wraps decimal.Decimal with a tag-4 codec +// (see internal/cborx/decimal.go), but internal/cborx imports +// internal/decimal — delegating from decimal back to cborx would close an +// import cycle. So this file re-implements the tag-4 byte layout using only +// cbor-gen primitives (cbg.WriteMajorTypeHeader / cbg.CborReadHeader) and +// stdlib. The byte output is byte-identical to cborx.Decimal and is covered +// by internal/cborx's round-trip and golden tests, plus by the round-trip +// tests attached to every clearing/core codec that carries a Decimal field. +// +// Encoding (RFC 8949 §3.4.4): +// +// - major type 6 (tag) with value 4, +// - wrapping a definite-length array of exactly two elements, +// - element 0: exponent (int32 always fits in CBOR's native integer +// range), shortest-form signed integer, +// - element 1: mantissa as a tag-2 / tag-3 bignum (RFC 8949 §3.4.3). +// +// Owned by the CBOR encoding migration, Wave 2-core +// (docs/plans/cbor-encoding.md §15.10). Never hand-edited after this +// landing except to track a change in decimal.Decimal's API. +package decimal + +import ( + "errors" + "fmt" + "io" + "math" + "math/big" + + cbg "github.com/whyrusleeping/cbor-gen" // layer-guard: allow +) + +// tag constants mirror internal/cborx (kept local to avoid the import cycle +// with that package; the byte-level output is identical — see the package +// doc). +const ( + tagDecimalFraction uint64 = 4 + tagUnsignedBignum uint64 = 2 + tagNegativeBignum uint64 = 3 +) + +// MarshalCBOR writes d as a canonical RFC 8949 tag-4 decimal fraction. +// The exponent (int32) is written shortest-form; the mantissa is written +// as a tag-2/tag-3 bignum so the sign rides on the tag. +// +// Value receiver — cbor-gen's tuple emitter calls `t.Field.MarshalCBOR(cw)` +// on field values; Decimal is a small by-value type, so a value receiver +// is both idiomatic and sufficient. +func (d Decimal) MarshalCBOR(w io.Writer) error { + if err := cbg.WriteMajorTypeHeader(w, cbg.MajTag, tagDecimalFraction); err != nil { + return fmt.Errorf("decimal: MarshalCBOR: write tag: %w", err) + } + if err := cbg.WriteMajorTypeHeader(w, cbg.MajArray, 2); err != nil { + return fmt.Errorf("decimal: MarshalCBOR: write array header: %w", err) + } + + exp := int64(d.Exponent()) + if exp >= 0 { + if err := cbg.WriteMajorTypeHeader(w, cbg.MajUnsignedInt, uint64(exp)); err != nil { + return fmt.Errorf("decimal: MarshalCBOR: write exponent: %w", err) + } + } else { + if err := cbg.WriteMajorTypeHeader(w, cbg.MajNegativeInt, uint64(-exp)-1); err != nil { + return fmt.Errorf("decimal: MarshalCBOR: write negative exponent: %w", err) + } + } + + return writeBignum(w, d.Coefficient()) +} + +// UnmarshalCBOR decodes a tag-4 two-element array into d. Non-canonical +// encodings (wrong tag, wrong array length, indefinite-length containers, +// over-wide integer headers, non-canonical bignums) are rejected. +func (d *Decimal) UnmarshalCBOR(r io.Reader) error { + maj, val, err := cbg.CborReadHeader(r) + if err != nil { + return fmt.Errorf("decimal: UnmarshalCBOR: read tag: %w", err) + } + if maj != cbg.MajTag { + return fmt.Errorf("decimal: UnmarshalCBOR: expected tag (major 6), got major %d", maj) + } + if val != tagDecimalFraction { + return fmt.Errorf("decimal: UnmarshalCBOR: expected tag 4, got tag %d", val) + } + + maj, val, err = cbg.CborReadHeader(r) + if err != nil { + return fmt.Errorf("decimal: UnmarshalCBOR: read array header: %w", err) + } + if maj != cbg.MajArray { + return fmt.Errorf("decimal: UnmarshalCBOR: expected array (major 4), got major %d", maj) + } + if val != 2 { + return fmt.Errorf("decimal: UnmarshalCBOR: expected 2 elements, got %d", val) + } + + maj, val, err = cbg.CborReadHeader(r) + if err != nil { + return fmt.Errorf("decimal: UnmarshalCBOR: read exponent: %w", err) + } + var expI int64 + switch maj { + case cbg.MajUnsignedInt: + if val > math.MaxInt32 { + return fmt.Errorf("decimal: UnmarshalCBOR: exponent %d overflows int32", val) + } + expI = int64(val) + case cbg.MajNegativeInt: + if val > math.MaxInt32 { + return errors.New("decimal: UnmarshalCBOR: negative exponent overflows int32") + } + expI = -int64(val) - 1 + default: + return fmt.Errorf("decimal: UnmarshalCBOR: exponent must be integer, got major %d", maj) + } + if expI < math.MinInt32 || expI > math.MaxInt32 { + return fmt.Errorf("decimal: UnmarshalCBOR: exponent %d out of int32 range", expI) + } + + mant, err := readBignum(r) + if err != nil { + return fmt.Errorf("decimal: UnmarshalCBOR: mantissa: %w", err) + } + if mant == nil { + return errors.New("decimal: UnmarshalCBOR: mantissa decoded to nil big.Int") + } + + *d = NewFromBigInt(mant, int32(expI)) + return nil +} + +// writeBignum writes a *big.Int as an RFC 8949 tag-2 / tag-3 bignum. +// Non-negative values use tag 2; negative values use tag 3 wrapping the +// magnitude of (-1 - n). Zero is canonical tag-2 + empty byte string. +func writeBignum(w io.Writer, n *big.Int) error { + if n == nil { + n = new(big.Int) + } + var ( + tag uint64 + mag []byte + sign = n.Sign() + ) + switch { + case sign >= 0: + tag = tagUnsignedBignum + mag = n.Bytes() + default: + tag = tagNegativeBignum + neg := new(big.Int).Neg(n) + neg.Sub(neg, big.NewInt(1)) + mag = neg.Bytes() + } + if err := cbg.WriteMajorTypeHeader(w, cbg.MajTag, tag); err != nil { + return fmt.Errorf("decimal: writeBignum: write tag: %w", err) + } + if err := cbg.WriteMajorTypeHeader(w, cbg.MajByteString, uint64(len(mag))); err != nil { + return fmt.Errorf("decimal: writeBignum: write length: %w", err) + } + if len(mag) > 0 { + if _, err := w.Write(mag); err != nil { + return fmt.Errorf("decimal: writeBignum: write bytes: %w", err) + } + } + return nil +} + +// readBignum decodes a tag-2 / tag-3 bignum, enforcing the canonical-zero +// and canonical-leading-byte rules of RFC 8949 §4.2. +func readBignum(r io.Reader) (*big.Int, error) { + maj, val, err := cbg.CborReadHeader(r) + if err != nil { + return nil, fmt.Errorf("decimal: readBignum: read tag: %w", err) + } + if maj != cbg.MajTag { + return nil, fmt.Errorf("decimal: readBignum: expected tag (major 6), got major %d", maj) + } + tag := val + + maj, val, err = cbg.CborReadHeader(r) + if err != nil { + return nil, fmt.Errorf("decimal: readBignum: read length: %w", err) + } + if maj != cbg.MajByteString { + return nil, fmt.Errorf("decimal: readBignum: expected byte string (major 2), got major %d", maj) + } + length := val + + if length > 1<<20 { + return nil, fmt.Errorf("decimal: readBignum: refusing length %d (> 1 MiB)", length) + } + + var buf []byte + if length > 0 { + buf = make([]byte, length) + if _, err := io.ReadFull(r, buf); err != nil { + return nil, fmt.Errorf("decimal: readBignum: read bytes: %w", err) + } + if buf[0] == 0 { + return nil, errors.New("decimal: readBignum: non-canonical leading zero byte") + } + } + + switch tag { + case tagUnsignedBignum: + n := new(big.Int) + if length > 0 { + n.SetBytes(buf) + } + return n, nil + case tagNegativeBignum: + if length == 0 { + return big.NewInt(-1), nil + } + mag := new(big.Int).SetBytes(buf) + n := new(big.Int).Neg(mag) + n.Sub(n, big.NewInt(1)) + return n, nil + default: + return nil, fmt.Errorf("decimal: readBignum: expected tag 2 or 3, got tag %d", tag) + } +} diff --git a/pkg/decimal/const.go b/pkg/decimal/const.go new file mode 100644 index 0000000..e5d6fa8 --- /dev/null +++ b/pkg/decimal/const.go @@ -0,0 +1,63 @@ +package decimal + +import ( + "strings" +) + +const ( + strLn10 = "2.302585092994045684017991454684364207601101488628772976033327900967572609677352480235997205089598298341967784042286248633409525465082806756666287369098781689482907208325554680843799894826233198528393505308965377732628846163366222287698219886746543667474404243274365155048934314939391479619404400222105101714174800368808401264708068556774321622835522011480466371565912137345074785694768346361679210180644507064800027750268491674655058685693567342067058113642922455440575892572420824131469568901675894025677631135691929203337658714166023010570308963457207544037084746994016826928280848118428931484852494864487192780967627127577539702766860595249671667418348570442250719796500471495105049221477656763693866297697952211071826454973477266242570942932258279850258550978526538320760672631716430950599508780752371033310119785754733154142180842754386359177811705430982748238504564801909561029929182431823752535770975053956518769751037497088869218020518933950723853920514463419726528728696511086257149219884997874887377134568620916705849807828059751193854445009978131146915934666241071846692310107598438319191292230792503747298650929009880391941702654416816335727555703151596113564846546190897042819763365836983716328982174407366009162177850541779276367731145041782137660111010731042397832521894898817597921798666394319523936855916447118246753245630912528778330963604262982153040874560927760726641354787576616262926568298704957954913954918049209069438580790032763017941503117866862092408537949861264933479354871737451675809537088281067452440105892444976479686075120275724181874989395971643105518848195288330746699317814634930000321200327765654130472621883970596794457943468343218395304414844803701305753674262153675579814770458031413637793236291560128185336498466942261465206459942072917119370602444929358037007718981097362533224548366988505528285966192805098447175198503666680874970496982273220244823343097169111136813588418696549323714996941979687803008850408979618598756579894836445212043698216415292987811742973332588607915912510967187510929248475023930572665446276200923068791518135803477701295593646298412366497023355174586195564772461857717369368404676577047874319780573853271810933883496338813069945569399346101090745616033312247949360455361849123333063704751724871276379140924398331810164737823379692265637682071706935846394531616949411701841938119405416449466111274712819705817783293841742231409930022911502362192186723337268385688273533371925103412930705632544426611429765388301822384091026198582888433587455960453004548370789052578473166283701953392231047527564998119228742789713715713228319641003422124210082180679525276689858180956119208391760721080919923461516952599099473782780648128058792731993893453415320185969711021407542282796298237068941764740642225757212455392526179373652434440560595336591539160312524480149313234572453879524389036839236450507881731359711238145323701508413491122324390927681724749607955799151363982881058285740538000653371655553014196332241918087621018204919492651483892" +) + +var ( + ln10 = newConstApproximation(strLn10) +) + +type constApproximation struct { + exact Decimal + approximations []Decimal +} + +func newConstApproximation(value string) constApproximation { + parts := strings.Split(value, ".") + coeff, fractional := parts[0], parts[1] + + coeffLen := len(coeff) + maxPrecision := len(fractional) + + var approximations []Decimal + for p := 1; p < maxPrecision; p *= 2 { + r := RequireFromString(value[:coeffLen+p]) + approximations = append(approximations, r) + } + + return constApproximation{ + RequireFromString(value), + approximations, + } +} + +// Returns the smallest approximation available that's at least as precise +// as the passed precision (places after decimal point), i.e. Floor[ log2(precision) ] + 1 +func (c constApproximation) withPrecision(precision int32) Decimal { + i := 0 + + if precision >= 1 { + i++ + } + + for precision >= 16 { + precision /= 16 + i += 4 + } + + for precision >= 2 { + precision /= 2 + i++ + } + + if i >= len(c.approximations) { + return c.exact + } + + return c.approximations[i] +} diff --git a/pkg/decimal/const_test.go b/pkg/decimal/const_test.go new file mode 100644 index 0000000..f6a45fe --- /dev/null +++ b/pkg/decimal/const_test.go @@ -0,0 +1,39 @@ +package decimal + +import "testing" + +func TestConstApproximation(t *testing.T) { + for _, testCase := range []struct { + Const string + Precision int32 + ExpectedApproximation string + }{ + {"2.3025850929940456840179914546", 0, "2"}, + {"2.3025850929940456840179914546", 1, "2.3"}, + {"2.3025850929940456840179914546", 3, "2.302"}, + {"2.3025850929940456840179914546", 5, "2.302585"}, + {"2.3025850929940456840179914546", 10, "2.302585092994045"}, + {"2.3025850929940456840179914546", 15, "2.302585092994045"}, + {"2.3025850929940456840179914546", 16, "2.3025850929940456840179914546"}, + {"2.3025850929940456840179914546", 100, "2.3025850929940456840179914546"}, + {"2.3025850929940456840179914546", -1, "2"}, + {"2.3025850929940456840179914546", -5, "2"}, + {"-2.3025850929940456840179914546", 0, "-2"}, + {"-2.3025850929940456840179914546", 3, "-2.302"}, + {"-2.3025850929940456840179914546", 16, "-2.3025850929940456840179914546"}, + {"3.14159265359", 0, "3"}, + {"3.14159265359", 1, "3.1"}, + {"3.14159265359", 2, "3.141"}, + {"3.14159265359", 4, "3.1415926"}, + {"3.14159265359", 13, "3.14159265359"}, + } { + ca := newConstApproximation(testCase.Const) + expected, _ := NewFromString(testCase.ExpectedApproximation) + + approximation := ca.withPrecision(testCase.Precision) + + if approximation.Cmp(expected) != 0 { + t.Errorf("expected approximation %s, got %s - for const with %s precision %d", testCase.ExpectedApproximation, approximation.String(), testCase.Const, testCase.Precision) + } + } +} diff --git a/pkg/decimal/decimal-go.go b/pkg/decimal/decimal-go.go new file mode 100644 index 0000000..9958d69 --- /dev/null +++ b/pkg/decimal/decimal-go.go @@ -0,0 +1,415 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Multiprecision decimal numbers. +// For floating-point formatting only; not general purpose. +// Only operations are assign and (binary) left/right shift. +// Can do binary floating point in multiprecision decimal precisely +// because 2 divides 10; cannot do decimal floating point +// in multiprecision binary precisely. + +package decimal + +type decimal struct { + d [800]byte // digits, big-endian representation + nd int // number of digits used + dp int // decimal point + neg bool // negative flag + trunc bool // discarded nonzero digits beyond d[:nd] +} + +func (a *decimal) String() string { + n := 10 + a.nd + if a.dp > 0 { + n += a.dp + } + if a.dp < 0 { + n += -a.dp + } + + buf := make([]byte, n) + w := 0 + switch { + case a.nd == 0: + return "0" + + case a.dp <= 0: + // zeros fill space between decimal point and digits + buf[w] = '0' + w++ + buf[w] = '.' + w++ + w += digitZero(buf[w : w+-a.dp]) + w += copy(buf[w:], a.d[0:a.nd]) + + case a.dp < a.nd: + // decimal point in middle of digits + w += copy(buf[w:], a.d[0:a.dp]) + buf[w] = '.' + w++ + w += copy(buf[w:], a.d[a.dp:a.nd]) + + default: + // zeros fill space between digits and decimal point + w += copy(buf[w:], a.d[0:a.nd]) + w += digitZero(buf[w : w+a.dp-a.nd]) + } + return string(buf[0:w]) +} + +func digitZero(dst []byte) int { + for i := range dst { + dst[i] = '0' + } + return len(dst) +} + +// trim trailing zeros from number. +// (They are meaningless; the decimal point is tracked +// independent of the number of digits.) +func trim(a *decimal) { + for a.nd > 0 && a.d[a.nd-1] == '0' { + a.nd-- + } + if a.nd == 0 { + a.dp = 0 + } +} + +// Assign v to a. +func (a *decimal) Assign(v uint64) { + var buf [24]byte + + // Write reversed decimal in buf. + n := 0 + for v > 0 { + v1 := v / 10 + v -= 10 * v1 + buf[n] = byte(v + '0') + n++ + v = v1 + } + + // Reverse again to produce forward decimal in a.d. + a.nd = 0 + for n--; n >= 0; n-- { + a.d[a.nd] = buf[n] + a.nd++ + } + a.dp = a.nd + trim(a) +} + +// Maximum shift that we can do in one pass without overflow. +// A uint has 32 or 64 bits, and we have to be able to accommodate 9<> 63) +const maxShift = uintSize - 4 + +// Binary shift right (/ 2) by k bits. k <= maxShift to avoid overflow. +func rightShift(a *decimal, k uint) { + r := 0 // read pointer + w := 0 // write pointer + + // Pick up enough leading digits to cover first shift. + var n uint + for ; n>>k == 0; r++ { + if r >= a.nd { + if n == 0 { + // a == 0; shouldn't get here, but handle anyway. + a.nd = 0 + return + } + for n>>k == 0 { + n = n * 10 + r++ + } + break + } + c := uint(a.d[r]) + n = n*10 + c - '0' + } + a.dp -= r - 1 + + var mask uint = (1 << k) - 1 + + // Pick up a digit, put down a digit. + for ; r < a.nd; r++ { + c := uint(a.d[r]) + dig := n >> k + n &= mask + a.d[w] = byte(dig + '0') + w++ + n = n*10 + c - '0' + } + + // Put down extra digits. + for n > 0 { + dig := n >> k + n &= mask + if w < len(a.d) { + a.d[w] = byte(dig + '0') + w++ + } else if dig > 0 { + a.trunc = true + } + n = n * 10 + } + + a.nd = w + trim(a) +} + +// Cheat sheet for left shift: table indexed by shift count giving +// number of new digits that will be introduced by that shift. +// +// For example, leftcheats[4] = {2, "625"}. That means that +// if we are shifting by 4 (multiplying by 16), it will add 2 digits +// when the string prefix is "625" through "999", and one fewer digit +// if the string prefix is "000" through "624". +// +// Credit for this trick goes to Ken. + +type leftCheat struct { + delta int // number of new digits + cutoff string // minus one digit if original < a. +} + +var leftcheats = []leftCheat{ + // Leading digits of 1/2^i = 5^i. + // 5^23 is not an exact 64-bit floating point number, + // so have to use bc for the math. + // Go up to 60 to be large enough for 32bit and 64bit platforms. + /* + seq 60 | sed 's/^/5^/' | bc | + awk 'BEGIN{ print "\t{ 0, \"\" }," } + { + log2 = log(2)/log(10) + printf("\t{ %d, \"%s\" },\t// * %d\n", + int(log2*NR+1), $0, 2**NR) + }' + */ + {0, ""}, + {1, "5"}, // * 2 + {1, "25"}, // * 4 + {1, "125"}, // * 8 + {2, "625"}, // * 16 + {2, "3125"}, // * 32 + {2, "15625"}, // * 64 + {3, "78125"}, // * 128 + {3, "390625"}, // * 256 + {3, "1953125"}, // * 512 + {4, "9765625"}, // * 1024 + {4, "48828125"}, // * 2048 + {4, "244140625"}, // * 4096 + {4, "1220703125"}, // * 8192 + {5, "6103515625"}, // * 16384 + {5, "30517578125"}, // * 32768 + {5, "152587890625"}, // * 65536 + {6, "762939453125"}, // * 131072 + {6, "3814697265625"}, // * 262144 + {6, "19073486328125"}, // * 524288 + {7, "95367431640625"}, // * 1048576 + {7, "476837158203125"}, // * 2097152 + {7, "2384185791015625"}, // * 4194304 + {7, "11920928955078125"}, // * 8388608 + {8, "59604644775390625"}, // * 16777216 + {8, "298023223876953125"}, // * 33554432 + {8, "1490116119384765625"}, // * 67108864 + {9, "7450580596923828125"}, // * 134217728 + {9, "37252902984619140625"}, // * 268435456 + {9, "186264514923095703125"}, // * 536870912 + {10, "931322574615478515625"}, // * 1073741824 + {10, "4656612873077392578125"}, // * 2147483648 + {10, "23283064365386962890625"}, // * 4294967296 + {10, "116415321826934814453125"}, // * 8589934592 + {11, "582076609134674072265625"}, // * 17179869184 + {11, "2910383045673370361328125"}, // * 34359738368 + {11, "14551915228366851806640625"}, // * 68719476736 + {12, "72759576141834259033203125"}, // * 137438953472 + {12, "363797880709171295166015625"}, // * 274877906944 + {12, "1818989403545856475830078125"}, // * 549755813888 + {13, "9094947017729282379150390625"}, // * 1099511627776 + {13, "45474735088646411895751953125"}, // * 2199023255552 + {13, "227373675443232059478759765625"}, // * 4398046511104 + {13, "1136868377216160297393798828125"}, // * 8796093022208 + {14, "5684341886080801486968994140625"}, // * 17592186044416 + {14, "28421709430404007434844970703125"}, // * 35184372088832 + {14, "142108547152020037174224853515625"}, // * 70368744177664 + {15, "710542735760100185871124267578125"}, // * 140737488355328 + {15, "3552713678800500929355621337890625"}, // * 281474976710656 + {15, "17763568394002504646778106689453125"}, // * 562949953421312 + {16, "88817841970012523233890533447265625"}, // * 1125899906842624 + {16, "444089209850062616169452667236328125"}, // * 2251799813685248 + {16, "2220446049250313080847263336181640625"}, // * 4503599627370496 + {16, "11102230246251565404236316680908203125"}, // * 9007199254740992 + {17, "55511151231257827021181583404541015625"}, // * 18014398509481984 + {17, "277555756156289135105907917022705078125"}, // * 36028797018963968 + {17, "1387778780781445675529539585113525390625"}, // * 72057594037927936 + {18, "6938893903907228377647697925567626953125"}, // * 144115188075855872 + {18, "34694469519536141888238489627838134765625"}, // * 288230376151711744 + {18, "173472347597680709441192448139190673828125"}, // * 576460752303423488 + {19, "867361737988403547205962240695953369140625"}, // * 1152921504606846976 +} + +// Is the leading prefix of b lexicographically less than s? +func prefixIsLessThan(b []byte, s string) bool { + for i := 0; i < len(s); i++ { + if i >= len(b) { + return true + } + if b[i] != s[i] { + return b[i] < s[i] + } + } + return false +} + +// Binary shift left (* 2) by k bits. k <= maxShift to avoid overflow. +func leftShift(a *decimal, k uint) { + delta := leftcheats[k].delta + if prefixIsLessThan(a.d[0:a.nd], leftcheats[k].cutoff) { + delta-- + } + + r := a.nd // read index + w := a.nd + delta // write index + + // Pick up a digit, put down a digit. + var n uint + for r--; r >= 0; r-- { + n += (uint(a.d[r]) - '0') << k + quo := n / 10 + rem := n - 10*quo + w-- + if w < len(a.d) { + a.d[w] = byte(rem + '0') + } else if rem != 0 { + a.trunc = true + } + n = quo + } + + // Put down extra digits. + for n > 0 { + quo := n / 10 + rem := n - 10*quo + w-- + if w < len(a.d) { + a.d[w] = byte(rem + '0') + } else if rem != 0 { + a.trunc = true + } + n = quo + } + + a.nd += delta + if a.nd >= len(a.d) { + a.nd = len(a.d) + } + a.dp += delta + trim(a) +} + +// Binary shift left (k > 0) or right (k < 0). +func (a *decimal) Shift(k int) { + switch { + case a.nd == 0: + // nothing to do: a == 0 + case k > 0: + for k > maxShift { + leftShift(a, maxShift) + k -= maxShift + } + leftShift(a, uint(k)) + case k < 0: + for k < -maxShift { + rightShift(a, maxShift) + k += maxShift + } + rightShift(a, uint(-k)) + } +} + +// If we chop a at nd digits, should we round up? +func shouldRoundUp(a *decimal, nd int) bool { + if nd < 0 || nd >= a.nd { + return false + } + if a.d[nd] == '5' && nd+1 == a.nd { // exactly halfway - round to even + // if we truncated, a little higher than what's recorded - always round up + if a.trunc { + return true + } + return nd > 0 && (a.d[nd-1]-'0')%2 != 0 + } + // not halfway - digit tells all + return a.d[nd] >= '5' +} + +// Round a to nd digits (or fewer). +// If nd is zero, it means we're rounding +// just to the left of the digits, as in +// 0.09 -> 0.1. +func (a *decimal) Round(nd int) { + if nd < 0 || nd >= a.nd { + return + } + if shouldRoundUp(a, nd) { + a.RoundUp(nd) + } else { + a.RoundDown(nd) + } +} + +// Round a down to nd digits (or fewer). +func (a *decimal) RoundDown(nd int) { + if nd < 0 || nd >= a.nd { + return + } + a.nd = nd + trim(a) +} + +// Round a up to nd digits (or fewer). +func (a *decimal) RoundUp(nd int) { + if nd < 0 || nd >= a.nd { + return + } + + // round up + for i := nd - 1; i >= 0; i-- { + c := a.d[i] + if c < '9' { // can stop after this digit + a.d[i]++ + a.nd = i + 1 + return + } + } + + // Number is all 9s. + // Change to single 1 with adjusted decimal point. + a.d[0] = '1' + a.nd = 1 + a.dp++ +} + +// Extract integer part, rounded appropriately. +// No guarantees about overflow. +func (a *decimal) RoundedInteger() uint64 { + if a.dp > 20 { + return 0xFFFFFFFFFFFFFFFF + } + var i int + n := uint64(0) + for i = 0; i < a.dp && i < a.nd; i++ { + n = n*10 + uint64(a.d[i]-'0') + } + for ; i < a.dp; i++ { + n *= 10 + } + if shouldRoundUp(a, a.dp) { + n++ + } + return n +} diff --git a/pkg/decimal/decimal.go b/pkg/decimal/decimal.go new file mode 100644 index 0000000..2704ca3 --- /dev/null +++ b/pkg/decimal/decimal.go @@ -0,0 +1,1808 @@ +// Package decimal implements an arbitrary precision fixed-point decimal. +// +// The zero-value of a Decimal is 0, as you would expect. +// +// The best way to create a new Decimal is to use decimal.NewFromString, ex: +// +// n, err := decimal.NewFromString("-123.4567") +// n.String() // output: "-123.4567" +// +// To use Decimal as part of a struct: +// +// type StructName struct { +// Number Decimal +// } +// +// Note: This can "only" represent numbers with a maximum of 2^31 digits after the decimal point. +package decimal + +import ( + "database/sql/driver" + "encoding/binary" + "fmt" + "math" + "math/big" + "strconv" + "strings" +) + +// DivisionPrecision is the number of decimal places in the result when it +// doesn't divide exactly. +// +// Example: +// +// d1 := decimal.NewFromFloat(2).Div(decimal.NewFromFloat(3)) +// d1.String() // output: "0.6666666666666667" +// d2 := decimal.NewFromFloat(2).Div(decimal.NewFromFloat(30000)) +// d2.String() // output: "0.0000666666666667" +// d3 := decimal.NewFromFloat(20000).Div(decimal.NewFromFloat(3)) +// d3.String() // output: "6666.6666666666666667" +// decimal.DivisionPrecision = 3 +// d4 := decimal.NewFromFloat(2).Div(decimal.NewFromFloat(3)) +// d4.String() // output: "0.667" +var DivisionPrecision = 16 + +// PowPrecisionNegativeExponent specifies the maximum precision of the result (digits after decimal point) +// when calculating decimal power. Only used for cases where the exponent is a negative number. +// This constant applies to Pow, PowInt32 and PowBigInt methods, PowWithPrecision method is not constrained by it. +// +// Example: +// +// d1, err := decimal.NewFromFloat(15.2).PowInt32(-2) +// d1.String() // output: "0.0043282548476454" +// +// decimal.PowPrecisionNegativeExponent = 24 +// d2, err := decimal.NewFromFloat(15.2).PowInt32(-2) +// d2.String() // output: "0.004328254847645429362881" +var PowPrecisionNegativeExponent = 16 + +// MarshalJSONWithoutQuotes should be set to true if you want the decimal to +// be JSON marshaled as a number, instead of as a string. +// WARNING: this is dangerous for decimals with many digits, since many JSON +// unmarshallers (ex: Javascript's) will unmarshal JSON numbers to IEEE 754 +// double-precision floating point numbers, which means you can potentially +// silently lose precision. +var MarshalJSONWithoutQuotes = false + +// TrimTrailingZeros specifies whether trailing zeroes should be trimmed from a string representation of decimal. +// If set to true, trailing zeroes will be truncated (2.00 -> 2, 3.11 -> 3.11, 13.000 -> 13), +// otherwise trailing zeroes will be preserved (2.00 -> 2.00, 3.11 -> 3.11, 13.000 -> 13.000). +// Setting this value to false can be useful for APIs where exact decimal string representation matters. +var TrimTrailingZeros = true + +// UseScientificNotation specifies whether scientific notation should be used when a decimal is turned +// into a string that has a "negative" precision. +// +// For example, 1200 rounded to the nearest 100 cannot accurately be shown as "1200" because the last two +// digits are unknown. With this set to true, that number would be expressed as "1.2E3" instead. +var UseScientificNotation = false + +// Zero constant, to make computations faster. +// Zero should never be compared with == or != directly, please use decimal.Equal or decimal.Cmp instead. +var Zero = Decimal{} + +var zeroInt = big.NewInt(0) +var oneInt = big.NewInt(1) +var twoInt = big.NewInt(2) +var fiveInt = big.NewInt(5) +var tenInt = big.NewInt(10) + +// weiScale is 10^18, used by FromWei / ToWei. +var weiScale = new(big.Int).Exp(tenInt, big.NewInt(18), nil) + +// Decimal represents a fixed-point decimal. It is immutable. +// number = value * 10 ^ exp +type Decimal struct { + value *big.Int + + // NOTE(vadim): this must be an int32, because we cast it to float64 during + // calculations. If exp is 64 bit, we might lose precision. + // If we cared about being able to represent every possible decimal, we + // could make exp a *big.Int but it would hurt performance and numbers + // like that are unrealistic. + exp int32 +} + +func (d Decimal) getValue() *big.Int { + if d.value == nil { + return zeroInt + } + return d.value +} + +// New returns a new fixed-point decimal, value * 10 ^ exp. +func New(value int64, exp int32) Decimal { + return Decimal{ + value: big.NewInt(value), + exp: exp, + } +} + +// NewFromInt converts an int64 to Decimal. +// +// Example: +// +// NewFromInt(123).String() // output: "123" +// NewFromInt(-10).String() // output: "-10" +func NewFromInt(value int64) Decimal { + return Decimal{ + value: big.NewInt(value), + exp: 0, + } +} + +// NewFromInt32 converts an int32 to Decimal. +// +// Example: +// +// NewFromInt(123).String() // output: "123" +// NewFromInt(-10).String() // output: "-10" +func NewFromInt32(value int32) Decimal { + return Decimal{ + value: big.NewInt(int64(value)), + exp: 0, + } +} + +// NewFromUint64 converts an uint64 to Decimal. +// +// Example: +// +// NewFromUint64(123).String() // output: "123" +func NewFromUint64(value uint64) Decimal { + return Decimal{ + value: new(big.Int).SetUint64(value), + exp: 0, + } +} + +// NewFromBigInt returns a new Decimal from a big.Int, value * 10 ^ exp +func NewFromBigInt(value *big.Int, exp int32) Decimal { + return Decimal{ + value: new(big.Int).Set(value), + exp: exp, + } +} + +// NewFromBigRat returns a new Decimal from a big.Rat. The numerator and +// denominator are divided and rounded to the given precision. +// +// Example: +// +// d1 := NewFromBigRat(big.NewRat(0, 1), 0) // output: "0" +// d2 := NewFromBigRat(big.NewRat(4, 5), 1) // output: "0.8" +// d3 := NewFromBigRat(big.NewRat(1000, 3), 3) // output: "333.333" +// d4 := NewFromBigRat(big.NewRat(2, 7), 4) // output: "0.2857" +func NewFromBigRat(value *big.Rat, precision int32) Decimal { + return Decimal{ + value: new(big.Int).Set(value.Num()), + exp: 0, + }.DivRound(Decimal{ + value: new(big.Int).Set(value.Denom()), + exp: 0, + }, precision) +} + +// NewFromString returns a new Decimal from a string representation. +// Trailing zeroes are not trimmed. +// +// Example: +// +// d, err := NewFromString("-123.45") +// d2, err := NewFromString(".0001") +// d3, err := NewFromString("1.47000") +func NewFromString(value string) (Decimal, error) { + originalInput := value + var intString string + var exp int64 + + // Check if number is using scientific notation and find dots + eIndex := -1 + pIndex := -1 + for i, r := range value { + if r == 'E' || r == 'e' { + if eIndex > -1 { + return Decimal{}, fmt.Errorf("can't convert %s to decimal: multiple 'E' characters found", value) + } + eIndex = i + continue + } + + if r == '.' { + if pIndex > -1 { + return Decimal{}, fmt.Errorf("can't convert %s to decimal: too many .s", value) + } + pIndex = i + } + } + + if eIndex != -1 { + expInt, err := strconv.ParseInt(value[eIndex+1:], 10, 32) + if err != nil { + if e, ok := err.(*strconv.NumError); ok && e.Err == strconv.ErrRange { + return Decimal{}, fmt.Errorf("can't convert %s to decimal: fractional part too long", value) + } + return Decimal{}, fmt.Errorf("can't convert %s to decimal: exponent is not numeric", value) + } + value = value[:eIndex] + exp = expInt + } + + if pIndex == -1 { + // There is no decimal point, we can just parse the original string as + // an int + intString = value + } else { + if pIndex+1 < len(value) { + intString = value[:pIndex] + value[pIndex+1:] + } else { + intString = value[:pIndex] + } + expInt := -len(value[pIndex+1:]) + exp += int64(expInt) + } + + var dValue *big.Int + // strconv.ParseInt is faster than new(big.Int).SetString so this is just a shortcut for strings we know won't overflow + if len(intString) <= 18 { + parsed64, err := strconv.ParseInt(intString, 10, 64) + if err != nil { + return Decimal{}, fmt.Errorf("can't convert %s to decimal", value) + } + dValue = big.NewInt(parsed64) + } else { + dValue = new(big.Int) + _, ok := dValue.SetString(intString, 10) + if !ok { + return Decimal{}, fmt.Errorf("can't convert %s to decimal", value) + } + } + + if exp < math.MinInt32 || exp > math.MaxInt32 { + // NOTE(vadim): I doubt a string could realistically be this long + return Decimal{}, fmt.Errorf("can't convert %s to decimal: fractional part too long", originalInput) + } + + return Decimal{ + value: dValue, + exp: int32(exp), + }, nil +} + +// RequireFromString returns a new Decimal from a string representation +// or panics if NewFromString had returned an error. +// +// Example: +// +// d := RequireFromString("-123.45") +// d2 := RequireFromString(".0001") +func RequireFromString(value string) Decimal { + dec, err := NewFromString(value) + if err != nil { + panic(err) + } + return dec +} + +// NewFromFloat converts a float64 to Decimal. +// +// The converted number will contain the number of significant digits that can be +// represented in a float with reliable roundtrip. +// This is typically 15 digits, but may be more in some cases. +// See https://www.exploringbinary.com/decimal-precision-of-binary-floating-point-numbers/ for more information. +// +// For slightly faster conversion, use NewFromFloatWithExponent where you can specify the precision in absolute terms. +// +// NOTE: this will panic on NaN, +/-inf +func NewFromFloat(value float64) Decimal { + if value == 0 { + return New(0, 0) + } + return newFromFloat(value, math.Float64bits(value), &float64info) +} + +// NewFromFloat32 converts a float32 to Decimal. +// +// The converted number will contain the number of significant digits that can be +// represented in a float with reliable roundtrip. +// This is typically 6-8 digits depending on the input. +// See https://www.exploringbinary.com/decimal-precision-of-binary-floating-point-numbers/ for more information. +// +// For slightly faster conversion, use NewFromFloatWithExponent where you can specify the precision in absolute terms. +// +// NOTE: this will panic on NaN, +/-inf +func NewFromFloat32(value float32) Decimal { + if value == 0 { + return New(0, 0) + } + // XOR is workaround for https://github.com/golang/go/issues/26285 + a := math.Float32bits(value) ^ 0x80808080 + return newFromFloat(float64(value), uint64(a)^0x80808080, &float32info) +} + +func newFromFloat(val float64, bits uint64, flt *floatInfo) Decimal { + if math.IsNaN(val) || math.IsInf(val, 0) { + panic(fmt.Sprintf("Cannot create a Decimal from %v", val)) + } + exp := int(bits>>flt.mantbits) & (1<>(flt.expbits+flt.mantbits) != 0 + + roundShortest(&d, mant, exp, flt) + // If less than 19 digits, we can do calculation in an int64. + if d.nd < 19 { + tmp := int64(0) + m := int64(1) + for i := d.nd - 1; i >= 0; i-- { + tmp += m * int64(d.d[i]-'0') + m *= 10 + } + if d.neg { + tmp *= -1 + } + return Decimal{value: big.NewInt(tmp), exp: int32(d.dp) - int32(d.nd)} + } + dValue := new(big.Int) + dValue, ok := dValue.SetString(string(d.d[:d.nd]), 10) + if ok { + return Decimal{value: dValue, exp: int32(d.dp) - int32(d.nd)} + } + + return NewFromFloatWithExponent(val, int32(d.dp)-int32(d.nd)) +} + +// NewFromFloatWithExponent converts a float64 to Decimal, with an arbitrary +// number of fractional digits. +// +// Example: +// +// NewFromFloatWithExponent(123.456, -2).String() // output: "123.46" +func NewFromFloatWithExponent(value float64, exp int32) Decimal { + if math.IsNaN(value) || math.IsInf(value, 0) { + panic(fmt.Sprintf("Cannot create a Decimal from %v", value)) + } + + bits := math.Float64bits(value) + mant := bits & (1<<52 - 1) + exp2 := int32((bits >> 52) & (1<<11 - 1)) + sign := bits >> 63 + + if exp2 == 0 { + // specials + if mant == 0 { + return Decimal{} + } + // subnormal + exp2++ + } else { + // normal + mant |= 1 << 52 + } + + exp2 -= 1023 + 52 + + // normalizing base-2 values + for mant&1 == 0 { + mant = mant >> 1 + exp2++ + } + + // maximum number of fractional base-10 digits to represent 2^N exactly cannot be more than -N if N<0 + if exp < 0 && exp < exp2 { + if exp2 < 0 { + exp = exp2 + } else { + exp = 0 + } + } + + // representing 10^M * 2^N as 5^M * 2^(M+N) + exp2 -= exp + + temp := big.NewInt(1) + dMant := big.NewInt(int64(mant)) + + // applying 5^M + if exp > 0 { + temp = temp.SetInt64(int64(exp)) + temp = temp.Exp(fiveInt, temp, nil) + } else if exp < 0 { + temp = temp.SetInt64(-int64(exp)) + temp = temp.Exp(fiveInt, temp, nil) + dMant = dMant.Mul(dMant, temp) + temp = temp.SetUint64(1) + } + + // applying 2^(M+N) + if exp2 > 0 { + dMant = dMant.Lsh(dMant, uint(exp2)) + } else if exp2 < 0 { + temp = temp.Lsh(temp, uint(-exp2)) + } + + // rounding and downscaling + if exp > 0 || exp2 < 0 { + halfDown := new(big.Int).Rsh(temp, 1) + dMant = dMant.Add(dMant, halfDown) + dMant = dMant.Quo(dMant, temp) + } + + if sign == 1 { + dMant = dMant.Neg(dMant) + } + + return Decimal{ + value: dMant, + exp: exp, + } +} + +// Copy returns a copy of decimal with the same value and exponent, but a different pointer to value. +func (d Decimal) Copy() Decimal { + return Decimal{ + value: new(big.Int).Set(d.getValue()), + exp: d.exp, + } +} + +// rescale returns a rescaled version of the decimal. Returned +// decimal may be less precise if the given exponent is bigger +// than the initial exponent of the Decimal. +// NOTE: this will truncate, NOT round +// +// Example: +// +// d := New(12345, -4) +// d2 := d.rescale(-1) +// d3 := d2.rescale(-4) +// println(d1) +// println(d2) +// println(d3) +// +// Output: +// +// 1.2345 +// 1.2 +// 1.2000 +func (d Decimal) rescale(exp int32) Decimal { + if d.exp == exp { + return Decimal{ + new(big.Int).Set(d.getValue()), + d.exp, + } + } + + // NOTE(vadim): must convert exps to float64 before - to prevent overflow + diff := math.Abs(float64(exp) - float64(d.exp)) + value := new(big.Int).Set(d.getValue()) + + expScale := new(big.Int).Exp(tenInt, big.NewInt(int64(diff)), nil) + if exp > d.exp { + value = value.Quo(value, expScale) + } else if exp < d.exp { + value = value.Mul(value, expScale) + } + + return Decimal{ + value: value, + exp: exp, + } +} + +// Abs returns the absolute value of the decimal. +func (d Decimal) Abs() Decimal { + if !d.IsNegative() { + return d + } + d2Value := new(big.Int).Abs(d.getValue()) + return Decimal{ + value: d2Value, + exp: d.exp, + } +} + +// Add returns d + d2. +func (d Decimal) Add(d2 Decimal) Decimal { + rd, rd2 := RescalePair(d, d2) + + d3Value := new(big.Int).Add(rd.getValue(), rd2.getValue()) + return Decimal{ + value: d3Value, + exp: rd.exp, + } +} + +// Sub returns d - d2. +func (d Decimal) Sub(d2 Decimal) Decimal { + rd, rd2 := RescalePair(d, d2) + + d3Value := new(big.Int).Sub(rd.getValue(), rd2.getValue()) + return Decimal{ + value: d3Value, + exp: rd.exp, + } +} + +// Neg returns -d. +func (d Decimal) Neg() Decimal { + val := new(big.Int).Neg(d.getValue()) + return Decimal{ + value: val, + exp: d.exp, + } +} + +// Mul returns d * d2. +func (d Decimal) Mul(d2 Decimal) Decimal { + expInt64 := int64(d.exp) + int64(d2.exp) + if expInt64 > math.MaxInt32 || expInt64 < math.MinInt32 { + // NOTE(vadim): better to panic than give incorrect results, as + // Decimals are usually used for money + panic(fmt.Sprintf("exponent %v overflows an int32!", expInt64)) + } + + d3Value := new(big.Int).Mul(d.getValue(), d2.getValue()) + return Decimal{ + value: d3Value, + exp: int32(expInt64), + } +} + +// Shift shifts the decimal in base 10. +// It shifts left when shift is positive and right if shift is negative. +// In simpler terms, the given value for shift is added to the exponent +// of the decimal. +func (d Decimal) Shift(shift int32) Decimal { + expInt64 := int64(d.exp) + int64(shift) + if expInt64 > math.MaxInt32 || expInt64 < math.MinInt32 { + panic(fmt.Sprintf("exponent %v overflows an int32!", expInt64)) + } + return Decimal{ + value: new(big.Int).Set(d.getValue()), + exp: int32(expInt64), + } +} + +// Div returns d / d2. If it doesn't divide exactly, the result will have +// DivisionPrecision digits after the decimal point. +func (d Decimal) Div(d2 Decimal) Decimal { + return d.DivRound(d2, int32(DivisionPrecision)) +} + +// QuoRem does division with remainder +// d.QuoRem(d2,precision) returns quotient q and remainder r such that +// +// d = d2 * q + r, q an integer multiple of 10^(-precision) +// 0 <= r < abs(d2) * 10 ^(-precision) if d>=0 +// 0 >= r > -abs(d2) * 10 ^(-precision) if d<0 +// +// Note that precision<0 is allowed as input. +func (d Decimal) QuoRem(d2 Decimal, precision int32) (Decimal, Decimal) { + if d2.getValue().Sign() == 0 { + panic("decimal division by 0") + } + scale := -precision + e := int64(d.exp) - int64(d2.exp) - int64(scale) + if e > math.MaxInt32 || e < math.MinInt32 { + panic("overflow in decimal QuoRem") + } + var aa, bb, expo big.Int + var scalerest int32 + // d = a 10^ea + // d2 = b 10^eb + if e < 0 { + aa = *d.getValue() + expo.SetInt64(-e) + bb.Exp(tenInt, &expo, nil) + bb.Mul(d2.getValue(), &bb) + scalerest = d.exp + // now aa = a + // bb = b 10^(scale + eb - ea) + } else { + expo.SetInt64(e) + aa.Exp(tenInt, &expo, nil) + aa.Mul(d.getValue(), &aa) + bb = *d2.getValue() + scalerest = scale + d2.exp + // now aa = a ^ (ea - eb - scale) + // bb = b + } + var q, r big.Int + q.QuoRem(&aa, &bb, &r) + dq := Decimal{value: &q, exp: scale} + dr := Decimal{value: &r, exp: scalerest} + return dq, dr +} + +// DivRound divides and rounds to a given precision +// i.e. to an integer multiple of 10^(-precision) +// +// for a positive quotient digit 5 is rounded up, away from 0 +// if the quotient is negative then digit 5 is rounded down, away from 0 +// +// Note that precision<0 is allowed as input. +func (d Decimal) DivRound(d2 Decimal, precision int32) Decimal { + // QuoRem already checks initialization + q, r := d.QuoRem(d2, precision) + // the actual rounding decision is based on comparing r*10^precision and d2/2 + // instead compare 2 r 10 ^precision and d2 + var rv2 big.Int + rv2.Abs(r.getValue()) + rv2.Lsh(&rv2, 1) + // now rv2 = abs(r.value) * 2 + r2 := Decimal{value: &rv2, exp: r.exp + precision} + // r2 is now 2 * r * 10 ^ precision + var c = r2.Cmp(d2.Abs()) + + if c < 0 { + return q + } + + if d.getValue().Sign()*d2.getValue().Sign() < 0 { + return q.Sub(New(1, -precision)) + } + + return q.Add(New(1, -precision)) +} + +// Mod returns d % d2. +func (d Decimal) Mod(d2 Decimal) Decimal { + _, r := d.QuoRem(d2, 0) + return r +} + +// Pow returns d to the power of d2. +// When exponent is negative the returned decimal will have maximum precision of PowPrecisionNegativeExponent places after decimal point. +// +// Pow returns 0 (zero-value of Decimal) instead of error for power operation edge cases, to handle those edge cases use PowWithPrecision +// Edge cases not handled by Pow: +// - 0 ** 0 => undefined value +// - 0 ** y, where y < 0 => infinity +// - x ** y, where x < 0 and y is non-integer decimal => imaginary value +// +// Example: +// +// d1 := decimal.NewFromFloat(4.0) +// d2 := decimal.NewFromFloat(4.0) +// res1 := d1.Pow(d2) +// res1.String() // output: "256" +// +// d3 := decimal.NewFromFloat(5.0) +// d4 := decimal.NewFromFloat(5.73) +// res2 := d3.Pow(d4) +// res2.String() // output: "10118.08037125" +func (d Decimal) Pow(d2 Decimal) Decimal { + baseSign := d.Sign() + expSign := d2.Sign() + + if baseSign == 0 { + if expSign == 0 { + return Decimal{} + } + if expSign == 1 { + return Decimal{zeroInt, 0} + } + if expSign == -1 { + return Decimal{} + } + } + + if expSign == 0 { + return Decimal{oneInt, 0} + } + + // TODO: optimize extraction of fractional part + one := Decimal{oneInt, 0} + expIntPart, expFracPart := d2.QuoRem(one, 0) + + if baseSign == -1 && !expFracPart.IsZero() { + return Decimal{} + } + + intPartPow, _ := d.PowBigInt(expIntPart.getValue()) + + // if exponent is an integer we don't need to calculate d1**frac(d2) + if expFracPart.getValue().Sign() == 0 { + return intPartPow + } + + // TODO: optimize NumDigits for more performant precision adjustment + digitsBase := d.NumDigits() + digitsExponent := d2.NumDigits() + + precision := digitsBase + + if digitsExponent > precision { + precision += digitsExponent + } + + precision += 6 + + // Calculate x ** frac(y), where + // x ** frac(y) = exp(ln(x ** frac(y)) = exp(ln(x) * frac(y)) + fracPartPow, err := d.Abs().Ln(-d.exp + int32(precision)) + if err != nil { + return Decimal{} + } + + fracPartPow = fracPartPow.Mul(expFracPart) + + fracPartPow, err = fracPartPow.ExpTaylor(-d.exp + int32(precision)) + if err != nil { + return Decimal{} + } + + // Join integer and fractional part, + // base ** (expBase + expFrac) = base ** expBase * base ** expFrac + res := intPartPow.Mul(fracPartPow) + + return res +} + +// PowInt32 returns d to the power of exp, where exp is int32. +// Only returns error when d and exp is 0, thus result is undefined. +// +// When exponent is negative the returned decimal will have maximum precision of PowPrecisionNegativeExponent places after decimal point. +// +// Example: +// +// d1, err := decimal.NewFromFloat(4.0).PowInt32(4) +// d1.String() // output: "256" +// +// d2, err := decimal.NewFromFloat(3.13).PowInt32(5) +// d2.String() // output: "300.4150512793" +func (d Decimal) PowInt32(exp int32) (Decimal, error) { + if d.IsZero() && exp == 0 { + return Decimal{}, fmt.Errorf("cannot represent undefined value of 0**0") + } + + isExpNeg := exp < 0 + exp = abs(exp) + + n, result := d, New(1, 0) + + for exp > 0 { + if exp%2 == 1 { + result = result.Mul(n) + } + exp /= 2 + + if exp > 0 { + n = n.Mul(n) + } + } + + if isExpNeg { + return New(1, 0).DivRound(result, int32(PowPrecisionNegativeExponent)), nil + } + + return result, nil +} + +// PowBigInt returns d to the power of exp, where exp is big.Int. +// Only returns error when d and exp is 0, thus result is undefined. +// +// When exponent is negative the returned decimal will have maximum precision of PowPrecisionNegativeExponent places after decimal point. +// +// Example: +// +// d1, err := decimal.NewFromFloat(3.0).PowBigInt(big.NewInt(3)) +// d1.String() // output: "27" +// +// d2, err := decimal.NewFromFloat(629.25).PowBigInt(big.NewInt(5)) +// d2.String() // output: "98654323103449.5673828125" +func (d Decimal) PowBigInt(exp *big.Int) (Decimal, error) { + return d.powBigIntWithPrecision(exp, int32(PowPrecisionNegativeExponent)) +} + +func (d Decimal) powBigIntWithPrecision(exp *big.Int, precision int32) (Decimal, error) { + if d.IsZero() && exp.Sign() == 0 { + return Decimal{}, fmt.Errorf("cannot represent undefined value of 0**0") + } + + tmpExp := new(big.Int).Set(exp) + isExpNeg := exp.Sign() < 0 + + if isExpNeg { + tmpExp.Abs(tmpExp) + } + + n, result := d, New(1, 0) + + for tmpExp.Sign() > 0 { + if tmpExp.Bit(0) == 1 { + result = result.Mul(n) + } + tmpExp.Rsh(tmpExp, 1) + + if tmpExp.Sign() > 0 { + n = n.Mul(n) + } + } + + if isExpNeg { + return New(1, 0).DivRound(result, precision), nil + } + + return result, nil +} + +// ExpTaylor calculates the natural exponent of decimal (e to the power of d) using Taylor series expansion. +// Precision argument specifies how precise the result must be (number of digits after decimal point). +// Negative precision is allowed. +// +// ExpTaylor is much faster for large precision values than ExpHullAbrham. +// +// Example: +// +// d, err := NewFromFloat(26.1).ExpTaylor(2).String() +// d.String() // output: "216314672147.06" +// +// NewFromFloat(26.1).ExpTaylor(20).String() +// d.String() // output: "216314672147.05767284062928674083" +// +// NewFromFloat(26.1).ExpTaylor(-10).String() +// d.String() // output: "220000000000" +func (d Decimal) ExpTaylor(precision int32) (Decimal, error) { + // Note(mwoss): Implementation can be optimized by exclusively using big.Int API only + if d.IsZero() { + return Decimal{oneInt, 0}.Round(precision), nil + } + + var epsilon Decimal + var divPrecision int32 + if precision < 0 { + epsilon = New(1, -1) + divPrecision = 8 + } else { + epsilon = New(1, -precision-1) + divPrecision = precision + 1 + } + + decAbs := d.Abs() + pow := d.Abs() + factorial := New(1, 0) + + result := New(1, 0) + + for i := int64(1); ; { + step := pow.DivRound(factorial, divPrecision) + result = result.Add(step) + + // Stop Taylor series when current step is smaller than epsilon + if step.Cmp(epsilon) < 0 { + break + } + + pow = pow.Mul(decAbs) + + i++ + + // Compute factorial locally to avoid data races on a shared slice. + factorial = factorial.Mul(New(i, 0)) + } + + if d.Sign() < 0 { + result = New(1, 0).DivRound(result, precision+1) + } + + result = result.Round(precision) + return result, nil +} + +// Ln calculates natural logarithm of d. +// Precision argument specifies how precise the result must be (number of digits after decimal point). +// Negative precision is allowed. +// +// Example: +// +// d1, err := NewFromFloat(13.3).Ln(2) +// d1.String() // output: "2.59" +// +// d2, err := NewFromFloat(579.161).Ln(10) +// d2.String() // output: "6.3615805046" +func (d Decimal) Ln(precision int32) (Decimal, error) { + // Algorithm based on The Use of Iteration Methods for Approximating the Natural Logarithm, + // James F. Epperson, The American Mathematical Monthly, Vol. 96, No. 9, November 1989, pp. 831-835. + if d.IsNegative() { + return Decimal{}, fmt.Errorf("cannot calculate natural logarithm for negative decimals") + } + + if d.IsZero() { + return Decimal{}, fmt.Errorf("cannot represent natural logarithm of 0, result: -infinity") + } + + calcPrecision := precision + 2 + z := d.Copy() + + var comp1, comp3, comp2, comp4, reduceAdjust Decimal + comp1 = z.Sub(Decimal{oneInt, 0}) + comp3 = Decimal{oneInt, -1} + + // for decimal in range [0.9, 1.1] where ln(d) is close to 0 + usePowerSeries := false + + if comp1.Abs().Cmp(comp3) <= 0 { + usePowerSeries = true + } else { + // reduce input decimal to range [0.1, 1) + expDelta := int32(z.NumDigits()) + z.exp + z.exp -= expDelta + + // Input decimal was reduced by factor of 10^expDelta, thus we will need to add + // ln(10^expDelta) = expDelta * ln(10) + // to the result to compensate that + ln10 := ln10.withPrecision(calcPrecision) + reduceAdjust = NewFromInt32(expDelta) + reduceAdjust = reduceAdjust.Mul(ln10) + + comp1 = z.Sub(Decimal{oneInt, 0}) + + if comp1.Abs().Cmp(comp3) <= 0 { + usePowerSeries = true + } else { + // initial estimate using floats + zFloat := z.InexactFloat64() + comp1 = NewFromFloat(math.Log(zFloat)) + } + } + + epsilon := Decimal{oneInt, -calcPrecision} + + if usePowerSeries { + // Power Series - https://en.wikipedia.org/wiki/Logarithm#Power_series + // Calculating n-th term of formula: ln(z+1) = 2 sum [ 1 / (2n+1) * (z / (z+2))^(2n+1) ] + // until the difference between current and next term is smaller than epsilon. + // Coverage quite fast for decimals close to 1.0 + + // z + 2 + comp2 = comp1.Add(Decimal{twoInt, 0}) + // z / (z + 2) + comp3 = comp1.DivRound(comp2, calcPrecision) + // 2 * (z / (z + 2)) + comp1 = comp3.Add(comp3) + comp2 = comp1.Copy() + + for n := 1; ; n++ { + // 2 * (z / (z+2))^(2n+1) + comp2 = comp2.Mul(comp3).Mul(comp3) + + // 1 / (2n+1) * 2 * (z / (z+2))^(2n+1) + comp4 = NewFromInt(int64(2*n + 1)) + comp4 = comp2.DivRound(comp4, calcPrecision) + + // comp1 = 2 sum [ 1 / (2n+1) * (z / (z+2))^(2n+1) ] + comp1 = comp1.Add(comp4) + + if comp4.Abs().Cmp(epsilon) <= 0 { + break + } + } + } else { + // Halley's Iteration. + // Calculating n-th term of formula: a_(n+1) = a_n - 2 * (exp(a_n) - z) / (exp(a_n) + z), + // until the difference between current and next term is smaller than epsilon + var prevStep Decimal + maxIters := calcPrecision*2 + 10 + + for i := int32(0); i < maxIters; i++ { + // exp(a_n) + comp3, _ = comp1.ExpTaylor(calcPrecision) + // exp(a_n) - z + comp2 = comp3.Sub(z) + // 2 * (exp(a_n) - z) + comp2 = comp2.Add(comp2) + // exp(a_n) + z + comp4 = comp3.Add(z) + // 2 * (exp(a_n) - z) / (exp(a_n) + z) + comp3 = comp2.DivRound(comp4, calcPrecision) + // comp1 = a_(n+1) = a_n - 2 * (exp(a_n) - z) / (exp(a_n) + z) + comp1 = comp1.Sub(comp3) + + if prevStep.Add(comp3).IsZero() { + // If iteration steps oscillate we should return early and prevent an infinity loop + // NOTE(mwoss): This should be quite a rare case, returning error is not necessary + break + } + + if comp3.Abs().Cmp(epsilon) <= 0 { + break + } + + prevStep = comp3 + } + } + + comp1 = comp1.Add(reduceAdjust) + + return comp1.Round(precision), nil +} + +// NumDigits returns the number of digits of the decimal coefficient (d.Value) +func (d Decimal) NumDigits() int { + v := d.getValue() + if v.IsInt64() { + i64 := v.Int64() + // restrict fast path to integers with exact conversion to float64 + if i64 <= (1<<53) && i64 >= -(1<<53) { + if i64 == 0 { + return 1 + } + return int(math.Log10(math.Abs(float64(i64)))) + 1 + } + } + + estimatedNumDigits := int(float64(v.BitLen()) / math.Log2(10)) + + // estimatedNumDigits (lg10) may be off by 1, need to verify + digitsBigInt := big.NewInt(int64(estimatedNumDigits)) + errorCorrectionUnit := digitsBigInt.Exp(tenInt, digitsBigInt, nil) + + if v.CmpAbs(errorCorrectionUnit) >= 0 { + return estimatedNumDigits + 1 + } + + return estimatedNumDigits +} + +// IsInteger returns true when decimal can be represented as an integer value, otherwise, it returns false. +func (d Decimal) IsInteger() bool { + // The most typical case, all decimal with exponent higher or equal 0 can be represented as integer + if d.exp >= 0 { + return true + } + // When the exponent is negative we have to check every number after the decimal place + // If all of them are zeroes, we are sure that given decimal can be represented as an integer + var r big.Int + q := new(big.Int).Set(d.getValue()) + for z := abs(d.exp); z > 0; z-- { + q.QuoRem(q, tenInt, &r) + if r.Cmp(zeroInt) != 0 { + return false + } + } + return true +} + +// Abs calculates absolute value of any int32. Used for calculating absolute value of decimal's exponent. +func abs(n int32) int32 { + if n < 0 { + return -n + } + return n +} + +// Cmp compares the numbers represented by d and d2 and returns: +// +// -1 if d < d2 +// 0 if d == d2 +// +1 if d > d2 +func (d Decimal) Cmp(d2 Decimal) int { + if d.exp == d2.exp { + return d.getValue().Cmp(d2.getValue()) + } + + rd, rd2 := RescalePair(d, d2) + + return rd.getValue().Cmp(rd2.getValue()) +} + +// Equal returns whether the numbers represented by d and d2 are equal. +func (d Decimal) Equal(d2 Decimal) bool { + return d.Cmp(d2) == 0 +} + +// GreaterThan (GT) returns true when d is greater than d2. +func (d Decimal) GreaterThan(d2 Decimal) bool { + return d.Cmp(d2) == 1 +} + +// GreaterThanOrEqual (GTE) returns true when d is greater than or equal to d2. +func (d Decimal) GreaterThanOrEqual(d2 Decimal) bool { + cmp := d.Cmp(d2) + return cmp == 1 || cmp == 0 +} + +// LessThan (LT) returns true when d is less than d2. +func (d Decimal) LessThan(d2 Decimal) bool { + return d.Cmp(d2) == -1 +} + +// LessThanOrEqual (LTE) returns true when d is less than or equal to d2. +func (d Decimal) LessThanOrEqual(d2 Decimal) bool { + cmp := d.Cmp(d2) + return cmp == -1 || cmp == 0 +} + +// Sign returns: +// +// -1 if d < 0 +// 0 if d == 0 +// +1 if d > 0 +func (d Decimal) Sign() int { + return d.getValue().Sign() +} + +// IsPositive return +// +// true if d > 0 +// false if d == 0 +// false if d < 0 +func (d Decimal) IsPositive() bool { + return d.Sign() == 1 +} + +// IsNegative return +// +// true if d < 0 +// false if d == 0 +// false if d > 0 +func (d Decimal) IsNegative() bool { + return d.Sign() == -1 +} + +// IsZero return +// +// true if d == 0 +// false if d > 0 +// false if d < 0 +func (d Decimal) IsZero() bool { + return d.Sign() == 0 +} + +// Exponent returns the exponent, or scale component of the decimal. +func (d Decimal) Exponent() int32 { + return d.exp +} + +// Coefficient returns the coefficient of the decimal. It is scaled by 10^Exponent() +func (d Decimal) Coefficient() *big.Int { + // we copy the coefficient so that mutating the result does not mutate the Decimal. + return new(big.Int).Set(d.getValue()) +} + +// CoefficientInt64 returns the coefficient of the decimal as int64. It is scaled by 10^Exponent() +// If coefficient cannot be represented in an int64, the result will be undefined. +func (d Decimal) CoefficientInt64() int64 { + return d.getValue().Int64() +} + +// IntPart returns the integer component of the decimal. +func (d Decimal) IntPart() int64 { + scaledD := d.rescale(0) + return scaledD.getValue().Int64() +} + +// BigInt returns integer component of the decimal as a BigInt. +func (d Decimal) BigInt() *big.Int { + scaledD := d.rescale(0) + return scaledD.getValue() +} + +// BigFloat returns decimal as BigFloat. +// Be aware that casting decimal to BigFloat might cause a loss of precision. +func (d Decimal) BigFloat() *big.Float { + f := &big.Float{} + f.SetString(d.String()) + return f +} + +// Rat returns a rational number representation of the decimal. +func (d Decimal) Rat() *big.Rat { + if d.exp <= 0 { + // NOTE(vadim): must negate after casting to prevent int32 overflow + denom := new(big.Int).Exp(tenInt, big.NewInt(-int64(d.exp)), nil) + return new(big.Rat).SetFrac(d.getValue(), denom) + } + + mul := new(big.Int).Exp(tenInt, big.NewInt(int64(d.exp)), nil) + num := new(big.Int).Mul(d.getValue(), mul) + return new(big.Rat).SetFrac(num, oneInt) +} + +// Float64 returns the nearest float64 value for d and a bool indicating +// whether f represents d exactly. +// For more details, see the documentation for big.Rat.Float64 +func (d Decimal) Float64() (f float64, exact bool) { + return d.Rat().Float64() +} + +// InexactFloat64 returns the nearest float64 value for d. +// It doesn't indicate if the returned value represents d exactly. +func (d Decimal) InexactFloat64() float64 { + f, _ := d.Float64() + return f +} + +// String returns the string representation of the decimal +// with the fixed point. +// +// Example: +// +// d := New(-12345, -3) +// println(d.String()) +// +// Output: +// +// -12.345 +func (d Decimal) String() string { + return d.string(TrimTrailingZeros, UseScientificNotation) +} + +// StringFixed returns a rounded fixed-point string with places digits after +// the decimal point. +// +// Example: +// +// NewFromFloat(0).StringFixed(2) // output: "0.00" +// NewFromFloat(0).StringFixed(0) // output: "0" +// NewFromFloat(5.45).StringFixed(0) // output: "5" +// NewFromFloat(5.45).StringFixed(1) // output: "5.5" +// NewFromFloat(5.45).StringFixed(2) // output: "5.45" +// NewFromFloat(5.45).StringFixed(3) // output: "5.450" +// NewFromFloat(545).StringFixed(-1) // output: "540" +// +// Regardless of the UseScientificNotation option, the returned string will never be in scientific notation. +func (d Decimal) StringFixed(places int32) string { + rounded := d.Round(places) + return rounded.string(false, false) +} + +// Round rounds the decimal to places decimal places. +// If places < 0, it will round the integer part to the nearest 10^(-places). +// +// Example: +// +// NewFromFloat(5.45).Round(1).String() // output: "5.5" +// NewFromFloat(545).Round(-1).String() // output: "550" (with UseScientificNotation false, "5.5E2" if true) +func (d Decimal) Round(places int32) Decimal { + if d.exp == -places { + return d + } + // truncate to places + 1 + ret := d.rescale(-places - 1) + + // add sign(d) * 0.5 + if ret.value.Sign() < 0 { + ret.value.Sub(ret.value, fiveInt) + } else { + ret.value.Add(ret.value, fiveInt) + } + + // floor for positive numbers, ceil for negative numbers + _, m := ret.value.DivMod(ret.value, tenInt, new(big.Int)) + ret.exp++ + if ret.value.Sign() < 0 && m.Cmp(zeroInt) != 0 { + ret.value.Add(ret.value, oneInt) + } + + return ret +} + +// RoundCeil rounds the decimal towards +infinity. +// +// Example: +// +// NewFromFloat(545).RoundCeil(-2).String() // output: "600" +// NewFromFloat(500).RoundCeil(-2).String() // output: "500" +// NewFromFloat(1.1001).RoundCeil(2).String() // output: "1.11" +// NewFromFloat(-1.454).RoundCeil(1).String() // output: "-1.4" +func (d Decimal) RoundCeil(places int32) Decimal { + if d.exp >= -places { + return d + } + + rescaled := d.rescale(-places) + if d.Equal(rescaled) { + return d + } + + if d.getValue().Sign() > 0 { + rescaled.value = new(big.Int).Add(rescaled.getValue(), oneInt) + } + + return rescaled +} + +// RoundFloor rounds the decimal towards -infinity. +// +// Example: +// +// NewFromFloat(545).RoundFloor(-2).String() // output: "500" +// NewFromFloat(-500).RoundFloor(-2).String() // output: "-500" +// NewFromFloat(1.1001).RoundFloor(2).String() // output: "1.1" +// NewFromFloat(-1.454).RoundFloor(1).String() // output: "-1.5" +func (d Decimal) RoundFloor(places int32) Decimal { + if d.exp >= -places { + return d + } + + rescaled := d.rescale(-places) + if d.Equal(rescaled) { + return d + } + + if d.getValue().Sign() < 0 { + rescaled.value = new(big.Int).Sub(rescaled.getValue(), oneInt) + } + + return rescaled +} + +// RoundUp rounds the decimal away from zero. +// +// Example: +// +// NewFromFloat(545).RoundUp(-2).String() // output: "600" +// NewFromFloat(500).RoundUp(-2).String() // output: "500" +// NewFromFloat(1.1001).RoundUp(2).String() // output: "1.11" +// NewFromFloat(-1.454).RoundUp(1).String() // output: "-1.5" +func (d Decimal) RoundUp(places int32) Decimal { + if d.exp >= -places { + return d + } + + rescaled := d.rescale(-places) + if d.Equal(rescaled) { + return d + } + + if d.getValue().Sign() > 0 { + rescaled.value = new(big.Int).Add(rescaled.getValue(), oneInt) + } else if d.getValue().Sign() < 0 { + rescaled.value = new(big.Int).Sub(rescaled.getValue(), oneInt) + } + + return rescaled +} + +// RoundDown rounds the decimal towards zero. +// +// Example: +// +// NewFromFloat(545).RoundDown(-2).String() // output: "500" +// NewFromFloat(-500).RoundDown(-2).String() // output: "-500" +// NewFromFloat(1.1001).RoundDown(2).String() // output: "1.1" +// NewFromFloat(-1.454).RoundDown(1).String() // output: "-1.4" +func (d Decimal) RoundDown(places int32) Decimal { + if d.exp >= -places { + return d + } + + rescaled := d.rescale(-places) + if d.Equal(rescaled) { + return d + } + return rescaled +} + +// Floor returns the nearest integer value less than or equal to d. +func (d Decimal) Floor() Decimal { + if d.exp >= 0 { + return d + } + + exp := big.NewInt(10) + + // NOTE(vadim): must negate after casting to prevent int32 overflow + exp.Exp(exp, big.NewInt(-int64(d.exp)), nil) + + z := new(big.Int).Div(d.getValue(), exp) + return Decimal{value: z, exp: 0} +} + +// Ceil returns the nearest integer value greater than or equal to d. +func (d Decimal) Ceil() Decimal { + if d.exp >= 0 { + return d + } + + exp := big.NewInt(10) + + // NOTE(vadim): must negate after casting to prevent int32 overflow + exp.Exp(exp, big.NewInt(-int64(d.exp)), nil) + + z, m := new(big.Int).DivMod(d.getValue(), exp, new(big.Int)) + if m.Cmp(zeroInt) != 0 { + z.Add(z, oneInt) + } + return Decimal{value: z, exp: 0} +} + +// Truncate truncates off digits from the number, without rounding. +// +// NOTE: precision is the last digit that will not be truncated (must be >= 0). +// +// Example: +// +// decimal.NewFromString("123.456").Truncate(2).String() // "123.45" +func (d Decimal) Truncate(precision int32) Decimal { + if precision >= 0 && -precision > d.exp { + return d.rescale(-precision) + } + return d +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (d *Decimal) UnmarshalJSON(decimalBytes []byte) error { + if string(decimalBytes) == "null" { + return nil + } + + decimal, err := NewFromString(unquoteIfQuoted(string(decimalBytes))) + *d = decimal + if err != nil { + return fmt.Errorf("error decoding string '%s': %s", string(decimalBytes), err) + } + return nil +} + +// MarshalJSON implements the json.Marshaler interface. +func (d Decimal) MarshalJSON() ([]byte, error) { + var str string + if MarshalJSONWithoutQuotes { + str = d.String() + } else { + str = "\"" + d.String() + "\"" + } + return []byte(str), nil +} + +// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. As a string representation +// is already used when encoding to text, this method stores that string as []byte +func (d *Decimal) UnmarshalBinary(data []byte) error { + // Verify we have at least 4 bytes for the exponent. The GOB encoded value + // may be empty. + if len(data) < 4 { + return fmt.Errorf("error decoding binary %v: expected at least 4 bytes, got %d", data, len(data)) + } + + // Extract the exponent + d.exp = int32(binary.BigEndian.Uint32(data[:4])) + + // Extract the value + d.value = new(big.Int) + if err := d.value.GobDecode(data[4:]); err != nil { + return fmt.Errorf("error decoding binary %v: %s", data, err) + } + + return nil +} + +// MarshalBinary implements the encoding.BinaryMarshaler interface. +func (d Decimal) MarshalBinary() (data []byte, err error) { + // exp is written first, but encode value first to know output size + var valueData []byte + if valueData, err = d.getValue().GobEncode(); err != nil { + return nil, err + } + + // Write the exponent in front, since it's a fixed size + expData := make([]byte, 4, len(valueData)+4) + binary.BigEndian.PutUint32(expData, uint32(d.exp)) + + // Return the byte array + return append(expData, valueData...), nil +} + +// Scan implements the sql.Scanner interface for database deserialization. +func (d *Decimal) Scan(value interface{}) error { + // first try to see if the data is stored in database as a Numeric datatype + switch v := value.(type) { + + case float32: + *d = NewFromFloat(float64(v)) + return nil + + case float64: + // numeric in sqlite3 sends us float64 + *d = NewFromFloat(v) + return nil + + case int64: + // at least in sqlite3 when the value is 0 in db, the data is sent + // to us as an int64 instead of a float64 ... + *d = New(v, 0) + return nil + + case uint64: + // while clickhouse may send 0 in db as uint64 + *d = NewFromUint64(v) + return nil + + case string: + var err error + *d, err = NewFromString(unquoteIfQuoted(v)) + return err + + case []byte: + var err error + *d, err = NewFromString(unquoteIfQuoted(string(v))) + return err + + default: + return fmt.Errorf("could not convert value '%+v' to any known type", value) + } +} + +// Value implements the driver.Valuer interface for database serialization. +func (d Decimal) Value() (driver.Value, error) { + return d.String(), nil +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface for XML +// deserialization. +func (d *Decimal) UnmarshalText(text []byte) error { + str := string(text) + + dec, err := NewFromString(str) + *d = dec + if err != nil { + return fmt.Errorf("error decoding string '%s': %s", str, err) + } + + return nil +} + +// MarshalText implements the encoding.TextMarshaler interface for XML +// serialization. +func (d Decimal) MarshalText() (text []byte, err error) { + return []byte(d.String()), nil +} + +// GobEncode implements the gob.GobEncoder interface for gob serialization. +func (d Decimal) GobEncode() ([]byte, error) { + return d.MarshalBinary() +} + +// GobDecode implements the gob.GobDecoder interface for gob serialization. +func (d *Decimal) GobDecode(data []byte) error { + return d.UnmarshalBinary(data) +} + +func (d Decimal) string(trimTrailingZeros, useScientificNotation bool) string { + if d.exp == 0 { + return d.rescale(0).getValue().String() + } + if d.exp >= 0 { + if useScientificNotation { + return d.ScientificNotationString() + } else { + return d.rescale(0).value.String() + } + } + + abs := new(big.Int).Abs(d.getValue()) + str := abs.String() + + var intPart, fractionalPart string + + // NOTE(vadim): this cast to int will cause bugs if d.exp == INT_MIN + // and you are on a 32-bit machine. Won't fix this super-edge case. + dExpInt := int(d.exp) + if len(str) > -dExpInt { + intPart = str[:len(str)+dExpInt] + fractionalPart = str[len(str)+dExpInt:] + } else { + intPart = "0" + + num0s := -dExpInt - len(str) + fractionalPart = strings.Repeat("0", num0s) + str + } + + if trimTrailingZeros { + i := len(fractionalPart) - 1 + for ; i >= 0; i-- { + if fractionalPart[i] != '0' { + break + } + } + fractionalPart = fractionalPart[:i+1] + } + + number := intPart + if len(fractionalPart) > 0 { + number += "." + fractionalPart + } + + if d.getValue().Sign() < 0 { + return "-" + number + } + + return number +} + +// ScientificNotationString serializes the decimal into standard scientific notation. +// +// The notation is normalized to have one non-zero digit followed by a decimal point and +// the remaining significant digits followed by "E" and the base-10 exponent. +// +// A zero, which has no significant digits, is simply serialized to "0". +func (d Decimal) ScientificNotationString() string { + exp := int(d.exp) + intStr := new(big.Int).Abs(d.getValue()).String() + if intStr == "0" { + return intStr + } + first := intStr[0] + var remaining string + if len(intStr) > 1 { + remaining = "." + intStr[1:] + exp = exp + len(intStr) - 1 + } + number := string(first) + remaining + "E" + strconv.Itoa(exp) + if d.value.Sign() < 0 { + return "-" + number + } + return number +} + +// Min returns the smallest Decimal that was passed in the arguments. +// +// To call this function with an array, you must do: +// +// Min(arr[0], arr[1:]...) +// +// This makes it harder to accidentally call Min with 0 arguments. +func Min(first Decimal, rest ...Decimal) Decimal { + ans := first + for _, item := range rest { + if item.Cmp(ans) < 0 { + ans = item + } + } + return ans +} + +// Max returns the largest Decimal that was passed in the arguments. +// +// To call this function with an array, you must do: +// +// Max(arr[0], arr[1:]...) +// +// This makes it harder to accidentally call Max with 0 arguments. +func Max(first Decimal, rest ...Decimal) Decimal { + ans := first + for _, item := range rest { + if item.Cmp(ans) > 0 { + ans = item + } + } + return ans +} + +// Sum returns the combined total of the provided first and rest Decimals +func Sum(first Decimal, rest ...Decimal) Decimal { + total := first + for _, item := range rest { + total = total.Add(item) + } + + return total +} + +// RescalePair rescales two decimals to common exponential value (minimal exp of both decimals) +func RescalePair(d1 Decimal, d2 Decimal) (Decimal, Decimal) { + if d1.exp < d2.exp { + return d1, d2.rescale(d1.exp) + } else if d1.exp > d2.exp { + return d1.rescale(d2.exp), d2 + } + + return d1, d2 +} + +func unquoteIfQuoted(value string) string { + // If the amount is quoted, strip the quotes + if len(value) > 2 && value[0] == '"' && value[len(value)-1] == '"' { + return value[1 : len(value)-1] + } + + return value +} + +// NullDecimal represents a nullable decimal with compatibility for +// scanning null values from the database. +type NullDecimal struct { + Decimal Decimal + Valid bool +} + +func NewNullDecimal(d Decimal) NullDecimal { + return NullDecimal{ + Decimal: d, + Valid: true, + } +} + +// Scan implements the sql.Scanner interface for database deserialization. +func (d *NullDecimal) Scan(value interface{}) error { + if value == nil { + d.Valid = false + return nil + } + d.Valid = true + return d.Decimal.Scan(value) +} + +// Value implements the driver.Valuer interface for database serialization. +func (d NullDecimal) Value() (driver.Value, error) { + if !d.Valid { + return nil, nil + } + return d.Decimal.Value() +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (d *NullDecimal) UnmarshalJSON(decimalBytes []byte) error { + if string(decimalBytes) == "null" { + d.Valid = false + return nil + } + d.Valid = true + return d.Decimal.UnmarshalJSON(decimalBytes) +} + +// MarshalJSON implements the json.Marshaler interface. +func (d NullDecimal) MarshalJSON() ([]byte, error) { + if !d.Valid { + return []byte("null"), nil + } + return d.Decimal.MarshalJSON() +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface for XML +// deserialization +func (d *NullDecimal) UnmarshalText(text []byte) error { + str := string(text) + + // check for empty XML or XML without body e.g., + if str == "" { + d.Valid = false + return nil + } + if err := d.Decimal.UnmarshalText(text); err != nil { + d.Valid = false + return err + } + d.Valid = true + return nil +} + +// MarshalText implements the encoding.TextMarshaler interface for XML +// serialization. +func (d NullDecimal) MarshalText() (text []byte, err error) { + if !d.Valid { + return []byte{}, nil + } + return d.Decimal.MarshalText() +} + +// FromWei converts a *big.Int representing a value in wei (18 decimals) to a Decimal. +func FromWei(v *big.Int) Decimal { + return NewFromBigInt(v, -18) +} + +// ToWei scales the decimal to 18 decimal places and returns the result as a *big.Int. +// Fractional sub-wei digits are truncated. +func (d Decimal) ToWei() *big.Int { + // Scale to 18 decimals: multiply value by 10^(18 + exp). + // Use rescale to shift the decimal point, then extract the integer. + scaled := d.rescale(-18) + return new(big.Int).Set(scaled.getValue()) +} diff --git a/pkg/decimal/decimal_bench_test.go b/pkg/decimal/decimal_bench_test.go new file mode 100644 index 0000000..43ff6d6 --- /dev/null +++ b/pkg/decimal/decimal_bench_test.go @@ -0,0 +1,295 @@ +package decimal + +import ( + "fmt" + "math" + "math/big" + "math/rand" + "sort" + "strconv" + "testing" +) + +type DecimalSlice []Decimal + +func (p DecimalSlice) Len() int { return len(p) } +func (p DecimalSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } +func (p DecimalSlice) Less(i, j int) bool { return p[i].Cmp(p[j]) < 0 } + +func BenchmarkNewFromFloatWithExponent(b *testing.B) { + rng := rand.New(rand.NewSource(0xdead1337)) + in := make([]float64, b.N) + for i := range in { + in[i] = rng.NormFloat64() * 10e20 + } + b.ReportAllocs() + b.StartTimer() + for i := 0; i < b.N; i++ { + _ = NewFromFloatWithExponent(in[i], math.MinInt32) + } +} + +func BenchmarkNewFromFloat(b *testing.B) { + rng := rand.New(rand.NewSource(0xdead1337)) + in := make([]float64, b.N) + for i := range in { + in[i] = rng.NormFloat64() * 10e20 + } + b.ReportAllocs() + b.StartTimer() + for i := 0; i < b.N; i++ { + _ = NewFromFloat(in[i]) + } +} + +func BenchmarkNewFromStringFloat(b *testing.B) { + rng := rand.New(rand.NewSource(0xdead1337)) + in := make([]float64, b.N) + for i := range in { + in[i] = rng.NormFloat64() * 10e20 + } + b.ReportAllocs() + b.StartTimer() + for i := 0; i < b.N; i++ { + in := strconv.FormatFloat(in[i], 'f', -1, 64) + _, _ = NewFromString(in) + } +} + +func Benchmark_FloorFast(b *testing.B) { + input := New(200, 2) + b.ResetTimer() + for i := 0; i < b.N; i++ { + input.Floor() + } +} + +func Benchmark_FloorRegular(b *testing.B) { + input := New(200, -2) + b.ResetTimer() + for i := 0; i < b.N; i++ { + input.Floor() + } +} + +func Benchmark_DivideOriginal(b *testing.B) { + tcs := createDivTestCases() + b.ResetTimer() + for i := 0; i < b.N; i++ { + for _, tc := range tcs { + d := tc.d + if sign(tc.d2) == 0 { + continue + } + d2 := tc.d2 + prec := tc.prec + a := d.DivOld(d2, int(prec)) + if sign(a) > 2 { + panic("dummy panic") + } + } + } +} + +func Benchmark_DivideNew(b *testing.B) { + tcs := createDivTestCases() + b.ResetTimer() + for i := 0; i < b.N; i++ { + for _, tc := range tcs { + d := tc.d + if sign(tc.d2) == 0 { + continue + } + d2 := tc.d2 + prec := tc.prec + a := d.DivRound(d2, prec) + if sign(a) > 2 { + panic("dummy panic") + } + } + } +} + +func numDigits(b *testing.B, want int, val Decimal) { + b.Helper() + for i := 0; i < b.N; i++ { + if have := val.NumDigits(); have != want { + b.Fatalf("\nHave: %d\nWant: %d", have, want) + } + } +} + +func BenchmarkDecimal_NumDigits10(b *testing.B) { + numDigits(b, 10, New(3478512345, -3)) +} + +func BenchmarkDecimal_NumDigits100(b *testing.B) { + s := make([]byte, 102) + for i := range s { + s[i] = byte('0' + i%10) + } + s[0] = '-' + s[100] = '.' + d, err := NewFromString(string(s)) + if err != nil { + b.Log(d) + b.Error(err) + } + numDigits(b, 100, d) +} + +func Benchmark_Cmp(b *testing.B) { + decimals := DecimalSlice([]Decimal{}) + for i := 0; i < 1000000; i++ { + decimals = append(decimals, New(int64(i), 0)) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + sort.Sort(decimals) + } +} + +func BenchmarkDecimal_Add_different_precision(b *testing.B) { + d1 := NewFromFloat(1000.123) + d2 := NewFromFloat(500).Mul(NewFromFloat(0.12)) + + b.ReportAllocs() + b.StartTimer() + for i := 0; i < b.N; i++ { + d1.Sub(d2) + } +} + +func BenchmarkDecimal_Sub_different_precision(b *testing.B) { + d1 := NewFromFloat(1000.123) + d2 := NewFromFloat(500).Mul(NewFromFloat(0.12)) + + b.ReportAllocs() + b.StartTimer() + for i := 0; i < b.N; i++ { + d1.Sub(d2) + } +} + +func BenchmarkDecimal_Add_same_precision(b *testing.B) { + d1 := NewFromFloat(1000.123) + d2 := NewFromFloat(500.123) + + b.ReportAllocs() + b.StartTimer() + for i := 0; i < b.N; i++ { + d1.Add(d2) + } +} + +func BenchmarkDecimal_Sub_same_precision(b *testing.B) { + d1 := NewFromFloat(1000.123) + d2 := NewFromFloat(500.123) + + b.ReportAllocs() + b.StartTimer() + for i := 0; i < b.N; i++ { + d1.Add(d2) + } +} + +func BenchmarkDecimal_IsInteger(b *testing.B) { + d := RequireFromString("12.000") + + b.ReportAllocs() + b.StartTimer() + for i := 0; i < b.N; i++ { + d.IsInteger() + } +} + +func BenchmarkDecimal_Pow(b *testing.B) { + d1 := RequireFromString("5.2") + d2 := RequireFromString("6.3") + + for i := 0; i < b.N; i++ { + d1.Pow(d2) + } +} + +func BenchmarkDecimal_PowInt32(b *testing.B) { + d1 := RequireFromString("5.2") + d2 := int32(10) + + for i := 0; i < b.N; i++ { + _, _ = d1.PowInt32(d2) + } +} + +func BenchmarkDecimal_PowBigInt(b *testing.B) { + d1 := RequireFromString("5.2") + d2 := big.NewInt(10) + + for i := 0; i < b.N; i++ { + _, _ = d1.PowBigInt(d2) + } +} + +func BenchmarkDecimal_NewFromString(b *testing.B) { + count := 72 + prices := make([]string, 0, count) + for i := 1; i <= count; i++ { + prices = append(prices, fmt.Sprintf("%d.%d", i*100, i)) + } + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + for _, p := range prices { + d, err := NewFromString(p) + if err != nil { + b.Log(d) + b.Error(err) + } + } + } +} + +func BenchmarkDecimal_NewFromString_large_number(b *testing.B) { + count := 72 + prices := make([]string, 0, count) + for i := 1; i <= count; i++ { + prices = append(prices, "9323372036854775807.9223372036854775807") + } + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + for _, p := range prices { + d, err := NewFromString(p) + if err != nil { + b.Log(d) + b.Error(err) + } + } + } +} + +func BenchmarkDecimal_ExpTaylor(b *testing.B) { + b.ResetTimer() + + d := RequireFromString("30.412346346346") + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = d.ExpTaylor(10) + } +} + +func BenchmarkDecimal_UnmarshalJSON(b *testing.B) { + b.ResetTimer() + + bstr := []byte("1234.56789") + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = (&Decimal{}).UnmarshalJSON(bstr) + } +} diff --git a/pkg/decimal/decimal_test.go b/pkg/decimal/decimal_test.go new file mode 100644 index 0000000..40baa96 --- /dev/null +++ b/pkg/decimal/decimal_test.go @@ -0,0 +1,3331 @@ +package decimal + +import ( + "database/sql/driver" + "encoding/json" + "encoding/xml" + "fmt" + "math" + "math/big" + "math/rand" + "reflect" + "strconv" + "strings" + "testing" + "testing/quick" + "time" +) + +type testEnt struct { + float float64 + short string + exact string + inexact string +} + +var testTable = []*testEnt{ + {3.141592653589793, "3.141592653589793", "", "3.14159265358979300000000000000000000000000000000000004"}, + {3, "3", "", "3.0000000000000000000000002"}, + {1234567890123456, "1234567890123456", "", "1234567890123456.00000000000000002"}, + {1234567890123456000, "1234567890123456000", "", "1234567890123456000.0000000000000008"}, + {1234.567890123456, "1234.567890123456", "", "1234.5678901234560000000000000009"}, + {.1234567890123456, "0.1234567890123456", "", "0.12345678901234560000000000006"}, + {0, "0", "", "0.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001"}, + {.1111111111111110, "0.111111111111111", "", "0.111111111111111000000000000000009"}, + {.1111111111111111, "0.1111111111111111", "", "0.111111111111111100000000000000000000023423545644534234"}, + {.1111111111111119, "0.1111111111111119", "", "0.111111111111111900000000000000000000000000000000000134123984192834"}, + {.000000000000000001, "0.000000000000000001", "", "0.00000000000000000100000000000000000000000000000000012341234"}, + {.000000000000000002, "0.000000000000000002", "", "0.0000000000000000020000000000000000000012341234123"}, + {.000000000000000003, "0.000000000000000003", "", "0.00000000000000000299999999999999999999999900000000000123412341234"}, + {.000000000000000005, "0.000000000000000005", "", "0.00000000000000000500000000000000000023412341234"}, + {.000000000000000008, "0.000000000000000008", "", "0.0000000000000000080000000000000000001241234432"}, + {.1000000000000001, "0.1000000000000001", "", "0.10000000000000010000000000000012341234"}, + {.1000000000000002, "0.1000000000000002", "", "0.10000000000000020000000000001234123412"}, + {.1000000000000003, "0.1000000000000003", "", "0.1000000000000003000000000000001234123412"}, + {.1000000000000005, "0.1000000000000005", "", "0.1000000000000005000000000000000006441234"}, + {.1000000000000008, "0.1000000000000008", "", "0.100000000000000800000000000000000009999999999999999999999999999"}, + {1e25, "10000000000000000000000000", "", ""}, + {1.5e14, "150000000000000", "", ""}, + {1.5e15, "1500000000000000", "", ""}, + {1.5e16, "15000000000000000", "", ""}, + {1.0001e25, "10001000000000000000000000", "", ""}, + {1.0001000000000000033e25, "10001000000000000000000000", "", ""}, + {2e25, "20000000000000000000000000", "", ""}, + {4e25, "40000000000000000000000000", "", ""}, + {8e25, "80000000000000000000000000", "", ""}, + {1e250, "10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "", ""}, + {2e250, "20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "", ""}, + {math.MaxInt64, strconv.FormatFloat(float64(math.MaxInt64), 'f', -1, 64), "", strconv.FormatInt(math.MaxInt64, 10)}, + {1.29067116156722e-309, "0.00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000129067116156722", "", "0.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001290671161567218558822290567835270536800098852722416870074139002112543896676308448335063375297788379444685193974290737962187240854947838776604607190387984577130572928111657710645015086812756013489109884753559084166516937690932698276436869274093950997935137476803610007959500457935217950764794724766740819156974617155861568214427828145972181876775307023388139991104942469299524961281641158436752347582767153796914843896176260096039358494077706152272661453132497761307744086665088096215425146090058519888494342944692629602847826300550628670375451325582843627504604013541465361435761965354140678551369499812124085312128659002910905639984075064968459581691226705666561364681985266583563078466180095375402399087817404368974165082030458595596655868575908243656158447265625000000000000000000000000000000000000004440000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"}, + // go Issue 29491. + {498484681984085570, "498484681984085570", "", ""}, + {5.8339553793802237e+23, "583395537938022370000000", "", ""}, +} + +var testTableScientificNotation = map[string]string{ + "1e9": "1000000000", + "2.41E-3": "0.00241", + "24.2E-4": "0.00242", + "243E-5": "0.00243", + "1e-5": "0.00001", + "245E3": "245000", + "1.2345E-1": "0.12345", + "0e5": "0", + "0e-5": "0", + "0.e0": "0", + ".0e0": "0", + "123.456e0": "123.456", + "123.456e2": "12345.6", + "123.456e10": "1234560000000", +} + +var testMalformedDecimalStrings = map[string]error{ + "1ee10": fmt.Errorf("can't convert %s to decimal: multiple 'E' characters found", "1ee10"), + "123.45.66": fmt.Errorf("can't convert %s to decimal: too many .s", "123.45.66"), +} + +func init() { + for _, s := range testTable { + s.exact = strconv.FormatFloat(s.float, 'f', 1500, 64) + if strings.ContainsRune(s.exact, '.') { + s.exact = strings.TrimRight(s.exact, "0") + s.exact = strings.TrimRight(s.exact, ".") + } + } + + // add negatives + withNeg := testTable[:] + for _, s := range testTable { + if s.float > 0 && s.short != "0" && s.exact != "0" { + withNeg = append(withNeg, &testEnt{-s.float, "-" + s.short, "-" + s.exact, "-" + s.inexact}) + } + } + testTable = withNeg + + for e, s := range testTableScientificNotation { + if string(e[0]) != "-" && s != "0" { + testTableScientificNotation["-"+e] = "-" + s + } + } +} + +func TestNewFromFloat(t *testing.T) { + for _, x := range testTable { + s := x.short + d := NewFromFloat(x.float) + if d.String() != s { + t.Errorf("expected %s, got %s (float: %v) (%s, %d)", + s, d.String(), x.float, + d.value.String(), d.exp) + } + } + + shouldPanicOn := []float64{ + math.NaN(), + math.Inf(1), + math.Inf(-1), + } + + for _, n := range shouldPanicOn { + var d Decimal + if !didPanic(func() { d = NewFromFloat(n) }) { + t.Fatalf("Expected panic when creating a Decimal from %v, got %v instead", n, d.String()) + } + } +} + +func TestNewFromFloatRandom(t *testing.T) { + n := 0 + rng := rand.New(rand.NewSource(0xdead1337)) + for { + n++ + if n == 10 { + break + } + in := (rng.Float64() - 0.5) * math.MaxFloat64 * 2 + want, err := NewFromString(strconv.FormatFloat(in, 'f', -1, 64)) + if err != nil { + t.Error(err) + continue + } + got := NewFromFloat(in) + if !want.Equal(got) { + t.Errorf("in: %v, expected %s (%s, %d), got %s (%s, %d) ", + in, want.String(), want.value.String(), want.exp, + got.String(), got.value.String(), got.exp) + } + } +} + +func TestNewFromFloatQuick(t *testing.T) { + err := quick.Check(func(f float64) bool { + want, werr := NewFromString(strconv.FormatFloat(f, 'f', -1, 64)) + if werr != nil { + return true + } + got := NewFromFloat(f) + return got.Equal(want) + }, &quick.Config{}) + if err != nil { + t.Error(err) + } +} + +func TestNewFromFloat32Random(t *testing.T) { + n := 0 + rng := rand.New(rand.NewSource(0xdead1337)) + for { + n++ + if n == 10 { + break + } + in := float32((rng.Float64() - 0.5) * math.MaxFloat32 * 2) + want, err := NewFromString(strconv.FormatFloat(float64(in), 'f', -1, 32)) + if err != nil { + t.Error(err) + continue + } + got := NewFromFloat32(in) + if !want.Equal(got) { + t.Errorf("in: %v, expected %s (%s, %d), got %s (%s, %d) ", + in, want.String(), want.value.String(), want.exp, + got.String(), got.value.String(), got.exp) + } + } +} + +func TestNewFromFloat32Quick(t *testing.T) { + err := quick.Check(func(f float32) bool { + want, werr := NewFromString(strconv.FormatFloat(float64(f), 'f', -1, 32)) + if werr != nil { + return true + } + got := NewFromFloat32(f) + return got.Equal(want) + }, &quick.Config{}) + if err != nil { + t.Error(err) + } +} + +func TestNewFromString(t *testing.T) { + for _, x := range testTable { + s := x.short + d, err := NewFromString(s) + if err != nil { + t.Errorf("error while parsing %s", s) + } else if d.String() != s { + t.Errorf("expected %s, got %s (%s, %d)", + s, d.String(), + d.value.String(), d.exp) + } + } + + for _, x := range testTable { + s := x.exact + d, err := NewFromString(s) + if err != nil { + t.Errorf("error while parsing %s", s) + } else if d.String() != s { + t.Errorf("expected %s, got %s (%s, %d)", + s, d.String(), + d.value.String(), d.exp) + } + } + + for e, s := range testTableScientificNotation { + d, err := NewFromString(e) + if err != nil { + t.Errorf("error while parsing %s", e) + } else if d.String() != s { + t.Errorf("expected %s, got %s (%s, %d)", + s, d.String(), + d.value.String(), d.exp) + } + } + + for s, e := range testMalformedDecimalStrings { + _, err := NewFromString(s) + if err == nil { + t.Errorf("expected an error, got nil %s", s) + } else if err.Error() != e.Error() { + t.Errorf("expected %v error, got %v", e, err) + } + } +} + +func TestFloat64(t *testing.T) { + for _, x := range testTable { + if x.inexact == "" || x.inexact == "-" { + continue + } + s := x.exact + d, err := NewFromString(s) + if err != nil { + t.Errorf("error while parsing %s", s) + } else if f, exact := d.Float64(); !exact || f != x.float { + t.Errorf("cannot represent exactly %s", s) + } + s = x.inexact + d, err = NewFromString(s) + if err != nil { + t.Errorf("error while parsing %s", s) + } else if f, exact := d.Float64(); exact || f != x.float { + t.Errorf("%s should be represented inexactly", s) + } + } +} + +func TestNewFromStringErrs(t *testing.T) { + tests := []string{ + "", + "qwert", + "-", + ".", + "-.", + ".-", + "234-.56", + "234-56", + "2-", + "..", + "2..", + "..2", + ".5.2", + "8..2", + "8.1.", + "1e", + "1-e", + "1e9e", + "1ee9", + "1ee", + "1eE", + "1e-", + "1e-.", + "1e1.2", + "123.456e1.3", + "1e-1.2", + "123.456e-1.3", + "123.456Easdf", + "123.456e" + strconv.FormatInt(math.MinInt64, 10), + "123.456e" + strconv.FormatInt(math.MinInt32, 10), + "512.99 USD", + "$99.99", + "51,850.00", + "20_000_000.00", + "$20_000_000.00", + } + + for _, s := range tests { + _, err := NewFromString(s) + + if err == nil { + t.Errorf("error expected when parsing %s", s) + } + } +} + +func TestNewFromStringDeepEquals(t *testing.T) { + type StrCmp struct { + str1 string + str2 string + expected bool + } + tests := []StrCmp{ + {"1", "1", true}, + {"1.0", "1.0", true}, + {"10", "10.0", false}, + {"1.1", "1.10", false}, + {"1.001", "1.01", false}, + } + + for _, cmp := range tests { + d1, err1 := NewFromString(cmp.str1) + d2, err2 := NewFromString(cmp.str2) + + if err1 != nil || err2 != nil { + t.Errorf("error parsing strings to decimals") + } + + if reflect.DeepEqual(d1, d2) != cmp.expected { + t.Errorf("comparison result is different from expected results for %s and %s", + cmp.str1, cmp.str2) + } + } +} + +func TestRequireFromString(t *testing.T) { + s := "1.23" + defer func() { + err := recover() + if err != nil { + t.Errorf("error while parsing %s", s) + } + }() + + d := RequireFromString(s) + if d.String() != s { + t.Errorf("expected %s, got %s (%s, %d)", + s, d.String(), + d.value.String(), d.exp) + } +} + +func TestRequireFromStringErrs(t *testing.T) { + s := "qwert" + var d Decimal + var err interface{} + + func(d Decimal) { + defer func() { + err = recover() + }() + + RequireFromString(s) + }(d) + + if err == nil { + t.Errorf("panic expected when parsing %s", s) + } +} + +func TestNewFromFloatWithExponent(t *testing.T) { + type Inp struct { + float float64 + exp int32 + } + // some tests are taken from here https://www.cockroachlabs.com/blog/rounding-implementations-in-go/ + tests := map[Inp]string{ + Inp{123.4, -3}: "123.4", + Inp{123.4, -1}: "123.4", + Inp{123.412345, 1}: "120", + Inp{123.412345, 0}: "123", + Inp{123.412345, -5}: "123.41235", + Inp{123.412345, -6}: "123.412345", + Inp{123.412345, -7}: "123.412345", + Inp{123.412345, -28}: "123.4123450000000019599610823207", + Inp{1230000000, 3}: "1230000000", + Inp{123.9999999999999999, -7}: "124", + Inp{123.8989898999999999, -7}: "123.8989899", + Inp{0.49999999999999994, 0}: "0", + Inp{0.5, 0}: "1", + Inp{0., -1000}: "0", + Inp{0.5000000000000001, 0}: "1", + Inp{1.390671161567e-309, 0}: "0", + Inp{4.503599627370497e+15, 0}: "4503599627370497", + Inp{4.503599627370497e+60, 0}: "4503599627370497110902645731364739935039854989106233267453952", + Inp{4.503599627370497e+60, 1}: "4503599627370497110902645731364739935039854989106233267453950", + Inp{4.503599627370497e+60, -1}: "4503599627370497110902645731364739935039854989106233267453952", + Inp{50, 2}: "100", + Inp{49, 2}: "0", + Inp{50, 3}: "0", + // subnormals + Inp{1.390671161567e-309, -2000}: "0.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001390671161567000864431395448332752540137009987788957394095829635554502771758698872408926974382819387852542087331897381878220271350970912568035007740861074263206736245957501456549756342151614772544950978154339064833880234531754156635411349342950306987480369774780312897442981323940546749863054846093718407237782253156822124910364044261653195961209878120072488178603782495270845071470243842997312255994555557251870400944414666445871039673491570643357351279578519863428540219295076767898526278029257129758694673164251056158277568765100904638511604478844087596428177947970563689475826736810456067108202083804368114484417399279328807983736233036662284338182105684628835292230438999173947056675615385756827890872955322265625", + Inp{1.390671161567e-309, -862}: "0.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000013906711615670008644313954483327525401370099877889573940958296355545027717586988724089269743828193878525420873318973818782202713509709125680350077408610742632067362459575014565497563421516147725449509781543390648338802345317541566354113493429503069874803697747803128974429813239405467498630548460937184072377822531568221249103640442616531959612098781200724881786037824952708450714702438429973122559945555572518704009444146664458710396734915706433573512795785198634285402192950767678985262780292571297586946731642510561582775687651009046385116044788440876", + Inp{1.390671161567e-309, -863}: "0.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000013906711615670008644313954483327525401370099877889573940958296355545027717586988724089269743828193878525420873318973818782202713509709125680350077408610742632067362459575014565497563421516147725449509781543390648338802345317541566354113493429503069874803697747803128974429813239405467498630548460937184072377822531568221249103640442616531959612098781200724881786037824952708450714702438429973122559945555572518704009444146664458710396734915706433573512795785198634285402192950767678985262780292571297586946731642510561582775687651009046385116044788440876", + } + + // add negatives + for p, s := range tests { + if p.float > 0 { + if s != "0" { + tests[Inp{-p.float, p.exp}] = "-" + s + } else { + tests[Inp{-p.float, p.exp}] = "0" + } + } + } + + for input, s := range tests { + d := NewFromFloatWithExponent(input.float, input.exp) + if d.String() != s { + t.Errorf("expected %s, got %s (%s, %d)", + s, d.String(), + d.value.String(), d.exp) + } + } + + shouldPanicOn := []float64{ + math.NaN(), + math.Inf(1), + math.Inf(-1), + } + + for _, n := range shouldPanicOn { + var d Decimal + if !didPanic(func() { d = NewFromFloatWithExponent(n, 0) }) { + t.Fatalf("Expected panic when creating a Decimal from %v, got %v instead", n, d.String()) + } + } +} + +func TestNewFromInt(t *testing.T) { + tests := map[int64]string{ + 0: "0", + 1: "1", + 323412345: "323412345", + 9223372036854775807: "9223372036854775807", + -9223372036854775808: "-9223372036854775808", + } + + // add negatives + for p, s := range tests { + if p > 0 { + tests[-p] = "-" + s + } + } + + for input, s := range tests { + d := NewFromInt(input) + if d.String() != s { + t.Errorf("expected %s, got %s (%s, %d)", + s, d.String(), + d.value.String(), d.exp) + } + } +} + +func TestNewFromInt32(t *testing.T) { + tests := map[int32]string{ + 0: "0", + 1: "1", + 323412345: "323412345", + 2147483647: "2147483647", + -2147483648: "-2147483648", + } + + // add negatives + for p, s := range tests { + if p > 0 { + tests[-p] = "-" + s + } + } + + for input, s := range tests { + d := NewFromInt32(input) + if d.String() != s { + t.Errorf("expected %s, got %s (%s, %d)", + s, d.String(), + d.value.String(), d.exp) + } + } +} + +func TestNewFromUint64(t *testing.T) { + tests := map[uint64]string{ + 0: "0", + 1: "1", + 323412345: "323412345", + 9223372036854775807: "9223372036854775807", + 18446744073709551615: "18446744073709551615", + } + + for input, s := range tests { + d := NewFromUint64(input) + if d.String() != s { + t.Errorf("expected %s, got %s (%s, %d)", + s, d.String(), + d.value.String(), d.exp) + } + } +} + +func TestNewFromBigIntWithExponent(t *testing.T) { + type Inp struct { + val *big.Int + exp int32 + } + tests := map[Inp]string{ + Inp{big.NewInt(123412345), -3}: "123412.345", + Inp{big.NewInt(2234), -1}: "223.4", + Inp{big.NewInt(323412345), 1}: "3234123450", + Inp{big.NewInt(423412345), 0}: "423412345", + Inp{big.NewInt(52341235), -5}: "523.41235", + Inp{big.NewInt(623412345), -6}: "623.412345", + Inp{big.NewInt(723412345), -7}: "72.3412345", + } + + // add negatives + for p, s := range tests { + if p.val.Cmp(zeroInt) > 0 { + tests[Inp{p.val.Neg(p.val), p.exp}] = "-" + s + } + } + + for input, s := range tests { + d := NewFromBigInt(input.val, input.exp) + if d.String() != s { + t.Errorf("expected %s, got %s (%s, %d)", + s, d.String(), + d.value.String(), d.exp) + } + } +} + +func TestNewFromBigIntCopiesInput(t *testing.T) { + input := big.NewInt(12345) + d := NewFromBigInt(input, -2) + + input.SetInt64(0) + + if got, want := d.String(), "123.45"; got != want { + t.Fatalf("NewFromBigInt retained mutable input pointer: got %s, want %s", got, want) + } +} + +func TestNewFromBigRat(t *testing.T) { + mustParseRat := func(val string) *big.Rat { + num, _ := new(big.Rat).SetString(val) + return num + } + + type Inp struct { + val *big.Rat + prec int32 + } + + tests := map[Inp]string{ + Inp{big.NewRat(0, 1), 16}: "0", + Inp{big.NewRat(4, 5), 16}: "0.8", + Inp{big.NewRat(10, 2), 16}: "5", + Inp{big.NewRat(1023427554493, 43432632), 16}: "23563.5628642767953828", // rounded + Inp{big.NewRat(1, 434324545566634), 16}: "0.0000000000000023", + Inp{big.NewRat(1, 3), 16}: "0.3333333333333333", + Inp{big.NewRat(2, 3), 2}: "0.67", // rounded + Inp{big.NewRat(2, 3), 16}: "0.6666666666666667", // rounded + Inp{big.NewRat(10000, 3), 16}: "3333.3333333333333333", + Inp{mustParseRat("30702832066636633479"), 16}: "30702832066636633479", + Inp{mustParseRat("487028320159896636679.1827512895753"), 16}: "487028320159896636679.1827512895753", + Inp{mustParseRat("127028320612589896636633479.173582751289575278357832"), -2}: "127028320612589896636633500", // rounded + Inp{mustParseRat("127028320612589896636633479.173582751289575278357832"), 16}: "127028320612589896636633479.1735827512895753", // rounded + Inp{mustParseRat("127028320612589896636633479.173582751289575278357832"), 32}: "127028320612589896636633479.173582751289575278357832", + } + + // add negatives + for p, s := range tests { + if p.val.Cmp(new(big.Rat)) > 0 { + tests[Inp{p.val.Neg(p.val), p.prec}] = "-" + s + } + } + + for input, s := range tests { + d := NewFromBigRat(input.val, input.prec) + if d.String() != s { + t.Errorf("expected %s, got %s (%s, %d)", + s, d.String(), + d.value.String(), d.exp) + } + } +} + +func TestNewFromBigRatCopiesInput(t *testing.T) { + input := big.NewRat(2, 3) + d := NewFromBigRat(input, 4) + + input.SetInt64(0) + + if got, want := d.String(), "0.6667"; got != want { + t.Fatalf("NewFromBigRat retained mutable input pointer: got %s, want %s", got, want) + } +} + +func TestCopy(t *testing.T) { + origin := New(1, 0) + cpy := origin.Copy() + + if origin.value == cpy.value { + t.Error("expecting copy and origin to have different value pointers") + } + + if cpy.Cmp(origin) != 0 { + t.Error("expecting copy and origin to be equals, but they are not") + } + + //change value + cpy = cpy.Add(New(1, 0)) + + if cpy.Cmp(origin) == 0 { + t.Error("expecting copy and origin to have different values, but they are equal") + } +} + +func TestJSON(t *testing.T) { + for _, x := range testTable { + s := x.short + var doc struct { + Amount Decimal `json:"amount"` + } + docStr := `{"amount":"` + s + `"}` + docStrNumber := `{"amount":` + s + `}` + err := json.Unmarshal([]byte(docStr), &doc) + if err != nil { + t.Errorf("error unmarshaling %s: %v", docStr, err) + } else if doc.Amount.String() != s { + t.Errorf("expected %s, got %s (%s, %d)", + s, doc.Amount.String(), + doc.Amount.value.String(), doc.Amount.exp) + } + + out, err := json.Marshal(&doc) + if err != nil { + t.Errorf("error marshaling %+v: %v", doc, err) + } else if string(out) != docStr { + t.Errorf("expected %s, got %s", docStr, string(out)) + } + + // make sure unquoted marshalling works too + MarshalJSONWithoutQuotes = true + out, err = json.Marshal(&doc) + if err != nil { + t.Errorf("error marshaling %+v: %v", doc, err) + } else if string(out) != docStrNumber { + t.Errorf("expected %s, got %s", docStrNumber, string(out)) + } + MarshalJSONWithoutQuotes = false + } +} + +func TestUnmarshalJSONNull(t *testing.T) { + var doc struct { + Amount Decimal `json:"amount"` + } + docStr := `{"amount": null}` + err := json.Unmarshal([]byte(docStr), &doc) + if err != nil { + t.Errorf("error unmarshaling %s: %v", docStr, err) + } else if !doc.Amount.Equal(Zero) { + t.Errorf("expected Zero, got %s (%s, %d)", + doc.Amount.String(), + doc.Amount.value.String(), doc.Amount.exp) + } +} + +func TestBadJSON(t *testing.T) { + for _, testCase := range []string{ + "]o_o[", + "{", + `{"amount":""`, + `{"amount":""}`, + `{"amount":"nope"}`, + `0.333`, + } { + var doc struct { + Amount Decimal `json:"amount"` + } + err := json.Unmarshal([]byte(testCase), &doc) + if err == nil { + t.Errorf("expected error, got %+v", doc) + } + } +} + +func TestNullDecimalJSON(t *testing.T) { + for _, x := range testTable { + s := x.short + var doc struct { + Amount NullDecimal `json:"amount"` + } + docStr := `{"amount":"` + s + `"}` + docStrNumber := `{"amount":` + s + `}` + err := json.Unmarshal([]byte(docStr), &doc) + if err != nil { + t.Errorf("error unmarshaling %s: %v", docStr, err) + } else { + if !doc.Amount.Valid { + t.Errorf("expected %s to be valid (not NULL), got Valid = false", s) + } + if doc.Amount.Decimal.String() != s { + t.Errorf("expected %s, got %s (%s, %d)", + s, doc.Amount.Decimal.String(), + doc.Amount.Decimal.value.String(), doc.Amount.Decimal.exp) + } + } + + out, err := json.Marshal(&doc) + if err != nil { + t.Errorf("error marshaling %+v: %v", doc, err) + } else if string(out) != docStr { + t.Errorf("expected %s, got %s", docStr, string(out)) + } + + // make sure unquoted marshalling works too + MarshalJSONWithoutQuotes = true + out, err = json.Marshal(&doc) + if err != nil { + t.Errorf("error marshaling %+v: %v", doc, err) + } else if string(out) != docStrNumber { + t.Errorf("expected %s, got %s", docStrNumber, string(out)) + } + MarshalJSONWithoutQuotes = false + } + + var doc struct { + Amount NullDecimal `json:"amount"` + } + docStr := `{"amount": null}` + err := json.Unmarshal([]byte(docStr), &doc) + if err != nil { + t.Errorf("error unmarshaling %s: %v", docStr, err) + } else if doc.Amount.Valid { + t.Errorf("expected null value to have Valid = false, got Valid = true and Decimal = %s (%s, %d)", + doc.Amount.Decimal.String(), + doc.Amount.Decimal.value.String(), doc.Amount.Decimal.exp) + } + + expected := `{"amount":null}` + out, err := json.Marshal(&doc) + if err != nil { + t.Errorf("error marshaling %+v: %v", doc, err) + } else if string(out) != expected { + t.Errorf("expected %s, got %s", expected, string(out)) + } + + // make sure unquoted marshalling works too + MarshalJSONWithoutQuotes = true + expectedUnquoted := `{"amount":null}` + out, err = json.Marshal(&doc) + if err != nil { + t.Errorf("error marshaling %+v: %v", doc, err) + } else if string(out) != expectedUnquoted { + t.Errorf("expected %s, got %s", expectedUnquoted, string(out)) + } + MarshalJSONWithoutQuotes = false +} + +func TestNullDecimalBadJSON(t *testing.T) { + for _, testCase := range []string{ + "]o_o[", + "{", + `{"amount":""`, + `{"amount":""}`, + `{"amount":"nope"}`, + `{"amount":nope}`, + `0.333`, + } { + var doc struct { + Amount NullDecimal `json:"amount"` + } + err := json.Unmarshal([]byte(testCase), &doc) + if err == nil { + t.Errorf("expected error, got %+v", doc) + } + } +} + +func TestXML(t *testing.T) { + for _, x := range testTable { + s := x.short + var doc struct { + XMLName xml.Name `xml:"account"` + Amount Decimal `xml:"amount"` + } + docStr := `` + s + `` + err := xml.Unmarshal([]byte(docStr), &doc) + if err != nil { + t.Errorf("error unmarshaling %s: %v", docStr, err) + } else if doc.Amount.String() != s { + t.Errorf("expected %s, got %s (%s, %d)", + s, doc.Amount.String(), + doc.Amount.value.String(), doc.Amount.exp) + } + + out, err := xml.Marshal(&doc) + if err != nil { + t.Errorf("error marshaling %+v: %v", doc, err) + } else if string(out) != docStr { + t.Errorf("expected %s, got %s", docStr, string(out)) + } + } +} + +func TestBadXML(t *testing.T) { + for _, testCase := range []string{ + "o_o", + "7", + ``, + ``, + `nope`, + `0.333`, + } { + var doc struct { + XMLName xml.Name `xml:"account"` + Amount Decimal `xml:"amount"` + } + err := xml.Unmarshal([]byte(testCase), &doc) + if err == nil { + t.Errorf("expected error, got %+v", doc) + } + } +} + +func TestNullDecimalXML(t *testing.T) { + // test valid values + for _, x := range testTable { + s := x.short + var doc struct { + XMLName xml.Name `xml:"account"` + Amount NullDecimal `xml:"amount"` + } + docStr := `` + s + `` + err := xml.Unmarshal([]byte(docStr), &doc) + if err != nil { + t.Errorf("error unmarshaling %s: %v", docStr, err) + } else if doc.Amount.Decimal.String() != s { + t.Errorf("expected %s, got %s (%s, %d)", + s, doc.Amount.Decimal.String(), + doc.Amount.Decimal.value.String(), doc.Amount.Decimal.exp) + } + + out, err := xml.Marshal(&doc) + if err != nil { + t.Errorf("error marshaling %+v: %v", doc, err) + } else if string(out) != docStr { + t.Errorf("expected %s, got %s", docStr, string(out)) + } + } + + var doc struct { + XMLName xml.Name `xml:"account"` + Amount NullDecimal `xml:"amount"` + } + + // test for XML with empty body + docStr := `` + err := xml.Unmarshal([]byte(docStr), &doc) + if err != nil { + t.Errorf("error unmarshaling: %s: %v", docStr, err) + } else if doc.Amount.Valid { + t.Errorf("expected null value to have Valid = false, got Valid = true and Decimal = %s (%s, %d)", + doc.Amount.Decimal.String(), + doc.Amount.Decimal.value.String(), doc.Amount.Decimal.exp) + } + + expected := `` + out, err := xml.Marshal(&doc) + if err != nil { + t.Errorf("error marshaling %+v: %v", doc, err) + } else if string(out) != expected { + t.Errorf("expected %s, got %s", expected, string(out)) + } + + // test for empty XML + docStr = `` + err = xml.Unmarshal([]byte(docStr), &doc) + if err != nil { + t.Errorf("error unmarshaling: %s: %v", docStr, err) + } else if doc.Amount.Valid { + t.Errorf("expected null value to have Valid = false, got Valid = true and Decimal = %s (%s, %d)", + doc.Amount.Decimal.String(), + doc.Amount.Decimal.value.String(), doc.Amount.Decimal.exp) + } + + expected = `` + out, err = xml.Marshal(&doc) + if err != nil { + t.Errorf("error marshaling %+v: %v", doc, err) + } else if string(out) != expected { + t.Errorf("expected %s, got %s", expected, string(out)) + } +} + +func TestNullDecimalBadXML(t *testing.T) { + for _, testCase := range []string{ + "o_o", + "7", + ``, + `nope`, + `0.333`, + } { + var doc struct { + XMLName xml.Name `xml:"account"` + Amount NullDecimal `xml:"amount"` + } + err := xml.Unmarshal([]byte(testCase), &doc) + if err == nil { + t.Errorf("expected error, got %+v", doc) + } + } +} + +func TestDecimal_rescale(t *testing.T) { + type Inp struct { + int int64 + exp int32 + rescale int32 + } + tests := map[Inp]string{ + Inp{1234, -3, -5}: "1.234", + Inp{1234, -3, 0}: "1", + Inp{1234, 3, 0}: "1234000", + Inp{1234, -4, -4}: "0.1234", + } + + // add negatives + for p, s := range tests { + if p.int > 0 { + tests[Inp{-p.int, p.exp, p.rescale}] = "-" + s + } + } + + for input, s := range tests { + d := New(input.int, input.exp).rescale(input.rescale) + + if d.String() != s { + t.Errorf("expected %s, got %s (%s, %d)", + s, d.String(), + d.value.String(), d.exp) + } + } +} + +func TestDecimal_Floor(t *testing.T) { + assertFloor := func(input, expected Decimal) { + got := input.Floor() + if !got.Equal(expected) { + t.Errorf("Floor(%s): got %s, expected %s", input, got, expected) + } + } + type testDataString struct { + input string + expected string + } + testsWithStrings := []testDataString{ + {"1.999", "1"}, + {"1", "1"}, + {"1.01", "1"}, + {"0", "0"}, + {"0.9", "0"}, + {"0.1", "0"}, + {"-0.9", "-1"}, + {"-0.1", "-1"}, + {"-1.00", "-1"}, + {"-1.01", "-2"}, + {"-1.999", "-2"}, + } + for _, test := range testsWithStrings { + expected, _ := NewFromString(test.expected) + input, _ := NewFromString(test.input) + assertFloor(input, expected) + } + + type testDataDecimal struct { + input Decimal + expected string + } + testsWithDecimals := []testDataDecimal{ + {New(100, -1), "10"}, + {New(10, 0), "10"}, + {New(1, 1), "10"}, + {New(1999, -3), "1"}, + {New(101, -2), "1"}, + {New(1, 0), "1"}, + {New(0, 0), "0"}, + {New(9, -1), "0"}, + {New(1, -1), "0"}, + {New(-1, -1), "-1"}, + {New(-9, -1), "-1"}, + {New(-1, 0), "-1"}, + {New(-101, -2), "-2"}, + {New(-1999, -3), "-2"}, + } + for _, test := range testsWithDecimals { + expected, _ := NewFromString(test.expected) + assertFloor(test.input, expected) + } +} + +func TestDecimal_Ceil(t *testing.T) { + assertCeil := func(input, expected Decimal) { + got := input.Ceil() + if !got.Equal(expected) { + t.Errorf("Ceil(%s): got %s, expected %s", input, got, expected) + } + } + type testDataString struct { + input string + expected string + } + testsWithStrings := []testDataString{ + {"1.999", "2"}, + {"1", "1"}, + {"1.01", "2"}, + {"0", "0"}, + {"0.9", "1"}, + {"0.1", "1"}, + {"-0.9", "0"}, + {"-0.1", "0"}, + {"-1.00", "-1"}, + {"-1.01", "-1"}, + {"-1.999", "-1"}, + } + for _, test := range testsWithStrings { + expected, _ := NewFromString(test.expected) + input, _ := NewFromString(test.input) + assertCeil(input, expected) + } + + type testDataDecimal struct { + input Decimal + expected string + } + testsWithDecimals := []testDataDecimal{ + {New(100, -1), "10"}, + {New(10, 0), "10"}, + {New(1, 1), "10"}, + {New(1999, -3), "2"}, + {New(101, -2), "2"}, + {New(1, 0), "1"}, + {New(0, 0), "0"}, + {New(9, -1), "1"}, + {New(1, -1), "1"}, + {New(-1, -1), "0"}, + {New(-9, -1), "0"}, + {New(-1, 0), "-1"}, + {New(-101, -2), "-1"}, + {New(-1999, -3), "-1"}, + } + for _, test := range testsWithDecimals { + expected, _ := NewFromString(test.expected) + assertCeil(test.input, expected) + } +} + +func TestDecimal_RoundAndStringFixed(t *testing.T) { + type testData struct { + input string + places int32 + expected string + expectedFixed string + } + tests := []testData{ + {"1.454", 0, "1", ""}, + {"1.454", 1, "1.5", ""}, + {"1.454", 2, "1.45", ""}, + {"1.454", 3, "1.454", ""}, + {"1.454", 4, "1.454", "1.4540"}, + {"1.454", 5, "1.454", "1.45400"}, + {"1.554", 0, "2", ""}, + {"1.554", 1, "1.6", ""}, + {"1.554", 2, "1.55", ""}, + {"0.554", 0, "1", ""}, + {"0.454", 0, "0", ""}, + {"0.454", 5, "0.454", "0.45400"}, + {"0", 0, "0", ""}, + {"0", 1, "0", "0.0"}, + {"0", 2, "0", "0.00"}, + {"0", -1, "0", ""}, + {"5", 2, "5", "5.00"}, + {"5", 1, "5", "5.0"}, + {"5", 0, "5", ""}, + {"500", 2, "500", "500.00"}, + {"545", -1, "550", ""}, + {"545", -2, "500", ""}, + {"545", -3, "1000", ""}, + {"545", -4, "0", ""}, + {"499", -3, "0", ""}, + {"499", -4, "0", ""}, + } + + // add negative number tests + for _, test := range tests { + expected := test.expected + if expected != "0" { + expected = "-" + expected + } + expectedStr := test.expectedFixed + if strings.ContainsAny(expectedStr, "123456789") && expectedStr != "" { + expectedStr = "-" + expectedStr + } + tests = append(tests, + testData{"-" + test.input, test.places, expected, expectedStr}) + } + + for _, test := range tests { + d, err := NewFromString(test.input) + if err != nil { + t.Fatal(err) + } + + // test Round + expected, err := NewFromString(test.expected) + if err != nil { + t.Fatal(err) + } + got := d.Round(test.places) + if !got.Equal(expected) { + t.Errorf("Rounding %s to %d places, got %s, expected %s", + d, test.places, got, expected) + } + + // test StringFixed + if test.expectedFixed == "" { + test.expectedFixed = test.expected + } + gotStr := d.StringFixed(test.places) + if gotStr != test.expectedFixed { + t.Errorf("(%s).StringFixed(%d): got %s, expected %s", + d, test.places, gotStr, test.expectedFixed) + } + } +} + +func TestDecimal_RoundCeilAndStringFixed(t *testing.T) { + type testData struct { + input string + places int32 + expected string + expectedFixed string + } + tests := []testData{ + {"1.454", 0, "2", ""}, + {"1.454", 1, "1.5", ""}, + {"1.454", 2, "1.46", ""}, + {"1.454", 3, "1.454", ""}, + {"1.454", 4, "1.454", "1.4540"}, + {"1.454", 5, "1.454", "1.45400"}, + {"1.554", 0, "2", ""}, + {"1.554", 1, "1.6", ""}, + {"1.554", 2, "1.56", ""}, + {"0.554", 0, "1", ""}, + {"0.454", 0, "1", ""}, + {"0.454", 5, "0.454", "0.45400"}, + {"0", 0, "0", ""}, + {"0", 1, "0", "0.0"}, + {"0", 2, "0", "0.00"}, + {"0", -1, "0", ""}, + {"5", 2, "5", "5.00"}, + {"5", 1, "5", "5.0"}, + {"5", 0, "5", ""}, + {"500", 2, "500", "500.00"}, + {"500", -2, "500", ""}, + {"545", -1, "550", ""}, + {"545", -2, "600", ""}, + {"545", -3, "1000", ""}, + {"545", -4, "10000", ""}, + {"499", -3, "1000", ""}, + {"499", -4, "10000", ""}, + {"1.1001", 2, "1.11", ""}, + {"-1.1001", 2, "-1.10", ""}, + {"-1.454", 0, "-1", ""}, + {"-1.454", 1, "-1.4", ""}, + {"-1.454", 2, "-1.45", ""}, + {"-1.454", 3, "-1.454", ""}, + {"-1.454", 4, "-1.454", "-1.4540"}, + {"-1.454", 5, "-1.454", "-1.45400"}, + {"-1.554", 0, "-1", ""}, + {"-1.554", 1, "-1.5", ""}, + {"-1.554", 2, "-1.55", ""}, + {"-0.554", 0, "0", ""}, + {"-0.454", 0, "0", ""}, + {"-0.454", 5, "-0.454", "-0.45400"}, + {"-5", 2, "-5", "-5.00"}, + {"-5", 1, "-5", "-5.0"}, + {"-5", 0, "-5", ""}, + {"-500", 2, "-500", "-500.00"}, + {"-500", -2, "-500", ""}, + {"-545", -1, "-540", ""}, + {"-545", -2, "-500", ""}, + {"-545", -3, "0", ""}, + {"-545", -4, "0", ""}, + {"-499", -3, "0", ""}, + {"-499", -4, "0", ""}, + } + + for _, test := range tests { + d, err := NewFromString(test.input) + if err != nil { + t.Fatal(err) + } + + // test Round + expected, err := NewFromString(test.expected) + if err != nil { + t.Fatal(err) + } + got := d.RoundCeil(test.places) + if !got.Equal(expected) { + t.Errorf("Rounding ceil %s to %d places, got %s, expected %s", + d, test.places, got, expected) + } + + // test StringFixed + if test.expectedFixed == "" { + test.expectedFixed = test.expected + } + gotStr := got.StringFixed(test.places) + if gotStr != test.expectedFixed { + t.Errorf("(%s).StringFixed(%d): got %s, expected %s", + d, test.places, gotStr, test.expectedFixed) + } + } +} + +func TestDecimal_RoundFloorAndStringFixed(t *testing.T) { + type testData struct { + input string + places int32 + expected string + expectedFixed string + } + tests := []testData{ + {"1.454", 0, "1", ""}, + {"1.454", 1, "1.4", ""}, + {"1.454", 2, "1.45", ""}, + {"1.454", 3, "1.454", ""}, + {"1.454", 4, "1.454", "1.4540"}, + {"1.454", 5, "1.454", "1.45400"}, + {"1.554", 0, "1", ""}, + {"1.554", 1, "1.5", ""}, + {"1.554", 2, "1.55", ""}, + {"0.554", 0, "0", ""}, + {"0.454", 0, "0", ""}, + {"0.454", 5, "0.454", "0.45400"}, + {"0", 0, "0", ""}, + {"0", 1, "0", "0.0"}, + {"0", 2, "0", "0.00"}, + {"0", -1, "0", ""}, + {"5", 2, "5", "5.00"}, + {"5", 1, "5", "5.0"}, + {"5", 0, "5", ""}, + {"500", 2, "500", "500.00"}, + {"500", -2, "500", ""}, + {"545", -1, "540", ""}, + {"545", -2, "500", ""}, + {"545", -3, "0", ""}, + {"545", -4, "0", ""}, + {"499", -3, "0", ""}, + {"499", -4, "0", ""}, + {"1.1001", 2, "1.10", ""}, + {"-1.1001", 2, "-1.11", ""}, + {"-1.454", 0, "-2", ""}, + {"-1.454", 1, "-1.5", ""}, + {"-1.454", 2, "-1.46", ""}, + {"-1.454", 3, "-1.454", ""}, + {"-1.454", 4, "-1.454", "-1.4540"}, + {"-1.454", 5, "-1.454", "-1.45400"}, + {"-1.554", 0, "-2", ""}, + {"-1.554", 1, "-1.6", ""}, + {"-1.554", 2, "-1.56", ""}, + {"-0.554", 0, "-1", ""}, + {"-0.454", 0, "-1", ""}, + {"-0.454", 5, "-0.454", "-0.45400"}, + {"-5", 2, "-5", "-5.00"}, + {"-5", 1, "-5", "-5.0"}, + {"-5", 0, "-5", ""}, + {"-500", 2, "-500", "-500.00"}, + {"-500", -2, "-500", ""}, + {"-545", -1, "-550", ""}, + {"-545", -2, "-600", ""}, + {"-545", -3, "-1000", ""}, + {"-545", -4, "-10000", ""}, + {"-499", -3, "-1000", ""}, + {"-499", -4, "-10000", ""}, + } + + for _, test := range tests { + d, err := NewFromString(test.input) + if err != nil { + t.Fatal(err) + } + + // test Round + expected, err := NewFromString(test.expected) + if err != nil { + t.Fatal(err) + } + got := d.RoundFloor(test.places) + if !got.Equal(expected) { + t.Errorf("Rounding floor %s to %d places, got %s, expected %s", + d, test.places, got, expected) + } + + // test StringFixed + if test.expectedFixed == "" { + test.expectedFixed = test.expected + } + gotStr := got.StringFixed(test.places) + if gotStr != test.expectedFixed { + t.Errorf("(%s).StringFixed(%d): got %s, expected %s", + d, test.places, gotStr, test.expectedFixed) + } + } +} + +func TestDecimal_RoundUpAndStringFixed(t *testing.T) { + type testData struct { + input string + places int32 + expected string + expectedFixed string + } + tests := []testData{ + {"1.454", 0, "2", ""}, + {"1.454", 1, "1.5", ""}, + {"1.454", 2, "1.46", ""}, + {"1.454", 3, "1.454", ""}, + {"1.454", 4, "1.454", "1.4540"}, + {"1.454", 5, "1.454", "1.45400"}, + {"1.554", 0, "2", ""}, + {"1.554", 1, "1.6", ""}, + {"1.554", 2, "1.56", ""}, + {"0.554", 0, "1", ""}, + {"0.454", 0, "1", ""}, + {"0.454", 5, "0.454", "0.45400"}, + {"0", 0, "0", ""}, + {"0", 1, "0", "0.0"}, + {"0", 2, "0", "0.00"}, + {"0", -1, "0", ""}, + {"5", 2, "5", "5.00"}, + {"5", 1, "5", "5.0"}, + {"5", 0, "5", ""}, + {"500", 2, "500", "500.00"}, + {"500", -2, "500", ""}, + {"545", -1, "550", ""}, + {"545", -2, "600", ""}, + {"545", -3, "1000", ""}, + {"545", -4, "10000", ""}, + {"499", -3, "1000", ""}, + {"499", -4, "10000", ""}, + {"1.1001", 2, "1.11", ""}, + {"-1.1001", 2, "-1.11", ""}, + {"-1.454", 0, "-2", ""}, + {"-1.454", 1, "-1.5", ""}, + {"-1.454", 2, "-1.46", ""}, + {"-1.454", 3, "-1.454", ""}, + {"-1.454", 4, "-1.454", "-1.4540"}, + {"-1.454", 5, "-1.454", "-1.45400"}, + {"-1.554", 0, "-2", ""}, + {"-1.554", 1, "-1.6", ""}, + {"-1.554", 2, "-1.56", ""}, + {"-0.554", 0, "-1", ""}, + {"-0.454", 0, "-1", ""}, + {"-0.454", 5, "-0.454", "-0.45400"}, + {"-5", 2, "-5", "-5.00"}, + {"-5", 1, "-5", "-5.0"}, + {"-5", 0, "-5", ""}, + {"-500", 2, "-500", "-500.00"}, + {"-500", -2, "-500", ""}, + {"-545", -1, "-550", ""}, + {"-545", -2, "-600", ""}, + {"-545", -3, "-1000", ""}, + {"-545", -4, "-10000", ""}, + {"-499", -3, "-1000", ""}, + {"-499", -4, "-10000", ""}, + } + + for _, test := range tests { + d, err := NewFromString(test.input) + if err != nil { + t.Fatal(err) + } + + // test Round + expected, err := NewFromString(test.expected) + if err != nil { + t.Fatal(err) + } + got := d.RoundUp(test.places) + if !got.Equal(expected) { + t.Errorf("Rounding up %s to %d places, got %s, expected %s", + d, test.places, got, expected) + } + + // test StringFixed + if test.expectedFixed == "" { + test.expectedFixed = test.expected + } + gotStr := got.StringFixed(test.places) + if gotStr != test.expectedFixed { + t.Errorf("(%s).StringFixed(%d): got %s, expected %s", + d, test.places, gotStr, test.expectedFixed) + } + } +} + +func TestDecimal_RoundDownAndStringFixed(t *testing.T) { + type testData struct { + input string + places int32 + expected string + expectedFixed string + } + tests := []testData{ + {"1.454", 0, "1", ""}, + {"1.454", 1, "1.4", ""}, + {"1.454", 2, "1.45", ""}, + {"1.454", 3, "1.454", ""}, + {"1.454", 4, "1.454", "1.4540"}, + {"1.454", 5, "1.454", "1.45400"}, + {"1.554", 0, "1", ""}, + {"1.554", 1, "1.5", ""}, + {"1.554", 2, "1.55", ""}, + {"0.554", 0, "0", ""}, + {"0.454", 0, "0", ""}, + {"0.454", 5, "0.454", "0.45400"}, + {"0", 0, "0", ""}, + {"0", 1, "0", "0.0"}, + {"0", 2, "0", "0.00"}, + {"0", -1, "0", ""}, + {"5", 2, "5", "5.00"}, + {"5", 1, "5", "5.0"}, + {"5", 0, "5", ""}, + {"500", 2, "500", "500.00"}, + {"500", -2, "500", ""}, + {"545", -1, "540", ""}, + {"545", -2, "500", ""}, + {"545", -3, "0", ""}, + {"545", -4, "0", ""}, + {"499", -3, "0", ""}, + {"499", -4, "0", ""}, + {"1.1001", 2, "1.10", ""}, + {"-1.1001", 2, "-1.10", ""}, + {"-1.454", 0, "-1", ""}, + {"-1.454", 1, "-1.4", ""}, + {"-1.454", 2, "-1.45", ""}, + {"-1.454", 3, "-1.454", ""}, + {"-1.454", 4, "-1.454", "-1.4540"}, + {"-1.454", 5, "-1.454", "-1.45400"}, + {"-1.554", 0, "-1", ""}, + {"-1.554", 1, "-1.5", ""}, + {"-1.554", 2, "-1.55", ""}, + {"-0.554", 0, "0", ""}, + {"-0.454", 0, "0", ""}, + {"-0.454", 5, "-0.454", "-0.45400"}, + {"-5", 2, "-5", "-5.00"}, + {"-5", 1, "-5", "-5.0"}, + {"-5", 0, "-5", ""}, + {"-500", 2, "-500", "-500.00"}, + {"-500", -2, "-500", ""}, + {"-545", -1, "-540", ""}, + {"-545", -2, "-500", ""}, + {"-545", -3, "0", ""}, + {"-545", -4, "0", ""}, + {"-499", -3, "0", ""}, + {"-499", -4, "0", ""}, + } + + for _, test := range tests { + d, err := NewFromString(test.input) + if err != nil { + t.Fatal(err) + } + + // test Round + expected, err := NewFromString(test.expected) + if err != nil { + t.Fatal(err) + } + got := d.RoundDown(test.places) + if !got.Equal(expected) { + t.Errorf("Rounding down %s to %d places, got %s, expected %s", + d, test.places, got, expected) + } + + // test StringFixed + if test.expectedFixed == "" { + test.expectedFixed = test.expected + } + gotStr := got.StringFixed(test.places) + if gotStr != test.expectedFixed { + t.Errorf("(%s).StringFixed(%d): got %s, expected %s", + d, test.places, gotStr, test.expectedFixed) + } + } +} + +func TestDecimal_Uninitialized(t *testing.T) { + a := Decimal{} + b := Decimal{} + + decs := []Decimal{ + a, + a.rescale(10), + a.Abs(), + a.Add(b), + a.Sub(b), + a.Mul(b), + a.Shift(0), + a.Div(New(1, -1)), + a.Round(2), + a.Floor(), + a.Ceil(), + a.Truncate(2), + } + + for _, d := range decs { + if d.String() != "0" { + t.Errorf("expected 0, got %s", d.String()) + } + if d.StringFixed(3) != "0.000" { + t.Errorf("expected 0, got %s", d.StringFixed(3)) + } + } + + if a.Cmp(b) != 0 { + t.Errorf("a != b") + } + if a.Sign() != 0 { + t.Errorf("a.Sign() != 0") + } + if a.Exponent() != 0 { + t.Errorf("a.Exponent() != 0") + } + if a.IntPart() != 0 { + t.Errorf("a.IntPar() != 0") + } + f, _ := a.Float64() + if f != 0 { + t.Errorf("a.Float64() != 0") + } + if a.Rat().RatString() != "0" { + t.Errorf("a.Rat() != 0, got %s", a.Rat().RatString()) + } +} + +func TestDecimal_Add(t *testing.T) { + type Inp struct { + a string + b string + } + + inputs := map[Inp]string{ + Inp{"2", "3"}: "5", + Inp{"2454495034", "3451204593"}: "5905699627", + Inp{"24544.95034", ".3451204593"}: "24545.2954604593", + Inp{".1", ".1"}: "0.2", + Inp{".1", "-.1"}: "0", + Inp{"0", "1.001"}: "1.001", + } + + for inp, res := range inputs { + a, err := NewFromString(inp.a) + if err != nil { + t.FailNow() + } + b, err := NewFromString(inp.b) + if err != nil { + t.FailNow() + } + c := a.Add(b) + if c.String() != res { + t.Errorf("expected %s, got %s", res, c.String()) + } + } +} + +func TestDecimal_Sub(t *testing.T) { + type Inp struct { + a string + b string + } + + inputs := map[Inp]string{ + Inp{"2", "3"}: "-1", + Inp{"12", "3"}: "9", + Inp{"-2", "9"}: "-11", + Inp{"2454495034", "3451204593"}: "-996709559", + Inp{"24544.95034", ".3451204593"}: "24544.6052195407", + Inp{".1", "-.1"}: "0.2", + Inp{".1", ".1"}: "0", + Inp{"0", "1.001"}: "-1.001", + Inp{"1.001", "0"}: "1.001", + Inp{"2.3", ".3"}: "2", + } + + for inp, res := range inputs { + a, err := NewFromString(inp.a) + if err != nil { + t.FailNow() + } + b, err := NewFromString(inp.b) + if err != nil { + t.FailNow() + } + c := a.Sub(b) + if c.String() != res { + t.Errorf("expected %s, got %s", res, c.String()) + } + } +} + +func TestDecimal_Neg(t *testing.T) { + inputs := map[string]string{ + "0": "0", + "10": "-10", + "5.56": "-5.56", + "-10": "10", + "-5.56": "5.56", + } + + for inp, res := range inputs { + a, err := NewFromString(inp) + if err != nil { + t.FailNow() + } + b := a.Neg() + if b.String() != res { + t.Errorf("expected %s, got %s", res, b.String()) + } + } +} + +func TestDecimal_NegFromEmpty(t *testing.T) { + a := Decimal{} + b := a.Neg() + if b.String() != "0" { + t.Errorf("expected %s, got %s", "0", b) + } +} + +func TestDecimal_Mul(t *testing.T) { + type Inp struct { + a string + b string + } + + inputs := map[Inp]string{ + Inp{"2", "3"}: "6", + Inp{"2454495034", "3451204593"}: "8470964534836491162", + Inp{"24544.95034", ".3451204593"}: "8470.964534836491162", + Inp{".1", ".1"}: "0.01", + Inp{"0", "1.001"}: "0", + } + + for inp, res := range inputs { + a, err := NewFromString(inp.a) + if err != nil { + t.FailNow() + } + b, err := NewFromString(inp.b) + if err != nil { + t.FailNow() + } + c := a.Mul(b) + if c.String() != res { + t.Errorf("expected %s, got %s", res, c.String()) + } + } + + // positive scale + c := New(1234, 5).Mul(New(45, -1)) + if c.String() != "555300000" { + t.Errorf("Expected %s, got %s", "555300000", c.String()) + } +} + +func TestDecimal_Shift(t *testing.T) { + type Inp struct { + a string + b int32 + } + + inputs := map[Inp]string{ + Inp{"6", 3}: "6000", + Inp{"10", -2}: "0.1", + Inp{"2.2", 1}: "22", + Inp{"-2.2", -1}: "-0.22", + Inp{"12.88", 5}: "1288000", + Inp{"-10234274355545544493", -3}: "-10234274355545544.493", + Inp{"-4612301402398.4753343454", 5}: "-461230140239847533.43454", + } + + for inp, expectedStr := range inputs { + num, _ := NewFromString(inp.a) + + got := num.Shift(inp.b) + expected, _ := NewFromString(expectedStr) + if !got.Equal(expected) { + t.Errorf("expected %v when shifting %v by %v, got %v", + expected, num, inp.b, got) + } + } +} + +func TestDecimal_Div(t *testing.T) { + type Inp struct { + a string + b string + } + + inputs := map[Inp]string{ + Inp{"6", "3"}: "2", + Inp{"10", "2"}: "5", + Inp{"2.2", "1.1"}: "2", + Inp{"-2.2", "-1.1"}: "2", + Inp{"12.88", "5.6"}: "2.3", + Inp{"1023427554493", "43432632"}: "23563.5628642767953828", // rounded + Inp{"1", "434324545566634"}: "0.0000000000000023", + Inp{"1", "3"}: "0.3333333333333333", + Inp{"2", "3"}: "0.6666666666666667", // rounded + Inp{"10000", "3"}: "3333.3333333333333333", + Inp{"10234274355545544493", "-3"}: "-3411424785181848164.3333333333333333", + Inp{"-4612301402398.4753343454", "23.5"}: "-196268144782.9138440146978723", + } + + for inp, expectedStr := range inputs { + num, err := NewFromString(inp.a) + if err != nil { + t.FailNow() + } + denom, err := NewFromString(inp.b) + if err != nil { + t.FailNow() + } + got := num.Div(denom) + expected, _ := NewFromString(expectedStr) + if !got.Equal(expected) { + t.Errorf("expected %v when dividing %v by %v, got %v", + expected, num, denom, got) + } + got2 := num.DivRound(denom, int32(DivisionPrecision)) + if !got2.Equal(expected) { + t.Errorf("expected %v on DivRound (%v,%v), got %v", expected, num, denom, got2) + } + } + + type Inp2 struct { + n int64 + exp int32 + n2 int64 + exp2 int32 + } + + // test code path where exp > 0 + inputs2 := map[Inp2]string{ + Inp2{124, 10, 3, 1}: "41333333333.3333333333333333", + Inp2{124, 10, 3, 0}: "413333333333.3333333333333333", + Inp2{124, 10, 6, 1}: "20666666666.6666666666666667", + Inp2{124, 10, 6, 0}: "206666666666.6666666666666667", + Inp2{10, 10, 10, 1}: "1000000000", + } + + for inp, expectedAbs := range inputs2 { + for i := -1; i <= 1; i += 2 { + for j := -1; j <= 1; j += 2 { + n := inp.n * int64(i) + n2 := inp.n2 * int64(j) + num := New(n, inp.exp) + denom := New(n2, inp.exp2) + expected := expectedAbs + if i != j { + expected = "-" + expectedAbs + } + got := num.Div(denom) + if got.String() != expected { + t.Errorf("expected %s when dividing %v by %v, got %v", + expected, num, denom, got) + } + } + } + } +} + +func TestDecimal_QuoRem(t *testing.T) { + type Inp4 struct { + d string + d2 string + exp int32 + q string + r string + } + cases := []Inp4{ + {"10", "1", 0, "10", "0"}, + {"1", "10", 0, "0", "1"}, + {"1", "4", 2, "0.25", "0"}, + {"1", "8", 2, "0.12", "0.04"}, + {"10", "3", 1, "3.3", "0.1"}, + {"100", "3", 1, "33.3", "0.1"}, + {"1000", "10", -3, "0", "1000"}, + {"1e-3", "2e-5", 0, "50", "0"}, + {"1e-3", "2e-3", 1, "0.5", "0"}, + {"4e-3", "0.8", 4, "5e-3", "0"}, + {"4.1e-3", "0.8", 3, "5e-3", "1e-4"}, + {"-4", "-3", 0, "1", "-1"}, + {"-4", "3", 0, "-1", "-1"}, + } + + for _, inp4 := range cases { + d, _ := NewFromString(inp4.d) + d2, _ := NewFromString(inp4.d2) + prec := inp4.exp + q, r := d.QuoRem(d2, prec) + expectedQ, _ := NewFromString(inp4.q) + expectedR, _ := NewFromString(inp4.r) + if !q.Equal(expectedQ) || !r.Equal(expectedR) { + t.Errorf("bad QuoRem division %s , %s , %d got %v, %v expected %s , %s", + inp4.d, inp4.d2, prec, q, r, inp4.q, inp4.r) + } + if !d.Equal(d2.Mul(q).Add(r)) { + t.Errorf("not fitting: d=%v, d2= %v, prec=%d, q=%v, r=%v", + d, d2, prec, q, r) + } + if !q.Equal(q.Truncate(prec)) { + t.Errorf("quotient wrong precision: d=%v, d2= %v, prec=%d, q=%v, r=%v", + d, d2, prec, q, r) + } + if r.Abs().Cmp(d2.Abs().Mul(New(1, -prec))) >= 0 { + t.Errorf("remainder too large: d=%v, d2= %v, prec=%d, q=%v, r=%v", + d, d2, prec, q, r) + } + if r.Sign()*d.Sign() < 0 { + t.Errorf("signum of divisor and rest do not match: d=%v, d2= %v, prec=%d, q=%v, r=%v", + d, d2, prec, q, r) + } + } +} + +type DivTestCase struct { + d Decimal + d2 Decimal + prec int32 +} + +func createDivTestCases() []DivTestCase { + res := make([]DivTestCase, 0) + var n int32 = 5 + a := []int{1, 2, 3, 6, 7, 10, 100, 14, 5, 400, 0, 1000000, 1000000 + 1, 1000000 - 1} + for s := -1; s < 2; s = s + 2 { // 2 + for s2 := -1; s2 < 2; s2 = s2 + 2 { // 2 + for e1 := -n; e1 <= n; e1++ { // 2n+1 + for e2 := -n; e2 <= n; e2++ { // 2n+1 + var prec int32 + for prec = -n; prec <= n; prec++ { // 2n+1 + for _, v1 := range a { // 11 + for _, v2 := range a { // 11, even if 0 is skipped + sign1 := New(int64(s), 0) + sign2 := New(int64(s2), 0) + d := sign1.Mul(New(int64(v1), e1)) + d2 := sign2.Mul(New(int64(v2), e2)) + res = append(res, DivTestCase{d, d2, prec}) + } + } + } + } + } + } + } + return res +} + +func TestDecimal_QuoRem2(t *testing.T) { + for _, tc := range createDivTestCases() { + d := tc.d + if sign(tc.d2) == 0 { + continue + } + d2 := tc.d2 + prec := tc.prec + q, r := d.QuoRem(d2, prec) + // rule 1: d = d2*q +r + if !d.Equal(d2.Mul(q).Add(r)) { + t.Errorf("not fitting, d=%v, d2=%v, prec=%d, q=%v, r=%v", + d, d2, prec, q, r) + } + // rule 2: q is integral multiple of 10^(-prec) + if !q.Equal(q.Truncate(prec)) { + t.Errorf("quotient wrong precision, d=%v, d2=%v, prec=%d, q=%v, r=%v", + d, d2, prec, q, r) + } + // rule 3: abs(r)= 0 { + t.Errorf("remainder too large, d=%v, d2=%v, prec=%d, q=%v, r=%v", + d, d2, prec, q, r) + } + // rule 4: r and d have the same sign + if r.Sign()*d.Sign() < 0 { + t.Errorf("signum of divisor and rest do not match, "+ + "d=%v, d2=%v, prec=%d, q=%v, r=%v", + d, d2, prec, q, r) + } + } +} + +// this is the old Div method from decimal +// Div returns d / d2. If it doesn't divide exactly, the result will have +// DivisionPrecision digits after the decimal point. +func (d Decimal) DivOld(d2 Decimal, prec int) Decimal { + // NOTE(vadim): division is hard, use Rat to do it + ratNum := d.Rat() + ratDenom := d2.Rat() + + quoRat := big.NewRat(0, 1).Quo(ratNum, ratDenom) + + // HACK(vadim): converting from Rat to Decimal inefficiently for now + ret, err := NewFromString(quoRat.FloatString(prec)) + if err != nil { + panic(err) // this should never happen + } + return ret +} + +func sign(d Decimal) int { + return d.Sign() +} + +// rules for rounded divide, rounded to integer +// rounded_divide(d,d2) = q +// sign q * sign (d/d2) >= 0 +// for d and d2 >0 : +// q is already rounded +// q = d/d2 + r , with r > -0.5 and r <= 0.5 +// thus q-d/d2 = r, with r > -0.5 and r <= 0.5 +// and d2 q -d = r d2 with r d2 > -d2/2 and r d2 <= d2/2 +// and 2 (d2 q -d) = x with x > -d2 and x <= d2 +// if we factor in precision then x > -d2 * 10^(-precision) and x <= d2 * 10(-precision) + +func TestDecimal_DivRound(t *testing.T) { + cases := []struct { + d string + d2 string + prec int32 + result string + }{ + {"2", "2", 0, "1"}, + {"1", "2", 0, "1"}, + {"-1", "2", 0, "-1"}, + {"-1", "-2", 0, "1"}, + {"1", "-2", 0, "-1"}, + {"1", "-20", 1, "-0.1"}, + {"1", "-20", 2, "-0.05"}, + {"1", "20.0000000000000000001", 1, "0"}, + {"1", "19.9999999999999999999", 1, "0.1"}, + } + for _, s := range cases { + d, _ := NewFromString(s.d) + d2, _ := NewFromString(s.d2) + result, _ := NewFromString(s.result) + prec := s.prec + q := d.DivRound(d2, prec) + if sign(q)*sign(d)*sign(d2) < 0 { + t.Errorf("sign of quotient wrong, got: %v/%v is about %v", d, d2, q) + } + x := q.Mul(d2).Abs().Sub(d.Abs()).Mul(New(2, 0)) + if x.Cmp(d2.Abs().Mul(New(1, -prec))) > 0 { + t.Errorf("wrong rounding, got: %v/%v prec=%d is about %v", d, d2, prec, q) + } + if x.Cmp(d2.Abs().Mul(New(-1, -prec))) <= 0 { + t.Errorf("wrong rounding, got: %v/%v prec=%d is about %v", d, d2, prec, q) + } + if !q.Equal(result) { + t.Errorf("rounded division wrong %s / %s scale %d = %s, got %v", s.d, s.d2, prec, s.result, q) + } + } +} + +func TestDecimal_DivRound2(t *testing.T) { + for _, tc := range createDivTestCases() { + d := tc.d + if sign(tc.d2) == 0 { + continue + } + d2 := tc.d2 + prec := tc.prec + q := d.DivRound(d2, prec) + if sign(q)*sign(d)*sign(d2) < 0 { + t.Errorf("sign of quotient wrong, got: %v/%v is about %v", d, d2, q) + } + x := q.Mul(d2).Abs().Sub(d.Abs()).Mul(New(2, 0)) + if x.Cmp(d2.Abs().Mul(New(1, -prec))) > 0 { + t.Errorf("wrong rounding, got: %v/%v prec=%d is about %v", d, d2, prec, q) + } + if x.Cmp(d2.Abs().Mul(New(-1, -prec))) <= 0 { + t.Errorf("wrong rounding, got: %v/%v prec=%d is about %v", d, d2, prec, q) + } + } +} + +func TestDecimal_Mod(t *testing.T) { + type Inp struct { + a string + b string + } + + inputs := map[Inp]string{ + Inp{"3", "2"}: "1", + Inp{"3451204593", "2454495034"}: "996709559", + Inp{"9999999999", "1275"}: "324", + Inp{"9999999999.9999998", "1275.49"}: "239.2399998", + Inp{"24544.95034", "0.3451204593"}: "0.3283950433", + Inp{"0.499999999999999999", "0.25"}: "0.249999999999999999", + Inp{"0.989512958912895912", "0.000001"}: "0.000000958912895912", + Inp{"0.1", "0.1"}: "0", + Inp{"0", "1.001"}: "0", + Inp{"-7.5", "2"}: "-1.5", + Inp{"7.5", "-2"}: "1.5", + Inp{"-7.5", "-2"}: "-1.5", + Inp{"41", "21"}: "20", + Inp{"400000000001", "200000000001"}: "200000000000", + } + + for inp, res := range inputs { + a, err := NewFromString(inp.a) + if err != nil { + t.FailNow() + } + b, err := NewFromString(inp.b) + if err != nil { + t.FailNow() + } + c := a.Mod(b) + if c.String() != res { + t.Errorf("expected %s, got %s", res, c.String()) + } + } +} + +func TestDecimal_Overflow(t *testing.T) { + if !didPanic(func() { New(1, math.MinInt32).Mul(New(1, math.MinInt32)) }) { + t.Fatalf("should have gotten an overflow panic") + } + if !didPanic(func() { New(1, math.MaxInt32).Mul(New(1, math.MaxInt32)) }) { + t.Fatalf("should have gotten an overflow panic") + } +} + +func TestDecimal_ExtremeValues(t *testing.T) { + // NOTE(vadim): this test takes pretty much forever + if testing.Short() { + t.Skip() + } + + // NOTE(vadim): Seriously, the numbers involved are so large that this + // test will take way too long, so mark it as success if it takes over + // 1 second. The way this test typically fails (integer overflow) is that + // a wrong result appears quickly, so if it takes a long time then it is + // probably working properly. + // Why even bother testing this? Completeness, I guess. -Vadim + const timeLimit = 1 * time.Second + test := func(f func()) { + c := make(chan bool) + go func() { + f() + close(c) + }() + select { + case <-c: + case <-time.After(timeLimit): + } + } + + test(func() { + got := New(123, math.MinInt32).Floor() + if !got.Equal(NewFromFloat(0)) { + t.Errorf("Error: got %s, expected 0", got) + } + }) + test(func() { + got := New(123, math.MinInt32).Ceil() + if !got.Equal(NewFromFloat(1)) { + t.Errorf("Error: got %s, expected 1", got) + } + }) + test(func() { + got := New(123, math.MinInt32).Rat().FloatString(10) + expected := "0.0000000000" + if got != expected { + t.Errorf("Error: got %s, expected %s", got, expected) + } + }) +} + +func TestIntPart(t *testing.T) { + for _, testCase := range []struct { + Dec string + IntPart int64 + }{ + {"0.01", 0}, + {"12.1", 12}, + {"9999.999", 9999}, + {"-32768.01234", -32768}, + } { + d, err := NewFromString(testCase.Dec) + if err != nil { + t.Fatal(err) + } + if d.IntPart() != testCase.IntPart { + t.Errorf("expect %d, got %d", testCase.IntPart, d.IntPart()) + } + } +} + +func TestBigInt(t *testing.T) { + testCases := []struct { + Dec string + BigIntRep string + }{ + {"0.0", "0"}, + {"0.00000", "0"}, + {"0.01", "0"}, + {"12.1", "12"}, + {"9999.999", "9999"}, + {"-32768.01234", "-32768"}, + {"-572372.0000000001", "-572372"}, + } + + for _, testCase := range testCases { + d, err := NewFromString(testCase.Dec) + if err != nil { + t.Fatal(err) + } + if d.BigInt().String() != testCase.BigIntRep { + t.Errorf("expect %s, got %s", testCase.BigIntRep, d.BigInt()) + } + } +} + +func TestBigIntReturnsCopy(t *testing.T) { + d := New(123, 0) + got := d.BigInt() + + got.SetInt64(0) + + if d.IntPart() != 123 { + t.Fatalf("mutating BigInt result mutated Decimal: got %s", d.String()) + } +} + +func TestBigFloat(t *testing.T) { + testCases := []struct { + Dec string + BigFloatRep string + }{ + {"0.0", "0"}, + {"0.00000", "0"}, + {"0.01", "0.01"}, + {"12.1", "12.1"}, + {"9999.999", "9999.999"}, + {"-32768.01234", "-32768.01234"}, + {"-572372.0000000001", "-572372"}, + {"512.012345123451234512345", "512.0123451"}, + {"1.010101010101010101010101010101", "1.01010101"}, + {"55555555.555555555555555555555", "55555555.56"}, + } + + for _, testCase := range testCases { + d, err := NewFromString(testCase.Dec) + if err != nil { + t.Fatal(err) + } + if d.BigFloat().String() != testCase.BigFloatRep { + t.Errorf("expect %s, got %s", testCase.BigFloatRep, d.BigFloat()) + } + } +} + +func TestDecimal_Min(t *testing.T) { + // the first element in the array is the expected answer, rest are inputs + testCases := [][]float64{ + {0, 0}, + {1, 1}, + {-1, -1}, + {1, 1, 2}, + {-2, 1, 2, -2}, + {-3, 0, 2, -2, -3}, + } + + for _, test := range testCases { + expected, input := test[0], test[1:] + expectedDecimal := NewFromFloat(expected) + decimalInput := []Decimal{} + for _, inp := range input { + d := NewFromFloat(inp) + decimalInput = append(decimalInput, d) + } + got := Min(decimalInput[0], decimalInput[1:]...) + if !got.Equal(expectedDecimal) { + t.Errorf("Expected %v, got %v, input=%+v", expectedDecimal, got, + decimalInput) + } + } +} + +func TestDecimal_Max(t *testing.T) { + // the first element in the array is the expected answer, rest are inputs + testCases := [][]float64{ + {0, 0}, + {1, 1}, + {-1, -1}, + {2, 1, 2}, + {2, 1, 2, -2}, + {3, 0, 3, -2}, + {-2, -3, -2}, + } + + for _, test := range testCases { + expected, input := test[0], test[1:] + expectedDecimal := NewFromFloat(expected) + decimalInput := []Decimal{} + for _, inp := range input { + d := NewFromFloat(inp) + decimalInput = append(decimalInput, d) + } + got := Max(decimalInput[0], decimalInput[1:]...) + if !got.Equal(expectedDecimal) { + t.Errorf("Expected %v, got %v, input=%+v", expectedDecimal, got, + decimalInput) + } + } +} + +func scanHelper(t *testing.T, dbval interface{}, expected Decimal) { + t.Helper() + + a := Decimal{} + if err := a.Scan(dbval); err != nil { + // Scan failed... no need to test result value + t.Errorf("a.Scan(%v) failed with message: %s", dbval, err) + } else if !a.Equal(expected) { + // Scan succeeded... test resulting values + t.Errorf("%s does not equal to %s", a, expected) + } +} + +func TestDecimal_Scan(t *testing.T) { + // test the Scan method that implements the sql.Scanner interface + // check different types received from various database drivers + + dbvalue := 54.33 + expected := NewFromFloat(dbvalue) + scanHelper(t, dbvalue, expected) + + // apparently MySQL 5.7.16 and returns these as float32 so we need + // to handle these as well + dbvalueFloat32 := float32(54.33) + expected = NewFromFloat(float64(dbvalueFloat32)) + scanHelper(t, dbvalueFloat32, expected) + + // at least SQLite returns an int64 when 0 is stored in the db + // and you specified a numeric format on the schema + dbvalueInt := int64(0) + expected = New(dbvalueInt, 0) + scanHelper(t, dbvalueInt, expected) + + // also test uint64 + dbvalueUint64 := uint64(2) + expected = New(2, 0) + scanHelper(t, dbvalueUint64, expected) + + // in case you specified a varchar in your SQL schema, + // the database driver may return either []byte or string + valueStr := "535.666" + dbvalueStr := []byte(valueStr) + expected, err := NewFromString(valueStr) + if err != nil { + t.Fatal(err) + } + scanHelper(t, dbvalueStr, expected) + scanHelper(t, valueStr, expected) + + type foo struct{} + a := Decimal{} + err = a.Scan(foo{}) + if err == nil { + t.Errorf("a.Scan(Foo{}) should have thrown an error but did not") + } +} + +func TestDecimal_Value(t *testing.T) { + // Make sure this does implement the database/sql's driver.Valuer interface + var d Decimal + if _, ok := interface{}(d).(driver.Valuer); !ok { + t.Error("Decimal does not implement driver.Valuer") + } + + // check that normal case is handled appropriately + a := New(1234, -2) + expected := "12.34" + value, err := a.Value() + if err != nil { + t.Errorf("Decimal(12.34).Value() failed with message: %s", err) + } else if value.(string) != expected { + t.Errorf("%s does not equal to %s", a, expected) + } +} + +// old tests after this line + +func TestDecimal_Scale(t *testing.T) { + a := New(1234, -3) + if a.Exponent() != -3 { + t.Errorf("error") + } +} + +func TestDecimal_Abs1(t *testing.T) { + a := New(-1234, -4) + b := New(1234, -4) + + c := a.Abs() + if c.Cmp(b) != 0 { + t.Errorf("error") + } +} + +func TestDecimal_Abs2(t *testing.T) { + a := New(-1234, -4) + b := New(1234, -4) + + c := b.Abs() + if c.Cmp(a) == 0 { + t.Errorf("error") + } +} + +func TestDecimal_Equalities(t *testing.T) { + a := New(1234, 3) + b := New(1234, 3) + c := New(1234, 4) + + if !a.Equal(b) { + t.Errorf("%q should equal %q", a, b) + } + if a.Equal(c) { + t.Errorf("%q should not equal %q", a, c) + } + + if !c.GreaterThan(b) { + t.Errorf("%q should be greater than %q", c, b) + } + if b.GreaterThan(c) { + t.Errorf("%q should not be greater than %q", b, c) + } + if !a.GreaterThanOrEqual(b) { + t.Errorf("%q should be greater or equal %q", a, b) + } + if !c.GreaterThanOrEqual(b) { + t.Errorf("%q should be greater or equal %q", c, b) + } + if b.GreaterThanOrEqual(c) { + t.Errorf("%q should not be greater or equal %q", b, c) + } + if !b.LessThan(c) { + t.Errorf("%q should be less than %q", a, b) + } + if c.LessThan(b) { + t.Errorf("%q should not be less than %q", a, b) + } + if !a.LessThanOrEqual(b) { + t.Errorf("%q should be less than or equal %q", a, b) + } + if !b.LessThanOrEqual(c) { + t.Errorf("%q should be less than or equal %q", a, b) + } + if c.LessThanOrEqual(b) { + t.Errorf("%q should not be less than or equal %q", a, b) + } +} + +func TestDecimal_ScalesNotEqual(t *testing.T) { + a := New(1234, 2) + b := New(1234, 3) + if a.Equal(b) { + t.Errorf("%q should not equal %q", a, b) + } +} + +func TestDecimal_Cmp1(t *testing.T) { + a := New(123, 3) + b := New(-1234, 2) + + if a.Cmp(b) != 1 { + t.Errorf("Error") + } +} + +func TestDecimal_Cmp2(t *testing.T) { + a := New(123, 3) + b := New(1234, 2) + + if a.Cmp(b) != -1 { + t.Errorf("Error") + } +} + +func TestDecimal_Pow(t *testing.T) { + for _, testCase := range []struct { + Base string + Exponent string + Expected string + }{ + {"0.0", "1.0", "0.0"}, + {"0.0", "5.7", "0.0"}, + {"0.0", "-3.2", "0.0"}, + {"3.13", "0.0", "1.0"}, + {"-591.5", "0.0", "1.0"}, + {"3.0", "3.0", "27.0"}, + {"3.0", "10.0", "59049.0"}, + {"3.13", "5.0", "300.4150512793"}, + {"4.0", "2.0", "16.0"}, + {"4.0", "-2.0", "0.0625"}, + {"629.25", "5.0", "98654323103449.5673828125"}, + {"5.0", "5.73", "10118.08037159375"}, + {"962.0", "3.2791", "6055212360.0000044205714144"}, + {"5.69169126", "5.18515912", "8242.26344757948412597909547972726268869189399260047793106028930864"}, + {"13.1337", "3.5196719618391835", "8636.856220644773844815693636723928750940666269885"}, + {"67762386.283696923", "4.85917691669163916681738", "112761146905370140621385730157437443321.91755738117317148674362233906499698561022574811238435007575701773212242750262081945556470501"}, + {"-3.0", "6.0", "729"}, + {"-13.757", "5.0", "-492740.983929899460557"}, + {"3.0", "-6.0", "0.0013717421124829"}, + {"13.757", "-5.0", "0.000002029463821"}, + {"66.12", "-7.61313", "0.000000000000013854086588876805036"}, + {"6696871.12", "-2.61313", "0.000000000000000001455988684546983"}, + {"-3.0", "-6.0", "0.0013717421124829"}, + {"-13.757", "-5.0", "-0.000002029463821"}, + } { + base, _ := NewFromString(testCase.Base) + exp, _ := NewFromString(testCase.Exponent) + expected, _ := NewFromString(testCase.Expected) + + result := base.Pow(exp) + + if result.Cmp(expected) != 0 { + t.Errorf("expected %s, got %s, for %s^%s", testCase.Expected, result.String(), testCase.Base, testCase.Exponent) + } + } +} + +func TestDecimal_PowInt32(t *testing.T) { + for _, testCase := range []struct { + Decimal string + Exponent int32 + Expected string + }{ + {"0.0", 1, "0.0"}, + {"3.13", 0, "1.0"}, + {"-591.5", 0, "1.0"}, + {"3.0", 3, "27.0"}, + {"3.0", 10, "59049.0"}, + {"3.13", 5, "300.4150512793"}, + {"629.25", 5, "98654323103449.5673828125"}, + {"-3.0", 6, "729"}, + {"-13.757", 5, "-492740.983929899460557"}, + {"3.0", -6, "0.0013717421124829"}, + {"-13.757", -5, "-0.000002029463821"}, + } { + base, _ := NewFromString(testCase.Decimal) + expected, _ := NewFromString(testCase.Expected) + + result, _ := base.PowInt32(testCase.Exponent) + + if result.Cmp(expected) != 0 { + t.Errorf("expected %s, got %s, for %s**%d", testCase.Expected, result.String(), testCase.Decimal, testCase.Exponent) + } + } +} + +func TestDecimal_PowInt32_UndefinedResult(t *testing.T) { + base := RequireFromString("0") + + _, err := base.PowInt32(0) + + if err == nil { + t.Errorf("expected error, cannot be represent undefined value of 0**0") + } +} + +func TestDecimal_PowBigInt(t *testing.T) { + for _, testCase := range []struct { + Decimal string + Exponent *big.Int + Expected string + }{ + {"3.13", big.NewInt(0), "1.0"}, + {"-591.5", big.NewInt(0), "1.0"}, + {"3.0", big.NewInt(3), "27.0"}, + {"3.0", big.NewInt(10), "59049.0"}, + {"3.13", big.NewInt(5), "300.4150512793"}, + {"629.25", big.NewInt(5), "98654323103449.5673828125"}, + {"-3.0", big.NewInt(6), "729"}, + {"-13.757", big.NewInt(5), "-492740.983929899460557"}, + {"3.0", big.NewInt(-6), "0.0013717421124829"}, + {"-13.757", big.NewInt(-5), "-0.000002029463821"}, + } { + base, _ := NewFromString(testCase.Decimal) + expected, _ := NewFromString(testCase.Expected) + + result, _ := base.PowBigInt(testCase.Exponent) + + if result.Cmp(expected) != 0 { + t.Errorf("expected %s, got %s, for %s**%d", testCase.Expected, result.String(), testCase.Decimal, testCase.Exponent) + } + } +} + +func TestDecimal_PowBigInt_UndefinedResult(t *testing.T) { + base := RequireFromString("0") + + _, err := base.PowBigInt(big.NewInt(0)) + + if err == nil { + t.Errorf("expected error, undefined value of 0**0 cannot be represented") + } +} + +func TestDecimal_IsInteger(t *testing.T) { + for _, testCase := range []struct { + Dec string + IsInteger bool + }{ + {"0", true}, + {"0.0000", true}, + {"0.01", false}, + {"0.01010101010000", false}, + {"12.0", true}, + {"12.00000000000000", true}, + {"12.10000", false}, + {"9999.0000", true}, + {"99999999.000000000", true}, + {"-656323444.0000000000000", true}, + {"-32768.01234", false}, + {"-32768.0123423562623600000", false}, + } { + d, err := NewFromString(testCase.Dec) + if err != nil { + t.Fatal(err) + } + if d.IsInteger() != testCase.IsInteger { + t.Errorf("expect %t, got %t, for %s", testCase.IsInteger, d.IsInteger(), testCase.Dec) + } + } +} + +func TestDecimal_ExpTaylor(t *testing.T) { + for _, testCase := range []struct { + Dec string + Precision int32 + ExpectedDec string + }{ + {"0", 1, "1"}, + {"0.00", 5, "1"}, + {"0", -1, "0"}, + {"0.5", 5, "1.64872"}, + {"0.569297", 10, "1.7670243965"}, + {"0.569297", 16, "1.7670243965409497"}, + {"0.569297", 20, "1.76702439654094965215"}, + {"1.0", 0, "3"}, + {"1.0", 1, "2.7"}, + {"1.0", 5, "2.71828"}, + {"1.0", -1, "0"}, + {"1.0", -5, "0"}, + {"3.0", 1, "20.1"}, + {"3.0", 2, "20.09"}, + {"5.26", 0, "192"}, + {"5.26", 2, "192.48"}, + {"5.26", 10, "192.4814912972"}, + {"5.26", -2, "200"}, + {"5.2663117716", 2, "193.70"}, + {"5.2663117716", 10, "193.7002326701"}, + {"26.1", 2, "216314672147.06"}, + {"26.1", 20, "216314672147.05767284062928674083"}, + {"26.1", -2, "216314672100"}, + {"26.1", -10, "220000000000"}, + {"50.1591", 10, "6078834886623434464595.0793714061"}, + {"-0.5", 5, "0.60653"}, + {"-0.569297", 10, "0.5659231429"}, + {"-0.569297", 16, "0.565923142859576"}, + {"-0.569297", 20, "0.56592314285957604443"}, + {"-1.0", 1, "0.4"}, + {"-1.0", 5, "0.36788"}, + {"-1.0", -1, "0"}, + {"-1.0", -5, "0"}, + {"-3.0", 1, "0.1"}, + {"-3.0", 2, "0.05"}, + {"-3.0", 10, "0.0497870684"}, + {"-5.26", 2, "0.01"}, + {"-5.26", 10, "0.0051953047"}, + {"-5.26", -2, "0"}, + {"-5.2663117716", 2, "0.01"}, + {"-5.2663117716", 10, "0.0051626164"}, + {"-26.1", 2, "0"}, + {"-26.1", 15, "0.000000000004623"}, + {"-26.1", -2, "0"}, + {"-26.1", -10, "0"}, + {"-50.1591", 10, "0"}, + {"-50.1591", 30, "0.000000000000000000000164505208"}, + } { + d, _ := NewFromString(testCase.Dec) + expected, _ := NewFromString(testCase.ExpectedDec) + + exp, err := d.ExpTaylor(testCase.Precision) + if err != nil { + t.Fatal(err) + } + + if exp.Cmp(expected) != 0 { + t.Errorf("expected %s, got %s", testCase.ExpectedDec, exp.String()) + } + } +} + +func TestDecimal_Ln(t *testing.T) { + for _, testCase := range []struct { + Dec string + Precision int32 + Expected string + }{ + {"0.1", 25, "-2.3025850929940456840179915"}, + {"0.01", 25, "-4.6051701859880913680359829"}, + {"0.001", 25, "-6.9077552789821370520539744"}, + {"0.00000001", 25, "-18.4206807439523654721439316"}, + {"1.0", 10, "0.0"}, + {"1.01", 25, "0.0099503308531680828482154"}, + {"1.001", 25, "0.0009995003330835331668094"}, + {"1.0001", 25, "0.0000999950003333083353332"}, + {"1.1", 25, "0.0953101798043248600439521"}, + {"1.13", 25, "0.1222176327242492005461486"}, + {"3.13", 10, "1.1410330046"}, + {"3.13", 25, "1.1410330045520618486427824"}, + {"3.13", 50, "1.14103300455206184864278239988848193837089629107972"}, + {"3.13", 100, "1.1410330045520618486427823998884819383708962910797239760817078430268177216960996098918971117211892839"}, + {"5.71", 25, "1.7422190236679188486939833"}, + {"5.7185108151957193571930205", 50, "1.74370842450178929149992165925283704012576949094645"}, + {"839101.0351", 25, "13.6400864014410013994397240"}, + {"839101.0351094726488848490572028502", 50, "13.64008640145229044389152437468283605382056561604272"}, + {"5023583755703750094849.03519358513093500275017501750602739169823", 25, "49.9684305274348922267409953"}, + {"5023583755703750094849.03519358513093500275017501750602739169823", -1, "50.0"}, + {"66.12", 18, "4.191471272952823429"}, + } { + d, _ := NewFromString(testCase.Dec) + expected, _ := NewFromString(testCase.Expected) + + ln, err := d.Ln(testCase.Precision) + if err != nil { + t.Fatal(err) + } + + if ln.Cmp(expected) != 0 { + t.Errorf("expected %s, got %s, for decimal %s", testCase.Expected, ln.String(), testCase.Dec) + } + } +} + +func TestDecimal_LnZero(t *testing.T) { + d := New(0, 0) + + _, err := d.Ln(5) + + if err == nil { + t.Errorf("expected error, natural logarithm of 0 cannot be represented (-infinity)") + } +} + +func TestDecimal_LnNegative(t *testing.T) { + d := New(-20, 2) + + _, err := d.Ln(5) + + if err == nil { + t.Errorf("expected error, natural logarithm cannot be calculated for nagative decimals") + } +} + +func TestDecimal_NumDigits(t *testing.T) { + for _, testCase := range []struct { + Dec string + ExpectedNumDigits int + }{ + {"0", 1}, + {"0.00", 1}, + {"1.0", 2}, + {"3.0", 2}, + {"5.26", 3}, + {"5.2663117716", 11}, + {"3164836416948884.2162426426426267863", 35}, + {"26.1", 3}, + {"529.1591", 7}, + {"-1.0", 2}, + {"-3.0", 2}, + {"-5.26", 3}, + {"-5.2663117716", 11}, + {"-26.1", 3}, + {"", 1}, + } { + d, _ := NewFromString(testCase.Dec) + + nums := d.NumDigits() + if nums != testCase.ExpectedNumDigits { + t.Errorf("expected %d digits for decimal %s, got %d", testCase.ExpectedNumDigits, testCase.Dec, nums) + } + } +} + +func TestDecimal_Sign(t *testing.T) { + if Zero.Sign() != 0 { + t.Errorf("%q should have sign 0", Zero) + } + + one := New(1, 0) + if one.Sign() != 1 { + t.Errorf("%q should have sign 1", one) + } + + mone := New(-1, 0) + if mone.Sign() != -1 { + t.Errorf("%q should have sign -1", mone) + } +} + +func didPanic(f func()) bool { + ret := false + func() { + + defer func() { + if message := recover(); message != nil { + ret = true + } + }() + + // call the target function + f() + + }() + + return ret + +} + +func TestDecimal_Coefficient(t *testing.T) { + d := New(123, 0) + co := d.Coefficient() + if co.Int64() != 123 { + t.Error("Coefficient should be 123; Got:", co) + } + co.Set(big.NewInt(0)) + if d.IntPart() != 123 { + t.Error("Modifying coefficient modified Decimal; Got:", d) + } +} + +func TestDecimal_CoefficientInt64(t *testing.T) { + type Inp struct { + Dec string + Coefficient int64 + } + + testCases := []Inp{ + {"1", 1}, + {"1.111", 1111}, + {"1.000000", 1000000}, + {"1.121215125511", 1121215125511}, + {"100000000000000000", 100000000000000000}, + {"9223372036854775807", 9223372036854775807}, + {"10000000000000000000", -8446744073709551616}, // undefined value + } + + // add negative cases + for _, tc := range testCases { + testCases = append(testCases, Inp{"-" + tc.Dec, -tc.Coefficient}) + } + + for _, tc := range testCases { + d := RequireFromString(tc.Dec) + coefficient := d.CoefficientInt64() + if coefficient != tc.Coefficient { + t.Errorf("expect coefficient %d, got %d, for decimal %s", tc.Coefficient, coefficient, tc.Dec) + } + } +} + +func TestNullDecimal_Scan(t *testing.T) { + // test the Scan method that implements the + // sql.Scanner interface + // check for the for different type of values + // that are possible to be received from the database + // drivers + + // in normal operations the db driver (sqlite at least) + // will return an int64 if you specified a numeric format + + // Make sure handles nil values + a := NullDecimal{} + var dbvaluePtr interface{} + err := a.Scan(dbvaluePtr) + if err != nil { + // Scan failed... no need to test result value + t.Errorf("a.Scan(nil) failed with message: %s", err) + } else { + if a.Valid { + t.Errorf("%s is not null", a.Decimal) + } + } + + dbvalue := 54.33 + expected := NewFromFloat(dbvalue) + + err = a.Scan(dbvalue) + if err != nil { + // Scan failed... no need to test result value + t.Errorf("a.Scan(54.33) failed with message: %s", err) + + } else { + // Scan succeeded... test resulting values + if !a.Valid { + t.Errorf("%s is null", a.Decimal) + } else if !a.Decimal.Equal(expected) { + t.Errorf("%s does not equal to %s", a.Decimal, expected) + } + } + + // at least SQLite returns an int64 when 0 is stored in the db + // and you specified a numeric format on the schema + dbvalueInt := int64(0) + expected = New(dbvalueInt, 0) + + err = a.Scan(dbvalueInt) + if err != nil { + // Scan failed... no need to test result value + t.Errorf("a.Scan(0) failed with message: %s", err) + + } else { + // Scan succeeded... test resulting values + if !a.Valid { + t.Errorf("%s is null", a.Decimal) + } else if !a.Decimal.Equal(expected) { + t.Errorf("%v does not equal %v", a, expected) + } + } + + // in case you specified a varchar in your SQL schema, + // the database driver will return byte slice []byte + valueStr := "535.666" + dbvalueStr := []byte(valueStr) + expected, err = NewFromString(valueStr) + if err != nil { + t.Fatal(err) + } + + err = a.Scan(dbvalueStr) + if err != nil { + // Scan failed... no need to test result value + t.Errorf("a.Scan('535.666') failed with message: %s", err) + + } else { + // Scan succeeded... test resulting values + if !a.Valid { + t.Errorf("%s is null", a.Decimal) + } else if !a.Decimal.Equal(expected) { + t.Errorf("%v does not equal %v", a, expected) + } + } + + // lib/pq can also return strings + expected, err = NewFromString(valueStr) + if err != nil { + t.Fatal(err) + } + + err = a.Scan(valueStr) + if err != nil { + // Scan failed... no need to test result value + t.Errorf("a.Scan('535.666') failed with message: %s", err) + } else { + // Scan succeeded... test resulting values + if !a.Valid { + t.Errorf("%s is null", a.Decimal) + } else if !a.Decimal.Equal(expected) { + t.Errorf("%v does not equal %v", a, expected) + } + } +} + +func TestNullDecimal_Value(t *testing.T) { + // Make sure this does implement the database/sql's driver.Valuer interface + var nullDecimal NullDecimal + if _, ok := interface{}(nullDecimal).(driver.Valuer); !ok { + t.Error("NullDecimal does not implement driver.Valuer") + } + + // check that null is handled appropriately + value, err := nullDecimal.Value() + if err != nil { + t.Errorf("NullDecimal{}.Valid() failed with message: %s", err) + } else if value != nil { + t.Errorf("%v is not nil", value) + } + + // check that normal case is handled appropriately + a := NullDecimal{Decimal: New(1234, -2), Valid: true} + expected := "12.34" + value, err = a.Value() + if err != nil { + t.Errorf("NullDecimal(12.34).Value() failed with message: %s", err) + } else if value.(string) != expected { + t.Errorf("%v does not equal %v", a, expected) + } +} + +func TestBinary(t *testing.T) { + for _, y := range testTable { + x := y.float + + // Create the decimal + d1 := NewFromFloat(x) + + // Encode to binary + b, err := d1.MarshalBinary() + if err != nil { + t.Errorf("error marshalling %v to binary: %v", d1, err) + } + + // Restore from binary + var d2 Decimal + err = (&d2).UnmarshalBinary(b) + if err != nil { + t.Errorf("error unmarshalling from binary: %v", err) + } + + // The restored decimal should equal the original + if !d1.Equal(d2) { + t.Errorf("expected %v when restoring, got %v", d1, d2) + } + } +} + +func TestBinary_Zero(t *testing.T) { + var d1 Decimal + + b, err := d1.MarshalBinary() + if err != nil { + t.Fatalf("error marshalling %v to binary: %v", d1, err) + } + + var d2 Decimal + err = (&d2).UnmarshalBinary(b) + if err != nil { + t.Errorf("error unmarshalling from binary: %v", err) + } + + if !d1.Equal(d2) { + t.Errorf("expected %v when restoring, got %v", d1, d2) + } +} + +func TestBinary_DataTooShort(t *testing.T) { + var d Decimal + + err := d.UnmarshalBinary(nil) // nil slice has length 0 + if err == nil { + t.Fatalf("expected error, got %v", d) + } +} + +func TestBinary_InvalidValue(t *testing.T) { + var d Decimal + + err := d.UnmarshalBinary([]byte{0, 0, 0, 0, 'x'}) // valid exponent, invalid value + if err == nil { + t.Fatalf("expected error, got %v", d) + } +} + +func slicesEqual(a, b []byte) bool { + for i, val := range a { + if b[i] != val { + return false + } + } + return true +} + +func TestGobEncode(t *testing.T) { + for _, y := range testTable { + x := y.float + d1 := NewFromFloat(x) + + b1, err := d1.GobEncode() + if err != nil { + t.Errorf("error encoding %v to binary: %v", d1, err) + } + + d2 := NewFromFloat(x) + + b2, err := d2.GobEncode() + if err != nil { + t.Errorf("error encoding %v to binary: %v", d2, err) + } + + if !slicesEqual(b1, b2) { + t.Errorf("something about the gobencode is not working properly \n%v\n%v", b1, b2) + } + + var d3 Decimal + err = d3.GobDecode(b1) + if err != nil { + t.Errorf("Error gobdecoding %v, got %v", b1, d3) + } + var d4 Decimal + err = d4.GobDecode(b2) + if err != nil { + t.Errorf("Error gobdecoding %v, got %v", b2, d4) + } + + eq := d3.Equal(d4) + if eq != true { + t.Errorf("Encoding then decoding mutated Decimal") + } + + eq = d1.Equal(d3) + if eq != true { + t.Errorf("Error gobencoding/decoding %v, got %v", d1, d3) + } + } +} + +func TestSum(t *testing.T) { + vals := make([]Decimal, 10) + var i = int64(0) + + for key := range vals { + vals[key] = New(i, 0) + i++ + } + + sum := Sum(vals[0], vals[1:]...) + if !sum.Equal(New(45, 0)) { + t.Errorf("Failed to calculate sum, expected %s got %s", New(45, 0), sum) + } +} + +func TestNewNullDecimal(t *testing.T) { + d := NewFromInt(1) + nd := NewNullDecimal(d) + + if !nd.Valid { + t.Errorf("expected NullDecimal to be valid") + } + if nd.Decimal != d { + t.Errorf("expected NullDecimal to hold the provided Decimal") + } +} + +func TestDecimal_String(t *testing.T) { + type testData struct { + input string + expected string + } + + tests := []testData{ + {"1.22", "1.22"}, + {"1.00", "1"}, + {"153.192", "153.192"}, + {"999.999", "999.999"}, + {"0.0000000001", "0.0000000001"}, + {"0.0000000000", "0"}, + } + + for _, test := range tests { + d, err := NewFromString(test.input) + if err != nil { + t.Fatal(err) + } else if d.String() != test.expected { + t.Errorf("expected %s, got %s", test.expected, d.String()) + } + } +} + +func TestDecimal_StringWithTrailing(t *testing.T) { + type testData struct { + input string + expected string + } + + defer func() { + TrimTrailingZeros = true + }() + + TrimTrailingZeros = false + tests := []testData{ + {"1.00", "1.00"}, + {"0.00", "0.00"}, + {"129.123000", "129.123000"}, + {"1.0000E3", "1000.0"}, // 1000 to the nearest tenth + {"10000E-1", "1000.0"}, // 1000 to the nearest tenth + } + + for _, test := range tests { + d, err := NewFromString(test.input) + if err != nil { + t.Fatal(err) + } else if d.String() != test.expected { + x := d.String() + fmt.Println(x) + t.Errorf("expected %s, got %s", test.expected, d.String()) + } + } +} + +func TestDecimal_StringWithScientificNotationWhenNeeded(t *testing.T) { + type testData struct { + input string + expected string + } + + defer func() { + UseScientificNotation = false + }() + UseScientificNotation = true + + tests := []testData{ + {"1.0E3", "1.0E3"}, // 1000 to the nearest hundred + {"1.00E3", "1.00E3"}, // 1000 to the nearest ten + {"1.000E3", "1000"}, // 1000 to the nearest one + {"1E3", "1E3"}, // 1000 to the nearest thousand + {"-1E3", "-1E3"}, // -1000 to the nearest thousand + } + + for _, test := range tests { + d, err := NewFromString(test.input) + if err != nil { + t.Fatal(err) + } else if d.String() != test.expected { + x := d.String() + fmt.Println(x) + t.Errorf("expected %s, got %s", test.expected, d.String()) + } + } +} + +func TestDecimal_ScientificNotation(t *testing.T) { + type testData struct { + input string + expected string + } + + tests := []testData{ + {"1", "1E0"}, + {"1.0", "1.0E0"}, + {"10", "1.0E1"}, + {"123", "1.23E2"}, + {"1234", "1.234E3"}, + {"-1", "-1E0"}, + {"-10", "-1.0E1"}, + {"-123", "-1.23E2"}, + {"-1234", "-1.234E3"}, + {"0.1", "1E-1"}, + {"0.01", "1E-2"}, + {"0.123", "1.23E-1"}, + {"1.23", "1.23E0"}, + {"-0.1", "-1E-1"}, + {"-0.01", "-1E-2"}, + {"-0.010", "-1.0E-2"}, + {"-0.123", "-1.23E-1"}, + {"-1.23", "-1.23E0"}, + {"1E6", "1E6"}, + {"1e6", "1E6"}, + {"1.23E6", "1.23E6"}, + {"-1E6", "-1E6"}, + {"1E-6", "1E-6"}, + {"1.23E-6", "1.23E-6"}, + {"-1E-6", "-1E-6"}, + {"-1.0E-6", "-1.0E-6"}, + {"12345600", "1.2345600E7"}, + {"123456E2", "1.23456E7"}, + {"0", "0"}, + {"0E1", "0"}, + {"-0", "0"}, + {"-0.000", "0"}, + } + + for _, test := range tests { + d, err := NewFromString(test.input) + if err != nil { + t.Fatal(err) + } else if d.ScientificNotationString() != test.expected { + t.Errorf("expected %s, got %s", test.expected, d.ScientificNotationString()) + } + } +} + +func ExampleNewFromFloat32() { + fmt.Println(NewFromFloat32(123.123123123123).String()) + fmt.Println(NewFromFloat32(.123123123123123).String()) + fmt.Println(NewFromFloat32(-1e13).String()) + // OUTPUT: + //123.12312 + //0.123123124 + //-10000000000000 +} + +func ExampleNewFromFloat() { + fmt.Println(NewFromFloat(123.123123123123).String()) + fmt.Println(NewFromFloat(.123123123123123).String()) + fmt.Println(NewFromFloat(-1e13).String()) + // OUTPUT: + //123.123123123123 + //0.123123123123123 + //-10000000000000 +} + +func TestFromWei(t *testing.T) { + for _, tc := range []struct { + wei string + expected string + }{ + {"0", "0"}, + {"1", "0.000000000000000001"}, + {"1000000000000000000", "1"}, + {"1500000000000000000", "1.5"}, + {"123456789000000000000000000", "123456789"}, + {"-2500000000000000000", "-2.5"}, + } { + wei := new(big.Int) + wei.SetString(tc.wei, 10) + d := FromWei(wei) + if d.String() != tc.expected { + t.Errorf("FromWei(%s): expected %s, got %s", tc.wei, tc.expected, d.String()) + } + } +} + +func TestDecimal_ToWei(t *testing.T) { + for _, tc := range []struct { + dec string + expected string + }{ + {"0", "0"}, + {"1", "1000000000000000000"}, + {"1.5", "1500000000000000000"}, + {"0.000000000000000001", "1"}, + {"123456789", "123456789000000000000000000"}, + {"-2.5", "-2500000000000000000"}, + // sub-wei fractions are truncated + {"0.0000000000000000001", "0"}, + {"1.0000000000000000005", "1000000000000000000"}, + } { + d := RequireFromString(tc.dec) + got := d.ToWei() + expected := new(big.Int) + expected.SetString(tc.expected, 10) + if got.Cmp(expected) != 0 { + t.Errorf("(%s).ToWei(): expected %s, got %s", tc.dec, tc.expected, got.String()) + } + } +} + +func TestFromWei_ToWei_Roundtrip(t *testing.T) { + original := new(big.Int) + original.SetString("12345678901234567890", 10) + d := FromWei(original) + back := d.ToWei() + if back.Cmp(original) != 0 { + t.Errorf("roundtrip failed: started with %s, got back %s (decimal was %s)", + original.String(), back.String(), d.String()) + } +} + +func TestExpTaylor_Concurrent(t *testing.T) { + // Verify no data race when calling ExpTaylor concurrently. + // Run with -race to detect issues. + done := make(chan struct{}) + for i := 0; i < 8; i++ { + go func() { + d := NewFromFloat(1.5) + _, _ = d.ExpTaylor(10) + done <- struct{}{} + }() + } + for i := 0; i < 8; i++ { + <-done + } +} diff --git a/pkg/decimal/functional_test.go b/pkg/decimal/functional_test.go new file mode 100644 index 0000000..1b75d68 --- /dev/null +++ b/pkg/decimal/functional_test.go @@ -0,0 +1,446 @@ +package decimal + +import ( + "math" + "math/big" + "math/rand" + "testing" +) + +// --------------------------------------------------------------------------- +// Conservation Properties +// --------------------------------------------------------------------------- + +func TestFunctional_Addition_Conservation(t *testing.T) { + rng := rand.New(rand.NewSource(42)) + for i := 0; i < 1000; i++ { + a := NewFromInt(rng.Int63n(1e15)) + b := NewFromInt(rng.Int63n(1e15)) + result := a.Add(b).Sub(b) + if !result.Equal(a) { + t.Fatalf("iteration %d: (a+b)-b != a: a=%s b=%s got=%s", i, a.String(), b.String(), result.String()) + } + } +} + +func TestFunctional_Subtraction_Conservation(t *testing.T) { + rng := rand.New(rand.NewSource(42)) + for i := 0; i < 1000; i++ { + av := rng.Int63n(1e15) + bv := rng.Int63n(av + 1) // ensure b <= a + a := NewFromInt(av) + b := NewFromInt(bv) + result := a.Sub(b).Add(b) + if !result.Equal(a) { + t.Fatalf("iteration %d: (a-b)+b != a: a=%s b=%s got=%s", i, a.String(), b.String(), result.String()) + } + } +} + +func TestFunctional_AddSub_MixedScaleSignedConservation(t *testing.T) { + cases := []struct { + name string + a string + b string + }{ + {name: "positive_integer_plus_sub_wei", a: "1000000000000000000", b: "0.000000000000000001"}, + {name: "negative_integer_plus_fraction", a: "-42", b: "0.125"}, + {name: "fraction_plus_large_negative", a: "0.000000123456789", b: "-999999999999999999"}, + {name: "both_negative_mixed_scale", a: "-123.456", b: "-0.000000000000000001"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + a := RequireFromString(tc.a) + b := RequireFromString(tc.b) + + if got := a.Add(b).Sub(b); !got.Equal(a) { + t.Fatalf("(a+b)-b != a: a=%s b=%s got=%s", a, b, got) + } + if got := a.Sub(b).Add(b); !got.Equal(a) { + t.Fatalf("(a-b)+b != a: a=%s b=%s got=%s", a, b, got) + } + }) + } +} + +func TestFunctional_MultiplicationCommutative(t *testing.T) { + rng := rand.New(rand.NewSource(42)) + for i := 0; i < 1000; i++ { + a := NewFromInt(rng.Int63n(1e9)) + b := NewFromInt(rng.Int63n(1e9)) + ab := a.Mul(b) + ba := b.Mul(a) + if !ab.Equal(ba) { + t.Fatalf("iteration %d: a*b != b*a: a=%s b=%s a*b=%s b*a=%s", i, a.String(), b.String(), ab.String(), ba.String()) + } + } +} + +// --------------------------------------------------------------------------- +// Precision +// --------------------------------------------------------------------------- + +func TestFunctional_Multiplication_Precision_18Decimals(t *testing.T) { + // 1.000000000000000001 * 1.000000000000000001 + // = 1.000000000000000002000000000000000001 (exact) + // Decimal should preserve at least the 18-decimal term. + a := RequireFromString("1.000000000000000001") + result := a.Mul(a) + + // The exact product is 1.000000000000000002000000000000000001. + expected := RequireFromString("1.000000000000000002000000000000000001") + if !result.Equal(expected) { + t.Fatalf("expected %s, got %s", expected.String(), result.String()) + } +} + +func TestFunctional_Division_Deterministic(t *testing.T) { + one := NewFromInt(1) + three := NewFromInt(3) + first := one.Div(three) + for i := 1; i < 100; i++ { + got := one.Div(three) + if !got.Equal(first) { + t.Fatalf("iteration %d: 1/3 changed: expected %s, got %s", i, first.String(), got.String()) + } + } +} + +// --------------------------------------------------------------------------- +// Boundary Values +// --------------------------------------------------------------------------- + +func TestFunctional_MaxValue_NoOverflow(t *testing.T) { + // Use a very large number near the practical limit. + maxStr := "99999999999999999999999999999999999999999999999999999999999999999999999999999999" + maxVal := RequireFromString(maxStr) + zero := NewFromInt(0) + result := maxVal.Add(zero) + if !result.Equal(maxVal) { + t.Fatalf("max + 0 != max: got %s", result.String()) + } +} + +func TestFunctional_ZeroDivision(t *testing.T) { + // Decimal.Div uses QuoRem which panics on division by zero. + // Verify it panics rather than silently returning garbage. + a := NewFromInt(42) + zero := NewFromInt(0) + + defer func() { + r := recover() + if r == nil { + t.Fatal("expected panic on division by zero, but did not panic") + } + }() + + _ = a.Div(zero) + t.Fatal("should not reach here after division by zero") +} + +func TestFunctional_ZeroOperations(t *testing.T) { + zero := NewFromInt(0) + a := RequireFromString("123456.789") + + // 0 + a == a + if !zero.Add(a).Equal(a) { + t.Fatal("0 + a != a") + } + + // 0 * a == 0 + if !zero.Mul(a).IsZero() { + t.Fatal("0 * a != 0") + } + + // a - 0 == a + if !a.Sub(zero).Equal(a) { + t.Fatal("a - 0 != a") + } +} + +func TestFunctional_Shift_ExponentOverflowPanics(t *testing.T) { + for _, tc := range []struct { + name string + value Decimal + shift int32 + }{ + {name: "positive overflow", value: New(1, math.MaxInt32), shift: 1}, + {name: "negative overflow", value: New(1, math.MinInt32), shift: -1}, + } { + t.Run(tc.name, func(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatal("expected exponent overflow panic") + } + }() + _ = tc.value.Shift(tc.shift) + }) + } +} + +func TestFunctional_Shift_ExponentBoundaryAllowed(t *testing.T) { + if got := New(1, math.MaxInt32-1).Shift(1); got.Exponent() != math.MaxInt32 { + t.Fatalf("max boundary exponent = %d, want %d", got.Exponent(), math.MaxInt32) + } + if got := New(1, math.MinInt32+1).Shift(-1); got.Exponent() != math.MinInt32 { + t.Fatalf("min boundary exponent = %d, want %d", got.Exponent(), math.MinInt32) + } +} + +// --------------------------------------------------------------------------- +// String Round-Trip +// --------------------------------------------------------------------------- + +func TestFunctional_StringRoundTrip(t *testing.T) { + rng := rand.New(rand.NewSource(99)) + for i := 0; i < 1000; i++ { + // Generate value with up to 15 integer digits and up to 10 fractional digits. + intPart := rng.Int63n(1e15) + fracExp := int32(rng.Intn(10) + 1) // 1..10 + v := New(intPart, -fracExp) + + str := v.String() + parsed, err := NewFromString(str) + if err != nil { + t.Fatalf("iteration %d: parse error for %q: %v", i, str, err) + } + if !parsed.Equal(v) { + t.Fatalf("iteration %d: round-trip failed: original=%s parsed=%s", i, v.String(), parsed.String()) + } + } +} + +// --------------------------------------------------------------------------- +// Comparison Operators +// --------------------------------------------------------------------------- + +func TestFunctional_Comparison_LessThan(t *testing.T) { + cases := []struct { + a, b string + }{ + {"0", "1"}, + {"-1", "0"}, + {"1.5", "1.6"}, + {"0.0001", "0.001"}, + {"-100", "100"}, + {"999999999999999999", "1000000000000000000"}, + } + for _, tc := range cases { + a := RequireFromString(tc.a) + b := RequireFromString(tc.b) + if !a.LessThan(b) { + t.Errorf("%s should be < %s", tc.a, tc.b) + } + if a.GreaterThanOrEqual(b) { + t.Errorf("%s should NOT be >= %s", tc.a, tc.b) + } + } +} + +func TestFunctional_Comparison_Equal(t *testing.T) { + cases := []struct { + a, b string + }{ + {"0", "0"}, + {"1", "1"}, + {"-1", "-1"}, + {"123.456", "123.456"}, + {"1000000000000000000", "1000000000000000000"}, + {"0.000000000000000001", "0.000000000000000001"}, + } + for _, tc := range cases { + a := RequireFromString(tc.a) + b := RequireFromString(tc.b) + if !a.Equal(b) { + t.Errorf("%s should equal %s", tc.a, tc.b) + } + if a.Cmp(b) != 0 { + t.Errorf("Cmp(%s, %s) should be 0, got %d", tc.a, tc.b, a.Cmp(b)) + } + } +} + +func TestFunctional_Comparison_GreaterThan(t *testing.T) { + cases := []struct { + a, b string + }{ + {"1", "0"}, + {"0", "-1"}, + {"1.6", "1.5"}, + {"0.001", "0.0001"}, + {"100", "-100"}, + {"1000000000000000000", "999999999999999999"}, + } + for _, tc := range cases { + a := RequireFromString(tc.a) + b := RequireFromString(tc.b) + if !a.GreaterThan(b) { + t.Errorf("%s should be > %s", tc.a, tc.b) + } + if a.LessThanOrEqual(b) { + t.Errorf("%s should NOT be <= %s", tc.a, tc.b) + } + } +} + +// --------------------------------------------------------------------------- +// AMM Arithmetic Precision +// --------------------------------------------------------------------------- + +func TestFunctional_AMM_ConstantProduct_Precision(t *testing.T) { + // Constant-product invariant: (rA + dx) * (rB - dy) >= rA * rB + // where dy = floor(rB * dx / (rA + dx)) + // Using integer wei values with floor division (QuoRem precision 0) to + // guarantee the invariant holds -- exactly as an on-chain AMM would. + rA := RequireFromString("1000000000000000000") // 1e18 wei + rB := RequireFromString("2000000000000000000") // 2e18 wei + dx := RequireFromString("1000000000000000") // 1e15 wei + + rAPlusDx := rA.Add(dx) + // Floor division: dy = floor(rB * dx / (rA + dx)) + dy, _ := rB.Mul(dx).QuoRem(rAPlusDx, 0) + + kBefore := rA.Mul(rB) + kAfter := rAPlusDx.Mul(rB.Sub(dy)) + + // kAfter >= kBefore (no drain of reserves with floor division) + if kAfter.LessThan(kBefore) { + t.Fatalf("constant product violated: before=%s after=%s", kBefore.String(), kAfter.String()) + } + + // Precision loss should be bounded: kAfter - kBefore < rAPlusDx (one unit of rounding) + diff := kAfter.Sub(kBefore) + if diff.GreaterThanOrEqual(rAPlusDx) { + t.Fatalf("precision loss too large: diff=%s bound=%s", diff.String(), rAPlusDx.String()) + } +} + +func TestFunctional_AMM_LargeReserves(t *testing.T) { + // Reserves near 2^128 (~3.4e38). Verify swap math does not overflow. + bigReserve := new(big.Int).Exp(big.NewInt(2), big.NewInt(128), nil) + rA := NewFromBigInt(bigReserve, 0) + rB := NewFromBigInt(bigReserve, 0) + dx := NewFromInt(1e15) // relatively small swap + + rAPlusDx := rA.Add(dx) + dy, _ := rB.Mul(dx).QuoRem(rAPlusDx, 0) + + kBefore := rA.Mul(rB) + kAfter := rAPlusDx.Mul(rB.Sub(dy)) + + if kAfter.LessThan(kBefore) { + t.Fatalf("constant product violated with large reserves: before=%s after=%s", kBefore.String(), kAfter.String()) + } + + // dy must be positive and less than rB + if dy.IsZero() || dy.IsNegative() { + t.Fatalf("dy should be positive, got %s", dy.String()) + } + if dy.GreaterThanOrEqual(rB) { + t.Fatalf("dy >= rB: dy=%s rB=%s", dy.String(), rB.String()) + } +} + +func TestFunctional_AMM_SmallReserves(t *testing.T) { + // Reserves near 1 wei. Swap math should not underflow. + rA := NewFromInt(1000) // 1000 wei + rB := NewFromInt(1000) // 1000 wei + dx := NewFromInt(1) // 1 wei swap + + rAPlusDx := rA.Add(dx) + // Floor division: for tiny reserves this may truncate to 0 + dy, _ := rB.Mul(dx).QuoRem(rAPlusDx, 0) + + // dy should be non-negative + if dy.IsNegative() { + t.Fatalf("dy should not be negative, got %s", dy.String()) + } + + // Even if dy rounds to 0, kAfter must be >= kBefore + kBefore := rA.Mul(rB) + kAfter := rAPlusDx.Mul(rB.Sub(dy)) + if kAfter.LessThan(kBefore) { + t.Fatalf("constant product violated with small reserves: before=%s after=%s", kBefore.String(), kAfter.String()) + } +} + +// --------------------------------------------------------------------------- +// Edge Cases +// --------------------------------------------------------------------------- + +func TestFunctional_NegativeResult_Handling(t *testing.T) { + // Decimal is signed, so 0-1 should yield -1, not wrap. + zero := NewFromInt(0) + one := NewFromInt(1) + result := zero.Sub(one) + + if !result.IsNegative() { + t.Fatalf("0 - 1 should be negative, got %s", result.String()) + } + + expected := NewFromInt(-1) + if !result.Equal(expected) { + t.Fatalf("0 - 1 should be -1, got %s", result.String()) + } +} + +func TestFunctional_VerySmallValues(t *testing.T) { + // 1 wei = 10^-18 + oneWei := RequireFromString("0.000000000000000001") + + // Addition: 1 wei + 1 wei = 2 wei + twoWei := RequireFromString("0.000000000000000002") + if !oneWei.Add(oneWei).Equal(twoWei) { + t.Fatal("1 wei + 1 wei != 2 wei") + } + + // Subtraction: 2 wei - 1 wei = 1 wei + if !twoWei.Sub(oneWei).Equal(oneWei) { + t.Fatal("2 wei - 1 wei != 1 wei") + } + + // Multiplication: 1 wei * 1e18 = 1 + scale := RequireFromString("1000000000000000000") + if !oneWei.Mul(scale).Equal(NewFromInt(1)) { + t.Fatalf("1 wei * 1e18 != 1, got %s", oneWei.Mul(scale).String()) + } + + // Comparison + if !oneWei.LessThan(twoWei) { + t.Fatal("1 wei should be < 2 wei") + } + if !oneWei.GreaterThan(NewFromInt(0)) { + t.Fatal("1 wei should be > 0") + } +} + +func TestFunctional_VeryLargeValues(t *testing.T) { + // Values near 2^128 + bigVal := new(big.Int).Exp(big.NewInt(2), big.NewInt(128), nil) + d := NewFromBigInt(bigVal, 0) + + // Add 1 + d1 := d.Add(NewFromInt(1)) + expected := NewFromBigInt(new(big.Int).Add(bigVal, big.NewInt(1)), 0) + if !d1.Equal(expected) { + t.Fatalf("2^128 + 1 mismatch: got %s", d1.String()) + } + + // Multiply by 2 + d2 := d.Mul(NewFromInt(2)) + expected2 := NewFromBigInt(new(big.Int).Mul(bigVal, big.NewInt(2)), 0) + if !d2.Equal(expected2) { + t.Fatalf("2^128 * 2 mismatch: got %s", d2.String()) + } + + // Subtract to get back + if !d2.Sub(d).Equal(d) { + t.Fatal("2^129 - 2^128 != 2^128") + } + + // Division: 2^128 / 2^128 = 1 + one := d.Div(d) + if !one.Equal(NewFromInt(1)) { + t.Fatalf("2^128 / 2^128 should be 1, got %s", one.String()) + } +} diff --git a/pkg/decimal/rounding.go b/pkg/decimal/rounding.go new file mode 100644 index 0000000..d4b0cd0 --- /dev/null +++ b/pkg/decimal/rounding.go @@ -0,0 +1,160 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Multiprecision decimal numbers. +// For floating-point formatting only; not general purpose. +// Only operations are assign and (binary) left/right shift. +// Can do binary floating point in multiprecision decimal precisely +// because 2 divides 10; cannot do decimal floating point +// in multiprecision binary precisely. + +package decimal + +type floatInfo struct { + mantbits uint + expbits uint + bias int +} + +var float32info = floatInfo{23, 8, -127} +var float64info = floatInfo{52, 11, -1023} + +// roundShortest rounds d (= mant * 2^exp) to the shortest number of digits +// that will let the original floating point value be precisely reconstructed. +func roundShortest(d *decimal, mant uint64, exp int, flt *floatInfo) { + // If mantissa is zero, the number is zero; stop now. + if mant == 0 { + d.nd = 0 + return + } + + // Compute upper and lower such that any decimal number + // between upper and lower (possibly inclusive) + // will round to the original floating point number. + + // We may see at once that the number is already shortest. + // + // Suppose d is not denormal, so that 2^exp <= d < 10^dp. + // The closest shorter number is at least 10^(dp-nd) away. + // The lower/upper bounds computed below are at distance + // at most 2^(exp-mantbits). + // + // So the number is already shortest if 10^(dp-nd) > 2^(exp-mantbits), + // or equivalently log2(10)*(dp-nd) > exp-mantbits. + // It is true if 332/100*(dp-nd) >= exp-mantbits (log2(10) > 3.32). + minexp := flt.bias + 1 // minimum possible exponent + if exp > minexp && 332*(d.dp-d.nd) >= 100*(exp-int(flt.mantbits)) { + // The number is already shortest. + return + } + + // d = mant << (exp - mantbits) + // Next highest floating point number is mant+1 << exp-mantbits. + // Our upper bound is halfway between, mant*2+1 << exp-mantbits-1. + upper := new(decimal) + upper.Assign(mant*2 + 1) + upper.Shift(exp - int(flt.mantbits) - 1) + + // d = mant << (exp - mantbits) + // Next lowest floating point number is mant-1 << exp-mantbits, + // unless mant-1 drops the significant bit and exp is not the minimum exp, + // in which case the next lowest is mant*2-1 << exp-mantbits-1. + // Either way, call it mantlo << explo-mantbits. + // Our lower bound is halfway between, mantlo*2+1 << explo-mantbits-1. + var mantlo uint64 + var explo int + if mant > 1<= d.nd { + break + } + li := ui - upper.dp + lower.dp + l := byte('0') // lower digit + if li >= 0 && li < lower.nd { + l = lower.d[li] + } + m := byte('0') // middle digit + if mi >= 0 { + m = d.d[mi] + } + u := byte('0') // upper digit + if ui < upper.nd { + u = upper.d[ui] + } + + // Okay to round down (truncate) if lower has a different digit + // or if lower is inclusive and is exactly the result of rounding + // down (i.e., and we have reached the final digit of lower). + okdown := l != m || inclusive && li+1 == lower.nd + + switch { + case upperdelta == 0 && m+1 < u: + // Example: + // m = 12345xxx + // u = 12347xxx + upperdelta = 2 + case upperdelta == 0 && m != u: + // Example: + // m = 12345xxx + // u = 12346xxx + upperdelta = 1 + case upperdelta == 1 && (m != '9' || u != '0'): + // Example: + // m = 1234598x + // u = 1234600x + upperdelta = 2 + } + // Okay to round up if upper has a different digit and either upper + // is inclusive or upper is bigger than the result of rounding up. + okup := upperdelta > 0 && (inclusive || upperdelta > 1 || ui+1 < upper.nd) + + // If it's okay to do either, then round to the nearest one. + // If it's okay to do only one, do it. + switch { + case okdown && okup: + d.Round(mi + 1) + return + case okdown: + d.RoundDown(mi + 1) + return + case okup: + d.RoundUp(mi + 1) + return + } + } +} diff --git a/pkg/eip712/eip712.go b/pkg/eip712/eip712.go new file mode 100644 index 0000000..7686f4e --- /dev/null +++ b/pkg/eip712/eip712.go @@ -0,0 +1,233 @@ +// Package eip712 provides shared EIP-712 signature verification for both +// the gateway (HTTP->TCP relay) and the clearnode (TCP command verification). +package eip712 + +import ( + "fmt" + "math/big" + "sort" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/layer-3/clearnet-sdk/pkg/abiutil" +) + +const ( + Name = "Clearnet" + Version = "1" + RouterHex = "0x00000000000000000000000000000000434C5200" +) + +var ( + RouterAddr = common.HexToAddress(RouterHex) + + DomainTypeHash = crypto.Keccak256Hash([]byte( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")) + + // Per ADR-009 §6.1, every user-authorized field of an operation MUST + // appear in its EIP-712 typehash preimage. The schema here matches the + // authoritative table in ADR-009 §6.1; `docs/specs/edge/gateway.md §5.2` + // is the descriptive mirror. Any relay that could otherwise mutate a + // listed field post-signing is closed by inclusion in the preimage. + TransferAssetTypeHash = crypto.Keccak256Hash([]byte( + "TransferAsset(string asset,uint256 amount)")) + + TransferTypeHash = crypto.Keccak256Hash([]byte( + "Transfer(address to,TransferAsset[] assets,uint256 maxFee,uint64 nonce)TransferAsset(string asset,uint256 amount)")) + + SwapTypeHash = crypto.Keccak256Hash([]byte( + "Swap(string assetIn,string assetOut,uint256 amountIn,uint256 minAmountOut,uint256 maxFee,uint64 nonce)")) + + WithdrawalTypeHash = crypto.Keccak256Hash([]byte( + "Withdrawal(string asset,uint256 amount,uint256 chainId,address recipient,uint256 maxFee,uint64 nonce)")) +) + +// TransferAsset is the EIP-712 projection of one TransferOp asset leg. +type TransferAsset struct { + Asset string + Amount *big.Int +} + +// NetworkChainID derives a numeric EVM chain ID from a 4-byte NetworkID string. +// "YDEV" -> 0x59444556 -> 1497646422. +func NetworkChainID(networkID string) *big.Int { + if len(networkID) != 4 { + return big.NewInt(0) + } + b := []byte(networkID) + return new(big.Int).SetUint64(uint64(b[0])<<24 | uint64(b[1])<<16 | uint64(b[2])<<8 | uint64(b[3])) +} + +// ComputeDomainSeparator computes the EIP-712 domain separator for the given chain ID. +func ComputeDomainSeparator(chainID *big.Int) [32]byte { + nameHash := crypto.Keccak256Hash([]byte(Name)) + versionHash := crypto.Keccak256Hash([]byte(Version)) + args := abi.Arguments{ + {Type: abiutil.Bytes32}, {Type: abiutil.Bytes32}, {Type: abiutil.Bytes32}, + {Type: abiutil.Uint256}, {Type: abiutil.Address}, + } + packed, _ := args.Pack(DomainTypeHash, nameHash, versionHash, chainID, RouterAddr) + return crypto.Keccak256Hash(packed) +} + +// Digest computes keccak256("\x19\x01" || domainSeparator || structHash). +func Digest(domainSep [32]byte, structHash [32]byte) []byte { + msg := make([]byte, 2+32+32) + msg[0] = 0x19 + msg[1] = 0x01 + copy(msg[2:34], domainSep[:]) + copy(msg[34:66], structHash[:]) + return crypto.Keccak256(msg) +} + +// RecoverSigner recovers the ECDSA signer from an EIP-712 digest and signature. +func RecoverSigner(digest []byte, sig []byte) (common.Address, error) { + s := make([]byte, len(sig)) + copy(s, sig) + if len(s) == 65 && s[64] >= 27 { + s[64] -= 27 + } + pubBytes, err := crypto.Ecrecover(digest, s) + if err != nil { + return common.Address{}, err + } + pub, err := crypto.UnmarshalPubkey(pubBytes) + if err != nil { + return common.Address{}, err + } + return crypto.PubkeyToAddress(*pub), nil +} + +// bigOrZero returns v if non-nil, else zero. Callers may pass nil for optional +// user-authorized bounds (e.g. maxFee, minAmountOut); they still bind under the +// signature — nil marshals to uint256(0), so a signature produced with nil +// cannot be replayed against a nonzero bound. +func bigOrZero(v *big.Int) *big.Int { + if v == nil { + return big.NewInt(0) + } + return v +} + +// NormalizeTransferAssets returns a sorted, duplicate-free copy of the Transfer +// asset list used by the frozen EIP-712 Transfer schema. +func NormalizeTransferAssets(in []TransferAsset) ([]TransferAsset, error) { + if len(in) == 0 { + return nil, fmt.Errorf("transfer assets required") + } + out := make([]TransferAsset, len(in)) + for i, asset := range in { + if asset.Asset == "" { + return nil, fmt.Errorf("transfer asset %d missing Asset", i) + } + if asset.Amount == nil || asset.Amount.Sign() <= 0 { + return nil, fmt.Errorf("transfer asset %s Amount must be > 0", asset.Asset) + } + out[i] = TransferAsset{Asset: asset.Asset, Amount: new(big.Int).Set(asset.Amount)} + } + sort.Slice(out, func(i, j int) bool { return out[i].Asset < out[j].Asset }) + for i := 1; i < len(out); i++ { + if out[i-1].Asset == out[i].Asset { + return nil, fmt.Errorf("duplicate transfer asset %s", out[i].Asset) + } + } + return out, nil +} + +func hashTransferAsset(asset TransferAsset) ([32]byte, error) { + assetHash := crypto.Keccak256Hash([]byte(asset.Asset)) + args := abi.Arguments{ + {Type: abiutil.Bytes32}, {Type: abiutil.Bytes32}, {Type: abiutil.Uint256}, + } + packed, err := args.Pack(TransferAssetTypeHash, assetHash, bigOrZero(asset.Amount)) + if err != nil { + return [32]byte{}, err + } + return crypto.Keccak256Hash(packed), nil +} + +// HashTransferAssets hashes the canonical EIP-712 array payload for +// TransferAsset[]. +func HashTransferAssets(assets []TransferAsset) ([32]byte, error) { + normalized, err := NormalizeTransferAssets(assets) + if err != nil { + return [32]byte{}, err + } + buf := make([]byte, 0, 32*len(normalized)) + for _, asset := range normalized { + h, err := hashTransferAsset(asset) + if err != nil { + return [32]byte{}, err + } + buf = append(buf, h[:]...) + } + return crypto.Keccak256Hash(buf), nil +} + +// TransferStructHash returns the EIP-712 struct hash for the canonical +// multi-asset Transfer schema. +func TransferStructHash(to common.Address, assets []TransferAsset, maxFee *big.Int, nonce uint64) ([32]byte, error) { + assetsHash, err := HashTransferAssets(assets) + if err != nil { + return [32]byte{}, err + } + args := abi.Arguments{ + {Type: abiutil.Bytes32}, {Type: abiutil.Address}, {Type: abiutil.Bytes32}, + {Type: abiutil.Uint256}, {Type: abiutil.Uint64}, + } + packed, err := args.Pack(TransferTypeHash, to, assetsHash, bigOrZero(maxFee), nonce) + if err != nil { + return [32]byte{}, err + } + return crypto.Keccak256Hash(packed), nil +} + +// RecoverTransfer recovers the signer of a Transfer EIP-712 message. +// Per ADR-009 §6.1: Transfer(address to, TransferAsset[] assets, uint256 maxFee, uint64 nonce). +func RecoverTransfer(chainID *big.Int, to common.Address, assets []TransferAsset, maxFee *big.Int, nonce uint64, sig []byte) (common.Address, error) { + domainSep := ComputeDomainSeparator(chainID) + structHash, err := TransferStructHash(to, assets, maxFee, nonce) + if err != nil { + return common.Address{}, err + } + digest := Digest(domainSep, structHash) + return RecoverSigner(digest, sig) +} + +// RecoverSwap recovers the signer of a Swap EIP-712 message. +// Per ADR-009 §6.1: Swap(string assetIn, string assetOut, uint256 amountIn, uint256 minAmountOut, uint256 maxFee, uint64 nonce). +func RecoverSwap(chainID *big.Int, assetIn, assetOut string, amountIn, minAmountOut, maxFee *big.Int, nonce uint64, sig []byte) (common.Address, error) { + domainSep := ComputeDomainSeparator(chainID) + aiHash := crypto.Keccak256Hash([]byte(assetIn)) + aoHash := crypto.Keccak256Hash([]byte(assetOut)) + args := abi.Arguments{ + {Type: abiutil.Bytes32}, {Type: abiutil.Bytes32}, {Type: abiutil.Bytes32}, + {Type: abiutil.Uint256}, {Type: abiutil.Uint256}, {Type: abiutil.Uint256}, {Type: abiutil.Uint64}, + } + packed, err := args.Pack(SwapTypeHash, aiHash, aoHash, bigOrZero(amountIn), bigOrZero(minAmountOut), bigOrZero(maxFee), nonce) + if err != nil { + return common.Address{}, err + } + structHash := crypto.Keccak256Hash(packed) + digest := Digest(domainSep, structHash) + return RecoverSigner(digest, sig) +} + +// RecoverWithdrawal recovers the signer of a Withdrawal EIP-712 message. +// Per ADR-009 §6.1: Withdrawal(string asset, uint256 amount, uint256 chainId, address recipient, uint256 maxFee, uint64 nonce). +func RecoverWithdrawal(chainID *big.Int, asset string, amount *big.Int, targetChainID uint64, recipient common.Address, maxFee *big.Int, nonce uint64, sig []byte) (common.Address, error) { + domainSep := ComputeDomainSeparator(chainID) + assetHash := crypto.Keccak256Hash([]byte(asset)) + args := abi.Arguments{ + {Type: abiutil.Bytes32}, {Type: abiutil.Bytes32}, {Type: abiutil.Uint256}, + {Type: abiutil.Uint256}, {Type: abiutil.Address}, {Type: abiutil.Uint256}, {Type: abiutil.Uint64}, + } + packed, err := args.Pack(WithdrawalTypeHash, assetHash, bigOrZero(amount), new(big.Int).SetUint64(targetChainID), recipient, bigOrZero(maxFee), nonce) + if err != nil { + return common.Address{}, err + } + structHash := crypto.Keccak256Hash(packed) + digest := Digest(domainSep, structHash) + return RecoverSigner(digest, sig) +} diff --git a/pkg/eip712/eip712_property_test.go b/pkg/eip712/eip712_property_test.go new file mode 100644 index 0000000..ceeee1b --- /dev/null +++ b/pkg/eip712/eip712_property_test.go @@ -0,0 +1,357 @@ +package eip712 + +import ( + "bytes" + "crypto/ecdsa" + "math/big" + "math/rand" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +// TestProperty_ComputeDomainSeparator_PureAndChainSensitive asserts +// invariant C1: +// 1. Purity — two calls with the same chain ID produce identical output. +// 2. Chain-ID sensitivity — distinct chain IDs produce distinct +// separators. +// +// Mutation-check 2026-04-18: replaced `chainID` with a literal +// `big.NewInt(1)` inside ComputeDomainSeparator — sensitivity check +// failed because all separators collapsed to the mainnet value; +// restored. +func TestProperty_ComputeDomainSeparator_PureAndChainSensitive(t *testing.T) { + rng := rand.New(rand.NewSource(0x1C1_A)) + seen := make(map[[32]byte]int64) + for trial := 0; trial < 300; trial++ { + chainID := big.NewInt(rng.Int63()) + a := ComputeDomainSeparator(chainID) + b := ComputeDomainSeparator(chainID) + if a != b { + t.Fatalf("trial=%d: purity violated for chainID=%d: %x vs %x", trial, chainID, a, b) + } + if prior, ok := seen[a]; ok && prior != chainID.Int64() { + t.Fatalf("trial=%d: collision between chainID=%d and chainID=%d → %x", + trial, prior, chainID, a) + } + seen[a] = chainID.Int64() + } +} + +// TestProperty_Digest_StructuralFormat asserts invariant C2: Digest is +// exactly keccak256(0x19 || 0x01 || domainSep || structHash) for any +// pair of 32-byte inputs. +// +// Mutation-check 2026-04-18: swapped msg[0] and msg[1] (0x19 vs 0x01) +// — test failed on trial 0; restored. +func TestProperty_Digest_StructuralFormat(t *testing.T) { + rng := rand.New(rand.NewSource(0x1C2_A)) + for trial := 0; trial < 500; trial++ { + var sep, sh [32]byte + rng.Read(sep[:]) + rng.Read(sh[:]) + + got := Digest(sep, sh) + + // Reference implementation — hand-assembled, not cross-invoking Digest. + msg := make([]byte, 0, 66) + msg = append(msg, 0x19, 0x01) + msg = append(msg, sep[:]...) + msg = append(msg, sh[:]...) + want := crypto.Keccak256(msg) + + if !bytes.Equal(got, want) { + t.Fatalf("trial=%d: digest mismatch\n got=%x\n want=%x", trial, got, want) + } + } +} + +// TestProperty_NetworkChainID_InjectiveOver4Byte asserts invariant C3: +// distinct 4-byte ASCII strings produce distinct, non-zero chain IDs. +// Short-circuits on length != 4 (returns zero) are tested separately. +// +// Mutation-check 2026-04-18: dropped the bitshift for b[0] (producing +// collisions across any string sharing the last 3 bytes) — test failed; +// restored. +func TestProperty_NetworkChainID_InjectiveOver4Byte(t *testing.T) { + rng := rand.New(rand.NewSource(0x1C3_A)) + seen := make(map[string]string) + for trial := 0; trial < 500; trial++ { + var b [4]byte + for i := range b { + b[i] = byte(0x21 + rng.Intn(0x7e-0x21+1)) + } + s := string(b[:]) + id := NetworkChainID(s) + if id.Sign() == 0 { + t.Fatalf("trial=%d s=%q: chainID=0 for 4-byte string", trial, s) + } + key := id.String() + if prior, ok := seen[key]; ok && prior != s { + t.Fatalf("trial=%d: NetworkChainID collision %q and %q → %s", trial, prior, s, key) + } + seen[key] = s + } + + // Deterministic edges: non-4-byte inputs must map to zero. + for _, bad := range []string{"", "A", "AB", "ABC", "ABCDE", "TOO LONG"} { + if NetworkChainID(bad).Sign() != 0 { + t.Fatalf("NetworkChainID(%q) must be 0 (len != 4)", bad) + } + } +} + +// --------------------------------------------------------------------------- +// ADR-009 §6.1 positive-bind property tests — F-CORE-001..005 repair. +// +// Shape: for each newly-bound field X on operation Op: +// 1. Sign Op with (X = A) → sig_A. +// 2. Recover Op with (X = A, sig_A) → must recover signer address. +// 3. Recover Op with (X = B ≠ A, sig_A) → must NOT recover signer (binding check). +// +// Step 3 is the load-bearing assertion: it kills mutations on either +// side of the preimage that drop X (Sign's pack OR Recover's pack). +// If X is not in the Recover preimage, altering it still recovers the +// same address, breaking the assertion. +// +// Mutation-kills verified 2026-04-20: +// +// - Delete `uint256 maxFee` from TransferTypeHash → Recover digest no +// longer covers maxFee → altered-recover still produces original +// signer → TransferBindsMaxFee FAILs. +// - Same shape for Swap/Withdrawal/AddLiquidity/RemoveLiquidity and +// their respective newly-bound fields. +// --------------------------------------------------------------------------- + +// testKey returns a deterministic ECDSA key for property-test trials. +func testKey(t *testing.T, seed int64) *ecdsa.PrivateKey { + t.Helper() + // crypto.GenerateKey uses crypto/rand; we want determinism across + // trials so the test is reproducible. Use a simple seeded keccak of + // the seed as 32-byte private key material. + buf := make([]byte, 8) + for i := 0; i < 8; i++ { + buf[i] = byte(seed >> (8 * (7 - i))) + } + h := crypto.Keccak256Hash(buf) + k, err := crypto.ToECDSA(h[:]) + if err != nil { + t.Fatalf("ToECDSA: %v", err) + } + return k +} + +// rsvSign applies the v-normalisation both the gateway and TCP verifiers +// accept (v ∈ {27, 28}). crypto.Sign returns v ∈ {0, 1}. +func rsvSign(t *testing.T, digest []byte, key *ecdsa.PrivateKey) []byte { + t.Helper() + sig, err := crypto.Sign(digest, key) + if err != nil { + t.Fatalf("sign: %v", err) + } + if sig[64] < 27 { + sig[64] += 27 + } + return sig +} + +// signTransfer produces a Transfer EIP-712 signature using the SAME +// pack shape as RecoverTransfer. Kept as a local helper so the round- +// trip test exercises production-code symmetry: Sign + Recover must +// agree on the preimage, or recovery returns the wrong address. +func signTransfer(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, to common.Address, asset string, amount, maxFee *big.Int, nonce uint64) []byte { + t.Helper() + // Use RecoverTransfer's inverse shape by producing an inverse digest + // directly. The production path is: pack → keccak structHash → + // Digest(sep, structHash) → sign. We mirror it exactly. + // Reuse packing by calling a helper that matches eip712.go. + packed, err := packTransferPreimage(chainID, to, []TransferAsset{{Asset: asset, Amount: amount}}, maxFee, nonce) + if err != nil { + t.Fatalf("pack: %v", err) + } + return rsvSign(t, packed, key) +} + +func packTransferPreimage(chainID *big.Int, to common.Address, assets []TransferAsset, maxFee *big.Int, nonce uint64) ([]byte, error) { + // This helper deliberately does NOT reuse eip712.go's internal pack + // sequence verbatim — it reconstructs the SAME digest that + // RecoverTransfer computes, by calling the public Digest helper with + // a struct hash that the Recover* functions also produce. A mutation + // on RecoverTransfer's arg list would NOT affect this helper, so the + // round-trip test below catches the mutation via the recovered- + // address mismatch rather than the signature itself. + // + // We construct the struct hash the same way RecoverTransfer does. + structHash, err := TransferStructHash(to, assets, maxFee, nonce) + if err != nil { + return nil, err + } + sep := ComputeDomainSeparator(chainID) + return Digest(sep, structHash), nil +} + +func leftPad32(b []byte) []byte { + if len(b) >= 32 { + return b[:32] + } + out := make([]byte, 32) + copy(out[32-len(b):], b) + return out +} + +// TestProperty_EIP712_TransferBindsMaxFee — F-CORE-001 positive bind. +// +// Constructs a Transfer signature over (amount, maxFee=A) and verifies +// (a) Recover with the original maxFee returns the signer; (b) Recover +// with maxFee=B (B ≠ A) does NOT return the signer. (b) fails if +// TransferTypeHash or RecoverTransfer's pack drops maxFee. +func TestProperty_EIP712_TransferBindsMaxFee(t *testing.T) { + key := testKey(t, 0xC4_00) + signer := crypto.PubkeyToAddress(key.PublicKey) + + chainID := big.NewInt(1497646422) + to := common.HexToAddress("0x00000000000000000000000000000000000000A1") + rng := rand.New(rand.NewSource(0x1C4_A)) + for trial := 0; trial < 80; trial++ { + nonce := uint64(rng.Int63()) + amount := new(big.Int).SetInt64(rng.Int63()) + feeA := new(big.Int).SetInt64(rng.Int63n(1 << 32)) + feeB := new(big.Int).Add(feeA, big.NewInt(1+rng.Int63n(1000))) + + sig := signTransfer(t, key, chainID, to, "USDT", amount, feeA, nonce) + + assets := []TransferAsset{{Asset: "USDT", Amount: amount}} + gotA, err := RecoverTransfer(chainID, to, assets, feeA, nonce, sig) + if err != nil { + t.Fatalf("trial=%d: recover A: %v", trial, err) + } + if gotA != signer { + t.Fatalf("trial=%d: recover A mismatch: got %s want %s", trial, gotA, signer) + } + + gotB, err := RecoverTransfer(chainID, to, assets, feeB, nonce, sig) + if err != nil { + // A recovery error on the mutated-maxFee path is also an acceptable + // "not equal to signer" outcome. + continue + } + if gotB == signer { + t.Fatalf("trial=%d: Transfer signature did NOT bind maxFee (A=%s B=%s recovered to signer under both)", trial, feeA, feeB) + } + } +} + +// signSwap produces a Swap signature matching RecoverSwap's pack. +func signSwap(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, assetIn, assetOut string, amountIn, minAmountOut, maxFee *big.Int, nonce uint64) []byte { + t.Helper() + aiHash := crypto.Keccak256Hash([]byte(assetIn)) + aoHash := crypto.Keccak256Hash([]byte(assetOut)) + buf := make([]byte, 0, 32*7) + buf = append(buf, SwapTypeHash[:]...) + buf = append(buf, aiHash[:]...) + buf = append(buf, aoHash[:]...) + buf = append(buf, leftPad32(amountIn.Bytes())...) + buf = append(buf, leftPad32(minAmountOut.Bytes())...) + buf = append(buf, leftPad32(maxFee.Bytes())...) + buf = append(buf, leftPad32(new(big.Int).SetUint64(nonce).Bytes())...) + structHash := crypto.Keccak256Hash(buf) + sep := ComputeDomainSeparator(chainID) + return rsvSign(t, Digest(sep, structHash), key) +} + +// TestProperty_EIP712_SwapBindsMaxFee — retained from pre-repair inventory. +func TestProperty_EIP712_SwapBindsMaxFee(t *testing.T) { + key := testKey(t, 0xC4_01) + signer := crypto.PubkeyToAddress(key.PublicKey) + + chainID := big.NewInt(1497646422) + rng := rand.New(rand.NewSource(0x1C4_B)) + for trial := 0; trial < 80; trial++ { + nonce := uint64(rng.Int63()) + amountIn := new(big.Int).SetInt64(rng.Int63()) + minOut := new(big.Int).SetInt64(rng.Int63()) + feeA := new(big.Int).SetInt64(rng.Int63n(1 << 32)) + feeB := new(big.Int).Add(feeA, big.NewInt(1+rng.Int63n(1000))) + + sig := signSwap(t, key, chainID, "USDT", "YELLOW", amountIn, minOut, feeA, nonce) + + gotB, err := RecoverSwap(chainID, "USDT", "YELLOW", amountIn, minOut, feeB, nonce, sig) + if err != nil { + continue + } + if gotB == signer { + t.Fatalf("trial=%d: Swap signature did NOT bind maxFee", trial) + } + } +} + +// TestProperty_EIP712_SwapBindsMinAmountOut — F-CORE-002. +func TestProperty_EIP712_SwapBindsMinAmountOut(t *testing.T) { + key := testKey(t, 0xC5_01) + signer := crypto.PubkeyToAddress(key.PublicKey) + + chainID := big.NewInt(1497646422) + rng := rand.New(rand.NewSource(0x1C5_A)) + for trial := 0; trial < 80; trial++ { + nonce := uint64(rng.Int63()) + amountIn := new(big.Int).SetInt64(rng.Int63()) + fee := new(big.Int).SetInt64(rng.Int63n(1 << 32)) + minA := new(big.Int).SetInt64(rng.Int63n(1 << 40)) + minB := new(big.Int).Add(minA, big.NewInt(1+rng.Int63n(1000))) + + sig := signSwap(t, key, chainID, "USDT", "YELLOW", amountIn, minA, fee, nonce) + + gotB, err := RecoverSwap(chainID, "USDT", "YELLOW", amountIn, minB, fee, nonce, sig) + if err != nil { + continue + } + if gotB == signer { + t.Fatalf("trial=%d: Swap signature did NOT bind minAmountOut", trial) + } + } +} + +// signWithdrawal matches RecoverWithdrawal's pack. +func signWithdrawal(t *testing.T, key *ecdsa.PrivateKey, chainID *big.Int, asset string, amount *big.Int, targetChainID uint64, recipient common.Address, maxFee *big.Int, nonce uint64) []byte { + t.Helper() + assetHash := crypto.Keccak256Hash([]byte(asset)) + buf := make([]byte, 0, 32*7) + buf = append(buf, WithdrawalTypeHash[:]...) + buf = append(buf, assetHash[:]...) + buf = append(buf, leftPad32(amount.Bytes())...) + buf = append(buf, leftPad32(new(big.Int).SetUint64(targetChainID).Bytes())...) + buf = append(buf, leftPad32(recipient.Bytes())...) + buf = append(buf, leftPad32(maxFee.Bytes())...) + buf = append(buf, leftPad32(new(big.Int).SetUint64(nonce).Bytes())...) + structHash := crypto.Keccak256Hash(buf) + sep := ComputeDomainSeparator(chainID) + return rsvSign(t, Digest(sep, structHash), key) +} + +// TestProperty_EIP712_WithdrawalBindsMaxFee — F-CORE-005. +func TestProperty_EIP712_WithdrawalBindsMaxFee(t *testing.T) { + key := testKey(t, 0xC5_02) + signer := crypto.PubkeyToAddress(key.PublicKey) + + chainID := big.NewInt(1497646422) + recipient := common.HexToAddress("0x00000000000000000000000000000000000000B2") + rng := rand.New(rand.NewSource(0x1C5_B)) + for trial := 0; trial < 80; trial++ { + nonce := uint64(rng.Int63()) + amount := new(big.Int).SetInt64(rng.Int63()) + target := uint64(1 + rng.Int63n(1_000_000)) + feeA := new(big.Int).SetInt64(rng.Int63n(1 << 32)) + feeB := new(big.Int).Add(feeA, big.NewInt(1+rng.Int63n(1000))) + + sig := signWithdrawal(t, key, chainID, "USDT", amount, target, recipient, feeA, nonce) + + gotB, err := RecoverWithdrawal(chainID, "USDT", amount, target, recipient, feeB, nonce, sig) + if err != nil { + continue + } + if gotB == signer { + t.Fatalf("trial=%d: Withdrawal signature did NOT bind maxFee", trial) + } + } +} diff --git a/pkg/receipt/receipt_verifier.go b/pkg/receipt/receipt_verifier.go new file mode 100644 index 0000000..8722fd5 --- /dev/null +++ b/pkg/receipt/receipt_verifier.go @@ -0,0 +1,300 @@ +package receipt + +import ( + "context" + "encoding/binary" + "errors" + "fmt" + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/layer-3/clearnet-sdk/pkg/core" + "github.com/layer-3/clearnet-sdk/pkg/eip712" +) + +// SignerSource supplies the custody signer set and threshold that the +// receipt verifier checks signatures against. It is chain-agnostic: a node +// can serve receipts produced by custody systems on any L1 as long as some +// SignerSource implementation knows the active signer set. +// +// Implementations available today: +// - StaticSignerSource — manifest-driven; operator-managed list. +// +// Planned: a Registry-backed source that reads a single canonical signer +// directory from the on-chain Registry once that contract grows the right +// method. The interface here is the seam that lets that swap land +// without touching the verifier or its callers. See the ADR-005 +// 2026-05-12 receipt-model amendment. +type SignerSource interface { + // Load returns the current signer set and threshold. Implementations + // may be cached or pull from a remote source; the verifier calls Load + // at startup and on a periodic ticker. + Load(ctx context.Context) (signers []common.Address, threshold int, err error) +} + +const defaultReceiptVerifierMaxAge = 15 * time.Minute + +// ReceiptVerifier validates MintReceipts and BurnReceipts against a +// SignerSource. It caches signers and threshold and refreshes them +// periodically so verification is a fast in-memory check. +// +// Verification dispatches on signature length: +// - 65 bytes → ECDSA secp256k1 (EVM custody chains; ADR-005 §11.1). +// - 64 bytes → ED25519 (XRPL/Solana custody; not yet implemented; gated on +// a per-chain ADR per ADR-005 §10). +// - other → ignored. +// +// A signer's contribution is counted at most once across the receipt's +// signatures, so a duplicated signature does not satisfy the threshold. +type ReceiptVerifier struct { + source SignerSource + + mu sync.RWMutex + signers map[common.Address]struct{} + threshold int + // refreshedAt/maxAge bound how long a node may trust a cached signer set + // after refresh failures. Signer rotation must fail closed, not accept an + // old quorum forever when the source is unreachable. + refreshedAt time.Time + maxAge time.Duration +} + +// NewReceiptVerifier returns a verifier backed by the given SignerSource. +// Refresh must be called before either Verify* method; production callers +// do this at startup and on a periodic ticker (see RunReceiptVerifierRefresh). +// maxAge bounds cache staleness; non-positive falls back to the package +// default (15 minutes). +func NewReceiptVerifier(source SignerSource, maxAge time.Duration) *ReceiptVerifier { + if maxAge <= 0 { + maxAge = defaultReceiptVerifierMaxAge + } + return &ReceiptVerifier{source: source, maxAge: maxAge} +} + +// Refresh reads the current signer set and threshold from the SignerSource +// and replaces the cached view atomically. +func (rv *ReceiptVerifier) Refresh(ctx context.Context) error { + if rv == nil { + return errors.New("receipt verifier not configured") + } + if rv.source == nil { + return errors.New("receipt verifier has no signer source") + } + signers, threshold, err := rv.source.Load(ctx) + if err != nil { + return fmt.Errorf("load custody signers: %w", err) + } + if threshold <= 0 || threshold > len(signers) { + return fmt.Errorf("custody threshold = %d out of range for %d signers", threshold, len(signers)) + } + set := make(map[common.Address]struct{}, len(signers)) + for _, s := range signers { + set[s] = struct{}{} + } + rv.mu.Lock() + rv.signers = set + rv.threshold = threshold + rv.refreshedAt = time.Now() + rv.mu.Unlock() + return nil +} + +// SetSignersForTest seeds the cache from an explicit signer list. Tests use +// this to drive the verifier without an on-chain reader. +func (rv *ReceiptVerifier) SetSignersForTest(signers []common.Address, threshold int) { + if rv == nil { + return + } + set := make(map[common.Address]struct{}, len(signers)) + for _, s := range signers { + set[s] = struct{}{} + } + rv.mu.Lock() + rv.signers = set + rv.threshold = threshold + rv.refreshedAt = time.Now() + rv.mu.Unlock() +} + +// VerifyBurnReceipt checks that the receipt carries at least `threshold` +// distinct valid signatures from the cached signer set over BurnReceiptDigest. +func (rv *ReceiptVerifier) VerifyBurnReceipt(v *core.BurnReceipt) error { + if v == nil { + return errors.New("nil burn receipt") + } + return rv.verifySignatures(BurnReceiptDigest(v), v.Signatures) +} + +// VerifyMintReceipt checks that the receipt carries at least `threshold` +// distinct valid signatures from the cached signer set over MintReceiptDigest. +func (rv *ReceiptVerifier) VerifyMintReceipt(v *core.MintReceipt) error { + if v == nil { + return errors.New("nil mint receipt") + } + if v.Amount == nil || v.Amount.Sign() <= 0 { + return errors.New("mint receipt amount must be positive") + } + return rv.verifySignatures(MintReceiptDigest(v), v.Signatures) +} + +// verifySignatures is the shared signature-quorum check used by both receipt +// kinds. It enforces the staleness window, the count floor, and distinct-signer +// quorum. +func (rv *ReceiptVerifier) verifySignatures(digest []byte, sigs [][]byte) error { + if rv == nil { + return errors.New("receipt verifier not configured") + } + rv.mu.RLock() + signers := rv.signers + threshold := rv.threshold + refreshedAt := rv.refreshedAt + maxAge := rv.maxAge + rv.mu.RUnlock() + if threshold <= 0 || len(signers) == 0 { + return errors.New("receipt verifier signer set not initialised") + } + if maxAge > 0 { + age := time.Since(refreshedAt) + if refreshedAt.IsZero() || age > maxAge { + return fmt.Errorf("receipt verifier signer set stale: age %s > %s", age, maxAge) + } + } + if len(sigs) < threshold { + return fmt.Errorf("insufficient signatures: %d < %d", len(sigs), threshold) + } + seen := make(map[common.Address]struct{}, threshold) + for _, sig := range sigs { + addr, ok, err := recoverReceiptSigner(digest, sig) + if err != nil { + continue + } + if !ok { + continue + } + if _, isSigner := signers[addr]; !isSigner { + continue + } + if _, dup := seen[addr]; dup { + continue + } + seen[addr] = struct{}{} + if len(seen) >= threshold { + return nil + } + } + return fmt.Errorf("insufficient distinct signers: %d/%d", len(seen), threshold) +} + +// recoverReceiptSigner dispatches signature verification by length and +// returns the signer address that produced it. ED25519 is recognised but not +// yet implemented; non-canonical lengths are ignored (ok=false, err=nil). +func recoverReceiptSigner(digest, sig []byte) (common.Address, bool, error) { + switch len(sig) { + case 65: + addr, err := eip712.RecoverSigner(digest, sig) + if err != nil { + return common.Address{}, false, err + } + return addr, true, nil + case 64: + // XRPL / Solana ED25519 lands when its custody adapter is wired + // (ADR-005 §10). Until then the receipt path is ECDSA-only. + return common.Address{}, false, nil + default: + return common.Address{}, false, nil + } +} + +// BurnReceiptDigest is the keccak256 digest custody providers sign over for +// withdrawal execution attestations. +// Format: keccak256(WithdrawalID || BlockHash || EntryIndex[uint64be] || L1TxHash). +// Exported so custody-side tooling and the custodytesting package can build +// matching signatures. +func BurnReceiptDigest(v *core.BurnReceipt) []byte { + buf := make([]byte, 0, 32+32+8+32) + buf = append(buf, v.WithdrawalID[:]...) + buf = append(buf, v.BlockHash[:]...) + var index [8]byte + binary.BigEndian.PutUint64(index[:], v.EntryIndex) + buf = append(buf, index[:]...) + buf = append(buf, v.L1TxHash[:]...) + return crypto.Keccak256(buf) +} + +// MintReceiptDigest is the keccak256 digest custody providers sign over for +// deposit confirmation attestations. Exported so custody-side tooling can +// produce matching signatures. +// +// Format: keccak256( +// +// ChainID[uint64be] || L1TxHash || LogIndex[uint64be] || +// len(Account)[uint32be] || Account || +// len(Asset)[uint32be] || Asset || +// Amount[uint256be]) +// +// The length prefixes on Account and Asset prevent boundary-shift +// collisions between variable-length string pairs. +func MintReceiptDigest(v *core.MintReceipt) []byte { + var amountBE [32]byte + if v.Amount != nil && v.Amount.Sign() > 0 { + v.Amount.FillBytes(amountBE[:]) + } + buf := make([]byte, 0, 8+32+8+4+len(v.Account)+4+len(v.Asset)+32) + var u64 [8]byte + binary.BigEndian.PutUint64(u64[:], v.ChainID) + buf = append(buf, u64[:]...) + buf = append(buf, v.L1TxHash[:]...) + binary.BigEndian.PutUint64(u64[:], v.LogIndex) + buf = append(buf, u64[:]...) + var u32 [4]byte + binary.BigEndian.PutUint32(u32[:], uint32(len(v.Account))) + buf = append(buf, u32[:]...) + buf = append(buf, []byte(v.Account)...) + binary.BigEndian.PutUint32(u32[:], uint32(len(v.Asset))) + buf = append(buf, u32[:]...) + buf = append(buf, []byte(v.Asset)...) + buf = append(buf, amountBE[:]...) + return crypto.Keccak256(buf) +} + +// SignerCount reports the cached signer-set size for diagnostics. +func (rv *ReceiptVerifier) SignerCount() int { + if rv == nil { + return 0 + } + rv.mu.RLock() + defer rv.mu.RUnlock() + return len(rv.signers) +} + +// Threshold reports the cached signer threshold for diagnostics. +func (rv *ReceiptVerifier) Threshold() int { + if rv == nil { + return 0 + } + rv.mu.RLock() + defer rv.mu.RUnlock() + return rv.threshold +} + +// RunReceiptVerifierRefresh periodically refreshes the verifier's cached +// signer set. It returns when ctx is cancelled. +func RunReceiptVerifierRefresh(ctx context.Context, rv *ReceiptVerifier, interval time.Duration, onError func(error)) { + if rv == nil || interval <= 0 { + return + } + t := time.NewTicker(interval) + defer t.Stop() + for { + select { + case <-ctx.Done(): + return + case <-t.C: + if err := rv.Refresh(ctx); err != nil && onError != nil { + onError(err) + } + } + } +} diff --git a/pkg/receipt/receipt_verifier_test.go b/pkg/receipt/receipt_verifier_test.go new file mode 100644 index 0000000..a82c500 --- /dev/null +++ b/pkg/receipt/receipt_verifier_test.go @@ -0,0 +1,399 @@ +package receipt + +import ( + "context" + "crypto/ecdsa" + "errors" + "math/big" + "strings" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/layer-3/clearnet-sdk/pkg/core" +) + +// stubSignerSource is a controllable SignerSource for tests. +type stubSignerSource struct { + signers []common.Address + threshold int + loadErr error +} + +func (s *stubSignerSource) Load(context.Context) ([]common.Address, int, error) { + if s.loadErr != nil { + return nil, 0, s.loadErr + } + out := make([]common.Address, len(s.signers)) + copy(out, s.signers) + return out, s.threshold, nil +} + +func mustGenerateKey(t *testing.T) *ecdsa.PrivateKey { + t.Helper() + k, err := crypto.GenerateKey() + if err != nil { + t.Fatalf("generate key: %v", err) + } + return k +} + +func makeReceipt(seed byte) *core.BurnReceipt { + r := &core.BurnReceipt{} + r.WithdrawalID = [32]byte{seed, 0xA1} + r.BlockHash = [32]byte{seed, 0xB2} + r.EntryIndex = uint64(seed) + r.L1TxHash = [32]byte{seed, 0xC3} + return r +} + +func signWith(t *testing.T, r *core.BurnReceipt, keys ...*ecdsa.PrivateKey) { + t.Helper() + digest := BurnReceiptDigest(r) + r.Signatures = make([][]byte, len(keys)) + for i, k := range keys { + sig, err := crypto.Sign(digest, k) + if err != nil { + t.Fatalf("sign[%d]: %v", i, err) + } + r.Signatures[i] = sig + } +} + +func TestReceiptVerifier_RefreshPopulatesCache(t *testing.T) { + signers := []common.Address{ + common.HexToAddress("0x1111111111111111111111111111111111111111"), + common.HexToAddress("0x2222222222222222222222222222222222222222"), + common.HexToAddress("0x3333333333333333333333333333333333333333"), + } + rv := NewReceiptVerifier(&stubSignerSource{ + signers: signers, + threshold: 2, + }, 0) + if err := rv.Refresh(context.Background()); err != nil { + t.Fatalf("Refresh: %v", err) + } + if got := rv.SignerCount(); got != 3 { + t.Fatalf("SignerCount = %d, want 3", got) + } + if got := rv.Threshold(); got != 2 { + t.Fatalf("Threshold = %d, want 2", got) + } +} + +func TestReceiptVerifier_RefreshFailsClosedOnSourceErrors(t *testing.T) { + cases := []struct { + name string + source *stubSignerSource + want string + }{ + { + name: "Load error", + source: &stubSignerSource{loadErr: errors.New("source down")}, + want: "load custody signers", + }, + { + name: "threshold zero", + source: &stubSignerSource{ + signers: []common.Address{common.HexToAddress("0x01")}, + threshold: 0, + }, + want: "out of range", + }, + { + name: "threshold exceeds signer count", + source: &stubSignerSource{ + signers: []common.Address{common.HexToAddress("0x01")}, + threshold: 2, + }, + want: "out of range", + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + rv := NewReceiptVerifier(tc.source, 0) + err := rv.Refresh(context.Background()) + if err == nil { + t.Fatal("expected Refresh to fail") + } + if !strings.Contains(err.Error(), tc.want) { + t.Fatalf("Refresh error = %v, want substring %q", err, tc.want) + } + }) + } +} + +func TestReceiptVerifier_RefreshAtomicallyReplacesCache(t *testing.T) { + first := []common.Address{common.HexToAddress("0xAA")} + second := []common.Address{ + common.HexToAddress("0xBB"), + common.HexToAddress("0xCC"), + } + source := &stubSignerSource{signers: first, threshold: 1} + rv := NewReceiptVerifier(source, 0) + if err := rv.Refresh(context.Background()); err != nil { + t.Fatalf("first refresh: %v", err) + } + + source.signers = second + source.threshold = 2 + if err := rv.Refresh(context.Background()); err != nil { + t.Fatalf("second refresh: %v", err) + } + if got := rv.SignerCount(); got != 2 { + t.Fatalf("SignerCount after refresh = %d, want 2", got) + } + if got := rv.Threshold(); got != 2 { + t.Fatalf("Threshold after refresh = %d, want 2", got) + } +} + +func TestReceiptVerifier_VerifyHappyPath(t *testing.T) { + keys := []*ecdsa.PrivateKey{mustGenerateKey(t), mustGenerateKey(t), mustGenerateKey(t)} + addrs := []common.Address{ + crypto.PubkeyToAddress(keys[0].PublicKey), + crypto.PubkeyToAddress(keys[1].PublicKey), + crypto.PubkeyToAddress(keys[2].PublicKey), + } + rv := NewReceiptVerifier(nil, 0) + rv.SetSignersForTest(addrs, 2) + + r := makeReceipt(0x10) + signWith(t, r, keys[0], keys[2]) + + if err := rv.VerifyBurnReceipt(r); err != nil { + t.Fatalf("Verify: %v", err) + } +} + +func TestReceiptVerifier_VerifyAcceptsEthereumCanonicalV(t *testing.T) { + key := mustGenerateKey(t) + rv := NewReceiptVerifier(nil, 0) + rv.SetSignersForTest([]common.Address{crypto.PubkeyToAddress(key.PublicKey)}, 1) + + r := makeReceipt(0x17) + signWith(t, r, key) + r.Signatures[0][64] += 27 + + if err := rv.VerifyBurnReceipt(r); err != nil { + t.Fatalf("Verify with Ethereum v=27/28: %v", err) + } +} + +func TestReceiptVerifier_VerifyFailsClosedOnStaleCache(t *testing.T) { + key := mustGenerateKey(t) + rv := NewReceiptVerifier(nil, 0) + rv.SetSignersForTest([]common.Address{crypto.PubkeyToAddress(key.PublicKey)}, 1) + rv.mu.Lock() + rv.refreshedAt = time.Now().Add(-time.Hour) + rv.maxAge = time.Minute + rv.mu.Unlock() + + r := makeReceipt(0x18) + signWith(t, r, key) + err := rv.VerifyBurnReceipt(r) + if err == nil || !strings.Contains(err.Error(), "signer set stale") { + t.Fatalf("Verify: want stale signer set error, got %v", err) + } +} + +func TestReceiptVerifier_VerifyExitsAsSoonAsThresholdMet(t *testing.T) { + // Pool has 5 signers; threshold 3; receipt carries 5 sigs but the + // first three suffice. The verifier returns nil without scanning sigs + // 4 and 5 (which is the property under test — we feed garbage in + // those slots and verification still succeeds). + keys := make([]*ecdsa.PrivateKey, 5) + addrs := make([]common.Address, 5) + for i := range keys { + keys[i] = mustGenerateKey(t) + addrs[i] = crypto.PubkeyToAddress(keys[i].PublicKey) + } + rv := NewReceiptVerifier(nil, 0) + rv.SetSignersForTest(addrs, 3) + + r := makeReceipt(0x11) + signWith(t, r, keys[0], keys[1], keys[2]) + // Append two malformed sigs; if the verifier scanned them it would + // still succeed (they're skipped), but the test asserts threshold-met + // short-circuits regardless. + r.Signatures = append(r.Signatures, []byte("garbage"), make([]byte, 65)) + + if err := rv.VerifyBurnReceipt(r); err != nil { + t.Fatalf("Verify: %v", err) + } +} + +func TestReceiptVerifier_VerifyRejectsTooFewSignatures(t *testing.T) { + keys := []*ecdsa.PrivateKey{mustGenerateKey(t), mustGenerateKey(t), mustGenerateKey(t)} + addrs := []common.Address{ + crypto.PubkeyToAddress(keys[0].PublicKey), + crypto.PubkeyToAddress(keys[1].PublicKey), + crypto.PubkeyToAddress(keys[2].PublicKey), + } + rv := NewReceiptVerifier(nil, 0) + rv.SetSignersForTest(addrs, 3) + + r := makeReceipt(0x12) + signWith(t, r, keys[0], keys[1]) + + err := rv.VerifyBurnReceipt(r) + if err == nil || !strings.Contains(err.Error(), "insufficient signatures") { + t.Fatalf("Verify: want 'insufficient signatures' error, got %v", err) + } +} + +func TestReceiptVerifier_VerifyRejectsDuplicateSigner(t *testing.T) { + keys := []*ecdsa.PrivateKey{mustGenerateKey(t), mustGenerateKey(t), mustGenerateKey(t)} + addrs := []common.Address{ + crypto.PubkeyToAddress(keys[0].PublicKey), + crypto.PubkeyToAddress(keys[1].PublicKey), + crypto.PubkeyToAddress(keys[2].PublicKey), + } + rv := NewReceiptVerifier(nil, 0) + rv.SetSignersForTest(addrs, 2) + + r := makeReceipt(0x13) + // Two valid signatures from the same signer must NOT count as two + // distinct contributors. + signWith(t, r, keys[0], keys[0]) + + err := rv.VerifyBurnReceipt(r) + if err == nil || !strings.Contains(err.Error(), "insufficient distinct signers") { + t.Fatalf("Verify: want 'insufficient distinct signers' error, got %v", err) + } +} + +func TestReceiptVerifier_VerifyRejectsNonSigner(t *testing.T) { + signerKey := mustGenerateKey(t) + intruderKey := mustGenerateKey(t) + addrs := []common.Address{ + crypto.PubkeyToAddress(signerKey.PublicKey), + } + rv := NewReceiptVerifier(nil, 0) + rv.SetSignersForTest(addrs, 1) + + r := makeReceipt(0x14) + signWith(t, r, intruderKey) + + err := rv.VerifyBurnReceipt(r) + if err == nil || !strings.Contains(err.Error(), "insufficient distinct signers") { + t.Fatalf("Verify: want 'insufficient distinct signers' error, got %v", err) + } +} + +func TestReceiptVerifier_VerifyED25519IsStubAndIgnored(t *testing.T) { + // 64-byte signatures are recognised as ED25519 and ignored until the + // XRPL/Solana custody adapter lands. They must not be counted toward + // the threshold. + signerKey := mustGenerateKey(t) + addrs := []common.Address{crypto.PubkeyToAddress(signerKey.PublicKey)} + rv := NewReceiptVerifier(nil, 0) + rv.SetSignersForTest(addrs, 1) + + r := makeReceipt(0x15) + r.Signatures = [][]byte{make([]byte, 64)} + err := rv.VerifyBurnReceipt(r) + // Sig count >= threshold so the early-out doesn't trigger; ED25519 is + // recognised but ignored, so we end up with zero distinct signers. + if err == nil || !strings.Contains(err.Error(), "insufficient distinct signers") { + t.Fatalf("Verify: want ED25519-stub rejection, got %v", err) + } +} + +func TestReceiptVerifier_VerifyFailsClosedOnUninitialisedCache(t *testing.T) { + rv := NewReceiptVerifier(nil, 0) + r := makeReceipt(0x16) + r.Signatures = [][]byte{make([]byte, 65)} + err := rv.VerifyBurnReceipt(r) + if err == nil || !strings.Contains(err.Error(), "signer set not initialised") { + t.Fatalf("Verify: want uninitialised error, got %v", err) + } +} + +func TestReceiptVerifier_VerifyRejectsNilReceipt(t *testing.T) { + rv := NewReceiptVerifier(nil, 0) + rv.SetSignersForTest([]common.Address{common.HexToAddress("0x01")}, 1) + if err := rv.VerifyBurnReceipt(nil); err == nil { + t.Fatal("Verify(nil): want error") + } +} + +func TestReceiptVerifier_NilReceiverFailsClosed(t *testing.T) { + var rv *ReceiptVerifier + if err := rv.VerifyBurnReceipt(makeReceipt(0)); err == nil { + t.Fatal("Verify on nil verifier: want error") + } + if err := rv.Refresh(context.Background()); err == nil { + t.Fatal("Refresh on nil verifier: want error") + } +} + +func TestReceiptVerifier_DigestIsDeterministic(t *testing.T) { + r1 := makeReceipt(0x20) + r2 := makeReceipt(0x20) + d1 := BurnReceiptDigest(r1) + d2 := BurnReceiptDigest(r2) + if string(d1) != string(d2) { + t.Fatalf("digest non-deterministic: %x vs %x", d1, d2) + } + // Mutating any field changes the digest. + r2.L1TxHash[0] ^= 0xFF + d3 := BurnReceiptDigest(r2) + if string(d1) == string(d3) { + t.Fatal("digest unchanged after mutating L1TxHash") + } +} + +// Without uint32 length prefixes on Account and Asset, ("abc","XYZ") and +// ("abcX","YZ") would hash to the same preimage. +func TestMintReceiptDigest_NoStringCollision(t *testing.T) { + mk := func(account, asset string) *core.MintReceipt { + return &core.MintReceipt{ + ChainID: 1, + Account: account, + Asset: asset, + Amount: big.NewInt(1), + } + } + d1 := MintReceiptDigest(mk("abc", "XYZ")) + d2 := MintReceiptDigest(mk("abcX", "YZ")) + if string(d1) == string(d2) { + t.Fatalf("digest collided across (Account,Asset) boundary shift: %x", d1) + } +} + +func TestMintReceiptDigest_FieldSensitivity(t *testing.T) { + mk := func() *core.MintReceipt { + return &core.MintReceipt{ + ChainID: 1, + L1TxHash: [32]byte{0xAA}, + LogIndex: 1, + Account: "yellow://ynet/user/0xabc", + Asset: "USDC", + Amount: big.NewInt(1), + } + } + base := MintReceiptDigest(mk()) + + cases := []struct { + name string + mut func(*core.MintReceipt) + }{ + {"ChainID", func(r *core.MintReceipt) { r.ChainID = 2 }}, + {"L1TxHash", func(r *core.MintReceipt) { r.L1TxHash[0] ^= 0xFF }}, + {"LogIndex", func(r *core.MintReceipt) { r.LogIndex = 2 }}, + {"Account", func(r *core.MintReceipt) { r.Account = "yellow://ynet/user/0xabd" }}, + {"Asset", func(r *core.MintReceipt) { r.Asset = "USDT" }}, + {"Amount", func(r *core.MintReceipt) { r.Amount = big.NewInt(2) }}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + r := mk() + tc.mut(r) + if string(MintReceiptDigest(r)) == string(base) { + t.Fatalf("%s mutation did not change digest", tc.name) + } + }) + } +} diff --git a/pkg/receipt/static_signer_source.go b/pkg/receipt/static_signer_source.go new file mode 100644 index 0000000..31b41f9 --- /dev/null +++ b/pkg/receipt/static_signer_source.go @@ -0,0 +1,51 @@ +package receipt + +import ( + "context" + "errors" + "fmt" + + "github.com/ethereum/go-ethereum/common" +) + +// StaticSignerSource returns a fixed signer set and threshold supplied at +// construction time. It is the production SignerSource implementation for +// the receipt verifier today: operators publish the custody signer set in +// the manifest, and every node loads the same list. +// +// When the on-chain Registry grows a custody-signers view, a +// RegistrySignerSource will replace this implementation at the call site; +// no verifier-side code changes are needed. +type StaticSignerSource struct { + signers []common.Address + threshold int +} + +// NewStaticSignerSource validates the inputs and returns a source that +// hands the same (signers, threshold) pair to every Load call. +func NewStaticSignerSource(signers []common.Address, threshold int) (*StaticSignerSource, error) { + if len(signers) == 0 { + return nil, errors.New("custody signer set is empty") + } + if threshold <= 0 || threshold > len(signers) { + return nil, fmt.Errorf("custody threshold %d out of range for %d signers", threshold, len(signers)) + } + seen := make(map[common.Address]struct{}, len(signers)) + out := make([]common.Address, 0, len(signers)) + for _, s := range signers { + if _, dup := seen[s]; dup { + return nil, fmt.Errorf("duplicate custody signer %s", s.Hex()) + } + seen[s] = struct{}{} + out = append(out, s) + } + return &StaticSignerSource{signers: out, threshold: threshold}, nil +} + +// Load returns the configured signers and threshold. The slice is copied +// so callers can mutate it without affecting the source. +func (s *StaticSignerSource) Load(_ context.Context) ([]common.Address, int, error) { + out := make([]common.Address, len(s.signers)) + copy(out, s.signers) + return out, s.threshold, nil +} diff --git a/scripts/ci/check-preimage-goldens.sh b/scripts/ci/check-preimage-goldens.sh new file mode 100755 index 0000000..0b63d31 --- /dev/null +++ b/scripts/ci/check-preimage-goldens.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# check-preimage-goldens.sh — freeze the byte output of the signing +# preimages against committed goldens. +# +# The SDK owns the byte-for-byte custody↔clearnet contract (ADR-009 §4): +# +# 1. Block.SigningMessage = canonical CBOR of BlockHeader. +# 2. BlockEntry CBOR (input to EntryHash = keccak256 over this). +# 3. BlockEntry.Payload = canonical CBOR of the typed op (Transfer, +# Swap, Withdrawal, Repeg, SessionClose, SessionChallenge). +# 4. FinalizedWithdrawal.SigningMessage (BLS finality preimage). +# 5. BLS G1/G2 serialization + aggregate layout pinned to the Solidity +# verifiers (BLS.sol / Slasher.sol). +# +# A change to any of these bytes without a coordinated version-byte +# bump (ADR-009 §5) is a schema-family break. This guard runs the +# committed golden-tests in compare mode and fails on any drift. It +# intentionally does NOT pass `-update` — a re-seed is an explicit +# action under an explicit commit message, not CI-automatic. +# +# Usage: +# +# ./scripts/ci/check-preimage-goldens.sh # verify only +# +# To regenerate after an intentional, reviewed change: +# +# go test ./pkg/core/ -run TestGoldens_Preimages -update +# go test ./pkg/bls/ -run TestGoldens_Preimages -update +# +# Then commit both the source change and the touched +# testdata/goldens/* files in the same commit so the CI run that +# follows sees a clean tree. + +set -euo pipefail +cd "$(git rev-parse --show-toplevel)" + +echo "check-preimage-goldens: running TestGoldens_Preimages in compare mode..." +go test -run '^TestGoldens_Preimages$' -count=1 ./pkg/core/ ./pkg/bls/ + +# Defense in depth: if the testdata directory disappeared or was wiped, +# the test would trivially pass in -update mode only. We explicitly +# assert the fixture files exist so an accidental `rm -rf` gets caught. +missing=0 +for f in \ + testdata/goldens/preimages/block_header/empty_accounts.golden.hex \ + testdata/goldens/preimages/block_header/empty_accounts.input.json \ + testdata/goldens/preimages/block_header/single_account.golden.hex \ + testdata/goldens/preimages/block_header/single_account.input.json \ + testdata/goldens/preimages/entry_hash/transfer.golden.hex \ + testdata/goldens/preimages/entry_hash/transfer.input.json \ + testdata/goldens/preimages/op_payload/transfer_single_asset.golden.hex \ + testdata/goldens/preimages/op_payload/transfer_single_asset.input.json \ + testdata/goldens/preimages/op_payload/swap.golden.hex \ + testdata/goldens/preimages/op_payload/swap.input.json \ + testdata/goldens/preimages/op_payload/withdrawal.golden.hex \ + testdata/goldens/preimages/op_payload/withdrawal.input.json \ + testdata/goldens/preimages/op_payload/repeg.golden.hex \ + testdata/goldens/preimages/op_payload/repeg.input.json \ + testdata/goldens/preimages/op_payload/session_close.golden.hex \ + testdata/goldens/preimages/op_payload/session_close.input.json \ + testdata/goldens/preimages/op_payload/session_challenge.golden.hex \ + testdata/goldens/preimages/op_payload/session_challenge.input.json \ + testdata/goldens/preimages/finalized_withdrawal/header.golden.hex \ + testdata/goldens/preimages/finalized_withdrawal/header.input.json \ + testdata/goldens/solidity-preimages/bls/g1_identity.golden.hex \ + testdata/goldens/solidity-preimages/bls/g1_generator.golden.hex \ + testdata/goldens/solidity-preimages/bls/g1_random.golden.hex \ + testdata/goldens/solidity-preimages/bls/g2_identity.golden.hex \ + testdata/goldens/solidity-preimages/bls/g2_generator.golden.hex \ + testdata/goldens/solidity-preimages/bls/g2_random.golden.hex \ + testdata/goldens/solidity-preimages/bls/aggregate_sig.golden.hex \ + ; do + if [[ ! -s "$f" ]]; then + echo "check-preimage-goldens: MISSING fixture: $f" >&2 + missing=1 + fi +done +if [[ $missing -ne 0 ]]; then + echo "check-preimage-goldens: one or more fixture files missing; " \ + "re-run the golden tests with -update to regenerate." >&2 + exit 1 +fi + +echo "check-preimage-goldens: ok" diff --git a/testdata/cbor/primitives/addr20_vitalik_like.golden.hex b/testdata/cbor/primitives/addr20_vitalik_like.golden.hex new file mode 100644 index 0000000..2fcacc6 --- /dev/null +++ b/testdata/cbor/primitives/addr20_vitalik_like.golden.hex @@ -0,0 +1 @@ +54d8da6bf26964af9d7eed9e03e53415d37aa96045 diff --git a/testdata/cbor/primitives/addr20_vitalik_like.input.json b/testdata/cbor/primitives/addr20_vitalik_like.input.json new file mode 100644 index 0000000..220ed7c --- /dev/null +++ b/testdata/cbor/primitives/addr20_vitalik_like.input.json @@ -0,0 +1,3 @@ +{ + "hex": "d8da6bf26964af9d7eed9e03e53415d37aa96045" +} diff --git a/testdata/cbor/primitives/addr20_zero.golden.hex b/testdata/cbor/primitives/addr20_zero.golden.hex new file mode 100644 index 0000000..53557db --- /dev/null +++ b/testdata/cbor/primitives/addr20_zero.golden.hex @@ -0,0 +1 @@ +540000000000000000000000000000000000000000 diff --git a/testdata/cbor/primitives/addr20_zero.input.json b/testdata/cbor/primitives/addr20_zero.input.json new file mode 100644 index 0000000..2895785 --- /dev/null +++ b/testdata/cbor/primitives/addr20_zero.input.json @@ -0,0 +1,3 @@ +{ + "hex": "0000000000000000000000000000000000000000" +} diff --git a/testdata/cbor/primitives/bigint_boundary_255.golden.hex b/testdata/cbor/primitives/bigint_boundary_255.golden.hex new file mode 100644 index 0000000..f4bdc24 --- /dev/null +++ b/testdata/cbor/primitives/bigint_boundary_255.golden.hex @@ -0,0 +1 @@ +c241ff diff --git a/testdata/cbor/primitives/bigint_boundary_255.input.json b/testdata/cbor/primitives/bigint_boundary_255.input.json new file mode 100644 index 0000000..ef19498 --- /dev/null +++ b/testdata/cbor/primitives/bigint_boundary_255.input.json @@ -0,0 +1,4 @@ +{ + "hex": "ff", + "sign": 1 +} diff --git a/testdata/cbor/primitives/bigint_boundary_256.golden.hex b/testdata/cbor/primitives/bigint_boundary_256.golden.hex new file mode 100644 index 0000000..1ac7118 --- /dev/null +++ b/testdata/cbor/primitives/bigint_boundary_256.golden.hex @@ -0,0 +1 @@ +c2420100 diff --git a/testdata/cbor/primitives/bigint_boundary_256.input.json b/testdata/cbor/primitives/bigint_boundary_256.input.json new file mode 100644 index 0000000..d4eb498 --- /dev/null +++ b/testdata/cbor/primitives/bigint_boundary_256.input.json @@ -0,0 +1,4 @@ +{ + "hex": "100", + "sign": 1 +} diff --git a/testdata/cbor/primitives/bigint_minus_25.golden.hex b/testdata/cbor/primitives/bigint_minus_25.golden.hex new file mode 100644 index 0000000..775cd78 --- /dev/null +++ b/testdata/cbor/primitives/bigint_minus_25.golden.hex @@ -0,0 +1 @@ +c34118 diff --git a/testdata/cbor/primitives/bigint_minus_25.input.json b/testdata/cbor/primitives/bigint_minus_25.input.json new file mode 100644 index 0000000..6f6621d --- /dev/null +++ b/testdata/cbor/primitives/bigint_minus_25.input.json @@ -0,0 +1,4 @@ +{ + "hex": "-19", + "sign": -1 +} diff --git a/testdata/cbor/primitives/bigint_minus_one.golden.hex b/testdata/cbor/primitives/bigint_minus_one.golden.hex new file mode 100644 index 0000000..30d0655 --- /dev/null +++ b/testdata/cbor/primitives/bigint_minus_one.golden.hex @@ -0,0 +1 @@ +c340 diff --git a/testdata/cbor/primitives/bigint_minus_one.input.json b/testdata/cbor/primitives/bigint_minus_one.input.json new file mode 100644 index 0000000..30a82a1 --- /dev/null +++ b/testdata/cbor/primitives/bigint_minus_one.input.json @@ -0,0 +1,4 @@ +{ + "hex": "-1", + "sign": -1 +} diff --git a/testdata/cbor/primitives/bigint_neg_two_pow_128.golden.hex b/testdata/cbor/primitives/bigint_neg_two_pow_128.golden.hex new file mode 100644 index 0000000..e43c21e --- /dev/null +++ b/testdata/cbor/primitives/bigint_neg_two_pow_128.golden.hex @@ -0,0 +1 @@ +c350ffffffffffffffffffffffffffffffff diff --git a/testdata/cbor/primitives/bigint_neg_two_pow_128.input.json b/testdata/cbor/primitives/bigint_neg_two_pow_128.input.json new file mode 100644 index 0000000..89ebcc1 --- /dev/null +++ b/testdata/cbor/primitives/bigint_neg_two_pow_128.input.json @@ -0,0 +1,4 @@ +{ + "hex": "-100000000000000000000000000000000", + "sign": -1 +} diff --git a/testdata/cbor/primitives/bigint_one.golden.hex b/testdata/cbor/primitives/bigint_one.golden.hex new file mode 100644 index 0000000..549cadc --- /dev/null +++ b/testdata/cbor/primitives/bigint_one.golden.hex @@ -0,0 +1 @@ +c24101 diff --git a/testdata/cbor/primitives/bigint_one.input.json b/testdata/cbor/primitives/bigint_one.input.json new file mode 100644 index 0000000..83628f1 --- /dev/null +++ b/testdata/cbor/primitives/bigint_one.input.json @@ -0,0 +1,4 @@ +{ + "hex": "1", + "sign": 1 +} diff --git a/testdata/cbor/primitives/bigint_two_pow_128.golden.hex b/testdata/cbor/primitives/bigint_two_pow_128.golden.hex new file mode 100644 index 0000000..d1259e5 --- /dev/null +++ b/testdata/cbor/primitives/bigint_two_pow_128.golden.hex @@ -0,0 +1 @@ +c2510100000000000000000000000000000000 diff --git a/testdata/cbor/primitives/bigint_two_pow_128.input.json b/testdata/cbor/primitives/bigint_two_pow_128.input.json new file mode 100644 index 0000000..7573206 --- /dev/null +++ b/testdata/cbor/primitives/bigint_two_pow_128.input.json @@ -0,0 +1,4 @@ +{ + "hex": "100000000000000000000000000000000", + "sign": 1 +} diff --git a/testdata/cbor/primitives/bigint_u32_max.golden.hex b/testdata/cbor/primitives/bigint_u32_max.golden.hex new file mode 100644 index 0000000..4c6de73 --- /dev/null +++ b/testdata/cbor/primitives/bigint_u32_max.golden.hex @@ -0,0 +1 @@ +c244ffffffff diff --git a/testdata/cbor/primitives/bigint_u32_max.input.json b/testdata/cbor/primitives/bigint_u32_max.input.json new file mode 100644 index 0000000..4bbf5e5 --- /dev/null +++ b/testdata/cbor/primitives/bigint_u32_max.input.json @@ -0,0 +1,4 @@ +{ + "hex": "ffffffff", + "sign": 1 +} diff --git a/testdata/cbor/primitives/bigint_u32_max_plus_one.golden.hex b/testdata/cbor/primitives/bigint_u32_max_plus_one.golden.hex new file mode 100644 index 0000000..0b45158 --- /dev/null +++ b/testdata/cbor/primitives/bigint_u32_max_plus_one.golden.hex @@ -0,0 +1 @@ +c2450100000000 diff --git a/testdata/cbor/primitives/bigint_u32_max_plus_one.input.json b/testdata/cbor/primitives/bigint_u32_max_plus_one.input.json new file mode 100644 index 0000000..a7d90f4 --- /dev/null +++ b/testdata/cbor/primitives/bigint_u32_max_plus_one.input.json @@ -0,0 +1,4 @@ +{ + "hex": "100000000", + "sign": 1 +} diff --git a/testdata/cbor/primitives/bigint_u64_max.golden.hex b/testdata/cbor/primitives/bigint_u64_max.golden.hex new file mode 100644 index 0000000..55662a2 --- /dev/null +++ b/testdata/cbor/primitives/bigint_u64_max.golden.hex @@ -0,0 +1 @@ +c248ffffffffffffffff diff --git a/testdata/cbor/primitives/bigint_u64_max.input.json b/testdata/cbor/primitives/bigint_u64_max.input.json new file mode 100644 index 0000000..10d23f4 --- /dev/null +++ b/testdata/cbor/primitives/bigint_u64_max.input.json @@ -0,0 +1,4 @@ +{ + "hex": "ffffffffffffffff", + "sign": 1 +} diff --git a/testdata/cbor/primitives/bigint_zero.golden.hex b/testdata/cbor/primitives/bigint_zero.golden.hex new file mode 100644 index 0000000..1b3b81f --- /dev/null +++ b/testdata/cbor/primitives/bigint_zero.golden.hex @@ -0,0 +1 @@ +c240 diff --git a/testdata/cbor/primitives/bigint_zero.input.json b/testdata/cbor/primitives/bigint_zero.input.json new file mode 100644 index 0000000..fb632df --- /dev/null +++ b/testdata/cbor/primitives/bigint_zero.input.json @@ -0,0 +1,4 @@ +{ + "hex": "0", + "sign": 0 +} diff --git a/testdata/cbor/primitives/decimal_mega_unit.golden.hex b/testdata/cbor/primitives/decimal_mega_unit.golden.hex new file mode 100644 index 0000000..79490f1 --- /dev/null +++ b/testdata/cbor/primitives/decimal_mega_unit.golden.hex @@ -0,0 +1 @@ +c48206c24101 diff --git a/testdata/cbor/primitives/decimal_mega_unit.input.json b/testdata/cbor/primitives/decimal_mega_unit.input.json new file mode 100644 index 0000000..30b4c12 --- /dev/null +++ b/testdata/cbor/primitives/decimal_mega_unit.input.json @@ -0,0 +1,4 @@ +{ + "mantissa": "1", + "exponent": 6 +} diff --git a/testdata/cbor/primitives/decimal_neg_half.golden.hex b/testdata/cbor/primitives/decimal_neg_half.golden.hex new file mode 100644 index 0000000..9d7723e --- /dev/null +++ b/testdata/cbor/primitives/decimal_neg_half.golden.hex @@ -0,0 +1 @@ +c48220c34104 diff --git a/testdata/cbor/primitives/decimal_neg_half.input.json b/testdata/cbor/primitives/decimal_neg_half.input.json new file mode 100644 index 0000000..0007f10 --- /dev/null +++ b/testdata/cbor/primitives/decimal_neg_half.input.json @@ -0,0 +1,4 @@ +{ + "mantissa": "-5", + "exponent": -1 +} diff --git a/testdata/cbor/primitives/decimal_one.golden.hex b/testdata/cbor/primitives/decimal_one.golden.hex new file mode 100644 index 0000000..ea5e34d --- /dev/null +++ b/testdata/cbor/primitives/decimal_one.golden.hex @@ -0,0 +1 @@ +c48200c24101 diff --git a/testdata/cbor/primitives/decimal_one.input.json b/testdata/cbor/primitives/decimal_one.input.json new file mode 100644 index 0000000..8c8f470 --- /dev/null +++ b/testdata/cbor/primitives/decimal_one.input.json @@ -0,0 +1,4 @@ +{ + "mantissa": "1", + "exponent": 0 +} diff --git a/testdata/cbor/primitives/decimal_one_point_five.golden.hex b/testdata/cbor/primitives/decimal_one_point_five.golden.hex new file mode 100644 index 0000000..581dfe8 --- /dev/null +++ b/testdata/cbor/primitives/decimal_one_point_five.golden.hex @@ -0,0 +1 @@ +c48220c2410f diff --git a/testdata/cbor/primitives/decimal_one_point_five.input.json b/testdata/cbor/primitives/decimal_one_point_five.input.json new file mode 100644 index 0000000..bacdf67 --- /dev/null +++ b/testdata/cbor/primitives/decimal_one_point_five.input.json @@ -0,0 +1,4 @@ +{ + "mantissa": "15", + "exponent": -1 +} diff --git a/testdata/cbor/primitives/decimal_ten_pow_40_at_-18.golden.hex b/testdata/cbor/primitives/decimal_ten_pow_40_at_-18.golden.hex new file mode 100644 index 0000000..0806db0 --- /dev/null +++ b/testdata/cbor/primitives/decimal_ten_pow_40_at_-18.golden.hex @@ -0,0 +1 @@ +c48231c2511d6329f1c35ca4bfabb9f5610000000000 diff --git a/testdata/cbor/primitives/decimal_ten_pow_40_at_-18.input.json b/testdata/cbor/primitives/decimal_ten_pow_40_at_-18.input.json new file mode 100644 index 0000000..183192d --- /dev/null +++ b/testdata/cbor/primitives/decimal_ten_pow_40_at_-18.input.json @@ -0,0 +1,4 @@ +{ + "mantissa": "10000000000000000000000000000000000000000", + "exponent": -18 +} diff --git a/testdata/cbor/primitives/decimal_wei_precision.golden.hex b/testdata/cbor/primitives/decimal_wei_precision.golden.hex new file mode 100644 index 0000000..e6d310a --- /dev/null +++ b/testdata/cbor/primitives/decimal_wei_precision.golden.hex @@ -0,0 +1 @@ +c48231c24101 diff --git a/testdata/cbor/primitives/decimal_wei_precision.input.json b/testdata/cbor/primitives/decimal_wei_precision.input.json new file mode 100644 index 0000000..db83971 --- /dev/null +++ b/testdata/cbor/primitives/decimal_wei_precision.input.json @@ -0,0 +1,4 @@ +{ + "mantissa": "1", + "exponent": -18 +} diff --git a/testdata/cbor/primitives/decimal_zero.golden.hex b/testdata/cbor/primitives/decimal_zero.golden.hex new file mode 100644 index 0000000..17f3153 --- /dev/null +++ b/testdata/cbor/primitives/decimal_zero.golden.hex @@ -0,0 +1 @@ +c48200c240 diff --git a/testdata/cbor/primitives/decimal_zero.input.json b/testdata/cbor/primitives/decimal_zero.input.json new file mode 100644 index 0000000..1a85956 --- /dev/null +++ b/testdata/cbor/primitives/decimal_zero.input.json @@ -0,0 +1,4 @@ +{ + "mantissa": "0", + "exponent": 0 +} diff --git a/testdata/cbor/primitives/envelope_v1_bigint_one.golden.hex b/testdata/cbor/primitives/envelope_v1_bigint_one.golden.hex new file mode 100644 index 0000000..79bc375 --- /dev/null +++ b/testdata/cbor/primitives/envelope_v1_bigint_one.golden.hex @@ -0,0 +1 @@ +01c24101 diff --git a/testdata/cbor/primitives/envelope_v1_bigint_one.input.json b/testdata/cbor/primitives/envelope_v1_bigint_one.input.json new file mode 100644 index 0000000..3db9746 --- /dev/null +++ b/testdata/cbor/primitives/envelope_v1_bigint_one.input.json @@ -0,0 +1,4 @@ +{ + "version": "V1 (0x01)", + "body_description": "BigInt 1" +} diff --git a/testdata/cbor/primitives/envelope_v1_bigint_zero.golden.hex b/testdata/cbor/primitives/envelope_v1_bigint_zero.golden.hex new file mode 100644 index 0000000..8357b91 --- /dev/null +++ b/testdata/cbor/primitives/envelope_v1_bigint_zero.golden.hex @@ -0,0 +1 @@ +01c240 diff --git a/testdata/cbor/primitives/envelope_v1_bigint_zero.input.json b/testdata/cbor/primitives/envelope_v1_bigint_zero.input.json new file mode 100644 index 0000000..aef53fa --- /dev/null +++ b/testdata/cbor/primitives/envelope_v1_bigint_zero.input.json @@ -0,0 +1,4 @@ +{ + "version": "V1 (0x01)", + "body_description": "BigInt 0" +} diff --git a/testdata/cbor/primitives/hash32_incrementing.golden.hex b/testdata/cbor/primitives/hash32_incrementing.golden.hex new file mode 100644 index 0000000..2b73176 --- /dev/null +++ b/testdata/cbor/primitives/hash32_incrementing.golden.hex @@ -0,0 +1 @@ +5820000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f diff --git a/testdata/cbor/primitives/hash32_incrementing.input.json b/testdata/cbor/primitives/hash32_incrementing.input.json new file mode 100644 index 0000000..d28a0ec --- /dev/null +++ b/testdata/cbor/primitives/hash32_incrementing.input.json @@ -0,0 +1,3 @@ +{ + "hex": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" +} diff --git a/testdata/cbor/primitives/hash32_max.golden.hex b/testdata/cbor/primitives/hash32_max.golden.hex new file mode 100644 index 0000000..072b8c2 --- /dev/null +++ b/testdata/cbor/primitives/hash32_max.golden.hex @@ -0,0 +1 @@ +5820ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff diff --git a/testdata/cbor/primitives/hash32_max.input.json b/testdata/cbor/primitives/hash32_max.input.json new file mode 100644 index 0000000..bbc7e3b --- /dev/null +++ b/testdata/cbor/primitives/hash32_max.input.json @@ -0,0 +1,3 @@ +{ + "hex": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" +} diff --git a/testdata/cbor/primitives/hash32_one.golden.hex b/testdata/cbor/primitives/hash32_one.golden.hex new file mode 100644 index 0000000..bac586e --- /dev/null +++ b/testdata/cbor/primitives/hash32_one.golden.hex @@ -0,0 +1 @@ +58200101010101010101010101010101010101010101010101010101010101010101 diff --git a/testdata/cbor/primitives/hash32_one.input.json b/testdata/cbor/primitives/hash32_one.input.json new file mode 100644 index 0000000..f5f3dd6 --- /dev/null +++ b/testdata/cbor/primitives/hash32_one.input.json @@ -0,0 +1,3 @@ +{ + "hex": "0101010101010101010101010101010101010101010101010101010101010101" +} diff --git a/testdata/cbor/primitives/hash32_zero.golden.hex b/testdata/cbor/primitives/hash32_zero.golden.hex new file mode 100644 index 0000000..a18e096 --- /dev/null +++ b/testdata/cbor/primitives/hash32_zero.golden.hex @@ -0,0 +1 @@ +58200000000000000000000000000000000000000000000000000000000000000000 diff --git a/testdata/cbor/primitives/hash32_zero.input.json b/testdata/cbor/primitives/hash32_zero.input.json new file mode 100644 index 0000000..06b96d1 --- /dev/null +++ b/testdata/cbor/primitives/hash32_zero.input.json @@ -0,0 +1,3 @@ +{ + "hex": "0000000000000000000000000000000000000000000000000000000000000000" +} diff --git a/testdata/cbor/primitives/maybe_bigint_nil.golden.hex b/testdata/cbor/primitives/maybe_bigint_nil.golden.hex new file mode 100644 index 0000000..59ce900 --- /dev/null +++ b/testdata/cbor/primitives/maybe_bigint_nil.golden.hex @@ -0,0 +1 @@ +f6 diff --git a/testdata/cbor/primitives/maybe_bigint_nil.input.json b/testdata/cbor/primitives/maybe_bigint_nil.input.json new file mode 100644 index 0000000..66ff88d --- /dev/null +++ b/testdata/cbor/primitives/maybe_bigint_nil.input.json @@ -0,0 +1,3 @@ +{ + "present": false +} diff --git a/testdata/cbor/primitives/maybe_bigint_present_negative.golden.hex b/testdata/cbor/primitives/maybe_bigint_present_negative.golden.hex new file mode 100644 index 0000000..0fb5ba6 --- /dev/null +++ b/testdata/cbor/primitives/maybe_bigint_present_negative.golden.hex @@ -0,0 +1 @@ +c34129 diff --git a/testdata/cbor/primitives/maybe_bigint_present_negative.input.json b/testdata/cbor/primitives/maybe_bigint_present_negative.input.json new file mode 100644 index 0000000..29ecea7 --- /dev/null +++ b/testdata/cbor/primitives/maybe_bigint_present_negative.input.json @@ -0,0 +1,4 @@ +{ + "present": true, + "value": "-42" +} diff --git a/testdata/cbor/primitives/maybe_bigint_present_positive.golden.hex b/testdata/cbor/primitives/maybe_bigint_present_positive.golden.hex new file mode 100644 index 0000000..d625be4 --- /dev/null +++ b/testdata/cbor/primitives/maybe_bigint_present_positive.golden.hex @@ -0,0 +1 @@ +c2412a diff --git a/testdata/cbor/primitives/maybe_bigint_present_positive.input.json b/testdata/cbor/primitives/maybe_bigint_present_positive.input.json new file mode 100644 index 0000000..3dbf5a3 --- /dev/null +++ b/testdata/cbor/primitives/maybe_bigint_present_positive.input.json @@ -0,0 +1,4 @@ +{ + "present": true, + "value": "42" +} diff --git a/testdata/cbor/primitives/maybe_bigint_zero.golden.hex b/testdata/cbor/primitives/maybe_bigint_zero.golden.hex new file mode 100644 index 0000000..1b3b81f --- /dev/null +++ b/testdata/cbor/primitives/maybe_bigint_zero.golden.hex @@ -0,0 +1 @@ +c240 diff --git a/testdata/cbor/primitives/maybe_bigint_zero.input.json b/testdata/cbor/primitives/maybe_bigint_zero.input.json new file mode 100644 index 0000000..283f153 --- /dev/null +++ b/testdata/cbor/primitives/maybe_bigint_zero.input.json @@ -0,0 +1,4 @@ +{ + "present": true, + "value": "0" +} diff --git a/testdata/cbor/primitives/time_epoch.golden.hex b/testdata/cbor/primitives/time_epoch.golden.hex new file mode 100644 index 0000000..4daddb7 --- /dev/null +++ b/testdata/cbor/primitives/time_epoch.golden.hex @@ -0,0 +1 @@ +00 diff --git a/testdata/cbor/primitives/time_epoch.input.json b/testdata/cbor/primitives/time_epoch.input.json new file mode 100644 index 0000000..08c0878 --- /dev/null +++ b/testdata/cbor/primitives/time_epoch.input.json @@ -0,0 +1,3 @@ +{ + "unix_nano": 0 +} diff --git a/testdata/cbor/primitives/time_minus_one_ns.golden.hex b/testdata/cbor/primitives/time_minus_one_ns.golden.hex new file mode 100644 index 0000000..209e3ef --- /dev/null +++ b/testdata/cbor/primitives/time_minus_one_ns.golden.hex @@ -0,0 +1 @@ +20 diff --git a/testdata/cbor/primitives/time_minus_one_ns.input.json b/testdata/cbor/primitives/time_minus_one_ns.input.json new file mode 100644 index 0000000..5b88147 --- /dev/null +++ b/testdata/cbor/primitives/time_minus_one_ns.input.json @@ -0,0 +1,3 @@ +{ + "unix_nano": -1 +} diff --git a/testdata/cbor/primitives/time_modern.golden.hex b/testdata/cbor/primitives/time_modern.golden.hex new file mode 100644 index 0000000..2643978 --- /dev/null +++ b/testdata/cbor/primitives/time_modern.golden.hex @@ -0,0 +1 @@ +1b17979cfe3d85cd15 diff --git a/testdata/cbor/primitives/time_modern.input.json b/testdata/cbor/primitives/time_modern.input.json new file mode 100644 index 0000000..9da70be --- /dev/null +++ b/testdata/cbor/primitives/time_modern.input.json @@ -0,0 +1,3 @@ +{ + "unix_nano": 1700000000123456789 +} diff --git a/testdata/cbor/primitives/time_one_ns.golden.hex b/testdata/cbor/primitives/time_one_ns.golden.hex new file mode 100644 index 0000000..8a0f05e --- /dev/null +++ b/testdata/cbor/primitives/time_one_ns.golden.hex @@ -0,0 +1 @@ +01 diff --git a/testdata/cbor/primitives/time_one_ns.input.json b/testdata/cbor/primitives/time_one_ns.input.json new file mode 100644 index 0000000..13d084c --- /dev/null +++ b/testdata/cbor/primitives/time_one_ns.input.json @@ -0,0 +1,3 @@ +{ + "unix_nano": 1 +} diff --git a/testdata/goldens/preimages/block_header/empty_accounts.golden.hex b/testdata/goldens/preimages/block_header/empty_accounts.golden.hex new file mode 100644 index 0000000..8caab1e --- /dev/null +++ b/testdata/goldens/preimages/block_header/empty_accounts.golden.hex @@ -0,0 +1 @@ +86582011111111111111111111111111111111111111111111111111111111111111111a6553f10058202222222222222222222222222222222222222222222222222222222222222222582033333333333333333333333333333333333333333333333333333333333333330780 diff --git a/testdata/goldens/preimages/block_header/empty_accounts.input.json b/testdata/goldens/preimages/block_header/empty_accounts.input.json new file mode 100644 index 0000000..41629b4 --- /dev/null +++ b/testdata/goldens/preimages/block_header/empty_accounts.input.json @@ -0,0 +1,12 @@ +{ + "name": "empty_accounts", + "description": "BlockHeader preimage (ADR-009 §4) with no AccountSnapshots. canonical-CBOR([]). Block.Hash = keccak256(preimage).", + "anchorHex": "1111111111111111111111111111111111111111111111111111111111111111", + "sealedAt": 1700000000, + "stateRootHex": "2222222222222222222222222222222222222222222222222222222222222222", + "entriesDigestHex": "3333333333333333333333333333333333333333333333333333333333333333", + "k": 7, + "accounts": [], + "cborBytes": 110, + "derivedBlockHashHex": "36b833cd85b2bf21bc0695b6c018cf66ba7565b8bd5f82c7783808cb7255a370" +} diff --git a/testdata/goldens/preimages/block_header/single_account.golden.hex b/testdata/goldens/preimages/block_header/single_account.golden.hex new file mode 100644 index 0000000..bbc9372 --- /dev/null +++ b/testdata/goldens/preimages/block_header/single_account.golden.hex @@ -0,0 +1 @@ +86582011111111111111111111111111111111111111111111111111111111111111111a6553f10058202222222222222222222222222222222222222222222222222222222222222222582033333333333333333333333333333333333333333333333333333333333333330781835820444444444444444444444444444444444444444444444444444444444444444458205555555555555555555555555555555555555555555555555555555555555555182a diff --git a/testdata/goldens/preimages/block_header/single_account.input.json b/testdata/goldens/preimages/block_header/single_account.input.json new file mode 100644 index 0000000..5d5010b --- /dev/null +++ b/testdata/goldens/preimages/block_header/single_account.input.json @@ -0,0 +1,18 @@ +{ + "name": "single_account", + "description": "BlockHeader preimage with one AccountSnapshot. Q6 binds per-account chain continuity (PrevBlockRef + PostNonce) under the BLS signature.", + "anchorHex": "1111111111111111111111111111111111111111111111111111111111111111", + "sealedAt": 1700000000, + "stateRootHex": "2222222222222222222222222222222222222222222222222222222222222222", + "entriesDigestHex": "3333333333333333333333333333333333333333333333333333333333333333", + "k": 7, + "accounts": [ + { + "accountIDHex": "4444444444444444444444444444444444444444444444444444444444444444", + "prevBlockRefHex": "5555555555555555555555555555555555555555555555555555555555555555", + "postNonce": 42 + } + ], + "cborBytes": 181, + "derivedBlockHashHex": "7ee9a01c94f4dc1705c80c1af1c34b216952a3a458d41e0ed64b139cd80a6133" +} diff --git a/testdata/goldens/preimages/entry_hash/transfer.golden.hex b/testdata/goldens/preimages/entry_hash/transfer.golden.hex new file mode 100644 index 0000000..e766d6d --- /dev/null +++ b/testdata/goldens/preimages/entry_hash/transfer.golden.hex @@ -0,0 +1 @@ +8401673078416c696365015829837274782d676f6c64656e2d7472616e73666572653078426f6281826455534454c48200c24307a120 diff --git a/testdata/goldens/preimages/entry_hash/transfer.input.json b/testdata/goldens/preimages/entry_hash/transfer.input.json new file mode 100644 index 0000000..01fb3b1 --- /dev/null +++ b/testdata/goldens/preimages/entry_hash/transfer.input.json @@ -0,0 +1,11 @@ +{ + "name": "transfer", + "description": "EntryHash preimage = canonical CBOR of BlockEntry tuple (ADR-009 §4). EntryHash = keccak256(preimage). Payload is the CBOR-encoded TransferOp.", + "entryType": 1, + "entryTypeStr": "OpTransfer", + "account": "0xAlice", + "nonce": 1, + "payloadHex": "837274782d676f6c64656e2d7472616e73666572653078426f6281826455534454c48200c24307a120", + "cborBytes": 54, + "derivedEntryHashHex": "1e96c85e2b26b2c129db6dc72138a74e95113cf4e6d2a241f17e16f41707e4f7" +} diff --git a/testdata/goldens/preimages/finalized_withdrawal/header.golden.hex b/testdata/goldens/preimages/finalized_withdrawal/header.golden.hex new file mode 100644 index 0000000..1308abf --- /dev/null +++ b/testdata/goldens/preimages/finalized_withdrawal/header.golden.hex @@ -0,0 +1 @@ +845820cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc5820dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd031a65540168 diff --git a/testdata/goldens/preimages/finalized_withdrawal/header.input.json b/testdata/goldens/preimages/finalized_withdrawal/header.input.json new file mode 100644 index 0000000..aa0adc1 --- /dev/null +++ b/testdata/goldens/preimages/finalized_withdrawal/header.input.json @@ -0,0 +1,10 @@ +{ + "name": "header", + "description": "FinalizedWithdrawal.SigningMessage() = canonical CBOR of FinalizedWithdrawalHeader{WithdrawalID, BlockHash, EntryIndex, FinalizedAt} (ADR-009 §4). Finality hash = keccak256(preimage).", + "withdrawalIDHex": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "blockHashHex": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "entryIndex": 3, + "finalizedAt": 1700004200, + "cborBytes": 75, + "derivedFinalizedHashHex": "eeac931e7ba4ae5d7d7657da6a6e26acb8c007894be8bf30dd19f5c7e546dcc6" +} diff --git a/testdata/goldens/preimages/op_payload/repeg.golden.hex b/testdata/goldens/preimages/op_payload/repeg.golden.hex new file mode 100644 index 0000000..3acaab9 --- /dev/null +++ b/testdata/goldens/preimages/op_payload/repeg.golden.hex @@ -0,0 +1 @@ +876d706f6f6c3a4554482d55534443c24207d0c24207dac24203e8c24203e9182ac24207d5 diff --git a/testdata/goldens/preimages/op_payload/repeg.input.json b/testdata/goldens/preimages/op_payload/repeg.input.json new file mode 100644 index 0000000..6430c09 --- /dev/null +++ b/testdata/goldens/preimages/op_payload/repeg.input.json @@ -0,0 +1,15 @@ +{ + "name": "repeg", + "description": "RepegOp payload = canonical CBOR of RepegOp tuple.", + "opType": "RepegOp", + "op": { + "epoch": 42, + "newPriceScaleDec": "2010", + "newVirtPriceDec": "1001", + "oldPriceScaleDec": "2000", + "oldVirtPriceDec": "1000", + "poolID": "pool:ETH-USDC", + "priceEMADec": "2005" + }, + "cborBytes": 37 +} diff --git a/testdata/goldens/preimages/op_payload/session_challenge.golden.hex b/testdata/goldens/preimages/op_payload/session_challenge.golden.hex new file mode 100644 index 0000000..d9b52d1 --- /dev/null +++ b/testdata/goldens/preimages/op_payload/session_challenge.golden.hex @@ -0,0 +1 @@ +845820bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb05061a6553ff10 diff --git a/testdata/goldens/preimages/op_payload/session_challenge.input.json b/testdata/goldens/preimages/op_payload/session_challenge.input.json new file mode 100644 index 0000000..3c0746c --- /dev/null +++ b/testdata/goldens/preimages/op_payload/session_challenge.input.json @@ -0,0 +1,12 @@ +{ + "name": "session_challenge", + "description": "SessionChallengeOp payload = canonical CBOR of SessionChallengeOp tuple.", + "opType": "SessionChallengeOp", + "op": { + "newDeadline": 1700003600, + "newVersion": 6, + "previousVersion": 5, + "sessionIDHex": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "cborBytes": 42 +} diff --git a/testdata/goldens/preimages/op_payload/session_close.golden.hex b/testdata/goldens/preimages/op_payload/session_close.golden.hex new file mode 100644 index 0000000..4c0b5b8 --- /dev/null +++ b/testdata/goldens/preimages/op_payload/session_close.golden.hex @@ -0,0 +1 @@ +855820aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa182ac48200c2430f4240c48200c24307a120f5 diff --git a/testdata/goldens/preimages/op_payload/session_close.input.json b/testdata/goldens/preimages/op_payload/session_close.input.json new file mode 100644 index 0000000..c3d5dc3 --- /dev/null +++ b/testdata/goldens/preimages/op_payload/session_close.input.json @@ -0,0 +1,13 @@ +{ + "name": "session_close", + "description": "SessionCloseOp payload = canonical CBOR of SessionCloseOp tuple.", + "opType": "SessionCloseOp", + "op": { + "cooperative": true, + "serviceAmountDec": "500000", + "sessionIDHex": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "userAmountDec": "1000000", + "version": 42 + }, + "cborBytes": 54 +} diff --git a/testdata/goldens/preimages/op_payload/swap.golden.hex b/testdata/goldens/preimages/op_payload/swap.golden.hex new file mode 100644 index 0000000..d8ccc7b --- /dev/null +++ b/testdata/goldens/preimages/op_payload/swap.golden.hex @@ -0,0 +1 @@ +8a6e74782d676f6c64656e2d73776170634554486455534443c48200c24203e8c48200c24207d06d706f6f6c3a4554482d55534443c48200c24103181ec48200c24207d0c48200c24207d1 diff --git a/testdata/goldens/preimages/op_payload/swap.input.json b/testdata/goldens/preimages/op_payload/swap.input.json new file mode 100644 index 0000000..89affd5 --- /dev/null +++ b/testdata/goldens/preimages/op_payload/swap.input.json @@ -0,0 +1,18 @@ +{ + "name": "swap", + "description": "SwapOp payload = canonical CBOR of SwapOp tuple.", + "opType": "SwapOp", + "op": { + "amountInDec": "1000", + "amountOutDec": "2000", + "assetIn": "ETH", + "assetOut": "USDC", + "feeDec": "3", + "feeRate": 30, + "poolID": "pool:ETH-USDC", + "priceEMADec": "2000", + "spotPriceDec": "2001", + "txID": "tx-golden-swap" + }, + "cborBytes": 75 +} diff --git a/testdata/goldens/preimages/op_payload/transfer_single_asset.golden.hex b/testdata/goldens/preimages/op_payload/transfer_single_asset.golden.hex new file mode 100644 index 0000000..152dd18 --- /dev/null +++ b/testdata/goldens/preimages/op_payload/transfer_single_asset.golden.hex @@ -0,0 +1 @@ +837074782d676f6c64656e2d786665722d31653078426f6281826455534454c48200c24203e8 diff --git a/testdata/goldens/preimages/op_payload/transfer_single_asset.input.json b/testdata/goldens/preimages/op_payload/transfer_single_asset.input.json new file mode 100644 index 0000000..0c71e09 --- /dev/null +++ b/testdata/goldens/preimages/op_payload/transfer_single_asset.input.json @@ -0,0 +1,16 @@ +{ + "name": "transfer_single_asset", + "description": "TransferOp payload = canonical CBOR of TransferOp tuple (ADR-009 §4); replaces the deleted hand-rolled op_encoding.go layout.", + "opType": "TransferOp", + "op": { + "assets": [ + { + "amountDec": "1000", + "asset": "USDT" + } + ], + "to": "0xBob", + "txID": "tx-golden-xfer-1" + }, + "cborBytes": 38 +} diff --git a/testdata/goldens/preimages/op_payload/withdrawal.golden.hex b/testdata/goldens/preimages/op_payload/withdrawal.golden.hex new file mode 100644 index 0000000..c854f8c --- /dev/null +++ b/testdata/goldens/preimages/op_payload/withdrawal.golden.hex @@ -0,0 +1 @@ +866455534454782a307841306238303030303030303030303030303030303030303030303030303030303030303030303031c48200c243989680016b3078526563697069656e7443aabbcc diff --git a/testdata/goldens/preimages/op_payload/withdrawal.input.json b/testdata/goldens/preimages/op_payload/withdrawal.input.json new file mode 100644 index 0000000..6815fbb --- /dev/null +++ b/testdata/goldens/preimages/op_payload/withdrawal.input.json @@ -0,0 +1,14 @@ +{ + "name": "withdrawal", + "description": "WithdrawalOp payload = canonical CBOR of WithdrawalOp tuple.", + "opType": "WithdrawalOp", + "op": { + "amountDec": "10000000", + "asset": "USDT", + "chainID": 1, + "l1Asset": "0xA0b8000000000000000000000000000000000001", + "recipient": "0xRecipient", + "userSignatureHex": "aabbcc" + }, + "cborBytes": 75 +} diff --git a/testdata/goldens/solidity-preimages/bls/aggregate_sig.golden.hex b/testdata/goldens/solidity-preimages/bls/aggregate_sig.golden.hex new file mode 100644 index 0000000..b15400e --- /dev/null +++ b/testdata/goldens/solidity-preimages/bls/aggregate_sig.golden.hex @@ -0,0 +1 @@ +10683dfead0cc7947f4678e1bae35c16d4ee51984f17223ed7d7b3789b81c2491572b2614eca1e45f0f511cb46e37347c559c69b2685721d92326531043be37c128c346a31bc241603a100f846d4dd6a6ba972b2e713b9e28d6b395d4ebecc43024742bc9f0f237cb22881025dbc98e6d8f3bc021d949bdd7e03e815c77ef631129d672996109f3fc2e4dbc91a8e7dbd67662602507a02cceecfb916b42a81b91f9a2cdcfe87062305b9193c666ed4b680fb6a3551b64d5eeef7381f0f6554f5 diff --git a/testdata/goldens/solidity-preimages/bls/aggregate_sig.input.json b/testdata/goldens/solidity-preimages/bls/aggregate_sig.input.json new file mode 100644 index 0000000..b4ba998 --- /dev/null +++ b/testdata/goldens/solidity-preimages/bls/aggregate_sig.input.json @@ -0,0 +1,12 @@ +{ + "name": "aggregate_sig", + "description": "Two partial signatures over the literal 32-byte msgHash, aggregated via AggregateG1; aggregate G2 pubkey via AggregateG2. Layout: sigma(G1,64B) || apkG2(128B).", + "msgHashHex": "6d91a2b8e2f9c0d1a3b4c5d6e7f8091a2b3c4d5e6f70819a2b3c4d5e6f708192", + "signerASeed": "clearnet/cbor-w0/bls/agg/A", + "signerBSeed": "clearnet/cbor-w0/bls/agg/B", + "partialSigmaAHex": "29b89d92db15b9219d3b0226599f9d175c0c40d74492c72a731112b1661de36016fc323187ba7672d0c1fda5aeaf69da3b1a1d8af64fbac766f28ee7546821ef", + "partialSigmaBHex": "21995186d53257403eede946a9876d9801a66d35260caa47d9974c94150c7f6817576f381e267de569cc9d325678aeec2157fee999a67b886f6db1f811c529ce", + "aggregateSigmaHex": "10683dfead0cc7947f4678e1bae35c16d4ee51984f17223ed7d7b3789b81c2491572b2614eca1e45f0f511cb46e37347c559c69b2685721d92326531043be37c", + "aggregateApkG2Hex": "128c346a31bc241603a100f846d4dd6a6ba972b2e713b9e28d6b395d4ebecc43024742bc9f0f237cb22881025dbc98e6d8f3bc021d949bdd7e03e815c77ef631129d672996109f3fc2e4dbc91a8e7dbd67662602507a02cceecfb916b42a81b91f9a2cdcfe87062305b9193c666ed4b680fb6a3551b64d5eeef7381f0f6554f5", + "payloadOrder": "aggSigmaG1(64) || aggApkG2(128)" +} diff --git a/testdata/goldens/solidity-preimages/bls/g1_generator.golden.hex b/testdata/goldens/solidity-preimages/bls/g1_generator.golden.hex new file mode 100644 index 0000000..5b3878a --- /dev/null +++ b/testdata/goldens/solidity-preimages/bls/g1_generator.golden.hex @@ -0,0 +1 @@ +00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002 diff --git a/testdata/goldens/solidity-preimages/bls/g1_generator.input.json b/testdata/goldens/solidity-preimages/bls/g1_generator.input.json new file mode 100644 index 0000000..a53ddca --- /dev/null +++ b/testdata/goldens/solidity-preimages/bls/g1_generator.input.json @@ -0,0 +1,4 @@ +{ + "name": "g1_generator", + "kind": "generator" +} diff --git a/testdata/goldens/solidity-preimages/bls/g1_identity.golden.hex b/testdata/goldens/solidity-preimages/bls/g1_identity.golden.hex new file mode 100644 index 0000000..5934177 --- /dev/null +++ b/testdata/goldens/solidity-preimages/bls/g1_identity.golden.hex @@ -0,0 +1 @@ +00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 diff --git a/testdata/goldens/solidity-preimages/bls/g1_identity.input.json b/testdata/goldens/solidity-preimages/bls/g1_identity.input.json new file mode 100644 index 0000000..3149fa2 --- /dev/null +++ b/testdata/goldens/solidity-preimages/bls/g1_identity.input.json @@ -0,0 +1,4 @@ +{ + "name": "g1_identity", + "kind": "identity" +} diff --git a/testdata/goldens/solidity-preimages/bls/g1_random.golden.hex b/testdata/goldens/solidity-preimages/bls/g1_random.golden.hex new file mode 100644 index 0000000..a004ecc --- /dev/null +++ b/testdata/goldens/solidity-preimages/bls/g1_random.golden.hex @@ -0,0 +1 @@ +141d33e28330d85cb28dc1b2524a784cb549eca938fc291544e5b237ece71ec1044d147b1930b93d30ae95b3ea8228d62a899959874c69c9bc3041d6d1f64bc3 diff --git a/testdata/goldens/solidity-preimages/bls/g1_random.input.json b/testdata/goldens/solidity-preimages/bls/g1_random.input.json new file mode 100644 index 0000000..fef3814 --- /dev/null +++ b/testdata/goldens/solidity-preimages/bls/g1_random.input.json @@ -0,0 +1,7 @@ +{ + "name": "g1_random", + "kind": "scalarMult", + "seed": "clearnet/cbor-w0/bls/g1_random", + "derivedX": "9097853632371537951580042013856860608436358523328362333810656637733080342209", + "derivedY": "1945439971974228031712510374078339820465753707339180081495973508374953610179" +} diff --git a/testdata/goldens/solidity-preimages/bls/g2_generator.golden.hex b/testdata/goldens/solidity-preimages/bls/g2_generator.golden.hex new file mode 100644 index 0000000..4007b2b --- /dev/null +++ b/testdata/goldens/solidity-preimages/bls/g2_generator.golden.hex @@ -0,0 +1 @@ +198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c21800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa diff --git a/testdata/goldens/solidity-preimages/bls/g2_generator.input.json b/testdata/goldens/solidity-preimages/bls/g2_generator.input.json new file mode 100644 index 0000000..b988d57 --- /dev/null +++ b/testdata/goldens/solidity-preimages/bls/g2_generator.input.json @@ -0,0 +1,4 @@ +{ + "name": "g2_generator", + "kind": "generator" +} diff --git a/testdata/goldens/solidity-preimages/bls/g2_identity.golden.hex b/testdata/goldens/solidity-preimages/bls/g2_identity.golden.hex new file mode 100644 index 0000000..56deb17 --- /dev/null +++ b/testdata/goldens/solidity-preimages/bls/g2_identity.golden.hex @@ -0,0 +1 @@ +0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 diff --git a/testdata/goldens/solidity-preimages/bls/g2_identity.input.json b/testdata/goldens/solidity-preimages/bls/g2_identity.input.json new file mode 100644 index 0000000..76a6943 --- /dev/null +++ b/testdata/goldens/solidity-preimages/bls/g2_identity.input.json @@ -0,0 +1,4 @@ +{ + "name": "g2_identity", + "kind": "identity" +} diff --git a/testdata/goldens/solidity-preimages/bls/g2_random.golden.hex b/testdata/goldens/solidity-preimages/bls/g2_random.golden.hex new file mode 100644 index 0000000..26c970a --- /dev/null +++ b/testdata/goldens/solidity-preimages/bls/g2_random.golden.hex @@ -0,0 +1 @@ +23b4ac89e3e525090a97aef7a6566eeae989ecaa07073e4e5a3f64ac7899f4fe205dc02d3aedbf056f1cb2eae89c17b38976b8cb1d85897dc1ace27c74bc68e22af80616781192fdacfd6953a7da83122bb0989c2ca69cb93a20eebe1034ac1a1915ae2060ccc572c3c144d12078e36af6746de1010043830cfdb0e9647ca048 diff --git a/testdata/goldens/solidity-preimages/bls/g2_random.input.json b/testdata/goldens/solidity-preimages/bls/g2_random.input.json new file mode 100644 index 0000000..8281800 --- /dev/null +++ b/testdata/goldens/solidity-preimages/bls/g2_random.input.json @@ -0,0 +1,9 @@ +{ + "name": "g2_random", + "kind": "scalarMult", + "seed": "clearnet/cbor-w0/bls/g2_random", + "derivedXIm": "16150172989958929007130831107915661727987029882366757975090598562883639571710", + "derivedXRe": "14639654286391014001697619346149569659778900705582873405756417099414768478434", + "derivedYIm": "19435359728803839646011450715851183366657259083461106360949356773167722703898", + "derivedYRe": "11346126779718858706962926177873718192865472829058392029423680762526595588168" +} From 8c7c1624dc9504e80604e02049fcac910a992fda Mon Sep 17 00:00:00 2001 From: Anton Filonenko Date: Tue, 16 Jun 2026 11:52:57 +0300 Subject: [PATCH 02/15] feat: add blockchain adapter layer, signer seam, and devnet tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds the consumer-facing blockchain layer on top of the protocol primitives, plus a real-node integration harness. - pkg/sign: pluggable, algorithm-aware Signer (secp256k1/ed25519) with an in-memory KeySigner for clients/CLI/tests and EVM digest helpers; KMS backends satisfy the same interface - pkg/core: chain-agnostic adapter interfaces (VaultDepositor, VaultWithdrawalFinalizer with Pack/Validate/Sign/Merge/Submit/ VerifyExecution, Registry/Token/Fraud/Faucet readers+writers) and Slot types - pkg/blockchain/evm: generated contract bindings (regenerated from vendored ABI/bytecode via `go generate`), on-chain BLS pubkey cache, and per-concern adapters — Depositor, WithdrawalFinalizer, Registry, Token, Fraud, Faucet - pkg/blockchain/btc: m-of-n P2WSH multisig vault — depositor + finalizer - pkg/blockchain/xrpl: multi-sign vault — depositor + finalizer - devnet/: docker-compose (anvil + bitcoind + rippled) with a readiness gate; self-provisioning deposit + withdrawal integration tests per chain - Makefile: build / lint / test / generate / devnet / integration Co-Authored-By: Claude Fable 5 --- .gitignore | 6 + Makefile | 32 + devnet/README.md | 56 + devnet/docker-compose.yml | 39 + devnet/rippled.cfg | 49 + devnet/wait/main.go | 76 + go.mod | 41 +- go.sum | 237 +- pkg/blockchain/btc/depositor.go | 230 ++ pkg/blockchain/btc/multisig.go | 234 ++ pkg/blockchain/btc/rpc.go | 65 + pkg/blockchain/btc/txbuild.go | 72 + pkg/blockchain/btc/vault_integration_test.go | 356 +++ pkg/blockchain/btc/withdrawal_finalizer.go | 506 ++++ pkg/blockchain/evm/abi_refresher/main.go | 116 + pkg/blockchain/evm/adapter.go | 148 ++ pkg/blockchain/evm/adjudicator_abi.go | 440 ++++ pkg/blockchain/evm/artifacts/Custody.abi | 289 +++ pkg/blockchain/evm/artifacts/Custody.bin | 1 + pkg/blockchain/evm/artifacts/Faucet.abi | 224 ++ pkg/blockchain/evm/artifacts/Faucet.bin | 1 + pkg/blockchain/evm/artifacts/MockERC20.abi | 258 ++ pkg/blockchain/evm/artifacts/MockERC20.bin | 1 + pkg/blockchain/evm/artifacts/NodeID.abi | 778 ++++++ pkg/blockchain/evm/artifacts/NodeID.bin | 1 + pkg/blockchain/evm/artifacts/README.md | 58 + pkg/blockchain/evm/artifacts/Registry.abi | 925 ++++++++ pkg/blockchain/evm/artifacts/Registry.bin | 1 + pkg/blockchain/evm/artifacts/Slasher.abi | 117 + pkg/blockchain/evm/artifacts/Slasher.bin | 1 + pkg/blockchain/evm/artifacts/YellowToken.abi | 549 +++++ pkg/blockchain/evm/artifacts/YellowToken.bin | 1 + pkg/blockchain/evm/bls_cache.go | 463 ++++ pkg/blockchain/evm/bls_cache_test.go | 324 +++ pkg/blockchain/evm/custody_abi.go | 856 +++++++ pkg/blockchain/evm/depositor.go | 93 + pkg/blockchain/evm/faucet_abi.go | 1041 ++++++++ pkg/blockchain/evm/faucet_adapter.go | 79 + pkg/blockchain/evm/fraud_adapter.go | 66 + pkg/blockchain/evm/generate.go | 5 + pkg/blockchain/evm/mockerc20_abi.go | 781 ++++++ pkg/blockchain/evm/nodeid_abi.go | 1823 ++++++++++++++ pkg/blockchain/evm/registry_abi.go | 2084 +++++++++++++++++ pkg/blockchain/evm/registry_adapter.go | 202 ++ pkg/blockchain/evm/token_adapter.go | 43 + pkg/blockchain/evm/vault_integration_test.go | 200 ++ pkg/blockchain/evm/withdrawal_finalizer.go | 391 ++++ pkg/blockchain/evm/yellowtoken_abi.go | 1077 +++++++++ pkg/blockchain/xrpl/depositor.go | 87 + pkg/blockchain/xrpl/vault_integration_test.go | 309 +++ pkg/blockchain/xrpl/wire.go | 330 +++ pkg/blockchain/xrpl/withdrawal_finalizer.go | 188 ++ pkg/core/block.go | 2 +- pkg/core/blockchain.go | 130 + pkg/core/slot.go | 47 + pkg/sign/ethereum.go | 108 + pkg/sign/key.go | 74 + pkg/sign/sign.go | 40 + 58 files changed, 16742 insertions(+), 9 deletions(-) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 devnet/README.md create mode 100644 devnet/docker-compose.yml create mode 100644 devnet/rippled.cfg create mode 100644 devnet/wait/main.go create mode 100644 pkg/blockchain/btc/depositor.go create mode 100644 pkg/blockchain/btc/multisig.go create mode 100644 pkg/blockchain/btc/rpc.go create mode 100644 pkg/blockchain/btc/txbuild.go create mode 100644 pkg/blockchain/btc/vault_integration_test.go create mode 100644 pkg/blockchain/btc/withdrawal_finalizer.go create mode 100644 pkg/blockchain/evm/abi_refresher/main.go create mode 100644 pkg/blockchain/evm/adapter.go create mode 100644 pkg/blockchain/evm/adjudicator_abi.go create mode 100644 pkg/blockchain/evm/artifacts/Custody.abi create mode 100644 pkg/blockchain/evm/artifacts/Custody.bin create mode 100644 pkg/blockchain/evm/artifacts/Faucet.abi create mode 100644 pkg/blockchain/evm/artifacts/Faucet.bin create mode 100644 pkg/blockchain/evm/artifacts/MockERC20.abi create mode 100644 pkg/blockchain/evm/artifacts/MockERC20.bin create mode 100644 pkg/blockchain/evm/artifacts/NodeID.abi create mode 100644 pkg/blockchain/evm/artifacts/NodeID.bin create mode 100644 pkg/blockchain/evm/artifacts/README.md create mode 100644 pkg/blockchain/evm/artifacts/Registry.abi create mode 100644 pkg/blockchain/evm/artifacts/Registry.bin create mode 100644 pkg/blockchain/evm/artifacts/Slasher.abi create mode 100644 pkg/blockchain/evm/artifacts/Slasher.bin create mode 100644 pkg/blockchain/evm/artifacts/YellowToken.abi create mode 100644 pkg/blockchain/evm/artifacts/YellowToken.bin create mode 100644 pkg/blockchain/evm/bls_cache.go create mode 100644 pkg/blockchain/evm/bls_cache_test.go create mode 100644 pkg/blockchain/evm/custody_abi.go create mode 100644 pkg/blockchain/evm/depositor.go create mode 100644 pkg/blockchain/evm/faucet_abi.go create mode 100644 pkg/blockchain/evm/faucet_adapter.go create mode 100644 pkg/blockchain/evm/fraud_adapter.go create mode 100644 pkg/blockchain/evm/generate.go create mode 100644 pkg/blockchain/evm/mockerc20_abi.go create mode 100644 pkg/blockchain/evm/nodeid_abi.go create mode 100644 pkg/blockchain/evm/registry_abi.go create mode 100644 pkg/blockchain/evm/registry_adapter.go create mode 100644 pkg/blockchain/evm/token_adapter.go create mode 100644 pkg/blockchain/evm/vault_integration_test.go create mode 100644 pkg/blockchain/evm/withdrawal_finalizer.go create mode 100644 pkg/blockchain/evm/yellowtoken_abi.go create mode 100644 pkg/blockchain/xrpl/depositor.go create mode 100644 pkg/blockchain/xrpl/vault_integration_test.go create mode 100644 pkg/blockchain/xrpl/wire.go create mode 100644 pkg/blockchain/xrpl/withdrawal_finalizer.go create mode 100644 pkg/core/blockchain.go create mode 100644 pkg/core/slot.go create mode 100644 pkg/sign/ethereum.go create mode 100644 pkg/sign/key.go create mode 100644 pkg/sign/sign.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0665444 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# Stray `go build` output for the refresher command +/abi_refresher + +# Go workspace files +go.work +go.work.sum diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9a2a228 --- /dev/null +++ b/Makefile @@ -0,0 +1,32 @@ +.PHONY: build lint test generate devnet devnet-down integration + +build: + go build ./... + +lint: + go vet ./... + +test: + go test -race ./... + +# Regenerate all generated code: +# - pkg/blockchain/evm/*_abi.go from the vendored ABI/bytecode in +# pkg/blockchain/evm/artifacts (see that dir's README to refresh them) +# - pkg/core/cbor_gen.go from pkg/core/gen +# Requires go-ethereum's abigen logic (vendored via the module — no external +# binary needed); commit the result. +generate: + go generate ./... + +# Bring the local devnet up and block until every node answers RPC. +devnet: + docker compose -f devnet/docker-compose.yml up -d + go run ./devnet/wait + +devnet-down: + docker compose -f devnet/docker-compose.yml down -v + +# Build-tagged blockchain flow tests (deposit + withdrawal per chain). Every +# test self-provisions against the devnet — no setup, no env. See devnet/README.md. +integration: + go test -tags integration ./pkg/blockchain/... -v diff --git a/devnet/README.md b/devnet/README.md new file mode 100644 index 0000000..a43bd0f --- /dev/null +++ b/devnet/README.md @@ -0,0 +1,56 @@ +# Devnet & blockchain integration tests + +Full **deposit** and **withdrawal** flows per chain, exercised end-to-end +against real nodes. The tests are build-tagged `integration` and live next to +each adapter (`pkg/blockchain//vault_integration_test.go`). Each +withdrawal test runs the whole *k-of-n* quorum in-process — it holds N local +`sign.KeySigner`s and drives `Pack → Validate → Sign → Merge → Submit → +VerifyExecution` itself, so no p2p mesh is needed. + +## Run + +```sh +make devnet # anvil + bitcoind(regtest) + rippled(standalone); blocks until all answer RPC +make integration # go test -tags integration ./pkg/blockchain/... +make devnet-down +``` + +`make devnet` returns only once every node answers (the `devnet/wait` probe). +`make integration` then needs **no setup and no env** — every test +self-provisions against the devnet and is **idempotent**: each run uses fresh +keys / accounts / a freshly-deployed contract, so re-running is a clean run. +Only each node's funder persists (anvil account 0, the bitcoind coinbase +wallet, the XRPL genesis master). + +## What each test provisions + +- **EVM** — deploys a fresh `Custody` vault over N freshly-generated signer + keys (funded from anvil account 0), deposits native ETH, then runs the quorum + withdrawal. +- **BTC** — creates a legacy wallet, mines to maturity, generates a fresh vault + + depositor, watch-imports their addresses, funds the depositor, deposits to + the per-account P2WSH address, then runs the quorum withdrawal (mining to + confirm between steps). +- **XRPL** — funds a fresh vault + depositor from the genesis master, + `SignerListSet`s the vault over fresh signer keys, `TicketCreate`s a ticket, + then deposits and runs the quorum withdrawal. Standalone rippled does not + auto-close ledgers, so the test calls `ledger_accept` after each submit. + +## Optional overrides + +Defaults target the devnet; override the endpoints if pointing elsewhere: + +| env | default | +|---|---| +| `EVM_RPC_URL` / `EVM_DEPLOYER_KEY` | `http://127.0.0.1:8545` / anvil account 0 | +| `BTC_RPC_URL` / `BTC_RPC_USER` / `BTC_RPC_PASS` | `http://127.0.0.1:18443` / `sdk` / `sdk` | +| `XRPL_RPC_URL` | `http://127.0.0.1:5005` | + +## Notes + +- The tests double as the **executable spec** for the adapter interfaces: the + withdrawal flow shows exactly how a custody node calls the SDK + (`Pack`/`Validate`/`Sign`/`Merge`/`Submit`/`VerifyExecution`); the only piece + left to the caller is collecting the quorum's signatures over its mesh. +- The rippled image is `linux/amd64` (pinned via `platform:`); on Apple + silicon it runs under emulation — works, just slower to start. diff --git a/devnet/docker-compose.yml b/devnet/docker-compose.yml new file mode 100644 index 0000000..9a4191f --- /dev/null +++ b/devnet/docker-compose.yml @@ -0,0 +1,39 @@ +# Local devnet for the blockchain integration tests: one node per supported +# chain. Bring up with `make devnet` (or `docker compose -f devnet/docker-compose.yml up -d`). +# +# anvil — EVM JSON-RPC at :8545 (the EVM test self-deploys Custody here) +# bitcoind — regtest JSON-RPC at :18443 (user/pass: sdk/sdk) +# rippled — standalone JSON-RPC at :5005 +# +# No per-chain setup needed: every integration test self-provisions against +# these nodes (fresh keys/accounts/contract each run). See devnet/README.md. +services: + anvil: + image: ghcr.io/foundry-rs/foundry:latest + entrypoint: ["anvil", "--host", "0.0.0.0", "--chain-id", "31337"] + ports: + - "8545:8545" + + bitcoind: + image: ruimarinho/bitcoin-core:24 + command: + - -regtest=1 + - -server=1 + - -rpcbind=0.0.0.0 + - -rpcallowip=0.0.0.0/0 + - -rpcuser=sdk + - -rpcpassword=sdk + - -fallbackfee=0.0002 + - -txindex=1 + ports: + - "18443:18443" + + rippled: + image: xrpllabsofficial/xrpld:latest + platform: linux/amd64 + command: ["-a", "--start"] + volumes: + - ./rippled.cfg:/opt/ripple/etc/rippled.cfg:ro + ports: + - "5005:5005" + - "6006:6006" diff --git a/devnet/rippled.cfg b/devnet/rippled.cfg new file mode 100644 index 0000000..82bd09f --- /dev/null +++ b/devnet/rippled.cfg @@ -0,0 +1,49 @@ +[server] +port_rpc +port_ws +port_peer + +[port_rpc] +port = 5005 +ip = 0.0.0.0 +admin = 0.0.0.0 +protocol = http + +[port_ws] +port = 6006 +ip = 0.0.0.0 +admin = 0.0.0.0 +protocol = ws + +[port_peer] +port = 51235 +ip = 0.0.0.0 +protocol = peer + +[node_size] +tiny + +[node_db] +type=NuDB +path=/var/lib/rippled/db/nudb + +[database_path] +/var/lib/rippled/db + +[debug_logfile] +/var/log/rippled/debug.log + +[sntp_servers] +time.windows.com +time.apple.com +time.nist.gov +pool.ntp.org + +[validators_file] +validators.txt + +[rpc_startup] +{ "command": "log_level", "severity": "warning" } + +[ssl_verify] +0 diff --git a/devnet/wait/main.go b/devnet/wait/main.go new file mode 100644 index 0000000..8adabac --- /dev/null +++ b/devnet/wait/main.go @@ -0,0 +1,76 @@ +// Command wait blocks until every devnet node answers RPC, then exits 0 — so +// `make devnet` returns only once the infra is ready to drive. Each endpoint is +// polled until it responds or the deadline elapses. +package main + +import ( + "bytes" + "context" + "fmt" + "net/http" + "os" + "time" +) + +type probe struct { + name string + url string + user string + pass string + body string +} + +func main() { + probes := []probe{ + {name: "anvil", url: envOr("EVM_RPC_URL", "http://127.0.0.1:8545"), + body: `{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}`}, + {name: "bitcoind", url: envOr("BTC_RPC_URL", "http://127.0.0.1:18443"), + user: envOr("BTC_RPC_USER", "sdk"), pass: envOr("BTC_RPC_PASS", "sdk"), + body: `{"jsonrpc":"1.0","id":1,"method":"getblockchaininfo","params":[]}`}, + {name: "rippled", url: envOr("XRPL_RPC_URL", "http://127.0.0.1:5005"), + body: `{"method":"server_info","params":[{}]}`}, + } + + deadline := time.Now().Add(90 * time.Second) + client := &http.Client{Timeout: 3 * time.Second} + for _, p := range probes { + if err := waitOne(client, p, deadline); err != nil { + fmt.Fprintf(os.Stderr, "devnet: %s not ready: %v\n", p.name, err) + os.Exit(1) + } + fmt.Printf("devnet: %s ready\n", p.name) + } +} + +func waitOne(client *http.Client, p probe, deadline time.Time) error { + var last error + for time.Now().Before(deadline) { + req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, p.url, bytes.NewReader([]byte(p.body))) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + if p.user != "" { + req.SetBasicAuth(p.user, p.pass) + } + resp, err := client.Do(req) + if err == nil { + resp.Body.Close() + if resp.StatusCode == http.StatusOK { + return nil + } + last = fmt.Errorf("status %d", resp.StatusCode) + } else { + last = err + } + time.Sleep(time.Second) + } + return fmt.Errorf("timed out: %v", last) +} + +func envOr(key, def string) string { + if v := os.Getenv(key); v != "" { + return v + } + return def +} diff --git a/go.mod b/go.mod index 3e4720c..e6be6fa 100644 --- a/go.mod +++ b/go.mod @@ -3,30 +3,67 @@ module github.com/layer-3/clearnet-sdk go 1.26 require ( + github.com/Peersyst/xrpl-go v0.1.18 + github.com/btcsuite/btcd v0.25.0 + github.com/btcsuite/btcd/btcutil v1.2.0 + github.com/btcsuite/btcd/chaincfg/chainhash v1.2.0 github.com/consensys/gnark-crypto v0.20.1 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 github.com/ethereum/go-ethereum v1.17.3 github.com/fxamacker/cbor/v2 v2.9.1 github.com/ipfs/go-cid v0.0.6 github.com/whyrusleeping/cbor-gen v0.3.1 - golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 ) require ( + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect + github.com/StackExchange/wmi v1.2.1 // indirect github.com/bits-and-blooms/bitset v1.24.4 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect + github.com/bsv-blockchain/go-sdk v1.2.9 // indirect + github.com/btcsuite/btcd/btcec/v2 v2.3.5 // indirect + github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect + github.com/deckarep/golang-set/v2 v2.6.0 // indirect + github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect + github.com/decred/dcrd/crypto/ripemd160 v1.0.2 // indirect + github.com/ethereum/c-kzg-4844/v2 v2.1.6 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.0 // indirect github.com/holiman/uint256 v1.3.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/kcalvinalvin/anet v0.0.0-20251112173137-d8ddc1f6dbee // indirect github.com/klauspost/cpuid/v2 v2.0.9 // indirect github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 // indirect github.com/minio/sha256-simd v1.0.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mr-tron/base58 v1.1.3 // indirect github.com/multiformats/go-base32 v0.0.3 // indirect github.com/multiformats/go-base36 v0.1.0 // indirect github.com/multiformats/go-multibase v0.0.3 // indirect github.com/multiformats/go-multihash v0.0.13 // indirect github.com/multiformats/go-varint v0.0.5 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/supranational/blst v0.3.16 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect github.com/x448/float16 v0.8.4 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect golang.org/x/crypto v0.48.0 // indirect + golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.41.0 // indirect ) diff --git a/go.sum b/go.sum index 6911993..f00d0b7 100644 --- a/go.sum +++ b/go.sum @@ -1,35 +1,179 @@ +github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= +github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Peersyst/xrpl-go v0.1.18 h1:PUIEGvnkO9oQ4yZRr6EHjglay1xmrjFhujQNVkXynqs= +github.com/Peersyst/xrpl-go v0.1.18/go.mod h1:38j60Mr65poIHdhmjvNXnwbcUFNo8J7CBDot7ZWgrb8= github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU= github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= +github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= +github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= +github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= +github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE= github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bsv-blockchain/go-sdk v1.2.9 h1:LwFzuts+J5X7A+ECx0LNowtUgIahCkNNlXckdiEMSDk= +github.com/bsv-blockchain/go-sdk v1.2.9/go.mod h1:KiHWa/hblo3Bzr+IsX11v0sn1E6elGbNX0VXl5mOq6E= +github.com/btcsuite/btcd v0.25.0 h1:JPbjwvHGpSywBRuorFFqTjaVP4y6Qw69XJ1nQ6MyWJM= +github.com/btcsuite/btcd v0.25.0/go.mod h1:qbPE+pEiR9643E1s1xu57awsRhlCIm1ZIi6FfeRA4KE= +github.com/btcsuite/btcd/btcec/v2 v2.3.5 h1:dpAlnAwmT1yIBm3exhT1/8iUSD98RDJM5vqJVQDQLiU= +github.com/btcsuite/btcd/btcec/v2 v2.3.5/go.mod h1:m22FrOAiuxl/tht9wIqAoGHcbnCCaPWyauO8y2LGGtQ= +github.com/btcsuite/btcd/btcutil v1.2.0 h1:p3+S2g3Q+7G5NOh4Ji+2UrBOrg5Z0Q4ykzShWG1Dhgs= +github.com/btcsuite/btcd/btcutil v1.2.0/go.mod h1:/Taflm113pYjUpbWKKQEfa6XOtI/+WS8awxeMZpY75k= +github.com/btcsuite/btcd/chaincfg/chainhash v1.2.0 h1:yMIg99+4aBvqfl/HzJRKfxTX9rGfikoI9uvFzterhc8= +github.com/btcsuite/btcd/chaincfg/chainhash v1.2.0/go.mod h1:Y72Ren9gfhlEvnwnT78BGcSNO2UMphTKLn9AorF+5rg= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= +github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= +github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/pebble v1.1.5 h1:5AAWCBWbat0uE0blr8qzufZP5tBjkRyy/jWe1QWLnvw= +github.com/cockroachdb/pebble v1.1.5/go.mod h1:17wO9el1YEigxkP/YtV8NtCivQDgoCyBg5c4VR/eOWo= +github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= +github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= github.com/consensys/gnark-crypto v0.20.1 h1:PXDUBvk8AzhvWowHLWBEAfUQcV1/aZgWIqD6eMpXmDg= github.com/consensys/gnark-crypto v0.20.1/go.mod h1:RBWrSgy+IDbGR69RRV313th3M/aZU1ubk2om+qHuTSc= +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/crate-crypto/go-eth-kzg v1.5.0 h1:FYRiJMJG2iv+2Dy3fi14SVGjcPteZ5HAAUe4YWlJygc= +github.com/crate-crypto/go-eth-kzg v1.5.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= -github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= +github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= +github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/crypto/ripemd160 v1.0.2 h1:TvGTmUBHDU75OHro9ojPLK+Yv7gDl2hnUvRocRCjsys= +github.com/decred/dcrd/crypto/ripemd160 v1.0.2/go.mod h1:uGfjDyePSpa75cSQLzNdVmWlbQMBuiJkvXw/MNKRY4M= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/deepmap/oapi-codegen v1.6.0 h1:w/d1ntwh91XI0b/8ja7+u5SvA4IFfM0UNNLmiDR1gg0= +github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M= +github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= +github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= +github.com/ethereum/c-kzg-4844/v2 v2.1.6 h1:xQymkKCT5E2Jiaoqf3v4wsNgjZLY0lRSkZn27fRjSls= +github.com/ethereum/c-kzg-4844/v2 v2.1.6/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw= +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= github.com/ethereum/go-ethereum v1.17.3 h1:Ev/sQHH+UdKZHWjuVzhu2pxhi/sXaPZl23Q+Q5LDd4Q= github.com/ethereum/go-ethereum v1.17.3/go.mod h1:f2EhRwqewIZkGoQekywI2Y2RZAMTSavLNkD9qItFy1A= +github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= +github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ= github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= +github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= +github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grafana/pyroscope-go v1.2.7 h1:VWBBlqxjyR0Cwk2W6UrE8CdcdD80GOFNutj0Kb1T8ac= +github.com/grafana/pyroscope-go v1.2.7/go.mod h1:o/bpSLiJYYP6HQtvcoVKiE9s5RiNgjYTj1DhiddP2Pc= +github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og= +github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= +github.com/graph-gophers/graphql-go v1.3.0 h1:Eb9x/q6MFpCLz7jBCiP/WTxjSDrYLR1QY41SORZyNJ0= +github.com/graph-gophers/graphql-go v1.3.0/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= +github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= +github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db h1:IZUYC/xb3giYwBLMnr8d0TGTzPKFGNTCGgGLoyeX330= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db/go.mod h1:xTEYN9KCHxuYHs+NmrmzFcnvHMzLLNiGFafCb1n3Mfg= +github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= +github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/influxdata/influxdb-client-go/v2 v2.4.0 h1:HGBfZYStlx3Kqvsv1h2pJixbCl/jhnFtxpKFAv9Tu5k= +github.com/influxdata/influxdb-client-go/v2 v2.4.0/go.mod h1:vLNHdxTJkIf2mSLvGrpj8TCcISApPoXkaxP8g9uRlW8= +github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c h1:qSHzRbhzK8RdXOsAdfDgO49TtqC1oZ+acxPrkfTxcCs= +github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU= +github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= github.com/ipfs/go-cid v0.0.6 h1:go0y+GcDOGeJIV01FeBsta4FHngoA4Wz7KMeLkXAhMs= github.com/ipfs/go-cid v0.0.6/go.mod h1:6Ux9z5e+HpkQdckYoX1PG/6xqKspzlEIR5SDmgqgC/I= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kcalvinalvin/anet v0.0.0-20251112173137-d8ddc1f6dbee h1:FPP9HDkBbPyniu+u7FHZg+kKFX1WW0gxOGteJ0h3AJk= +github.com/kcalvinalvin/anet v0.0.0-20251112173137-d8ddc1f6dbee/go.mod h1:N6sz6HwJAenJ6d+/xmSl0ikfV05ZrVGmjt1ryy/WOtE= +github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= +github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 h1:lYpkrQH5ajf0OXOcUbGjvZxxijuBwbbmlSxLiuofa+g= github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ= github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= +github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mr-tron/base58 v1.1.0/go.mod h1:xcD2VGqlgYjBdcBLw+TuYLr8afG+Hj8g2eTVqeSzSU8= github.com/mr-tron/base58 v1.1.3 h1:v+sk57XuaCKGXpWtVBX8YJzO7hMGx4Aajh4TQbdEFdc= github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= @@ -43,27 +187,108 @@ github.com/multiformats/go-multihash v0.0.13 h1:06x+mk/zj1FoMsgNejLpy6QTvJqlSt/B github.com/multiformats/go-multihash v0.0.13/go.mod h1:VdAWLKTwram9oKAatUcLxBNUjdtcVwxObEQBtRfuyjc= github.com/multiformats/go-varint v0.0.5 h1:XVZwSo04Cs3j/jS0uAEPpT3JY6DzMcVLLoWOSnCxOjg= github.com/multiformats/go-varint v0.0.5/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= +github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQmzR3rNLYGGz4g/UgFcjb28p/viDM= +github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= +github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= +github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= +github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= +github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= +github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c= +github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= +github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM= +github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.15.0 h1:5fCgGYogn0hFdhyhLbw7hEsWxufKtY9klyvdNfFlFhM= +github.com/prometheus/client_golang v1.15.0/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= +github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE= +github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= +github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= github.com/whyrusleeping/cbor-gen v0.3.1 h1:82ioxmhEYut7LBVGhGq8xoRkXPLElVuh5mV67AFfdv0= github.com/whyrusleeping/cbor-gen v0.3.1/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= +go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= +golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/blockchain/btc/depositor.go b/pkg/blockchain/btc/depositor.go new file mode 100644 index 0000000..a1d5eb4 --- /dev/null +++ b/pkg/blockchain/btc/depositor.go @@ -0,0 +1,230 @@ +package btc + +import ( + "context" + "encoding/hex" + "fmt" + "sort" + "strings" + + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + + "github.com/layer-3/clearnet-sdk/pkg/core" + "github.com/layer-3/clearnet-sdk/pkg/decimal" + "github.com/layer-3/clearnet-sdk/pkg/sign" +) + +// Depositor funds a per-account deposit address from the depositor's own +// P2WPKH wallet (the key the supplied sign.Signer holds). It implements +// core.VaultDepositor. The deposit address is derived from the vault's pubkeys +// + threshold (the same address the withdrawal finalizer can later spend). +type Depositor struct { + net *chaincfg.Params + rpc RPC + signer sign.Signer + signerPub []byte + depositAddr btcutil.Address // depositor's own P2WPKH address (funding source) + vaultPubkeys [][]byte + threshold int + cfg Config +} + +var _ core.VaultDepositor = (*Depositor)(nil) + +// NewDepositor builds the BTC depositor. signer is the depositor's secp256k1 +// key; vaultPubkeys + threshold define the vault whose per-account deposit +// addresses funds are sent to. +func NewDepositor(net *chaincfg.Params, rpc RPC, signer sign.Signer, vaultPubkeys [][]byte, threshold int, cfg Config) (*Depositor, error) { + if signer.Algorithm() != sign.AlgSecp256k1 { + return nil, fmt.Errorf("btc: depositor signer must be secp256k1, got %s", signer.Algorithm()) + } + pub := signer.PublicKey() + addr, err := btcutil.NewAddressWitnessPubKeyHash(btcutil.Hash160(pub), net) + if err != nil { + return nil, fmt.Errorf("btc: derive depositor address: %w", err) + } + return &Depositor{ + net: net, + rpc: rpc, + signer: signer, + signerPub: pub, + depositAddr: addr, + vaultPubkeys: vaultPubkeys, + threshold: threshold, + cfg: cfg, + }, nil +} + +// DepositorAddress returns the depositor's own P2WPKH funding address. +func (d *Depositor) DepositorAddress() string { return d.depositAddr.EncodeAddress() } + +// Deposit sends `amount` satoshis from the depositor's wallet to the per-account +// deposit address for `account`. asset must be native BTC ("" or "BTC"). Builds, +// signs (P2WPKH), and broadcasts the funding tx. +func (d *Depositor) Deposit(ctx context.Context, asset string, amount decimal.Decimal, account string) (core.TxRef, error) { + if a := strings.ToUpper(strings.TrimSpace(asset)); a != "" && a != "BTC" { + return core.TxRef{}, fmt.Errorf("btc: only native BTC deposits supported, got asset %q", asset) + } + amt := amount.BigInt() + if !amt.IsInt64() || amt.Int64() <= 0 { + return core.TxRef{}, fmt.Errorf("btc: amount %s not a positive int64 satoshi value", amount.String()) + } + sats := amt.Int64() + + depositAddr, _, err := DepositAddress(account, d.threshold, d.vaultPubkeys, d.net) + if err != nil { + return core.TxRef{}, fmt.Errorf("btc: derive deposit address: %w", err) + } + + myAddr := d.depositAddr.EncodeAddress() + unspent, err := d.rpc.ListUnspent(ctx, int(d.cfg.ConfirmationDepth), []string{myAddr}) + if err != nil { + return core.TxRef{}, fmt.Errorf("btc: list depositor utxos: %w", err) + } + utxos, scripts, err := depositorUTXOs(unspent, myAddr, d.net) + if err != nil { + return core.TxRef{}, err + } + feeRate, err := d.rpc.EstimateSmartFeeSatPerVByte(ctx, d.cfg.FeeConfTarget, d.cfg.FallbackFeeRate) + if err != nil { + return core.TxRef{}, fmt.Errorf("btc: estimate fee: %w", err) + } + // numFixedOutputs = recipient (deposit address); change is sized in. + selected, feeSats, err := SelectUTXOs(utxos, sats, feeRate, 1) + if err != nil { + return core.TxRef{}, err + } + + tx, err := buildDepositTx(selected, depositAddr, sats, d.depositAddr, feeSats) + if err != nil { + return core.TxRef{}, err + } + if err := d.signP2WPKH(ctx, tx, selected, scripts); err != nil { + return core.TxRef{}, err + } + + raw, err := serializeTx(tx) + if err != nil { + return core.TxRef{}, err + } + hash := [32]byte(tx.TxHash()) + txid := hashToTxid(hash) + if _, err := d.rpc.SendRawTransaction(ctx, hex.EncodeToString(raw)); err != nil { + if isAlreadyKnown(err) { + return core.TxRef{Hash: hash, Raw: txid}, nil + } + return core.TxRef{}, fmt.Errorf("btc: sendrawtransaction: %w", err) + } + return core.TxRef{Hash: hash, Raw: txid}, nil +} + +// depositorUTXOs filters unspent outputs to the depositor's own address and +// returns the UTXO set plus a per-outpoint amount/script index for signing. +func depositorUTXOs(unspent []Unspent, myAddr string, net *chaincfg.Params) ([]UTXO, map[wire.OutPoint]int64, error) { + addr, err := btcutil.DecodeAddress(myAddr, net) + if err != nil { + return nil, nil, fmt.Errorf("btc: decode depositor addr: %w", err) + } + myScript, err := txscript.PayToAddrScript(addr) + if err != nil { + return nil, nil, fmt.Errorf("btc: depositor pkScript: %w", err) + } + myScriptHex := strings.ToLower(hex.EncodeToString(myScript)) + + utxos := make([]UTXO, 0, len(unspent)) + amounts := make(map[wire.OutPoint]int64) + for _, u := range unspent { + if strings.ToLower(u.ScriptPubKey) != myScriptHex { + continue + } + h, err := chainhash.NewHashFromStr(u.TxID) + if err != nil { + return nil, nil, fmt.Errorf("btc: bad txid %q: %w", u.TxID, err) + } + utxos = append(utxos, UTXO{TxID: *h, Vout: u.Vout, Amount: u.AmountSats}) + amounts[wire.OutPoint{Hash: *h, Index: u.Vout}] = u.AmountSats + } + return utxos, amounts, nil +} + +// buildDepositTx builds the unsigned funding tx: output 0 pays the deposit +// address `sats`, with change back to the depositor above dust. +func buildDepositTx(utxos []UTXO, depositAddr btcutil.Address, sats int64, change btcutil.Address, feeSats int64) (*wire.MsgTx, error) { + if len(utxos) == 0 { + return nil, fmt.Errorf("btc: no depositor UTXOs selected") + } + ordered := make([]UTXO, len(utxos)) + copy(ordered, utxos) + sort.Slice(ordered, func(i, j int) bool { + if c := compareHash(ordered[i].TxID[:], ordered[j].TxID[:]); c != 0 { + return c < 0 + } + return ordered[i].Vout < ordered[j].Vout + }) + + var inTotal int64 + tx := wire.NewMsgTx(wire.TxVersion) + for _, u := range ordered { + op := wire.NewOutPoint(&u.TxID, u.Vout) + tx.AddTxIn(wire.NewTxIn(op, nil, nil)) + inTotal += u.Amount + } + depScript, err := txscript.PayToAddrScript(depositAddr) + if err != nil { + return nil, fmt.Errorf("btc: deposit script: %w", err) + } + tx.AddTxOut(wire.NewTxOut(sats, depScript)) + + rem := inTotal - sats - feeSats + if rem < 0 { + return nil, fmt.Errorf("btc: depositor inputs %d below amount %d + fee %d", inTotal, sats, feeSats) + } + if rem >= dustThresholdSats { + changeScript, err := txscript.PayToAddrScript(change) + if err != nil { + return nil, fmt.Errorf("btc: change script: %w", err) + } + tx.AddTxOut(wire.NewTxOut(rem, changeScript)) + } + return tx, nil +} + +// signP2WPKH signs every input as a P2WPKH spend with the depositor key and +// installs the [sig, pubkey] witness. +func (d *Depositor) signP2WPKH(ctx context.Context, tx *wire.MsgTx, utxos []UTXO, amounts map[wire.OutPoint]int64) error { + pkh := btcutil.Hash160(d.signerPub) + // BIP-143 scriptCode for P2WPKH is the corresponding P2PKH script. + scriptCode, err := txscript.NewScriptBuilder(). + AddOp(txscript.OP_DUP).AddOp(txscript.OP_HASH160). + AddData(pkh). + AddOp(txscript.OP_EQUALVERIFY).AddOp(txscript.OP_CHECKSIG).Script() + if err != nil { + return fmt.Errorf("btc: build scriptCode: %w", err) + } + myScript, err := txscript.PayToAddrScript(d.depositAddr) + if err != nil { + return fmt.Errorf("btc: depositor pkScript: %w", err) + } + fetcher := txscript.NewMultiPrevOutFetcher(nil) + for op, amt := range amounts { + fetcher.AddPrevOut(op, wire.NewTxOut(amt, myScript)) + } + sigHashes := txscript.NewTxSigHashes(tx, fetcher) + for idx, in := range tx.TxIn { + amt := amounts[in.PreviousOutPoint] + sighash, err := txscript.CalcWitnessSigHash(scriptCode, sigHashes, txscript.SigHashAll, tx, idx, amt) + if err != nil { + return fmt.Errorf("btc: sighash input %d: %w", idx, err) + } + der, err := d.signer.Sign(ctx, sighash) + if err != nil { + return fmt.Errorf("btc: sign input %d: %w", idx, err) + } + tx.TxIn[idx].Witness = wire.TxWitness{append(der, byte(txscript.SigHashAll)), d.signerPub} + } + return nil +} diff --git a/pkg/blockchain/btc/multisig.go b/pkg/blockchain/btc/multisig.go new file mode 100644 index 0000000..8618192 --- /dev/null +++ b/pkg/blockchain/btc/multisig.go @@ -0,0 +1,234 @@ +// Package btc implements the Bitcoin custody vault adapter. The vault is a +// native SegWit P2WSH (BIP 141) address whose redeem script is an m-of-n +// OP_CHECKMULTISIG over the providers' secp256k1 public keys — no smart +// contract. See custody docs/btc_spec.md. +// +// This file holds the chain primitives that are a pure function of their +// inputs: redeem-script construction, vault address derivation, the +// deterministic unsigned-transaction builder, BIP-143 sighash computation, and +// witness assembly. They carry no daemon state and no network access, so every +// provider that observes the same authorized withdrawal and the same UTXO set +// builds a byte-identical transaction — the precondition for non-interactive +// multisig signing. +package btc + +import ( + "bytes" + "crypto/sha256" + "fmt" + "sort" + + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" +) + +// dustThresholdSats is the minimum value (in satoshis) a P2WSH change output +// may carry; below this the output is omitted and the remainder goes to fee. +const dustThresholdSats = 330 + +// RedeemScript builds the m-of-n OP_CHECKMULTISIG redeem script over the given +// compressed secp256k1 public keys. Keys are sorted lexicographically (BIP 67) +// before assembly so the script — and therefore the vault address — is +// independent of the order in which keys were supplied. +func RedeemScript(threshold int, pubkeys [][]byte) ([]byte, error) { + sorted, err := sortedPubkeys(threshold, pubkeys) + if err != nil { + return nil, err + } + b := txscript.NewScriptBuilder() + b.AddInt64(int64(threshold)) // OP_m + for _, pk := range sorted { + b.AddData(pk) + } + b.AddInt64(int64(len(sorted))) // OP_n + b.AddOp(txscript.OP_CHECKMULTISIG) + return b.Script() +} + +// TaggedRedeemScript prefixes the m-of-n redeem script with ` OP_DROP`, +// yielding a distinct witness script — and therefore a distinct P2WSH address — +// per tag while leaving the signing semantics identical: OP_DROP discards the +// tag before OP_CHECKMULTISIG, so the same keys sign the same way with no key +// derivation. This is how per-account deposit addresses are derived +// (tag = AccountTag(accountURI)); every deposit address is full m-of-n custody. +func TaggedRedeemScript(tag []byte, threshold int, pubkeys [][]byte) ([]byte, error) { + if len(tag) == 0 || len(tag) > 64 { + return nil, fmt.Errorf("btc: tag must be 1..64 bytes, got %d", len(tag)) + } + sorted, err := sortedPubkeys(threshold, pubkeys) + if err != nil { + return nil, err + } + b := txscript.NewScriptBuilder() + b.AddData(tag) + b.AddOp(txscript.OP_DROP) + b.AddInt64(int64(threshold)) // OP_m + for _, pk := range sorted { + b.AddData(pk) + } + b.AddInt64(int64(len(sorted))) // OP_n + b.AddOp(txscript.OP_CHECKMULTISIG) + return b.Script() +} + +// AccountTag derives the per-account script tag from a clearnet account URI: +// the 32-byte SHA256 of the URI. +func AccountTag(accountURI string) []byte { return sha256Sum([]byte(accountURI)) } + +// DepositAddress derives the per-account deposit P2WSH address and its witness +// script. Watch-only: a pure function of accountURI plus the base pubkeys, no +// private keys involved. +func DepositAddress(accountURI string, threshold int, pubkeys [][]byte, net *chaincfg.Params) (btcutil.Address, []byte, error) { + redeem, err := TaggedRedeemScript(AccountTag(accountURI), threshold, pubkeys) + if err != nil { + return nil, nil, err + } + addr, err := VaultAddress(redeem, net) + if err != nil { + return nil, nil, err + } + return addr, redeem, nil +} + +// sortedPubkeys validates the key set and returns BIP-67 (lexicographically) +// sorted copies of the compressed pubkeys. +func sortedPubkeys(threshold int, pubkeys [][]byte) ([][]byte, error) { + if threshold < 1 || threshold > len(pubkeys) { + return nil, fmt.Errorf("btc: threshold %d out of range for %d keys", threshold, len(pubkeys)) + } + if len(pubkeys) > 15 { + return nil, fmt.Errorf("btc: OP_CHECKMULTISIG supports at most 15 keys, got %d", len(pubkeys)) + } + sorted := make([][]byte, len(pubkeys)) + for i, pk := range pubkeys { + if len(pk) != 33 { + return nil, fmt.Errorf("btc: pubkey %d is %d bytes, want 33 (compressed)", i, len(pk)) + } + cp := make([]byte, 33) + copy(cp, pk) + sorted[i] = cp + } + sort.Slice(sorted, func(i, j int) bool { return bytes.Compare(sorted[i], sorted[j]) < 0 }) + return sorted, nil +} + +// VaultAddress derives the P2WSH bech32 address for a redeem script: the +// witness program is SHA256(redeemScript). +func VaultAddress(redeemScript []byte, net *chaincfg.Params) (btcutil.Address, error) { + return btcutil.NewAddressWitnessScriptHash(sha256Sum(redeemScript), net) +} + +// PkScript returns the scriptPubKey for an address. +func PkScript(addr btcutil.Address) ([]byte, error) { + return txscript.PayToAddrScript(addr) +} + +// UTXO is a spendable vault output. +type UTXO struct { + TxID chainhash.Hash + Vout uint32 + Amount int64 // satoshis +} + +// BuildUnsignedTx constructs the canonical unsigned withdrawal transaction. +// Inputs are the selected vault UTXOs (sorted BIP-69) so construction is +// order-independent. Outputs are emitted in a fixed canonical order: recipient, +// then change back to the vault (omitted if below dust), then a zero-value +// OP_RETURN carrying the 32-byte clearnet WithdrawalID. +func BuildUnsignedTx( + utxos []UTXO, + recipient btcutil.Address, + amount int64, + vault btcutil.Address, + withdrawalID [32]byte, + feeSats int64, +) (*wire.MsgTx, error) { + if len(utxos) == 0 { + return nil, fmt.Errorf("btc: no UTXOs selected") + } + if amount <= 0 { + return nil, fmt.Errorf("btc: non-positive amount %d", amount) + } + if feeSats < 0 { + return nil, fmt.Errorf("btc: negative fee %d", feeSats) + } + + ordered := make([]UTXO, len(utxos)) + copy(ordered, utxos) + sort.Slice(ordered, func(i, j int) bool { + if c := bytes.Compare(ordered[i].TxID[:], ordered[j].TxID[:]); c != 0 { + return c < 0 + } + return ordered[i].Vout < ordered[j].Vout + }) + + var inTotal int64 + tx := wire.NewMsgTx(wire.TxVersion) + for _, u := range ordered { + tx.AddTxIn(wire.NewTxIn(wire.NewOutPoint(&u.TxID, u.Vout), nil, nil)) + inTotal += u.Amount + } + + recipientScript, err := txscript.PayToAddrScript(recipient) + if err != nil { + return nil, fmt.Errorf("btc: recipient script: %w", err) + } + vaultScript, err := txscript.PayToAddrScript(vault) + if err != nil { + return nil, fmt.Errorf("btc: vault script: %w", err) + } + + change := inTotal - amount - feeSats + if change < 0 { + return nil, fmt.Errorf("btc: inputs %d below amount %d + fee %d", inTotal, amount, feeSats) + } + + tx.AddTxOut(wire.NewTxOut(amount, recipientScript)) + if change >= dustThresholdSats { + tx.AddTxOut(wire.NewTxOut(change, vaultScript)) + } + + opReturn, err := txscript.NullDataScript(withdrawalID[:]) + if err != nil { + return nil, fmt.Errorf("btc: OP_RETURN script: %w", err) + } + tx.AddTxOut(wire.NewTxOut(0, opReturn)) + + return tx, nil +} + +// SighashAll computes the BIP-143 SIGHASH_ALL digest for one input. +func SighashAll( + tx *wire.MsgTx, + inputIdx int, + redeemScript []byte, + amount int64, + prevFetcher txscript.PrevOutputFetcher, +) ([]byte, error) { + sigHashes := txscript.NewTxSigHashes(tx, prevFetcher) + return txscript.CalcWitnessSigHash(redeemScript, sigHashes, txscript.SigHashAll, tx, inputIdx, amount) +} + +// AssembleWitness builds the witness stack for a P2WSH multisig input: +// +// [ , sig_1, ..., sig_m, redeemScript ] +// +// The leading empty element is the OP_CHECKMULTISIG off-by-one workaround. Each +// signature is DER-encoded with the SIGHASH_ALL type byte already appended, and +// the slice MUST already be ordered to match the position of the corresponding +// public key in the redeem script. +func AssembleWitness(redeemScript []byte, orderedSigs [][]byte) wire.TxWitness { + w := make(wire.TxWitness, 0, len(orderedSigs)+2) + w = append(w, nil) // CHECKMULTISIG dummy + w = append(w, orderedSigs...) + w = append(w, redeemScript) + return w +} + +func sha256Sum(b []byte) []byte { + h := sha256.Sum256(b) + return h[:] +} diff --git a/pkg/blockchain/btc/rpc.go b/pkg/blockchain/btc/rpc.go new file mode 100644 index 0000000..baec153 --- /dev/null +++ b/pkg/blockchain/btc/rpc.go @@ -0,0 +1,65 @@ +package btc + +import ( + "context" + "strings" +) + +// RPC is the bitcoind RPC surface the adapters depend on. It is supplied by the +// caller (mirroring how the EVM adapters take a caller-supplied +// *ethclient.Client), so the SDK carries no JSON-RPC client of its own. The +// block/raw-tx methods back the withdrawal-execution scan. +type RPC interface { + ListUnspent(ctx context.Context, minConf int, addrs []string) ([]Unspent, error) + GetTxOut(ctx context.Context, txid string, vout uint32, includeMempool bool) (*TxOut, error) + SendRawTransaction(ctx context.Context, hexTx string) (string, error) + EstimateSmartFeeSatPerVByte(ctx context.Context, confTarget int, fallbackRate int64) (int64, error) + + // For VerifyExecution: scan recent blocks for the OP_RETURN . + GetBlockCount(ctx context.Context) (int64, error) + GetBlockHash(ctx context.Context, height int64) (string, error) + GetBlockTxids(ctx context.Context, blockHash string) ([]string, error) + GetRawTransaction(ctx context.Context, txid string) (*RawTx, error) +} + +// Unspent is a vault UTXO as reported by ListUnspent. +type Unspent struct { + TxID string + Vout uint32 + AmountSats int64 + Confirmations int64 + ScriptPubKey string +} + +// TxOut is a single output as reported by GetTxOut. +type TxOut struct { + AmountSats int64 + ScriptPubKey string + Confirmations int64 +} + +// RawTx is a decoded transaction as reported by GetRawTransaction. +type RawTx struct { + TxID string + Confirmations int64 + Vouts []RawVout +} + +// RawVout is one output of a RawTx, with its scriptPubKey hex. +type RawVout struct { + ValueSats int64 + ScriptPubKeyHex string +} + +// isAlreadyKnown reports whether a SendRawTransaction error means the tx (or a +// prior attempt spending the same inputs) is already in the chain/mempool — the +// UTXO-model analogue of EVM's executed[withdrawalID] guard. Matched on the +// error text since the concrete RPC client (and its typed error) is caller- +// supplied. +func isAlreadyKnown(err error) bool { + msg := strings.ToLower(err.Error()) + return strings.Contains(msg, "already in block chain") || + strings.Contains(msg, "txn-already-known") || + strings.Contains(msg, "missingorspent") || + strings.Contains(msg, "missing inputs") +} diff --git a/pkg/blockchain/btc/txbuild.go b/pkg/blockchain/btc/txbuild.go new file mode 100644 index 0000000..94c6596 --- /dev/null +++ b/pkg/blockchain/btc/txbuild.go @@ -0,0 +1,72 @@ +package btc + +import ( + "fmt" + "sort" +) + +// Witness/size constants for a P2WSH m-of-n input, used for fee estimation. +const ( + // p2wshInputVBytes is the vsize contribution of one signed P2WSH input + // (witness bytes already discounted 4x), sized conservatively. + p2wshInputVBytes = 120 + // txOverheadVBytes covers version, locktime, segwit marker/flag, counters. + txOverheadVBytes = 11 + // outputVBytes is a conservative upper bound on one output's vsize. + outputVBytes = 43 +) + +// EstimateFeeSats returns the fee for a transaction with numInputs P2WSH inputs +// and numOutputs outputs at the given rate. +func EstimateFeeSats(numInputs, numOutputs int, satPerVByte int64) int64 { + vsize := int64(txOverheadVBytes) + + int64(numInputs)*p2wshInputVBytes + + int64(numOutputs)*outputVBytes + return vsize * satPerVByte +} + +// SelectUTXOs deterministically chooses inputs to cover amount plus the fee the +// resulting transaction will pay. UTXOs are sorted by (amount desc, txid, vout) +// and accumulated greedily until they cover amount + fee, where the fee grows +// with each added input. numFixedOutputs is the count of always-present outputs +// (recipient + OP_RETURN = 2); a change output is assumed for fee sizing. +func SelectUTXOs(available []UTXO, amount int64, satPerVByte int64, numFixedOutputs int) (selected []UTXO, feeSats int64, err error) { + if amount <= 0 { + return nil, 0, fmt.Errorf("btc: non-positive amount %d", amount) + } + pool := make([]UTXO, len(available)) + copy(pool, available) + sort.Slice(pool, func(i, j int) bool { + if pool[i].Amount != pool[j].Amount { + return pool[i].Amount > pool[j].Amount // largest first + } + if c := compareHash(pool[i].TxID[:], pool[j].TxID[:]); c != 0 { + return c < 0 + } + return pool[i].Vout < pool[j].Vout + }) + + var total int64 + for i, u := range pool { + total += u.Amount + n := i + 1 + fee := EstimateFeeSats(n, numFixedOutputs+1, satPerVByte) + if total >= amount+fee { + return pool[:n], fee, nil + } + } + return nil, 0, fmt.Errorf("btc: insufficient vault balance: have %d, need %d + fee at %d sat/vB", + total, amount, satPerVByte) +} + +func compareHash(a, b []byte) int { + for i := range a { + if a[i] != b[i] { + if a[i] < b[i] { + return -1 + } + return 1 + } + } + return 0 +} diff --git a/pkg/blockchain/btc/vault_integration_test.go b/pkg/blockchain/btc/vault_integration_test.go new file mode 100644 index 0000000..117db9c --- /dev/null +++ b/pkg/blockchain/btc/vault_integration_test.go @@ -0,0 +1,356 @@ +//go:build integration + +package btc + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "os" + "strings" + "testing" + "time" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/layer-3/clearnet-sdk/pkg/core" + "github.com/layer-3/clearnet-sdk/pkg/decimal" + "github.com/layer-3/clearnet-sdk/pkg/sign" +) + +// BTC full deposit + withdrawal flow against a real bitcoind (the devnet +// regtest by default). Self-provisioning: each run generates a fresh vault +// (m-of-n P2WSH) + depositor, funds the depositor from a mined node wallet, +// deposits, then runs the quorum withdrawal — so re-running is a clean run with +// no shared state (only the node wallet's coinbase persists). +// +// Build-tagged `integration`. Defaults target `make devnet`; override with +// BTC_RPC_URL / BTC_RPC_USER / BTC_RPC_PASS. + +const ( + defaultBTCRPC = "http://127.0.0.1:18443" + btcWallet = "sdk" + btcSignerCount = 3 + btcThreshold = 2 +) + +func TestIntegrationBTC_DepositAndWithdraw(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) + defer cancel() + + node := &bitcoindRPC{ + url: btcEnv("BTC_RPC_URL", defaultBTCRPC), + wallet: btcWallet, + user: btcEnv("BTC_RPC_USER", "sdk"), + pass: btcEnv("BTC_RPC_PASS", "sdk"), + http: &http.Client{Timeout: 30 * time.Second}, + } + net := &chaincfg.RegressionNetParams + + // ── Setup: wallet + mined funds ─────────────────────────────────────────── + node.ensureWallet(ctx, t) + miner := node.getNewAddress(ctx, t) + node.generateToAddress(ctx, t, 101, miner) // coinbase maturity + + // Fresh keys this run → fresh vault, depositor, deposit address. + signers := make([]sign.Signer, btcSignerCount) + pubkeys := make([][]byte, btcSignerCount) + for i := range signers { + signers[i] = genSecpSigner(t) + pubkeys[i] = signers[i].PublicKey() + } + depositorSigner := genSecpSigner(t) + const account = "yellow://ynet/user/btc-itest" + cfg := Config{ConfirmationDepth: 1, FeeConfTarget: 6, FallbackFeeRate: 5, FeeCapSatPerVByte: 10_000} + + depositor, err := NewDepositor(net, node, depositorSigner, pubkeys, btcThreshold, cfg) + if err != nil { + t.Fatalf("NewDepositor: %v", err) + } + depositAddr, _, err := DepositAddress(account, btcThreshold, pubkeys, net) + if err != nil { + t.Fatalf("DepositAddress: %v", err) + } + + // Watch the depositor + deposit addresses so listunspent/gettxout see them, + // then fund the depositor from the node wallet. + node.importAddress(ctx, t, depositor.DepositorAddress()) + node.importAddress(ctx, t, depositAddr.EncodeAddress()) + node.sendToAddress(ctx, t, depositor.DepositorAddress(), 1.0) // 1 BTC + node.generateToAddress(ctx, t, 1, miner) + + // ── Deposit flow ────────────────────────────────────────────────────────── + depRef, err := depositor.Deposit(ctx, "BTC", decimal.NewFromInt(20_000_000), account) // 0.2 BTC + if err != nil { + t.Fatalf("Deposit: %v", err) + } + node.generateToAddress(ctx, t, 1, miner) // confirm the deposit UTXO + t.Logf("deposit tx %s -> %s", depRef.Raw, depositAddr.EncodeAddress()) + + // ── Withdrawal flow (quorum in-process) ─────────────────────────────────── + finalizers := make([]*WithdrawalFinalizer, btcSignerCount) + for i, s := range signers { + f, err := NewWithdrawalFinalizer(net, node, s, pubkeys, btcThreshold, cfg) + if err != nil { + t.Fatalf("NewWithdrawalFinalizer %d: %v", i, err) + } + if err := f.RegisterDepositAccounts(account); err != nil { + t.Fatalf("register deposit account: %v", err) + } + finalizers[i] = f + } + + var wid [32]byte + wid[0], wid[31] = 0xB7, 0xC0 + op := &core.WithdrawalOp{Recipient: miner, Amount: decimal.NewFromInt(10_000_000)} // 0.1 BTC to the miner addr + + packed, err := finalizers[0].Pack(ctx, op, wid) + if err != nil { + t.Fatalf("Pack: %v", err) + } + shares := make([][]byte, 0, len(finalizers)) + for i, f := range finalizers { + if err := f.Validate(ctx, packed, op, wid); err != nil { + t.Fatalf("Validate[%d]: %v", i, err) + } + s, err := f.Sign(ctx, packed) + if err != nil { + t.Fatalf("Sign[%d]: %v", i, err) + } + shares = append(shares, s) + } + merged, err := finalizers[0].Merge(ctx, packed, shares) + if err != nil { + t.Fatalf("Merge: %v", err) + } + ref, err := finalizers[0].Submit(ctx, merged) + if err != nil { + t.Fatalf("Submit: %v", err) + } + node.generateToAddress(ctx, t, 1, miner) // confirm the withdrawal + t.Logf("withdrawal tx %s", ref.Raw) + + if _, executed, err := finalizers[0].VerifyExecution(ctx, wid); err != nil { + t.Fatalf("VerifyExecution: %v", err) + } else if !executed { + t.Fatal("withdrawal not reported executed") + } +} + +func genSecpSigner(t *testing.T) sign.Signer { + t.Helper() + k, err := crypto.GenerateKey() + if err != nil { + t.Fatalf("gen key: %v", err) + } + return sign.NewKeySignerFromECDSA(k) +} + +func btcEnv(key, def string) string { + if v := os.Getenv(key); v != "" { + return v + } + return def +} + +// ── minimal bitcoind JSON-RPC client (implements btc.RPC + test setup) ──────── + +type bitcoindRPC struct { + url string // node endpoint + wallet string // wallet name (wallet RPCs route to /wallet/) + user string + pass string + http *http.Client +} + +var _ RPC = (*bitcoindRPC)(nil) + +func (c *bitcoindRPC) post(ctx context.Context, endpoint, method string, params []any, out any) error { + body, _ := json.Marshal(map[string]any{"jsonrpc": "1.0", "id": "sdk", "method": method, "params": params}) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body)) + if err != nil { + return err + } + req.SetBasicAuth(c.user, c.pass) + req.Header.Set("Content-Type", "application/json") + resp, err := c.http.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + var env struct { + Result json.RawMessage `json:"result"` + Error *struct { + Code int `json:"code"` + Message string `json:"message"` + } `json:"error"` + } + if err := json.NewDecoder(resp.Body).Decode(&env); err != nil { + return err + } + if env.Error != nil { + return fmt.Errorf("rpc %s: %d %s", method, env.Error.Code, env.Error.Message) + } + if out != nil { + return json.Unmarshal(env.Result, out) + } + return nil +} + +func (c *bitcoindRPC) call(ctx context.Context, method string, params []any, out any) error { + return c.post(ctx, c.url, method, params, out) +} + +func (c *bitcoindRPC) walletCall(ctx context.Context, method string, params []any, out any) error { + return c.post(ctx, c.url+"/wallet/"+c.wallet, method, params, out) +} + +// --- btc.RPC interface --- + +func (c *bitcoindRPC) ListUnspent(ctx context.Context, minConf int, addrs []string) ([]Unspent, error) { + var raw []struct { + TxID string `json:"txid"` + Vout uint32 `json:"vout"` + Amount float64 `json:"amount"` + Confirmations int64 `json:"confirmations"` + ScriptPubKey string `json:"scriptPubKey"` + } + if err := c.walletCall(ctx, "listunspent", []any{minConf, 9999999, addrs}, &raw); err != nil { + return nil, err + } + out := make([]Unspent, len(raw)) + for i, u := range raw { + out[i] = Unspent{TxID: u.TxID, Vout: u.Vout, AmountSats: btcToSats(u.Amount), Confirmations: u.Confirmations, ScriptPubKey: u.ScriptPubKey} + } + return out, nil +} + +func (c *bitcoindRPC) GetTxOut(ctx context.Context, txid string, vout uint32, includeMempool bool) (*TxOut, error) { + var raw *struct { + Confirmations int64 `json:"confirmations"` + Value float64 `json:"value"` + ScriptPubKey struct { + Hex string `json:"hex"` + } `json:"scriptPubKey"` + } + if err := c.call(ctx, "gettxout", []any{txid, vout, includeMempool}, &raw); err != nil { + return nil, err + } + if raw == nil { + return nil, nil + } + return &TxOut{AmountSats: btcToSats(raw.Value), ScriptPubKey: raw.ScriptPubKey.Hex, Confirmations: raw.Confirmations}, nil +} + +func (c *bitcoindRPC) SendRawTransaction(ctx context.Context, hexTx string) (string, error) { + var txid string + return txid, c.call(ctx, "sendrawtransaction", []any{hexTx}, &txid) +} + +func (c *bitcoindRPC) EstimateSmartFeeSatPerVByte(ctx context.Context, confTarget int, fallbackRate int64) (int64, error) { + var raw struct { + FeeRate float64 `json:"feerate"` + } + if err := c.call(ctx, "estimatesmartfee", []any{confTarget}, &raw); err != nil || raw.FeeRate <= 0 { + return fallbackRate, nil // regtest has no fee estimate + } + rate := int64(raw.FeeRate*1e8/1000 + 0.5) + if rate < 1 { + rate = fallbackRate + } + return rate, nil +} + +func (c *bitcoindRPC) GetBlockCount(ctx context.Context) (int64, error) { + var n int64 + return n, c.call(ctx, "getblockcount", []any{}, &n) +} + +func (c *bitcoindRPC) GetBlockHash(ctx context.Context, height int64) (string, error) { + var h string + return h, c.call(ctx, "getblockhash", []any{height}, &h) +} + +func (c *bitcoindRPC) GetBlockTxids(ctx context.Context, blockHash string) ([]string, error) { + var raw struct { + Tx []string `json:"tx"` + } + if err := c.call(ctx, "getblock", []any{blockHash, 1}, &raw); err != nil { + return nil, err + } + return raw.Tx, nil +} + +func (c *bitcoindRPC) GetRawTransaction(ctx context.Context, txid string) (*RawTx, error) { + var raw struct { + TxID string `json:"txid"` + Vout []struct { + Value float64 `json:"value"` + ScriptPubKey struct { + Hex string `json:"hex"` + } `json:"scriptPubKey"` + } `json:"vout"` + } + // verbose=true; blockhash omitted (txindex=1 on the devnet node). + if err := c.call(ctx, "getrawtransaction", []any{txid, true}, &raw); err != nil { + return nil, err + } + out := &RawTx{TxID: raw.TxID, Vouts: make([]RawVout, len(raw.Vout))} + for i, vo := range raw.Vout { + out.Vouts[i] = RawVout{ValueSats: btcToSats(vo.Value), ScriptPubKeyHex: vo.ScriptPubKey.Hex} + } + return out, nil +} + +// --- test-only setup helpers --- + +func (c *bitcoindRPC) ensureWallet(ctx context.Context, t *testing.T) { + t.Helper() + // Legacy wallet (descriptors=false) so importaddress watch-only works. + err := c.call(ctx, "createwallet", []any{c.wallet, false, false, "", false, false}, nil) + if err != nil && !strings.Contains(strings.ToLower(err.Error()), "already exists") && + !strings.Contains(strings.ToLower(err.Error()), "already loaded") { + // loadwallet covers the "exists on disk but not loaded" case. + if lerr := c.call(ctx, "loadwallet", []any{c.wallet}, nil); lerr != nil && + !strings.Contains(strings.ToLower(lerr.Error()), "already loaded") { + t.Fatalf("createwallet/loadwallet: %v / %v", err, lerr) + } + } +} + +func (c *bitcoindRPC) getNewAddress(ctx context.Context, t *testing.T) string { + t.Helper() + var addr string + if err := c.walletCall(ctx, "getnewaddress", []any{"", "bech32"}, &addr); err != nil { + t.Fatalf("getnewaddress: %v", err) + } + return addr +} + +func (c *bitcoindRPC) generateToAddress(ctx context.Context, t *testing.T, n int, addr string) { + t.Helper() + if err := c.call(ctx, "generatetoaddress", []any{n, addr}, nil); err != nil { + t.Fatalf("generatetoaddress: %v", err) + } +} + +func (c *bitcoindRPC) importAddress(ctx context.Context, t *testing.T, addr string) { + t.Helper() + // rescan=false: every address we import is funded only afterwards. + if err := c.walletCall(ctx, "importaddress", []any{addr, "", false}, nil); err != nil { + t.Fatalf("importaddress %s: %v", addr, err) + } +} + +func (c *bitcoindRPC) sendToAddress(ctx context.Context, t *testing.T, addr string, btc float64) { + t.Helper() + var txid string + if err := c.walletCall(ctx, "sendtoaddress", []any{addr, btc}, &txid); err != nil { + t.Fatalf("sendtoaddress: %v", err) + } +} + +func btcToSats(v float64) int64 { return int64(v*1e8 + 0.5) } diff --git a/pkg/blockchain/btc/withdrawal_finalizer.go b/pkg/blockchain/btc/withdrawal_finalizer.go new file mode 100644 index 0000000..5624bbe --- /dev/null +++ b/pkg/blockchain/btc/withdrawal_finalizer.go @@ -0,0 +1,506 @@ +package btc + +import ( + "bytes" + "context" + "encoding/hex" + "encoding/json" + "fmt" + "sort" + "strings" + "sync" + + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + + "github.com/layer-3/clearnet-sdk/pkg/core" + "github.com/layer-3/clearnet-sdk/pkg/sign" +) + +// executionScanBlocks bounds how many recent blocks VerifyExecution scans for +// the OP_RETURN marker. +const executionScanBlocks = int64(100) + +// Config carries the per-chain tunables the finalizer needs. +type Config struct { + ConfirmationDepth uint64 // min confirmations for a vault UTXO to be spendable + FeeConfTarget int // estimatesmartfee confirmation target (blocks) + FallbackFeeRate int64 // sat/vByte used when the node can't estimate + FeeCapSatPerVByte int64 // ceiling Validate accepts on a canonical tx +} + +// WithdrawalFinalizer is the Bitcoin m-of-n P2WSH vault withdrawal path. It +// owns this node's signer (one of the vault keys); the quorum's shares are +// merged off-mesh by the caller. It implements core.VaultWithdrawalFinalizer. +type WithdrawalFinalizer struct { + net *chaincfg.Params + rpc RPC + signer sign.Signer + signerPub []byte + pubkeys [][]byte + threshold int + cfg Config + + vaultAddr btcutil.Address + vaultScript []byte + pubkeyPos map[string]int + + mu sync.RWMutex + spendScripts map[string][]byte + watchAddrs []string +} + +var _ core.VaultWithdrawalFinalizer = (*WithdrawalFinalizer)(nil) + +// NewWithdrawalFinalizer builds the vault finalizer. pubkeys are the providers' +// 33-byte compressed keys; signer is this node's identity and its public key +// must be one of pubkeys. +func NewWithdrawalFinalizer(net *chaincfg.Params, rpc RPC, signer sign.Signer, pubkeys [][]byte, threshold int, cfg Config) (*WithdrawalFinalizer, error) { + if signer.Algorithm() != sign.AlgSecp256k1 { + return nil, fmt.Errorf("btc: signer must be secp256k1, got %s", signer.Algorithm()) + } + redeem, err := RedeemScript(threshold, pubkeys) + if err != nil { + return nil, err + } + vaultAddr, err := VaultAddress(redeem, net) + if err != nil { + return nil, fmt.Errorf("btc: derive vault address: %w", err) + } + vaultScript, err := PkScript(vaultAddr) + if err != nil { + return nil, fmt.Errorf("btc: vault pkScript: %w", err) + } + pos := redeemKeyPositions(pubkeys) + pub := signer.PublicKey() + if _, ok := pos[hex.EncodeToString(pub)]; !ok { + return nil, fmt.Errorf("btc: signer pubkey not in the provided key set") + } + return &WithdrawalFinalizer{ + net: net, + rpc: rpc, + signer: signer, + signerPub: pub, + pubkeys: pubkeys, + threshold: threshold, + cfg: cfg, + vaultAddr: vaultAddr, + vaultScript: vaultScript, + pubkeyPos: pos, + spendScripts: map[string][]byte{strings.ToLower(hex.EncodeToString(vaultScript)): redeem}, + watchAddrs: []string{vaultAddr.EncodeAddress()}, + }, nil +} + +// RegisterDepositAccounts adds the tagged deposit addresses for the given +// account URIs to the spendable set, so withdrawals can select and sign UTXOs +// that landed at per-account deposit addresses. +func (f *WithdrawalFinalizer) RegisterDepositAccounts(accountURIs ...string) error { + f.mu.Lock() + defer f.mu.Unlock() + for _, acct := range accountURIs { + redeem, err := TaggedRedeemScript(AccountTag(acct), f.threshold, f.pubkeys) + if err != nil { + return err + } + addr, err := VaultAddress(redeem, f.net) + if err != nil { + return err + } + pk, err := PkScript(addr) + if err != nil { + return err + } + key := strings.ToLower(hex.EncodeToString(pk)) + if _, ok := f.spendScripts[key]; !ok { + f.spendScripts[key] = redeem + f.watchAddrs = append(f.watchAddrs, addr.EncodeAddress()) + } + } + return nil +} + +func (f *WithdrawalFinalizer) watchAddresses() []string { + f.mu.RLock() + defer f.mu.RUnlock() + out := make([]string, len(f.watchAddrs)) + copy(out, f.watchAddrs) + return out +} + +func (f *WithdrawalFinalizer) resolveScript(pkScriptHex string) ([]byte, bool) { + f.mu.RLock() + defer f.mu.RUnlock() + s, ok := f.spendScripts[strings.ToLower(pkScriptHex)] + return s, ok +} + +// Pack selects vault UTXOs, sizes the fee, and builds the canonical unsigned +// transaction (recipient, optional change, OP_RETURN ). +func (f *WithdrawalFinalizer) Pack(ctx context.Context, op *core.WithdrawalOp, withdrawalID [32]byte) ([]byte, error) { + recipient, amount, err := f.parseOp(op) + if err != nil { + return nil, err + } + unspent, err := f.rpc.ListUnspent(ctx, int(f.cfg.ConfirmationDepth), f.watchAddresses()) + if err != nil { + return nil, fmt.Errorf("btc: list vault utxos: %w", err) + } + utxos, err := f.toUTXOs(unspent) + if err != nil { + return nil, err + } + feeRate, err := f.rpc.EstimateSmartFeeSatPerVByte(ctx, f.cfg.FeeConfTarget, f.cfg.FallbackFeeRate) + if err != nil { + return nil, fmt.Errorf("btc: estimate fee: %w", err) + } + selected, feeSats, err := SelectUTXOs(utxos, amount, feeRate, 2) + if err != nil { + return nil, err + } + tx, err := BuildUnsignedTx(selected, recipient, amount, f.vaultAddr, withdrawalID, feeSats) + if err != nil { + return nil, err + } + return serializeTx(tx) +} + +// Validate re-derives the trust-bound shape from the op and asserts the packed +// tx matches: output 0 pays the exact recipient/amount, the final output is +// OP_RETURN , any middle output is change to the vault, every +// input is a confirmed vault UTXO, and the implied fee is within the ceiling. +func (f *WithdrawalFinalizer) Validate(ctx context.Context, packed []byte, op *core.WithdrawalOp, withdrawalID [32]byte) error { + recipient, amount, err := f.parseOp(op) + if err != nil { + return err + } + tx, err := deserializeTx(packed) + if err != nil { + return fmt.Errorf("btc validate: %w", err) + } + if n := len(tx.TxOut); n != 2 && n != 3 { + return fmt.Errorf("btc validate: expected 2 or 3 outputs, got %d", n) + } + recipientScript, err := txscript.PayToAddrScript(recipient) + if err != nil { + return fmt.Errorf("btc validate: recipient script: %w", err) + } + if !bytes.Equal(tx.TxOut[0].PkScript, recipientScript) { + return fmt.Errorf("btc validate: output 0 not the op recipient") + } + if tx.TxOut[0].Value != amount { + return fmt.Errorf("btc validate: output 0 value %d != op amount %d", tx.TxOut[0].Value, amount) + } + wantOpReturn, err := txscript.NullDataScript(withdrawalID[:]) + if err != nil { + return fmt.Errorf("btc validate: opreturn script: %w", err) + } + last := tx.TxOut[len(tx.TxOut)-1] + if last.Value != 0 || !bytes.Equal(last.PkScript, wantOpReturn) { + return fmt.Errorf("btc validate: final output is not OP_RETURN ") + } + if len(tx.TxOut) == 3 { + change := tx.TxOut[1] + if !bytes.Equal(change.PkScript, f.vaultScript) { + return fmt.Errorf("btc validate: change output not paid to the vault") + } + if change.Value < dustThresholdSats { + return fmt.Errorf("btc validate: change output %d below dust", change.Value) + } + } + totalIn, err := f.sumValidatedInputs(ctx, tx) + if err != nil { + return err + } + var totalOut int64 + for _, o := range tx.TxOut { + totalOut += o.Value + } + fee := totalIn - totalOut + if fee < 0 { + return fmt.Errorf("btc validate: outputs exceed inputs (fee %d)", fee) + } + if cap := EstimateFeeSats(len(tx.TxIn), len(tx.TxOut), f.cfg.FeeCapSatPerVByte); f.cfg.FeeCapSatPerVByte > 0 && fee > cap { + return fmt.Errorf("btc validate: fee %d exceeds ceiling %d", fee, cap) + } + return nil +} + +// SigShare is one signer's contribution: a DER+sighash signature per input, in +// input order, plus the signer's 33-byte compressed pubkey (hex). +type SigShare struct { + PubKey string `json:"pubkey"` + Sigs []string `json:"sigs"` +} + +// Sign produces this node's signature over every input of the packed tx, each +// under its own witness script. Returns a JSON SigShare. +func (f *WithdrawalFinalizer) Sign(ctx context.Context, packed []byte) ([]byte, error) { + tx, err := deserializeTx(packed) + if err != nil { + return nil, fmt.Errorf("btc sign: %w", err) + } + prevFetcher, amounts, redeems, err := f.prevOutputs(ctx, tx) + if err != nil { + return nil, err + } + sigs := make([]string, len(tx.TxIn)) + for idx := range tx.TxIn { + sighash, err := SighashAll(tx, idx, redeems[idx], amounts[idx], prevFetcher) + if err != nil { + return nil, fmt.Errorf("btc sign: sighash input %d: %w", idx, err) + } + der, err := f.signer.Sign(ctx, sighash) + if err != nil { + return nil, fmt.Errorf("btc sign: input %d: %w", idx, err) + } + sigs[idx] = hex.EncodeToString(append(der, byte(txscript.SigHashAll))) + } + return json.Marshal(SigShare{PubKey: hex.EncodeToString(f.signerPub), Sigs: sigs}) +} + +// Merge assembles the witness for every input from the collected shares (the +// threshold lowest by redeem-script key position) and returns the fully-signed +// tx serialization. +func (f *WithdrawalFinalizer) Merge(ctx context.Context, packed []byte, shares [][]byte) ([]byte, error) { + tx, err := deserializeTx(packed) + if err != nil { + return nil, fmt.Errorf("btc merge: %w", err) + } + _, _, redeems, err := f.prevOutputs(ctx, tx) + if err != nil { + return nil, fmt.Errorf("btc merge: %w", err) + } + + parsed := make([]SigShare, 0, len(shares)) + for _, raw := range shares { + var s SigShare + if err := json.Unmarshal(raw, &s); err != nil { + return nil, fmt.Errorf("btc merge: decode share: %w", err) + } + if _, ok := f.pubkeyPos[strings.ToLower(s.PubKey)]; !ok { + return nil, fmt.Errorf("btc merge: share from unknown signer %s", s.PubKey) + } + if len(s.Sigs) != len(tx.TxIn) { + return nil, fmt.Errorf("btc merge: share has %d sigs, tx has %d inputs", len(s.Sigs), len(tx.TxIn)) + } + parsed = append(parsed, s) + } + + for idx := range tx.TxIn { + type posSig struct { + pos int + sig []byte + } + collected := make([]posSig, 0, len(parsed)) + for _, s := range parsed { + sig, err := hex.DecodeString(s.Sigs[idx]) + if err != nil { + return nil, fmt.Errorf("btc merge: decode sig: %w", err) + } + collected = append(collected, posSig{pos: f.pubkeyPos[strings.ToLower(s.PubKey)], sig: sig}) + } + if len(collected) < f.threshold { + return nil, fmt.Errorf("btc merge: input %d has %d sigs, need %d", idx, len(collected), f.threshold) + } + sort.Slice(collected, func(i, j int) bool { return collected[i].pos < collected[j].pos }) + ordered := make([][]byte, f.threshold) + for i := 0; i < f.threshold; i++ { + ordered[i] = collected[i].sig + } + tx.TxIn[idx].Witness = AssembleWitness(redeems[idx], ordered) + } + return serializeTx(tx) +} + +// Submit broadcasts the merged signed tx, returning its hash. Idempotent on an +// already-known/spent reply. +func (f *WithdrawalFinalizer) Submit(ctx context.Context, merged []byte) (core.TxRef, error) { + tx, err := deserializeTx(merged) + if err != nil { + return core.TxRef{}, fmt.Errorf("btc submit: %w", err) + } + hash := [32]byte(tx.TxHash()) + txid := hashToTxid(hash) + if _, err := f.rpc.SendRawTransaction(ctx, hex.EncodeToString(merged)); err != nil { + if isAlreadyKnown(err) { + return core.TxRef{Hash: hash, Raw: txid}, nil + } + return core.TxRef{}, fmt.Errorf("btc submit: sendrawtransaction: %w", err) + } + return core.TxRef{Hash: hash, Raw: txid}, nil +} + +// VerifyExecution scans the most recent blocks for a tx carrying +// OP_RETURN . Returns the tx hash + true on a hit. Bounded by +// executionScanBlocks; a withdrawal older than that window reads as not-found. +func (f *WithdrawalFinalizer) VerifyExecution(ctx context.Context, withdrawalID [32]byte) ([32]byte, bool, error) { + marker, err := txscript.NullDataScript(withdrawalID[:]) + if err != nil { + return [32]byte{}, false, fmt.Errorf("btc verify: opreturn script: %w", err) + } + markerHex := hex.EncodeToString(marker) + + head, err := f.rpc.GetBlockCount(ctx) + if err != nil { + return [32]byte{}, false, fmt.Errorf("btc verify: block count: %w", err) + } + for h := head; h >= 0 && h > head-executionScanBlocks; h-- { + blockHash, err := f.rpc.GetBlockHash(ctx, h) + if err != nil { + return [32]byte{}, false, fmt.Errorf("btc verify: block hash %d: %w", h, err) + } + txids, err := f.rpc.GetBlockTxids(ctx, blockHash) + if err != nil { + return [32]byte{}, false, fmt.Errorf("btc verify: block txids: %w", err) + } + for _, txid := range txids { + raw, err := f.rpc.GetRawTransaction(ctx, txid) + if err != nil || raw == nil { + continue + } + for _, vo := range raw.Vouts { + if strings.EqualFold(vo.ScriptPubKeyHex, markerHex) { + return txidToHash(txid), true, nil + } + } + } + } + return [32]byte{}, false, nil +} + +// --- helpers --- + +func (f *WithdrawalFinalizer) parseOp(op *core.WithdrawalOp) (btcutil.Address, int64, error) { + addr, err := btcutil.DecodeAddress(op.Recipient, f.net) + if err != nil { + return nil, 0, fmt.Errorf("btc: decode recipient %q: %w", op.Recipient, err) + } + if !addr.IsForNet(f.net) { + return nil, 0, fmt.Errorf("btc: recipient %q not valid for %s", op.Recipient, f.net.Name) + } + amt := op.Amount.BigInt() + if !amt.IsInt64() || amt.Int64() <= 0 { + return nil, 0, fmt.Errorf("btc: amount %s not a positive int64 satoshi value", op.Amount.String()) + } + return addr, amt.Int64(), nil +} + +func (f *WithdrawalFinalizer) toUTXOs(unspent []Unspent) ([]UTXO, error) { + out := make([]UTXO, 0, len(unspent)) + for _, u := range unspent { + if _, ok := f.resolveScript(u.ScriptPubKey); !ok { + continue + } + h, err := chainhash.NewHashFromStr(u.TxID) + if err != nil { + return nil, fmt.Errorf("btc: bad utxo txid %q: %w", u.TxID, err) + } + out = append(out, UTXO{TxID: *h, Vout: u.Vout, Amount: u.AmountSats}) + } + return out, nil +} + +func (f *WithdrawalFinalizer) prevOutputs(ctx context.Context, tx *wire.MsgTx) (txscript.PrevOutputFetcher, []int64, [][]byte, error) { + fetcher := txscript.NewMultiPrevOutFetcher(nil) + amounts := make([]int64, len(tx.TxIn)) + redeems := make([][]byte, len(tx.TxIn)) + for i, in := range tx.TxIn { + out, err := f.rpc.GetTxOut(ctx, in.PreviousOutPoint.Hash.String(), in.PreviousOutPoint.Index, true) + if err != nil { + return nil, nil, nil, fmt.Errorf("btc: gettxout input %d: %w", i, err) + } + if out == nil { + return nil, nil, nil, fmt.Errorf("btc: input %d references a spent or unknown output", i) + } + redeem, ok := f.resolveScript(out.ScriptPubKey) + if !ok { + return nil, nil, nil, fmt.Errorf("btc: input %d not a vault output", i) + } + pkScript, err := hex.DecodeString(out.ScriptPubKey) + if err != nil { + return nil, nil, nil, fmt.Errorf("btc: input %d bad scriptPubKey %q: %w", i, out.ScriptPubKey, err) + } + amounts[i] = out.AmountSats + redeems[i] = redeem + fetcher.AddPrevOut(in.PreviousOutPoint, wire.NewTxOut(out.AmountSats, pkScript)) + } + return fetcher, amounts, redeems, nil +} + +func (f *WithdrawalFinalizer) sumValidatedInputs(ctx context.Context, tx *wire.MsgTx) (int64, error) { + var total int64 + for i, in := range tx.TxIn { + out, err := f.rpc.GetTxOut(ctx, in.PreviousOutPoint.Hash.String(), in.PreviousOutPoint.Index, true) + if err != nil { + return 0, fmt.Errorf("btc validate: gettxout input %d: %w", i, err) + } + if out == nil { + return 0, fmt.Errorf("btc validate: input %d spent or unknown", i) + } + if _, ok := f.resolveScript(out.ScriptPubKey); !ok { + return 0, fmt.Errorf("btc validate: input %d not a vault output", i) + } + if out.Confirmations < int64(f.cfg.ConfirmationDepth) { + return 0, fmt.Errorf("btc validate: input %d has %d confs, need %d", i, out.Confirmations, f.cfg.ConfirmationDepth) + } + total += out.AmountSats + } + return total, nil +} + +func serializeTx(tx *wire.MsgTx) ([]byte, error) { + var buf bytes.Buffer + if err := tx.Serialize(&buf); err != nil { + return nil, fmt.Errorf("serialize tx: %w", err) + } + return buf.Bytes(), nil +} + +func deserializeTx(b []byte) (*wire.MsgTx, error) { + tx := wire.NewMsgTx(wire.TxVersion) + if err := tx.Deserialize(bytes.NewReader(b)); err != nil { + return nil, fmt.Errorf("deserialize tx: %w", err) + } + return tx, nil +} + +func redeemKeyPositions(pubkeys [][]byte) map[string]int { + sorted := make([][]byte, len(pubkeys)) + for i, pk := range pubkeys { + cp := make([]byte, len(pk)) + copy(cp, pk) + sorted[i] = cp + } + sort.Slice(sorted, func(i, j int) bool { return bytes.Compare(sorted[i], sorted[j]) < 0 }) + pos := make(map[string]int, len(sorted)) + for i, pk := range sorted { + pos[hex.EncodeToString(pk)] = i + } + return pos +} + +// hashToTxid reverses a 32-byte tx hash into the big-endian hex txid bitcoind +// displays; txidToHash is the inverse. +func hashToTxid(h [32]byte) string { + var r [32]byte + for i := 0; i < 32; i++ { + r[i] = h[31-i] + } + return hex.EncodeToString(r[:]) +} + +func txidToHash(txid string) [32]byte { + b, err := hex.DecodeString(txid) + var out [32]byte + if err != nil || len(b) != 32 { + return out + } + for i := 0; i < 32; i++ { + out[i] = b[31-i] + } + return out +} diff --git a/pkg/blockchain/evm/abi_refresher/main.go b/pkg/blockchain/evm/abi_refresher/main.go new file mode 100644 index 0000000..7f3932d --- /dev/null +++ b/pkg/blockchain/evm/abi_refresher/main.go @@ -0,0 +1,116 @@ +// Command abi_refresher regenerates the EVM contract bindings (the +// `pkg/blockchain/evm/*_abi.go` files) from the vendored ABI + bytecode files +// under `pkg/blockchain/evm/artifacts/`, using go-ethereum's abigen library directly +// — no bash, no jq, no external abigen binary, no forge build. +// +// The vendored `.abi` (interface) and `.bin` (deploy bytecode) +// files are committed: they are the contract surface this package binds, so a +// contract change shows up as a reviewable diff here. Regeneration is fully +// self-contained: +// +// go generate ./pkg/blockchain/evm/... # or: go run ./pkg/blockchain/evm/abi_refresher +// +// Refreshing the vendored files (only when a contract's ABI/bytecode actually +// changes) is done from a repo that owns the Solidity source, e.g.: +// +// jq -r '.abi' clearnet/contracts/evm/out/Custody.sol/Custody.json > artifacts/Custody.abi +// jq -r '.bytecode.object' clearnet/contracts/evm/out/Custody.sol/Custody.json > artifacts/Custody.bin +// +// For each contract abigen.Bind emits the Caller/Transactor/Filterer + +// Deploy tuple into `` under `package evm`. Bytecode is kept so custody +// (and devnet deploy paths) can deploy via the generated `Deploy*` helpers; +// this package itself only calls/reads. Paths are resolved relative to this +// source file, so the working directory does not matter. +package main + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi/abigen" +) + +// pkgName is the package the generated bindings belong to. +const pkgName = "evm" + +// artifactsSubdir is the vendored ABI + bytecode directory, relative to the +// evm package directory. +const artifactsSubdir = "artifacts" + +// contract maps one vendored artifact to its generated binding. name is the +// `.abi` / `.bin` basename and the abigen --type; out is the +// generated file written into the evm package. +type contract struct { + name string + out string +} + +var contracts = []contract{ + {"Slasher", "adjudicator_abi.go"}, + {"Registry", "registry_abi.go"}, + {"MockERC20", "mockerc20_abi.go"}, + {"Custody", "custody_abi.go"}, + {"NodeID", "nodeid_abi.go"}, + {"Faucet", "faucet_abi.go"}, + {"YellowToken", "yellowtoken_abi.go"}, +} + +func main() { + evmDir := packageDir() + artifactsDir := filepath.Join(evmDir, artifactsSubdir) + + for _, c := range contracts { + if err := generate(evmDir, artifactsDir, c); err != nil { + fmt.Fprintf(os.Stderr, "abi_refresher: %s: %v\n", c.name, err) + os.Exit(1) + } + fmt.Printf("abi_refresher: wrote %s\n", c.out) + } +} + +func generate(evmDir, artifactsDir string, c contract) error { + abiJSON, err := os.ReadFile(filepath.Join(artifactsDir, c.name+".abi")) + if err != nil { + return fmt.Errorf("read abi: %w", err) + } + binHex, err := os.ReadFile(filepath.Join(artifactsDir, c.name+".bin")) + if err != nil { + return fmt.Errorf("read bin: %w", err) + } + // abigen wants the deploy bytecode as a bare hex string with neither the + // 0x prefix nor a trailing newline. + bytecode := strings.TrimPrefix(strings.TrimSpace(string(binHex)), "0x") + + code, err := abigen.Bind( + []string{c.name}, + []string{string(abiJSON)}, + []string{bytecode}, + nil, // fsigs — only used by the combined-json path + pkgName, + nil, // libs + nil, // aliases + ) + if err != nil { + return fmt.Errorf("abigen bind: %w", err) + } + + if err := os.WriteFile(filepath.Join(evmDir, c.out), []byte(code), 0o644); err != nil { + return fmt.Errorf("write binding: %w", err) + } + return nil +} + +// packageDir returns the absolute path of the evm package directory (the +// parent of this abi_refresher command), resolved from this source file so the +// working directory is irrelevant. +func packageDir() string { + _, file, _, ok := runtime.Caller(0) + if !ok { + panic("abi_refresher: runtime.Caller failed") + } + // file = /abi_refresher/main.go + return filepath.Dir(filepath.Dir(file)) +} diff --git a/pkg/blockchain/evm/adapter.go b/pkg/blockchain/evm/adapter.go new file mode 100644 index 0000000..f01c7c4 --- /dev/null +++ b/pkg/blockchain/evm/adapter.go @@ -0,0 +1,148 @@ +// Package evm implements the chain-agnostic adapter interfaces (see pkg/core) +// against an EVM chain, one focused type per concern: Depositor and +// WithdrawalFinalizer (the vault money path), plus RegistryAdapter, +// TokenAdapter, FraudAdapter, and FaucetAdapter. Each wraps the relevant +// generated binding(s) over a caller-supplied *ethclient.Client; write-capable +// types additionally take a sign.Signer (the registry/token/faucet/fraud +// adapters take a raw key). The package never dials on its own behalf beyond +// resolving the chain ID for the transactor. +package evm + +import ( + "context" + "crypto/ecdsa" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + gethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" + + "github.com/layer-3/clearnet-sdk/pkg/core" + "github.com/layer-3/clearnet-sdk/pkg/sign" +) + +// signerTransactOpts builds TransactOpts whose tx signature is produced by a +// sign.Signer (so KMS keys work), routing through SignEthDigest. Returns the +// signer's Ethereum address alongside. The caller sets per-tx fields (Value, +// gas) on the returned opts. +func signerTransactOpts(ctx context.Context, client *ethclient.Client, s sign.Signer) (*bind.TransactOpts, common.Address, error) { + addr, err := sign.EthAddress(s) + if err != nil { + return nil, common.Address{}, err + } + chainID, err := client.ChainID(ctx) + if err != nil { + return nil, common.Address{}, fmt.Errorf("get chain ID: %w", err) + } + txSigner := gethtypes.LatestSignerForChainID(chainID) + opts := &bind.TransactOpts{ + From: addr, + Context: ctx, + Signer: func(from common.Address, tx *gethtypes.Transaction) (*gethtypes.Transaction, error) { + if from != addr { + return nil, bind.ErrNotAuthorized + } + sig, err := sign.SignEthDigest(ctx, s, txSigner.Hash(tx).Bytes(), addr) + if err != nil { + return nil, fmt.Errorf("evm: sign tx: %w", err) + } + return tx.WithSignature(txSigner, sig) + }, + } + return opts, addr, nil +} + +// newTransactor builds a chain-ID-bound keyed transactor for write-capable +// adapters. The chain ID is read once from the client at construction. +func newTransactor(ctx context.Context, client *ethclient.Client, key *ecdsa.PrivateKey) (*bind.TransactOpts, error) { + chainID, err := client.ChainID(ctx) + if err != nil { + return nil, fmt.Errorf("get chain ID: %w", err) + } + auth, err := bind.NewKeyedTransactorWithChainID(key, chainID) + if err != nil { + return nil, fmt.Errorf("create transactor: %w", err) + } + return auth, nil +} + +// txOpts clones the stored transactor with a per-call context (and no value). +func txOpts(auth *bind.TransactOpts, ctx context.Context) *bind.TransactOpts { + o := *auth + o.Context = ctx + o.Value = nil + return &o +} + +// waitMined blocks until tx is mined and reverts on a failed receipt. +func waitMined(ctx context.Context, client *ethclient.Client, tx *gethtypes.Transaction) error { + receipt, err := bind.WaitMined(ctx, client, tx) + if err != nil { + return fmt.Errorf("wait mined: %w", err) + } + if receipt.Status == 0 { + return fmt.Errorf("transaction reverted (tx=%s)", tx.Hash().Hex()) + } + return nil +} + +// depositAssetAddress parses an asset address; the zero address denotes native ETH. +func depositAssetAddress(asset string) (common.Address, error) { + if !common.IsHexAddress(asset) { + return common.Address{}, fmt.Errorf("invalid asset address %q", asset) + } + return common.HexToAddress(asset), nil +} + +// convertNodeRecord maps a Registry NodeRecord binding struct into a core.Slot. +func convertNodeRecord(n NodeRecord) *core.Slot { + slot := &core.Slot{ + Index: uint64(n.Index), + TokenID: new(big.Int).SetUint64(uint64(n.TokenId)), + ActivatedAt: n.ActivatedAt, + DeactivatedAt: n.DeactivatedAt, + Collateral: new(big.Int).Add(n.OperatorCollateral, n.SponsorCollateral), + } + // Encode G2 pubkey if present (zero point means slot has no BLS key yet). + if !isZeroG2(n.BlsPubkeyG2) { + var pk [128]byte + copyG2ToBytes(pk[:], n.BlsPubkeyG2) + slot.BLSPubKey = pk[:] + } + return slot +} + +func isZeroG2(g2 [4]*big.Int) bool { + for _, c := range g2 { + if c != nil && c.Sign() != 0 { + return false + } + } + return true +} + +func copyG2ToBytes(dst []byte, g2 [4]*big.Int) { + for i, c := range g2 { + if c == nil { + continue + } + b := c.Bytes() + // Right-align into 32-byte slots. + copy(dst[i*32+(32-len(b)):(i+1)*32], b) + } +} + +// parseRegistryActivationFromReceipt extracts (tokenId, nodeId) from the +// NodeActivated event in a register() receipt. +func parseRegistryActivationFromReceipt(registry *Registry, receipt *gethtypes.Receipt) (uint32, [32]byte, error) { + for _, log := range receipt.Logs { + event, err := registry.ParseNodeActivated(*log) + if err != nil { + continue + } + return event.TokenId, event.NodeId, nil + } + return 0, [32]byte{}, fmt.Errorf("NodeActivated event not found in receipt") +} diff --git a/pkg/blockchain/evm/adjudicator_abi.go b/pkg/blockchain/evm/adjudicator_abi.go new file mode 100644 index 0000000..b12ca50 --- /dev/null +++ b/pkg/blockchain/evm/adjudicator_abi.go @@ -0,0 +1,440 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package evm + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// SlasherMetaData contains all meta data concerning the Slasher contract. +var SlasherMetaData = &bind.MetaData{ + ABI: "[{\"type\":\"constructor\",\"inputs\":[{\"name\":\"_registry\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"MIN_CLUSTER_SIZE\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"REGISTRY\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"submitWithdrawalFraudEvidence\",\"inputs\":[{\"name\":\"challengedObject\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"anchorHeader\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"anchorSignature\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"entryIndex\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"smtProof\",\"type\":\"bytes32[]\",\"internalType\":\"bytes32[]\"},{\"name\":\"smtBitmask\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"balanceKey\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"provenBalance\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"event\",\"name\":\"FraudEvidenceSubmitted\",\"inputs\":[{\"name\":\"blockHash\",\"type\":\"bytes32\",\"indexed\":true,\"internalType\":\"bytes32\"},{\"name\":\"prover\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"signersSlashed\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"}],\"anonymous\":false},{\"type\":\"error\",\"name\":\"ReentrancyGuardReentrantCall\",\"inputs\":[]}]", + Bin: "0x60a0346100de57601f61312c38819003918201601f19168301916001600160401b038311848410176100e2578084926020946040528339810103126100de57516001600160a01b0381168082036100de5760017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055156100a95760805260405161303590816100f7823960805181818161010901528181610e7401528181611b1901526125aa0152f35b60405162461bcd60e51b815260206004820152600d60248201526c5a65726f20726567697374727960981b6044820152606490fd5b5f80fd5b634e487b7160e01b5f52604160045260245ffdfe60806040526004361015610011575f80fd5b5f3560e01c806306433b1b146100f75780631cca16681461003f57638ec1daee1461003a575f80fd5b6101c2565b346100f3576101003660031901126100f3576004356001600160401b0381116100f357610070903690600401610145565b906024356001600160401b0381116100f357610090903690600401610145565b92906044356001600160401b0381116100f3576100b1903690600401610145565b946100ba610183565b608435966001600160401b0388116100f3576100dd6100f1983690600401610192565b94909360a4359660c4359860e4359a6101dd565b005b5f80fd5b346100f3575f3660031901126100f3577f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166080908152602090f35b5f9103126100f357565b9181601f840112156100f3578235916001600160401b0383116100f357602083818601950101116100f357565b6001600160401b038116036100f357565b6064359061019082610172565b565b9181601f840112156100f3578235916001600160401b0383116100f3576020808501948460051b0101116100f357565b346100f3575f3660031901126100f357602060405160058152f35b909a9695939899949a60027f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00541461039f5760027f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055369061023e9261044b565b91369061024a9261044b565b916001600160401b031661025d916106b3565b9161026782610825565b91805190602001208060a08501511461027f90610481565b83519960208b0151602086019b8c51805190602001209060800151604088015160808901519160608a0151936102b49561097e565b986020850151926080860151906102ca94610a97565b61032c976103259660c09561031d946103189489156103965760408051602081018381528183018d90529061030c81606081015b03601f1981018352826103e2565b519020925b0151610c01565b6104bf565b0151116104ff565b3390610e6e565b9051602081519101207f852c66b7cb153328335457559e23c7ae13dbd71edc4c8d92830c24702b7bf3566040518061036a3395829190602083019252565b0390a361019060017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055565b60405f92610311565b633ee5aeb560e01b5f5260045ffd5b634e487b7160e01b5f52604160045260245ffd5b604081019081106001600160401b038211176103dd57604052565b6103ae565b90601f801991011681019081106001600160401b038211176103dd57604052565b60405190610190610140836103e2565b604051906101906040836103e2565b9061019060405192836103e2565b6001600160401b0381116103dd57601f01601f191660200190565b92919261045782610430565b9161046560405193846103e2565b8294818452818301116100f3578281602093845f960137010152565b1561048857565b60405162461bcd60e51b815260206004820152600f60248201526e082dcc6d0dee440dad2e6dac2e8c6d608b1b6044820152606490fd5b156104c657565b60405162461bcd60e51b815260206004820152601160248201527024b73b30b634b21029a6aa10383937b7b360791b6044820152606490fd5b1561050657565b60405162461bcd60e51b815260206004820152601260248201527110985b185b98d9481cdd59999a58da595b9d60721b6044820152606490fd5b6040519060c082018281106001600160401b038211176103dd57604052606060a0835f81525f60208201525f60408201525f838201525f60808201520152565b6040519060e082018281106001600160401b038211176103dd576040525f60c0836105a9610540565b815260606020820152606060408201526060808201528260808201528260a08201520152565b156105d657565b60405162461bcd60e51b815260206004820152601960248201527f496e76616c6964206368616c6c656e676564206f626a656374000000000000006044820152606490fd5b1561062257565b60405162461bcd60e51b815260206004820152601760248201527f456e747269657320646967657374206d69736d617463680000000000000000006044820152606490fd5b1561066e57565b60405162461bcd60e51b815260206004820152601960248201527f547261696c696e67206368616c6c656e676564206279746573000000000000006044820152606490fd5b9190916106be610580565b926106c882610fcc565b600b146106d4906105cf565b6106de9083611068565b908551526106ec90836110f3565b91908551602001526106fe918361128c565b91929060c08701526107109084611068565b908651604001526107219084611068565b9190865160600152855160600151146107399061061b565b61074390836110f3565b908551608001526107549083611481565b9392919060608801526080870152604086015261077191836115aa565b9190855160a0019060a0870152526107899082611683565b6107939082611683565b61079d9082611683565b9051146107a990610667565b81516107b49061176c565b6020830152565b634e487b7160e01b5f52601160045260245ffd5b919082039182116107dc57565b6107bb565b156107e857565b60405162461bcd60e51b8152602060048201526015602482015274547261696c696e672068656164657220627974657360581b6044820152606490fd5b906006610830610540565b9261083a81610fcc565b929092036108ba5761088861087c61087061086461085b6101909686611068565b908952856110f3565b90602089015284611068565b90604088015283611068565b906060870152826110f3565b919060808601526108ae61089c8383611683565b926108a781856107cf565b9083611853565b60a086015251146107e1565b60405162461bcd60e51b815260206004820152600e60248201526d24b73b30b634b2103432b0b232b960911b6044820152606490fd5b9080601f830112156100f3576040519161090b6040846103e2565b8290604081019283116100f357905b8282106109275750505090565b815181526020918201910161091a565b9080601f830112156100f357604051916109526080846103e2565b8290608081019283116100f357905b82821061096e5750505090565b8151815260209182019101610961565b929091959493958151820160e083602083019203126100f3578060806109aa6109b193604087016108f0565b9401610937565b925f945f5b61010081106109ce57506109cb979850611aa7565b90565b8060031c6020811015610a03578a901a6001600783161b1660ff166109f6575b6001016109b6565b6001811b909617956109ee565b610b9a565b919060405192610a196040856103e2565b8390604081019283116100f357905b828210610a3457505050565b8135815260209182019101610a28565b919060405192610a556080856103e2565b8390608081019283116100f357905b828210610a7057505050565b8135815260209182019101610a64565b6001600160401b0381116103dd5760051b60200190565b949383019290610100828503126100f35781359284603f840112156100f357610ac38560208501610a08565b9185607f850112156100f357610adc8660608601610a44565b9360e0810135906001600160401b0382116100f357019786601f8a0112156100f357883598610b0a8a610a80565b97610b18604051998a6103e2565b8a89526020808a019b60051b830101918183116100f357602081019b5b838d10610b4e5750505050610b4b979850611aa7565b50565b8c356001600160401b0381116100f357820183603f820112156100f357602091610b81858360408680960135910161044b565b8152019c019b610b35565b5f1981146107dc5760010190565b634e487b7160e01b5f52603260045260245ffd5b9190811015610a035760051b0190565b15610bc557565b60405162461bcd60e51b8152602060048201526014602482015273534d543a20756e75736564207369626c696e677360601b6044820152606490fd5b9391959495925f905f925f905b6101008210610c2d5750505050610c29929394955014610bbe565b1490565b9091929560019081808c861c16145f14610cda57610c55610c4d87610b8c565b968887610bae565b355b83851c8316610cb157604080516020810193845290810191909152610c7f81606081016102fe565b519020965b604051610ca5816102fe602082019480869091604092825260208201520190565b51902093920190610c0e565b60408051602081019283529081019290925290610cd181606081016102fe565b51902096610c84565b87610c57565b805115610a035760200190565b805160011015610a035760400190565b8051821015610a035760209160051b010190565b51906001600160a01b03821682036100f357565b519061019082610172565b519063ffffffff821682036100f357565b906101c0828203126100f357610deb90610140610d5c610403565b93610d6681610d11565b8552610d7460208201610d25565b6020860152610d8560408201610d25565b6040860152610d9660608201610d25565b6060860152610da760808201610d30565b6080860152610db860a08201610d30565b60a086015260c081015160c086015260e081015160e0860152610ddf8361010083016108f0565b61010086015201610937565b61012082015290565b6040513d5f823e3d90fd5b90602082018092116107dc57565b90600182018092116107dc57565b90600282018092116107dc57565b90600482018092116107dc57565b90600382018092116107dc57565b90600882018092116107dc57565b90600582018092116107dc57565b919082018092116107dc57565b91905f907f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031690825b8551811015610fc557610ed86101c0610eb88389610cfd565b51604051809381926308899cf560e41b8352600483019190602083019252565b0381875afa8015610f9157610eff915f91610f96575b5060e060c082015191015190610e61565b80610f0e575b50600101610e9f565b9093610f1a8588610cfd565b51843b156100f35760405163158ece2760e01b8152600481019190915260248101929092526001600160a01b03831660448301525f8260648183885af1908115610f9157600192610f7092610f77575b50610b8c565b9390610f05565b80610f855f610f8b936103e2565b8061013b565b5f610f6a565b610df4565b610fb891506101c03d8111610fbe575b610fb081836103e2565b810190610d41565b5f610eee565b503d610fa6565b5050509150565b600491610fdb5f60ff93611d9e565b94919390931603610fe857565b60405162461bcd60e51b815260206004820152600e60248201526d457870656374656420617272617960901b6044820152606490fd5b91610fdb60ff92600494611d9e565b1561103457565b60405162461bcd60e51b815260206004820152600c60248201526b21a127a91037bb32b9393ab760a11b6044820152606490fd5b60ff6110776002949383611d9e565b9591929092161490816110e8575b50156110b0576020836109cb926110a761109e83610dff565b8251101561102d565b01015192610dff565b60405162461bcd60e51b815260206004820152601060248201526f22bc3832b1ba32b210313cba32b9999960811b6044820152606490fd5b60209150145f611085565b60ff916110ff91611d9e565b9391939290931661110c57565b60405162461bcd60e51b815260206004820152600d60248201526c115e1c1958dd1959081d5a5b9d609a1b6044820152606490fd5b1561114857565b60405162461bcd60e51b815260206004820152601960248201527f456e74727920696e646578206f7574206f6620626f756e6473000000000000006044820152606490fd5b1561119457565b60405162461bcd60e51b815260206004820152600d60248201526c496e76616c696420656e74727960981b6044820152606490fd5b156111d057565b60405162461bcd60e51b815260206004820152600e60248201526d139bdd081dda5d1a191c985dd85b60921b6044820152606490fd5b805191908290602001825e015f815290565b602061019091611232949360405195869284840190611206565b9081520380855201836103e2565b1561124757565b60405162461bcd60e51b815260206004820152601960248201527f496e76616c6964207769746864726177616c20616d6f756e74000000000000006044820152606490fd5b9091925f9361129c5f948461101e565b90936112a9828410611141565b6060925f915b8383106112df575050506112c4851515611240565b6112d257505f915b93929190565b60208151910120916112cc565b9294978691949296978588145f1461139157505091600161138861134c97959361137b611368611352611343600361135b9f9c9a611336600461133061132861133d948e61101e565b90921461118d565b8b6110f3565b92146111c9565b88611fda565b889d919d611683565b87612054565b9d909d9b612075565b602081519101209c612139565b9a5b611374818c6107cf565b9086611853565b6020815191012090611218565b940191906112af565b979694926113889061137b6113ac87956001959d9a98611683565b9961136a565b156113b957565b60405162461bcd60e51b8152602060048201526013602482015272546f6f206d616e792076616c696461746f727360681b6044820152606490fd5b906113fe82610a80565b61140b60405191826103e2565b828152809261141c601f1991610a80565b01905f5b82811061142c57505050565b806060602080938501015201611420565b1561144457565b60405162461bcd60e51b8152602060048201526015602482015274496e76616c69642076616c696461746f72206b657960581b6044820152606490fd5b9061148e6003918361101e565b9190910361152357906114ba926114a86114b19383612054565b83949194611068565b8395919561101e565b9390926114cb6101008611156113b2565b6114d4856113f4565b945f915b8183106114e85750505093929190565b9091946114f760019183612054565b9690611503828a610cfd565b5261151b6080611513838b610cfd565b51511461143d565b0191906114d8565b60405162461bcd60e51b815260206004820152601360248201527224b73b30b634b21030ba3a32b9ba30ba34b7b760691b6044820152606490fd5b1561156557565b60405162461bcd60e51b815260206004820152601a60248201527f4163636f756e7420736e617073686f74206e6f7420666f756e640000000000006044820152606490fd5b5f93916115b7818361101e565b9490945f915f5b8281106115eb57505050906115d66115e6939261155e565b6115e081866107cf565b91611853565b929190565b6115f76003988761101e565b9890980361163e57611622826116106116199a89611068565b899b919b611068565b89939193611683565b9914611632575b506001016115be565b98506001935083611629565b60405162461bcd60e51b815260206004820152601860248201527f496e76616c6964206163636f756e7420736e617073686f7400000000000000006044820152606490fd5b9061168e9082611d9e565b9160ff16908282158015611762575b61175a5750600282148015611750575b61172e576004821461170257506006146116f95760405162461bcd60e51b815260206004820152601060248201526f2ab739bab83837b93a32b21021a127a960811b6044820152606490fd5b6109cb91611683565b91925f9291505b8183106117165750505090565b9091926117239082611683565b926001019190611709565b91905061174b6109cb936117428484610e61565b9051101561102d565b610e61565b50600382146116ad565b935050505090565b506001831461169d565b6109cb6117e5916102fe6117808251612295565b916117e56117916020830151612300565b916117e56117a26040830151612295565b6117e56117b26060850151612295565b916117e560a06117c56080880151612300565b960151604051604360f91b60208201529c8d9b91999160218d0190611206565b90611206565b604080519091906117fc83826103e2565b60208152918290601f190190369060200137565b9061181a82610430565b61182760405191826103e2565b8281528092611838601f1991610430565b0190602036910137565b908151811015610a03570160200190565b929190928184018085116107dc578151106118bb5761187182611810565b935f5b8381106118815750505050565b806118a861189a61189460019486610e61565b86611842565b516001600160f81b03191690565b5f1a6118b48289611842565b5301611874565b60405162461bcd60e51b815260206004820152600d60248201526c29b634b1b29037bb32b9393ab760991b6044820152606490fd5b156118f757565b60405162461bcd60e51b8152602060048201526014602482015273496e76616c696420636c75737465722073697a6560601b6044820152606490fd5b1561193a57565b60405162461bcd60e51b8152602060048201526015602482015274125b9d985b1a59081d985b1a59185d1bdc881cd95d605a1b6044820152606490fd5b600181901b91906001600160ff1b038116036107dc57565b906006820291808304600614901517156107dc57565b908160051b91808304602014901517156107dc57565b634e487b7160e01b5f52601260045260245ffd5b156119d657565b60405162461bcd60e51b81526020600482015260126024820152714e6f7420656e6f756768207369676e65727360701b6044820152606490fd5b60405190611a1d826103c2565b5f6020838281520152565b15611a2f57565b60405162461bcd60e51b815260206004820152600c60248201526b082a09640dad2e6dac2e8c6d60a31b6044820152606490fd5b15611a6a57565b60405162461bcd60e51b8152602060048201526015602482015274496e76616c696420424c53207369676e617475726560581b6044820152606490fd5b94611b0d9297939496611ae8929660058a101580611c8b575b611ac9906118f0565b611ae382518b8110159081611c7e575b5099989799611933565b612541565b95611b06611b01611afa895193611977565b6003900490565b610e0d565b11156119cf565b611b15611a10565b5f937f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031693915b8751861015611bf157611b7e906101c0611b5e888b610cfd565b51604051809481926308899cf560e41b8352600483019190602083019252565b0381895afa918215610f9157600192610100915f91611bd2575b5001518051919060200151611bab610413565b928352602083015287611bc25750955b0194611b44565b90611bcc916127fb565b95611bbb565b611beb91506101c03d8111610fbe57610fb081836103e2565b5f611b98565b6101909550611c799450611c58611c5d919792939497611c116040610422565b855181529060208601516020830152611c3b611c2d6040610422565b966040810151885260600190565b516020870152611c49610413565b95869283526020830152612927565b611a28565b80519060200151611c6c610413565b91825260208201526129b9565b611a63565b610100915011155f611ad9565b506101008a1115611ac0565b15611c9e57565b60405162461bcd60e51b815260206004820152601260248201527109cdedcc6c2dcdedcd2c6c2d840ead2dce8760731b6044820152606490fd5b15611cdf57565b60405162461bcd60e51b81526020600482015260136024820152722737b731b0b737b734b1b0b6103ab4b73a189b60691b6044820152606490fd5b15611d2157565b60405162461bcd60e51b81526020600482015260136024820152722737b731b0b737b734b1b0b6103ab4b73a199960691b6044820152606490fd5b15611d6357565b60405162461bcd60e51b8152602060048201526013602482015272139bdb98d85b9bdb9a58d85b081d5a5b9d0d8d606a1b6044820152606490fd5b90915f92611dae8351821061102d565b611dc4611dbe61189a8386611842565b60f81c90565b92611dda601f600586901c600716951692610e0d565b9160188110611fd15760188114611f9d5760198114611f4b57601a8114611ea357601b14611e395760405162461bcd60e51b815260206004820152600f60248201526e24b73232b334b734ba329021a127a960891b6044820152606490fd5b611e4561109e83610e45565b5f905b60088210611e70575050611e6a90611e6563ffffffff8611611d5c565b610e45565b91929190565b909460019060081b611e9a611e94611dbe61189a611e8e8b89610e61565b87611842565b60ff1690565b17950190611e48565b50819450611f37611e94611dbe61189a85611f2c611f26611e94611dbe61189a611f2086611f19611f13611e6a9f8f61189a611dbe91611ee861109e611e9495610e29565b611f0d611f07611f01611e94611dbe61189a8c87611842565b60181b90565b97610e0d565b90611842565b60101b90565b1796610e1b565b8b611842565b60081b90565b1794611f0d8a610e37565b1793611f4661ffff8611611d1a565b610e29565b50819450611f5e61109e611e6a93610e1b565b611f8a611e94611dbe61189a611f80611f26611e94611dbe61189a8d8a611842565b94611f0d8a610e0d565b1793611f9860ff8611611cd8565b610e1b565b50819450611e94611dbe61189a84611fc394611fbe61109e611e6a98610e0d565b611842565b93611b016018861015611c97565b94505091929190565b60ff611fe96003949383611d9e565b9591929092160361201957808401938481116107dc5782612010612015945187111561102d565b611853565b9190565b60405162461bcd60e51b815260206004820152601360248201527245787065637465642062797465732d6c696b6560681b6044820152606490fd5b60ff611fe96002949383611d9e565b60ff60209116019060ff82116107dc57565b906120808251611810565b915f5b81518110156120e9578061209960019284611842565b5160f81c604181101581816120dd575b50156120d8576120b890612063565b60f81b6001600160f81b0319165f1a6120d18287611842565b5301612083565b6120b8565b605a915011155f6120a9565b5050565b156120f457565b60405162461bcd60e51b815260206004820152601a60248201527f496e76616c6964207769746864726177616c207061796c6f61640000000000006044820152606490fd5b600661214482610fcc565b91909103612258576121569082611683565b6121609082611683565b5f9061216c9083611d9e565b9160ff1660061460ff6121cd816121c76121b560026121af6121a76121e79a6121a18a60069c6121e19c61224d575b506129cc565b8d61101e565b909214612a0b565b8a611d9e565b93909116159081612244575b50612a49565b87611d9e565b949192909216149081612239575b50612a95565b83612054565b9290936121f8602086511115612ae1565b5f925b85518410156122265760019060081b61221d611e94611dbe61189a888b611842565b179301926121fb565b90939194506101909250935110156120ed565b60029150145f6121db565b9050155f6121c1565b60049150145f61219b565b60405162461bcd60e51b81526020600482015260156024820152740496e76616c6964207769746864726177616c206f7605c1b6044820152606490fd5b604051600b60fb1b6020820152600160fd1b60218201526022808201929092529081526109cb6042826103e2565b156122ca57565b60405162461bcd60e51b815260206004820152600e60248201526d75696e7420746f6f206c6172676560901b6044820152606490fd5b601881106123f75760ff8111156123c85761ffff8111156123995763ffffffff81111561236a576109cb8161233f6001600160401b03809411156122c3565b604051601b60f81b6020820152921660c01b6001600160c01b031916602183015281602981016102fe565b604051600d60f91b602082015260e09190911b6001600160e01b03191660218201526109cb81602581016102fe565b604051601960f81b602082015260f09190911b6001600160f01b03191660218201526109cb81602381016102fe565b604051600360fb1b602082015260f89190911b6001600160f81b03191660218201526109cb81602281016102fe565b60405160f89190911b6001600160f81b03191660208201526109cb81602181016102fe565b9061242682610a80565b61243360405191826103e2565b8281528092611838601f1991610a80565b908160209103126100f3575190565b1561245a57565b60405162461bcd60e51b815260206004820152600e60248201526d2ab735b737bbb71039b4b3b732b960911b6044820152606490fd5b908160209103126100f357516109cb81610172565b906001600160401b03809116911601906001600160401b0382116107dc57565b156124cc57565b60405162461bcd60e51b815260206004820152600e60248201526d04e6f646520696e207761726d75760941b6044820152606490fd5b1561250957565b60405162461bcd60e51b815260206004820152601060248201526f223ab83634b1b0ba329039b4b3b732b960811b6044820152606490fd5b919290925f915f5b8551811015612579576001811b8316612565575b600101612549565b92612571600191610b8c565b93905061255d565b5092916125859061241c565b5f915f5b865181101561279d576001811b82166125a5575b600101612589565b6126067f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031660206125e76125e1858c610cfd565b51612b20565b60405180948192630c038c9b60e11b8352600483019190602083019252565b0381845afa918215610f91575f9261276d575b50612625821515612453565b6040516308899cf560e41b815260048101839052906101c082602481845afa918215610f915761268f602060049481935f9161274e575b50016126826001600160401b0361267a83516001600160401b031690565b161515612453565b516001600160401b031690565b916040519384809262d4200960e41b82525afa918215610f91576126d6926126ce926126c2925f9261271e575b506124a5565b6001600160401b031690565b8710156124c5565b5f5b8581106126ff5750906001916126f76126f087610b8c565b9686610cfd565b52905061259d565b806127188361271060019489610cfd565b511415612502565b016126d8565b61274091925060203d8111612747575b61273881836103e2565b810190612490565b905f6126bc565b503d61272e565b61276791506101c03d8111610fbe57610fb081836103e2565b5f61265c565b61278f91925060203d8111612796575b61278781836103e2565b810190612444565b905f612619565b503d61277d565b505093505050565b604051906127b46020836103e2565b6020368337565b156127c257565b60405162461bcd60e51b8152602060048201526011602482015270109314ce881958d059190819985a5b1959607a1b6044820152606490fd5b608061285c612866929493946020612811611a10565b96816040519361282187866103e2565b86368637805185520151828401528051604084015201516060820152604090815193849161284f84846103e2565b8336843760065afa6127bb565b8051845260200190565b516020830152565b6040516060919061287f83826103e2565b6002815291601f1901825f5b82811061289757505050565b6020906128a2611a10565b8282850101520161288b565b604051906128bb826103c2565b81602060409182516128cd84826103e2565b8336823781528251926128e081856103e2565b3684370152565b604051606091906128f883826103e2565b6002815291601f1901825f5b82811061291057505050565b60209061291b6128ae565b82828501015201612904565b612946604051612936816103c2565b6001815260026020820152612b73565b9161294f61286e565b906129586128e7565b92825115610a03576020830152815115610a03576109cb93612978612bfe565b61298185610ce0565b5261298b84610ce0565b5061299583610ced565b5261299f82610ced565b506129a983610ced565b526129b382610ced565b50612d43565b90916129c761294691612e78565b612b73565b156129d357565b60405162461bcd60e51b815260206004820152601060248201526f115e1c1958dd195908191958da5b585b60821b6044820152606490fd5b15612a1257565b60405162461bcd60e51b815260206004820152600f60248201526e125b9d985b1a5908191958da5b585b608a1b6044820152606490fd5b15612a5057565b60405162461bcd60e51b815260206004820152601c60248201527f446563696d616c206578706f6e656e7420756e737570706f72746564000000006044820152606490fd5b15612a9c57565b60405162461bcd60e51b815260206004820152601860248201527f457870656374656420756e7369676e6564206269676e756d00000000000000006044820152606490fd5b15612ae857565b60405162461bcd60e51b815260206004820152601060248201526f416d6f756e7420746f6f206c6172676560801b6044820152606490fd5b612b2d608082511461143d565b6020810151906040810151906080606082015191015190604051926020840194855260408401526060830152608082015260808152612b6d60a0826103e2565b51902090565b612b7b611a10565b5080511580612bf2575b612bd9575f5160206130155f395f51905f5260208251920151065f5160206130155f395f51905f52035f5160206130155f395f51905f5281116107dc5760405191612bcf836103c2565b8252602082015290565b50604051612be6816103c2565b5f81525f602082015290565b50602081015115612b85565b612c066128ae565b50604051612c13816103c2565b7f198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c281527f1800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed6020820152604051612c68816103c2565b7f090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b81527f12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa602082015260405191612bcf836103c2565b15612cc557565b60405162461bcd60e51b815260206004820152601460248201527308498a67440d8cadccee8d040dad2e6dac2e8c6d60631b6044820152606490fd5b15612d0857565b60405162461bcd60e51b8152602060048201526013602482015272109314ce881c185a5c9a5b99c819985a5b1959606a1b6044820152606490fd5b612d508151835114612cbe565b8051612d63612d5e8261198f565b6119a5565b91612d75612d708361198f565b61241c565b935f5b838110612da75750505050612da260206001938193612d956127a5565b9485920160085afa612d01565b511490565b80612db360019261198f565b612dbd8286610cfd565b5151612dc9828a610cfd565b526020612dd68387610cfd565b510151612deb612de583610e0d565b8a610cfd565b52612df68285610cfd565b515151612e05612de583610e1b565b52612e1b612e138386610cfd565b515160200190565b51612e28612de583610e37565b526020612e358386610cfd565b51015151612e45612de583610e29565b52612e71612e6b612e646020612e5b8689610cfd565b51015160200190565b5192610e53565b89610cfd565b5201612d78565b612e9890612e84611a10565b505f5160206130155f395f51905f52900690565b5f905f5b6101008310612ee15760405162461bcd60e51b8152602060048201526014602482015273109314ce881a185cda151bd1cc4819985a5b195960621b6044820152606490fd5b612f54575f5160206130155f395f51905f52828208915f5160206130155f395f51905f526003818581818009090892612f1984612f59565b5f945f5160206130155f395f51905f5282800914612f3c57505060010191612e9c565b9250925050612f49610413565b918252602082015290565b6119bb565b60206040518181019282845282604083015282606083015260808201527f0c19139cb84c680a6e14116da060561765e05aa45a1c72a34f082305b61f3f5260a08201525f5160206130155f395f51905f5260c082015260c08152612fbe60e0826103e2565b81612fc76117eb565b01928391519060055afa15612fda575190565b60405162461bcd60e51b8152602060048201526012602482015271109314ce881b5bd9115e1c0819985a5b195960721b6044820152606490fdfe30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47", +} + +// SlasherABI is the input ABI used to generate the binding from. +// Deprecated: Use SlasherMetaData.ABI instead. +var SlasherABI = SlasherMetaData.ABI + +// SlasherBin is the compiled bytecode used for deploying new contracts. +// Deprecated: Use SlasherMetaData.Bin instead. +var SlasherBin = SlasherMetaData.Bin + +// DeploySlasher deploys a new Ethereum contract, binding an instance of Slasher to it. +func DeploySlasher(auth *bind.TransactOpts, backend bind.ContractBackend, _registry common.Address) (common.Address, *types.Transaction, *Slasher, error) { + parsed, err := SlasherMetaData.GetAbi() + if err != nil { + return common.Address{}, nil, nil, err + } + if parsed == nil { + return common.Address{}, nil, nil, errors.New("GetABI returned nil") + } + + address, tx, contract, err := bind.DeployContract(auth, *parsed, common.FromHex(SlasherBin), backend, _registry) + if err != nil { + return common.Address{}, nil, nil, err + } + return address, tx, &Slasher{SlasherCaller: SlasherCaller{contract: contract}, SlasherTransactor: SlasherTransactor{contract: contract}, SlasherFilterer: SlasherFilterer{contract: contract}}, nil +} + +// Slasher is an auto generated Go binding around an Ethereum contract. +type Slasher struct { + SlasherCaller // Read-only binding to the contract + SlasherTransactor // Write-only binding to the contract + SlasherFilterer // Log filterer for contract events +} + +// SlasherCaller is an auto generated read-only Go binding around an Ethereum contract. +type SlasherCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// SlasherTransactor is an auto generated write-only Go binding around an Ethereum contract. +type SlasherTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// SlasherFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type SlasherFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// SlasherSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type SlasherSession struct { + Contract *Slasher // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// SlasherCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type SlasherCallerSession struct { + Contract *SlasherCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// SlasherTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type SlasherTransactorSession struct { + Contract *SlasherTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// SlasherRaw is an auto generated low-level Go binding around an Ethereum contract. +type SlasherRaw struct { + Contract *Slasher // Generic contract binding to access the raw methods on +} + +// SlasherCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type SlasherCallerRaw struct { + Contract *SlasherCaller // Generic read-only contract binding to access the raw methods on +} + +// SlasherTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type SlasherTransactorRaw struct { + Contract *SlasherTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewSlasher creates a new instance of Slasher, bound to a specific deployed contract. +func NewSlasher(address common.Address, backend bind.ContractBackend) (*Slasher, error) { + contract, err := bindSlasher(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &Slasher{SlasherCaller: SlasherCaller{contract: contract}, SlasherTransactor: SlasherTransactor{contract: contract}, SlasherFilterer: SlasherFilterer{contract: contract}}, nil +} + +// NewSlasherCaller creates a new read-only instance of Slasher, bound to a specific deployed contract. +func NewSlasherCaller(address common.Address, caller bind.ContractCaller) (*SlasherCaller, error) { + contract, err := bindSlasher(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &SlasherCaller{contract: contract}, nil +} + +// NewSlasherTransactor creates a new write-only instance of Slasher, bound to a specific deployed contract. +func NewSlasherTransactor(address common.Address, transactor bind.ContractTransactor) (*SlasherTransactor, error) { + contract, err := bindSlasher(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &SlasherTransactor{contract: contract}, nil +} + +// NewSlasherFilterer creates a new log filterer instance of Slasher, bound to a specific deployed contract. +func NewSlasherFilterer(address common.Address, filterer bind.ContractFilterer) (*SlasherFilterer, error) { + contract, err := bindSlasher(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &SlasherFilterer{contract: contract}, nil +} + +// bindSlasher binds a generic wrapper to an already deployed contract. +func bindSlasher(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := SlasherMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Slasher *SlasherRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Slasher.Contract.SlasherCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Slasher *SlasherRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Slasher.Contract.SlasherTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Slasher *SlasherRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Slasher.Contract.SlasherTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Slasher *SlasherCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Slasher.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Slasher *SlasherTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Slasher.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Slasher *SlasherTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Slasher.Contract.contract.Transact(opts, method, params...) +} + +// MINCLUSTERSIZE is a free data retrieval call binding the contract method 0x8ec1daee. +// +// Solidity: function MIN_CLUSTER_SIZE() view returns(uint256) +func (_Slasher *SlasherCaller) MINCLUSTERSIZE(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _Slasher.contract.Call(opts, &out, "MIN_CLUSTER_SIZE") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// MINCLUSTERSIZE is a free data retrieval call binding the contract method 0x8ec1daee. +// +// Solidity: function MIN_CLUSTER_SIZE() view returns(uint256) +func (_Slasher *SlasherSession) MINCLUSTERSIZE() (*big.Int, error) { + return _Slasher.Contract.MINCLUSTERSIZE(&_Slasher.CallOpts) +} + +// MINCLUSTERSIZE is a free data retrieval call binding the contract method 0x8ec1daee. +// +// Solidity: function MIN_CLUSTER_SIZE() view returns(uint256) +func (_Slasher *SlasherCallerSession) MINCLUSTERSIZE() (*big.Int, error) { + return _Slasher.Contract.MINCLUSTERSIZE(&_Slasher.CallOpts) +} + +// REGISTRY is a free data retrieval call binding the contract method 0x06433b1b. +// +// Solidity: function REGISTRY() view returns(address) +func (_Slasher *SlasherCaller) REGISTRY(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _Slasher.contract.Call(opts, &out, "REGISTRY") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// REGISTRY is a free data retrieval call binding the contract method 0x06433b1b. +// +// Solidity: function REGISTRY() view returns(address) +func (_Slasher *SlasherSession) REGISTRY() (common.Address, error) { + return _Slasher.Contract.REGISTRY(&_Slasher.CallOpts) +} + +// REGISTRY is a free data retrieval call binding the contract method 0x06433b1b. +// +// Solidity: function REGISTRY() view returns(address) +func (_Slasher *SlasherCallerSession) REGISTRY() (common.Address, error) { + return _Slasher.Contract.REGISTRY(&_Slasher.CallOpts) +} + +// SubmitWithdrawalFraudEvidence is a paid mutator transaction binding the contract method 0x1cca1668. +// +// Solidity: function submitWithdrawalFraudEvidence(bytes challengedObject, bytes anchorHeader, bytes anchorSignature, uint64 entryIndex, bytes32[] smtProof, uint256 smtBitmask, bytes32 balanceKey, uint256 provenBalance) returns() +func (_Slasher *SlasherTransactor) SubmitWithdrawalFraudEvidence(opts *bind.TransactOpts, challengedObject []byte, anchorHeader []byte, anchorSignature []byte, entryIndex uint64, smtProof [][32]byte, smtBitmask *big.Int, balanceKey [32]byte, provenBalance *big.Int) (*types.Transaction, error) { + return _Slasher.contract.Transact(opts, "submitWithdrawalFraudEvidence", challengedObject, anchorHeader, anchorSignature, entryIndex, smtProof, smtBitmask, balanceKey, provenBalance) +} + +// SubmitWithdrawalFraudEvidence is a paid mutator transaction binding the contract method 0x1cca1668. +// +// Solidity: function submitWithdrawalFraudEvidence(bytes challengedObject, bytes anchorHeader, bytes anchorSignature, uint64 entryIndex, bytes32[] smtProof, uint256 smtBitmask, bytes32 balanceKey, uint256 provenBalance) returns() +func (_Slasher *SlasherSession) SubmitWithdrawalFraudEvidence(challengedObject []byte, anchorHeader []byte, anchorSignature []byte, entryIndex uint64, smtProof [][32]byte, smtBitmask *big.Int, balanceKey [32]byte, provenBalance *big.Int) (*types.Transaction, error) { + return _Slasher.Contract.SubmitWithdrawalFraudEvidence(&_Slasher.TransactOpts, challengedObject, anchorHeader, anchorSignature, entryIndex, smtProof, smtBitmask, balanceKey, provenBalance) +} + +// SubmitWithdrawalFraudEvidence is a paid mutator transaction binding the contract method 0x1cca1668. +// +// Solidity: function submitWithdrawalFraudEvidence(bytes challengedObject, bytes anchorHeader, bytes anchorSignature, uint64 entryIndex, bytes32[] smtProof, uint256 smtBitmask, bytes32 balanceKey, uint256 provenBalance) returns() +func (_Slasher *SlasherTransactorSession) SubmitWithdrawalFraudEvidence(challengedObject []byte, anchorHeader []byte, anchorSignature []byte, entryIndex uint64, smtProof [][32]byte, smtBitmask *big.Int, balanceKey [32]byte, provenBalance *big.Int) (*types.Transaction, error) { + return _Slasher.Contract.SubmitWithdrawalFraudEvidence(&_Slasher.TransactOpts, challengedObject, anchorHeader, anchorSignature, entryIndex, smtProof, smtBitmask, balanceKey, provenBalance) +} + +// SlasherFraudEvidenceSubmittedIterator is returned from FilterFraudEvidenceSubmitted and is used to iterate over the raw logs and unpacked data for FraudEvidenceSubmitted events raised by the Slasher contract. +type SlasherFraudEvidenceSubmittedIterator struct { + Event *SlasherFraudEvidenceSubmitted // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *SlasherFraudEvidenceSubmittedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(SlasherFraudEvidenceSubmitted) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(SlasherFraudEvidenceSubmitted) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *SlasherFraudEvidenceSubmittedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *SlasherFraudEvidenceSubmittedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// SlasherFraudEvidenceSubmitted represents a FraudEvidenceSubmitted event raised by the Slasher contract. +type SlasherFraudEvidenceSubmitted struct { + BlockHash [32]byte + Prover common.Address + SignersSlashed *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterFraudEvidenceSubmitted is a free log retrieval operation binding the contract event 0x852c66b7cb153328335457559e23c7ae13dbd71edc4c8d92830c24702b7bf356. +// +// Solidity: event FraudEvidenceSubmitted(bytes32 indexed blockHash, address indexed prover, uint256 signersSlashed) +func (_Slasher *SlasherFilterer) FilterFraudEvidenceSubmitted(opts *bind.FilterOpts, blockHash [][32]byte, prover []common.Address) (*SlasherFraudEvidenceSubmittedIterator, error) { + + var blockHashRule []interface{} + for _, blockHashItem := range blockHash { + blockHashRule = append(blockHashRule, blockHashItem) + } + var proverRule []interface{} + for _, proverItem := range prover { + proverRule = append(proverRule, proverItem) + } + + logs, sub, err := _Slasher.contract.FilterLogs(opts, "FraudEvidenceSubmitted", blockHashRule, proverRule) + if err != nil { + return nil, err + } + return &SlasherFraudEvidenceSubmittedIterator{contract: _Slasher.contract, event: "FraudEvidenceSubmitted", logs: logs, sub: sub}, nil +} + +// WatchFraudEvidenceSubmitted is a free log subscription operation binding the contract event 0x852c66b7cb153328335457559e23c7ae13dbd71edc4c8d92830c24702b7bf356. +// +// Solidity: event FraudEvidenceSubmitted(bytes32 indexed blockHash, address indexed prover, uint256 signersSlashed) +func (_Slasher *SlasherFilterer) WatchFraudEvidenceSubmitted(opts *bind.WatchOpts, sink chan<- *SlasherFraudEvidenceSubmitted, blockHash [][32]byte, prover []common.Address) (event.Subscription, error) { + + var blockHashRule []interface{} + for _, blockHashItem := range blockHash { + blockHashRule = append(blockHashRule, blockHashItem) + } + var proverRule []interface{} + for _, proverItem := range prover { + proverRule = append(proverRule, proverItem) + } + + logs, sub, err := _Slasher.contract.WatchLogs(opts, "FraudEvidenceSubmitted", blockHashRule, proverRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(SlasherFraudEvidenceSubmitted) + if err := _Slasher.contract.UnpackLog(event, "FraudEvidenceSubmitted", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseFraudEvidenceSubmitted is a log parse operation binding the contract event 0x852c66b7cb153328335457559e23c7ae13dbd71edc4c8d92830c24702b7bf356. +// +// Solidity: event FraudEvidenceSubmitted(bytes32 indexed blockHash, address indexed prover, uint256 signersSlashed) +func (_Slasher *SlasherFilterer) ParseFraudEvidenceSubmitted(log types.Log) (*SlasherFraudEvidenceSubmitted, error) { + event := new(SlasherFraudEvidenceSubmitted) + if err := _Slasher.contract.UnpackLog(event, "FraudEvidenceSubmitted", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} diff --git a/pkg/blockchain/evm/artifacts/Custody.abi b/pkg/blockchain/evm/artifacts/Custody.abi new file mode 100644 index 0000000..6d5ebf7 --- /dev/null +++ b/pkg/blockchain/evm/artifacts/Custody.abi @@ -0,0 +1,289 @@ +[ + { + "type": "constructor", + "inputs": [ + { + "name": "initialSigners", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "initialThreshold", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "receive", + "stateMutability": "payable" + }, + { + "type": "function", + "name": "deposit", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + }, + { + "name": "asset", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "execute", + "inputs": [ + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "asset", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "withdrawalId", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "signatures", + "type": "bytes[]", + "internalType": "bytes[]" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "executed", + "inputs": [ + { + "name": "withdrawalId", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "isSigner", + "inputs": [ + { + "name": "addr", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "signers", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address[]", + "internalType": "address[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "threshold", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "updateSigners", + "inputs": [ + { + "name": "newSigners", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "newThreshold", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "signatures", + "type": "bytes[]", + "internalType": "bytes[]" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "event", + "name": "Deposited", + "inputs": [ + { + "name": "depositor", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "account", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "asset", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Executed", + "inputs": [ + { + "name": "withdrawalId", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "to", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "asset", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "SignersUpdated", + "inputs": [ + { + "name": "newSigners", + "type": "address[]", + "indexed": false, + "internalType": "address[]" + }, + { + "name": "newThreshold", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "ECDSAInvalidSignature", + "inputs": [] + }, + { + "type": "error", + "name": "ECDSAInvalidSignatureLength", + "inputs": [ + { + "name": "length", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "ECDSAInvalidSignatureS", + "inputs": [ + { + "name": "s", + "type": "bytes32", + "internalType": "bytes32" + } + ] + }, + { + "type": "error", + "name": "ReentrancyGuardReentrantCall", + "inputs": [] + }, + { + "type": "error", + "name": "SafeERC20FailedOperation", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "address" + } + ] + } +] diff --git a/pkg/blockchain/evm/artifacts/Custody.bin b/pkg/blockchain/evm/artifacts/Custody.bin new file mode 100644 index 0000000..0ef1520 --- /dev/null +++ b/pkg/blockchain/evm/artifacts/Custody.bin @@ -0,0 +1 @@ +0x60806040523461039c5761136c80380380610019816103a0565b928339810160408282031261039c5781516001600160401b03811161039c5782019181601f8401121561039c578251926001600160401b03841161029a578360051b9260206100698186016103a0565b8096815201916020839582010191821161039c57602001915b81831061037c575050506020015160017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055801561033757808351106102f35760038351106102ae575f5b83518110156101fc576001600160a01b036100e882866103c5565b5116156101b75780610126575b6001906001600160a01b0361010a82876103c5565b51165f528160205260405f208260ff19825416179055016100cd565b6001600160a01b0361013882866103c5565b51165f1982018281116101a3576001600160a01b039061015890876103c5565b5116106100f557606460405162461bcd60e51b815260206004820152602060248201527f5369676e657273206d75737420626520736f7274656420617363656e64696e676044820152fd5b634e487b7160e01b5f52601160045260245ffd5b60405162461bcd60e51b815260206004820152601360248201527f5a65726f2061646472657373207369676e6572000000000000000000000000006044820152606490fd5b509151906001600160401b03821161029a5768010000000000000000821161029a576003548260035580831061026f575b5060035f5260205f205f5b8381106102525784600255604051610f7e90816103ee8239f35b82516001600160a01b031681830155602090920191600101610238565b60035f52828060205f20019103905f5b82811061028d57505061022d565b5f8282015560010161027f565b634e487b7160e01b5f52604160045260245ffd5b60405162461bcd60e51b815260206004820152601760248201527f4e656564206174206c656173742033207369676e6572730000000000000000006044820152606490fd5b606460405162461bcd60e51b815260206004820152602060248201527f4e6f7420656e6f756768207369676e65727320666f72207468726573686f6c646044820152fd5b60405162461bcd60e51b815260206004820152601a60248201527f5468726573686f6c64206d75737420626520706f7369746976650000000000006044820152606490fd5b82516001600160a01b038116810361039c57815260209283019201610082565b5f80fd5b6040519190601f01601f191682016001600160401b0381118382101761029a57604052565b80518210156103d95760209160051b010190565b634e487b7160e01b5f52603260045260245ffdfe608080604052600436101561001c575b50361561001a575f80fd5b005b5f3560e01c9081630e2411ac1461066957508063191d0a49146103d557806342cde4e8146103b857806346f0975a146103035780637df73e27146102c65780638340f549146100a75763a9fcfb3314610075575f61000f565b346100a35760203660031901126100a3576004355f525f602052602060ff60405f2054166040519015158152f35b5f80fd5b60603660031901126100a3576100bb610aad565b6100c3610ac3565b604435916100cf610dc4565b6001600160a01b0316918215610292576100ea811515610b85565b6001600160a01b038216806101ad57508034036101735761014a7f4174a9435a04d04d274c76779cad136a41fde6937c56241c09ab9d3c7064a1a9915b604080516001600160a01b03909516855260208501919091523393918291820190565b0390a360017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055005b60405162461bcd60e51b815260206004820152601260248201527108aa89040ecc2d8eaca40dad2e6dac2e8c6d60731b6044820152606490fd5b3461024d576040516323b872dd60e01b5f5233600452306024528260445260205f60648180865af19060015f511482161561022c575b6040525f6060521561021a575061014a7f4174a9435a04d04d274c76779cad136a41fde6937c56241c09ab9d3c7064a1a991610127565b635274afe760e01b5f5260045260245ffd5b90600181151661024457823b15153d151616906101e3565b503d5f823e3d90fd5b60405162461bcd60e51b815260206004820152601b60248201527f4554482073656e742077697468204552433230206465706f73697400000000006044820152606490fd5b60405162461bcd60e51b815260206004820152600c60248201526b16995c9bc81858d8dbdd5b9d60a21b6044820152606490fd5b346100a35760203660031901126100a3576001600160a01b036102e7610aad565b165f526001602052602060ff60405f2054166040519015158152f35b346100a3575f3660031901126100a3576040518060206003549283815201809260035f525f516020610f5e5f395f51905f52905f5b818110610399575050508161034e910382610b2b565b604051918291602083019060208452518091526040830191905f5b818110610377575050500390f35b82516001600160a01b0316845285945060209384019390920191600101610369565b82546001600160a01b0316845260209093019260019283019201610338565b346100a3575f3660031901126100a3576020600254604051908152f35b346100a35760a03660031901126100a3576103ee610aad565b6103f6610ac3565b6064359060443560843567ffffffffffffffff81116100a35761041d903690600401610a7c565b9490610427610dc4565b845f525f60205260ff60405f205416610631576001600160a01b0382169586156105fb576104a49061045a851515610b85565b604051926020840146815230604086015289606086015260018060a01b038816948560808201528760a08201528960c082015260c0815261049c60e082610b2b565b519020610bdb565b845f525f60205260405f20600160ff1982541617905580155f1461057e57505f80808481945af13d15610579573d6104db81610bbf565b906104e96040519283610b2b565b81525f60203d92013e5b1561053e577fe57dd573634102b6cae74aab341f709f6fc3ae2bdc0a35f9a47a85f45b677a21915b604080516001600160a01b0390921682526020820192909252908190810161014a565b60405162461bcd60e51b8152602060048201526013602482015272115512081d1c985b9cd9995c8819985a5b1959606a1b6044820152606490fd5b6104f3565b90505f9291925060405163a9059cbb60e01b5f52856004528360245260205f60448180865af19060015f51148216156105e3575b6040521561021a5750907fe57dd573634102b6cae74aab341f709f6fc3ae2bdc0a35f9a47a85f45b677a219161051b565b90600181151661024457823b15153d151616906105b2565b60405162461bcd60e51b815260206004820152600e60248201526d16995c9bc81c9958da5c1a595b9d60921b6044820152606490fd5b60405162461bcd60e51b815260206004820152601060248201526f105b1c9958591e48195e1958dd5d195960821b6044820152606490fd5b346100a35760603660031901126100a35760043567ffffffffffffffff81116100a35761069a903690600401610a7c565b6024359260443567ffffffffffffffff81116100a3576106be903690600401610a7c565b90918515610a3a57508483106109f657600383106109b15761074c916040516020810190610700816106f28a898b87610ad9565b03601f198101835282610b2b565b519020604051602081019146835230604083015260806060830152600d60a08301526c7570646174655369676e65727360981b60c0830152608082015260c0815261049c60e082610b2b565b5f5b600354811015610790575f516020610f5e5f395f51905f528101546001600160a01b03165f908152600160208190526040909120805460ff191690550161074e565b50905f5b828110610881575067ffffffffffffffff821161086d5768010000000000000000821161086d57816003548160035580821061083f575b50508060035f525f5b838110610817575050610812837feb4dc7fab86d67670d7a4d7443a38860da1aa053f26529c8f41cc68e5d6a93369460025560405193849384610ad9565b0390a1005b600190602061082584610b71565b930192815f516020610f5e5f395f51905f520155016107d4565b035f5b818110610851578391506107cb565b5f8482015f516020610f5e5f395f51905f520155600101610842565b634e487b7160e01b5f52604160045260245ffd5b6001600160a01b0361089c610897838686610b4d565b610b71565b161561097657806108dc575b6001906001600160a01b036108c1610897838787610b4d565b165f528160205260405f208260ff1982541617905501610794565b6108ea610897828585610b4d565b5f198201828111610962576001600160a01b039061090d90610897908787610b4d565b166001600160a01b03909116116108a857606460405162461bcd60e51b815260206004820152602060248201527f5369676e657273206d75737420626520736f7274656420617363656e64696e676044820152fd5b634e487b7160e01b5f52601160045260245ffd5b60405162461bcd60e51b81526020600482015260136024820152722d32b9379030b2323932b9b99039b4b3b732b960691b6044820152606490fd5b60405162461bcd60e51b815260206004820152601760248201527f4e656564206174206c656173742033207369676e6572730000000000000000006044820152606490fd5b606460405162461bcd60e51b815260206004820152602060248201527f4e6f7420656e6f756768207369676e65727320666f72207468726573686f6c646044820152fd5b62461bcd60e51b815260206004820152601a60248201527f5468726573686f6c64206d75737420626520706f7369746976650000000000006044820152606490fd5b9181601f840112156100a35782359167ffffffffffffffff83116100a3576020808501948460051b0101116100a357565b600435906001600160a01b03821682036100a357565b602435906001600160a01b03821682036100a357565b6040808252810183905293929160608501905f905b808210610b0057505060209150930152565b909183356001600160a01b03811691908290036100a357908152602093840193019160010190610aee565b90601f8019910116810190811067ffffffffffffffff82111761086d57604052565b9190811015610b5d5760051b0190565b634e487b7160e01b5f52603260045260245ffd5b356001600160a01b03811681036100a35790565b15610b8c57565b60405162461bcd60e51b815260206004820152600b60248201526a16995c9bc8185b5bdd5b9d60aa1b6044820152606490fd5b67ffffffffffffffff811161086d57601f01601f191660200190565b9060025490818410610d8d575f948592835b86881015610d39578760051b840135601e19853603018112156100a35784019081359167ffffffffffffffff83116100a357602081019083360382136100a357610c3684610bbf565b90610c446040519283610b2b565b84825260208536920101116100a3575f602085610c7696610c6d95838601378301015288610e22565b90939193610e5c565b6001600160a01b038281169116811115610ce8575f52600160205260ff60405f20541615610cb457935f198114610962576001978801970193610bed565b60405162461bcd60e51b815260206004820152600c60248201526b2737ba10309039b4b3b732b960a11b6044820152606490fd5b60405162461bcd60e51b815260206004820152602360248201527f5369676e617475726573206e6f74206f726465726564206f72206475706c696360448201526261746560e81b6064820152608490fd5b509450945050905010610d4857565b60405162461bcd60e51b815260206004820152601d60248201527f496e73756666696369656e742076616c6964207369676e6174757265730000006044820152606490fd5b60405162461bcd60e51b815260206004820152600f60248201526e10995b1bddc81d1a1c995cda1bdb19608a1b6044820152606490fd5b60027f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f005414610e135760027f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055565b633ee5aeb560e01b5f5260045ffd5b8151919060418303610e5257610e4b9250602082015190606060408401519301515f1a90610ed0565b9192909190565b50505f9160029190565b6004811015610ebc5780610e6e575050565b60018103610e855763f645eedf60e01b5f5260045ffd5b60028103610ea0575063fce698f760e01b5f5260045260245ffd5b600314610eaa5750565b6335e2f38360e21b5f5260045260245ffd5b634e487b7160e01b5f52602160045260245ffd5b91907f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08411610f52579160209360809260ff5f9560405194855216868401526040830152606082015282805260015afa15610f47575f516001600160a01b03811615610f3d57905f905f90565b505f906001905f90565b6040513d5f823e3d90fd5b5050505f916003919056fec2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b diff --git a/pkg/blockchain/evm/artifacts/Faucet.abi b/pkg/blockchain/evm/artifacts/Faucet.abi new file mode 100644 index 0000000..73305fe --- /dev/null +++ b/pkg/blockchain/evm/artifacts/Faucet.abi @@ -0,0 +1,224 @@ +[ + { + "type": "constructor", + "inputs": [ + { + "name": "_token", + "type": "address", + "internalType": "contract IERC20" + }, + { + "name": "_dripAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "_cooldown", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "TOKEN", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract IERC20" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "cooldown", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "drip", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "dripAmount", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "dripTo", + "inputs": [ + { + "name": "recipient", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "lastDrip", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "owner", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "setCooldown", + "inputs": [ + { + "name": "_cooldown", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setDripAmount", + "inputs": [ + { + "name": "_dripAmount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setOwner", + "inputs": [ + { + "name": "_owner", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "withdraw", + "inputs": [ + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "event", + "name": "CooldownUpdated", + "inputs": [ + { + "name": "newCooldown", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "DripAmountUpdated", + "inputs": [ + { + "name": "newAmount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Dripped", + "inputs": [ + { + "name": "recipient", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnerUpdated", + "inputs": [ + { + "name": "newOwner", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + } +] diff --git a/pkg/blockchain/evm/artifacts/Faucet.bin b/pkg/blockchain/evm/artifacts/Faucet.bin new file mode 100644 index 0000000..b9b97e1 --- /dev/null +++ b/pkg/blockchain/evm/artifacts/Faucet.bin @@ -0,0 +1 @@ +0x60a03461009a57601f6107bb38819003918201601f19168301916001600160401b0383118484101761009e5780849260609460405283398101031261009a578051906001600160a01b038216820361009a5760406020820151910151916080523360018060a01b03195f5416175f5560015560025560405161070890816100b3823960805181818161011d0152818161026d01526104e30152f35b5f80fd5b634e487b7160e01b5f52604160045260245ffdfe6080806040526004361015610012575f80fd5b5f3560e01c9081630935f004146103d35750806313af40351461032c5780632e1a7d4d1461021e57806335a1529b146102015780634fc3f41a146101b5578063543f8c5814610169578063787a08a61461014c57806382bfefc8146101085780638da5cb5b146100e15780639f678cca146100c85763cabee26e14610095575f80fd5b346100c45760203660031901126100c4576004356001600160a01b03811681036100c4576100c2906104a6565b005b5f80fd5b346100c4575f3660031901126100c4576100c2336104a6565b346100c4575f3660031901126100c4575f546040516001600160a01b039091168152602090f35b346100c4575f3660031901126100c4576040517f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03168152602090f35b346100c4575f3660031901126100c4576020600254604051908152f35b346100c45760203660031901126100c4577f33f3faee0788ab897d8f674abe1dde6d93ba901e4a1502161294734ba178e3c760206004356101a861045a565b80600155604051908152a1005b346100c45760203660031901126100c4577f583d8b24c5439ab7d810e51e37e8db41ba66f1168fd7b752ceae0c7681c5272c60206004356101f461045a565b80600255604051908152a1005b346100c4575f3660031901126100c4576020600154604051908152f35b346100c45760203660031901126100c45761023761045a565b5f5460405163a9059cbb60e01b81526001600160a01b03909116600480830191909152356024820152602081806044810103815f7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03165af1908115610321575f916102f2575b50156102ad57005b60405162461bcd60e51b815260206004820152601760248201527f4661756365743a207769746864726177206661696c65640000000000000000006044820152606490fd5b610314915060203d60201161031a575b61030c818361040c565b810190610442565b816102a5565b503d610302565b6040513d5f823e3d90fd5b346100c45760203660031901126100c4576004356001600160a01b038116908190036100c45761035a61045a565b8015610397575f80546001600160a01b031916821781557f4ffd725fc4a22075e9ec71c59edf9c38cdeb588a91b24fc5b61388c5be41282b9080a2005b60405162461bcd60e51b81526020600482015260146024820152734661756365743a207a65726f206164647265737360601b6044820152606490fd5b346100c45760203660031901126100c4576004356001600160a01b03811691908290036100c4576020915f526003825260405f20548152f35b90601f8019910116810190811067ffffffffffffffff82111761042e57604052565b634e487b7160e01b5f52604160045260245ffd5b908160209103126100c4575180151581036100c45790565b5f546001600160a01b0316330361046d57565b60405162461bcd60e51b81526020600482015260116024820152702330bab1b2ba1d103737ba1037bbb732b960791b6044820152606490fd5b6001600160a01b0381165f818152600360205260409020549091901580156106d2575b1561068d576040516370a0823160e01b81523060048201527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031690602081602481855afa908115610321575f9161065b575b5060015411610616575f838152600360209081526040808320429055600154905163a9059cbb60e01b81526001600160a01b0395909516600486015260248501529183916044918391905af1908115610321575f916105f7575b50156105b2577f0daf449977d5acafa35195e10b3eb92f97839892a6653afaba222379b58d8a9b6020600154604051908152a2565b60405162461bcd60e51b815260206004820152601760248201527f4661756365743a207472616e73666572206661696c65640000000000000000006044820152606490fd5b610610915060203d60201161031a5761030c818361040c565b5f61057d565b60405162461bcd60e51b815260206004820152601c60248201527f4661756365743a20696e73756666696369656e742062616c616e6365000000006044820152606490fd5b90506020813d602011610685575b816106766020938361040c565b810103126100c457515f610523565b3d9150610669565b60405162461bcd60e51b815260206004820152601760248201527f4661756365743a20636f6f6c646f776e206163746976650000000000000000006044820152606490fd5b50815f52600360205260405f205460025481018091116106f4574210156104c9565b634e487b7160e01b5f52601160045260245ffd diff --git a/pkg/blockchain/evm/artifacts/MockERC20.abi b/pkg/blockchain/evm/artifacts/MockERC20.abi new file mode 100644 index 0000000..d777b6b --- /dev/null +++ b/pkg/blockchain/evm/artifacts/MockERC20.abi @@ -0,0 +1,258 @@ +[ + { + "type": "constructor", + "inputs": [ + { + "name": "_name", + "type": "string", + "internalType": "string" + }, + { + "name": "_symbol", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "allowance", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "approve", + "inputs": [ + { + "name": "spender", + "type": "address", + "internalType": "address" + }, + { + "name": "value", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "balanceOf", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "decimals", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint8", + "internalType": "uint8" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "mint", + "inputs": [ + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "name", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "symbol", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "totalSupply", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "transfer", + "inputs": [ + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "value", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "transferFrom", + "inputs": [ + { + "name": "from", + "type": "address", + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "value", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "event", + "name": "Approval", + "inputs": [ + { + "name": "owner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "spender", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "value", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Transfer", + "inputs": [ + { + "name": "from", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "value", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + } +] diff --git a/pkg/blockchain/evm/artifacts/MockERC20.bin b/pkg/blockchain/evm/artifacts/MockERC20.bin new file mode 100644 index 0000000..96ad9e2 --- /dev/null +++ b/pkg/blockchain/evm/artifacts/MockERC20.bin @@ -0,0 +1 @@ +0x60806040523461032e576109b48038038061001981610332565b92833981019060408183031261032e5780516001600160401b03811161032e5782610045918301610357565b60208201519092906001600160401b03811161032e576100659201610357565b6002805460ff1916601217905581516001600160401b038111610239575f54600181811c91168015610324575b602082101461021b57601f81116102b7575b50602092601f821160011461025857928192935f9261024d575b50508160011b915f199060031b1c1916175f555b80516001600160401b03811161023957600154600181811c9116801561022f575b602082101461021b57601f81116101ad575b50602091601f821160011461014d579181925f92610142575b50508160011b915f199060031b1c1916176001555b60405161060b90816103a98239f35b015190505f8061011e565b601f1982169260015f52805f20915f5b8581106101955750836001951061017d575b505050811b01600155610133565b01515f1960f88460031b161c191690555f808061016f565b9192602060018192868501518155019401920161015d565b818111156101055760015f52601f820160051c7fb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf660208410610213575b81601f9101920160051c03905f5b828110610206575050610105565b5f828201556001016101f8565b5f91506101ea565b634e487b7160e01b5f52602260045260245ffd5b90607f16906100f3565b634e487b7160e01b5f52604160045260245ffd5b015190505f806100be565b601f198216935f8052805f20915f5b86811061029f5750836001959610610287575b505050811b015f556100d2565b01515f1960f88460031b161c191690555f808061027a565b91926020600181928685015181550194019201610267565b818111156100a4575f8052601f820160051c7f290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e5636020841061031c575b81601f9101920160051c03905f5b82811061030f5750506100a4565b5f82820155600101610301565b5f91506102f3565b90607f1690610092565b5f80fd5b6040519190601f01601f191682016001600160401b0381118382101761023957604052565b81601f8201121561032e578051906001600160401b03821161023957610386601f8301601f1916602001610332565b928284526020838301011161032e57815f9260208093018386015e830101529056fe60806040526004361015610011575f80fd5b5f3560e01c806306fdde03146104b5578063095ea7b31461043c57806318160ddd1461041f57806323b872dd1461035a578063313ce5671461033a57806340c10f19146102c057806370a082311461028857806395d89b411461016a578063a9059cbb146100db5763dd62ed3e14610087575f80fd5b346100d75760403660031901126100d7576100a06105b1565b6100a86105c7565b6001600160a01b039182165f908152600560209081526040808320949093168252928352819020549051908152f35b5f80fd5b346100d75760403660031901126100d7576100f46105b1565b60243590335f52600460205260405f2061010f8382546105dd565b905560018060a01b031690815f52600460205260405f206101318282546105fe565b90556040519081527fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef60203392a3602060405160018152f35b346100d7575f3660031901126100d7576040515f6001548060011c9060018116801561027e575b60208310811461026a5782855290811561024e57506001146101f8575b50819003601f01601f191681019067ffffffffffffffff8211818310176101e457604082905281906101e09082610587565b0390f35b634e487b7160e01b5f52604160045260245ffd5b60015f9081529091507fb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf65b828210610238575060209150820101826101ae565b6001816020925483858801015201910190610223565b90506020925060ff191682840152151560051b820101826101ae565b634e487b7160e01b5f52602260045260245ffd5b91607f1691610191565b346100d75760203660031901126100d7576001600160a01b036102a96105b1565b165f526004602052602060405f2054604051908152f35b346100d75760403660031901126100d7576102d96105b1565b5f7fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef60206024359360018060a01b03169384845260048252604084206103208282546105fe565b905561032e816003546105fe565b600355604051908152a3005b346100d7575f3660031901126100d757602060ff60025416604051908152f35b346100d75760603660031901126100d7576103736105b1565b61037b6105c7565b7fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef60206044359360018060a01b031692835f526005825260405f2060018060a01b0333165f52825260405f206103d28682546105dd565b9055835f526004825260405f206103ea8682546105dd565b905560018060a01b031693845f526004825260405f2061040b8282546105fe565b9055604051908152a3602060405160018152f35b346100d7575f3660031901126100d7576020600354604051908152f35b346100d75760403660031901126100d7576104556105b1565b335f8181526005602090815260408083206001600160a01b03909516808452948252918290206024359081905591519182527f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92591a3602060405160018152f35b346100d7575f3660031901126100d7576040515f5f548060011c9060018116801561057d575b60208310811461026a5782855290811561024e57506001146105295750819003601f01601f191681019067ffffffffffffffff8211818310176101e457604082905281906101e09082610587565b5f8080529091507f290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e5635b828210610567575060209150820101826101ae565b6001816020925483858801015201910190610552565b91607f16916104db565b602060409281835280519182918282860152018484015e5f828201840152601f01601f1916010190565b600435906001600160a01b03821682036100d757565b602435906001600160a01b03821682036100d757565b919082039182116105ea57565b634e487b7160e01b5f52601160045260245ffd5b919082018092116105ea5756 diff --git a/pkg/blockchain/evm/artifacts/NodeID.abi b/pkg/blockchain/evm/artifacts/NodeID.abi new file mode 100644 index 0000000..650a6d3 --- /dev/null +++ b/pkg/blockchain/evm/artifacts/NodeID.abi @@ -0,0 +1,778 @@ +[ + { + "type": "constructor", + "inputs": [ + { + "name": "_owner", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "MAX_NODES", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "approve", + "inputs": [ + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "availableSlots", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "balanceOf", + "inputs": [ + { + "name": "owner", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "baseTokenURI", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getApproved", + "inputs": [ + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "isApprovedForAll", + "inputs": [ + { + "name": "owner", + "type": "address", + "internalType": "address" + }, + { + "name": "operator", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "mint", + "inputs": [ + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "minActivationAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "vestingPeriod", + "type": "uint64", + "internalType": "uint64" + } + ], + "outputs": [ + { + "name": "tokenId", + "type": "uint32", + "internalType": "uint32" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "mintBatch", + "inputs": [ + { + "name": "recipients", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "minActivationAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "vestingPeriod", + "type": "uint64", + "internalType": "uint64" + } + ], + "outputs": [ + { + "name": "firstTokenId", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "lastTokenId", + "type": "uint32", + "internalType": "uint32" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "mintToRegistry", + "inputs": [ + { + "name": "minActivationAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "vestingPeriod", + "type": "uint64", + "internalType": "uint64" + } + ], + "outputs": [ + { + "name": "tokenId", + "type": "uint32", + "internalType": "uint32" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "name", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "owner", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "ownerOf", + "inputs": [ + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "registry", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "safeTransferFrom", + "inputs": [ + { + "name": "from", + "type": "address", + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "safeTransferFrom", + "inputs": [ + { + "name": "from", + "type": "address", + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setApprovalForAll", + "inputs": [ + { + "name": "operator", + "type": "address", + "internalType": "address" + }, + { + "name": "approved", + "type": "bool", + "internalType": "bool" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setBaseTokenURI", + "inputs": [ + { + "name": "baseURI", + "type": "string", + "internalType": "string" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setRegistry", + "inputs": [ + { + "name": "newRegistry", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "supportsInterface", + "inputs": [ + { + "name": "interfaceId", + "type": "bytes4", + "internalType": "bytes4" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "symbol", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "termsOf", + "inputs": [ + { + "name": "tokenId", + "type": "uint32", + "internalType": "uint32" + } + ], + "outputs": [ + { + "name": "", + "type": "tuple", + "internalType": "struct NodeID.Terms", + "components": [ + { + "name": "mintedAt", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "vestingPeriod", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "minActivationAmount", + "type": "uint256", + "internalType": "uint256" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "tokenURI", + "inputs": [ + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "transferFrom", + "inputs": [ + { + "name": "from", + "type": "address", + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "transferOwnership", + "inputs": [ + { + "name": "newOwner", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "event", + "name": "Approval", + "inputs": [ + { + "name": "owner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "approved", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ApprovalForAll", + "inputs": [ + { + "name": "owner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "operator", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "approved", + "type": "bool", + "indexed": false, + "internalType": "bool" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnershipTransferred", + "inputs": [ + { + "name": "oldOwner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newOwner", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RegistryUpdated", + "inputs": [ + { + "name": "oldRegistry", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newRegistry", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "SlotsMinted", + "inputs": [ + { + "name": "by", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "firstTokenId", + "type": "uint32", + "indexed": true, + "internalType": "uint32" + }, + { + "name": "lastTokenId", + "type": "uint32", + "indexed": true, + "internalType": "uint32" + }, + { + "name": "minActivationAmount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "vestingPeriod", + "type": "uint64", + "indexed": false, + "internalType": "uint64" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Transfer", + "inputs": [ + { + "name": "from", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "AllSlotsMinted", + "inputs": [] + }, + { + "type": "error", + "name": "DirectRegistryTransfer", + "inputs": [] + }, + { + "type": "error", + "name": "ERC721IncorrectOwner", + "inputs": [ + { + "name": "sender", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "owner", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ERC721InsufficientApproval", + "inputs": [ + { + "name": "operator", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "ERC721InvalidApprover", + "inputs": [ + { + "name": "approver", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ERC721InvalidOperator", + "inputs": [ + { + "name": "operator", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ERC721InvalidOwner", + "inputs": [ + { + "name": "owner", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ERC721InvalidReceiver", + "inputs": [ + { + "name": "receiver", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ERC721InvalidSender", + "inputs": [ + { + "name": "sender", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ERC721NonexistentToken", + "inputs": [ + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "NotOwner", + "inputs": [] + }, + { + "type": "error", + "name": "NotRegistry", + "inputs": [] + }, + { + "type": "error", + "name": "ZeroAddress", + "inputs": [] + }, + { + "type": "error", + "name": "ZeroCount", + "inputs": [] + } +] diff --git a/pkg/blockchain/evm/artifacts/NodeID.bin b/pkg/blockchain/evm/artifacts/NodeID.bin new file mode 100644 index 0000000..ffaef8f --- /dev/null +++ b/pkg/blockchain/evm/artifacts/NodeID.bin @@ -0,0 +1 @@ +0x60806040523461037a57611b496020813803918261001c8161037e565b93849283398101031261037a57516001600160a01b0381169081900361037a57610046604061037e565b90600e82526d10db19585c939bd9194814db1bdd60921b602083015261006c604061037e565b600681526510d394d313d560d21b602082015282519091906001600160401b038111610283575f54600181811c91168015610370575b602082101461026557601f8111610303575b506020601f82116001146102a257819293945f92610297575b50508160011b915f199060031b1c1916175f555b81516001600160401b03811161028357600154600181811c91168015610279575b602082101461026557601f81116101f7575b50602092601f821160011461019657928192935f9261018b575b50508160011b915f199060031b1c1916176001555b600163ffffffff196009541617600955801561017c57600680546001600160a01b0319169190911790556040516117a590816103a48239f35b63d92e233d60e01b5f5260045ffd5b015190505f8061012e565b601f1982169360015f52805f20915f5b8681106101df57508360019596106101c7575b505050811b01600155610143565b01515f1960f88460031b161c191690555f80806101b9565b919260206001819286850151815501940192016101a6565b818111156101145760015f52601f820160051c7fb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf66020841061025d575b81601f9101920160051c03905f5b828110610250575050610114565b5f82820155600101610242565b5f9150610234565b634e487b7160e01b5f52602260045260245ffd5b90607f1690610102565b634e487b7160e01b5f52604160045260245ffd5b015190505f806100cd565b601f198216905f8052805f20915f5b8181106102eb575095836001959697106102d3575b505050811b015f556100e1565b01515f1960f88460031b161c191690555f80806102c6565b9192602060018192868b0151815501940192016102b1565b818111156100b4575f8052601f820160051c7f290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e56360208410610368575b81601f9101920160051c03905f5b82811061035b5750506100b4565b5f8282015560010161034d565b5f915061033f565b90607f16906100a2565b5f80fd5b6040519190601f01601f191682016001600160401b038111838210176102835760405256fe6080806040526004361015610012575f80fd5b5f3560e01c90816301ffc9a714610d3b5750806306fdde0314610c99578063081812fc14610c5d578063095ea7b314610b7357806323b872dd14610b5c57806330176e13146109f657806342842e0e146109cd5780636352211e1461099d57806370a082311461094c5780637b103999146109245780638b3d35ae1461088e5780638da5cb5b146108665780638eeda103146107cc5780638f16e1cd146107af57806395d89b411461070a578063a22cb4651461066f578063a91ee0dc146105fc578063b88d4fde14610573578063c87b56dd14610554578063d547cfb714610485578063e6fb38131461042a578063e8804a2b1461033f578063e985e9c5146102e8578063f2fde38b146102665763ff875f031461012f575f80fd5b3461024e57606036600319011261024e576004356001600160401b03811161024e573660238201121561024e578060040135906001600160401b038211610252578160051b9060208201926101876040519485610e61565b8352602460208401928201019036821161024e57602401915b81831061022e57604063ffffffff61021f60243582817f9902251bf2894876d6d1dc26c5e2005e75018334706538cb7bf283598aefc42b6101f38b6101e3610e30565b9586916101ee6114e5565b611515565b93169586931694859488519182913395839092916001600160401b036020916040840195845216910152565b0390a482519182526020820152f35b82356001600160a01b038116810361024e578152602092830192016101a0565b5f80fd5b634e487b7160e01b5f52604160045260245ffd5b3461024e57602036600319011261024e5761027f610dca565b6102876114e5565b6001600160a01b031680156102d957600680546001600160a01b0319811683179091556001600160a01b03167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e05f80a3005b63d92e233d60e01b5f5260045ffd5b3461024e57604036600319011261024e57610301610dca565b610309610de0565b9060018060a01b03165f52600560205260405f209060018060a01b03165f52602052602060ff60405f2054166040519015158152f35b3461024e57604036600319011261024e576004356024356001600160401b038116810361024e576007546001600160a01b031633811480610421575b15610412576104097f9902251bf2894876d6d1dc26c5e2005e75018334706538cb7bf283598aefc42b9260209463ffffffff6103df83836040978851906103c28a83610e61565b60018252601f198a01368d8401376103d9826110d6565b52611515565b5016948593849386519182913395839092916001600160401b036020916040840195845216910152565b0390a451908152f35b633217675b60e21b5f5260045ffd5b5080151561037b565b3461024e575f36600319011261024e575f1963ffffffff600954160163ffffffff81116104715763ffffffff16620100000362010000811161047157602090604051908152f35b634e487b7160e01b5f52601160045260245ffd5b3461024e575f36600319011261024e576040515f6008546104a581610e9d565b808452906001811690811561053057506001146104e5575b6104e1836104cd81850382610e61565b604051918291602083526020830190610da6565b0390f35b60085f9081525f5160206117855f395f51905f52939250905b808210610516575090915081016020016104cd6104bd565b9192600181602092548385880101520191019092916104fe565b60ff191660208086019190915291151560051b840190910191506104cd90506104bd565b3461024e57602036600319011261024e576104e16104cd60043561124b565b3461024e57608036600319011261024e5761058c610dca565b610594610de0565b606435916001600160401b03831161024e573660238401121561024e578260040135916105c083610e82565b926105ce6040519485610e61565b808452366024828701011161024e576020815f9260246105fa980183880137850101526044359161110b565b005b3461024e57602036600319011261024e57610615610dca565b61061d6114e5565b6001600160a01b031680156102d957600780546001600160a01b0319811683179091556001600160a01b03167f482b97c53e48ffa324a976e2738053e9aff6eee04d8aac63b10e19411d869b825f80a3005b3461024e57604036600319011261024e57610688610dca565b6024359081151580920361024e576001600160a01b03169081156106f757335f52600560205260405f20825f5260205260405f2060ff1981541660ff83161790556040519081527f17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c3160203392a3005b50630b61174360e31b5f5260045260245ffd5b3461024e575f36600319011261024e576040515f60015461072a81610e9d565b80845290600181169081156105305750600114610751576104e1836104cd81850382610e61565b60015f9081527fb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6939250905b808210610795575090915081016020016104cd6104bd565b91926001816020925483858801015201910190929161077d565b3461024e575f36600319011261024e576020604051620100008152f35b3461024e57602036600319011261024e5760043563ffffffff811680910361024e575f604080516107fc81610e46565b8281528260208201520152610810816114b1565b505f52600a602052606060405f2060405161082a81610e46565b6001600160401b0382546040600183831695868652846020870194841c1684520154930192835260405193845251166020830152516040820152f35b3461024e575f36600319011261024e576006546040516001600160a01b039091168152602090f35b3461024e57606036600319011261024e5760207f9902251bf2894876d6d1dc26c5e2005e75018334706538cb7bf283598aefc42b6108ca610dca565b6104096024356108d8610e30565b906108e16114e5565b63ffffffff6103df83836040978851906108fb8a83610e61565b60018252601f198a01368d840137610912826110d6565b6001600160a01b039091169052611515565b3461024e575f36600319011261024e576007546040516001600160a01b039091168152602090f35b3461024e57602036600319011261024e576001600160a01b0361096d610dca565b16801561098a575f526003602052602060405f2054604051908152f35b6322718ad960e21b5f525f60045260245ffd5b3461024e57602036600319011261024e5760206109bb6004356114b1565b6040516001600160a01b039091168152f35b3461024e576105fa6109de36610df6565b90604051926109ee602085610e61565b5f845261110b565b3461024e57602036600319011261024e576004356001600160401b03811161024e573660238201121561024e5780600401356001600160401b03811161024e57366024828401011161024e57610a4a6114e5565b610a55600854610e9d565b601f8111610b05575b505f601f8211600114610a9a5781925f92610a8c575b50505f19600383901b1c191660019190911b17600855005b602492500101358280610a74565b601f198216925f5160206117855f395f51905f52915f5b858110610aea57508360019510610ace575b505050811b01600855005b01602401355f19600384901b60f8161c19169055828080610ac3565b90926020600181926024878701013581550194019101610ab1565b81811115610a5e57601f820160051c9060208310610b54575b601f82910160051c03905f5b828110610b38575050610a5e565b5f8282015f5160206117855f395f51905f520155600101610b2a565b5f9150610b1e565b3461024e576105fa610b6d36610df6565b91610ed5565b3461024e57604036600319011261024e57610b8c610dca565b602435610b98816114b1565b33151580610c4a575b80610c1d575b610c0a5781906001600160a01b0384811691167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9255f80a45f90815260046020526040902080546001600160a01b0319166001600160a01b03909216919091179055005b63a9fbf51f60e01b5f523360045260245ffd5b506001600160a01b0381165f90815260056020908152604080832033845290915290205460ff1615610ba7565b506001600160a01b038116331415610ba1565b3461024e57602036600319011261024e57600435610c7a816114b1565b505f526004602052602060018060a01b0360405f205416604051908152f35b3461024e575f36600319011261024e576040515f5f54610cb881610e9d565b80845290600181169081156105305750600114610cdf576104e1836104cd81850382610e61565b5f8080527f290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563939250905b808210610d21575090915081016020016104cd6104bd565b919260018160209254838588010152019101909291610d09565b3461024e57602036600319011261024e576004359063ffffffff60e01b821680920361024e576020916380ac58cd60e01b8114908115610d95575b8115610d84575b5015158152f35b6301ffc9a760e01b14905083610d7d565b635b5e139f60e01b81149150610d76565b805180835260209291819084018484015e5f828201840152601f01601f1916010190565b600435906001600160a01b038216820361024e57565b602435906001600160a01b038216820361024e57565b606090600319011261024e576004356001600160a01b038116810361024e57906024356001600160a01b038116810361024e579060443590565b604435906001600160401b038216820361024e57565b606081019081106001600160401b0382111761025257604052565b90601f801991011681019081106001600160401b0382111761025257604052565b6001600160401b03811161025257601f01601f191660200190565b90600182811c92168015610ecb575b6020831014610eb757565b634e487b7160e01b5f52602260045260245ffd5b91607f1691610eac565b6001600160a01b03909116919082156110c3575f828152600260205260409020546001600160a01b03161515806110af575b8061109a575b61108b575f828152600260205260409020546001600160a01b031692829033151580610ff6575b5084610fc3575b805f52600360205260405f2060018154019055815f52600260205260405f20816bffffffffffffffffffffffff60a01b825416179055847fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef5f80a46001600160a01b0316808303610fab57505050565b6364283d7b60e01b5f5260045260245260445260645ffd5b5f82815260046020526040902080546001600160a01b0319169055845f52600360205260405f205f198154019055610f3b565b9091508061103a575b1561100c5782905f610f34565b828461102457637e27328960e01b5f5260045260245ffd5b63177e802f60e01b5f523360045260245260445ffd5b503384148015611069575b80610fff57505f838152600460205260409020546001600160a01b03163314610fff565b505f84815260056020908152604080832033845290915290205460ff16611045565b63588e7fef60e11b5f5260045ffd5b506007546001600160a01b0316331415610f0d565b506007546001600160a01b03168314610f07565b633250574960e11b5f525f60045260245ffd5b8051156110e35760200190565b634e487b7160e01b5f52603260045260245ffd5b80518210156110e35760209160051b010190565b9291611118818386610ed5565b813b611125575b50505050565b604051630a85bd0160e11b81523360048201526001600160a01b0394851660248201526044810191909152608060648201529216919060209082908190611170906084830190610da6565b03815f865af15f9181611206575b506111d357503d156111cc573d61119481610e82565b906111a26040519283610e61565b81523d5f602083013e5b805190816111c75782633250574960e11b5f5260045260245ffd5b602001fd5b60606111ac565b6001600160e01b03191663757a42ff60e11b016111f457505f80808061111f565b633250574960e11b5f5260045260245ffd5b9091506020813d602011611243575b8161122260209383610e61565b8101031261024e57516001600160e01b03198116810361024e57905f61117e565b3d9150611215565b611254816114b1565b506008549061126282610e9d565b1561149b5780815f9272184f03e93ff9f4daa797ed6e38ed64bf6a1f0160401b811015611475575b50806d04ee2d6d415b85acef8100000000600a92101561145a575b662386f26fc10000811015611446575b6305f5e100811015611435575b612710811015611426575b6064811015611418575b101561140e575b6001820190600a60216113096112f385610e82565b946113016040519687610e61565b808652610e82565b602085019590601f19013687378401015b5f1901916f181899199a1a9b1b9c1cb0b131b232b360811b8282061a835304801561134857600a909161131a565b50506040519283915f9161135b81610e9d565b90600181169081156113ea575060011461139d575b50926005929161139a94518092825e0164173539b7b760d91b815203601a19810184520182610e61565b90565b90915060085f525f5160206117855f395f51905f525f905b8282106113cc57505082016020019061139a611370565b60209192939450806001915483858a010152019101859392916113b5565b60ff19166020808701919091528215159092028501909101925061139a9050611370565b90600101906112de565b6064600291049301926112d7565b612710600491049301926112cd565b6305f5e100600891049301926112c2565b662386f26fc10000601091049301926112b5565b6d04ee2d6d415b85acef8100000000602091049301926112a5565b6040935072184f03e93ff9f4daa797ed6e38ed64bf6a1f0160401b90049050600a61128a565b50506040516114ab602082610e61565b5f815290565b5f818152600260205260409020546001600160a01b03169081156114d3575090565b637e27328960e01b5f5260045260245ffd5b6006546001600160a01b031633036114f957565b6330cd747160e01b5f5260045ffd5b9190820180921161047157565b9291928051156117755763ffffffff6009541691611534825184611508565b925f19840184811161047157620100008111611766579394426001600160401b031694905f5b8551811015611745576001600160a01b0361157582886110f7565b5116156102d95763ffffffff61158b8286611508565b16906001600160a01b0361159f82896110f7565b511680156110c3575f838152600260205260409020546001600160a01b0316151580611731575b8061171c575b61108b575f838152600260205260409020546001600160a01b0316801515918490836116e9575b5f818152600360209081526040808320805460010190558483526002909152812080546001600160a01b0319166001600160a01b03841617905583907fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9080a4506116d657600191826040519161166983610e46565b8a83528c6001600160401b03602085019116815260408401918a83525f52600a6020526001600160401b0360405f209451166fffffffffffffffff00000000000000008554925160401b16916fffffffffffffffffffffffffffffffff191617178355519101550161155a565b6339e3563760e11b5f525f60045260245ffd5b5f82815260046020526040902080546001600160a01b0319169055825f52600360205260405f205f1981540190556115f3565b506007546001600160a01b03163314156115cc565b506007546001600160a01b031681146115c6565b5095919350955063ffffffff93508391501682196009541617600955921690565b6304710b1360e11b5f5260045ffd5b63011ee73b60e21b5f5260045ffdfef3f7a9fe364faab93b216da50a3214154f22a0a2b415b23a84c8169e8b636ee3 diff --git a/pkg/blockchain/evm/artifacts/README.md b/pkg/blockchain/evm/artifacts/README.md new file mode 100644 index 0000000..33f5b6e --- /dev/null +++ b/pkg/blockchain/evm/artifacts/README.md @@ -0,0 +1,58 @@ +# EVM contract artifacts + +Vendored **ABI + deploy bytecode** for the EVM contracts this package binds — +`.abi` (interface JSON) and `.bin` (deploy bytecode hex). +They are the source of truth for the generated `../*_abi.go` bindings: the +`abi_refresher` command reads these files and emits the bindings, so binding +regeneration needs **no Solidity source, no forge, no jq** in this repo. + +The `.abi` is the contract's wire interface — a change here is a reviewable +diff. The `.bin` is kept so `Deploy*` helpers work (clearnet's devnet/tests and +the integration tests deploy `Custody`); this package itself only calls/reads. + +## Regenerate the bindings (common case) + +After editing nothing but bumping the SDK, or after refreshing the files below: + +```sh +make generate # go generate ./... → runs abi_refresher (+ cbor-gen) +``` + +or directly: + +```sh +go generate ./pkg/blockchain/evm/... +``` + +This rewrites `../*_abi.go` from the `.abi`/`.bin` here. Commit the result. + +## Refresh the .abi / .bin (only when a contract changes) + +The files are produced by `forge build` in a repo that owns the Solidity +source (clearnet for Registry/YellowToken/etc.; custody for Custody). Build +there, then extract abi + bytecode into this directory. Foundry nests output +by source-file name, so the contract→json mapping matters (note YellowToken +lives in `Token.sol`): + +```sh +OUT=../clearnet/contracts/evm/out # a `forge build` output tree +DEST=pkg/blockchain/evm/artifacts # this directory, from repo root + +while read name json; do + jq -r '.abi' "$OUT/$json" > "$DEST/$name.abi" + jq -r '.bytecode.object' "$OUT/$json" > "$DEST/$name.bin" +done <<'EOF' +Slasher Slasher.sol/Slasher.json +Registry Registry.sol/Registry.json +MockERC20 MockERC20.sol/MockERC20.json +Custody Custody.sol/Custody.json +NodeID NodeID.sol/NodeID.json +Faucet Faucet.sol/Faucet.json +YellowToken Token.sol/YellowToken.json +EOF + +make generate # regenerate bindings from the refreshed files +``` + +Review the resulting `.abi`/`.bin` and `*_abi.go` diffs together — an ABI +change without a corresponding intentional code change is a red flag. diff --git a/pkg/blockchain/evm/artifacts/Registry.abi b/pkg/blockchain/evm/artifacts/Registry.abi new file mode 100644 index 0000000..d1c4eec --- /dev/null +++ b/pkg/blockchain/evm/artifacts/Registry.abi @@ -0,0 +1,925 @@ +[ + { + "type": "constructor", + "inputs": [ + { + "name": "nodeID", + "type": "address", + "internalType": "address" + }, + { + "name": "asset", + "type": "address", + "internalType": "address" + }, + { + "name": "networkId", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "unbondingPeriod", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "basePrice", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "targetPrice", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "owner_", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "ASSET", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract IERC20" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "BASE_PRICE", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "MAX_NODES", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "NETWORK_ID", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "NODE_ID", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "TARGET_PRICE", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "UNBONDING_PERIOD", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint64", + "internalType": "uint64" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "WARMUP_WINDOW", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint64", + "internalType": "uint64" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "activate", + "inputs": [ + { + "name": "tokenId", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "blsPubkeyG1", + "type": "uint256[2]", + "internalType": "uint256[2]" + }, + { + "name": "blsPubkeyG2", + "type": "uint256[4]", + "internalType": "uint256[4]" + }, + { + "name": "operatorCollateral", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "nodeId", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "activeCount", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint32", + "internalType": "uint32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "floorPrice", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "fund", + "inputs": [ + { + "name": "tokenId", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "getNodeByBlsG2Hash", + "inputs": [ + { + "name": "blsG2Hash", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getNodeById", + "inputs": [ + { + "name": "nodeId", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [ + { + "name": "", + "type": "tuple", + "internalType": "struct NodeRecord", + "components": [ + { + "name": "operator", + "type": "address", + "internalType": "address" + }, + { + "name": "activatedAt", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "deactivatedAt", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "vestedAt", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "index", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "tokenId", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "operatorCollateral", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "sponsorCollateral", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "blsPubkeyG1", + "type": "uint256[2]", + "internalType": "uint256[2]" + }, + { + "name": "blsPubkeyG2", + "type": "uint256[4]", + "internalType": "uint256[4]" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getNodeId", + "inputs": [ + { + "name": "tokenId", + "type": "uint32", + "internalType": "uint32" + } + ], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getNodeIds", + "inputs": [ + { + "name": "offset", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "limit", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bytes32[]", + "internalType": "bytes32[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getNodes", + "inputs": [ + { + "name": "offset", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "limit", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "tuple[]", + "internalType": "struct NodeRecord[]", + "components": [ + { + "name": "operator", + "type": "address", + "internalType": "address" + }, + { + "name": "activatedAt", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "deactivatedAt", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "vestedAt", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "index", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "tokenId", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "operatorCollateral", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "sponsorCollateral", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "blsPubkeyG1", + "type": "uint256[2]", + "internalType": "uint256[2]" + }, + { + "name": "blsPubkeyG2", + "type": "uint256[4]", + "internalType": "uint256[4]" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "liability", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "owner", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "register", + "inputs": [ + { + "name": "blsPubkeyG1", + "type": "uint256[2]", + "internalType": "uint256[2]" + }, + { + "name": "blsPubkeyG2", + "type": "uint256[4]", + "internalType": "uint256[4]" + }, + { + "name": "operatorCollateral", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "tokenId", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "nodeId", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "release", + "inputs": [ + { + "name": "tokenId", + "type": "uint32", + "internalType": "uint32" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setSlasher", + "inputs": [ + { + "name": "newSlasher", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "slash", + "inputs": [ + { + "name": "nodeId", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "recipient", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "slasher", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "totalNodes", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "transferOwnership", + "inputs": [ + { + "name": "newOwner", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "unlock", + "inputs": [ + { + "name": "tokenId", + "type": "uint32", + "internalType": "uint32" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "event", + "name": "NodeActivated", + "inputs": [ + { + "name": "operator", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "nodeId", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "tokenId", + "type": "uint32", + "indexed": true, + "internalType": "uint32" + }, + { + "name": "collateral", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "vestedAt", + "type": "uint64", + "indexed": false, + "internalType": "uint64" + }, + { + "name": "blsPubkeyG2", + "type": "uint256[4]", + "indexed": false, + "internalType": "uint256[4]" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "NodeFunded", + "inputs": [ + { + "name": "payer", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "nodeId", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "tokenId", + "type": "uint32", + "indexed": true, + "internalType": "uint32" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "totalCollateral", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "NodeReleased", + "inputs": [ + { + "name": "operator", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "nodeId", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "tokenId", + "type": "uint32", + "indexed": true, + "internalType": "uint32" + }, + { + "name": "collateral", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "NodeUnlocked", + "inputs": [ + { + "name": "operator", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "nodeId", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "availableAt", + "type": "uint64", + "indexed": false, + "internalType": "uint64" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnershipTransferred", + "inputs": [ + { + "name": "oldOwner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newOwner", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Slashed", + "inputs": [ + { + "name": "nodeId", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "recipient", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "fromOperator", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "fromSponsor", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "SlasherUpdated", + "inputs": [ + { + "name": "oldSlasher", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newSlasher", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "ActivationBelowFloor", + "inputs": [ + { + "name": "floor", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "provided", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "ActivationBelowMinimum", + "inputs": [ + { + "name": "minimum", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "provided", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "BlsKeyAlreadyRegistered", + "inputs": [] + }, + { + "type": "error", + "name": "BlsKeyMismatch", + "inputs": [] + }, + { + "type": "error", + "name": "InsufficientCollateral", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidPriceRange", + "inputs": [] + }, + { + "type": "error", + "name": "NodeNotActive", + "inputs": [] + }, + { + "type": "error", + "name": "NotNftOwner", + "inputs": [] + }, + { + "type": "error", + "name": "NotOwner", + "inputs": [] + }, + { + "type": "error", + "name": "NotSlasher", + "inputs": [] + }, + { + "type": "error", + "name": "ReentrancyGuardReentrantCall", + "inputs": [] + }, + { + "type": "error", + "name": "ReleaseNotAvailable", + "inputs": [ + { + "name": "availableAt", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "SafeERC20FailedOperation", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "SlotNotInactive", + "inputs": [] + }, + { + "type": "error", + "name": "ZeroAddress", + "inputs": [] + }, + { + "type": "error", + "name": "ZeroAmount", + "inputs": [] + }, + { + "type": "error", + "name": "ZeroBlsPubkey", + "inputs": [] + } +] diff --git a/pkg/blockchain/evm/artifacts/Registry.bin b/pkg/blockchain/evm/artifacts/Registry.bin new file mode 100644 index 0000000..63212f1 --- /dev/null +++ b/pkg/blockchain/evm/artifacts/Registry.bin @@ -0,0 +1 @@ +0x6101403461025657601f612fc538819003918201601f19168301916001600160401b0383118484101761025a5780849260e094604052833981010312610256576100488161026e565b906100556020820161026e565b604082015160608301516001600160401b038116949091908583036102565760808501519361008b60c060a0880151970161026e565b60017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055916001600160a01b0316908115610247576001600160a01b0316918215610247576001600160a01b031696871561024757156101f157841515806101e7575b156101d85760a05260c05260e05260805261010052610120525f80546001600160a01b031916919091179055604051612d42908161028382396080518181816108b501528181610f330152611013015260a051818181610a8001528181610d67015281816110e9015281816112430152818161211801526124bf015260c05181818161047b01528181610650015281816107ea015281816111c0015281816122d2015261236a015260e05181818161059f0152818161206f01526124470152610100518181816113100152611b070152610120518181816109ae01528181611b290152611b7e0152f35b6323f5f0b960e11b5f5260045ffd5b50848610156100ee565b60405162461bcd60e51b815260206004820152602860248201527f52656769737472793a20756e626f6e64696e67506572696f642063616e6e6f74604482015267206265207a65726f60c01b6064820152608490fd5b63d92e233d60e01b5f5260045ffd5b5f80fd5b634e487b7160e01b5f52604160045260245ffd5b51906001600160a01b03821682036102565756fe60806040526004361015610011575f80fd5b5f3560e01c8063038d67e8146101c45780630d420090146101bf578063158ece27146101ba57806318071936146101b557806328ce8b31146101b05780632db75d40146101ab5780634331ed1f146101a65780634800d97f146101a157806367d93c811461019c5780636ef67bae14610197578063705727b5146101925780637e99ce591461018d5780637fdd1867146101885780638899cf50146101835780638da5cb5b1461017e5780638f16e1cd146101795780639363c812146101745780639592d4241461016f578063aabc24961461016a578063ad12211114610165578063b134427114610160578063bdc43e921461015b578063d9a912ec14610156578063dda42b3714610151578063ef695be81461014c578063f2fde38b146101475763f86325ed14610142575f80fd5b6112f9565b611272565b61122e565b610f57565b610f14565b610eb1565b610e89565b610d07565b610c7f565b610c62565b610c23565b610c06565b610bdf565b610b9d565b610a05565b610997565b61097a565b610941565b610819565b6107d5565b6107af565b6105d0565b610588565b61055e565b610368565b61033b565b6102be565b905f905b600282106101da57505050565b60208060019285518152019301910190916101cd565b905f905b6004821061020157505050565b60208060019285518152019301910190916101f4565b80516001600160a01b031682526102bc919061014090610120906020818101516001600160401b0316908501526040818101516001600160401b0316908501526060818101516001600160401b03169085015260808181015163ffffffff169085015260a08181015163ffffffff169085015260c081015160c085015260e081015160e08501526102b26101008201516101008601906101c9565b01519101906101f0565b565b3461032d57604036600319011261032d576102dd60243560043561170d565b6040518091602082016020835281518091526020604084019201905f5b818110610308575050500390f35b9193509160206101c08261031f6001948851610217565b0194019101918493926102fa565b5f80fd5b5f91031261032d57565b3461032d575f36600319011261032d576020604051610e108152f35b6001600160a01b0381160361032d57565b3461032d57606036600319011261032d576024356004357f9a2bb3d9059142feaf2a6cbf5062a0047437076519c62ede693c2ec2240f13336044356103ac81610357565b6103b4611ac2565b6001546103cb906001600160a01b031633146117be565b6103d68415156117d4565b6103e8835f52600360205260405f2090565b60028101918254926003830192610413610403855487611544565b8015159081610553575b506117ea565b5f9185891161052c576104c59394955088956104308a8354611551565b82555b6104476104428b600254611551565b600255565b6001600160401b0361046360018501546001600160401b031690565b161591826104ef575b50506104e0575b5061049f87847f0000000000000000000000000000000000000000000000000000000000000000611bf6565b60405193849360018060a01b031697846040919493926060820195825260208201520152565b0390a36104de60015f516020612d225f395f51905f5255565b005b6104e990611ba0565b5f610473565b6104fd925054905490611544565b61052461051f61051660015463ffffffff9060a01c1690565b63ffffffff1690565b611afa565b115f8061046c565b6104c59394925061053d868a611551565b925f825561054c848254611551565b8155610433565b90508911155f61040d565b3461032d57602036600319011261032d576004355f526005602052602060405f2054604051908152f35b3461032d575f36600319011261032d5760206040517f00000000000000000000000000000000000000000000000000000000000000008152f35b63ffffffff81160361032d57565b3461032d57604036600319011261032d576004356105ed816105c2565b6024356105f8611ac2565b6106038115156117d4565b61061b8263ffffffff165f52600660205260405f2090565b549061062f825f52600360205260405f2090565b805460a01c6001600160401b0316151580610780575b61064e90611800565b7f000000000000000000000000000000000000000000000000000000000000000061067b83303384611c73565b600282019061068b848354611544565b825561069c61044285600254611544565b6040516370a0823160e01b815230600482015290602090829060249082906001600160a01b03165afa92831561077b57600363ffffffff9361070f7f12341d30af78a74af3697daeaf7b1662bc9b723f6aa8a3402bf3d8b3f87a077296610719955f9161074c575b5060025411156117ea565b5491015490611544565b604080519485526020850191909152941693339290819081015b0390a46104de60015f516020612d225f395f51905f5255565b61076e915060203d602011610774575b6107668183611367565b810190611816565b5f610704565b503d61075c565b611825565b5061064e6107a761079b60018401546001600160401b031690565b6001600160401b031690565b159050610645565b3461032d575f36600319011261032d57602063ffffffff60015460a01c16604051908152f35b3461032d575f36600319011261032d576040517f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03168152602090f35b3461032d57602036600319011261032d5763ffffffff60043561083b816105c2565b165f52600660205260405f20546108b061085d825f52600360205260405f2090565b80546001600160401b03906108999061088a6001600160a01b0382165b6001600160a01b03163314611830565b60a01c6001600160401b031690565b1615158061091e575b6108ab90611800565b611ba0565b6108e37f00000000000000000000000000000000000000000000000000000000000000006001600160401b034216611846565b6040516001600160401b0391909116815233907f0c833c7c9f5b9b8ed0085d7959eb025f59fa32a55b8d55223a819bc7c58db34590602090a3005b506108ab61093961079b60018401546001600160401b031690565b1590506108a2565b3461032d57602036600319011261032d5763ffffffff600435610963816105c2565b165f526006602052602060405f2054604051908152f35b3461032d575f36600319011261032d576020600254604051908152f35b3461032d575f36600319011261032d5760206040517f00000000000000000000000000000000000000000000000000000000000000008152f35b9060049160441161032d57565b9060249160641161032d57565b9060449160c41161032d57565b9060649160e41161032d57565b3461032d5760e036600319011261032d57610a1f366109d1565b610a28366109eb565b60c435610a33611ac2565b610a5461051f610a4f61051660015463ffffffff9060a01c1690565b6114fe565b90610a63818380821015611866565b60405163e8804a2b60e01b8152600481018390525f6024820152937f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03169390602086806044810103815f895af195861561077b575f96610b6c575b50604051638eeda10360e01b815263ffffffff8716600482015294606090869060249082905afa801561077b57610b06955f91610b3d575b503387611feb565b90610b1d60015f516020612d225f395f51905f5255565b6040805163ffffffff9092168252602082019290925290819081015b0390f35b610b5f915060603d606011610b65575b610b578183611367565b8101906118ad565b5f610afe565b503d610b4d565b610b8f91965060203d602011610b96575b610b878183611367565b810190611884565b945f610ac6565b503d610b7d565b3461032d57602036600319011261032d57600435610bb96113e2565b505f5260036020526101c0610bd060405f20611628565b610bdd6040518092610217565bf35b3461032d575f36600319011261032d575f546040516001600160a01b039091168152602090f35b3461032d575f36600319011261032d576020604051620100008152f35b3461032d575f36600319011261032d5763ffffffff60015460a01c1660018101809111610c5d57610c55602091611afa565b604051908152f35b6114ea565b3461032d575f36600319011261032d576020600454604051908152f35b3461032d57602036600319011261032d57600435610c9c81610357565b610cb060018060a01b035f541633146118fe565b6001600160a01b0316610cc4811515611914565b600180546001600160a01b0319811683179091556001600160a01b03167fe0d49a54274423183dadecbdf239eaac6e06ba88320b26fe8cc5ec9d050a63955f80a3005b3461032d5761010036600319011261032d57600435610d25816105c2565b610d2e366109de565b90610d38366109f8565b9060e435610d44611ac2565b6040516331a9108f60e11b815263ffffffff831660048201526001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169390602081602481885afa801561077b57610db4915f91610e5a575b506001600160a01b03163314611830565b604051638eeda10360e01b815263ffffffff8416600482015293606090859060249082905afa94851561077b57610b3995610e15955f91610e3b575b50610e0d61051f610a4f61051660015463ffffffff9060a01c1690565b9433906123ef565b610e2b60015f516020612d225f395f51905f5255565b6040519081529081906020820190565b610e54915060603d606011610b6557610b578183611367565b5f610df0565b610e7c915060203d602011610e82575b610e748183611367565b81019061192a565b5f610da3565b503d610e6a565b3461032d575f36600319011261032d576001546040516001600160a01b039091168152602090f35b3461032d57604036600319011261032d57610ed060243560043561198c565b6040518091602082016020835281518091526020604084019201905f5b818110610efb575050500390f35b8251845285945060209384019390920191600101610eed565b3461032d575f36600319011261032d5760206040516001600160401b037f0000000000000000000000000000000000000000000000000000000000000000168152f35b3461032d57602036600319011261032d57600435610f74816105c2565b610f7c611ac2565b610f948163ffffffff165f52600660205260405f2090565b54610faf610faa825f52600360205260405f2090565b611628565b8051909290610fc6906001600160a01b031661087a565b6001600160401b03610fe260208501516001600160401b031690565b1615158061120a575b610ff490611800565b61104161103861079b61101160408701516001600160401b031690565b7f000000000000000000000000000000000000000000000000000000000000000090611846565b80421015611a2e565b60c0830192611056845160e083015190611544565b9361106e61079b60608401516001600160401b031690565b42106112045750835b61108661044286600254611551565b6110a061109a608084015163ffffffff1690565b856125a2565b6110a9826126bb565b6110c36110be855f52600360205260405f2090565b611a74565b5f6110dc8463ffffffff165f52600660205260405f2090565b5581516001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000811693911692803b1561032d576040516323b872dd60e01b81523060048201526001600160a01b0394909416602485015263ffffffff851660448501525f908490606490829084905af191821561077b577f4f72a5ea49c0470a55beb3953816abf5c92fc73003b1049c241b133a0863208c9363ffffffff936111ea575b50806111ae575b50516040519586529216936001600160a01b03909216918060208101610733565b81516111e491906001600160a01b03167f0000000000000000000000000000000000000000000000000000000000000000611bf6565b5f61118d565b806111f85f6111fe93611367565b80610331565b5f611186565b51611077565b50610ff461122561079b60408601516001600160401b031690565b15159050610feb565b3461032d575f36600319011261032d576040517f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03168152602090f35b3461032d57602036600319011261032d5760043561128f81610357565b5f54906001600160a01b038216906112a83383146118fe565b6001600160a01b03169182906112bf821515611914565b6bffffffffffffffffffffffff60a01b16175f557f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e05f80a3005b3461032d575f36600319011261032d5760206040517f00000000000000000000000000000000000000000000000000000000000000008152f35b634e487b7160e01b5f52604160045260245ffd5b604081019081106001600160401b0382111761136257604052565b611333565b90601f801991011681019081106001600160401b0382111761136257604052565b604051906102bc61014083611367565b604051906102bc604083611367565b906102bc6040519283611367565b6001600160401b0381116113625760051b60200190565b604051906113db602083611367565b6020368337565b6040519061014082018281106001600160401b0382111761136257604052815f81525f60208201525f60408201525f60608201525f60808201525f60a08201525f60c08201525f60e082015260405161143c604082611367565b604036823761010082015261012060405191611459608084611367565b60803684370152565b60405190611471602083611367565b5f80835282815b82811061148457505050565b60209061148f6113e2565b82828501015201611478565b906114a5826113b5565b6114b26040519182611367565b82815280926114c3601f19916113b5565b01905f5b8281106114d357505050565b6020906114de6113e2565b828285010152016114c7565b634e487b7160e01b5f52601160045260245ffd5b9060018201809211610c5d57565b9060028201809211610c5d57565b9060038201809211610c5d57565b9060048201809211610c5d57565b9060058201809211610c5d57565b91908201809211610c5d57565b91908203918211610c5d57565b634e487b7160e01b5f52603260045260245ffd5b60045481101561158a5760045f5260205f2001905f90565b61155e565b80511561158a5760200190565b80516001101561158a5760400190565b805182101561158a5760209160051b010190565b60405191905f835b600282106115de575050506102bc604083611367565b60016020819285548152019301910190916115c8565b60405191905f835b60048210611612575050506102bc608083611367565b60016020819285548152019301910190916115fc565b906117056006611636611388565b84546001600160a01b0381168252909490611664906116549061088a565b6001600160401b03166020870152565b6116d96116cc6001830154611692611682826001600160401b031690565b6001600160401b031660408a0152565b6001600160401b03604082901c1660608901526116c0608082901c63ffffffff1663ffffffff1660808a0152565b60a01c63ffffffff1690565b63ffffffff1660a0870152565b600281015460c0860152600381015460e08601526116f9600482016115c0565b610100860152016115f4565b610120830152565b9060045490818310156117b057820190818311610c5d578082116117a8575b50818103818111610c5d576117409061149b565b91805b8281106117505750505090565b806117a161177d61176f611765600195611572565b90549060031b1c90565b5f52600360205260405f2090565b61179061178a8685611551565b91611628565b61179a82896115ac565b52866115ac565b5001611743565b90505f61172c565b5050506117bb611462565b90565b156117c557565b63dabc4ad960e01b5f5260045ffd5b156117db57565b631f2a200560e01b5f5260045ffd5b156117f157565b633a23d82560e01b5f5260045ffd5b1561180757565b6310e8397760e31b5f5260045ffd5b9081602091031261032d575190565b6040513d5f823e3d90fd5b1561183757565b634b64ae4760e01b5f5260045ffd5b906001600160401b03809116911601906001600160401b038211610c5d57565b1561186f575050565b630e9fc46d60e11b5f5260045260245260445ffd5b9081602091031261032d57516117bb816105c2565b51906001600160401b038216820361032d57565b9081606091031261032d576040519060608201908282106001600160401b038311176113625760409182526118e181611899565b83526118ef60208201611899565b60208401520151604082015290565b1561190557565b6330cd747160e01b5f5260045ffd5b1561191b57565b63d92e233d60e01b5f5260045ffd5b9081602091031261032d57516117bb81610357565b6040519061194e602083611367565b5f808352366020840137565b90611964826113b5565b6119716040519182611367565b8281528092611982601f19916113b5565b0190602036910137565b6004549182821015611a2357810190818111610c5d57828211611a1b575b808203828111610c5d576119bd9061195a565b92815b8381106119ce575050505090565b8181101561158a5760019060045f52807f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b0154611a14611a0e8684611551565b886115ac565b52016119c0565b8291506119aa565b5050506117bb61193f565b15611a365750565b633bc882ad60e11b5f5260045260245ffd5b90600682029180830460061490151715610c5d57565b908160051b9180830460201490151715610c5d57565b5f81555f60018201555f60028201555f60038201555f5b60028110611ab257506006015f5b60048110611aa5575050565b5f82820155600101611a99565b5f82820160040155600101611a8b565b60025f516020612d225f395f51905f525414611aeb5760025f516020612d225f395f51905f5255565b633ee5aeb560e01b5f5260045ffd5b62010000811015611b7b577f0000000000000000000000000000000000000000000000000000000000000000907f000000000000000000000000000000000000000000000000000000000000000082810391818311610c5d57611b5d84916126d8565b8084029384041491141715610c5d5760081c8101809111610c5d5790565b507f000000000000000000000000000000000000000000000000000000000000000090565b600101805467ffffffffffffffff1916426001600160401b031617905563ffffffff60015460a01c168015610c5d576001805463ffffffff60a01b19165f1990920160a01b63ffffffff60a01b16919091179055565b916040519163a9059cbb60e01b5f5260018060a01b031660045260245260205f60448180865af160015f5114811615611c54575b604091909152155b611c395750565b635274afe760e01b5f526001600160a01b031660045260245ffd5b6001811516611c6a573d15833b15151616611c2a565b503d5f823e3d90fd5b6040516323b872dd60e01b5f9081526001600160a01b039384166004529290931660245260449390935260209060648180865af160015f5114811615611cc4575b6040919091525f60605215611c32565b6001811516611c6a573d15833b15151616611cb4565b15611ce157565b633b093fe160e21b5f5260045ffd5b15611cf9575050565b636af5d51b60e01b5f5260045260245260445ffd5b6001600160401b03166001600160401b038114610c5d5760010190565b919060405192611d3c604085611367565b83906040810192831161032d57905b828210611d5757505050565b8135815260209182019101611d4b565b919060405192611d78608085611367565b83906080810192831161032d57905b828210611d9357505050565b8135815260209182019101611d87565b905f5b60028110611db357505050565b600190602083519301928185015501611da6565b905f5b60048110611dd757505050565b600190602083519301928185015501611dca565b815181546001600160a01b0319166001600160a01b039091161781556102bc9160069061012090611e51611e2960208301516001600160401b031690565b855467ffffffffffffffff60a01b191660a09190911b67ffffffffffffffff60a01b16178555565b611f3460018501611e8c611e6f60408501516001600160401b031690565b825467ffffffffffffffff19166001600160401b03909116178255565b611ed5611ea360608501516001600160401b031690565b82546fffffffffffffffff0000000000000000191660409190911b6fffffffffffffffff000000000000000016178255565b611f09611ee9608085015163ffffffff1690565b825463ffffffff60801b191660809190911b63ffffffff60801b16178255565b60a083015163ffffffff16815463ffffffff60a01b191660a09190911b63ffffffff60a01b16179055565b60c0810151600285015560e08101516003850155611f5a61010082015160048601611da3565b01519101611dc7565b60045468010000000000000000811015611362576001810160045560045481101561158a5760045f527f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b0155565b63ffffffff1663ffffffff8114610c5d5760010190565b6040906001600160401b036080949695939660c083019783521660208201520137565b969591929694909461201561200e8263ffffffff165f52600660205260405f2090565b5415611cda565b61202782604086015180821015611cf0565b5f928083106123b2575b5060015460c01c61206961204482611d0e565b600180546001600160c01b031660c09290921b6001600160c01b031916919091179055565b604080517f00000000000000000000000000000000000000000000000000000000000000006020820190815263ffffffff8516928201929092526001600160401b0390921660608301524460808301526001600160a01b03881660a0830152906120e08160c081015b03601f198101835282611367565b519020976120ef86828b612775565b6040516331a9108f60e11b815263ffffffff831660048201526020816024816001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000165afa801561077b5761224b9261216561221e928b945f91612393575b506001600160a01b03163014611830565b85612362575b8b6121848663ffffffff165f52600660205260405f2090565b556121ff6121af6121a960206001600160401b0342169b01516001600160401b031690565b8a611846565b986121dd6121c260045463ffffffff1690565b916116546121ce611388565b6001600160a01b039098168852565b5f60408601526001600160401b038a16606086015263ffffffff166080850152565b63ffffffff851660a08401528560c08401528660e08401523690611d2b565b61010082015261222e3688611d67565b6101208201526122468a5f52600360205260405f2090565b611deb565b61225488611f63565b61229761227261226d60015463ffffffff9060a01c1690565b611fb1565b6001805463ffffffff60a01b191660a09290921b63ffffffff60a01b16919091179055565b6122af6104426122a78585611544565b600254611544565b6040516370a0823160e01b81523060048201526020816024816001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000165afa95861561077b576123457f58cee7261629c956b111bc684df727bd2e9b0f5d954e24b93908951c431cd13e9563ffffffff956123408d9a61235d965f9161074c575060025411156117ea565b611544565b95604051948594169860018060a01b03169684611fc8565b0390a4565b61238e8630857f0000000000000000000000000000000000000000000000000000000000000000611c73565b61216b565b6123ac915060203d602011610e8257610e748183611367565b5f612154565b6123e8919350806123e38480936001600160401b036123db60208b01516001600160401b031690565b161515611866565b611551565b915f612031565b969591929694909461241261200e8263ffffffff165f52600660205260405f2090565b61242482604086015180821015611cf0565b5f92808310612572575b5060015460c01c61244161204482611d0e565b604080517f00000000000000000000000000000000000000000000000000000000000000006020820190815263ffffffff8516928201929092526001600160401b0390921660608301524460808301526001600160a01b03881660a0830152906124ae8160c081016120d2565b519020976124bd86828b612775565b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316803b1561032d576040516323b872dd60e01b81526001600160a01b038916600482015230602482015263ffffffff84166044820152905f908290606490829084905af1801561077b5761224b92899261221e9261255e575b5085612362578b6121848663ffffffff165f52600660205260405f2090565b806111f85f61256c93611367565b5f61253f565b61259b919350806123e38480936001600160401b036123db60208b01516001600160401b031690565b915f61242e565b906004545f198101818111610c5d5781111561158a5760045f527f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19a810154809303612648575b5050506004548015612634575f1981019060045482101561158a5760045f8181527f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19a9092019190915555565b634e487b7160e01b5f52603160045260245ffd5b81101561158a57600161268f6126b39360045f5280847f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b01555f52600360205260405f2090565b01805463ffffffff60801b191660809290921b63ffffffff60801b16919091179055565b5f80806125e8565b6101206126c991015161284a565b5f5260056020525f6040812055565b90811561272e5760018201808311610c5d5760011c825b8382106126fa575050565b90925082801561271a57808204908101809111610c5d5760011c906126ef565b634e487b7160e01b5f52601260045260245ffd5b5f9150565b1561273a57565b632095a11360e21b5f5260045ffd5b1561275057565b634d82eddb60e01b5f5260045ffd5b1561276657565b637e4c066f60e01b5f5260045ffd5b9161280e6128139161280761280261283d956127b961279682359260200190565b3561279f611398565b928084528160208501521590811591612840575b50612733565b6127c360406113a7565b8435815290602085013560208301526127dc60406113a7565b60408601358152606086013560208201526127f5611398565b928352602083015261291e565b612749565b3690611d67565b61284a565b61282f612828825f52600560205260405f2090565b541561275f565b5f52600560205260405f2090565b55565b905015155f6127b3565b805190602081015190606060408201519101519060405192602084019485526040840152606083015260808201526080815261288760a082611367565b51902090565b6040519061289a82611347565b5f6020838281520152565b604051906128b282611347565b81602060409182516128c48482611367565b8336823781528251926128d78185611367565b3684370152565b604051606091906128ef8382611367565b6002815291601f1901825f5b82811061290757505050565b6020906129126128a5565b828285010152016128fb565b91909161294060405161293081611347565b60018152600260208201526129e3565b9260405190612950606083611367565b6002825260405f5b8181106129cc5750506117bb939461296e6128de565b936129788461158f565b526129828361158f565b5061298b612aa7565b6129948561158f565b5261299e8461158f565b506129a88361159c565b526129b28261159c565b506129bc8361159c565b526129c68261159c565b50612bec565b6020906129d761288d565b82828701015201612958565b6129eb61288d565b5080511580612a9b575b612a82577f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd4760208251920151067f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47037f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd478111610c5d5760405191612a7883611347565b8252602082015290565b50604051612a8f81611347565b5f81525f602082015290565b506020810151156129f5565b612aaf6128a5565b50604051612abc81611347565b7f198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c281527f1800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed6020820152604051612b1181611347565b7f090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b81527f12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa602082015260405191612a7883611347565b15612b6e57565b60405162461bcd60e51b815260206004820152601460248201527308498a67440d8cadccee8d040dad2e6dac2e8c6d60631b6044820152606490fd5b15612bb157565b60405162461bcd60e51b8152602060048201526013602482015272109314ce881c185a5c9a5b99c819985a5b1959606a1b6044820152606490fd5b612bf98151835114612b67565b8051612c0c612c0782611a48565b611a5e565b91612c1e612c1983611a48565b61195a565b935f5b838110612c505750505050612c4b60206001938193612c3e6113cc565b9485920160085afa612baa565b511490565b80612c5c600192611a48565b612c6682866115ac565b5151612c72828a6115ac565b526020612c7f83876115ac565b510151612c94612c8e836114fe565b8a6115ac565b52612c9f82856115ac565b515151612cae612c8e8361150c565b52612cc4612cbc83866115ac565b515160200190565b51612cd1612c8e8361151a565b526020612cde83866115ac565b51015151612cee612c8e83611528565b52612d1a612d14612d0d6020612d0486896115ac565b51015160200190565b5192611536565b896115ac565b5201612c2156fe9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00 diff --git a/pkg/blockchain/evm/artifacts/Slasher.abi b/pkg/blockchain/evm/artifacts/Slasher.abi new file mode 100644 index 0000000..a68c442 --- /dev/null +++ b/pkg/blockchain/evm/artifacts/Slasher.abi @@ -0,0 +1,117 @@ +[ + { + "type": "constructor", + "inputs": [ + { + "name": "_registry", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "MIN_CLUSTER_SIZE", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "REGISTRY", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "submitWithdrawalFraudEvidence", + "inputs": [ + { + "name": "challengedObject", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "anchorHeader", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "anchorSignature", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "entryIndex", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "smtProof", + "type": "bytes32[]", + "internalType": "bytes32[]" + }, + { + "name": "smtBitmask", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "balanceKey", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "provenBalance", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "event", + "name": "FraudEvidenceSubmitted", + "inputs": [ + { + "name": "blockHash", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "prover", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "signersSlashed", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "ReentrancyGuardReentrantCall", + "inputs": [] + } +] diff --git a/pkg/blockchain/evm/artifacts/Slasher.bin b/pkg/blockchain/evm/artifacts/Slasher.bin new file mode 100644 index 0000000..dfff23b --- /dev/null +++ b/pkg/blockchain/evm/artifacts/Slasher.bin @@ -0,0 +1 @@ +0x60a0346100de57601f61312c38819003918201601f19168301916001600160401b038311848410176100e2578084926020946040528339810103126100de57516001600160a01b0381168082036100de5760017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055156100a95760805260405161303590816100f7823960805181818161010901528181610e7401528181611b1901526125aa0152f35b60405162461bcd60e51b815260206004820152600d60248201526c5a65726f20726567697374727960981b6044820152606490fd5b5f80fd5b634e487b7160e01b5f52604160045260245ffdfe60806040526004361015610011575f80fd5b5f3560e01c806306433b1b146100f75780631cca16681461003f57638ec1daee1461003a575f80fd5b6101c2565b346100f3576101003660031901126100f3576004356001600160401b0381116100f357610070903690600401610145565b906024356001600160401b0381116100f357610090903690600401610145565b92906044356001600160401b0381116100f3576100b1903690600401610145565b946100ba610183565b608435966001600160401b0388116100f3576100dd6100f1983690600401610192565b94909360a4359660c4359860e4359a6101dd565b005b5f80fd5b346100f3575f3660031901126100f3577f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166080908152602090f35b5f9103126100f357565b9181601f840112156100f3578235916001600160401b0383116100f357602083818601950101116100f357565b6001600160401b038116036100f357565b6064359061019082610172565b565b9181601f840112156100f3578235916001600160401b0383116100f3576020808501948460051b0101116100f357565b346100f3575f3660031901126100f357602060405160058152f35b909a9695939899949a60027f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00541461039f5760027f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055369061023e9261044b565b91369061024a9261044b565b916001600160401b031661025d916106b3565b9161026782610825565b91805190602001208060a08501511461027f90610481565b83519960208b0151602086019b8c51805190602001209060800151604088015160808901519160608a0151936102b49561097e565b986020850151926080860151906102ca94610a97565b61032c976103259660c09561031d946103189489156103965760408051602081018381528183018d90529061030c81606081015b03601f1981018352826103e2565b519020925b0151610c01565b6104bf565b0151116104ff565b3390610e6e565b9051602081519101207f852c66b7cb153328335457559e23c7ae13dbd71edc4c8d92830c24702b7bf3566040518061036a3395829190602083019252565b0390a361019060017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055565b60405f92610311565b633ee5aeb560e01b5f5260045ffd5b634e487b7160e01b5f52604160045260245ffd5b604081019081106001600160401b038211176103dd57604052565b6103ae565b90601f801991011681019081106001600160401b038211176103dd57604052565b60405190610190610140836103e2565b604051906101906040836103e2565b9061019060405192836103e2565b6001600160401b0381116103dd57601f01601f191660200190565b92919261045782610430565b9161046560405193846103e2565b8294818452818301116100f3578281602093845f960137010152565b1561048857565b60405162461bcd60e51b815260206004820152600f60248201526e082dcc6d0dee440dad2e6dac2e8c6d608b1b6044820152606490fd5b156104c657565b60405162461bcd60e51b815260206004820152601160248201527024b73b30b634b21029a6aa10383937b7b360791b6044820152606490fd5b1561050657565b60405162461bcd60e51b815260206004820152601260248201527110985b185b98d9481cdd59999a58da595b9d60721b6044820152606490fd5b6040519060c082018281106001600160401b038211176103dd57604052606060a0835f81525f60208201525f60408201525f838201525f60808201520152565b6040519060e082018281106001600160401b038211176103dd576040525f60c0836105a9610540565b815260606020820152606060408201526060808201528260808201528260a08201520152565b156105d657565b60405162461bcd60e51b815260206004820152601960248201527f496e76616c6964206368616c6c656e676564206f626a656374000000000000006044820152606490fd5b1561062257565b60405162461bcd60e51b815260206004820152601760248201527f456e747269657320646967657374206d69736d617463680000000000000000006044820152606490fd5b1561066e57565b60405162461bcd60e51b815260206004820152601960248201527f547261696c696e67206368616c6c656e676564206279746573000000000000006044820152606490fd5b9190916106be610580565b926106c882610fcc565b600b146106d4906105cf565b6106de9083611068565b908551526106ec90836110f3565b91908551602001526106fe918361128c565b91929060c08701526107109084611068565b908651604001526107219084611068565b9190865160600152855160600151146107399061061b565b61074390836110f3565b908551608001526107549083611481565b9392919060608801526080870152604086015261077191836115aa565b9190855160a0019060a0870152526107899082611683565b6107939082611683565b61079d9082611683565b9051146107a990610667565b81516107b49061176c565b6020830152565b634e487b7160e01b5f52601160045260245ffd5b919082039182116107dc57565b6107bb565b156107e857565b60405162461bcd60e51b8152602060048201526015602482015274547261696c696e672068656164657220627974657360581b6044820152606490fd5b906006610830610540565b9261083a81610fcc565b929092036108ba5761088861087c61087061086461085b6101909686611068565b908952856110f3565b90602089015284611068565b90604088015283611068565b906060870152826110f3565b919060808601526108ae61089c8383611683565b926108a781856107cf565b9083611853565b60a086015251146107e1565b60405162461bcd60e51b815260206004820152600e60248201526d24b73b30b634b2103432b0b232b960911b6044820152606490fd5b9080601f830112156100f3576040519161090b6040846103e2565b8290604081019283116100f357905b8282106109275750505090565b815181526020918201910161091a565b9080601f830112156100f357604051916109526080846103e2565b8290608081019283116100f357905b82821061096e5750505090565b8151815260209182019101610961565b929091959493958151820160e083602083019203126100f3578060806109aa6109b193604087016108f0565b9401610937565b925f945f5b61010081106109ce57506109cb979850611aa7565b90565b8060031c6020811015610a03578a901a6001600783161b1660ff166109f6575b6001016109b6565b6001811b909617956109ee565b610b9a565b919060405192610a196040856103e2565b8390604081019283116100f357905b828210610a3457505050565b8135815260209182019101610a28565b919060405192610a556080856103e2565b8390608081019283116100f357905b828210610a7057505050565b8135815260209182019101610a64565b6001600160401b0381116103dd5760051b60200190565b949383019290610100828503126100f35781359284603f840112156100f357610ac38560208501610a08565b9185607f850112156100f357610adc8660608601610a44565b9360e0810135906001600160401b0382116100f357019786601f8a0112156100f357883598610b0a8a610a80565b97610b18604051998a6103e2565b8a89526020808a019b60051b830101918183116100f357602081019b5b838d10610b4e5750505050610b4b979850611aa7565b50565b8c356001600160401b0381116100f357820183603f820112156100f357602091610b81858360408680960135910161044b565b8152019c019b610b35565b5f1981146107dc5760010190565b634e487b7160e01b5f52603260045260245ffd5b9190811015610a035760051b0190565b15610bc557565b60405162461bcd60e51b8152602060048201526014602482015273534d543a20756e75736564207369626c696e677360601b6044820152606490fd5b9391959495925f905f925f905b6101008210610c2d5750505050610c29929394955014610bbe565b1490565b9091929560019081808c861c16145f14610cda57610c55610c4d87610b8c565b968887610bae565b355b83851c8316610cb157604080516020810193845290810191909152610c7f81606081016102fe565b519020965b604051610ca5816102fe602082019480869091604092825260208201520190565b51902093920190610c0e565b60408051602081019283529081019290925290610cd181606081016102fe565b51902096610c84565b87610c57565b805115610a035760200190565b805160011015610a035760400190565b8051821015610a035760209160051b010190565b51906001600160a01b03821682036100f357565b519061019082610172565b519063ffffffff821682036100f357565b906101c0828203126100f357610deb90610140610d5c610403565b93610d6681610d11565b8552610d7460208201610d25565b6020860152610d8560408201610d25565b6040860152610d9660608201610d25565b6060860152610da760808201610d30565b6080860152610db860a08201610d30565b60a086015260c081015160c086015260e081015160e0860152610ddf8361010083016108f0565b61010086015201610937565b61012082015290565b6040513d5f823e3d90fd5b90602082018092116107dc57565b90600182018092116107dc57565b90600282018092116107dc57565b90600482018092116107dc57565b90600382018092116107dc57565b90600882018092116107dc57565b90600582018092116107dc57565b919082018092116107dc57565b91905f907f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031690825b8551811015610fc557610ed86101c0610eb88389610cfd565b51604051809381926308899cf560e41b8352600483019190602083019252565b0381875afa8015610f9157610eff915f91610f96575b5060e060c082015191015190610e61565b80610f0e575b50600101610e9f565b9093610f1a8588610cfd565b51843b156100f35760405163158ece2760e01b8152600481019190915260248101929092526001600160a01b03831660448301525f8260648183885af1908115610f9157600192610f7092610f77575b50610b8c565b9390610f05565b80610f855f610f8b936103e2565b8061013b565b5f610f6a565b610df4565b610fb891506101c03d8111610fbe575b610fb081836103e2565b810190610d41565b5f610eee565b503d610fa6565b5050509150565b600491610fdb5f60ff93611d9e565b94919390931603610fe857565b60405162461bcd60e51b815260206004820152600e60248201526d457870656374656420617272617960901b6044820152606490fd5b91610fdb60ff92600494611d9e565b1561103457565b60405162461bcd60e51b815260206004820152600c60248201526b21a127a91037bb32b9393ab760a11b6044820152606490fd5b60ff6110776002949383611d9e565b9591929092161490816110e8575b50156110b0576020836109cb926110a761109e83610dff565b8251101561102d565b01015192610dff565b60405162461bcd60e51b815260206004820152601060248201526f22bc3832b1ba32b210313cba32b9999960811b6044820152606490fd5b60209150145f611085565b60ff916110ff91611d9e565b9391939290931661110c57565b60405162461bcd60e51b815260206004820152600d60248201526c115e1c1958dd1959081d5a5b9d609a1b6044820152606490fd5b1561114857565b60405162461bcd60e51b815260206004820152601960248201527f456e74727920696e646578206f7574206f6620626f756e6473000000000000006044820152606490fd5b1561119457565b60405162461bcd60e51b815260206004820152600d60248201526c496e76616c696420656e74727960981b6044820152606490fd5b156111d057565b60405162461bcd60e51b815260206004820152600e60248201526d139bdd081dda5d1a191c985dd85b60921b6044820152606490fd5b805191908290602001825e015f815290565b602061019091611232949360405195869284840190611206565b9081520380855201836103e2565b1561124757565b60405162461bcd60e51b815260206004820152601960248201527f496e76616c6964207769746864726177616c20616d6f756e74000000000000006044820152606490fd5b9091925f9361129c5f948461101e565b90936112a9828410611141565b6060925f915b8383106112df575050506112c4851515611240565b6112d257505f915b93929190565b60208151910120916112cc565b9294978691949296978588145f1461139157505091600161138861134c97959361137b611368611352611343600361135b9f9c9a611336600461133061132861133d948e61101e565b90921461118d565b8b6110f3565b92146111c9565b88611fda565b889d919d611683565b87612054565b9d909d9b612075565b602081519101209c612139565b9a5b611374818c6107cf565b9086611853565b6020815191012090611218565b940191906112af565b979694926113889061137b6113ac87956001959d9a98611683565b9961136a565b156113b957565b60405162461bcd60e51b8152602060048201526013602482015272546f6f206d616e792076616c696461746f727360681b6044820152606490fd5b906113fe82610a80565b61140b60405191826103e2565b828152809261141c601f1991610a80565b01905f5b82811061142c57505050565b806060602080938501015201611420565b1561144457565b60405162461bcd60e51b8152602060048201526015602482015274496e76616c69642076616c696461746f72206b657960581b6044820152606490fd5b9061148e6003918361101e565b9190910361152357906114ba926114a86114b19383612054565b83949194611068565b8395919561101e565b9390926114cb6101008611156113b2565b6114d4856113f4565b945f915b8183106114e85750505093929190565b9091946114f760019183612054565b9690611503828a610cfd565b5261151b6080611513838b610cfd565b51511461143d565b0191906114d8565b60405162461bcd60e51b815260206004820152601360248201527224b73b30b634b21030ba3a32b9ba30ba34b7b760691b6044820152606490fd5b1561156557565b60405162461bcd60e51b815260206004820152601a60248201527f4163636f756e7420736e617073686f74206e6f7420666f756e640000000000006044820152606490fd5b5f93916115b7818361101e565b9490945f915f5b8281106115eb57505050906115d66115e6939261155e565b6115e081866107cf565b91611853565b929190565b6115f76003988761101e565b9890980361163e57611622826116106116199a89611068565b899b919b611068565b89939193611683565b9914611632575b506001016115be565b98506001935083611629565b60405162461bcd60e51b815260206004820152601860248201527f496e76616c6964206163636f756e7420736e617073686f7400000000000000006044820152606490fd5b9061168e9082611d9e565b9160ff16908282158015611762575b61175a5750600282148015611750575b61172e576004821461170257506006146116f95760405162461bcd60e51b815260206004820152601060248201526f2ab739bab83837b93a32b21021a127a960811b6044820152606490fd5b6109cb91611683565b91925f9291505b8183106117165750505090565b9091926117239082611683565b926001019190611709565b91905061174b6109cb936117428484610e61565b9051101561102d565b610e61565b50600382146116ad565b935050505090565b506001831461169d565b6109cb6117e5916102fe6117808251612295565b916117e56117916020830151612300565b916117e56117a26040830151612295565b6117e56117b26060850151612295565b916117e560a06117c56080880151612300565b960151604051604360f91b60208201529c8d9b91999160218d0190611206565b90611206565b604080519091906117fc83826103e2565b60208152918290601f190190369060200137565b9061181a82610430565b61182760405191826103e2565b8281528092611838601f1991610430565b0190602036910137565b908151811015610a03570160200190565b929190928184018085116107dc578151106118bb5761187182611810565b935f5b8381106118815750505050565b806118a861189a61189460019486610e61565b86611842565b516001600160f81b03191690565b5f1a6118b48289611842565b5301611874565b60405162461bcd60e51b815260206004820152600d60248201526c29b634b1b29037bb32b9393ab760991b6044820152606490fd5b156118f757565b60405162461bcd60e51b8152602060048201526014602482015273496e76616c696420636c75737465722073697a6560601b6044820152606490fd5b1561193a57565b60405162461bcd60e51b8152602060048201526015602482015274125b9d985b1a59081d985b1a59185d1bdc881cd95d605a1b6044820152606490fd5b600181901b91906001600160ff1b038116036107dc57565b906006820291808304600614901517156107dc57565b908160051b91808304602014901517156107dc57565b634e487b7160e01b5f52601260045260245ffd5b156119d657565b60405162461bcd60e51b81526020600482015260126024820152714e6f7420656e6f756768207369676e65727360701b6044820152606490fd5b60405190611a1d826103c2565b5f6020838281520152565b15611a2f57565b60405162461bcd60e51b815260206004820152600c60248201526b082a09640dad2e6dac2e8c6d60a31b6044820152606490fd5b15611a6a57565b60405162461bcd60e51b8152602060048201526015602482015274496e76616c696420424c53207369676e617475726560581b6044820152606490fd5b94611b0d9297939496611ae8929660058a101580611c8b575b611ac9906118f0565b611ae382518b8110159081611c7e575b5099989799611933565b612541565b95611b06611b01611afa895193611977565b6003900490565b610e0d565b11156119cf565b611b15611a10565b5f937f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031693915b8751861015611bf157611b7e906101c0611b5e888b610cfd565b51604051809481926308899cf560e41b8352600483019190602083019252565b0381895afa918215610f9157600192610100915f91611bd2575b5001518051919060200151611bab610413565b928352602083015287611bc25750955b0194611b44565b90611bcc916127fb565b95611bbb565b611beb91506101c03d8111610fbe57610fb081836103e2565b5f611b98565b6101909550611c799450611c58611c5d919792939497611c116040610422565b855181529060208601516020830152611c3b611c2d6040610422565b966040810151885260600190565b516020870152611c49610413565b95869283526020830152612927565b611a28565b80519060200151611c6c610413565b91825260208201526129b9565b611a63565b610100915011155f611ad9565b506101008a1115611ac0565b15611c9e57565b60405162461bcd60e51b815260206004820152601260248201527109cdedcc6c2dcdedcd2c6c2d840ead2dce8760731b6044820152606490fd5b15611cdf57565b60405162461bcd60e51b81526020600482015260136024820152722737b731b0b737b734b1b0b6103ab4b73a189b60691b6044820152606490fd5b15611d2157565b60405162461bcd60e51b81526020600482015260136024820152722737b731b0b737b734b1b0b6103ab4b73a199960691b6044820152606490fd5b15611d6357565b60405162461bcd60e51b8152602060048201526013602482015272139bdb98d85b9bdb9a58d85b081d5a5b9d0d8d606a1b6044820152606490fd5b90915f92611dae8351821061102d565b611dc4611dbe61189a8386611842565b60f81c90565b92611dda601f600586901c600716951692610e0d565b9160188110611fd15760188114611f9d5760198114611f4b57601a8114611ea357601b14611e395760405162461bcd60e51b815260206004820152600f60248201526e24b73232b334b734ba329021a127a960891b6044820152606490fd5b611e4561109e83610e45565b5f905b60088210611e70575050611e6a90611e6563ffffffff8611611d5c565b610e45565b91929190565b909460019060081b611e9a611e94611dbe61189a611e8e8b89610e61565b87611842565b60ff1690565b17950190611e48565b50819450611f37611e94611dbe61189a85611f2c611f26611e94611dbe61189a611f2086611f19611f13611e6a9f8f61189a611dbe91611ee861109e611e9495610e29565b611f0d611f07611f01611e94611dbe61189a8c87611842565b60181b90565b97610e0d565b90611842565b60101b90565b1796610e1b565b8b611842565b60081b90565b1794611f0d8a610e37565b1793611f4661ffff8611611d1a565b610e29565b50819450611f5e61109e611e6a93610e1b565b611f8a611e94611dbe61189a611f80611f26611e94611dbe61189a8d8a611842565b94611f0d8a610e0d565b1793611f9860ff8611611cd8565b610e1b565b50819450611e94611dbe61189a84611fc394611fbe61109e611e6a98610e0d565b611842565b93611b016018861015611c97565b94505091929190565b60ff611fe96003949383611d9e565b9591929092160361201957808401938481116107dc5782612010612015945187111561102d565b611853565b9190565b60405162461bcd60e51b815260206004820152601360248201527245787065637465642062797465732d6c696b6560681b6044820152606490fd5b60ff611fe96002949383611d9e565b60ff60209116019060ff82116107dc57565b906120808251611810565b915f5b81518110156120e9578061209960019284611842565b5160f81c604181101581816120dd575b50156120d8576120b890612063565b60f81b6001600160f81b0319165f1a6120d18287611842565b5301612083565b6120b8565b605a915011155f6120a9565b5050565b156120f457565b60405162461bcd60e51b815260206004820152601a60248201527f496e76616c6964207769746864726177616c207061796c6f61640000000000006044820152606490fd5b600661214482610fcc565b91909103612258576121569082611683565b6121609082611683565b5f9061216c9083611d9e565b9160ff1660061460ff6121cd816121c76121b560026121af6121a76121e79a6121a18a60069c6121e19c61224d575b506129cc565b8d61101e565b909214612a0b565b8a611d9e565b93909116159081612244575b50612a49565b87611d9e565b949192909216149081612239575b50612a95565b83612054565b9290936121f8602086511115612ae1565b5f925b85518410156122265760019060081b61221d611e94611dbe61189a888b611842565b179301926121fb565b90939194506101909250935110156120ed565b60029150145f6121db565b9050155f6121c1565b60049150145f61219b565b60405162461bcd60e51b81526020600482015260156024820152740496e76616c6964207769746864726177616c206f7605c1b6044820152606490fd5b604051600b60fb1b6020820152600160fd1b60218201526022808201929092529081526109cb6042826103e2565b156122ca57565b60405162461bcd60e51b815260206004820152600e60248201526d75696e7420746f6f206c6172676560901b6044820152606490fd5b601881106123f75760ff8111156123c85761ffff8111156123995763ffffffff81111561236a576109cb8161233f6001600160401b03809411156122c3565b604051601b60f81b6020820152921660c01b6001600160c01b031916602183015281602981016102fe565b604051600d60f91b602082015260e09190911b6001600160e01b03191660218201526109cb81602581016102fe565b604051601960f81b602082015260f09190911b6001600160f01b03191660218201526109cb81602381016102fe565b604051600360fb1b602082015260f89190911b6001600160f81b03191660218201526109cb81602281016102fe565b60405160f89190911b6001600160f81b03191660208201526109cb81602181016102fe565b9061242682610a80565b61243360405191826103e2565b8281528092611838601f1991610a80565b908160209103126100f3575190565b1561245a57565b60405162461bcd60e51b815260206004820152600e60248201526d2ab735b737bbb71039b4b3b732b960911b6044820152606490fd5b908160209103126100f357516109cb81610172565b906001600160401b03809116911601906001600160401b0382116107dc57565b156124cc57565b60405162461bcd60e51b815260206004820152600e60248201526d04e6f646520696e207761726d75760941b6044820152606490fd5b1561250957565b60405162461bcd60e51b815260206004820152601060248201526f223ab83634b1b0ba329039b4b3b732b960811b6044820152606490fd5b919290925f915f5b8551811015612579576001811b8316612565575b600101612549565b92612571600191610b8c565b93905061255d565b5092916125859061241c565b5f915f5b865181101561279d576001811b82166125a5575b600101612589565b6126067f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031660206125e76125e1858c610cfd565b51612b20565b60405180948192630c038c9b60e11b8352600483019190602083019252565b0381845afa918215610f91575f9261276d575b50612625821515612453565b6040516308899cf560e41b815260048101839052906101c082602481845afa918215610f915761268f602060049481935f9161274e575b50016126826001600160401b0361267a83516001600160401b031690565b161515612453565b516001600160401b031690565b916040519384809262d4200960e41b82525afa918215610f91576126d6926126ce926126c2925f9261271e575b506124a5565b6001600160401b031690565b8710156124c5565b5f5b8581106126ff5750906001916126f76126f087610b8c565b9686610cfd565b52905061259d565b806127188361271060019489610cfd565b511415612502565b016126d8565b61274091925060203d8111612747575b61273881836103e2565b810190612490565b905f6126bc565b503d61272e565b61276791506101c03d8111610fbe57610fb081836103e2565b5f61265c565b61278f91925060203d8111612796575b61278781836103e2565b810190612444565b905f612619565b503d61277d565b505093505050565b604051906127b46020836103e2565b6020368337565b156127c257565b60405162461bcd60e51b8152602060048201526011602482015270109314ce881958d059190819985a5b1959607a1b6044820152606490fd5b608061285c612866929493946020612811611a10565b96816040519361282187866103e2565b86368637805185520151828401528051604084015201516060820152604090815193849161284f84846103e2565b8336843760065afa6127bb565b8051845260200190565b516020830152565b6040516060919061287f83826103e2565b6002815291601f1901825f5b82811061289757505050565b6020906128a2611a10565b8282850101520161288b565b604051906128bb826103c2565b81602060409182516128cd84826103e2565b8336823781528251926128e081856103e2565b3684370152565b604051606091906128f883826103e2565b6002815291601f1901825f5b82811061291057505050565b60209061291b6128ae565b82828501015201612904565b612946604051612936816103c2565b6001815260026020820152612b73565b9161294f61286e565b906129586128e7565b92825115610a03576020830152815115610a03576109cb93612978612bfe565b61298185610ce0565b5261298b84610ce0565b5061299583610ced565b5261299f82610ced565b506129a983610ced565b526129b382610ced565b50612d43565b90916129c761294691612e78565b612b73565b156129d357565b60405162461bcd60e51b815260206004820152601060248201526f115e1c1958dd195908191958da5b585b60821b6044820152606490fd5b15612a1257565b60405162461bcd60e51b815260206004820152600f60248201526e125b9d985b1a5908191958da5b585b608a1b6044820152606490fd5b15612a5057565b60405162461bcd60e51b815260206004820152601c60248201527f446563696d616c206578706f6e656e7420756e737570706f72746564000000006044820152606490fd5b15612a9c57565b60405162461bcd60e51b815260206004820152601860248201527f457870656374656420756e7369676e6564206269676e756d00000000000000006044820152606490fd5b15612ae857565b60405162461bcd60e51b815260206004820152601060248201526f416d6f756e7420746f6f206c6172676560801b6044820152606490fd5b612b2d608082511461143d565b6020810151906040810151906080606082015191015190604051926020840194855260408401526060830152608082015260808152612b6d60a0826103e2565b51902090565b612b7b611a10565b5080511580612bf2575b612bd9575f5160206130155f395f51905f5260208251920151065f5160206130155f395f51905f52035f5160206130155f395f51905f5281116107dc5760405191612bcf836103c2565b8252602082015290565b50604051612be6816103c2565b5f81525f602082015290565b50602081015115612b85565b612c066128ae565b50604051612c13816103c2565b7f198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c281527f1800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed6020820152604051612c68816103c2565b7f090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b81527f12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa602082015260405191612bcf836103c2565b15612cc557565b60405162461bcd60e51b815260206004820152601460248201527308498a67440d8cadccee8d040dad2e6dac2e8c6d60631b6044820152606490fd5b15612d0857565b60405162461bcd60e51b8152602060048201526013602482015272109314ce881c185a5c9a5b99c819985a5b1959606a1b6044820152606490fd5b612d508151835114612cbe565b8051612d63612d5e8261198f565b6119a5565b91612d75612d708361198f565b61241c565b935f5b838110612da75750505050612da260206001938193612d956127a5565b9485920160085afa612d01565b511490565b80612db360019261198f565b612dbd8286610cfd565b5151612dc9828a610cfd565b526020612dd68387610cfd565b510151612deb612de583610e0d565b8a610cfd565b52612df68285610cfd565b515151612e05612de583610e1b565b52612e1b612e138386610cfd565b515160200190565b51612e28612de583610e37565b526020612e358386610cfd565b51015151612e45612de583610e29565b52612e71612e6b612e646020612e5b8689610cfd565b51015160200190565b5192610e53565b89610cfd565b5201612d78565b612e9890612e84611a10565b505f5160206130155f395f51905f52900690565b5f905f5b6101008310612ee15760405162461bcd60e51b8152602060048201526014602482015273109314ce881a185cda151bd1cc4819985a5b195960621b6044820152606490fd5b612f54575f5160206130155f395f51905f52828208915f5160206130155f395f51905f526003818581818009090892612f1984612f59565b5f945f5160206130155f395f51905f5282800914612f3c57505060010191612e9c565b9250925050612f49610413565b918252602082015290565b6119bb565b60206040518181019282845282604083015282606083015260808201527f0c19139cb84c680a6e14116da060561765e05aa45a1c72a34f082305b61f3f5260a08201525f5160206130155f395f51905f5260c082015260c08152612fbe60e0826103e2565b81612fc76117eb565b01928391519060055afa15612fda575190565b60405162461bcd60e51b8152602060048201526012602482015271109314ce881b5bd9115e1c0819985a5b195960721b6044820152606490fdfe30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47 diff --git a/pkg/blockchain/evm/artifacts/YellowToken.abi b/pkg/blockchain/evm/artifacts/YellowToken.abi new file mode 100644 index 0000000..7021d04 --- /dev/null +++ b/pkg/blockchain/evm/artifacts/YellowToken.abi @@ -0,0 +1,549 @@ +[ + { + "type": "constructor", + "inputs": [ + { + "name": "treasury", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "DOMAIN_SEPARATOR", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "SUPPLY_CAP", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "allowance", + "inputs": [ + { + "name": "owner", + "type": "address", + "internalType": "address" + }, + { + "name": "spender", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "approve", + "inputs": [ + { + "name": "spender", + "type": "address", + "internalType": "address" + }, + { + "name": "value", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "balanceOf", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "decimals", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint8", + "internalType": "uint8" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "eip712Domain", + "inputs": [], + "outputs": [ + { + "name": "fields", + "type": "bytes1", + "internalType": "bytes1" + }, + { + "name": "name", + "type": "string", + "internalType": "string" + }, + { + "name": "version", + "type": "string", + "internalType": "string" + }, + { + "name": "chainId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "verifyingContract", + "type": "address", + "internalType": "address" + }, + { + "name": "salt", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "extensions", + "type": "uint256[]", + "internalType": "uint256[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "name", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "nonces", + "inputs": [ + { + "name": "owner", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "permit", + "inputs": [ + { + "name": "owner", + "type": "address", + "internalType": "address" + }, + { + "name": "spender", + "type": "address", + "internalType": "address" + }, + { + "name": "value", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "deadline", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "v", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "r", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "s", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "symbol", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "totalSupply", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "transfer", + "inputs": [ + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "value", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "transferFrom", + "inputs": [ + { + "name": "from", + "type": "address", + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "value", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "event", + "name": "Approval", + "inputs": [ + { + "name": "owner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "spender", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "value", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "EIP712DomainChanged", + "inputs": [], + "anonymous": false + }, + { + "type": "event", + "name": "Transfer", + "inputs": [ + { + "name": "from", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "value", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "ECDSAInvalidSignature", + "inputs": [] + }, + { + "type": "error", + "name": "ECDSAInvalidSignatureLength", + "inputs": [ + { + "name": "length", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "ECDSAInvalidSignatureS", + "inputs": [ + { + "name": "s", + "type": "bytes32", + "internalType": "bytes32" + } + ] + }, + { + "type": "error", + "name": "ERC20InsufficientAllowance", + "inputs": [ + { + "name": "spender", + "type": "address", + "internalType": "address" + }, + { + "name": "allowance", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "needed", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "ERC20InsufficientBalance", + "inputs": [ + { + "name": "sender", + "type": "address", + "internalType": "address" + }, + { + "name": "balance", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "needed", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "ERC20InvalidApprover", + "inputs": [ + { + "name": "approver", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ERC20InvalidReceiver", + "inputs": [ + { + "name": "receiver", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ERC20InvalidSender", + "inputs": [ + { + "name": "sender", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ERC20InvalidSpender", + "inputs": [ + { + "name": "spender", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "ERC2612ExpiredSignature", + "inputs": [ + { + "name": "deadline", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "ERC2612InvalidSigner", + "inputs": [ + { + "name": "signer", + "type": "address", + "internalType": "address" + }, + { + "name": "owner", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "InvalidAccountNonce", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + }, + { + "name": "currentNonce", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "InvalidAddress", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidShortString", + "inputs": [] + }, + { + "type": "error", + "name": "StringTooLong", + "inputs": [ + { + "name": "str", + "type": "string", + "internalType": "string" + } + ] + } +] diff --git a/pkg/blockchain/evm/artifacts/YellowToken.bin b/pkg/blockchain/evm/artifacts/YellowToken.bin new file mode 100644 index 0000000..507e8bc --- /dev/null +++ b/pkg/blockchain/evm/artifacts/YellowToken.bin @@ -0,0 +1 @@ +0x61016080604052346105045760208161140980380380916100208285610508565b83398101031261050457516001600160a01b038116908190036105045760405161004b604082610508565b60068152602081016559656c6c6f7760d01b81526040519061006e604083610508565b600682526559656c6c6f7760d01b602083015260405192610090604085610508565b600684526559454c4c4f5760d01b6020850152604051936100b2604086610508565b60018552603160f81b60208601908152845190946001600160401b0382116103ff5760035490600182811c921680156104fa575b60208310146103e15781601f849311610484575b50602090601f831160011461041e575f92610413575b50508160011b915f199060031b1c1916176003555b8051906001600160401b0382116103ff5760045490600182811c921680156103f5575b60208310146103e15781601f84931161036b575b50602090601f8311600114610305575f926102fa575b50508160011b915f199060031b1c1916176004555b6101908161052b565b6101205261019d846106be565b61014052519020918260e05251902080610100524660a0526040519060208201927f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f8452604083015260608201524660808201523060a082015260a0815261020660c082610508565b5190206080523060c05280156102eb576002546b204fce5e3e2502611000000081018091116102d757600255805f525f60205260405f206b204fce5e3e2502611000000081540190555f7fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef60206040516b204fce5e3e250261100000008152a3604051610c069081610803823960805181610925015260a051816109e2015260c051816108ef015260e051816109740152610100518161099a0152610120518161037a015261014051816103a30152f35b634e487b7160e01b5f52601160045260245ffd5b63e6c4247b60e01b5f5260045ffd5b015190505f80610172565b60045f9081528281209350601f198516905b818110610353575090846001959493921061033b575b505050811b01600455610187565b01515f1960f88460031b161c191690555f808061032d565b92936020600181928786015181550195019301610317565b8281111561015c5760045f52909150601f830160051c7f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b602085106103d9575b849392601f0160051c82900391015f5b8281106103c957505061015c565b5f818301558594506001016103bb565b5f91506103ab565b634e487b7160e01b5f52602260045260245ffd5b91607f1691610148565b634e487b7160e01b5f52604160045260245ffd5b015190505f80610110565b60035f9081528281209350601f198516905b81811061046c5750908460019594939210610454575b505050811b01600355610125565b01515f1960f88460031b161c191690555f8080610446565b92936020600181928786015181550195019301610430565b828111156100fa5760035f52909150601f830160051c7fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b602085106104f2575b849392601f0160051c82900391015f5b8281106104e25750506100fa565b5f818301558594506001016104d4565b5f91506104c4565b91607f16916100e6565b5f80fd5b601f909101601f19168101906001600160401b038211908210176103ff57604052565b908151602081105f146105a5575090601f815111610565576020815191015160208210610556571790565b5f198260200360031b1b161790565b604460209160405192839163305a27a960e01b83528160048401528051918291826024860152018484015e5f828201840152601f01601f19168101030190fd5b6001600160401b0381116103ff57600554600181811c911680156106b4575b60208210146103e157601f8111610675575b50602092601f821160011461061457928192935f92610609575b50508160011b915f199060031b1c19161760055560ff90565b015190505f806105f0565b601f1982169360055f52805f20915f5b86811061065d5750836001959610610645575b505050811b0160055560ff90565b01515f1960f88460031b161c191690555f8080610637565b91926020600181928685015181550194019201610624565b818111156105d65760055f5260205f20601f80840160051c809201920160051c03905f5b8281106106a75750506105d6565b5f82820155600101610699565b90607f16906105c4565b908151602081105f146106e9575090601f815111610565576020815191015160208210610556571790565b6001600160401b0381116103ff57600654600181811c911680156107f8575b60208210146103e157601f81116107b9575b50602092601f821160011461075857928192935f9261074d575b50508160011b915f199060031b1c19161760065560ff90565b015190505f80610734565b601f1982169360065f52805f20915f5b8681106107a15750836001959610610789575b505050811b0160065560ff90565b01515f1960f88460031b161c191690555f808061077b565b91926020600181928685015181550194019201610768565b8181111561071a5760065f5260205f20601f80840160051c809201920160051c03905f5b8281106107eb57505061071a565b5f828201556001016107dd565b90607f169061070856fe6080806040526004361015610012575f80fd5b5f3560e01c90816306fdde031461064e57508063095ea7b3146106285780630cfccc831461060257806318160ddd146105e557806323b872dd14610506578063313ce567146104eb5780633644e515146104c957806370a08231146104925780637ecebe001461045a57806384b0196e1461036257806395d89b4114610280578063a9059cbb1461024f578063d505accf1461010a5763dd62ed3e146100b6575f80fd5b34610106576040366003190112610106576100cf610714565b6100d761072a565b6001600160a01b039182165f908152600160209081526040808320949093168252928352819020549051908152f35b5f80fd5b346101065760e036600319011261010657610123610714565b61012b61072a565b604435906064359260843560ff811681036101065784421161023c576101ff6102089160018060a01b03841696875f52600760205260405f20908154916001830190556040519060208201927f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c984528a604084015260018060a01b038916606084015289608084015260a083015260c082015260c081526101cd60e0826107f9565b5190206101d86108ec565b906040519161190160f01b83526002830152602282015260c43591604260a4359220610b05565b90929192610b92565b6001600160a01b031684810361022557506102239350610a08565b005b84906325c0072360e11b5f5260045260245260445ffd5b8463313c898160e11b5f5260045260245ffd5b346101065760403660031901126101065761027561026b610714565b602435903361082f565b602060405160018152f35b34610106575f366003190112610106576040515f6004546102a081610740565b808452906001811690811561033e57506001146102e0575b6102dc836102c8818503826107f9565b6040519182916020835260208301906106f0565b0390f35b60045f9081527f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b939250905b808210610324575090915081016020016102c86102b8565b91926001816020925483858801015201910190929161030c565b60ff191660208086019190915291151560051b840190910191506102c890506102b8565b34610106575f366003190112610106576103fe61039e7f0000000000000000000000000000000000000000000000000000000000000000610a6b565b6103c77f0000000000000000000000000000000000000000000000000000000000000000610ace565b602061040c604051926103da83856107f9565b5f84525f368137604051958695600f60f81b875260e08588015260e08701906106f0565b9085820360408701526106f0565b4660608501523060808501525f60a085015283810360c08501528180845192838152019301915f5b82811061044357505050500390f35b835185528695509381019392810192600101610434565b34610106576020366003190112610106576001600160a01b0361047b610714565b165f526007602052602060405f2054604051908152f35b34610106576020366003190112610106576001600160a01b036104b3610714565b165f525f602052602060405f2054604051908152f35b34610106575f3660031901126101065760206104e36108ec565b604051908152f35b34610106575f36600319011261010657602060405160128152f35b346101065760603660031901126101065761051f610714565b61052761072a565b6001600160a01b0382165f818152600160209081526040808320338452909152902054909260443592915f198110610565575b50610275935061082f565b8381106105ca5784156105b75733156105a457610275945f52600160205260405f2060018060a01b0333165f526020528360405f20910390558461055a565b634a1406b160e11b5f525f60045260245ffd5b63e602df0560e01b5f525f60045260245ffd5b8390637dc7a0d960e11b5f523360045260245260445260645ffd5b34610106575f366003190112610106576020600254604051908152f35b34610106575f3660031901126101065760206040516b204fce5e3e250261100000008152f35b3461010657604036600319011261010657610275610644610714565b6024359033610a08565b34610106575f366003190112610106575f60035461066b81610740565b808452906001811690811561033e5750600114610692576102dc836102c8818503826107f9565b60035f9081527fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b939250905b8082106106d6575090915081016020016102c86102b8565b9192600181602092548385880101520191019092916106be565b805180835260209291819084018484015e5f828201840152601f01601f1916010190565b600435906001600160a01b038216820361010657565b602435906001600160a01b038216820361010657565b90600182811c9216801561076e575b602083101461075a57565b634e487b7160e01b5f52602260045260245ffd5b91607f169161074f565b5f929181549161078783610740565b80835292600181169081156107dc57506001146107a357505050565b5f9081526020812093945091925b8383106107c2575060209250010190565b6001816020929493945483858701015201910191906107b1565b915050602093945060ff929192191683830152151560051b010190565b90601f8019910116810190811067ffffffffffffffff82111761081b57604052565b634e487b7160e01b5f52604160045260245ffd5b6001600160a01b03169081156108d9576001600160a01b03169182156108c657815f525f60205260405f20548181106108ad57817fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef92602092855f525f84520360405f2055845f525f825260405f20818154019055604051908152a3565b8263391434e360e21b5f5260045260245260445260645ffd5b63ec442f0560e01b5f525f60045260245ffd5b634b637e8f60e11b5f525f60045260245ffd5b307f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031614806109df575b15610947577f000000000000000000000000000000000000000000000000000000000000000090565b60405160208101907f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f82527f000000000000000000000000000000000000000000000000000000000000000060408201527f000000000000000000000000000000000000000000000000000000000000000060608201524660808201523060a082015260a081526109d960c0826107f9565b51902090565b507f0000000000000000000000000000000000000000000000000000000000000000461461091e565b6001600160a01b03169081156105b7576001600160a01b03169182156105a45760207f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92591835f526001825260405f20855f5282528060405f2055604051908152a3565b60ff8114610ab15760ff811690601f8211610aa25760405191610a8f6040846107f9565b6020808452838101919036833783525290565b632cd44ac360e21b5f5260045ffd5b50604051610acb81610ac4816005610778565b03826107f9565b90565b60ff8114610af25760ff811690601f8211610aa25760405191610a8f6040846107f9565b50604051610acb81610ac4816006610778565b91907f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08411610b87579160209360809260ff5f9560405194855216868401526040830152606082015282805260015afa15610b7c575f516001600160a01b03811615610b7257905f905f90565b505f906001905f90565b6040513d5f823e3d90fd5b5050505f9160039190565b6004811015610bf25780610ba4575050565b60018103610bbb5763f645eedf60e01b5f5260045ffd5b60028103610bd6575063fce698f760e01b5f5260045260245ffd5b600314610be05750565b6335e2f38360e21b5f5260045260245ffd5b634e487b7160e01b5f52602160045260245ffd diff --git a/pkg/blockchain/evm/bls_cache.go b/pkg/blockchain/evm/bls_cache.go new file mode 100644 index 0000000..8da7922 --- /dev/null +++ b/pkg/blockchain/evm/bls_cache.go @@ -0,0 +1,463 @@ +package evm + +import ( + "context" + "fmt" + "log/slog" + "math/big" + "sync" + "time" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" + + "github.com/layer-3/clearnet-sdk/pkg/core" +) + +// BLSPubkeyCacheSize is the expected G2 serialization length (ADR-008 / ISSUE-035 +// WS-3): 128 bytes in the X.A1 || X.A0 || Y.A1 || Y.A0 layout. +const BLSPubkeyCacheSize = 128 + +// g2Zero reports whether a [4]*big.Int G2 pubkey equals the zero point. +// Registry-side a zero key indicates "no BLS key bound to this slot" — should +// not happen for active or unbonding nodes, but the cache treats them as absent. +func g2Zero(g2 [4]*big.Int) bool { + for _, c := range g2 { + if c != nil && c.Sign() != 0 { + return false + } + } + return true +} + +// serializeG2 converts a [4]*big.Int G2 pubkey (x_im, x_re, y_im, y_re) into +// the 128-byte serialized form consumed by `consensus.DeserializeG2` and +// carried on block attestations. Layout matches `consensus.SerializeG2`: +// [X.A1 || X.A0 || Y.A1 || Y.A0]. +func serializeG2(g2 [4]*big.Int) []byte { + buf := make([]byte, BLSPubkeyCacheSize) + for i, c := range g2 { + if c == nil { + continue + } + c.FillBytes(buf[i*32 : (i+1)*32]) + } + return buf +} + +// BLSPubkeyCache maintains an in-memory map of nodeId → 128-byte G2 pubkey +// populated from the on-chain Registry (ADR-008 / 2026-05-16 amendment: BLS +// keys live on NodeRecord directly). +// +// Lifecycle: +// 1. Backfill(ctx) — startup full-sync via paginated `GetNodeIds` + `GetNodes`. +// Records the block height (watermark) so follow-up subscription starts +// exactly at the next block — no gap, no duplicate processing. +// 2. Watch(ctx) — poll for NodeActivated + NodeReleased events and update the +// map. Held until N confirmations deep to absorb reorgs. +// +// Lookup is lock-free in the common case (sync.RWMutex read path) and never +// issues an RPC — missing entries return nil and the caller hard-fails per +// ADR-008 "drop the NodeID fallback" step. +type BLSPubkeyCache struct { + // client is used for log subscriptions (FilterLogs) and head polling. + // Optional — when nil the cache is populated via Backfill only and + // Watch is a no-op (used by tests that don't need live updates). + client *ethclient.Client + + // registryAddr is required when client is set, so FilterLogs can filter + // logs by contract address. + registryAddr common.Address + + // reg is the thin binding surface needed for the initial full-sync and + // ad-hoc single-node lookups. + reg BLSPubkeyCacheRegistry + + // confirmations is the number of L1 block confirmations to wait before + // committing a NodeActivated / NodeReleased event into the cache. + confirmations uint64 + + mu sync.RWMutex + // keys is the forward index: nodeId → 128-byte G2 pubkey. + keys map[core.NodeID][]byte + // byPubkey is the reverse index: string(pubkey) → nodeId. + byPubkey map[string]core.NodeID + watermark uint64 // highest block height whose events are committed +} + +// BLSPubkeyCacheRegistry is the subset of the Registry binding used by the +// cache. Decouples from *Registry so tests can inject a stub. +// +// TODO: refactor the cache off this binding-shaped interface. Backfill can use +// core.RegistryReader directly — GetNodes returns []*core.Slot, where each +// Slot already carries .ID (nodeId) and .BLSPubKey (the serialized G2), so the +// GetNodeIds call and the local g2Zero/serializeG2 helpers become unnecessary. +// Watch cannot: it needs ethclient.FilterLogs over NodeActivated/NodeReleased, +// which is event-subscription, not a contract read. Doing this properly means +// introducing a log-subscription seam (so the cache depends on RegistryReader +// for sync + a log source for updates) rather than the raw *ethclient.Client. +type BLSPubkeyCacheRegistry interface { + TotalNodes(opts *bind.CallOpts) (*big.Int, error) + GetNodeIds(opts *bind.CallOpts, offset *big.Int, limit *big.Int) ([][32]byte, error) + GetNodes(opts *bind.CallOpts, offset *big.Int, limit *big.Int) ([]NodeRecord, error) + GetNodeById(opts *bind.CallOpts, nodeId [32]byte) (NodeRecord, error) +} + +// compile-time check: the generated Registry binding satisfies the cache's +// read surface (methods are hoisted via RegistryCaller embedding). +var _ BLSPubkeyCacheRegistry = (*Registry)(nil) + +// NewBLSPubkeyCache constructs an empty cache. Call Backfill(ctx) before +// Watch(ctx). The client may be nil for unit-test wiring that only exercises +// the lookup path; production callers supply the live ethclient. +func NewBLSPubkeyCache(client *ethclient.Client, registryAddr common.Address, reg BLSPubkeyCacheRegistry, confirmations uint64) *BLSPubkeyCache { + return &BLSPubkeyCache{ + client: client, + registryAddr: registryAddr, + reg: reg, + confirmations: confirmations, + keys: make(map[core.NodeID][]byte), + byPubkey: make(map[string]core.NodeID), + } +} + +// assignLocked writes (id → pubkey) into both indices. Caller MUST hold the +// write lock. pubkey is expected to be exactly BLSPubkeyCacheSize bytes; the +// caller already validated length. If id previously mapped to a different +// pubkey (key rotation, re-lock), the stale reverse entry is removed first +// to keep the indices consistent. +func (c *BLSPubkeyCache) assignLocked(id core.NodeID, pubkey []byte) { + if old, ok := c.keys[id]; ok { + delete(c.byPubkey, string(old)) + } + c.keys[id] = pubkey + c.byPubkey[string(pubkey)] = id +} + +// removeLocked deletes id from both indices. Caller MUST hold the write lock. +func (c *BLSPubkeyCache) removeLocked(id core.NodeID) { + if old, ok := c.keys[id]; ok { + delete(c.byPubkey, string(old)) + } + delete(c.keys, id) +} + +// Lookup returns the cached 128-byte G2 pubkey for a Slot, or nil if unknown. +// Wired into `cluster.SigningCoordinator.BLSPubKeyLookup` in production so +// SealBlock populates block.Validators with Registry-authoritative pubkeys. +func (c *BLSPubkeyCache) Lookup(id core.NodeID) []byte { + c.mu.RLock() + defer c.mu.RUnlock() + return c.keys[id] +} + +// Size returns the current cache size. Intended for diagnostics / tests. +func (c *BLSPubkeyCache) Size() int { + c.mu.RLock() + defer c.mu.RUnlock() + return len(c.keys) +} + +// Watermark returns the highest confirmed L1 block whose events have been +// committed into the cache. Advances on Backfill + on each confirmed event +// batch. Intended for diagnostics. +func (c *BLSPubkeyCache) Watermark() uint64 { + c.mu.RLock() + defer c.mu.RUnlock() + return c.watermark +} + +// Put records a (nodeId → G2 pubkey) entry. Exposed so tests can seed the +// cache without a chain adapter; production code always goes through +// Backfill / Watch. pubkey must be exactly 128 bytes (BLSPubkeyCacheSize). +func (c *BLSPubkeyCache) Put(id core.NodeID, pubkey []byte) { + if len(pubkey) != BLSPubkeyCacheSize { + return + } + c.mu.Lock() + defer c.mu.Unlock() + buf := make([]byte, BLSPubkeyCacheSize) + copy(buf, pubkey) + c.assignLocked(id, buf) +} + +// Delete removes an entry. Invoked by Watch on a confirmed NodeReleased event. +func (c *BLSPubkeyCache) Delete(id core.NodeID) { + c.mu.Lock() + defer c.mu.Unlock() + c.removeLocked(id) +} + +// NodeIDForPubkey returns the NodeID that registered the given 128-byte G2 +// pubkey on chain, or (zero, false) if no active node carries that pubkey. +// Off-chain verifiers (custody) use this to authorize signers carried in a +// block's Attestation.Validators against the live Registry — without it, the +// pairing check alone would accept any well-formed signature, including those +// produced by self-elected attacker keys. +func (c *BLSPubkeyCache) NodeIDForPubkey(pubkey []byte) (core.NodeID, bool) { + if len(pubkey) != BLSPubkeyCacheSize { + return core.NodeID{}, false + } + c.mu.RLock() + defer c.mu.RUnlock() + id, ok := c.byPubkey[string(pubkey)] + return id, ok +} + +// Backfill seeds the cache from the Registry by paginating GetNodeIds + +// GetNodes (both iterate `_nodeIds` in identical order, so positions match +// by index). Records the block height at which the snapshot was taken as +// the watermark so subsequent Watch starts exactly at watermark+1. +// +// Returns the set of (active + unbonding) nodes whose BLS keys we will need +// during the unbonding window — slashing evidence against an unbonding node +// must still verify, so the cache holds keys until `NodeReleased`. +func (c *BLSPubkeyCache) Backfill(ctx context.Context) error { + if c.reg == nil { + return fmt.Errorf("bls cache: registry not configured") + } + opts := &bind.CallOpts{Context: ctx} + + // Snapshot block height FIRST so we know the floor for Watch — any event + // that arrives at a later block is guaranteed not already in the snapshot. + var startBlock uint64 + if c.client != nil { + bn, err := c.client.BlockNumber(ctx) + if err != nil { + return fmt.Errorf("bls cache: block number: %w", err) + } + startBlock = bn + } + + total, err := c.reg.TotalNodes(opts) + if err != nil { + return fmt.Errorf("bls cache: total nodes: %w", err) + } + if total == nil || total.Sign() == 0 { + c.setWatermark(startBlock) + return nil + } + + ids, err := c.reg.GetNodeIds(opts, big.NewInt(0), total) + if err != nil { + return fmt.Errorf("bls cache: get node ids: %w", err) + } + if len(ids) == 0 { + c.setWatermark(startBlock) + return nil + } + + records, err := c.reg.GetNodes(opts, big.NewInt(0), total) + if err != nil { + return fmt.Errorf("bls cache: get nodes: %w", err) + } + if len(records) != len(ids) { + return fmt.Errorf("bls cache: record count mismatch: got %d records, want %d", len(records), len(ids)) + } + + c.mu.Lock() + for i, id := range ids { + if g2Zero(records[i].BlsPubkeyG2) { + continue + } + c.assignLocked(core.NodeID(id), serializeG2(records[i].BlsPubkeyG2)) + } + // B6: monotonic watermark — never rewind past a prior Watch-driven advance. + if startBlock > c.watermark { + c.watermark = startBlock + } + c.mu.Unlock() + + slog.Info("BLSPubkeyCache backfill complete", + "entries", c.Size(), + "watermark", startBlock, + "total_nodes", total.String(), + ) + return nil +} + +func (c *BLSPubkeyCache) setWatermark(h uint64) { + c.mu.Lock() + // B6: monotonic watermark — never regress below a prior advance. + if h > c.watermark { + c.watermark = h + } + c.mu.Unlock() +} + +// Event signatures for the post 2026-05-16 Registry shape: +// +// event NodeActivated( +// address indexed operator, // topic[1] +// bytes32 indexed nodeId, // topic[2] +// uint32 indexed tokenId, // topic[3] +// uint256 collateral, // data[0..32) +// uint64 vestedAt, // data[32..64) +// uint256[4] blsPubkeyG2 // data[64..192) +// ); +// +// event NodeReleased( +// address indexed operator, // topic[1] +// bytes32 indexed nodeId, // topic[2] +// uint32 indexed tokenId, // topic[3] +// uint256 collateral // data[0..32) +// ); +var ( + nodeActivatedEventSig = crypto.Keccak256Hash([]byte("NodeActivated(address,bytes32,uint32,uint256,uint64,uint256[4])")) + nodeReleasedEventSig = crypto.Keccak256Hash([]byte("NodeReleased(address,bytes32,uint32,uint256)")) + + // blsCachePollInterval controls how often the watcher polls for new + // confirmed events. The cache is used by SigningCoordinator.BLSPubKeyLookup + // which fires on every block seal, so missing a newly-activated node for + // up to one interval is acceptable (PeerAnnouncement fast-path covers + // the gap). + blsCachePollInterval = 2 * time.Second +) + +// Watch polls the chain for NodeActivated / NodeReleased events and updates +// the cache once they reach c.confirmations depth. Returns when ctx is +// cancelled. +// +// Design: HTTP-poll (vs WatchLogs subscription) — subscriptions require a +// websocket client which is not always available (QA deploys use plain HTTP); +// polling is the common denominator. The poll interval is small enough (2s) +// that the fallback to PeerAnnouncement's fast-path covers any in-flight gap. +func (c *BLSPubkeyCache) Watch(ctx context.Context) error { + if c.client == nil { + <-ctx.Done() + return nil + } + if c.registryAddr == (common.Address{}) { + return fmt.Errorf("bls cache: registry address required for Watch") + } + + ticker := time.NewTicker(blsCachePollInterval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return nil + case <-ticker.C: + if err := c.pollOnce(ctx); err != nil { + slog.Debug("BLSPubkeyCache poll failed", "error", err) + } + } + } +} + +// pollOnce fetches every NodeActivated + NodeReleased log between +// (watermark, head-confirmations] and applies them to the cache in order. +// Safe to call concurrently with Lookup — only takes the write lock while +// mutating the map. +func (c *BLSPubkeyCache) pollOnce(ctx context.Context) error { + head, err := c.client.BlockNumber(ctx) + if err != nil { + return fmt.Errorf("block number: %w", err) + } + if head < c.confirmations { + return nil + } + confirmed := head - c.confirmations + + c.mu.RLock() + from := c.watermark + 1 + c.mu.RUnlock() + if from > confirmed { + return nil // nothing new to commit + } + + query := ethereum.FilterQuery{ + FromBlock: new(big.Int).SetUint64(from), + ToBlock: new(big.Int).SetUint64(confirmed), + Addresses: []common.Address{c.registryAddr}, + Topics: [][]common.Hash{ + {nodeActivatedEventSig, nodeReleasedEventSig}, + }, + } + + logs, err := c.client.FilterLogs(ctx, query) + if err != nil { + return fmt.Errorf("filter logs: %w", err) + } + + for _, l := range logs { + c.applyLog(l) + } + + c.mu.Lock() + c.watermark = confirmed + c.mu.Unlock() + return nil +} + +// applyLog parses a single NodeActivated / NodeReleased log and updates the +// cache. Unknown topics are ignored so a future ABI extension doesn't break +// the poller. +func (c *BLSPubkeyCache) applyLog(l types.Log) { + if len(l.Topics) < 3 { + return + } + // Topic[2] = indexed nodeId (bytes32) for NodeActivated and NodeReleased. + var nodeID core.NodeID + copy(nodeID[:], l.Topics[2].Bytes()) + + switch l.Topics[0] { + case nodeActivatedEventSig: + // Event data layout (non-indexed fields, 32-byte slots): + // [0..32) uint256 collateral + // [32..64) uint64 vestedAt (right-padded) + // [64..192) uint256[4] blsPubkeyG2 — [x_im, x_re, y_im, y_re] + if len(l.Data) < 192 { + slog.Warn("NodeActivated log has short data", "len", len(l.Data), "tx", l.TxHash.Hex()) + return + } + pubkey := make([]byte, BLSPubkeyCacheSize) + copy(pubkey, l.Data[64:192]) + c.mu.Lock() + c.assignLocked(nodeID, pubkey) + c.mu.Unlock() + slog.Debug("BLSPubkeyCache: NodeActivated committed", + "node", fmt.Sprintf("%x", nodeID[:8]), + "block", l.BlockNumber, + ) + case nodeReleasedEventSig: + c.mu.Lock() + c.removeLocked(nodeID) + c.mu.Unlock() + slog.Debug("BLSPubkeyCache: NodeReleased committed", + "node", fmt.Sprintf("%x", nodeID[:8]), + "block", l.BlockNumber, + ) + } +} + +// LookupWithRefresh returns the pubkey for id, issuing a single cold-miss +// `GetNodeById` RPC if the cache does not yet carry an entry. Intended as a +// narrow escape hatch: a freshly-activated node whose NodeActivated event +// has not yet cleared the confirmation window but whose partial signature +// the sealer wants to include. Returns nil + false when both the cache and +// the on-chain lookup return zero. +func (c *BLSPubkeyCache) LookupWithRefresh(ctx context.Context, id core.NodeID) ([]byte, bool) { + if k := c.Lookup(id); len(k) > 0 { + return k, true + } + if c.reg == nil { + return nil, false + } + rec, err := c.reg.GetNodeById(&bind.CallOpts{Context: ctx}, [32]byte(id)) + if err != nil { + slog.Debug("BLSPubkeyCache: cold-miss lookup failed", "error", err, "node", fmt.Sprintf("%x", id[:8])) + return nil, false + } + if g2Zero(rec.BlsPubkeyG2) { + return nil, false + } + buf := serializeG2(rec.BlsPubkeyG2) + c.Put(id, buf) + return buf, true +} diff --git a/pkg/blockchain/evm/bls_cache_test.go b/pkg/blockchain/evm/bls_cache_test.go new file mode 100644 index 0000000..778ecc8 --- /dev/null +++ b/pkg/blockchain/evm/bls_cache_test.go @@ -0,0 +1,324 @@ +package evm + +import ( + "context" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + + "github.com/layer-3/clearnet-sdk/pkg/core" +) + +// TestBLSPubkeyCache_PutDeleteLookup exercises the low-level mutation helpers +// without touching a chain adapter. +func TestBLSPubkeyCache_PutDeleteLookup(t *testing.T) { + cache := NewBLSPubkeyCache(nil, common.Address{}, nil, 0) + + id := core.NodeID{0x01} + good := make([]byte, BLSPubkeyCacheSize) + for i := range good { + good[i] = byte(i) + } + + // Wrong size is silently dropped — a mis-decoded event must not poison + // the cache. + cache.Put(id, good[:10]) + if cache.Lookup(id) != nil { + t.Fatal("Put accepted wrong-sized pubkey — cache may have been poisoned") + } + + cache.Put(id, good) + got := cache.Lookup(id) + if len(got) != BLSPubkeyCacheSize { + t.Fatalf("Lookup returned %d bytes, want %d", len(got), BLSPubkeyCacheSize) + } + for i, b := range good { + if got[i] != b { + t.Fatalf("byte %d mismatch: got %02x, want %02x", i, got[i], b) + } + } + + cache.Delete(id) + if cache.Lookup(id) != nil { + t.Fatal("Lookup returned an entry after Delete") + } +} + +// TestBLSPubkeyCache_ApplyLogDecodesNodeActivated pins the event-payload layout +// the poller consumes. A regenerated binding with a re-ordered struct would +// silently flip bytes otherwise. Topic[2] is the indexed nodeId; data carries +// (collateral, vestedAt, blsPubkeyG2[4]) as 6 ABI slots after the 2026-05-16 +// event slim-down. +func TestBLSPubkeyCache_ApplyLogDecodesNodeActivated(t *testing.T) { + var nodeID [32]byte + nodeID[0] = 0xAA + nodeID[31] = 0xBB + + data := make([]byte, 6*32) + // The first two slots (collateral, vestedAt) are ignored by the cache; + // fill them with junk to prove the slice boundaries are correct. + for j := 0; j < 64; j++ { + data[j] = 0xEF + } + // Per-quadrant marker so a mis-aligned slice would corrupt the result. + for i := 0; i < 4; i++ { + marker := byte(0x10 + i) + for j := 0; j < 32; j++ { + data[64+i*32+j] = marker + } + } + + log := types.Log{ + Topics: []common.Hash{nodeActivatedEventSig, common.Hash{}, common.BytesToHash(nodeID[:]), common.Hash{}}, + Data: data, + } + + cache := NewBLSPubkeyCache(nil, common.Address{}, nil, 0) + cache.applyLog(log) + + got := cache.Lookup(core.NodeID(nodeID)) + if len(got) != 128 { + t.Fatalf("post-applyLog pubkey length = %d, want 128", len(got)) + } + for i := 0; i < 4; i++ { + want := byte(0x10 + i) + for j := 0; j < 32; j++ { + if got[i*32+j] != want { + t.Fatalf("pubkey byte %d = %02x, want %02x (field %d) — NodeActivated event slice is misaligned", + i*32+j, got[i*32+j], want, i) + } + } + } +} + +// TestBLSPubkeyCache_ApplyLogEvictsOnRelease pins that NodeReleased clears +// the entry so a subsequent Lookup returns nil — a post-slashing operator +// re-activating on a fresh nodeId must not inherit the old pubkey. +func TestBLSPubkeyCache_ApplyLogEvictsOnRelease(t *testing.T) { + var nodeID [32]byte + nodeID[0] = 0xCC + + cache := NewBLSPubkeyCache(nil, common.Address{}, nil, 0) + cache.Put(core.NodeID(nodeID), make([]byte, BLSPubkeyCacheSize)) + if cache.Lookup(core.NodeID(nodeID)) == nil { + t.Fatal("Put did not populate the cache") + } + + log := types.Log{ + Topics: []common.Hash{nodeReleasedEventSig, common.Hash{}, common.BytesToHash(nodeID[:]), common.Hash{}}, + Data: []byte{}, + } + cache.applyLog(log) + + if cache.Lookup(core.NodeID(nodeID)) != nil { + t.Fatal("NodeReleased did not evict the entry") + } +} + +// TestBLSPubkeyCache_G2Zero_FourFieldGuard pins invariant B2: `g2Zero` must +// check ALL four big.Int fields of the [4]*big.Int G2 pubkey. A regression +// that drops any single field check would silently treat a partial-zero point +// (e.g. a valid G2 with one field == 0 by coincidence) as "unlocked" and evict +// the cache entry, causing downstream BLS verify failures. Or worse: a non-zero +// point whose unchecked field happens to be zero passes the null-guard and +// falsely-present entries poison the cache. +// +// ADR-008 §cache lookup semantics requires the Registry-returned zero point +// (all four fields = 0) to be treated as "not locked"; anything else is a +// locked node and must populate the cache. +func TestBLSPubkeyCache_G2Zero_FourFieldGuard(t *testing.T) { + // All-zero → treated as absent (the one true case where the predicate holds). + allZero := [4]*big.Int{big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0)} + if !g2Zero(allZero) { + t.Fatal("g2Zero(all-zero) = false, want true") + } + + // Each of the 4 fields individually non-zero → must be treated as present. + // A mutant that drops the Sign() check on any one field will see one of + // these four sub-cases return true (= cache-evict) and fail. + cases := []struct { + name string + p [4]*big.Int + }{ + {"[0] non-zero", [4]*big.Int{big.NewInt(1), big.NewInt(0), big.NewInt(0), big.NewInt(0)}}, + {"[1] non-zero", [4]*big.Int{big.NewInt(0), big.NewInt(1), big.NewInt(0), big.NewInt(0)}}, + {"[2] non-zero", [4]*big.Int{big.NewInt(0), big.NewInt(0), big.NewInt(1), big.NewInt(0)}}, + {"[3] non-zero", [4]*big.Int{big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(1)}}, + } + for _, tc := range cases { + if g2Zero(tc.p) { + t.Fatalf("%s: g2Zero returned true; partial-zero must be treated as PRESENT (mutation-kill: dropping the Sign() check on that field would misclassify a live node as unlocked)", tc.name) + } + } +} + +// stubBLSRegistry is a minimal BLSPubkeyCacheRegistry fake for watermark / +// backfill tests that don't need live block-number tracking. +type stubBLSRegistry struct { + total *big.Int + ids [][32]byte + pubkeys [][4]*big.Int + idsErr error + totalErr error +} + +func (s *stubBLSRegistry) TotalNodes(_ *bind.CallOpts) (*big.Int, error) { + return s.total, s.totalErr +} +func (s *stubBLSRegistry) GetNodeIds(_ *bind.CallOpts, _ *big.Int, _ *big.Int) ([][32]byte, error) { + return s.ids, s.idsErr +} +func (s *stubBLSRegistry) GetNodes(_ *bind.CallOpts, _ *big.Int, _ *big.Int) ([]NodeRecord, error) { + records := make([]NodeRecord, len(s.ids)) + for i := range s.ids { + records[i].BlsPubkeyG2 = s.pubkeys[i] + } + return records, nil +} +func (s *stubBLSRegistry) GetNodeById(_ *bind.CallOpts, id [32]byte) (NodeRecord, error) { + for i, known := range s.ids { + if known == id { + return NodeRecord{BlsPubkeyG2: s.pubkeys[i]}, nil + } + } + return NodeRecord{BlsPubkeyG2: [4]*big.Int{big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0)}}, nil +} + +// TestBLSPubkeyCache_Watermark_NeverRegresses pins invariant B6: the cache +// watermark never goes backward across Backfill calls. If the empty-total +// branch of Backfill dropped the setWatermark call (or if setWatermark were +// accidentally a no-op), a second Backfill after events have been polled into +// the cache would leave the watermark advanced-then-frozen-then-rewound, +// causing Watch.pollOnce to re-fetch the same log range on every tick. +// +// The test uses a stub registry + nil client so it can control the watermark +// transition directly: simulate a prior Watch that advanced the cache's +// watermark, then call Backfill and assert the watermark is not rewound below +// the prior value. +func TestBLSPubkeyCache_Watermark_NeverRegresses(t *testing.T) { + reg := &stubBLSRegistry{total: big.NewInt(0)} + cache := NewBLSPubkeyCache(nil, common.Address{}, reg, 0) + + // Simulate some prior Watch-driven advance (e.g. after confirmed events). + cache.setWatermark(42) + if got := cache.Watermark(); got != 42 { + t.Fatalf("pre-condition: Watermark = %d, want 42", got) + } + + // Second Backfill, against an empty registry + nil client, takes the + // `startBlock=0` path. Monotonicity must still hold — the watermark must + // NEVER regress from 42. + if err := cache.Backfill(context.Background()); err != nil { + t.Fatalf("Backfill: %v", err) + } + + if wm := cache.Watermark(); wm < 42 { + t.Fatalf("watermark regressed after Backfill: got %d, want >=42 (Backfill unconditionally assigned a lower startBlock — would cause double-application of confirmed events in Watch.pollOnce on next tick)", wm) + } +} + +// TestBLSPubkeyCache_NodeIDForPubkey pins the reverse index used by off-chain +// verifiers (custody) to authorize block-attached pubkeys against the live +// Registry. Forward and reverse indices MUST stay in lockstep across Put, +// Delete, NodeLocked, NodeWithdrawn, and re-Put (key rotation). +func TestBLSPubkeyCache_NodeIDForPubkey(t *testing.T) { + cache := NewBLSPubkeyCache(nil, common.Address{}, nil, 0) + + id := core.NodeID{0xAB, 0xCD} + pk := make([]byte, BLSPubkeyCacheSize) + for i := range pk { + pk[i] = byte(i) + } + + // Unknown pubkey before any Put. + if _, ok := cache.NodeIDForPubkey(pk); ok { + t.Fatal("NodeIDForPubkey returned true for never-seen pubkey") + } + + // Wrong-size input is rejected without touching the lock or map. + if _, ok := cache.NodeIDForPubkey(pk[:64]); ok { + t.Fatal("NodeIDForPubkey accepted a 64-byte input") + } + + cache.Put(id, pk) + got, ok := cache.NodeIDForPubkey(pk) + if !ok { + t.Fatal("NodeIDForPubkey did not find pubkey after Put") + } + if got != id { + t.Fatalf("reverse lookup mapped to wrong NodeID: got %x, want %x", got, id) + } + + // Re-Put with a different pubkey for the same NodeID — typical key rotation + // shape. The old reverse entry MUST be evicted so a stale pubkey can't + // authorize a signature after the chain has moved on. + rotated := make([]byte, BLSPubkeyCacheSize) + for i := range rotated { + rotated[i] = byte(0xFF - i) + } + cache.Put(id, rotated) + if _, ok := cache.NodeIDForPubkey(pk); ok { + t.Fatal("stale pubkey still reverse-resolves after key rotation; reverse index leaked") + } + if got, ok := cache.NodeIDForPubkey(rotated); !ok || got != id { + t.Fatalf("rotated pubkey did not reverse-resolve to %x: got %x ok=%v", id, got, ok) + } + + // Delete must clear both directions. + cache.Delete(id) + if _, ok := cache.NodeIDForPubkey(rotated); ok { + t.Fatal("reverse index still resolves after Delete; withdrawn validator could still authorize signatures") + } + if cache.Lookup(id) != nil { + t.Fatal("forward index still resolves after Delete") + } +} + +// TestBLSPubkeyCache_ReverseIndex_ViaEvents pins that the on-chain event +// path (applyLog) also maintains the reverse index. Both production write +// paths — Put for tests/cold-miss, applyLog for NodeLocked / NodeWithdrawn — +// must keep forward and reverse in sync, otherwise custody's authorization +// check becomes a partial view of the Registry depending on which path +// populated a given entry. +func TestBLSPubkeyCache_ReverseIndex_ViaEvents(t *testing.T) { + cache := NewBLSPubkeyCache(nil, common.Address{}, nil, 0) + + var nodeID [32]byte + nodeID[0] = 0x42 + + data := make([]byte, 6*32) + // Per-quadrant marker so the resulting pubkey is unique and predictable. + for i := 0; i < 4; i++ { + marker := byte(0x20 + i) + for j := 0; j < 32; j++ { + data[64+i*32+j] = marker + } + } + + lockLog := types.Log{ + Topics: []common.Hash{nodeActivatedEventSig, common.Hash{}, common.BytesToHash(nodeID[:]), common.Hash{}}, + Data: data, + } + cache.applyLog(lockLog) + + pk := cache.Lookup(core.NodeID(nodeID)) + if len(pk) != BLSPubkeyCacheSize { + t.Fatalf("Lookup after NodeActivated returned %d bytes, want %d", len(pk), BLSPubkeyCacheSize) + } + if got, ok := cache.NodeIDForPubkey(pk); !ok || got != core.NodeID(nodeID) { + t.Fatalf("NodeActivated did not populate reverse index: got %x ok=%v", got, ok) + } + + releaseLog := types.Log{ + Topics: []common.Hash{nodeReleasedEventSig, common.Hash{}, common.BytesToHash(nodeID[:]), common.Hash{}}, + Data: []byte{}, + } + cache.applyLog(releaseLog) + + if _, ok := cache.NodeIDForPubkey(pk); ok { + t.Fatal("NodeReleased did not evict reverse index entry") + } +} diff --git a/pkg/blockchain/evm/custody_abi.go b/pkg/blockchain/evm/custody_abi.go new file mode 100644 index 0000000..0ced375 --- /dev/null +++ b/pkg/blockchain/evm/custody_abi.go @@ -0,0 +1,856 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package evm + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// CustodyMetaData contains all meta data concerning the Custody contract. +var CustodyMetaData = &bind.MetaData{ + ABI: "[{\"type\":\"constructor\",\"inputs\":[{\"name\":\"initialSigners\",\"type\":\"address[]\",\"internalType\":\"address[]\"},{\"name\":\"initialThreshold\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"nonpayable\"},{\"type\":\"receive\",\"stateMutability\":\"payable\"},{\"type\":\"function\",\"name\":\"deposit\",\"inputs\":[{\"name\":\"account\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"asset\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"amount\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[],\"stateMutability\":\"payable\"},{\"type\":\"function\",\"name\":\"execute\",\"inputs\":[{\"name\":\"to\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"asset\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"amount\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"withdrawalId\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"signatures\",\"type\":\"bytes[]\",\"internalType\":\"bytes[]\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"executed\",\"inputs\":[{\"name\":\"withdrawalId\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"isSigner\",\"inputs\":[{\"name\":\"addr\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"signers\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address[]\",\"internalType\":\"address[]\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"threshold\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"updateSigners\",\"inputs\":[{\"name\":\"newSigners\",\"type\":\"address[]\",\"internalType\":\"address[]\"},{\"name\":\"newThreshold\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"signatures\",\"type\":\"bytes[]\",\"internalType\":\"bytes[]\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"event\",\"name\":\"Deposited\",\"inputs\":[{\"name\":\"depositor\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"account\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"asset\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"},{\"name\":\"amount\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"Executed\",\"inputs\":[{\"name\":\"withdrawalId\",\"type\":\"bytes32\",\"indexed\":true,\"internalType\":\"bytes32\"},{\"name\":\"to\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"asset\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"},{\"name\":\"amount\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"SignersUpdated\",\"inputs\":[{\"name\":\"newSigners\",\"type\":\"address[]\",\"indexed\":false,\"internalType\":\"address[]\"},{\"name\":\"newThreshold\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"}],\"anonymous\":false},{\"type\":\"error\",\"name\":\"ECDSAInvalidSignature\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ECDSAInvalidSignatureLength\",\"inputs\":[{\"name\":\"length\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]},{\"type\":\"error\",\"name\":\"ECDSAInvalidSignatureS\",\"inputs\":[{\"name\":\"s\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}]},{\"type\":\"error\",\"name\":\"ReentrancyGuardReentrantCall\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"SafeERC20FailedOperation\",\"inputs\":[{\"name\":\"token\",\"type\":\"address\",\"internalType\":\"address\"}]}]", + Bin: "0x60806040523461039c5761136c80380380610019816103a0565b928339810160408282031261039c5781516001600160401b03811161039c5782019181601f8401121561039c578251926001600160401b03841161029a578360051b9260206100698186016103a0565b8096815201916020839582010191821161039c57602001915b81831061037c575050506020015160017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055801561033757808351106102f35760038351106102ae575f5b83518110156101fc576001600160a01b036100e882866103c5565b5116156101b75780610126575b6001906001600160a01b0361010a82876103c5565b51165f528160205260405f208260ff19825416179055016100cd565b6001600160a01b0361013882866103c5565b51165f1982018281116101a3576001600160a01b039061015890876103c5565b5116106100f557606460405162461bcd60e51b815260206004820152602060248201527f5369676e657273206d75737420626520736f7274656420617363656e64696e676044820152fd5b634e487b7160e01b5f52601160045260245ffd5b60405162461bcd60e51b815260206004820152601360248201527f5a65726f2061646472657373207369676e6572000000000000000000000000006044820152606490fd5b509151906001600160401b03821161029a5768010000000000000000821161029a576003548260035580831061026f575b5060035f5260205f205f5b8381106102525784600255604051610f7e90816103ee8239f35b82516001600160a01b031681830155602090920191600101610238565b60035f52828060205f20019103905f5b82811061028d57505061022d565b5f8282015560010161027f565b634e487b7160e01b5f52604160045260245ffd5b60405162461bcd60e51b815260206004820152601760248201527f4e656564206174206c656173742033207369676e6572730000000000000000006044820152606490fd5b606460405162461bcd60e51b815260206004820152602060248201527f4e6f7420656e6f756768207369676e65727320666f72207468726573686f6c646044820152fd5b60405162461bcd60e51b815260206004820152601a60248201527f5468726573686f6c64206d75737420626520706f7369746976650000000000006044820152606490fd5b82516001600160a01b038116810361039c57815260209283019201610082565b5f80fd5b6040519190601f01601f191682016001600160401b0381118382101761029a57604052565b80518210156103d95760209160051b010190565b634e487b7160e01b5f52603260045260245ffdfe608080604052600436101561001c575b50361561001a575f80fd5b005b5f3560e01c9081630e2411ac1461066957508063191d0a49146103d557806342cde4e8146103b857806346f0975a146103035780637df73e27146102c65780638340f549146100a75763a9fcfb3314610075575f61000f565b346100a35760203660031901126100a3576004355f525f602052602060ff60405f2054166040519015158152f35b5f80fd5b60603660031901126100a3576100bb610aad565b6100c3610ac3565b604435916100cf610dc4565b6001600160a01b0316918215610292576100ea811515610b85565b6001600160a01b038216806101ad57508034036101735761014a7f4174a9435a04d04d274c76779cad136a41fde6937c56241c09ab9d3c7064a1a9915b604080516001600160a01b03909516855260208501919091523393918291820190565b0390a360017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055005b60405162461bcd60e51b815260206004820152601260248201527108aa89040ecc2d8eaca40dad2e6dac2e8c6d60731b6044820152606490fd5b3461024d576040516323b872dd60e01b5f5233600452306024528260445260205f60648180865af19060015f511482161561022c575b6040525f6060521561021a575061014a7f4174a9435a04d04d274c76779cad136a41fde6937c56241c09ab9d3c7064a1a991610127565b635274afe760e01b5f5260045260245ffd5b90600181151661024457823b15153d151616906101e3565b503d5f823e3d90fd5b60405162461bcd60e51b815260206004820152601b60248201527f4554482073656e742077697468204552433230206465706f73697400000000006044820152606490fd5b60405162461bcd60e51b815260206004820152600c60248201526b16995c9bc81858d8dbdd5b9d60a21b6044820152606490fd5b346100a35760203660031901126100a3576001600160a01b036102e7610aad565b165f526001602052602060ff60405f2054166040519015158152f35b346100a3575f3660031901126100a3576040518060206003549283815201809260035f525f516020610f5e5f395f51905f52905f5b818110610399575050508161034e910382610b2b565b604051918291602083019060208452518091526040830191905f5b818110610377575050500390f35b82516001600160a01b0316845285945060209384019390920191600101610369565b82546001600160a01b0316845260209093019260019283019201610338565b346100a3575f3660031901126100a3576020600254604051908152f35b346100a35760a03660031901126100a3576103ee610aad565b6103f6610ac3565b6064359060443560843567ffffffffffffffff81116100a35761041d903690600401610a7c565b9490610427610dc4565b845f525f60205260ff60405f205416610631576001600160a01b0382169586156105fb576104a49061045a851515610b85565b604051926020840146815230604086015289606086015260018060a01b038816948560808201528760a08201528960c082015260c0815261049c60e082610b2b565b519020610bdb565b845f525f60205260405f20600160ff1982541617905580155f1461057e57505f80808481945af13d15610579573d6104db81610bbf565b906104e96040519283610b2b565b81525f60203d92013e5b1561053e577fe57dd573634102b6cae74aab341f709f6fc3ae2bdc0a35f9a47a85f45b677a21915b604080516001600160a01b0390921682526020820192909252908190810161014a565b60405162461bcd60e51b8152602060048201526013602482015272115512081d1c985b9cd9995c8819985a5b1959606a1b6044820152606490fd5b6104f3565b90505f9291925060405163a9059cbb60e01b5f52856004528360245260205f60448180865af19060015f51148216156105e3575b6040521561021a5750907fe57dd573634102b6cae74aab341f709f6fc3ae2bdc0a35f9a47a85f45b677a219161051b565b90600181151661024457823b15153d151616906105b2565b60405162461bcd60e51b815260206004820152600e60248201526d16995c9bc81c9958da5c1a595b9d60921b6044820152606490fd5b60405162461bcd60e51b815260206004820152601060248201526f105b1c9958591e48195e1958dd5d195960821b6044820152606490fd5b346100a35760603660031901126100a35760043567ffffffffffffffff81116100a35761069a903690600401610a7c565b6024359260443567ffffffffffffffff81116100a3576106be903690600401610a7c565b90918515610a3a57508483106109f657600383106109b15761074c916040516020810190610700816106f28a898b87610ad9565b03601f198101835282610b2b565b519020604051602081019146835230604083015260806060830152600d60a08301526c7570646174655369676e65727360981b60c0830152608082015260c0815261049c60e082610b2b565b5f5b600354811015610790575f516020610f5e5f395f51905f528101546001600160a01b03165f908152600160208190526040909120805460ff191690550161074e565b50905f5b828110610881575067ffffffffffffffff821161086d5768010000000000000000821161086d57816003548160035580821061083f575b50508060035f525f5b838110610817575050610812837feb4dc7fab86d67670d7a4d7443a38860da1aa053f26529c8f41cc68e5d6a93369460025560405193849384610ad9565b0390a1005b600190602061082584610b71565b930192815f516020610f5e5f395f51905f520155016107d4565b035f5b818110610851578391506107cb565b5f8482015f516020610f5e5f395f51905f520155600101610842565b634e487b7160e01b5f52604160045260245ffd5b6001600160a01b0361089c610897838686610b4d565b610b71565b161561097657806108dc575b6001906001600160a01b036108c1610897838787610b4d565b165f528160205260405f208260ff1982541617905501610794565b6108ea610897828585610b4d565b5f198201828111610962576001600160a01b039061090d90610897908787610b4d565b166001600160a01b03909116116108a857606460405162461bcd60e51b815260206004820152602060248201527f5369676e657273206d75737420626520736f7274656420617363656e64696e676044820152fd5b634e487b7160e01b5f52601160045260245ffd5b60405162461bcd60e51b81526020600482015260136024820152722d32b9379030b2323932b9b99039b4b3b732b960691b6044820152606490fd5b60405162461bcd60e51b815260206004820152601760248201527f4e656564206174206c656173742033207369676e6572730000000000000000006044820152606490fd5b606460405162461bcd60e51b815260206004820152602060248201527f4e6f7420656e6f756768207369676e65727320666f72207468726573686f6c646044820152fd5b62461bcd60e51b815260206004820152601a60248201527f5468726573686f6c64206d75737420626520706f7369746976650000000000006044820152606490fd5b9181601f840112156100a35782359167ffffffffffffffff83116100a3576020808501948460051b0101116100a357565b600435906001600160a01b03821682036100a357565b602435906001600160a01b03821682036100a357565b6040808252810183905293929160608501905f905b808210610b0057505060209150930152565b909183356001600160a01b03811691908290036100a357908152602093840193019160010190610aee565b90601f8019910116810190811067ffffffffffffffff82111761086d57604052565b9190811015610b5d5760051b0190565b634e487b7160e01b5f52603260045260245ffd5b356001600160a01b03811681036100a35790565b15610b8c57565b60405162461bcd60e51b815260206004820152600b60248201526a16995c9bc8185b5bdd5b9d60aa1b6044820152606490fd5b67ffffffffffffffff811161086d57601f01601f191660200190565b9060025490818410610d8d575f948592835b86881015610d39578760051b840135601e19853603018112156100a35784019081359167ffffffffffffffff83116100a357602081019083360382136100a357610c3684610bbf565b90610c446040519283610b2b565b84825260208536920101116100a3575f602085610c7696610c6d95838601378301015288610e22565b90939193610e5c565b6001600160a01b038281169116811115610ce8575f52600160205260ff60405f20541615610cb457935f198114610962576001978801970193610bed565b60405162461bcd60e51b815260206004820152600c60248201526b2737ba10309039b4b3b732b960a11b6044820152606490fd5b60405162461bcd60e51b815260206004820152602360248201527f5369676e617475726573206e6f74206f726465726564206f72206475706c696360448201526261746560e81b6064820152608490fd5b509450945050905010610d4857565b60405162461bcd60e51b815260206004820152601d60248201527f496e73756666696369656e742076616c6964207369676e6174757265730000006044820152606490fd5b60405162461bcd60e51b815260206004820152600f60248201526e10995b1bddc81d1a1c995cda1bdb19608a1b6044820152606490fd5b60027f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f005414610e135760027f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055565b633ee5aeb560e01b5f5260045ffd5b8151919060418303610e5257610e4b9250602082015190606060408401519301515f1a90610ed0565b9192909190565b50505f9160029190565b6004811015610ebc5780610e6e575050565b60018103610e855763f645eedf60e01b5f5260045ffd5b60028103610ea0575063fce698f760e01b5f5260045260245ffd5b600314610eaa5750565b6335e2f38360e21b5f5260045260245ffd5b634e487b7160e01b5f52602160045260245ffd5b91907f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08411610f52579160209360809260ff5f9560405194855216868401526040830152606082015282805260015afa15610f47575f516001600160a01b03811615610f3d57905f905f90565b505f906001905f90565b6040513d5f823e3d90fd5b5050505f916003919056fec2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b", +} + +// CustodyABI is the input ABI used to generate the binding from. +// Deprecated: Use CustodyMetaData.ABI instead. +var CustodyABI = CustodyMetaData.ABI + +// CustodyBin is the compiled bytecode used for deploying new contracts. +// Deprecated: Use CustodyMetaData.Bin instead. +var CustodyBin = CustodyMetaData.Bin + +// DeployCustody deploys a new Ethereum contract, binding an instance of Custody to it. +func DeployCustody(auth *bind.TransactOpts, backend bind.ContractBackend, initialSigners []common.Address, initialThreshold *big.Int) (common.Address, *types.Transaction, *Custody, error) { + parsed, err := CustodyMetaData.GetAbi() + if err != nil { + return common.Address{}, nil, nil, err + } + if parsed == nil { + return common.Address{}, nil, nil, errors.New("GetABI returned nil") + } + + address, tx, contract, err := bind.DeployContract(auth, *parsed, common.FromHex(CustodyBin), backend, initialSigners, initialThreshold) + if err != nil { + return common.Address{}, nil, nil, err + } + return address, tx, &Custody{CustodyCaller: CustodyCaller{contract: contract}, CustodyTransactor: CustodyTransactor{contract: contract}, CustodyFilterer: CustodyFilterer{contract: contract}}, nil +} + +// Custody is an auto generated Go binding around an Ethereum contract. +type Custody struct { + CustodyCaller // Read-only binding to the contract + CustodyTransactor // Write-only binding to the contract + CustodyFilterer // Log filterer for contract events +} + +// CustodyCaller is an auto generated read-only Go binding around an Ethereum contract. +type CustodyCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// CustodyTransactor is an auto generated write-only Go binding around an Ethereum contract. +type CustodyTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// CustodyFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type CustodyFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// CustodySession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type CustodySession struct { + Contract *Custody // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// CustodyCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type CustodyCallerSession struct { + Contract *CustodyCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// CustodyTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type CustodyTransactorSession struct { + Contract *CustodyTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// CustodyRaw is an auto generated low-level Go binding around an Ethereum contract. +type CustodyRaw struct { + Contract *Custody // Generic contract binding to access the raw methods on +} + +// CustodyCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type CustodyCallerRaw struct { + Contract *CustodyCaller // Generic read-only contract binding to access the raw methods on +} + +// CustodyTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type CustodyTransactorRaw struct { + Contract *CustodyTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewCustody creates a new instance of Custody, bound to a specific deployed contract. +func NewCustody(address common.Address, backend bind.ContractBackend) (*Custody, error) { + contract, err := bindCustody(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &Custody{CustodyCaller: CustodyCaller{contract: contract}, CustodyTransactor: CustodyTransactor{contract: contract}, CustodyFilterer: CustodyFilterer{contract: contract}}, nil +} + +// NewCustodyCaller creates a new read-only instance of Custody, bound to a specific deployed contract. +func NewCustodyCaller(address common.Address, caller bind.ContractCaller) (*CustodyCaller, error) { + contract, err := bindCustody(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &CustodyCaller{contract: contract}, nil +} + +// NewCustodyTransactor creates a new write-only instance of Custody, bound to a specific deployed contract. +func NewCustodyTransactor(address common.Address, transactor bind.ContractTransactor) (*CustodyTransactor, error) { + contract, err := bindCustody(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &CustodyTransactor{contract: contract}, nil +} + +// NewCustodyFilterer creates a new log filterer instance of Custody, bound to a specific deployed contract. +func NewCustodyFilterer(address common.Address, filterer bind.ContractFilterer) (*CustodyFilterer, error) { + contract, err := bindCustody(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &CustodyFilterer{contract: contract}, nil +} + +// bindCustody binds a generic wrapper to an already deployed contract. +func bindCustody(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := CustodyMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Custody *CustodyRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Custody.Contract.CustodyCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Custody *CustodyRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Custody.Contract.CustodyTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Custody *CustodyRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Custody.Contract.CustodyTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Custody *CustodyCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Custody.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Custody *CustodyTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Custody.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Custody *CustodyTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Custody.Contract.contract.Transact(opts, method, params...) +} + +// Executed is a free data retrieval call binding the contract method 0xa9fcfb33. +// +// Solidity: function executed(bytes32 withdrawalId) view returns(bool) +func (_Custody *CustodyCaller) Executed(opts *bind.CallOpts, withdrawalId [32]byte) (bool, error) { + var out []interface{} + err := _Custody.contract.Call(opts, &out, "executed", withdrawalId) + + if err != nil { + return *new(bool), err + } + + out0 := *abi.ConvertType(out[0], new(bool)).(*bool) + + return out0, err + +} + +// Executed is a free data retrieval call binding the contract method 0xa9fcfb33. +// +// Solidity: function executed(bytes32 withdrawalId) view returns(bool) +func (_Custody *CustodySession) Executed(withdrawalId [32]byte) (bool, error) { + return _Custody.Contract.Executed(&_Custody.CallOpts, withdrawalId) +} + +// Executed is a free data retrieval call binding the contract method 0xa9fcfb33. +// +// Solidity: function executed(bytes32 withdrawalId) view returns(bool) +func (_Custody *CustodyCallerSession) Executed(withdrawalId [32]byte) (bool, error) { + return _Custody.Contract.Executed(&_Custody.CallOpts, withdrawalId) +} + +// IsSigner is a free data retrieval call binding the contract method 0x7df73e27. +// +// Solidity: function isSigner(address addr) view returns(bool) +func (_Custody *CustodyCaller) IsSigner(opts *bind.CallOpts, addr common.Address) (bool, error) { + var out []interface{} + err := _Custody.contract.Call(opts, &out, "isSigner", addr) + + if err != nil { + return *new(bool), err + } + + out0 := *abi.ConvertType(out[0], new(bool)).(*bool) + + return out0, err + +} + +// IsSigner is a free data retrieval call binding the contract method 0x7df73e27. +// +// Solidity: function isSigner(address addr) view returns(bool) +func (_Custody *CustodySession) IsSigner(addr common.Address) (bool, error) { + return _Custody.Contract.IsSigner(&_Custody.CallOpts, addr) +} + +// IsSigner is a free data retrieval call binding the contract method 0x7df73e27. +// +// Solidity: function isSigner(address addr) view returns(bool) +func (_Custody *CustodyCallerSession) IsSigner(addr common.Address) (bool, error) { + return _Custody.Contract.IsSigner(&_Custody.CallOpts, addr) +} + +// Signers is a free data retrieval call binding the contract method 0x46f0975a. +// +// Solidity: function signers() view returns(address[]) +func (_Custody *CustodyCaller) Signers(opts *bind.CallOpts) ([]common.Address, error) { + var out []interface{} + err := _Custody.contract.Call(opts, &out, "signers") + + if err != nil { + return *new([]common.Address), err + } + + out0 := *abi.ConvertType(out[0], new([]common.Address)).(*[]common.Address) + + return out0, err + +} + +// Signers is a free data retrieval call binding the contract method 0x46f0975a. +// +// Solidity: function signers() view returns(address[]) +func (_Custody *CustodySession) Signers() ([]common.Address, error) { + return _Custody.Contract.Signers(&_Custody.CallOpts) +} + +// Signers is a free data retrieval call binding the contract method 0x46f0975a. +// +// Solidity: function signers() view returns(address[]) +func (_Custody *CustodyCallerSession) Signers() ([]common.Address, error) { + return _Custody.Contract.Signers(&_Custody.CallOpts) +} + +// Threshold is a free data retrieval call binding the contract method 0x42cde4e8. +// +// Solidity: function threshold() view returns(uint256) +func (_Custody *CustodyCaller) Threshold(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _Custody.contract.Call(opts, &out, "threshold") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// Threshold is a free data retrieval call binding the contract method 0x42cde4e8. +// +// Solidity: function threshold() view returns(uint256) +func (_Custody *CustodySession) Threshold() (*big.Int, error) { + return _Custody.Contract.Threshold(&_Custody.CallOpts) +} + +// Threshold is a free data retrieval call binding the contract method 0x42cde4e8. +// +// Solidity: function threshold() view returns(uint256) +func (_Custody *CustodyCallerSession) Threshold() (*big.Int, error) { + return _Custody.Contract.Threshold(&_Custody.CallOpts) +} + +// Deposit is a paid mutator transaction binding the contract method 0x8340f549. +// +// Solidity: function deposit(address account, address asset, uint256 amount) payable returns() +func (_Custody *CustodyTransactor) Deposit(opts *bind.TransactOpts, account common.Address, asset common.Address, amount *big.Int) (*types.Transaction, error) { + return _Custody.contract.Transact(opts, "deposit", account, asset, amount) +} + +// Deposit is a paid mutator transaction binding the contract method 0x8340f549. +// +// Solidity: function deposit(address account, address asset, uint256 amount) payable returns() +func (_Custody *CustodySession) Deposit(account common.Address, asset common.Address, amount *big.Int) (*types.Transaction, error) { + return _Custody.Contract.Deposit(&_Custody.TransactOpts, account, asset, amount) +} + +// Deposit is a paid mutator transaction binding the contract method 0x8340f549. +// +// Solidity: function deposit(address account, address asset, uint256 amount) payable returns() +func (_Custody *CustodyTransactorSession) Deposit(account common.Address, asset common.Address, amount *big.Int) (*types.Transaction, error) { + return _Custody.Contract.Deposit(&_Custody.TransactOpts, account, asset, amount) +} + +// Execute is a paid mutator transaction binding the contract method 0x191d0a49. +// +// Solidity: function execute(address to, address asset, uint256 amount, bytes32 withdrawalId, bytes[] signatures) returns() +func (_Custody *CustodyTransactor) Execute(opts *bind.TransactOpts, to common.Address, asset common.Address, amount *big.Int, withdrawalId [32]byte, signatures [][]byte) (*types.Transaction, error) { + return _Custody.contract.Transact(opts, "execute", to, asset, amount, withdrawalId, signatures) +} + +// Execute is a paid mutator transaction binding the contract method 0x191d0a49. +// +// Solidity: function execute(address to, address asset, uint256 amount, bytes32 withdrawalId, bytes[] signatures) returns() +func (_Custody *CustodySession) Execute(to common.Address, asset common.Address, amount *big.Int, withdrawalId [32]byte, signatures [][]byte) (*types.Transaction, error) { + return _Custody.Contract.Execute(&_Custody.TransactOpts, to, asset, amount, withdrawalId, signatures) +} + +// Execute is a paid mutator transaction binding the contract method 0x191d0a49. +// +// Solidity: function execute(address to, address asset, uint256 amount, bytes32 withdrawalId, bytes[] signatures) returns() +func (_Custody *CustodyTransactorSession) Execute(to common.Address, asset common.Address, amount *big.Int, withdrawalId [32]byte, signatures [][]byte) (*types.Transaction, error) { + return _Custody.Contract.Execute(&_Custody.TransactOpts, to, asset, amount, withdrawalId, signatures) +} + +// UpdateSigners is a paid mutator transaction binding the contract method 0x0e2411ac. +// +// Solidity: function updateSigners(address[] newSigners, uint256 newThreshold, bytes[] signatures) returns() +func (_Custody *CustodyTransactor) UpdateSigners(opts *bind.TransactOpts, newSigners []common.Address, newThreshold *big.Int, signatures [][]byte) (*types.Transaction, error) { + return _Custody.contract.Transact(opts, "updateSigners", newSigners, newThreshold, signatures) +} + +// UpdateSigners is a paid mutator transaction binding the contract method 0x0e2411ac. +// +// Solidity: function updateSigners(address[] newSigners, uint256 newThreshold, bytes[] signatures) returns() +func (_Custody *CustodySession) UpdateSigners(newSigners []common.Address, newThreshold *big.Int, signatures [][]byte) (*types.Transaction, error) { + return _Custody.Contract.UpdateSigners(&_Custody.TransactOpts, newSigners, newThreshold, signatures) +} + +// UpdateSigners is a paid mutator transaction binding the contract method 0x0e2411ac. +// +// Solidity: function updateSigners(address[] newSigners, uint256 newThreshold, bytes[] signatures) returns() +func (_Custody *CustodyTransactorSession) UpdateSigners(newSigners []common.Address, newThreshold *big.Int, signatures [][]byte) (*types.Transaction, error) { + return _Custody.Contract.UpdateSigners(&_Custody.TransactOpts, newSigners, newThreshold, signatures) +} + +// Receive is a paid mutator transaction binding the contract receive function. +// +// Solidity: receive() payable returns() +func (_Custody *CustodyTransactor) Receive(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Custody.contract.RawTransact(opts, nil) // calldata is disallowed for receive function +} + +// Receive is a paid mutator transaction binding the contract receive function. +// +// Solidity: receive() payable returns() +func (_Custody *CustodySession) Receive() (*types.Transaction, error) { + return _Custody.Contract.Receive(&_Custody.TransactOpts) +} + +// Receive is a paid mutator transaction binding the contract receive function. +// +// Solidity: receive() payable returns() +func (_Custody *CustodyTransactorSession) Receive() (*types.Transaction, error) { + return _Custody.Contract.Receive(&_Custody.TransactOpts) +} + +// CustodyDepositedIterator is returned from FilterDeposited and is used to iterate over the raw logs and unpacked data for Deposited events raised by the Custody contract. +type CustodyDepositedIterator struct { + Event *CustodyDeposited // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *CustodyDepositedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(CustodyDeposited) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(CustodyDeposited) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *CustodyDepositedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *CustodyDepositedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// CustodyDeposited represents a Deposited event raised by the Custody contract. +type CustodyDeposited struct { + Depositor common.Address + Account common.Address + Asset common.Address + Amount *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterDeposited is a free log retrieval operation binding the contract event 0x4174a9435a04d04d274c76779cad136a41fde6937c56241c09ab9d3c7064a1a9. +// +// Solidity: event Deposited(address indexed depositor, address indexed account, address asset, uint256 amount) +func (_Custody *CustodyFilterer) FilterDeposited(opts *bind.FilterOpts, depositor []common.Address, account []common.Address) (*CustodyDepositedIterator, error) { + + var depositorRule []interface{} + for _, depositorItem := range depositor { + depositorRule = append(depositorRule, depositorItem) + } + var accountRule []interface{} + for _, accountItem := range account { + accountRule = append(accountRule, accountItem) + } + + logs, sub, err := _Custody.contract.FilterLogs(opts, "Deposited", depositorRule, accountRule) + if err != nil { + return nil, err + } + return &CustodyDepositedIterator{contract: _Custody.contract, event: "Deposited", logs: logs, sub: sub}, nil +} + +// WatchDeposited is a free log subscription operation binding the contract event 0x4174a9435a04d04d274c76779cad136a41fde6937c56241c09ab9d3c7064a1a9. +// +// Solidity: event Deposited(address indexed depositor, address indexed account, address asset, uint256 amount) +func (_Custody *CustodyFilterer) WatchDeposited(opts *bind.WatchOpts, sink chan<- *CustodyDeposited, depositor []common.Address, account []common.Address) (event.Subscription, error) { + + var depositorRule []interface{} + for _, depositorItem := range depositor { + depositorRule = append(depositorRule, depositorItem) + } + var accountRule []interface{} + for _, accountItem := range account { + accountRule = append(accountRule, accountItem) + } + + logs, sub, err := _Custody.contract.WatchLogs(opts, "Deposited", depositorRule, accountRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(CustodyDeposited) + if err := _Custody.contract.UnpackLog(event, "Deposited", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseDeposited is a log parse operation binding the contract event 0x4174a9435a04d04d274c76779cad136a41fde6937c56241c09ab9d3c7064a1a9. +// +// Solidity: event Deposited(address indexed depositor, address indexed account, address asset, uint256 amount) +func (_Custody *CustodyFilterer) ParseDeposited(log types.Log) (*CustodyDeposited, error) { + event := new(CustodyDeposited) + if err := _Custody.contract.UnpackLog(event, "Deposited", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// CustodyExecutedIterator is returned from FilterExecuted and is used to iterate over the raw logs and unpacked data for Executed events raised by the Custody contract. +type CustodyExecutedIterator struct { + Event *CustodyExecuted // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *CustodyExecutedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(CustodyExecuted) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(CustodyExecuted) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *CustodyExecutedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *CustodyExecutedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// CustodyExecuted represents a Executed event raised by the Custody contract. +type CustodyExecuted struct { + WithdrawalId [32]byte + To common.Address + Asset common.Address + Amount *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterExecuted is a free log retrieval operation binding the contract event 0xe57dd573634102b6cae74aab341f709f6fc3ae2bdc0a35f9a47a85f45b677a21. +// +// Solidity: event Executed(bytes32 indexed withdrawalId, address indexed to, address asset, uint256 amount) +func (_Custody *CustodyFilterer) FilterExecuted(opts *bind.FilterOpts, withdrawalId [][32]byte, to []common.Address) (*CustodyExecutedIterator, error) { + + var withdrawalIdRule []interface{} + for _, withdrawalIdItem := range withdrawalId { + withdrawalIdRule = append(withdrawalIdRule, withdrawalIdItem) + } + var toRule []interface{} + for _, toItem := range to { + toRule = append(toRule, toItem) + } + + logs, sub, err := _Custody.contract.FilterLogs(opts, "Executed", withdrawalIdRule, toRule) + if err != nil { + return nil, err + } + return &CustodyExecutedIterator{contract: _Custody.contract, event: "Executed", logs: logs, sub: sub}, nil +} + +// WatchExecuted is a free log subscription operation binding the contract event 0xe57dd573634102b6cae74aab341f709f6fc3ae2bdc0a35f9a47a85f45b677a21. +// +// Solidity: event Executed(bytes32 indexed withdrawalId, address indexed to, address asset, uint256 amount) +func (_Custody *CustodyFilterer) WatchExecuted(opts *bind.WatchOpts, sink chan<- *CustodyExecuted, withdrawalId [][32]byte, to []common.Address) (event.Subscription, error) { + + var withdrawalIdRule []interface{} + for _, withdrawalIdItem := range withdrawalId { + withdrawalIdRule = append(withdrawalIdRule, withdrawalIdItem) + } + var toRule []interface{} + for _, toItem := range to { + toRule = append(toRule, toItem) + } + + logs, sub, err := _Custody.contract.WatchLogs(opts, "Executed", withdrawalIdRule, toRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(CustodyExecuted) + if err := _Custody.contract.UnpackLog(event, "Executed", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseExecuted is a log parse operation binding the contract event 0xe57dd573634102b6cae74aab341f709f6fc3ae2bdc0a35f9a47a85f45b677a21. +// +// Solidity: event Executed(bytes32 indexed withdrawalId, address indexed to, address asset, uint256 amount) +func (_Custody *CustodyFilterer) ParseExecuted(log types.Log) (*CustodyExecuted, error) { + event := new(CustodyExecuted) + if err := _Custody.contract.UnpackLog(event, "Executed", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// CustodySignersUpdatedIterator is returned from FilterSignersUpdated and is used to iterate over the raw logs and unpacked data for SignersUpdated events raised by the Custody contract. +type CustodySignersUpdatedIterator struct { + Event *CustodySignersUpdated // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *CustodySignersUpdatedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(CustodySignersUpdated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(CustodySignersUpdated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *CustodySignersUpdatedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *CustodySignersUpdatedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// CustodySignersUpdated represents a SignersUpdated event raised by the Custody contract. +type CustodySignersUpdated struct { + NewSigners []common.Address + NewThreshold *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterSignersUpdated is a free log retrieval operation binding the contract event 0xeb4dc7fab86d67670d7a4d7443a38860da1aa053f26529c8f41cc68e5d6a9336. +// +// Solidity: event SignersUpdated(address[] newSigners, uint256 newThreshold) +func (_Custody *CustodyFilterer) FilterSignersUpdated(opts *bind.FilterOpts) (*CustodySignersUpdatedIterator, error) { + + logs, sub, err := _Custody.contract.FilterLogs(opts, "SignersUpdated") + if err != nil { + return nil, err + } + return &CustodySignersUpdatedIterator{contract: _Custody.contract, event: "SignersUpdated", logs: logs, sub: sub}, nil +} + +// WatchSignersUpdated is a free log subscription operation binding the contract event 0xeb4dc7fab86d67670d7a4d7443a38860da1aa053f26529c8f41cc68e5d6a9336. +// +// Solidity: event SignersUpdated(address[] newSigners, uint256 newThreshold) +func (_Custody *CustodyFilterer) WatchSignersUpdated(opts *bind.WatchOpts, sink chan<- *CustodySignersUpdated) (event.Subscription, error) { + + logs, sub, err := _Custody.contract.WatchLogs(opts, "SignersUpdated") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(CustodySignersUpdated) + if err := _Custody.contract.UnpackLog(event, "SignersUpdated", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseSignersUpdated is a log parse operation binding the contract event 0xeb4dc7fab86d67670d7a4d7443a38860da1aa053f26529c8f41cc68e5d6a9336. +// +// Solidity: event SignersUpdated(address[] newSigners, uint256 newThreshold) +func (_Custody *CustodyFilterer) ParseSignersUpdated(log types.Log) (*CustodySignersUpdated, error) { + event := new(CustodySignersUpdated) + if err := _Custody.contract.UnpackLog(event, "SignersUpdated", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} diff --git a/pkg/blockchain/evm/depositor.go b/pkg/blockchain/evm/depositor.go new file mode 100644 index 0000000..c3427ea --- /dev/null +++ b/pkg/blockchain/evm/depositor.go @@ -0,0 +1,93 @@ +package evm + +import ( + "context" + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + + "github.com/layer-3/clearnet-sdk/pkg/core" + "github.com/layer-3/clearnet-sdk/pkg/decimal" + "github.com/layer-3/clearnet-sdk/pkg/sign" +) + +// Depositor moves funds into the EVM Custody vault on behalf of the depositor +// whose key the supplied sign.Signer holds. It implements core.VaultDepositor. +type Depositor struct { + client *ethclient.Client + custody *Custody + custodyAddr common.Address + signer sign.Signer +} + +var _ core.VaultDepositor = (*Depositor)(nil) + +// NewDepositor binds the Custody vault at custodyAddr over client; signer is the +// depositor's secp256k1 identity (it pays and, for ERC-20, approves). +func NewDepositor(client *ethclient.Client, custodyAddr common.Address, signer sign.Signer) (*Depositor, error) { + custody, err := NewCustody(custodyAddr, client) + if err != nil { + return nil, fmt.Errorf("load custody: %w", err) + } + return &Depositor{client: client, custody: custody, custodyAddr: custodyAddr, signer: signer}, nil +} + +// Deposit credits `account` with `amount` of `asset`. For an ERC-20 (asset is a +// non-zero hex address) it approves the vault then calls +// Custody.deposit(account, asset, amount); for the zero address it sends native +// ETH with msg.value == amount. Blocks until the deposit tx mines. +func (d *Depositor) Deposit(ctx context.Context, asset string, amount decimal.Decimal, account string) (core.TxRef, error) { + assetAddr, err := depositAssetAddress(asset) + if err != nil { + return core.TxRef{}, err + } + amt := amount.BigInt() + accountAddr := common.HexToAddress(account) + + if assetAddr == (common.Address{}) { + opts, _, err := signerTransactOpts(ctx, d.client, d.signer) + if err != nil { + return core.TxRef{}, err + } + opts.Value = amt + tx, err := d.custody.Deposit(opts, accountAddr, common.Address{}, amt) + if err != nil { + return core.TxRef{}, fmt.Errorf("ETH deposit: %w", err) + } + if err := waitMined(ctx, d.client, tx); err != nil { + return core.TxRef{}, err + } + return core.TxRef{Hash: tx.Hash(), Raw: tx.Hash().Hex()}, nil + } + + // ERC-20: approve the vault, then deposit. + token, err := NewMockERC20(assetAddr, d.client) + if err != nil { + return core.TxRef{}, fmt.Errorf("ERC20 bind %s: %w", assetAddr.Hex(), err) + } + approveOpts, _, err := signerTransactOpts(ctx, d.client, d.signer) + if err != nil { + return core.TxRef{}, err + } + approveTx, err := token.Approve(approveOpts, d.custodyAddr, amt) + if err != nil { + return core.TxRef{}, fmt.Errorf("ERC20 approve: %w", err) + } + if err := waitMined(ctx, d.client, approveTx); err != nil { + return core.TxRef{}, fmt.Errorf("ERC20 approve wait: %w", err) + } + + depositOpts, _, err := signerTransactOpts(ctx, d.client, d.signer) + if err != nil { + return core.TxRef{}, err + } + tx, err := d.custody.Deposit(depositOpts, accountAddr, assetAddr, amt) + if err != nil { + return core.TxRef{}, fmt.Errorf("ERC20 deposit: %w", err) + } + if err := waitMined(ctx, d.client, tx); err != nil { + return core.TxRef{}, err + } + return core.TxRef{Hash: tx.Hash(), Raw: tx.Hash().Hex()}, nil +} diff --git a/pkg/blockchain/evm/faucet_abi.go b/pkg/blockchain/evm/faucet_abi.go new file mode 100644 index 0000000..3a0a6eb --- /dev/null +++ b/pkg/blockchain/evm/faucet_abi.go @@ -0,0 +1,1041 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package evm + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// FaucetMetaData contains all meta data concerning the Faucet contract. +var FaucetMetaData = &bind.MetaData{ + ABI: "[{\"type\":\"constructor\",\"inputs\":[{\"name\":\"_token\",\"type\":\"address\",\"internalType\":\"contractIERC20\"},{\"name\":\"_dripAmount\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"_cooldown\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"TOKEN\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"contractIERC20\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"cooldown\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"drip\",\"inputs\":[],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"dripAmount\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"dripTo\",\"inputs\":[{\"name\":\"recipient\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"lastDrip\",\"inputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"owner\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"setCooldown\",\"inputs\":[{\"name\":\"_cooldown\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"setDripAmount\",\"inputs\":[{\"name\":\"_dripAmount\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"setOwner\",\"inputs\":[{\"name\":\"_owner\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"withdraw\",\"inputs\":[{\"name\":\"amount\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"event\",\"name\":\"CooldownUpdated\",\"inputs\":[{\"name\":\"newCooldown\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"DripAmountUpdated\",\"inputs\":[{\"name\":\"newAmount\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"Dripped\",\"inputs\":[{\"name\":\"recipient\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"amount\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"OwnerUpdated\",\"inputs\":[{\"name\":\"newOwner\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"}],\"anonymous\":false}]", + Bin: "0x60a03461009a57601f6107bb38819003918201601f19168301916001600160401b0383118484101761009e5780849260609460405283398101031261009a578051906001600160a01b038216820361009a5760406020820151910151916080523360018060a01b03195f5416175f5560015560025560405161070890816100b3823960805181818161011d0152818161026d01526104e30152f35b5f80fd5b634e487b7160e01b5f52604160045260245ffdfe6080806040526004361015610012575f80fd5b5f3560e01c9081630935f004146103d35750806313af40351461032c5780632e1a7d4d1461021e57806335a1529b146102015780634fc3f41a146101b5578063543f8c5814610169578063787a08a61461014c57806382bfefc8146101085780638da5cb5b146100e15780639f678cca146100c85763cabee26e14610095575f80fd5b346100c45760203660031901126100c4576004356001600160a01b03811681036100c4576100c2906104a6565b005b5f80fd5b346100c4575f3660031901126100c4576100c2336104a6565b346100c4575f3660031901126100c4575f546040516001600160a01b039091168152602090f35b346100c4575f3660031901126100c4576040517f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03168152602090f35b346100c4575f3660031901126100c4576020600254604051908152f35b346100c45760203660031901126100c4577f33f3faee0788ab897d8f674abe1dde6d93ba901e4a1502161294734ba178e3c760206004356101a861045a565b80600155604051908152a1005b346100c45760203660031901126100c4577f583d8b24c5439ab7d810e51e37e8db41ba66f1168fd7b752ceae0c7681c5272c60206004356101f461045a565b80600255604051908152a1005b346100c4575f3660031901126100c4576020600154604051908152f35b346100c45760203660031901126100c45761023761045a565b5f5460405163a9059cbb60e01b81526001600160a01b03909116600480830191909152356024820152602081806044810103815f7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03165af1908115610321575f916102f2575b50156102ad57005b60405162461bcd60e51b815260206004820152601760248201527f4661756365743a207769746864726177206661696c65640000000000000000006044820152606490fd5b610314915060203d60201161031a575b61030c818361040c565b810190610442565b816102a5565b503d610302565b6040513d5f823e3d90fd5b346100c45760203660031901126100c4576004356001600160a01b038116908190036100c45761035a61045a565b8015610397575f80546001600160a01b031916821781557f4ffd725fc4a22075e9ec71c59edf9c38cdeb588a91b24fc5b61388c5be41282b9080a2005b60405162461bcd60e51b81526020600482015260146024820152734661756365743a207a65726f206164647265737360601b6044820152606490fd5b346100c45760203660031901126100c4576004356001600160a01b03811691908290036100c4576020915f526003825260405f20548152f35b90601f8019910116810190811067ffffffffffffffff82111761042e57604052565b634e487b7160e01b5f52604160045260245ffd5b908160209103126100c4575180151581036100c45790565b5f546001600160a01b0316330361046d57565b60405162461bcd60e51b81526020600482015260116024820152702330bab1b2ba1d103737ba1037bbb732b960791b6044820152606490fd5b6001600160a01b0381165f818152600360205260409020549091901580156106d2575b1561068d576040516370a0823160e01b81523060048201527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031690602081602481855afa908115610321575f9161065b575b5060015411610616575f838152600360209081526040808320429055600154905163a9059cbb60e01b81526001600160a01b0395909516600486015260248501529183916044918391905af1908115610321575f916105f7575b50156105b2577f0daf449977d5acafa35195e10b3eb92f97839892a6653afaba222379b58d8a9b6020600154604051908152a2565b60405162461bcd60e51b815260206004820152601760248201527f4661756365743a207472616e73666572206661696c65640000000000000000006044820152606490fd5b610610915060203d60201161031a5761030c818361040c565b5f61057d565b60405162461bcd60e51b815260206004820152601c60248201527f4661756365743a20696e73756666696369656e742062616c616e6365000000006044820152606490fd5b90506020813d602011610685575b816106766020938361040c565b810103126100c457515f610523565b3d9150610669565b60405162461bcd60e51b815260206004820152601760248201527f4661756365743a20636f6f6c646f776e206163746976650000000000000000006044820152606490fd5b50815f52600360205260405f205460025481018091116106f4574210156104c9565b634e487b7160e01b5f52601160045260245ffd", +} + +// FaucetABI is the input ABI used to generate the binding from. +// Deprecated: Use FaucetMetaData.ABI instead. +var FaucetABI = FaucetMetaData.ABI + +// FaucetBin is the compiled bytecode used for deploying new contracts. +// Deprecated: Use FaucetMetaData.Bin instead. +var FaucetBin = FaucetMetaData.Bin + +// DeployFaucet deploys a new Ethereum contract, binding an instance of Faucet to it. +func DeployFaucet(auth *bind.TransactOpts, backend bind.ContractBackend, _token common.Address, _dripAmount *big.Int, _cooldown *big.Int) (common.Address, *types.Transaction, *Faucet, error) { + parsed, err := FaucetMetaData.GetAbi() + if err != nil { + return common.Address{}, nil, nil, err + } + if parsed == nil { + return common.Address{}, nil, nil, errors.New("GetABI returned nil") + } + + address, tx, contract, err := bind.DeployContract(auth, *parsed, common.FromHex(FaucetBin), backend, _token, _dripAmount, _cooldown) + if err != nil { + return common.Address{}, nil, nil, err + } + return address, tx, &Faucet{FaucetCaller: FaucetCaller{contract: contract}, FaucetTransactor: FaucetTransactor{contract: contract}, FaucetFilterer: FaucetFilterer{contract: contract}}, nil +} + +// Faucet is an auto generated Go binding around an Ethereum contract. +type Faucet struct { + FaucetCaller // Read-only binding to the contract + FaucetTransactor // Write-only binding to the contract + FaucetFilterer // Log filterer for contract events +} + +// FaucetCaller is an auto generated read-only Go binding around an Ethereum contract. +type FaucetCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// FaucetTransactor is an auto generated write-only Go binding around an Ethereum contract. +type FaucetTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// FaucetFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type FaucetFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// FaucetSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type FaucetSession struct { + Contract *Faucet // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// FaucetCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type FaucetCallerSession struct { + Contract *FaucetCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// FaucetTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type FaucetTransactorSession struct { + Contract *FaucetTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// FaucetRaw is an auto generated low-level Go binding around an Ethereum contract. +type FaucetRaw struct { + Contract *Faucet // Generic contract binding to access the raw methods on +} + +// FaucetCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type FaucetCallerRaw struct { + Contract *FaucetCaller // Generic read-only contract binding to access the raw methods on +} + +// FaucetTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type FaucetTransactorRaw struct { + Contract *FaucetTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewFaucet creates a new instance of Faucet, bound to a specific deployed contract. +func NewFaucet(address common.Address, backend bind.ContractBackend) (*Faucet, error) { + contract, err := bindFaucet(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &Faucet{FaucetCaller: FaucetCaller{contract: contract}, FaucetTransactor: FaucetTransactor{contract: contract}, FaucetFilterer: FaucetFilterer{contract: contract}}, nil +} + +// NewFaucetCaller creates a new read-only instance of Faucet, bound to a specific deployed contract. +func NewFaucetCaller(address common.Address, caller bind.ContractCaller) (*FaucetCaller, error) { + contract, err := bindFaucet(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &FaucetCaller{contract: contract}, nil +} + +// NewFaucetTransactor creates a new write-only instance of Faucet, bound to a specific deployed contract. +func NewFaucetTransactor(address common.Address, transactor bind.ContractTransactor) (*FaucetTransactor, error) { + contract, err := bindFaucet(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &FaucetTransactor{contract: contract}, nil +} + +// NewFaucetFilterer creates a new log filterer instance of Faucet, bound to a specific deployed contract. +func NewFaucetFilterer(address common.Address, filterer bind.ContractFilterer) (*FaucetFilterer, error) { + contract, err := bindFaucet(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &FaucetFilterer{contract: contract}, nil +} + +// bindFaucet binds a generic wrapper to an already deployed contract. +func bindFaucet(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := FaucetMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Faucet *FaucetRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Faucet.Contract.FaucetCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Faucet *FaucetRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Faucet.Contract.FaucetTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Faucet *FaucetRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Faucet.Contract.FaucetTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Faucet *FaucetCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Faucet.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Faucet *FaucetTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Faucet.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Faucet *FaucetTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Faucet.Contract.contract.Transact(opts, method, params...) +} + +// TOKEN is a free data retrieval call binding the contract method 0x82bfefc8. +// +// Solidity: function TOKEN() view returns(address) +func (_Faucet *FaucetCaller) TOKEN(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _Faucet.contract.Call(opts, &out, "TOKEN") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// TOKEN is a free data retrieval call binding the contract method 0x82bfefc8. +// +// Solidity: function TOKEN() view returns(address) +func (_Faucet *FaucetSession) TOKEN() (common.Address, error) { + return _Faucet.Contract.TOKEN(&_Faucet.CallOpts) +} + +// TOKEN is a free data retrieval call binding the contract method 0x82bfefc8. +// +// Solidity: function TOKEN() view returns(address) +func (_Faucet *FaucetCallerSession) TOKEN() (common.Address, error) { + return _Faucet.Contract.TOKEN(&_Faucet.CallOpts) +} + +// Cooldown is a free data retrieval call binding the contract method 0x787a08a6. +// +// Solidity: function cooldown() view returns(uint256) +func (_Faucet *FaucetCaller) Cooldown(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _Faucet.contract.Call(opts, &out, "cooldown") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// Cooldown is a free data retrieval call binding the contract method 0x787a08a6. +// +// Solidity: function cooldown() view returns(uint256) +func (_Faucet *FaucetSession) Cooldown() (*big.Int, error) { + return _Faucet.Contract.Cooldown(&_Faucet.CallOpts) +} + +// Cooldown is a free data retrieval call binding the contract method 0x787a08a6. +// +// Solidity: function cooldown() view returns(uint256) +func (_Faucet *FaucetCallerSession) Cooldown() (*big.Int, error) { + return _Faucet.Contract.Cooldown(&_Faucet.CallOpts) +} + +// DripAmount is a free data retrieval call binding the contract method 0x35a1529b. +// +// Solidity: function dripAmount() view returns(uint256) +func (_Faucet *FaucetCaller) DripAmount(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _Faucet.contract.Call(opts, &out, "dripAmount") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// DripAmount is a free data retrieval call binding the contract method 0x35a1529b. +// +// Solidity: function dripAmount() view returns(uint256) +func (_Faucet *FaucetSession) DripAmount() (*big.Int, error) { + return _Faucet.Contract.DripAmount(&_Faucet.CallOpts) +} + +// DripAmount is a free data retrieval call binding the contract method 0x35a1529b. +// +// Solidity: function dripAmount() view returns(uint256) +func (_Faucet *FaucetCallerSession) DripAmount() (*big.Int, error) { + return _Faucet.Contract.DripAmount(&_Faucet.CallOpts) +} + +// LastDrip is a free data retrieval call binding the contract method 0x0935f004. +// +// Solidity: function lastDrip(address ) view returns(uint256) +func (_Faucet *FaucetCaller) LastDrip(opts *bind.CallOpts, arg0 common.Address) (*big.Int, error) { + var out []interface{} + err := _Faucet.contract.Call(opts, &out, "lastDrip", arg0) + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// LastDrip is a free data retrieval call binding the contract method 0x0935f004. +// +// Solidity: function lastDrip(address ) view returns(uint256) +func (_Faucet *FaucetSession) LastDrip(arg0 common.Address) (*big.Int, error) { + return _Faucet.Contract.LastDrip(&_Faucet.CallOpts, arg0) +} + +// LastDrip is a free data retrieval call binding the contract method 0x0935f004. +// +// Solidity: function lastDrip(address ) view returns(uint256) +func (_Faucet *FaucetCallerSession) LastDrip(arg0 common.Address) (*big.Int, error) { + return _Faucet.Contract.LastDrip(&_Faucet.CallOpts, arg0) +} + +// Owner is a free data retrieval call binding the contract method 0x8da5cb5b. +// +// Solidity: function owner() view returns(address) +func (_Faucet *FaucetCaller) Owner(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _Faucet.contract.Call(opts, &out, "owner") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// Owner is a free data retrieval call binding the contract method 0x8da5cb5b. +// +// Solidity: function owner() view returns(address) +func (_Faucet *FaucetSession) Owner() (common.Address, error) { + return _Faucet.Contract.Owner(&_Faucet.CallOpts) +} + +// Owner is a free data retrieval call binding the contract method 0x8da5cb5b. +// +// Solidity: function owner() view returns(address) +func (_Faucet *FaucetCallerSession) Owner() (common.Address, error) { + return _Faucet.Contract.Owner(&_Faucet.CallOpts) +} + +// Drip is a paid mutator transaction binding the contract method 0x9f678cca. +// +// Solidity: function drip() returns() +func (_Faucet *FaucetTransactor) Drip(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Faucet.contract.Transact(opts, "drip") +} + +// Drip is a paid mutator transaction binding the contract method 0x9f678cca. +// +// Solidity: function drip() returns() +func (_Faucet *FaucetSession) Drip() (*types.Transaction, error) { + return _Faucet.Contract.Drip(&_Faucet.TransactOpts) +} + +// Drip is a paid mutator transaction binding the contract method 0x9f678cca. +// +// Solidity: function drip() returns() +func (_Faucet *FaucetTransactorSession) Drip() (*types.Transaction, error) { + return _Faucet.Contract.Drip(&_Faucet.TransactOpts) +} + +// DripTo is a paid mutator transaction binding the contract method 0xcabee26e. +// +// Solidity: function dripTo(address recipient) returns() +func (_Faucet *FaucetTransactor) DripTo(opts *bind.TransactOpts, recipient common.Address) (*types.Transaction, error) { + return _Faucet.contract.Transact(opts, "dripTo", recipient) +} + +// DripTo is a paid mutator transaction binding the contract method 0xcabee26e. +// +// Solidity: function dripTo(address recipient) returns() +func (_Faucet *FaucetSession) DripTo(recipient common.Address) (*types.Transaction, error) { + return _Faucet.Contract.DripTo(&_Faucet.TransactOpts, recipient) +} + +// DripTo is a paid mutator transaction binding the contract method 0xcabee26e. +// +// Solidity: function dripTo(address recipient) returns() +func (_Faucet *FaucetTransactorSession) DripTo(recipient common.Address) (*types.Transaction, error) { + return _Faucet.Contract.DripTo(&_Faucet.TransactOpts, recipient) +} + +// SetCooldown is a paid mutator transaction binding the contract method 0x4fc3f41a. +// +// Solidity: function setCooldown(uint256 _cooldown) returns() +func (_Faucet *FaucetTransactor) SetCooldown(opts *bind.TransactOpts, _cooldown *big.Int) (*types.Transaction, error) { + return _Faucet.contract.Transact(opts, "setCooldown", _cooldown) +} + +// SetCooldown is a paid mutator transaction binding the contract method 0x4fc3f41a. +// +// Solidity: function setCooldown(uint256 _cooldown) returns() +func (_Faucet *FaucetSession) SetCooldown(_cooldown *big.Int) (*types.Transaction, error) { + return _Faucet.Contract.SetCooldown(&_Faucet.TransactOpts, _cooldown) +} + +// SetCooldown is a paid mutator transaction binding the contract method 0x4fc3f41a. +// +// Solidity: function setCooldown(uint256 _cooldown) returns() +func (_Faucet *FaucetTransactorSession) SetCooldown(_cooldown *big.Int) (*types.Transaction, error) { + return _Faucet.Contract.SetCooldown(&_Faucet.TransactOpts, _cooldown) +} + +// SetDripAmount is a paid mutator transaction binding the contract method 0x543f8c58. +// +// Solidity: function setDripAmount(uint256 _dripAmount) returns() +func (_Faucet *FaucetTransactor) SetDripAmount(opts *bind.TransactOpts, _dripAmount *big.Int) (*types.Transaction, error) { + return _Faucet.contract.Transact(opts, "setDripAmount", _dripAmount) +} + +// SetDripAmount is a paid mutator transaction binding the contract method 0x543f8c58. +// +// Solidity: function setDripAmount(uint256 _dripAmount) returns() +func (_Faucet *FaucetSession) SetDripAmount(_dripAmount *big.Int) (*types.Transaction, error) { + return _Faucet.Contract.SetDripAmount(&_Faucet.TransactOpts, _dripAmount) +} + +// SetDripAmount is a paid mutator transaction binding the contract method 0x543f8c58. +// +// Solidity: function setDripAmount(uint256 _dripAmount) returns() +func (_Faucet *FaucetTransactorSession) SetDripAmount(_dripAmount *big.Int) (*types.Transaction, error) { + return _Faucet.Contract.SetDripAmount(&_Faucet.TransactOpts, _dripAmount) +} + +// SetOwner is a paid mutator transaction binding the contract method 0x13af4035. +// +// Solidity: function setOwner(address _owner) returns() +func (_Faucet *FaucetTransactor) SetOwner(opts *bind.TransactOpts, _owner common.Address) (*types.Transaction, error) { + return _Faucet.contract.Transact(opts, "setOwner", _owner) +} + +// SetOwner is a paid mutator transaction binding the contract method 0x13af4035. +// +// Solidity: function setOwner(address _owner) returns() +func (_Faucet *FaucetSession) SetOwner(_owner common.Address) (*types.Transaction, error) { + return _Faucet.Contract.SetOwner(&_Faucet.TransactOpts, _owner) +} + +// SetOwner is a paid mutator transaction binding the contract method 0x13af4035. +// +// Solidity: function setOwner(address _owner) returns() +func (_Faucet *FaucetTransactorSession) SetOwner(_owner common.Address) (*types.Transaction, error) { + return _Faucet.Contract.SetOwner(&_Faucet.TransactOpts, _owner) +} + +// Withdraw is a paid mutator transaction binding the contract method 0x2e1a7d4d. +// +// Solidity: function withdraw(uint256 amount) returns() +func (_Faucet *FaucetTransactor) Withdraw(opts *bind.TransactOpts, amount *big.Int) (*types.Transaction, error) { + return _Faucet.contract.Transact(opts, "withdraw", amount) +} + +// Withdraw is a paid mutator transaction binding the contract method 0x2e1a7d4d. +// +// Solidity: function withdraw(uint256 amount) returns() +func (_Faucet *FaucetSession) Withdraw(amount *big.Int) (*types.Transaction, error) { + return _Faucet.Contract.Withdraw(&_Faucet.TransactOpts, amount) +} + +// Withdraw is a paid mutator transaction binding the contract method 0x2e1a7d4d. +// +// Solidity: function withdraw(uint256 amount) returns() +func (_Faucet *FaucetTransactorSession) Withdraw(amount *big.Int) (*types.Transaction, error) { + return _Faucet.Contract.Withdraw(&_Faucet.TransactOpts, amount) +} + +// FaucetCooldownUpdatedIterator is returned from FilterCooldownUpdated and is used to iterate over the raw logs and unpacked data for CooldownUpdated events raised by the Faucet contract. +type FaucetCooldownUpdatedIterator struct { + Event *FaucetCooldownUpdated // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *FaucetCooldownUpdatedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(FaucetCooldownUpdated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(FaucetCooldownUpdated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *FaucetCooldownUpdatedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *FaucetCooldownUpdatedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// FaucetCooldownUpdated represents a CooldownUpdated event raised by the Faucet contract. +type FaucetCooldownUpdated struct { + NewCooldown *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterCooldownUpdated is a free log retrieval operation binding the contract event 0x583d8b24c5439ab7d810e51e37e8db41ba66f1168fd7b752ceae0c7681c5272c. +// +// Solidity: event CooldownUpdated(uint256 newCooldown) +func (_Faucet *FaucetFilterer) FilterCooldownUpdated(opts *bind.FilterOpts) (*FaucetCooldownUpdatedIterator, error) { + + logs, sub, err := _Faucet.contract.FilterLogs(opts, "CooldownUpdated") + if err != nil { + return nil, err + } + return &FaucetCooldownUpdatedIterator{contract: _Faucet.contract, event: "CooldownUpdated", logs: logs, sub: sub}, nil +} + +// WatchCooldownUpdated is a free log subscription operation binding the contract event 0x583d8b24c5439ab7d810e51e37e8db41ba66f1168fd7b752ceae0c7681c5272c. +// +// Solidity: event CooldownUpdated(uint256 newCooldown) +func (_Faucet *FaucetFilterer) WatchCooldownUpdated(opts *bind.WatchOpts, sink chan<- *FaucetCooldownUpdated) (event.Subscription, error) { + + logs, sub, err := _Faucet.contract.WatchLogs(opts, "CooldownUpdated") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(FaucetCooldownUpdated) + if err := _Faucet.contract.UnpackLog(event, "CooldownUpdated", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseCooldownUpdated is a log parse operation binding the contract event 0x583d8b24c5439ab7d810e51e37e8db41ba66f1168fd7b752ceae0c7681c5272c. +// +// Solidity: event CooldownUpdated(uint256 newCooldown) +func (_Faucet *FaucetFilterer) ParseCooldownUpdated(log types.Log) (*FaucetCooldownUpdated, error) { + event := new(FaucetCooldownUpdated) + if err := _Faucet.contract.UnpackLog(event, "CooldownUpdated", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// FaucetDripAmountUpdatedIterator is returned from FilterDripAmountUpdated and is used to iterate over the raw logs and unpacked data for DripAmountUpdated events raised by the Faucet contract. +type FaucetDripAmountUpdatedIterator struct { + Event *FaucetDripAmountUpdated // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *FaucetDripAmountUpdatedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(FaucetDripAmountUpdated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(FaucetDripAmountUpdated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *FaucetDripAmountUpdatedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *FaucetDripAmountUpdatedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// FaucetDripAmountUpdated represents a DripAmountUpdated event raised by the Faucet contract. +type FaucetDripAmountUpdated struct { + NewAmount *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterDripAmountUpdated is a free log retrieval operation binding the contract event 0x33f3faee0788ab897d8f674abe1dde6d93ba901e4a1502161294734ba178e3c7. +// +// Solidity: event DripAmountUpdated(uint256 newAmount) +func (_Faucet *FaucetFilterer) FilterDripAmountUpdated(opts *bind.FilterOpts) (*FaucetDripAmountUpdatedIterator, error) { + + logs, sub, err := _Faucet.contract.FilterLogs(opts, "DripAmountUpdated") + if err != nil { + return nil, err + } + return &FaucetDripAmountUpdatedIterator{contract: _Faucet.contract, event: "DripAmountUpdated", logs: logs, sub: sub}, nil +} + +// WatchDripAmountUpdated is a free log subscription operation binding the contract event 0x33f3faee0788ab897d8f674abe1dde6d93ba901e4a1502161294734ba178e3c7. +// +// Solidity: event DripAmountUpdated(uint256 newAmount) +func (_Faucet *FaucetFilterer) WatchDripAmountUpdated(opts *bind.WatchOpts, sink chan<- *FaucetDripAmountUpdated) (event.Subscription, error) { + + logs, sub, err := _Faucet.contract.WatchLogs(opts, "DripAmountUpdated") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(FaucetDripAmountUpdated) + if err := _Faucet.contract.UnpackLog(event, "DripAmountUpdated", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseDripAmountUpdated is a log parse operation binding the contract event 0x33f3faee0788ab897d8f674abe1dde6d93ba901e4a1502161294734ba178e3c7. +// +// Solidity: event DripAmountUpdated(uint256 newAmount) +func (_Faucet *FaucetFilterer) ParseDripAmountUpdated(log types.Log) (*FaucetDripAmountUpdated, error) { + event := new(FaucetDripAmountUpdated) + if err := _Faucet.contract.UnpackLog(event, "DripAmountUpdated", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// FaucetDrippedIterator is returned from FilterDripped and is used to iterate over the raw logs and unpacked data for Dripped events raised by the Faucet contract. +type FaucetDrippedIterator struct { + Event *FaucetDripped // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *FaucetDrippedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(FaucetDripped) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(FaucetDripped) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *FaucetDrippedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *FaucetDrippedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// FaucetDripped represents a Dripped event raised by the Faucet contract. +type FaucetDripped struct { + Recipient common.Address + Amount *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterDripped is a free log retrieval operation binding the contract event 0x0daf449977d5acafa35195e10b3eb92f97839892a6653afaba222379b58d8a9b. +// +// Solidity: event Dripped(address indexed recipient, uint256 amount) +func (_Faucet *FaucetFilterer) FilterDripped(opts *bind.FilterOpts, recipient []common.Address) (*FaucetDrippedIterator, error) { + + var recipientRule []interface{} + for _, recipientItem := range recipient { + recipientRule = append(recipientRule, recipientItem) + } + + logs, sub, err := _Faucet.contract.FilterLogs(opts, "Dripped", recipientRule) + if err != nil { + return nil, err + } + return &FaucetDrippedIterator{contract: _Faucet.contract, event: "Dripped", logs: logs, sub: sub}, nil +} + +// WatchDripped is a free log subscription operation binding the contract event 0x0daf449977d5acafa35195e10b3eb92f97839892a6653afaba222379b58d8a9b. +// +// Solidity: event Dripped(address indexed recipient, uint256 amount) +func (_Faucet *FaucetFilterer) WatchDripped(opts *bind.WatchOpts, sink chan<- *FaucetDripped, recipient []common.Address) (event.Subscription, error) { + + var recipientRule []interface{} + for _, recipientItem := range recipient { + recipientRule = append(recipientRule, recipientItem) + } + + logs, sub, err := _Faucet.contract.WatchLogs(opts, "Dripped", recipientRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(FaucetDripped) + if err := _Faucet.contract.UnpackLog(event, "Dripped", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseDripped is a log parse operation binding the contract event 0x0daf449977d5acafa35195e10b3eb92f97839892a6653afaba222379b58d8a9b. +// +// Solidity: event Dripped(address indexed recipient, uint256 amount) +func (_Faucet *FaucetFilterer) ParseDripped(log types.Log) (*FaucetDripped, error) { + event := new(FaucetDripped) + if err := _Faucet.contract.UnpackLog(event, "Dripped", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// FaucetOwnerUpdatedIterator is returned from FilterOwnerUpdated and is used to iterate over the raw logs and unpacked data for OwnerUpdated events raised by the Faucet contract. +type FaucetOwnerUpdatedIterator struct { + Event *FaucetOwnerUpdated // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *FaucetOwnerUpdatedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(FaucetOwnerUpdated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(FaucetOwnerUpdated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *FaucetOwnerUpdatedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *FaucetOwnerUpdatedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// FaucetOwnerUpdated represents a OwnerUpdated event raised by the Faucet contract. +type FaucetOwnerUpdated struct { + NewOwner common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterOwnerUpdated is a free log retrieval operation binding the contract event 0x4ffd725fc4a22075e9ec71c59edf9c38cdeb588a91b24fc5b61388c5be41282b. +// +// Solidity: event OwnerUpdated(address indexed newOwner) +func (_Faucet *FaucetFilterer) FilterOwnerUpdated(opts *bind.FilterOpts, newOwner []common.Address) (*FaucetOwnerUpdatedIterator, error) { + + var newOwnerRule []interface{} + for _, newOwnerItem := range newOwner { + newOwnerRule = append(newOwnerRule, newOwnerItem) + } + + logs, sub, err := _Faucet.contract.FilterLogs(opts, "OwnerUpdated", newOwnerRule) + if err != nil { + return nil, err + } + return &FaucetOwnerUpdatedIterator{contract: _Faucet.contract, event: "OwnerUpdated", logs: logs, sub: sub}, nil +} + +// WatchOwnerUpdated is a free log subscription operation binding the contract event 0x4ffd725fc4a22075e9ec71c59edf9c38cdeb588a91b24fc5b61388c5be41282b. +// +// Solidity: event OwnerUpdated(address indexed newOwner) +func (_Faucet *FaucetFilterer) WatchOwnerUpdated(opts *bind.WatchOpts, sink chan<- *FaucetOwnerUpdated, newOwner []common.Address) (event.Subscription, error) { + + var newOwnerRule []interface{} + for _, newOwnerItem := range newOwner { + newOwnerRule = append(newOwnerRule, newOwnerItem) + } + + logs, sub, err := _Faucet.contract.WatchLogs(opts, "OwnerUpdated", newOwnerRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(FaucetOwnerUpdated) + if err := _Faucet.contract.UnpackLog(event, "OwnerUpdated", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseOwnerUpdated is a log parse operation binding the contract event 0x4ffd725fc4a22075e9ec71c59edf9c38cdeb588a91b24fc5b61388c5be41282b. +// +// Solidity: event OwnerUpdated(address indexed newOwner) +func (_Faucet *FaucetFilterer) ParseOwnerUpdated(log types.Log) (*FaucetOwnerUpdated, error) { + event := new(FaucetOwnerUpdated) + if err := _Faucet.contract.UnpackLog(event, "OwnerUpdated", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} diff --git a/pkg/blockchain/evm/faucet_adapter.go b/pkg/blockchain/evm/faucet_adapter.go new file mode 100644 index 0000000..1a34675 --- /dev/null +++ b/pkg/blockchain/evm/faucet_adapter.go @@ -0,0 +1,79 @@ +package evm + +import ( + "context" + "crypto/ecdsa" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + + "github.com/layer-3/clearnet-sdk/pkg/core" +) + +// FaucetAdapter wraps the Faucet binding for testnet token drips and parameter +// reads. It implements core.FaucetWriter and core.FaucetReader. +type FaucetAdapter struct { + client *ethclient.Client + faucet *Faucet + auth *bind.TransactOpts +} + +var ( + _ core.FaucetWriter = (*FaucetAdapter)(nil) + _ core.FaucetReader = (*FaucetAdapter)(nil) +) + +// NewFaucetAdapter binds the Faucet at faucetAddr over client with a +// transactor for the given key (needed for the drip writes). +func NewFaucetAdapter(ctx context.Context, client *ethclient.Client, faucetAddr common.Address, key *ecdsa.PrivateKey) (*FaucetAdapter, error) { + faucet, err := NewFaucet(faucetAddr, client) + if err != nil { + return nil, fmt.Errorf("load faucet: %w", err) + } + auth, err := newTransactor(ctx, client, key) + if err != nil { + return nil, err + } + return &FaucetAdapter{client: client, faucet: faucet, auth: auth}, nil +} + +// ─── Write ─────────────────────────────────────────────────────────────────── + +// Drip claims tokens to the transactor's own address. +func (a *FaucetAdapter) Drip(ctx context.Context) error { + tx, err := a.faucet.Drip(txOpts(a.auth, ctx)) + if err != nil { + return fmt.Errorf("faucet drip: %w", err) + } + return waitMined(ctx, a.client, tx) +} + +// DripTo claims tokens to recipient. +func (a *FaucetAdapter) DripTo(ctx context.Context, recipient common.Address) error { + tx, err := a.faucet.DripTo(txOpts(a.auth, ctx), recipient) + if err != nil { + return fmt.Errorf("faucet drip-to: %w", err) + } + return waitMined(ctx, a.client, tx) +} + +// ─── Read ──────────────────────────────────────────────────────────────────── + +func (a *FaucetAdapter) DripAmount(ctx context.Context) (*big.Int, error) { + return a.faucet.DripAmount(&bind.CallOpts{Context: ctx}) +} + +func (a *FaucetAdapter) Cooldown(ctx context.Context) (*big.Int, error) { + return a.faucet.Cooldown(&bind.CallOpts{Context: ctx}) +} + +func (a *FaucetAdapter) Owner(ctx context.Context) (common.Address, error) { + return a.faucet.Owner(&bind.CallOpts{Context: ctx}) +} + +func (a *FaucetAdapter) LastDrip(ctx context.Context, addr common.Address) (*big.Int, error) { + return a.faucet.LastDrip(&bind.CallOpts{Context: ctx}, addr) +} diff --git a/pkg/blockchain/evm/fraud_adapter.go b/pkg/blockchain/evm/fraud_adapter.go new file mode 100644 index 0000000..150801f --- /dev/null +++ b/pkg/blockchain/evm/fraud_adapter.go @@ -0,0 +1,66 @@ +package evm + +import ( + "context" + "crypto/ecdsa" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + + "github.com/layer-3/clearnet-sdk/pkg/core" +) + +// FraudAdapter wraps the Slasher binding to submit withdrawal fraud evidence. +// It implements core.FraudEvidenceSubmitter. +type FraudAdapter struct { + client *ethclient.Client + slasher *Slasher + auth *bind.TransactOpts +} + +var _ core.FraudEvidenceSubmitter = (*FraudAdapter)(nil) + +// NewFraudAdapter binds the Slasher at slasherAddr over client with a +// transactor for the given key. +func NewFraudAdapter(ctx context.Context, client *ethclient.Client, slasherAddr common.Address, key *ecdsa.PrivateKey) (*FraudAdapter, error) { + slasher, err := NewSlasher(slasherAddr, client) + if err != nil { + return nil, fmt.Errorf("load slasher: %w", err) + } + auth, err := newTransactor(ctx, client, key) + if err != nil { + return nil, err + } + return &FraudAdapter{client: client, slasher: slasher, auth: auth}, nil +} + +// SubmitWithdrawalFraudEvidence submits byte-exact withdrawal fraud evidence to +// Slasher.sol and waits for the slashing transaction to mine. +func (a *FraudAdapter) SubmitWithdrawalFraudEvidence(ctx context.Context, evidence core.WithdrawalFraudEvidence) error { + provenBalance := evidence.ProvenBalance + if provenBalance == nil { + provenBalance = new(big.Int) + } + smtBitmask := evidence.SMTBitmask + if smtBitmask == nil { + smtBitmask = new(big.Int) + } + tx, err := a.slasher.SubmitWithdrawalFraudEvidence( + txOpts(a.auth, ctx), + evidence.ChallengedObject, + evidence.AnchorHeader, + evidence.AnchorSignature, + evidence.EntryIndex, + evidence.SMTProof, + smtBitmask, + evidence.BalanceKey, + provenBalance, + ) + if err != nil { + return fmt.Errorf("submit withdrawal fraud evidence: %w", err) + } + return waitMined(ctx, a.client, tx) +} diff --git a/pkg/blockchain/evm/generate.go b/pkg/blockchain/evm/generate.go new file mode 100644 index 0000000..a35e5ee --- /dev/null +++ b/pkg/blockchain/evm/generate.go @@ -0,0 +1,5 @@ +package evm + +// Regenerate the *_abi.go bindings from the vendored ABI + bytecode files +// under ./artifacts. See ./abi_refresher for the workflow. +//go:generate go run ./abi_refresher diff --git a/pkg/blockchain/evm/mockerc20_abi.go b/pkg/blockchain/evm/mockerc20_abi.go new file mode 100644 index 0000000..5b1c0aa --- /dev/null +++ b/pkg/blockchain/evm/mockerc20_abi.go @@ -0,0 +1,781 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package evm + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// MockERC20MetaData contains all meta data concerning the MockERC20 contract. +var MockERC20MetaData = &bind.MetaData{ + ABI: "[{\"type\":\"constructor\",\"inputs\":[{\"name\":\"_name\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"_symbol\",\"type\":\"string\",\"internalType\":\"string\"}],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"allowance\",\"inputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"approve\",\"inputs\":[{\"name\":\"spender\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"value\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"balanceOf\",\"inputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"decimals\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint8\",\"internalType\":\"uint8\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"mint\",\"inputs\":[{\"name\":\"to\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"amount\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"name\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"string\",\"internalType\":\"string\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"symbol\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"string\",\"internalType\":\"string\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"totalSupply\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"transfer\",\"inputs\":[{\"name\":\"to\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"value\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"transferFrom\",\"inputs\":[{\"name\":\"from\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"to\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"value\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"stateMutability\":\"nonpayable\"},{\"type\":\"event\",\"name\":\"Approval\",\"inputs\":[{\"name\":\"owner\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"spender\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"value\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"Transfer\",\"inputs\":[{\"name\":\"from\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"to\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"value\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"}],\"anonymous\":false}]", + Bin: "0x60806040523461032e576109b48038038061001981610332565b92833981019060408183031261032e5780516001600160401b03811161032e5782610045918301610357565b60208201519092906001600160401b03811161032e576100659201610357565b6002805460ff1916601217905581516001600160401b038111610239575f54600181811c91168015610324575b602082101461021b57601f81116102b7575b50602092601f821160011461025857928192935f9261024d575b50508160011b915f199060031b1c1916175f555b80516001600160401b03811161023957600154600181811c9116801561022f575b602082101461021b57601f81116101ad575b50602091601f821160011461014d579181925f92610142575b50508160011b915f199060031b1c1916176001555b60405161060b90816103a98239f35b015190505f8061011e565b601f1982169260015f52805f20915f5b8581106101955750836001951061017d575b505050811b01600155610133565b01515f1960f88460031b161c191690555f808061016f565b9192602060018192868501518155019401920161015d565b818111156101055760015f52601f820160051c7fb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf660208410610213575b81601f9101920160051c03905f5b828110610206575050610105565b5f828201556001016101f8565b5f91506101ea565b634e487b7160e01b5f52602260045260245ffd5b90607f16906100f3565b634e487b7160e01b5f52604160045260245ffd5b015190505f806100be565b601f198216935f8052805f20915f5b86811061029f5750836001959610610287575b505050811b015f556100d2565b01515f1960f88460031b161c191690555f808061027a565b91926020600181928685015181550194019201610267565b818111156100a4575f8052601f820160051c7f290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e5636020841061031c575b81601f9101920160051c03905f5b82811061030f5750506100a4565b5f82820155600101610301565b5f91506102f3565b90607f1690610092565b5f80fd5b6040519190601f01601f191682016001600160401b0381118382101761023957604052565b81601f8201121561032e578051906001600160401b03821161023957610386601f8301601f1916602001610332565b928284526020838301011161032e57815f9260208093018386015e830101529056fe60806040526004361015610011575f80fd5b5f3560e01c806306fdde03146104b5578063095ea7b31461043c57806318160ddd1461041f57806323b872dd1461035a578063313ce5671461033a57806340c10f19146102c057806370a082311461028857806395d89b411461016a578063a9059cbb146100db5763dd62ed3e14610087575f80fd5b346100d75760403660031901126100d7576100a06105b1565b6100a86105c7565b6001600160a01b039182165f908152600560209081526040808320949093168252928352819020549051908152f35b5f80fd5b346100d75760403660031901126100d7576100f46105b1565b60243590335f52600460205260405f2061010f8382546105dd565b905560018060a01b031690815f52600460205260405f206101318282546105fe565b90556040519081527fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef60203392a3602060405160018152f35b346100d7575f3660031901126100d7576040515f6001548060011c9060018116801561027e575b60208310811461026a5782855290811561024e57506001146101f8575b50819003601f01601f191681019067ffffffffffffffff8211818310176101e457604082905281906101e09082610587565b0390f35b634e487b7160e01b5f52604160045260245ffd5b60015f9081529091507fb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf65b828210610238575060209150820101826101ae565b6001816020925483858801015201910190610223565b90506020925060ff191682840152151560051b820101826101ae565b634e487b7160e01b5f52602260045260245ffd5b91607f1691610191565b346100d75760203660031901126100d7576001600160a01b036102a96105b1565b165f526004602052602060405f2054604051908152f35b346100d75760403660031901126100d7576102d96105b1565b5f7fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef60206024359360018060a01b03169384845260048252604084206103208282546105fe565b905561032e816003546105fe565b600355604051908152a3005b346100d7575f3660031901126100d757602060ff60025416604051908152f35b346100d75760603660031901126100d7576103736105b1565b61037b6105c7565b7fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef60206044359360018060a01b031692835f526005825260405f2060018060a01b0333165f52825260405f206103d28682546105dd565b9055835f526004825260405f206103ea8682546105dd565b905560018060a01b031693845f526004825260405f2061040b8282546105fe565b9055604051908152a3602060405160018152f35b346100d7575f3660031901126100d7576020600354604051908152f35b346100d75760403660031901126100d7576104556105b1565b335f8181526005602090815260408083206001600160a01b03909516808452948252918290206024359081905591519182527f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92591a3602060405160018152f35b346100d7575f3660031901126100d7576040515f5f548060011c9060018116801561057d575b60208310811461026a5782855290811561024e57506001146105295750819003601f01601f191681019067ffffffffffffffff8211818310176101e457604082905281906101e09082610587565b5f8080529091507f290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e5635b828210610567575060209150820101826101ae565b6001816020925483858801015201910190610552565b91607f16916104db565b602060409281835280519182918282860152018484015e5f828201840152601f01601f1916010190565b600435906001600160a01b03821682036100d757565b602435906001600160a01b03821682036100d757565b919082039182116105ea57565b634e487b7160e01b5f52601160045260245ffd5b919082018092116105ea5756", +} + +// MockERC20ABI is the input ABI used to generate the binding from. +// Deprecated: Use MockERC20MetaData.ABI instead. +var MockERC20ABI = MockERC20MetaData.ABI + +// MockERC20Bin is the compiled bytecode used for deploying new contracts. +// Deprecated: Use MockERC20MetaData.Bin instead. +var MockERC20Bin = MockERC20MetaData.Bin + +// DeployMockERC20 deploys a new Ethereum contract, binding an instance of MockERC20 to it. +func DeployMockERC20(auth *bind.TransactOpts, backend bind.ContractBackend, _name string, _symbol string) (common.Address, *types.Transaction, *MockERC20, error) { + parsed, err := MockERC20MetaData.GetAbi() + if err != nil { + return common.Address{}, nil, nil, err + } + if parsed == nil { + return common.Address{}, nil, nil, errors.New("GetABI returned nil") + } + + address, tx, contract, err := bind.DeployContract(auth, *parsed, common.FromHex(MockERC20Bin), backend, _name, _symbol) + if err != nil { + return common.Address{}, nil, nil, err + } + return address, tx, &MockERC20{MockERC20Caller: MockERC20Caller{contract: contract}, MockERC20Transactor: MockERC20Transactor{contract: contract}, MockERC20Filterer: MockERC20Filterer{contract: contract}}, nil +} + +// MockERC20 is an auto generated Go binding around an Ethereum contract. +type MockERC20 struct { + MockERC20Caller // Read-only binding to the contract + MockERC20Transactor // Write-only binding to the contract + MockERC20Filterer // Log filterer for contract events +} + +// MockERC20Caller is an auto generated read-only Go binding around an Ethereum contract. +type MockERC20Caller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// MockERC20Transactor is an auto generated write-only Go binding around an Ethereum contract. +type MockERC20Transactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// MockERC20Filterer is an auto generated log filtering Go binding around an Ethereum contract events. +type MockERC20Filterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// MockERC20Session is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type MockERC20Session struct { + Contract *MockERC20 // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// MockERC20CallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type MockERC20CallerSession struct { + Contract *MockERC20Caller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// MockERC20TransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type MockERC20TransactorSession struct { + Contract *MockERC20Transactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// MockERC20Raw is an auto generated low-level Go binding around an Ethereum contract. +type MockERC20Raw struct { + Contract *MockERC20 // Generic contract binding to access the raw methods on +} + +// MockERC20CallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type MockERC20CallerRaw struct { + Contract *MockERC20Caller // Generic read-only contract binding to access the raw methods on +} + +// MockERC20TransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type MockERC20TransactorRaw struct { + Contract *MockERC20Transactor // Generic write-only contract binding to access the raw methods on +} + +// NewMockERC20 creates a new instance of MockERC20, bound to a specific deployed contract. +func NewMockERC20(address common.Address, backend bind.ContractBackend) (*MockERC20, error) { + contract, err := bindMockERC20(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &MockERC20{MockERC20Caller: MockERC20Caller{contract: contract}, MockERC20Transactor: MockERC20Transactor{contract: contract}, MockERC20Filterer: MockERC20Filterer{contract: contract}}, nil +} + +// NewMockERC20Caller creates a new read-only instance of MockERC20, bound to a specific deployed contract. +func NewMockERC20Caller(address common.Address, caller bind.ContractCaller) (*MockERC20Caller, error) { + contract, err := bindMockERC20(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &MockERC20Caller{contract: contract}, nil +} + +// NewMockERC20Transactor creates a new write-only instance of MockERC20, bound to a specific deployed contract. +func NewMockERC20Transactor(address common.Address, transactor bind.ContractTransactor) (*MockERC20Transactor, error) { + contract, err := bindMockERC20(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &MockERC20Transactor{contract: contract}, nil +} + +// NewMockERC20Filterer creates a new log filterer instance of MockERC20, bound to a specific deployed contract. +func NewMockERC20Filterer(address common.Address, filterer bind.ContractFilterer) (*MockERC20Filterer, error) { + contract, err := bindMockERC20(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &MockERC20Filterer{contract: contract}, nil +} + +// bindMockERC20 binds a generic wrapper to an already deployed contract. +func bindMockERC20(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := MockERC20MetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_MockERC20 *MockERC20Raw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _MockERC20.Contract.MockERC20Caller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_MockERC20 *MockERC20Raw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _MockERC20.Contract.MockERC20Transactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_MockERC20 *MockERC20Raw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _MockERC20.Contract.MockERC20Transactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_MockERC20 *MockERC20CallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _MockERC20.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_MockERC20 *MockERC20TransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _MockERC20.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_MockERC20 *MockERC20TransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _MockERC20.Contract.contract.Transact(opts, method, params...) +} + +// Allowance is a free data retrieval call binding the contract method 0xdd62ed3e. +// +// Solidity: function allowance(address , address ) view returns(uint256) +func (_MockERC20 *MockERC20Caller) Allowance(opts *bind.CallOpts, arg0 common.Address, arg1 common.Address) (*big.Int, error) { + var out []interface{} + err := _MockERC20.contract.Call(opts, &out, "allowance", arg0, arg1) + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// Allowance is a free data retrieval call binding the contract method 0xdd62ed3e. +// +// Solidity: function allowance(address , address ) view returns(uint256) +func (_MockERC20 *MockERC20Session) Allowance(arg0 common.Address, arg1 common.Address) (*big.Int, error) { + return _MockERC20.Contract.Allowance(&_MockERC20.CallOpts, arg0, arg1) +} + +// Allowance is a free data retrieval call binding the contract method 0xdd62ed3e. +// +// Solidity: function allowance(address , address ) view returns(uint256) +func (_MockERC20 *MockERC20CallerSession) Allowance(arg0 common.Address, arg1 common.Address) (*big.Int, error) { + return _MockERC20.Contract.Allowance(&_MockERC20.CallOpts, arg0, arg1) +} + +// BalanceOf is a free data retrieval call binding the contract method 0x70a08231. +// +// Solidity: function balanceOf(address ) view returns(uint256) +func (_MockERC20 *MockERC20Caller) BalanceOf(opts *bind.CallOpts, arg0 common.Address) (*big.Int, error) { + var out []interface{} + err := _MockERC20.contract.Call(opts, &out, "balanceOf", arg0) + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// BalanceOf is a free data retrieval call binding the contract method 0x70a08231. +// +// Solidity: function balanceOf(address ) view returns(uint256) +func (_MockERC20 *MockERC20Session) BalanceOf(arg0 common.Address) (*big.Int, error) { + return _MockERC20.Contract.BalanceOf(&_MockERC20.CallOpts, arg0) +} + +// BalanceOf is a free data retrieval call binding the contract method 0x70a08231. +// +// Solidity: function balanceOf(address ) view returns(uint256) +func (_MockERC20 *MockERC20CallerSession) BalanceOf(arg0 common.Address) (*big.Int, error) { + return _MockERC20.Contract.BalanceOf(&_MockERC20.CallOpts, arg0) +} + +// Decimals is a free data retrieval call binding the contract method 0x313ce567. +// +// Solidity: function decimals() view returns(uint8) +func (_MockERC20 *MockERC20Caller) Decimals(opts *bind.CallOpts) (uint8, error) { + var out []interface{} + err := _MockERC20.contract.Call(opts, &out, "decimals") + + if err != nil { + return *new(uint8), err + } + + out0 := *abi.ConvertType(out[0], new(uint8)).(*uint8) + + return out0, err + +} + +// Decimals is a free data retrieval call binding the contract method 0x313ce567. +// +// Solidity: function decimals() view returns(uint8) +func (_MockERC20 *MockERC20Session) Decimals() (uint8, error) { + return _MockERC20.Contract.Decimals(&_MockERC20.CallOpts) +} + +// Decimals is a free data retrieval call binding the contract method 0x313ce567. +// +// Solidity: function decimals() view returns(uint8) +func (_MockERC20 *MockERC20CallerSession) Decimals() (uint8, error) { + return _MockERC20.Contract.Decimals(&_MockERC20.CallOpts) +} + +// Name is a free data retrieval call binding the contract method 0x06fdde03. +// +// Solidity: function name() view returns(string) +func (_MockERC20 *MockERC20Caller) Name(opts *bind.CallOpts) (string, error) { + var out []interface{} + err := _MockERC20.contract.Call(opts, &out, "name") + + if err != nil { + return *new(string), err + } + + out0 := *abi.ConvertType(out[0], new(string)).(*string) + + return out0, err + +} + +// Name is a free data retrieval call binding the contract method 0x06fdde03. +// +// Solidity: function name() view returns(string) +func (_MockERC20 *MockERC20Session) Name() (string, error) { + return _MockERC20.Contract.Name(&_MockERC20.CallOpts) +} + +// Name is a free data retrieval call binding the contract method 0x06fdde03. +// +// Solidity: function name() view returns(string) +func (_MockERC20 *MockERC20CallerSession) Name() (string, error) { + return _MockERC20.Contract.Name(&_MockERC20.CallOpts) +} + +// Symbol is a free data retrieval call binding the contract method 0x95d89b41. +// +// Solidity: function symbol() view returns(string) +func (_MockERC20 *MockERC20Caller) Symbol(opts *bind.CallOpts) (string, error) { + var out []interface{} + err := _MockERC20.contract.Call(opts, &out, "symbol") + + if err != nil { + return *new(string), err + } + + out0 := *abi.ConvertType(out[0], new(string)).(*string) + + return out0, err + +} + +// Symbol is a free data retrieval call binding the contract method 0x95d89b41. +// +// Solidity: function symbol() view returns(string) +func (_MockERC20 *MockERC20Session) Symbol() (string, error) { + return _MockERC20.Contract.Symbol(&_MockERC20.CallOpts) +} + +// Symbol is a free data retrieval call binding the contract method 0x95d89b41. +// +// Solidity: function symbol() view returns(string) +func (_MockERC20 *MockERC20CallerSession) Symbol() (string, error) { + return _MockERC20.Contract.Symbol(&_MockERC20.CallOpts) +} + +// TotalSupply is a free data retrieval call binding the contract method 0x18160ddd. +// +// Solidity: function totalSupply() view returns(uint256) +func (_MockERC20 *MockERC20Caller) TotalSupply(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _MockERC20.contract.Call(opts, &out, "totalSupply") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// TotalSupply is a free data retrieval call binding the contract method 0x18160ddd. +// +// Solidity: function totalSupply() view returns(uint256) +func (_MockERC20 *MockERC20Session) TotalSupply() (*big.Int, error) { + return _MockERC20.Contract.TotalSupply(&_MockERC20.CallOpts) +} + +// TotalSupply is a free data retrieval call binding the contract method 0x18160ddd. +// +// Solidity: function totalSupply() view returns(uint256) +func (_MockERC20 *MockERC20CallerSession) TotalSupply() (*big.Int, error) { + return _MockERC20.Contract.TotalSupply(&_MockERC20.CallOpts) +} + +// Approve is a paid mutator transaction binding the contract method 0x095ea7b3. +// +// Solidity: function approve(address spender, uint256 value) returns(bool) +func (_MockERC20 *MockERC20Transactor) Approve(opts *bind.TransactOpts, spender common.Address, value *big.Int) (*types.Transaction, error) { + return _MockERC20.contract.Transact(opts, "approve", spender, value) +} + +// Approve is a paid mutator transaction binding the contract method 0x095ea7b3. +// +// Solidity: function approve(address spender, uint256 value) returns(bool) +func (_MockERC20 *MockERC20Session) Approve(spender common.Address, value *big.Int) (*types.Transaction, error) { + return _MockERC20.Contract.Approve(&_MockERC20.TransactOpts, spender, value) +} + +// Approve is a paid mutator transaction binding the contract method 0x095ea7b3. +// +// Solidity: function approve(address spender, uint256 value) returns(bool) +func (_MockERC20 *MockERC20TransactorSession) Approve(spender common.Address, value *big.Int) (*types.Transaction, error) { + return _MockERC20.Contract.Approve(&_MockERC20.TransactOpts, spender, value) +} + +// Mint is a paid mutator transaction binding the contract method 0x40c10f19. +// +// Solidity: function mint(address to, uint256 amount) returns() +func (_MockERC20 *MockERC20Transactor) Mint(opts *bind.TransactOpts, to common.Address, amount *big.Int) (*types.Transaction, error) { + return _MockERC20.contract.Transact(opts, "mint", to, amount) +} + +// Mint is a paid mutator transaction binding the contract method 0x40c10f19. +// +// Solidity: function mint(address to, uint256 amount) returns() +func (_MockERC20 *MockERC20Session) Mint(to common.Address, amount *big.Int) (*types.Transaction, error) { + return _MockERC20.Contract.Mint(&_MockERC20.TransactOpts, to, amount) +} + +// Mint is a paid mutator transaction binding the contract method 0x40c10f19. +// +// Solidity: function mint(address to, uint256 amount) returns() +func (_MockERC20 *MockERC20TransactorSession) Mint(to common.Address, amount *big.Int) (*types.Transaction, error) { + return _MockERC20.Contract.Mint(&_MockERC20.TransactOpts, to, amount) +} + +// Transfer is a paid mutator transaction binding the contract method 0xa9059cbb. +// +// Solidity: function transfer(address to, uint256 value) returns(bool) +func (_MockERC20 *MockERC20Transactor) Transfer(opts *bind.TransactOpts, to common.Address, value *big.Int) (*types.Transaction, error) { + return _MockERC20.contract.Transact(opts, "transfer", to, value) +} + +// Transfer is a paid mutator transaction binding the contract method 0xa9059cbb. +// +// Solidity: function transfer(address to, uint256 value) returns(bool) +func (_MockERC20 *MockERC20Session) Transfer(to common.Address, value *big.Int) (*types.Transaction, error) { + return _MockERC20.Contract.Transfer(&_MockERC20.TransactOpts, to, value) +} + +// Transfer is a paid mutator transaction binding the contract method 0xa9059cbb. +// +// Solidity: function transfer(address to, uint256 value) returns(bool) +func (_MockERC20 *MockERC20TransactorSession) Transfer(to common.Address, value *big.Int) (*types.Transaction, error) { + return _MockERC20.Contract.Transfer(&_MockERC20.TransactOpts, to, value) +} + +// TransferFrom is a paid mutator transaction binding the contract method 0x23b872dd. +// +// Solidity: function transferFrom(address from, address to, uint256 value) returns(bool) +func (_MockERC20 *MockERC20Transactor) TransferFrom(opts *bind.TransactOpts, from common.Address, to common.Address, value *big.Int) (*types.Transaction, error) { + return _MockERC20.contract.Transact(opts, "transferFrom", from, to, value) +} + +// TransferFrom is a paid mutator transaction binding the contract method 0x23b872dd. +// +// Solidity: function transferFrom(address from, address to, uint256 value) returns(bool) +func (_MockERC20 *MockERC20Session) TransferFrom(from common.Address, to common.Address, value *big.Int) (*types.Transaction, error) { + return _MockERC20.Contract.TransferFrom(&_MockERC20.TransactOpts, from, to, value) +} + +// TransferFrom is a paid mutator transaction binding the contract method 0x23b872dd. +// +// Solidity: function transferFrom(address from, address to, uint256 value) returns(bool) +func (_MockERC20 *MockERC20TransactorSession) TransferFrom(from common.Address, to common.Address, value *big.Int) (*types.Transaction, error) { + return _MockERC20.Contract.TransferFrom(&_MockERC20.TransactOpts, from, to, value) +} + +// MockERC20ApprovalIterator is returned from FilterApproval and is used to iterate over the raw logs and unpacked data for Approval events raised by the MockERC20 contract. +type MockERC20ApprovalIterator struct { + Event *MockERC20Approval // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *MockERC20ApprovalIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(MockERC20Approval) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(MockERC20Approval) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *MockERC20ApprovalIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *MockERC20ApprovalIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// MockERC20Approval represents a Approval event raised by the MockERC20 contract. +type MockERC20Approval struct { + Owner common.Address + Spender common.Address + Value *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterApproval is a free log retrieval operation binding the contract event 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925. +// +// Solidity: event Approval(address indexed owner, address indexed spender, uint256 value) +func (_MockERC20 *MockERC20Filterer) FilterApproval(opts *bind.FilterOpts, owner []common.Address, spender []common.Address) (*MockERC20ApprovalIterator, error) { + + var ownerRule []interface{} + for _, ownerItem := range owner { + ownerRule = append(ownerRule, ownerItem) + } + var spenderRule []interface{} + for _, spenderItem := range spender { + spenderRule = append(spenderRule, spenderItem) + } + + logs, sub, err := _MockERC20.contract.FilterLogs(opts, "Approval", ownerRule, spenderRule) + if err != nil { + return nil, err + } + return &MockERC20ApprovalIterator{contract: _MockERC20.contract, event: "Approval", logs: logs, sub: sub}, nil +} + +// WatchApproval is a free log subscription operation binding the contract event 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925. +// +// Solidity: event Approval(address indexed owner, address indexed spender, uint256 value) +func (_MockERC20 *MockERC20Filterer) WatchApproval(opts *bind.WatchOpts, sink chan<- *MockERC20Approval, owner []common.Address, spender []common.Address) (event.Subscription, error) { + + var ownerRule []interface{} + for _, ownerItem := range owner { + ownerRule = append(ownerRule, ownerItem) + } + var spenderRule []interface{} + for _, spenderItem := range spender { + spenderRule = append(spenderRule, spenderItem) + } + + logs, sub, err := _MockERC20.contract.WatchLogs(opts, "Approval", ownerRule, spenderRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(MockERC20Approval) + if err := _MockERC20.contract.UnpackLog(event, "Approval", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseApproval is a log parse operation binding the contract event 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925. +// +// Solidity: event Approval(address indexed owner, address indexed spender, uint256 value) +func (_MockERC20 *MockERC20Filterer) ParseApproval(log types.Log) (*MockERC20Approval, error) { + event := new(MockERC20Approval) + if err := _MockERC20.contract.UnpackLog(event, "Approval", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// MockERC20TransferIterator is returned from FilterTransfer and is used to iterate over the raw logs and unpacked data for Transfer events raised by the MockERC20 contract. +type MockERC20TransferIterator struct { + Event *MockERC20Transfer // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *MockERC20TransferIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(MockERC20Transfer) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(MockERC20Transfer) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *MockERC20TransferIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *MockERC20TransferIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// MockERC20Transfer represents a Transfer event raised by the MockERC20 contract. +type MockERC20Transfer struct { + From common.Address + To common.Address + Value *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterTransfer is a free log retrieval operation binding the contract event 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef. +// +// Solidity: event Transfer(address indexed from, address indexed to, uint256 value) +func (_MockERC20 *MockERC20Filterer) FilterTransfer(opts *bind.FilterOpts, from []common.Address, to []common.Address) (*MockERC20TransferIterator, error) { + + var fromRule []interface{} + for _, fromItem := range from { + fromRule = append(fromRule, fromItem) + } + var toRule []interface{} + for _, toItem := range to { + toRule = append(toRule, toItem) + } + + logs, sub, err := _MockERC20.contract.FilterLogs(opts, "Transfer", fromRule, toRule) + if err != nil { + return nil, err + } + return &MockERC20TransferIterator{contract: _MockERC20.contract, event: "Transfer", logs: logs, sub: sub}, nil +} + +// WatchTransfer is a free log subscription operation binding the contract event 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef. +// +// Solidity: event Transfer(address indexed from, address indexed to, uint256 value) +func (_MockERC20 *MockERC20Filterer) WatchTransfer(opts *bind.WatchOpts, sink chan<- *MockERC20Transfer, from []common.Address, to []common.Address) (event.Subscription, error) { + + var fromRule []interface{} + for _, fromItem := range from { + fromRule = append(fromRule, fromItem) + } + var toRule []interface{} + for _, toItem := range to { + toRule = append(toRule, toItem) + } + + logs, sub, err := _MockERC20.contract.WatchLogs(opts, "Transfer", fromRule, toRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(MockERC20Transfer) + if err := _MockERC20.contract.UnpackLog(event, "Transfer", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseTransfer is a log parse operation binding the contract event 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef. +// +// Solidity: event Transfer(address indexed from, address indexed to, uint256 value) +func (_MockERC20 *MockERC20Filterer) ParseTransfer(log types.Log) (*MockERC20Transfer, error) { + event := new(MockERC20Transfer) + if err := _MockERC20.contract.UnpackLog(event, "Transfer", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} diff --git a/pkg/blockchain/evm/nodeid_abi.go b/pkg/blockchain/evm/nodeid_abi.go new file mode 100644 index 0000000..92be6ab --- /dev/null +++ b/pkg/blockchain/evm/nodeid_abi.go @@ -0,0 +1,1823 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package evm + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// NodeIDTerms is an auto generated low-level Go binding around an user-defined struct. +type NodeIDTerms struct { + MintedAt uint64 + VestingPeriod uint64 + MinActivationAmount *big.Int +} + +// NodeIDMetaData contains all meta data concerning the NodeID contract. +var NodeIDMetaData = &bind.MetaData{ + ABI: "[{\"type\":\"constructor\",\"inputs\":[{\"name\":\"_owner\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"MAX_NODES\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"approve\",\"inputs\":[{\"name\":\"to\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"tokenId\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"availableSlots\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"balanceOf\",\"inputs\":[{\"name\":\"owner\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"baseTokenURI\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"string\",\"internalType\":\"string\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"getApproved\",\"inputs\":[{\"name\":\"tokenId\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"isApprovedForAll\",\"inputs\":[{\"name\":\"owner\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"operator\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"mint\",\"inputs\":[{\"name\":\"to\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"minActivationAmount\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"vestingPeriod\",\"type\":\"uint64\",\"internalType\":\"uint64\"}],\"outputs\":[{\"name\":\"tokenId\",\"type\":\"uint32\",\"internalType\":\"uint32\"}],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"mintBatch\",\"inputs\":[{\"name\":\"recipients\",\"type\":\"address[]\",\"internalType\":\"address[]\"},{\"name\":\"minActivationAmount\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"vestingPeriod\",\"type\":\"uint64\",\"internalType\":\"uint64\"}],\"outputs\":[{\"name\":\"firstTokenId\",\"type\":\"uint32\",\"internalType\":\"uint32\"},{\"name\":\"lastTokenId\",\"type\":\"uint32\",\"internalType\":\"uint32\"}],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"mintToRegistry\",\"inputs\":[{\"name\":\"minActivationAmount\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"vestingPeriod\",\"type\":\"uint64\",\"internalType\":\"uint64\"}],\"outputs\":[{\"name\":\"tokenId\",\"type\":\"uint32\",\"internalType\":\"uint32\"}],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"name\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"string\",\"internalType\":\"string\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"owner\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"ownerOf\",\"inputs\":[{\"name\":\"tokenId\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"registry\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"safeTransferFrom\",\"inputs\":[{\"name\":\"from\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"to\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"tokenId\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"safeTransferFrom\",\"inputs\":[{\"name\":\"from\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"to\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"tokenId\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"data\",\"type\":\"bytes\",\"internalType\":\"bytes\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"setApprovalForAll\",\"inputs\":[{\"name\":\"operator\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"approved\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"setBaseTokenURI\",\"inputs\":[{\"name\":\"baseURI\",\"type\":\"string\",\"internalType\":\"string\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"setRegistry\",\"inputs\":[{\"name\":\"newRegistry\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"supportsInterface\",\"inputs\":[{\"name\":\"interfaceId\",\"type\":\"bytes4\",\"internalType\":\"bytes4\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"symbol\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"string\",\"internalType\":\"string\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"termsOf\",\"inputs\":[{\"name\":\"tokenId\",\"type\":\"uint32\",\"internalType\":\"uint32\"}],\"outputs\":[{\"name\":\"\",\"type\":\"tuple\",\"internalType\":\"structNodeID.Terms\",\"components\":[{\"name\":\"mintedAt\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"vestingPeriod\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"minActivationAmount\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"tokenURI\",\"inputs\":[{\"name\":\"tokenId\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[{\"name\":\"\",\"type\":\"string\",\"internalType\":\"string\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"transferFrom\",\"inputs\":[{\"name\":\"from\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"to\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"tokenId\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"transferOwnership\",\"inputs\":[{\"name\":\"newOwner\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"event\",\"name\":\"Approval\",\"inputs\":[{\"name\":\"owner\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"approved\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"tokenId\",\"type\":\"uint256\",\"indexed\":true,\"internalType\":\"uint256\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"ApprovalForAll\",\"inputs\":[{\"name\":\"owner\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"operator\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"approved\",\"type\":\"bool\",\"indexed\":false,\"internalType\":\"bool\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"OwnershipTransferred\",\"inputs\":[{\"name\":\"oldOwner\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"newOwner\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"RegistryUpdated\",\"inputs\":[{\"name\":\"oldRegistry\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"newRegistry\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"SlotsMinted\",\"inputs\":[{\"name\":\"by\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"firstTokenId\",\"type\":\"uint32\",\"indexed\":true,\"internalType\":\"uint32\"},{\"name\":\"lastTokenId\",\"type\":\"uint32\",\"indexed\":true,\"internalType\":\"uint32\"},{\"name\":\"minActivationAmount\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"},{\"name\":\"vestingPeriod\",\"type\":\"uint64\",\"indexed\":false,\"internalType\":\"uint64\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"Transfer\",\"inputs\":[{\"name\":\"from\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"to\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"tokenId\",\"type\":\"uint256\",\"indexed\":true,\"internalType\":\"uint256\"}],\"anonymous\":false},{\"type\":\"error\",\"name\":\"AllSlotsMinted\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"DirectRegistryTransfer\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ERC721IncorrectOwner\",\"inputs\":[{\"name\":\"sender\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"tokenId\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"owner\",\"type\":\"address\",\"internalType\":\"address\"}]},{\"type\":\"error\",\"name\":\"ERC721InsufficientApproval\",\"inputs\":[{\"name\":\"operator\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"tokenId\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]},{\"type\":\"error\",\"name\":\"ERC721InvalidApprover\",\"inputs\":[{\"name\":\"approver\",\"type\":\"address\",\"internalType\":\"address\"}]},{\"type\":\"error\",\"name\":\"ERC721InvalidOperator\",\"inputs\":[{\"name\":\"operator\",\"type\":\"address\",\"internalType\":\"address\"}]},{\"type\":\"error\",\"name\":\"ERC721InvalidOwner\",\"inputs\":[{\"name\":\"owner\",\"type\":\"address\",\"internalType\":\"address\"}]},{\"type\":\"error\",\"name\":\"ERC721InvalidReceiver\",\"inputs\":[{\"name\":\"receiver\",\"type\":\"address\",\"internalType\":\"address\"}]},{\"type\":\"error\",\"name\":\"ERC721InvalidSender\",\"inputs\":[{\"name\":\"sender\",\"type\":\"address\",\"internalType\":\"address\"}]},{\"type\":\"error\",\"name\":\"ERC721NonexistentToken\",\"inputs\":[{\"name\":\"tokenId\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]},{\"type\":\"error\",\"name\":\"NotOwner\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"NotRegistry\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ZeroAddress\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ZeroCount\",\"inputs\":[]}]", + Bin: "0x60806040523461037a57611b496020813803918261001c8161037e565b93849283398101031261037a57516001600160a01b0381169081900361037a57610046604061037e565b90600e82526d10db19585c939bd9194814db1bdd60921b602083015261006c604061037e565b600681526510d394d313d560d21b602082015282519091906001600160401b038111610283575f54600181811c91168015610370575b602082101461026557601f8111610303575b506020601f82116001146102a257819293945f92610297575b50508160011b915f199060031b1c1916175f555b81516001600160401b03811161028357600154600181811c91168015610279575b602082101461026557601f81116101f7575b50602092601f821160011461019657928192935f9261018b575b50508160011b915f199060031b1c1916176001555b600163ffffffff196009541617600955801561017c57600680546001600160a01b0319169190911790556040516117a590816103a48239f35b63d92e233d60e01b5f5260045ffd5b015190505f8061012e565b601f1982169360015f52805f20915f5b8681106101df57508360019596106101c7575b505050811b01600155610143565b01515f1960f88460031b161c191690555f80806101b9565b919260206001819286850151815501940192016101a6565b818111156101145760015f52601f820160051c7fb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf66020841061025d575b81601f9101920160051c03905f5b828110610250575050610114565b5f82820155600101610242565b5f9150610234565b634e487b7160e01b5f52602260045260245ffd5b90607f1690610102565b634e487b7160e01b5f52604160045260245ffd5b015190505f806100cd565b601f198216905f8052805f20915f5b8181106102eb575095836001959697106102d3575b505050811b015f556100e1565b01515f1960f88460031b161c191690555f80806102c6565b9192602060018192868b0151815501940192016102b1565b818111156100b4575f8052601f820160051c7f290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e56360208410610368575b81601f9101920160051c03905f5b82811061035b5750506100b4565b5f8282015560010161034d565b5f915061033f565b90607f16906100a2565b5f80fd5b6040519190601f01601f191682016001600160401b038111838210176102835760405256fe6080806040526004361015610012575f80fd5b5f3560e01c90816301ffc9a714610d3b5750806306fdde0314610c99578063081812fc14610c5d578063095ea7b314610b7357806323b872dd14610b5c57806330176e13146109f657806342842e0e146109cd5780636352211e1461099d57806370a082311461094c5780637b103999146109245780638b3d35ae1461088e5780638da5cb5b146108665780638eeda103146107cc5780638f16e1cd146107af57806395d89b411461070a578063a22cb4651461066f578063a91ee0dc146105fc578063b88d4fde14610573578063c87b56dd14610554578063d547cfb714610485578063e6fb38131461042a578063e8804a2b1461033f578063e985e9c5146102e8578063f2fde38b146102665763ff875f031461012f575f80fd5b3461024e57606036600319011261024e576004356001600160401b03811161024e573660238201121561024e578060040135906001600160401b038211610252578160051b9060208201926101876040519485610e61565b8352602460208401928201019036821161024e57602401915b81831061022e57604063ffffffff61021f60243582817f9902251bf2894876d6d1dc26c5e2005e75018334706538cb7bf283598aefc42b6101f38b6101e3610e30565b9586916101ee6114e5565b611515565b93169586931694859488519182913395839092916001600160401b036020916040840195845216910152565b0390a482519182526020820152f35b82356001600160a01b038116810361024e578152602092830192016101a0565b5f80fd5b634e487b7160e01b5f52604160045260245ffd5b3461024e57602036600319011261024e5761027f610dca565b6102876114e5565b6001600160a01b031680156102d957600680546001600160a01b0319811683179091556001600160a01b03167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e05f80a3005b63d92e233d60e01b5f5260045ffd5b3461024e57604036600319011261024e57610301610dca565b610309610de0565b9060018060a01b03165f52600560205260405f209060018060a01b03165f52602052602060ff60405f2054166040519015158152f35b3461024e57604036600319011261024e576004356024356001600160401b038116810361024e576007546001600160a01b031633811480610421575b15610412576104097f9902251bf2894876d6d1dc26c5e2005e75018334706538cb7bf283598aefc42b9260209463ffffffff6103df83836040978851906103c28a83610e61565b60018252601f198a01368d8401376103d9826110d6565b52611515565b5016948593849386519182913395839092916001600160401b036020916040840195845216910152565b0390a451908152f35b633217675b60e21b5f5260045ffd5b5080151561037b565b3461024e575f36600319011261024e575f1963ffffffff600954160163ffffffff81116104715763ffffffff16620100000362010000811161047157602090604051908152f35b634e487b7160e01b5f52601160045260245ffd5b3461024e575f36600319011261024e576040515f6008546104a581610e9d565b808452906001811690811561053057506001146104e5575b6104e1836104cd81850382610e61565b604051918291602083526020830190610da6565b0390f35b60085f9081525f5160206117855f395f51905f52939250905b808210610516575090915081016020016104cd6104bd565b9192600181602092548385880101520191019092916104fe565b60ff191660208086019190915291151560051b840190910191506104cd90506104bd565b3461024e57602036600319011261024e576104e16104cd60043561124b565b3461024e57608036600319011261024e5761058c610dca565b610594610de0565b606435916001600160401b03831161024e573660238401121561024e578260040135916105c083610e82565b926105ce6040519485610e61565b808452366024828701011161024e576020815f9260246105fa980183880137850101526044359161110b565b005b3461024e57602036600319011261024e57610615610dca565b61061d6114e5565b6001600160a01b031680156102d957600780546001600160a01b0319811683179091556001600160a01b03167f482b97c53e48ffa324a976e2738053e9aff6eee04d8aac63b10e19411d869b825f80a3005b3461024e57604036600319011261024e57610688610dca565b6024359081151580920361024e576001600160a01b03169081156106f757335f52600560205260405f20825f5260205260405f2060ff1981541660ff83161790556040519081527f17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c3160203392a3005b50630b61174360e31b5f5260045260245ffd5b3461024e575f36600319011261024e576040515f60015461072a81610e9d565b80845290600181169081156105305750600114610751576104e1836104cd81850382610e61565b60015f9081527fb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6939250905b808210610795575090915081016020016104cd6104bd565b91926001816020925483858801015201910190929161077d565b3461024e575f36600319011261024e576020604051620100008152f35b3461024e57602036600319011261024e5760043563ffffffff811680910361024e575f604080516107fc81610e46565b8281528260208201520152610810816114b1565b505f52600a602052606060405f2060405161082a81610e46565b6001600160401b0382546040600183831695868652846020870194841c1684520154930192835260405193845251166020830152516040820152f35b3461024e575f36600319011261024e576006546040516001600160a01b039091168152602090f35b3461024e57606036600319011261024e5760207f9902251bf2894876d6d1dc26c5e2005e75018334706538cb7bf283598aefc42b6108ca610dca565b6104096024356108d8610e30565b906108e16114e5565b63ffffffff6103df83836040978851906108fb8a83610e61565b60018252601f198a01368d840137610912826110d6565b6001600160a01b039091169052611515565b3461024e575f36600319011261024e576007546040516001600160a01b039091168152602090f35b3461024e57602036600319011261024e576001600160a01b0361096d610dca565b16801561098a575f526003602052602060405f2054604051908152f35b6322718ad960e21b5f525f60045260245ffd5b3461024e57602036600319011261024e5760206109bb6004356114b1565b6040516001600160a01b039091168152f35b3461024e576105fa6109de36610df6565b90604051926109ee602085610e61565b5f845261110b565b3461024e57602036600319011261024e576004356001600160401b03811161024e573660238201121561024e5780600401356001600160401b03811161024e57366024828401011161024e57610a4a6114e5565b610a55600854610e9d565b601f8111610b05575b505f601f8211600114610a9a5781925f92610a8c575b50505f19600383901b1c191660019190911b17600855005b602492500101358280610a74565b601f198216925f5160206117855f395f51905f52915f5b858110610aea57508360019510610ace575b505050811b01600855005b01602401355f19600384901b60f8161c19169055828080610ac3565b90926020600181926024878701013581550194019101610ab1565b81811115610a5e57601f820160051c9060208310610b54575b601f82910160051c03905f5b828110610b38575050610a5e565b5f8282015f5160206117855f395f51905f520155600101610b2a565b5f9150610b1e565b3461024e576105fa610b6d36610df6565b91610ed5565b3461024e57604036600319011261024e57610b8c610dca565b602435610b98816114b1565b33151580610c4a575b80610c1d575b610c0a5781906001600160a01b0384811691167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9255f80a45f90815260046020526040902080546001600160a01b0319166001600160a01b03909216919091179055005b63a9fbf51f60e01b5f523360045260245ffd5b506001600160a01b0381165f90815260056020908152604080832033845290915290205460ff1615610ba7565b506001600160a01b038116331415610ba1565b3461024e57602036600319011261024e57600435610c7a816114b1565b505f526004602052602060018060a01b0360405f205416604051908152f35b3461024e575f36600319011261024e576040515f5f54610cb881610e9d565b80845290600181169081156105305750600114610cdf576104e1836104cd81850382610e61565b5f8080527f290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563939250905b808210610d21575090915081016020016104cd6104bd565b919260018160209254838588010152019101909291610d09565b3461024e57602036600319011261024e576004359063ffffffff60e01b821680920361024e576020916380ac58cd60e01b8114908115610d95575b8115610d84575b5015158152f35b6301ffc9a760e01b14905083610d7d565b635b5e139f60e01b81149150610d76565b805180835260209291819084018484015e5f828201840152601f01601f1916010190565b600435906001600160a01b038216820361024e57565b602435906001600160a01b038216820361024e57565b606090600319011261024e576004356001600160a01b038116810361024e57906024356001600160a01b038116810361024e579060443590565b604435906001600160401b038216820361024e57565b606081019081106001600160401b0382111761025257604052565b90601f801991011681019081106001600160401b0382111761025257604052565b6001600160401b03811161025257601f01601f191660200190565b90600182811c92168015610ecb575b6020831014610eb757565b634e487b7160e01b5f52602260045260245ffd5b91607f1691610eac565b6001600160a01b03909116919082156110c3575f828152600260205260409020546001600160a01b03161515806110af575b8061109a575b61108b575f828152600260205260409020546001600160a01b031692829033151580610ff6575b5084610fc3575b805f52600360205260405f2060018154019055815f52600260205260405f20816bffffffffffffffffffffffff60a01b825416179055847fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef5f80a46001600160a01b0316808303610fab57505050565b6364283d7b60e01b5f5260045260245260445260645ffd5b5f82815260046020526040902080546001600160a01b0319169055845f52600360205260405f205f198154019055610f3b565b9091508061103a575b1561100c5782905f610f34565b828461102457637e27328960e01b5f5260045260245ffd5b63177e802f60e01b5f523360045260245260445ffd5b503384148015611069575b80610fff57505f838152600460205260409020546001600160a01b03163314610fff565b505f84815260056020908152604080832033845290915290205460ff16611045565b63588e7fef60e11b5f5260045ffd5b506007546001600160a01b0316331415610f0d565b506007546001600160a01b03168314610f07565b633250574960e11b5f525f60045260245ffd5b8051156110e35760200190565b634e487b7160e01b5f52603260045260245ffd5b80518210156110e35760209160051b010190565b9291611118818386610ed5565b813b611125575b50505050565b604051630a85bd0160e11b81523360048201526001600160a01b0394851660248201526044810191909152608060648201529216919060209082908190611170906084830190610da6565b03815f865af15f9181611206575b506111d357503d156111cc573d61119481610e82565b906111a26040519283610e61565b81523d5f602083013e5b805190816111c75782633250574960e11b5f5260045260245ffd5b602001fd5b60606111ac565b6001600160e01b03191663757a42ff60e11b016111f457505f80808061111f565b633250574960e11b5f5260045260245ffd5b9091506020813d602011611243575b8161122260209383610e61565b8101031261024e57516001600160e01b03198116810361024e57905f61117e565b3d9150611215565b611254816114b1565b506008549061126282610e9d565b1561149b5780815f9272184f03e93ff9f4daa797ed6e38ed64bf6a1f0160401b811015611475575b50806d04ee2d6d415b85acef8100000000600a92101561145a575b662386f26fc10000811015611446575b6305f5e100811015611435575b612710811015611426575b6064811015611418575b101561140e575b6001820190600a60216113096112f385610e82565b946113016040519687610e61565b808652610e82565b602085019590601f19013687378401015b5f1901916f181899199a1a9b1b9c1cb0b131b232b360811b8282061a835304801561134857600a909161131a565b50506040519283915f9161135b81610e9d565b90600181169081156113ea575060011461139d575b50926005929161139a94518092825e0164173539b7b760d91b815203601a19810184520182610e61565b90565b90915060085f525f5160206117855f395f51905f525f905b8282106113cc57505082016020019061139a611370565b60209192939450806001915483858a010152019101859392916113b5565b60ff19166020808701919091528215159092028501909101925061139a9050611370565b90600101906112de565b6064600291049301926112d7565b612710600491049301926112cd565b6305f5e100600891049301926112c2565b662386f26fc10000601091049301926112b5565b6d04ee2d6d415b85acef8100000000602091049301926112a5565b6040935072184f03e93ff9f4daa797ed6e38ed64bf6a1f0160401b90049050600a61128a565b50506040516114ab602082610e61565b5f815290565b5f818152600260205260409020546001600160a01b03169081156114d3575090565b637e27328960e01b5f5260045260245ffd5b6006546001600160a01b031633036114f957565b6330cd747160e01b5f5260045ffd5b9190820180921161047157565b9291928051156117755763ffffffff6009541691611534825184611508565b925f19840184811161047157620100008111611766579394426001600160401b031694905f5b8551811015611745576001600160a01b0361157582886110f7565b5116156102d95763ffffffff61158b8286611508565b16906001600160a01b0361159f82896110f7565b511680156110c3575f838152600260205260409020546001600160a01b0316151580611731575b8061171c575b61108b575f838152600260205260409020546001600160a01b0316801515918490836116e9575b5f818152600360209081526040808320805460010190558483526002909152812080546001600160a01b0319166001600160a01b03841617905583907fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9080a4506116d657600191826040519161166983610e46565b8a83528c6001600160401b03602085019116815260408401918a83525f52600a6020526001600160401b0360405f209451166fffffffffffffffff00000000000000008554925160401b16916fffffffffffffffffffffffffffffffff191617178355519101550161155a565b6339e3563760e11b5f525f60045260245ffd5b5f82815260046020526040902080546001600160a01b0319169055825f52600360205260405f205f1981540190556115f3565b506007546001600160a01b03163314156115cc565b506007546001600160a01b031681146115c6565b5095919350955063ffffffff93508391501682196009541617600955921690565b6304710b1360e11b5f5260045ffd5b63011ee73b60e21b5f5260045ffdfef3f7a9fe364faab93b216da50a3214154f22a0a2b415b23a84c8169e8b636ee3", +} + +// NodeIDABI is the input ABI used to generate the binding from. +// Deprecated: Use NodeIDMetaData.ABI instead. +var NodeIDABI = NodeIDMetaData.ABI + +// NodeIDBin is the compiled bytecode used for deploying new contracts. +// Deprecated: Use NodeIDMetaData.Bin instead. +var NodeIDBin = NodeIDMetaData.Bin + +// DeployNodeID deploys a new Ethereum contract, binding an instance of NodeID to it. +func DeployNodeID(auth *bind.TransactOpts, backend bind.ContractBackend, _owner common.Address) (common.Address, *types.Transaction, *NodeID, error) { + parsed, err := NodeIDMetaData.GetAbi() + if err != nil { + return common.Address{}, nil, nil, err + } + if parsed == nil { + return common.Address{}, nil, nil, errors.New("GetABI returned nil") + } + + address, tx, contract, err := bind.DeployContract(auth, *parsed, common.FromHex(NodeIDBin), backend, _owner) + if err != nil { + return common.Address{}, nil, nil, err + } + return address, tx, &NodeID{NodeIDCaller: NodeIDCaller{contract: contract}, NodeIDTransactor: NodeIDTransactor{contract: contract}, NodeIDFilterer: NodeIDFilterer{contract: contract}}, nil +} + +// NodeID is an auto generated Go binding around an Ethereum contract. +type NodeID struct { + NodeIDCaller // Read-only binding to the contract + NodeIDTransactor // Write-only binding to the contract + NodeIDFilterer // Log filterer for contract events +} + +// NodeIDCaller is an auto generated read-only Go binding around an Ethereum contract. +type NodeIDCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// NodeIDTransactor is an auto generated write-only Go binding around an Ethereum contract. +type NodeIDTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// NodeIDFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type NodeIDFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// NodeIDSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type NodeIDSession struct { + Contract *NodeID // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// NodeIDCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type NodeIDCallerSession struct { + Contract *NodeIDCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// NodeIDTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type NodeIDTransactorSession struct { + Contract *NodeIDTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// NodeIDRaw is an auto generated low-level Go binding around an Ethereum contract. +type NodeIDRaw struct { + Contract *NodeID // Generic contract binding to access the raw methods on +} + +// NodeIDCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type NodeIDCallerRaw struct { + Contract *NodeIDCaller // Generic read-only contract binding to access the raw methods on +} + +// NodeIDTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type NodeIDTransactorRaw struct { + Contract *NodeIDTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewNodeID creates a new instance of NodeID, bound to a specific deployed contract. +func NewNodeID(address common.Address, backend bind.ContractBackend) (*NodeID, error) { + contract, err := bindNodeID(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &NodeID{NodeIDCaller: NodeIDCaller{contract: contract}, NodeIDTransactor: NodeIDTransactor{contract: contract}, NodeIDFilterer: NodeIDFilterer{contract: contract}}, nil +} + +// NewNodeIDCaller creates a new read-only instance of NodeID, bound to a specific deployed contract. +func NewNodeIDCaller(address common.Address, caller bind.ContractCaller) (*NodeIDCaller, error) { + contract, err := bindNodeID(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &NodeIDCaller{contract: contract}, nil +} + +// NewNodeIDTransactor creates a new write-only instance of NodeID, bound to a specific deployed contract. +func NewNodeIDTransactor(address common.Address, transactor bind.ContractTransactor) (*NodeIDTransactor, error) { + contract, err := bindNodeID(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &NodeIDTransactor{contract: contract}, nil +} + +// NewNodeIDFilterer creates a new log filterer instance of NodeID, bound to a specific deployed contract. +func NewNodeIDFilterer(address common.Address, filterer bind.ContractFilterer) (*NodeIDFilterer, error) { + contract, err := bindNodeID(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &NodeIDFilterer{contract: contract}, nil +} + +// bindNodeID binds a generic wrapper to an already deployed contract. +func bindNodeID(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := NodeIDMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_NodeID *NodeIDRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _NodeID.Contract.NodeIDCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_NodeID *NodeIDRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _NodeID.Contract.NodeIDTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_NodeID *NodeIDRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _NodeID.Contract.NodeIDTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_NodeID *NodeIDCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _NodeID.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_NodeID *NodeIDTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _NodeID.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_NodeID *NodeIDTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _NodeID.Contract.contract.Transact(opts, method, params...) +} + +// MAXNODES is a free data retrieval call binding the contract method 0x8f16e1cd. +// +// Solidity: function MAX_NODES() view returns(uint256) +func (_NodeID *NodeIDCaller) MAXNODES(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _NodeID.contract.Call(opts, &out, "MAX_NODES") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// MAXNODES is a free data retrieval call binding the contract method 0x8f16e1cd. +// +// Solidity: function MAX_NODES() view returns(uint256) +func (_NodeID *NodeIDSession) MAXNODES() (*big.Int, error) { + return _NodeID.Contract.MAXNODES(&_NodeID.CallOpts) +} + +// MAXNODES is a free data retrieval call binding the contract method 0x8f16e1cd. +// +// Solidity: function MAX_NODES() view returns(uint256) +func (_NodeID *NodeIDCallerSession) MAXNODES() (*big.Int, error) { + return _NodeID.Contract.MAXNODES(&_NodeID.CallOpts) +} + +// AvailableSlots is a free data retrieval call binding the contract method 0xe6fb3813. +// +// Solidity: function availableSlots() view returns(uint256) +func (_NodeID *NodeIDCaller) AvailableSlots(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _NodeID.contract.Call(opts, &out, "availableSlots") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// AvailableSlots is a free data retrieval call binding the contract method 0xe6fb3813. +// +// Solidity: function availableSlots() view returns(uint256) +func (_NodeID *NodeIDSession) AvailableSlots() (*big.Int, error) { + return _NodeID.Contract.AvailableSlots(&_NodeID.CallOpts) +} + +// AvailableSlots is a free data retrieval call binding the contract method 0xe6fb3813. +// +// Solidity: function availableSlots() view returns(uint256) +func (_NodeID *NodeIDCallerSession) AvailableSlots() (*big.Int, error) { + return _NodeID.Contract.AvailableSlots(&_NodeID.CallOpts) +} + +// BalanceOf is a free data retrieval call binding the contract method 0x70a08231. +// +// Solidity: function balanceOf(address owner) view returns(uint256) +func (_NodeID *NodeIDCaller) BalanceOf(opts *bind.CallOpts, owner common.Address) (*big.Int, error) { + var out []interface{} + err := _NodeID.contract.Call(opts, &out, "balanceOf", owner) + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// BalanceOf is a free data retrieval call binding the contract method 0x70a08231. +// +// Solidity: function balanceOf(address owner) view returns(uint256) +func (_NodeID *NodeIDSession) BalanceOf(owner common.Address) (*big.Int, error) { + return _NodeID.Contract.BalanceOf(&_NodeID.CallOpts, owner) +} + +// BalanceOf is a free data retrieval call binding the contract method 0x70a08231. +// +// Solidity: function balanceOf(address owner) view returns(uint256) +func (_NodeID *NodeIDCallerSession) BalanceOf(owner common.Address) (*big.Int, error) { + return _NodeID.Contract.BalanceOf(&_NodeID.CallOpts, owner) +} + +// BaseTokenURI is a free data retrieval call binding the contract method 0xd547cfb7. +// +// Solidity: function baseTokenURI() view returns(string) +func (_NodeID *NodeIDCaller) BaseTokenURI(opts *bind.CallOpts) (string, error) { + var out []interface{} + err := _NodeID.contract.Call(opts, &out, "baseTokenURI") + + if err != nil { + return *new(string), err + } + + out0 := *abi.ConvertType(out[0], new(string)).(*string) + + return out0, err + +} + +// BaseTokenURI is a free data retrieval call binding the contract method 0xd547cfb7. +// +// Solidity: function baseTokenURI() view returns(string) +func (_NodeID *NodeIDSession) BaseTokenURI() (string, error) { + return _NodeID.Contract.BaseTokenURI(&_NodeID.CallOpts) +} + +// BaseTokenURI is a free data retrieval call binding the contract method 0xd547cfb7. +// +// Solidity: function baseTokenURI() view returns(string) +func (_NodeID *NodeIDCallerSession) BaseTokenURI() (string, error) { + return _NodeID.Contract.BaseTokenURI(&_NodeID.CallOpts) +} + +// GetApproved is a free data retrieval call binding the contract method 0x081812fc. +// +// Solidity: function getApproved(uint256 tokenId) view returns(address) +func (_NodeID *NodeIDCaller) GetApproved(opts *bind.CallOpts, tokenId *big.Int) (common.Address, error) { + var out []interface{} + err := _NodeID.contract.Call(opts, &out, "getApproved", tokenId) + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// GetApproved is a free data retrieval call binding the contract method 0x081812fc. +// +// Solidity: function getApproved(uint256 tokenId) view returns(address) +func (_NodeID *NodeIDSession) GetApproved(tokenId *big.Int) (common.Address, error) { + return _NodeID.Contract.GetApproved(&_NodeID.CallOpts, tokenId) +} + +// GetApproved is a free data retrieval call binding the contract method 0x081812fc. +// +// Solidity: function getApproved(uint256 tokenId) view returns(address) +func (_NodeID *NodeIDCallerSession) GetApproved(tokenId *big.Int) (common.Address, error) { + return _NodeID.Contract.GetApproved(&_NodeID.CallOpts, tokenId) +} + +// IsApprovedForAll is a free data retrieval call binding the contract method 0xe985e9c5. +// +// Solidity: function isApprovedForAll(address owner, address operator) view returns(bool) +func (_NodeID *NodeIDCaller) IsApprovedForAll(opts *bind.CallOpts, owner common.Address, operator common.Address) (bool, error) { + var out []interface{} + err := _NodeID.contract.Call(opts, &out, "isApprovedForAll", owner, operator) + + if err != nil { + return *new(bool), err + } + + out0 := *abi.ConvertType(out[0], new(bool)).(*bool) + + return out0, err + +} + +// IsApprovedForAll is a free data retrieval call binding the contract method 0xe985e9c5. +// +// Solidity: function isApprovedForAll(address owner, address operator) view returns(bool) +func (_NodeID *NodeIDSession) IsApprovedForAll(owner common.Address, operator common.Address) (bool, error) { + return _NodeID.Contract.IsApprovedForAll(&_NodeID.CallOpts, owner, operator) +} + +// IsApprovedForAll is a free data retrieval call binding the contract method 0xe985e9c5. +// +// Solidity: function isApprovedForAll(address owner, address operator) view returns(bool) +func (_NodeID *NodeIDCallerSession) IsApprovedForAll(owner common.Address, operator common.Address) (bool, error) { + return _NodeID.Contract.IsApprovedForAll(&_NodeID.CallOpts, owner, operator) +} + +// Name is a free data retrieval call binding the contract method 0x06fdde03. +// +// Solidity: function name() view returns(string) +func (_NodeID *NodeIDCaller) Name(opts *bind.CallOpts) (string, error) { + var out []interface{} + err := _NodeID.contract.Call(opts, &out, "name") + + if err != nil { + return *new(string), err + } + + out0 := *abi.ConvertType(out[0], new(string)).(*string) + + return out0, err + +} + +// Name is a free data retrieval call binding the contract method 0x06fdde03. +// +// Solidity: function name() view returns(string) +func (_NodeID *NodeIDSession) Name() (string, error) { + return _NodeID.Contract.Name(&_NodeID.CallOpts) +} + +// Name is a free data retrieval call binding the contract method 0x06fdde03. +// +// Solidity: function name() view returns(string) +func (_NodeID *NodeIDCallerSession) Name() (string, error) { + return _NodeID.Contract.Name(&_NodeID.CallOpts) +} + +// Owner is a free data retrieval call binding the contract method 0x8da5cb5b. +// +// Solidity: function owner() view returns(address) +func (_NodeID *NodeIDCaller) Owner(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _NodeID.contract.Call(opts, &out, "owner") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// Owner is a free data retrieval call binding the contract method 0x8da5cb5b. +// +// Solidity: function owner() view returns(address) +func (_NodeID *NodeIDSession) Owner() (common.Address, error) { + return _NodeID.Contract.Owner(&_NodeID.CallOpts) +} + +// Owner is a free data retrieval call binding the contract method 0x8da5cb5b. +// +// Solidity: function owner() view returns(address) +func (_NodeID *NodeIDCallerSession) Owner() (common.Address, error) { + return _NodeID.Contract.Owner(&_NodeID.CallOpts) +} + +// OwnerOf is a free data retrieval call binding the contract method 0x6352211e. +// +// Solidity: function ownerOf(uint256 tokenId) view returns(address) +func (_NodeID *NodeIDCaller) OwnerOf(opts *bind.CallOpts, tokenId *big.Int) (common.Address, error) { + var out []interface{} + err := _NodeID.contract.Call(opts, &out, "ownerOf", tokenId) + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// OwnerOf is a free data retrieval call binding the contract method 0x6352211e. +// +// Solidity: function ownerOf(uint256 tokenId) view returns(address) +func (_NodeID *NodeIDSession) OwnerOf(tokenId *big.Int) (common.Address, error) { + return _NodeID.Contract.OwnerOf(&_NodeID.CallOpts, tokenId) +} + +// OwnerOf is a free data retrieval call binding the contract method 0x6352211e. +// +// Solidity: function ownerOf(uint256 tokenId) view returns(address) +func (_NodeID *NodeIDCallerSession) OwnerOf(tokenId *big.Int) (common.Address, error) { + return _NodeID.Contract.OwnerOf(&_NodeID.CallOpts, tokenId) +} + +// Registry is a free data retrieval call binding the contract method 0x7b103999. +// +// Solidity: function registry() view returns(address) +func (_NodeID *NodeIDCaller) Registry(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _NodeID.contract.Call(opts, &out, "registry") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// Registry is a free data retrieval call binding the contract method 0x7b103999. +// +// Solidity: function registry() view returns(address) +func (_NodeID *NodeIDSession) Registry() (common.Address, error) { + return _NodeID.Contract.Registry(&_NodeID.CallOpts) +} + +// Registry is a free data retrieval call binding the contract method 0x7b103999. +// +// Solidity: function registry() view returns(address) +func (_NodeID *NodeIDCallerSession) Registry() (common.Address, error) { + return _NodeID.Contract.Registry(&_NodeID.CallOpts) +} + +// SupportsInterface is a free data retrieval call binding the contract method 0x01ffc9a7. +// +// Solidity: function supportsInterface(bytes4 interfaceId) view returns(bool) +func (_NodeID *NodeIDCaller) SupportsInterface(opts *bind.CallOpts, interfaceId [4]byte) (bool, error) { + var out []interface{} + err := _NodeID.contract.Call(opts, &out, "supportsInterface", interfaceId) + + if err != nil { + return *new(bool), err + } + + out0 := *abi.ConvertType(out[0], new(bool)).(*bool) + + return out0, err + +} + +// SupportsInterface is a free data retrieval call binding the contract method 0x01ffc9a7. +// +// Solidity: function supportsInterface(bytes4 interfaceId) view returns(bool) +func (_NodeID *NodeIDSession) SupportsInterface(interfaceId [4]byte) (bool, error) { + return _NodeID.Contract.SupportsInterface(&_NodeID.CallOpts, interfaceId) +} + +// SupportsInterface is a free data retrieval call binding the contract method 0x01ffc9a7. +// +// Solidity: function supportsInterface(bytes4 interfaceId) view returns(bool) +func (_NodeID *NodeIDCallerSession) SupportsInterface(interfaceId [4]byte) (bool, error) { + return _NodeID.Contract.SupportsInterface(&_NodeID.CallOpts, interfaceId) +} + +// Symbol is a free data retrieval call binding the contract method 0x95d89b41. +// +// Solidity: function symbol() view returns(string) +func (_NodeID *NodeIDCaller) Symbol(opts *bind.CallOpts) (string, error) { + var out []interface{} + err := _NodeID.contract.Call(opts, &out, "symbol") + + if err != nil { + return *new(string), err + } + + out0 := *abi.ConvertType(out[0], new(string)).(*string) + + return out0, err + +} + +// Symbol is a free data retrieval call binding the contract method 0x95d89b41. +// +// Solidity: function symbol() view returns(string) +func (_NodeID *NodeIDSession) Symbol() (string, error) { + return _NodeID.Contract.Symbol(&_NodeID.CallOpts) +} + +// Symbol is a free data retrieval call binding the contract method 0x95d89b41. +// +// Solidity: function symbol() view returns(string) +func (_NodeID *NodeIDCallerSession) Symbol() (string, error) { + return _NodeID.Contract.Symbol(&_NodeID.CallOpts) +} + +// TermsOf is a free data retrieval call binding the contract method 0x8eeda103. +// +// Solidity: function termsOf(uint32 tokenId) view returns((uint64,uint64,uint256)) +func (_NodeID *NodeIDCaller) TermsOf(opts *bind.CallOpts, tokenId uint32) (NodeIDTerms, error) { + var out []interface{} + err := _NodeID.contract.Call(opts, &out, "termsOf", tokenId) + + if err != nil { + return *new(NodeIDTerms), err + } + + out0 := *abi.ConvertType(out[0], new(NodeIDTerms)).(*NodeIDTerms) + + return out0, err + +} + +// TermsOf is a free data retrieval call binding the contract method 0x8eeda103. +// +// Solidity: function termsOf(uint32 tokenId) view returns((uint64,uint64,uint256)) +func (_NodeID *NodeIDSession) TermsOf(tokenId uint32) (NodeIDTerms, error) { + return _NodeID.Contract.TermsOf(&_NodeID.CallOpts, tokenId) +} + +// TermsOf is a free data retrieval call binding the contract method 0x8eeda103. +// +// Solidity: function termsOf(uint32 tokenId) view returns((uint64,uint64,uint256)) +func (_NodeID *NodeIDCallerSession) TermsOf(tokenId uint32) (NodeIDTerms, error) { + return _NodeID.Contract.TermsOf(&_NodeID.CallOpts, tokenId) +} + +// TokenURI is a free data retrieval call binding the contract method 0xc87b56dd. +// +// Solidity: function tokenURI(uint256 tokenId) view returns(string) +func (_NodeID *NodeIDCaller) TokenURI(opts *bind.CallOpts, tokenId *big.Int) (string, error) { + var out []interface{} + err := _NodeID.contract.Call(opts, &out, "tokenURI", tokenId) + + if err != nil { + return *new(string), err + } + + out0 := *abi.ConvertType(out[0], new(string)).(*string) + + return out0, err + +} + +// TokenURI is a free data retrieval call binding the contract method 0xc87b56dd. +// +// Solidity: function tokenURI(uint256 tokenId) view returns(string) +func (_NodeID *NodeIDSession) TokenURI(tokenId *big.Int) (string, error) { + return _NodeID.Contract.TokenURI(&_NodeID.CallOpts, tokenId) +} + +// TokenURI is a free data retrieval call binding the contract method 0xc87b56dd. +// +// Solidity: function tokenURI(uint256 tokenId) view returns(string) +func (_NodeID *NodeIDCallerSession) TokenURI(tokenId *big.Int) (string, error) { + return _NodeID.Contract.TokenURI(&_NodeID.CallOpts, tokenId) +} + +// Approve is a paid mutator transaction binding the contract method 0x095ea7b3. +// +// Solidity: function approve(address to, uint256 tokenId) returns() +func (_NodeID *NodeIDTransactor) Approve(opts *bind.TransactOpts, to common.Address, tokenId *big.Int) (*types.Transaction, error) { + return _NodeID.contract.Transact(opts, "approve", to, tokenId) +} + +// Approve is a paid mutator transaction binding the contract method 0x095ea7b3. +// +// Solidity: function approve(address to, uint256 tokenId) returns() +func (_NodeID *NodeIDSession) Approve(to common.Address, tokenId *big.Int) (*types.Transaction, error) { + return _NodeID.Contract.Approve(&_NodeID.TransactOpts, to, tokenId) +} + +// Approve is a paid mutator transaction binding the contract method 0x095ea7b3. +// +// Solidity: function approve(address to, uint256 tokenId) returns() +func (_NodeID *NodeIDTransactorSession) Approve(to common.Address, tokenId *big.Int) (*types.Transaction, error) { + return _NodeID.Contract.Approve(&_NodeID.TransactOpts, to, tokenId) +} + +// Mint is a paid mutator transaction binding the contract method 0x8b3d35ae. +// +// Solidity: function mint(address to, uint256 minActivationAmount, uint64 vestingPeriod) returns(uint32 tokenId) +func (_NodeID *NodeIDTransactor) Mint(opts *bind.TransactOpts, to common.Address, minActivationAmount *big.Int, vestingPeriod uint64) (*types.Transaction, error) { + return _NodeID.contract.Transact(opts, "mint", to, minActivationAmount, vestingPeriod) +} + +// Mint is a paid mutator transaction binding the contract method 0x8b3d35ae. +// +// Solidity: function mint(address to, uint256 minActivationAmount, uint64 vestingPeriod) returns(uint32 tokenId) +func (_NodeID *NodeIDSession) Mint(to common.Address, minActivationAmount *big.Int, vestingPeriod uint64) (*types.Transaction, error) { + return _NodeID.Contract.Mint(&_NodeID.TransactOpts, to, minActivationAmount, vestingPeriod) +} + +// Mint is a paid mutator transaction binding the contract method 0x8b3d35ae. +// +// Solidity: function mint(address to, uint256 minActivationAmount, uint64 vestingPeriod) returns(uint32 tokenId) +func (_NodeID *NodeIDTransactorSession) Mint(to common.Address, minActivationAmount *big.Int, vestingPeriod uint64) (*types.Transaction, error) { + return _NodeID.Contract.Mint(&_NodeID.TransactOpts, to, minActivationAmount, vestingPeriod) +} + +// MintBatch is a paid mutator transaction binding the contract method 0xff875f03. +// +// Solidity: function mintBatch(address[] recipients, uint256 minActivationAmount, uint64 vestingPeriod) returns(uint32 firstTokenId, uint32 lastTokenId) +func (_NodeID *NodeIDTransactor) MintBatch(opts *bind.TransactOpts, recipients []common.Address, minActivationAmount *big.Int, vestingPeriod uint64) (*types.Transaction, error) { + return _NodeID.contract.Transact(opts, "mintBatch", recipients, minActivationAmount, vestingPeriod) +} + +// MintBatch is a paid mutator transaction binding the contract method 0xff875f03. +// +// Solidity: function mintBatch(address[] recipients, uint256 minActivationAmount, uint64 vestingPeriod) returns(uint32 firstTokenId, uint32 lastTokenId) +func (_NodeID *NodeIDSession) MintBatch(recipients []common.Address, minActivationAmount *big.Int, vestingPeriod uint64) (*types.Transaction, error) { + return _NodeID.Contract.MintBatch(&_NodeID.TransactOpts, recipients, minActivationAmount, vestingPeriod) +} + +// MintBatch is a paid mutator transaction binding the contract method 0xff875f03. +// +// Solidity: function mintBatch(address[] recipients, uint256 minActivationAmount, uint64 vestingPeriod) returns(uint32 firstTokenId, uint32 lastTokenId) +func (_NodeID *NodeIDTransactorSession) MintBatch(recipients []common.Address, minActivationAmount *big.Int, vestingPeriod uint64) (*types.Transaction, error) { + return _NodeID.Contract.MintBatch(&_NodeID.TransactOpts, recipients, minActivationAmount, vestingPeriod) +} + +// MintToRegistry is a paid mutator transaction binding the contract method 0xe8804a2b. +// +// Solidity: function mintToRegistry(uint256 minActivationAmount, uint64 vestingPeriod) returns(uint32 tokenId) +func (_NodeID *NodeIDTransactor) MintToRegistry(opts *bind.TransactOpts, minActivationAmount *big.Int, vestingPeriod uint64) (*types.Transaction, error) { + return _NodeID.contract.Transact(opts, "mintToRegistry", minActivationAmount, vestingPeriod) +} + +// MintToRegistry is a paid mutator transaction binding the contract method 0xe8804a2b. +// +// Solidity: function mintToRegistry(uint256 minActivationAmount, uint64 vestingPeriod) returns(uint32 tokenId) +func (_NodeID *NodeIDSession) MintToRegistry(minActivationAmount *big.Int, vestingPeriod uint64) (*types.Transaction, error) { + return _NodeID.Contract.MintToRegistry(&_NodeID.TransactOpts, minActivationAmount, vestingPeriod) +} + +// MintToRegistry is a paid mutator transaction binding the contract method 0xe8804a2b. +// +// Solidity: function mintToRegistry(uint256 minActivationAmount, uint64 vestingPeriod) returns(uint32 tokenId) +func (_NodeID *NodeIDTransactorSession) MintToRegistry(minActivationAmount *big.Int, vestingPeriod uint64) (*types.Transaction, error) { + return _NodeID.Contract.MintToRegistry(&_NodeID.TransactOpts, minActivationAmount, vestingPeriod) +} + +// SafeTransferFrom is a paid mutator transaction binding the contract method 0x42842e0e. +// +// Solidity: function safeTransferFrom(address from, address to, uint256 tokenId) returns() +func (_NodeID *NodeIDTransactor) SafeTransferFrom(opts *bind.TransactOpts, from common.Address, to common.Address, tokenId *big.Int) (*types.Transaction, error) { + return _NodeID.contract.Transact(opts, "safeTransferFrom", from, to, tokenId) +} + +// SafeTransferFrom is a paid mutator transaction binding the contract method 0x42842e0e. +// +// Solidity: function safeTransferFrom(address from, address to, uint256 tokenId) returns() +func (_NodeID *NodeIDSession) SafeTransferFrom(from common.Address, to common.Address, tokenId *big.Int) (*types.Transaction, error) { + return _NodeID.Contract.SafeTransferFrom(&_NodeID.TransactOpts, from, to, tokenId) +} + +// SafeTransferFrom is a paid mutator transaction binding the contract method 0x42842e0e. +// +// Solidity: function safeTransferFrom(address from, address to, uint256 tokenId) returns() +func (_NodeID *NodeIDTransactorSession) SafeTransferFrom(from common.Address, to common.Address, tokenId *big.Int) (*types.Transaction, error) { + return _NodeID.Contract.SafeTransferFrom(&_NodeID.TransactOpts, from, to, tokenId) +} + +// SafeTransferFrom0 is a paid mutator transaction binding the contract method 0xb88d4fde. +// +// Solidity: function safeTransferFrom(address from, address to, uint256 tokenId, bytes data) returns() +func (_NodeID *NodeIDTransactor) SafeTransferFrom0(opts *bind.TransactOpts, from common.Address, to common.Address, tokenId *big.Int, data []byte) (*types.Transaction, error) { + return _NodeID.contract.Transact(opts, "safeTransferFrom0", from, to, tokenId, data) +} + +// SafeTransferFrom0 is a paid mutator transaction binding the contract method 0xb88d4fde. +// +// Solidity: function safeTransferFrom(address from, address to, uint256 tokenId, bytes data) returns() +func (_NodeID *NodeIDSession) SafeTransferFrom0(from common.Address, to common.Address, tokenId *big.Int, data []byte) (*types.Transaction, error) { + return _NodeID.Contract.SafeTransferFrom0(&_NodeID.TransactOpts, from, to, tokenId, data) +} + +// SafeTransferFrom0 is a paid mutator transaction binding the contract method 0xb88d4fde. +// +// Solidity: function safeTransferFrom(address from, address to, uint256 tokenId, bytes data) returns() +func (_NodeID *NodeIDTransactorSession) SafeTransferFrom0(from common.Address, to common.Address, tokenId *big.Int, data []byte) (*types.Transaction, error) { + return _NodeID.Contract.SafeTransferFrom0(&_NodeID.TransactOpts, from, to, tokenId, data) +} + +// SetApprovalForAll is a paid mutator transaction binding the contract method 0xa22cb465. +// +// Solidity: function setApprovalForAll(address operator, bool approved) returns() +func (_NodeID *NodeIDTransactor) SetApprovalForAll(opts *bind.TransactOpts, operator common.Address, approved bool) (*types.Transaction, error) { + return _NodeID.contract.Transact(opts, "setApprovalForAll", operator, approved) +} + +// SetApprovalForAll is a paid mutator transaction binding the contract method 0xa22cb465. +// +// Solidity: function setApprovalForAll(address operator, bool approved) returns() +func (_NodeID *NodeIDSession) SetApprovalForAll(operator common.Address, approved bool) (*types.Transaction, error) { + return _NodeID.Contract.SetApprovalForAll(&_NodeID.TransactOpts, operator, approved) +} + +// SetApprovalForAll is a paid mutator transaction binding the contract method 0xa22cb465. +// +// Solidity: function setApprovalForAll(address operator, bool approved) returns() +func (_NodeID *NodeIDTransactorSession) SetApprovalForAll(operator common.Address, approved bool) (*types.Transaction, error) { + return _NodeID.Contract.SetApprovalForAll(&_NodeID.TransactOpts, operator, approved) +} + +// SetBaseTokenURI is a paid mutator transaction binding the contract method 0x30176e13. +// +// Solidity: function setBaseTokenURI(string baseURI) returns() +func (_NodeID *NodeIDTransactor) SetBaseTokenURI(opts *bind.TransactOpts, baseURI string) (*types.Transaction, error) { + return _NodeID.contract.Transact(opts, "setBaseTokenURI", baseURI) +} + +// SetBaseTokenURI is a paid mutator transaction binding the contract method 0x30176e13. +// +// Solidity: function setBaseTokenURI(string baseURI) returns() +func (_NodeID *NodeIDSession) SetBaseTokenURI(baseURI string) (*types.Transaction, error) { + return _NodeID.Contract.SetBaseTokenURI(&_NodeID.TransactOpts, baseURI) +} + +// SetBaseTokenURI is a paid mutator transaction binding the contract method 0x30176e13. +// +// Solidity: function setBaseTokenURI(string baseURI) returns() +func (_NodeID *NodeIDTransactorSession) SetBaseTokenURI(baseURI string) (*types.Transaction, error) { + return _NodeID.Contract.SetBaseTokenURI(&_NodeID.TransactOpts, baseURI) +} + +// SetRegistry is a paid mutator transaction binding the contract method 0xa91ee0dc. +// +// Solidity: function setRegistry(address newRegistry) returns() +func (_NodeID *NodeIDTransactor) SetRegistry(opts *bind.TransactOpts, newRegistry common.Address) (*types.Transaction, error) { + return _NodeID.contract.Transact(opts, "setRegistry", newRegistry) +} + +// SetRegistry is a paid mutator transaction binding the contract method 0xa91ee0dc. +// +// Solidity: function setRegistry(address newRegistry) returns() +func (_NodeID *NodeIDSession) SetRegistry(newRegistry common.Address) (*types.Transaction, error) { + return _NodeID.Contract.SetRegistry(&_NodeID.TransactOpts, newRegistry) +} + +// SetRegistry is a paid mutator transaction binding the contract method 0xa91ee0dc. +// +// Solidity: function setRegistry(address newRegistry) returns() +func (_NodeID *NodeIDTransactorSession) SetRegistry(newRegistry common.Address) (*types.Transaction, error) { + return _NodeID.Contract.SetRegistry(&_NodeID.TransactOpts, newRegistry) +} + +// TransferFrom is a paid mutator transaction binding the contract method 0x23b872dd. +// +// Solidity: function transferFrom(address from, address to, uint256 tokenId) returns() +func (_NodeID *NodeIDTransactor) TransferFrom(opts *bind.TransactOpts, from common.Address, to common.Address, tokenId *big.Int) (*types.Transaction, error) { + return _NodeID.contract.Transact(opts, "transferFrom", from, to, tokenId) +} + +// TransferFrom is a paid mutator transaction binding the contract method 0x23b872dd. +// +// Solidity: function transferFrom(address from, address to, uint256 tokenId) returns() +func (_NodeID *NodeIDSession) TransferFrom(from common.Address, to common.Address, tokenId *big.Int) (*types.Transaction, error) { + return _NodeID.Contract.TransferFrom(&_NodeID.TransactOpts, from, to, tokenId) +} + +// TransferFrom is a paid mutator transaction binding the contract method 0x23b872dd. +// +// Solidity: function transferFrom(address from, address to, uint256 tokenId) returns() +func (_NodeID *NodeIDTransactorSession) TransferFrom(from common.Address, to common.Address, tokenId *big.Int) (*types.Transaction, error) { + return _NodeID.Contract.TransferFrom(&_NodeID.TransactOpts, from, to, tokenId) +} + +// TransferOwnership is a paid mutator transaction binding the contract method 0xf2fde38b. +// +// Solidity: function transferOwnership(address newOwner) returns() +func (_NodeID *NodeIDTransactor) TransferOwnership(opts *bind.TransactOpts, newOwner common.Address) (*types.Transaction, error) { + return _NodeID.contract.Transact(opts, "transferOwnership", newOwner) +} + +// TransferOwnership is a paid mutator transaction binding the contract method 0xf2fde38b. +// +// Solidity: function transferOwnership(address newOwner) returns() +func (_NodeID *NodeIDSession) TransferOwnership(newOwner common.Address) (*types.Transaction, error) { + return _NodeID.Contract.TransferOwnership(&_NodeID.TransactOpts, newOwner) +} + +// TransferOwnership is a paid mutator transaction binding the contract method 0xf2fde38b. +// +// Solidity: function transferOwnership(address newOwner) returns() +func (_NodeID *NodeIDTransactorSession) TransferOwnership(newOwner common.Address) (*types.Transaction, error) { + return _NodeID.Contract.TransferOwnership(&_NodeID.TransactOpts, newOwner) +} + +// NodeIDApprovalIterator is returned from FilterApproval and is used to iterate over the raw logs and unpacked data for Approval events raised by the NodeID contract. +type NodeIDApprovalIterator struct { + Event *NodeIDApproval // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *NodeIDApprovalIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(NodeIDApproval) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(NodeIDApproval) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *NodeIDApprovalIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *NodeIDApprovalIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// NodeIDApproval represents a Approval event raised by the NodeID contract. +type NodeIDApproval struct { + Owner common.Address + Approved common.Address + TokenId *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterApproval is a free log retrieval operation binding the contract event 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925. +// +// Solidity: event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId) +func (_NodeID *NodeIDFilterer) FilterApproval(opts *bind.FilterOpts, owner []common.Address, approved []common.Address, tokenId []*big.Int) (*NodeIDApprovalIterator, error) { + + var ownerRule []interface{} + for _, ownerItem := range owner { + ownerRule = append(ownerRule, ownerItem) + } + var approvedRule []interface{} + for _, approvedItem := range approved { + approvedRule = append(approvedRule, approvedItem) + } + var tokenIdRule []interface{} + for _, tokenIdItem := range tokenId { + tokenIdRule = append(tokenIdRule, tokenIdItem) + } + + logs, sub, err := _NodeID.contract.FilterLogs(opts, "Approval", ownerRule, approvedRule, tokenIdRule) + if err != nil { + return nil, err + } + return &NodeIDApprovalIterator{contract: _NodeID.contract, event: "Approval", logs: logs, sub: sub}, nil +} + +// WatchApproval is a free log subscription operation binding the contract event 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925. +// +// Solidity: event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId) +func (_NodeID *NodeIDFilterer) WatchApproval(opts *bind.WatchOpts, sink chan<- *NodeIDApproval, owner []common.Address, approved []common.Address, tokenId []*big.Int) (event.Subscription, error) { + + var ownerRule []interface{} + for _, ownerItem := range owner { + ownerRule = append(ownerRule, ownerItem) + } + var approvedRule []interface{} + for _, approvedItem := range approved { + approvedRule = append(approvedRule, approvedItem) + } + var tokenIdRule []interface{} + for _, tokenIdItem := range tokenId { + tokenIdRule = append(tokenIdRule, tokenIdItem) + } + + logs, sub, err := _NodeID.contract.WatchLogs(opts, "Approval", ownerRule, approvedRule, tokenIdRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(NodeIDApproval) + if err := _NodeID.contract.UnpackLog(event, "Approval", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseApproval is a log parse operation binding the contract event 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925. +// +// Solidity: event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId) +func (_NodeID *NodeIDFilterer) ParseApproval(log types.Log) (*NodeIDApproval, error) { + event := new(NodeIDApproval) + if err := _NodeID.contract.UnpackLog(event, "Approval", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// NodeIDApprovalForAllIterator is returned from FilterApprovalForAll and is used to iterate over the raw logs and unpacked data for ApprovalForAll events raised by the NodeID contract. +type NodeIDApprovalForAllIterator struct { + Event *NodeIDApprovalForAll // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *NodeIDApprovalForAllIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(NodeIDApprovalForAll) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(NodeIDApprovalForAll) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *NodeIDApprovalForAllIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *NodeIDApprovalForAllIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// NodeIDApprovalForAll represents a ApprovalForAll event raised by the NodeID contract. +type NodeIDApprovalForAll struct { + Owner common.Address + Operator common.Address + Approved bool + Raw types.Log // Blockchain specific contextual infos +} + +// FilterApprovalForAll is a free log retrieval operation binding the contract event 0x17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c31. +// +// Solidity: event ApprovalForAll(address indexed owner, address indexed operator, bool approved) +func (_NodeID *NodeIDFilterer) FilterApprovalForAll(opts *bind.FilterOpts, owner []common.Address, operator []common.Address) (*NodeIDApprovalForAllIterator, error) { + + var ownerRule []interface{} + for _, ownerItem := range owner { + ownerRule = append(ownerRule, ownerItem) + } + var operatorRule []interface{} + for _, operatorItem := range operator { + operatorRule = append(operatorRule, operatorItem) + } + + logs, sub, err := _NodeID.contract.FilterLogs(opts, "ApprovalForAll", ownerRule, operatorRule) + if err != nil { + return nil, err + } + return &NodeIDApprovalForAllIterator{contract: _NodeID.contract, event: "ApprovalForAll", logs: logs, sub: sub}, nil +} + +// WatchApprovalForAll is a free log subscription operation binding the contract event 0x17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c31. +// +// Solidity: event ApprovalForAll(address indexed owner, address indexed operator, bool approved) +func (_NodeID *NodeIDFilterer) WatchApprovalForAll(opts *bind.WatchOpts, sink chan<- *NodeIDApprovalForAll, owner []common.Address, operator []common.Address) (event.Subscription, error) { + + var ownerRule []interface{} + for _, ownerItem := range owner { + ownerRule = append(ownerRule, ownerItem) + } + var operatorRule []interface{} + for _, operatorItem := range operator { + operatorRule = append(operatorRule, operatorItem) + } + + logs, sub, err := _NodeID.contract.WatchLogs(opts, "ApprovalForAll", ownerRule, operatorRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(NodeIDApprovalForAll) + if err := _NodeID.contract.UnpackLog(event, "ApprovalForAll", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseApprovalForAll is a log parse operation binding the contract event 0x17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c31. +// +// Solidity: event ApprovalForAll(address indexed owner, address indexed operator, bool approved) +func (_NodeID *NodeIDFilterer) ParseApprovalForAll(log types.Log) (*NodeIDApprovalForAll, error) { + event := new(NodeIDApprovalForAll) + if err := _NodeID.contract.UnpackLog(event, "ApprovalForAll", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// NodeIDOwnershipTransferredIterator is returned from FilterOwnershipTransferred and is used to iterate over the raw logs and unpacked data for OwnershipTransferred events raised by the NodeID contract. +type NodeIDOwnershipTransferredIterator struct { + Event *NodeIDOwnershipTransferred // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *NodeIDOwnershipTransferredIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(NodeIDOwnershipTransferred) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(NodeIDOwnershipTransferred) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *NodeIDOwnershipTransferredIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *NodeIDOwnershipTransferredIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// NodeIDOwnershipTransferred represents a OwnershipTransferred event raised by the NodeID contract. +type NodeIDOwnershipTransferred struct { + OldOwner common.Address + NewOwner common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterOwnershipTransferred is a free log retrieval operation binding the contract event 0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0. +// +// Solidity: event OwnershipTransferred(address indexed oldOwner, address indexed newOwner) +func (_NodeID *NodeIDFilterer) FilterOwnershipTransferred(opts *bind.FilterOpts, oldOwner []common.Address, newOwner []common.Address) (*NodeIDOwnershipTransferredIterator, error) { + + var oldOwnerRule []interface{} + for _, oldOwnerItem := range oldOwner { + oldOwnerRule = append(oldOwnerRule, oldOwnerItem) + } + var newOwnerRule []interface{} + for _, newOwnerItem := range newOwner { + newOwnerRule = append(newOwnerRule, newOwnerItem) + } + + logs, sub, err := _NodeID.contract.FilterLogs(opts, "OwnershipTransferred", oldOwnerRule, newOwnerRule) + if err != nil { + return nil, err + } + return &NodeIDOwnershipTransferredIterator{contract: _NodeID.contract, event: "OwnershipTransferred", logs: logs, sub: sub}, nil +} + +// WatchOwnershipTransferred is a free log subscription operation binding the contract event 0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0. +// +// Solidity: event OwnershipTransferred(address indexed oldOwner, address indexed newOwner) +func (_NodeID *NodeIDFilterer) WatchOwnershipTransferred(opts *bind.WatchOpts, sink chan<- *NodeIDOwnershipTransferred, oldOwner []common.Address, newOwner []common.Address) (event.Subscription, error) { + + var oldOwnerRule []interface{} + for _, oldOwnerItem := range oldOwner { + oldOwnerRule = append(oldOwnerRule, oldOwnerItem) + } + var newOwnerRule []interface{} + for _, newOwnerItem := range newOwner { + newOwnerRule = append(newOwnerRule, newOwnerItem) + } + + logs, sub, err := _NodeID.contract.WatchLogs(opts, "OwnershipTransferred", oldOwnerRule, newOwnerRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(NodeIDOwnershipTransferred) + if err := _NodeID.contract.UnpackLog(event, "OwnershipTransferred", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseOwnershipTransferred is a log parse operation binding the contract event 0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0. +// +// Solidity: event OwnershipTransferred(address indexed oldOwner, address indexed newOwner) +func (_NodeID *NodeIDFilterer) ParseOwnershipTransferred(log types.Log) (*NodeIDOwnershipTransferred, error) { + event := new(NodeIDOwnershipTransferred) + if err := _NodeID.contract.UnpackLog(event, "OwnershipTransferred", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// NodeIDRegistryUpdatedIterator is returned from FilterRegistryUpdated and is used to iterate over the raw logs and unpacked data for RegistryUpdated events raised by the NodeID contract. +type NodeIDRegistryUpdatedIterator struct { + Event *NodeIDRegistryUpdated // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *NodeIDRegistryUpdatedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(NodeIDRegistryUpdated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(NodeIDRegistryUpdated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *NodeIDRegistryUpdatedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *NodeIDRegistryUpdatedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// NodeIDRegistryUpdated represents a RegistryUpdated event raised by the NodeID contract. +type NodeIDRegistryUpdated struct { + OldRegistry common.Address + NewRegistry common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterRegistryUpdated is a free log retrieval operation binding the contract event 0x482b97c53e48ffa324a976e2738053e9aff6eee04d8aac63b10e19411d869b82. +// +// Solidity: event RegistryUpdated(address indexed oldRegistry, address indexed newRegistry) +func (_NodeID *NodeIDFilterer) FilterRegistryUpdated(opts *bind.FilterOpts, oldRegistry []common.Address, newRegistry []common.Address) (*NodeIDRegistryUpdatedIterator, error) { + + var oldRegistryRule []interface{} + for _, oldRegistryItem := range oldRegistry { + oldRegistryRule = append(oldRegistryRule, oldRegistryItem) + } + var newRegistryRule []interface{} + for _, newRegistryItem := range newRegistry { + newRegistryRule = append(newRegistryRule, newRegistryItem) + } + + logs, sub, err := _NodeID.contract.FilterLogs(opts, "RegistryUpdated", oldRegistryRule, newRegistryRule) + if err != nil { + return nil, err + } + return &NodeIDRegistryUpdatedIterator{contract: _NodeID.contract, event: "RegistryUpdated", logs: logs, sub: sub}, nil +} + +// WatchRegistryUpdated is a free log subscription operation binding the contract event 0x482b97c53e48ffa324a976e2738053e9aff6eee04d8aac63b10e19411d869b82. +// +// Solidity: event RegistryUpdated(address indexed oldRegistry, address indexed newRegistry) +func (_NodeID *NodeIDFilterer) WatchRegistryUpdated(opts *bind.WatchOpts, sink chan<- *NodeIDRegistryUpdated, oldRegistry []common.Address, newRegistry []common.Address) (event.Subscription, error) { + + var oldRegistryRule []interface{} + for _, oldRegistryItem := range oldRegistry { + oldRegistryRule = append(oldRegistryRule, oldRegistryItem) + } + var newRegistryRule []interface{} + for _, newRegistryItem := range newRegistry { + newRegistryRule = append(newRegistryRule, newRegistryItem) + } + + logs, sub, err := _NodeID.contract.WatchLogs(opts, "RegistryUpdated", oldRegistryRule, newRegistryRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(NodeIDRegistryUpdated) + if err := _NodeID.contract.UnpackLog(event, "RegistryUpdated", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseRegistryUpdated is a log parse operation binding the contract event 0x482b97c53e48ffa324a976e2738053e9aff6eee04d8aac63b10e19411d869b82. +// +// Solidity: event RegistryUpdated(address indexed oldRegistry, address indexed newRegistry) +func (_NodeID *NodeIDFilterer) ParseRegistryUpdated(log types.Log) (*NodeIDRegistryUpdated, error) { + event := new(NodeIDRegistryUpdated) + if err := _NodeID.contract.UnpackLog(event, "RegistryUpdated", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// NodeIDSlotsMintedIterator is returned from FilterSlotsMinted and is used to iterate over the raw logs and unpacked data for SlotsMinted events raised by the NodeID contract. +type NodeIDSlotsMintedIterator struct { + Event *NodeIDSlotsMinted // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *NodeIDSlotsMintedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(NodeIDSlotsMinted) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(NodeIDSlotsMinted) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *NodeIDSlotsMintedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *NodeIDSlotsMintedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// NodeIDSlotsMinted represents a SlotsMinted event raised by the NodeID contract. +type NodeIDSlotsMinted struct { + By common.Address + FirstTokenId uint32 + LastTokenId uint32 + MinActivationAmount *big.Int + VestingPeriod uint64 + Raw types.Log // Blockchain specific contextual infos +} + +// FilterSlotsMinted is a free log retrieval operation binding the contract event 0x9902251bf2894876d6d1dc26c5e2005e75018334706538cb7bf283598aefc42b. +// +// Solidity: event SlotsMinted(address indexed by, uint32 indexed firstTokenId, uint32 indexed lastTokenId, uint256 minActivationAmount, uint64 vestingPeriod) +func (_NodeID *NodeIDFilterer) FilterSlotsMinted(opts *bind.FilterOpts, by []common.Address, firstTokenId []uint32, lastTokenId []uint32) (*NodeIDSlotsMintedIterator, error) { + + var byRule []interface{} + for _, byItem := range by { + byRule = append(byRule, byItem) + } + var firstTokenIdRule []interface{} + for _, firstTokenIdItem := range firstTokenId { + firstTokenIdRule = append(firstTokenIdRule, firstTokenIdItem) + } + var lastTokenIdRule []interface{} + for _, lastTokenIdItem := range lastTokenId { + lastTokenIdRule = append(lastTokenIdRule, lastTokenIdItem) + } + + logs, sub, err := _NodeID.contract.FilterLogs(opts, "SlotsMinted", byRule, firstTokenIdRule, lastTokenIdRule) + if err != nil { + return nil, err + } + return &NodeIDSlotsMintedIterator{contract: _NodeID.contract, event: "SlotsMinted", logs: logs, sub: sub}, nil +} + +// WatchSlotsMinted is a free log subscription operation binding the contract event 0x9902251bf2894876d6d1dc26c5e2005e75018334706538cb7bf283598aefc42b. +// +// Solidity: event SlotsMinted(address indexed by, uint32 indexed firstTokenId, uint32 indexed lastTokenId, uint256 minActivationAmount, uint64 vestingPeriod) +func (_NodeID *NodeIDFilterer) WatchSlotsMinted(opts *bind.WatchOpts, sink chan<- *NodeIDSlotsMinted, by []common.Address, firstTokenId []uint32, lastTokenId []uint32) (event.Subscription, error) { + + var byRule []interface{} + for _, byItem := range by { + byRule = append(byRule, byItem) + } + var firstTokenIdRule []interface{} + for _, firstTokenIdItem := range firstTokenId { + firstTokenIdRule = append(firstTokenIdRule, firstTokenIdItem) + } + var lastTokenIdRule []interface{} + for _, lastTokenIdItem := range lastTokenId { + lastTokenIdRule = append(lastTokenIdRule, lastTokenIdItem) + } + + logs, sub, err := _NodeID.contract.WatchLogs(opts, "SlotsMinted", byRule, firstTokenIdRule, lastTokenIdRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(NodeIDSlotsMinted) + if err := _NodeID.contract.UnpackLog(event, "SlotsMinted", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseSlotsMinted is a log parse operation binding the contract event 0x9902251bf2894876d6d1dc26c5e2005e75018334706538cb7bf283598aefc42b. +// +// Solidity: event SlotsMinted(address indexed by, uint32 indexed firstTokenId, uint32 indexed lastTokenId, uint256 minActivationAmount, uint64 vestingPeriod) +func (_NodeID *NodeIDFilterer) ParseSlotsMinted(log types.Log) (*NodeIDSlotsMinted, error) { + event := new(NodeIDSlotsMinted) + if err := _NodeID.contract.UnpackLog(event, "SlotsMinted", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// NodeIDTransferIterator is returned from FilterTransfer and is used to iterate over the raw logs and unpacked data for Transfer events raised by the NodeID contract. +type NodeIDTransferIterator struct { + Event *NodeIDTransfer // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *NodeIDTransferIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(NodeIDTransfer) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(NodeIDTransfer) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *NodeIDTransferIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *NodeIDTransferIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// NodeIDTransfer represents a Transfer event raised by the NodeID contract. +type NodeIDTransfer struct { + From common.Address + To common.Address + TokenId *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterTransfer is a free log retrieval operation binding the contract event 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef. +// +// Solidity: event Transfer(address indexed from, address indexed to, uint256 indexed tokenId) +func (_NodeID *NodeIDFilterer) FilterTransfer(opts *bind.FilterOpts, from []common.Address, to []common.Address, tokenId []*big.Int) (*NodeIDTransferIterator, error) { + + var fromRule []interface{} + for _, fromItem := range from { + fromRule = append(fromRule, fromItem) + } + var toRule []interface{} + for _, toItem := range to { + toRule = append(toRule, toItem) + } + var tokenIdRule []interface{} + for _, tokenIdItem := range tokenId { + tokenIdRule = append(tokenIdRule, tokenIdItem) + } + + logs, sub, err := _NodeID.contract.FilterLogs(opts, "Transfer", fromRule, toRule, tokenIdRule) + if err != nil { + return nil, err + } + return &NodeIDTransferIterator{contract: _NodeID.contract, event: "Transfer", logs: logs, sub: sub}, nil +} + +// WatchTransfer is a free log subscription operation binding the contract event 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef. +// +// Solidity: event Transfer(address indexed from, address indexed to, uint256 indexed tokenId) +func (_NodeID *NodeIDFilterer) WatchTransfer(opts *bind.WatchOpts, sink chan<- *NodeIDTransfer, from []common.Address, to []common.Address, tokenId []*big.Int) (event.Subscription, error) { + + var fromRule []interface{} + for _, fromItem := range from { + fromRule = append(fromRule, fromItem) + } + var toRule []interface{} + for _, toItem := range to { + toRule = append(toRule, toItem) + } + var tokenIdRule []interface{} + for _, tokenIdItem := range tokenId { + tokenIdRule = append(tokenIdRule, tokenIdItem) + } + + logs, sub, err := _NodeID.contract.WatchLogs(opts, "Transfer", fromRule, toRule, tokenIdRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(NodeIDTransfer) + if err := _NodeID.contract.UnpackLog(event, "Transfer", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseTransfer is a log parse operation binding the contract event 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef. +// +// Solidity: event Transfer(address indexed from, address indexed to, uint256 indexed tokenId) +func (_NodeID *NodeIDFilterer) ParseTransfer(log types.Log) (*NodeIDTransfer, error) { + event := new(NodeIDTransfer) + if err := _NodeID.contract.UnpackLog(event, "Transfer", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} diff --git a/pkg/blockchain/evm/registry_abi.go b/pkg/blockchain/evm/registry_abi.go new file mode 100644 index 0000000..77dc1b5 --- /dev/null +++ b/pkg/blockchain/evm/registry_abi.go @@ -0,0 +1,2084 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package evm + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// NodeRecord is an auto generated low-level Go binding around an user-defined struct. +type NodeRecord struct { + Operator common.Address + ActivatedAt uint64 + DeactivatedAt uint64 + VestedAt uint64 + Index uint32 + TokenId uint32 + OperatorCollateral *big.Int + SponsorCollateral *big.Int + BlsPubkeyG1 [2]*big.Int + BlsPubkeyG2 [4]*big.Int +} + +// RegistryMetaData contains all meta data concerning the Registry contract. +var RegistryMetaData = &bind.MetaData{ + ABI: "[{\"type\":\"constructor\",\"inputs\":[{\"name\":\"nodeID\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"asset\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"networkId\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"unbondingPeriod\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"basePrice\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"targetPrice\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"owner_\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"ASSET\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"contractIERC20\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"BASE_PRICE\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"MAX_NODES\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"NETWORK_ID\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"NODE_ID\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"TARGET_PRICE\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"UNBONDING_PERIOD\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint64\",\"internalType\":\"uint64\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"WARMUP_WINDOW\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint64\",\"internalType\":\"uint64\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"activate\",\"inputs\":[{\"name\":\"tokenId\",\"type\":\"uint32\",\"internalType\":\"uint32\"},{\"name\":\"blsPubkeyG1\",\"type\":\"uint256[2]\",\"internalType\":\"uint256[2]\"},{\"name\":\"blsPubkeyG2\",\"type\":\"uint256[4]\",\"internalType\":\"uint256[4]\"},{\"name\":\"operatorCollateral\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[{\"name\":\"nodeId\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"activeCount\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint32\",\"internalType\":\"uint32\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"floorPrice\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"fund\",\"inputs\":[{\"name\":\"tokenId\",\"type\":\"uint32\",\"internalType\":\"uint32\"},{\"name\":\"amount\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"getNodeByBlsG2Hash\",\"inputs\":[{\"name\":\"blsG2Hash\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"getNodeById\",\"inputs\":[{\"name\":\"nodeId\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"outputs\":[{\"name\":\"\",\"type\":\"tuple\",\"internalType\":\"structNodeRecord\",\"components\":[{\"name\":\"operator\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"activatedAt\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"deactivatedAt\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"vestedAt\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"index\",\"type\":\"uint32\",\"internalType\":\"uint32\"},{\"name\":\"tokenId\",\"type\":\"uint32\",\"internalType\":\"uint32\"},{\"name\":\"operatorCollateral\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"sponsorCollateral\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"blsPubkeyG1\",\"type\":\"uint256[2]\",\"internalType\":\"uint256[2]\"},{\"name\":\"blsPubkeyG2\",\"type\":\"uint256[4]\",\"internalType\":\"uint256[4]\"}]}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"getNodeId\",\"inputs\":[{\"name\":\"tokenId\",\"type\":\"uint32\",\"internalType\":\"uint32\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"getNodeIds\",\"inputs\":[{\"name\":\"offset\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"limit\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bytes32[]\",\"internalType\":\"bytes32[]\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"getNodes\",\"inputs\":[{\"name\":\"offset\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"limit\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[{\"name\":\"\",\"type\":\"tuple[]\",\"internalType\":\"structNodeRecord[]\",\"components\":[{\"name\":\"operator\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"activatedAt\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"deactivatedAt\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"vestedAt\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"index\",\"type\":\"uint32\",\"internalType\":\"uint32\"},{\"name\":\"tokenId\",\"type\":\"uint32\",\"internalType\":\"uint32\"},{\"name\":\"operatorCollateral\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"sponsorCollateral\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"blsPubkeyG1\",\"type\":\"uint256[2]\",\"internalType\":\"uint256[2]\"},{\"name\":\"blsPubkeyG2\",\"type\":\"uint256[4]\",\"internalType\":\"uint256[4]\"}]}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"liability\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"owner\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"register\",\"inputs\":[{\"name\":\"blsPubkeyG1\",\"type\":\"uint256[2]\",\"internalType\":\"uint256[2]\"},{\"name\":\"blsPubkeyG2\",\"type\":\"uint256[4]\",\"internalType\":\"uint256[4]\"},{\"name\":\"operatorCollateral\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[{\"name\":\"tokenId\",\"type\":\"uint32\",\"internalType\":\"uint32\"},{\"name\":\"nodeId\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"release\",\"inputs\":[{\"name\":\"tokenId\",\"type\":\"uint32\",\"internalType\":\"uint32\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"setSlasher\",\"inputs\":[{\"name\":\"newSlasher\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"slash\",\"inputs\":[{\"name\":\"nodeId\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"amount\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"recipient\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"slasher\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"totalNodes\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"transferOwnership\",\"inputs\":[{\"name\":\"newOwner\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"unlock\",\"inputs\":[{\"name\":\"tokenId\",\"type\":\"uint32\",\"internalType\":\"uint32\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"event\",\"name\":\"NodeActivated\",\"inputs\":[{\"name\":\"operator\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"nodeId\",\"type\":\"bytes32\",\"indexed\":true,\"internalType\":\"bytes32\"},{\"name\":\"tokenId\",\"type\":\"uint32\",\"indexed\":true,\"internalType\":\"uint32\"},{\"name\":\"collateral\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"},{\"name\":\"vestedAt\",\"type\":\"uint64\",\"indexed\":false,\"internalType\":\"uint64\"},{\"name\":\"blsPubkeyG2\",\"type\":\"uint256[4]\",\"indexed\":false,\"internalType\":\"uint256[4]\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"NodeFunded\",\"inputs\":[{\"name\":\"payer\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"nodeId\",\"type\":\"bytes32\",\"indexed\":true,\"internalType\":\"bytes32\"},{\"name\":\"tokenId\",\"type\":\"uint32\",\"indexed\":true,\"internalType\":\"uint32\"},{\"name\":\"amount\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"},{\"name\":\"totalCollateral\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"NodeReleased\",\"inputs\":[{\"name\":\"operator\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"nodeId\",\"type\":\"bytes32\",\"indexed\":true,\"internalType\":\"bytes32\"},{\"name\":\"tokenId\",\"type\":\"uint32\",\"indexed\":true,\"internalType\":\"uint32\"},{\"name\":\"collateral\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"NodeUnlocked\",\"inputs\":[{\"name\":\"operator\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"nodeId\",\"type\":\"bytes32\",\"indexed\":true,\"internalType\":\"bytes32\"},{\"name\":\"availableAt\",\"type\":\"uint64\",\"indexed\":false,\"internalType\":\"uint64\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"OwnershipTransferred\",\"inputs\":[{\"name\":\"oldOwner\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"newOwner\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"Slashed\",\"inputs\":[{\"name\":\"nodeId\",\"type\":\"bytes32\",\"indexed\":true,\"internalType\":\"bytes32\"},{\"name\":\"amount\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"},{\"name\":\"recipient\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"fromOperator\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"},{\"name\":\"fromSponsor\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"SlasherUpdated\",\"inputs\":[{\"name\":\"oldSlasher\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"newSlasher\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"}],\"anonymous\":false},{\"type\":\"error\",\"name\":\"ActivationBelowFloor\",\"inputs\":[{\"name\":\"floor\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"provided\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]},{\"type\":\"error\",\"name\":\"ActivationBelowMinimum\",\"inputs\":[{\"name\":\"minimum\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"provided\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]},{\"type\":\"error\",\"name\":\"BlsKeyAlreadyRegistered\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"BlsKeyMismatch\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"InsufficientCollateral\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"InvalidPriceRange\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"NodeNotActive\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"NotNftOwner\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"NotOwner\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"NotSlasher\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ReentrancyGuardReentrantCall\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ReleaseNotAvailable\",\"inputs\":[{\"name\":\"availableAt\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]},{\"type\":\"error\",\"name\":\"SafeERC20FailedOperation\",\"inputs\":[{\"name\":\"token\",\"type\":\"address\",\"internalType\":\"address\"}]},{\"type\":\"error\",\"name\":\"SlotNotInactive\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ZeroAddress\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ZeroAmount\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ZeroBlsPubkey\",\"inputs\":[]}]", + Bin: "0x6101403461025657601f612fc538819003918201601f19168301916001600160401b0383118484101761025a5780849260e094604052833981010312610256576100488161026e565b906100556020820161026e565b604082015160608301516001600160401b038116949091908583036102565760808501519361008b60c060a0880151970161026e565b60017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055916001600160a01b0316908115610247576001600160a01b0316918215610247576001600160a01b031696871561024757156101f157841515806101e7575b156101d85760a05260c05260e05260805261010052610120525f80546001600160a01b031916919091179055604051612d42908161028382396080518181816108b501528181610f330152611013015260a051818181610a8001528181610d67015281816110e9015281816112430152818161211801526124bf015260c05181818161047b01528181610650015281816107ea015281816111c0015281816122d2015261236a015260e05181818161059f0152818161206f01526124470152610100518181816113100152611b070152610120518181816109ae01528181611b290152611b7e0152f35b6323f5f0b960e11b5f5260045ffd5b50848610156100ee565b60405162461bcd60e51b815260206004820152602860248201527f52656769737472793a20756e626f6e64696e67506572696f642063616e6e6f74604482015267206265207a65726f60c01b6064820152608490fd5b63d92e233d60e01b5f5260045ffd5b5f80fd5b634e487b7160e01b5f52604160045260245ffd5b51906001600160a01b03821682036102565756fe60806040526004361015610011575f80fd5b5f3560e01c8063038d67e8146101c45780630d420090146101bf578063158ece27146101ba57806318071936146101b557806328ce8b31146101b05780632db75d40146101ab5780634331ed1f146101a65780634800d97f146101a157806367d93c811461019c5780636ef67bae14610197578063705727b5146101925780637e99ce591461018d5780637fdd1867146101885780638899cf50146101835780638da5cb5b1461017e5780638f16e1cd146101795780639363c812146101745780639592d4241461016f578063aabc24961461016a578063ad12211114610165578063b134427114610160578063bdc43e921461015b578063d9a912ec14610156578063dda42b3714610151578063ef695be81461014c578063f2fde38b146101475763f86325ed14610142575f80fd5b6112f9565b611272565b61122e565b610f57565b610f14565b610eb1565b610e89565b610d07565b610c7f565b610c62565b610c23565b610c06565b610bdf565b610b9d565b610a05565b610997565b61097a565b610941565b610819565b6107d5565b6107af565b6105d0565b610588565b61055e565b610368565b61033b565b6102be565b905f905b600282106101da57505050565b60208060019285518152019301910190916101cd565b905f905b6004821061020157505050565b60208060019285518152019301910190916101f4565b80516001600160a01b031682526102bc919061014090610120906020818101516001600160401b0316908501526040818101516001600160401b0316908501526060818101516001600160401b03169085015260808181015163ffffffff169085015260a08181015163ffffffff169085015260c081015160c085015260e081015160e08501526102b26101008201516101008601906101c9565b01519101906101f0565b565b3461032d57604036600319011261032d576102dd60243560043561170d565b6040518091602082016020835281518091526020604084019201905f5b818110610308575050500390f35b9193509160206101c08261031f6001948851610217565b0194019101918493926102fa565b5f80fd5b5f91031261032d57565b3461032d575f36600319011261032d576020604051610e108152f35b6001600160a01b0381160361032d57565b3461032d57606036600319011261032d576024356004357f9a2bb3d9059142feaf2a6cbf5062a0047437076519c62ede693c2ec2240f13336044356103ac81610357565b6103b4611ac2565b6001546103cb906001600160a01b031633146117be565b6103d68415156117d4565b6103e8835f52600360205260405f2090565b60028101918254926003830192610413610403855487611544565b8015159081610553575b506117ea565b5f9185891161052c576104c59394955088956104308a8354611551565b82555b6104476104428b600254611551565b600255565b6001600160401b0361046360018501546001600160401b031690565b161591826104ef575b50506104e0575b5061049f87847f0000000000000000000000000000000000000000000000000000000000000000611bf6565b60405193849360018060a01b031697846040919493926060820195825260208201520152565b0390a36104de60015f516020612d225f395f51905f5255565b005b6104e990611ba0565b5f610473565b6104fd925054905490611544565b61052461051f61051660015463ffffffff9060a01c1690565b63ffffffff1690565b611afa565b115f8061046c565b6104c59394925061053d868a611551565b925f825561054c848254611551565b8155610433565b90508911155f61040d565b3461032d57602036600319011261032d576004355f526005602052602060405f2054604051908152f35b3461032d575f36600319011261032d5760206040517f00000000000000000000000000000000000000000000000000000000000000008152f35b63ffffffff81160361032d57565b3461032d57604036600319011261032d576004356105ed816105c2565b6024356105f8611ac2565b6106038115156117d4565b61061b8263ffffffff165f52600660205260405f2090565b549061062f825f52600360205260405f2090565b805460a01c6001600160401b0316151580610780575b61064e90611800565b7f000000000000000000000000000000000000000000000000000000000000000061067b83303384611c73565b600282019061068b848354611544565b825561069c61044285600254611544565b6040516370a0823160e01b815230600482015290602090829060249082906001600160a01b03165afa92831561077b57600363ffffffff9361070f7f12341d30af78a74af3697daeaf7b1662bc9b723f6aa8a3402bf3d8b3f87a077296610719955f9161074c575b5060025411156117ea565b5491015490611544565b604080519485526020850191909152941693339290819081015b0390a46104de60015f516020612d225f395f51905f5255565b61076e915060203d602011610774575b6107668183611367565b810190611816565b5f610704565b503d61075c565b611825565b5061064e6107a761079b60018401546001600160401b031690565b6001600160401b031690565b159050610645565b3461032d575f36600319011261032d57602063ffffffff60015460a01c16604051908152f35b3461032d575f36600319011261032d576040517f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03168152602090f35b3461032d57602036600319011261032d5763ffffffff60043561083b816105c2565b165f52600660205260405f20546108b061085d825f52600360205260405f2090565b80546001600160401b03906108999061088a6001600160a01b0382165b6001600160a01b03163314611830565b60a01c6001600160401b031690565b1615158061091e575b6108ab90611800565b611ba0565b6108e37f00000000000000000000000000000000000000000000000000000000000000006001600160401b034216611846565b6040516001600160401b0391909116815233907f0c833c7c9f5b9b8ed0085d7959eb025f59fa32a55b8d55223a819bc7c58db34590602090a3005b506108ab61093961079b60018401546001600160401b031690565b1590506108a2565b3461032d57602036600319011261032d5763ffffffff600435610963816105c2565b165f526006602052602060405f2054604051908152f35b3461032d575f36600319011261032d576020600254604051908152f35b3461032d575f36600319011261032d5760206040517f00000000000000000000000000000000000000000000000000000000000000008152f35b9060049160441161032d57565b9060249160641161032d57565b9060449160c41161032d57565b9060649160e41161032d57565b3461032d5760e036600319011261032d57610a1f366109d1565b610a28366109eb565b60c435610a33611ac2565b610a5461051f610a4f61051660015463ffffffff9060a01c1690565b6114fe565b90610a63818380821015611866565b60405163e8804a2b60e01b8152600481018390525f6024820152937f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03169390602086806044810103815f895af195861561077b575f96610b6c575b50604051638eeda10360e01b815263ffffffff8716600482015294606090869060249082905afa801561077b57610b06955f91610b3d575b503387611feb565b90610b1d60015f516020612d225f395f51905f5255565b6040805163ffffffff9092168252602082019290925290819081015b0390f35b610b5f915060603d606011610b65575b610b578183611367565b8101906118ad565b5f610afe565b503d610b4d565b610b8f91965060203d602011610b96575b610b878183611367565b810190611884565b945f610ac6565b503d610b7d565b3461032d57602036600319011261032d57600435610bb96113e2565b505f5260036020526101c0610bd060405f20611628565b610bdd6040518092610217565bf35b3461032d575f36600319011261032d575f546040516001600160a01b039091168152602090f35b3461032d575f36600319011261032d576020604051620100008152f35b3461032d575f36600319011261032d5763ffffffff60015460a01c1660018101809111610c5d57610c55602091611afa565b604051908152f35b6114ea565b3461032d575f36600319011261032d576020600454604051908152f35b3461032d57602036600319011261032d57600435610c9c81610357565b610cb060018060a01b035f541633146118fe565b6001600160a01b0316610cc4811515611914565b600180546001600160a01b0319811683179091556001600160a01b03167fe0d49a54274423183dadecbdf239eaac6e06ba88320b26fe8cc5ec9d050a63955f80a3005b3461032d5761010036600319011261032d57600435610d25816105c2565b610d2e366109de565b90610d38366109f8565b9060e435610d44611ac2565b6040516331a9108f60e11b815263ffffffff831660048201526001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169390602081602481885afa801561077b57610db4915f91610e5a575b506001600160a01b03163314611830565b604051638eeda10360e01b815263ffffffff8416600482015293606090859060249082905afa94851561077b57610b3995610e15955f91610e3b575b50610e0d61051f610a4f61051660015463ffffffff9060a01c1690565b9433906123ef565b610e2b60015f516020612d225f395f51905f5255565b6040519081529081906020820190565b610e54915060603d606011610b6557610b578183611367565b5f610df0565b610e7c915060203d602011610e82575b610e748183611367565b81019061192a565b5f610da3565b503d610e6a565b3461032d575f36600319011261032d576001546040516001600160a01b039091168152602090f35b3461032d57604036600319011261032d57610ed060243560043561198c565b6040518091602082016020835281518091526020604084019201905f5b818110610efb575050500390f35b8251845285945060209384019390920191600101610eed565b3461032d575f36600319011261032d5760206040516001600160401b037f0000000000000000000000000000000000000000000000000000000000000000168152f35b3461032d57602036600319011261032d57600435610f74816105c2565b610f7c611ac2565b610f948163ffffffff165f52600660205260405f2090565b54610faf610faa825f52600360205260405f2090565b611628565b8051909290610fc6906001600160a01b031661087a565b6001600160401b03610fe260208501516001600160401b031690565b1615158061120a575b610ff490611800565b61104161103861079b61101160408701516001600160401b031690565b7f000000000000000000000000000000000000000000000000000000000000000090611846565b80421015611a2e565b60c0830192611056845160e083015190611544565b9361106e61079b60608401516001600160401b031690565b42106112045750835b61108661044286600254611551565b6110a061109a608084015163ffffffff1690565b856125a2565b6110a9826126bb565b6110c36110be855f52600360205260405f2090565b611a74565b5f6110dc8463ffffffff165f52600660205260405f2090565b5581516001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000811693911692803b1561032d576040516323b872dd60e01b81523060048201526001600160a01b0394909416602485015263ffffffff851660448501525f908490606490829084905af191821561077b577f4f72a5ea49c0470a55beb3953816abf5c92fc73003b1049c241b133a0863208c9363ffffffff936111ea575b50806111ae575b50516040519586529216936001600160a01b03909216918060208101610733565b81516111e491906001600160a01b03167f0000000000000000000000000000000000000000000000000000000000000000611bf6565b5f61118d565b806111f85f6111fe93611367565b80610331565b5f611186565b51611077565b50610ff461122561079b60408601516001600160401b031690565b15159050610feb565b3461032d575f36600319011261032d576040517f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03168152602090f35b3461032d57602036600319011261032d5760043561128f81610357565b5f54906001600160a01b038216906112a83383146118fe565b6001600160a01b03169182906112bf821515611914565b6bffffffffffffffffffffffff60a01b16175f557f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e05f80a3005b3461032d575f36600319011261032d5760206040517f00000000000000000000000000000000000000000000000000000000000000008152f35b634e487b7160e01b5f52604160045260245ffd5b604081019081106001600160401b0382111761136257604052565b611333565b90601f801991011681019081106001600160401b0382111761136257604052565b604051906102bc61014083611367565b604051906102bc604083611367565b906102bc6040519283611367565b6001600160401b0381116113625760051b60200190565b604051906113db602083611367565b6020368337565b6040519061014082018281106001600160401b0382111761136257604052815f81525f60208201525f60408201525f60608201525f60808201525f60a08201525f60c08201525f60e082015260405161143c604082611367565b604036823761010082015261012060405191611459608084611367565b60803684370152565b60405190611471602083611367565b5f80835282815b82811061148457505050565b60209061148f6113e2565b82828501015201611478565b906114a5826113b5565b6114b26040519182611367565b82815280926114c3601f19916113b5565b01905f5b8281106114d357505050565b6020906114de6113e2565b828285010152016114c7565b634e487b7160e01b5f52601160045260245ffd5b9060018201809211610c5d57565b9060028201809211610c5d57565b9060038201809211610c5d57565b9060048201809211610c5d57565b9060058201809211610c5d57565b91908201809211610c5d57565b91908203918211610c5d57565b634e487b7160e01b5f52603260045260245ffd5b60045481101561158a5760045f5260205f2001905f90565b61155e565b80511561158a5760200190565b80516001101561158a5760400190565b805182101561158a5760209160051b010190565b60405191905f835b600282106115de575050506102bc604083611367565b60016020819285548152019301910190916115c8565b60405191905f835b60048210611612575050506102bc608083611367565b60016020819285548152019301910190916115fc565b906117056006611636611388565b84546001600160a01b0381168252909490611664906116549061088a565b6001600160401b03166020870152565b6116d96116cc6001830154611692611682826001600160401b031690565b6001600160401b031660408a0152565b6001600160401b03604082901c1660608901526116c0608082901c63ffffffff1663ffffffff1660808a0152565b60a01c63ffffffff1690565b63ffffffff1660a0870152565b600281015460c0860152600381015460e08601526116f9600482016115c0565b610100860152016115f4565b610120830152565b9060045490818310156117b057820190818311610c5d578082116117a8575b50818103818111610c5d576117409061149b565b91805b8281106117505750505090565b806117a161177d61176f611765600195611572565b90549060031b1c90565b5f52600360205260405f2090565b61179061178a8685611551565b91611628565b61179a82896115ac565b52866115ac565b5001611743565b90505f61172c565b5050506117bb611462565b90565b156117c557565b63dabc4ad960e01b5f5260045ffd5b156117db57565b631f2a200560e01b5f5260045ffd5b156117f157565b633a23d82560e01b5f5260045ffd5b1561180757565b6310e8397760e31b5f5260045ffd5b9081602091031261032d575190565b6040513d5f823e3d90fd5b1561183757565b634b64ae4760e01b5f5260045ffd5b906001600160401b03809116911601906001600160401b038211610c5d57565b1561186f575050565b630e9fc46d60e11b5f5260045260245260445ffd5b9081602091031261032d57516117bb816105c2565b51906001600160401b038216820361032d57565b9081606091031261032d576040519060608201908282106001600160401b038311176113625760409182526118e181611899565b83526118ef60208201611899565b60208401520151604082015290565b1561190557565b6330cd747160e01b5f5260045ffd5b1561191b57565b63d92e233d60e01b5f5260045ffd5b9081602091031261032d57516117bb81610357565b6040519061194e602083611367565b5f808352366020840137565b90611964826113b5565b6119716040519182611367565b8281528092611982601f19916113b5565b0190602036910137565b6004549182821015611a2357810190818111610c5d57828211611a1b575b808203828111610c5d576119bd9061195a565b92815b8381106119ce575050505090565b8181101561158a5760019060045f52807f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b0154611a14611a0e8684611551565b886115ac565b52016119c0565b8291506119aa565b5050506117bb61193f565b15611a365750565b633bc882ad60e11b5f5260045260245ffd5b90600682029180830460061490151715610c5d57565b908160051b9180830460201490151715610c5d57565b5f81555f60018201555f60028201555f60038201555f5b60028110611ab257506006015f5b60048110611aa5575050565b5f82820155600101611a99565b5f82820160040155600101611a8b565b60025f516020612d225f395f51905f525414611aeb5760025f516020612d225f395f51905f5255565b633ee5aeb560e01b5f5260045ffd5b62010000811015611b7b577f0000000000000000000000000000000000000000000000000000000000000000907f000000000000000000000000000000000000000000000000000000000000000082810391818311610c5d57611b5d84916126d8565b8084029384041491141715610c5d5760081c8101809111610c5d5790565b507f000000000000000000000000000000000000000000000000000000000000000090565b600101805467ffffffffffffffff1916426001600160401b031617905563ffffffff60015460a01c168015610c5d576001805463ffffffff60a01b19165f1990920160a01b63ffffffff60a01b16919091179055565b916040519163a9059cbb60e01b5f5260018060a01b031660045260245260205f60448180865af160015f5114811615611c54575b604091909152155b611c395750565b635274afe760e01b5f526001600160a01b031660045260245ffd5b6001811516611c6a573d15833b15151616611c2a565b503d5f823e3d90fd5b6040516323b872dd60e01b5f9081526001600160a01b039384166004529290931660245260449390935260209060648180865af160015f5114811615611cc4575b6040919091525f60605215611c32565b6001811516611c6a573d15833b15151616611cb4565b15611ce157565b633b093fe160e21b5f5260045ffd5b15611cf9575050565b636af5d51b60e01b5f5260045260245260445ffd5b6001600160401b03166001600160401b038114610c5d5760010190565b919060405192611d3c604085611367565b83906040810192831161032d57905b828210611d5757505050565b8135815260209182019101611d4b565b919060405192611d78608085611367565b83906080810192831161032d57905b828210611d9357505050565b8135815260209182019101611d87565b905f5b60028110611db357505050565b600190602083519301928185015501611da6565b905f5b60048110611dd757505050565b600190602083519301928185015501611dca565b815181546001600160a01b0319166001600160a01b039091161781556102bc9160069061012090611e51611e2960208301516001600160401b031690565b855467ffffffffffffffff60a01b191660a09190911b67ffffffffffffffff60a01b16178555565b611f3460018501611e8c611e6f60408501516001600160401b031690565b825467ffffffffffffffff19166001600160401b03909116178255565b611ed5611ea360608501516001600160401b031690565b82546fffffffffffffffff0000000000000000191660409190911b6fffffffffffffffff000000000000000016178255565b611f09611ee9608085015163ffffffff1690565b825463ffffffff60801b191660809190911b63ffffffff60801b16178255565b60a083015163ffffffff16815463ffffffff60a01b191660a09190911b63ffffffff60a01b16179055565b60c0810151600285015560e08101516003850155611f5a61010082015160048601611da3565b01519101611dc7565b60045468010000000000000000811015611362576001810160045560045481101561158a5760045f527f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b0155565b63ffffffff1663ffffffff8114610c5d5760010190565b6040906001600160401b036080949695939660c083019783521660208201520137565b969591929694909461201561200e8263ffffffff165f52600660205260405f2090565b5415611cda565b61202782604086015180821015611cf0565b5f928083106123b2575b5060015460c01c61206961204482611d0e565b600180546001600160c01b031660c09290921b6001600160c01b031916919091179055565b604080517f00000000000000000000000000000000000000000000000000000000000000006020820190815263ffffffff8516928201929092526001600160401b0390921660608301524460808301526001600160a01b03881660a0830152906120e08160c081015b03601f198101835282611367565b519020976120ef86828b612775565b6040516331a9108f60e11b815263ffffffff831660048201526020816024816001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000165afa801561077b5761224b9261216561221e928b945f91612393575b506001600160a01b03163014611830565b85612362575b8b6121848663ffffffff165f52600660205260405f2090565b556121ff6121af6121a960206001600160401b0342169b01516001600160401b031690565b8a611846565b986121dd6121c260045463ffffffff1690565b916116546121ce611388565b6001600160a01b039098168852565b5f60408601526001600160401b038a16606086015263ffffffff166080850152565b63ffffffff851660a08401528560c08401528660e08401523690611d2b565b61010082015261222e3688611d67565b6101208201526122468a5f52600360205260405f2090565b611deb565b61225488611f63565b61229761227261226d60015463ffffffff9060a01c1690565b611fb1565b6001805463ffffffff60a01b191660a09290921b63ffffffff60a01b16919091179055565b6122af6104426122a78585611544565b600254611544565b6040516370a0823160e01b81523060048201526020816024816001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000165afa95861561077b576123457f58cee7261629c956b111bc684df727bd2e9b0f5d954e24b93908951c431cd13e9563ffffffff956123408d9a61235d965f9161074c575060025411156117ea565b611544565b95604051948594169860018060a01b03169684611fc8565b0390a4565b61238e8630857f0000000000000000000000000000000000000000000000000000000000000000611c73565b61216b565b6123ac915060203d602011610e8257610e748183611367565b5f612154565b6123e8919350806123e38480936001600160401b036123db60208b01516001600160401b031690565b161515611866565b611551565b915f612031565b969591929694909461241261200e8263ffffffff165f52600660205260405f2090565b61242482604086015180821015611cf0565b5f92808310612572575b5060015460c01c61244161204482611d0e565b604080517f00000000000000000000000000000000000000000000000000000000000000006020820190815263ffffffff8516928201929092526001600160401b0390921660608301524460808301526001600160a01b03881660a0830152906124ae8160c081016120d2565b519020976124bd86828b612775565b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316803b1561032d576040516323b872dd60e01b81526001600160a01b038916600482015230602482015263ffffffff84166044820152905f908290606490829084905af1801561077b5761224b92899261221e9261255e575b5085612362578b6121848663ffffffff165f52600660205260405f2090565b806111f85f61256c93611367565b5f61253f565b61259b919350806123e38480936001600160401b036123db60208b01516001600160401b031690565b915f61242e565b906004545f198101818111610c5d5781111561158a5760045f527f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19a810154809303612648575b5050506004548015612634575f1981019060045482101561158a5760045f8181527f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19a9092019190915555565b634e487b7160e01b5f52603160045260245ffd5b81101561158a57600161268f6126b39360045f5280847f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b01555f52600360205260405f2090565b01805463ffffffff60801b191660809290921b63ffffffff60801b16919091179055565b5f80806125e8565b6101206126c991015161284a565b5f5260056020525f6040812055565b90811561272e5760018201808311610c5d5760011c825b8382106126fa575050565b90925082801561271a57808204908101809111610c5d5760011c906126ef565b634e487b7160e01b5f52601260045260245ffd5b5f9150565b1561273a57565b632095a11360e21b5f5260045ffd5b1561275057565b634d82eddb60e01b5f5260045ffd5b1561276657565b637e4c066f60e01b5f5260045ffd5b9161280e6128139161280761280261283d956127b961279682359260200190565b3561279f611398565b928084528160208501521590811591612840575b50612733565b6127c360406113a7565b8435815290602085013560208301526127dc60406113a7565b60408601358152606086013560208201526127f5611398565b928352602083015261291e565b612749565b3690611d67565b61284a565b61282f612828825f52600560205260405f2090565b541561275f565b5f52600560205260405f2090565b55565b905015155f6127b3565b805190602081015190606060408201519101519060405192602084019485526040840152606083015260808201526080815261288760a082611367565b51902090565b6040519061289a82611347565b5f6020838281520152565b604051906128b282611347565b81602060409182516128c48482611367565b8336823781528251926128d78185611367565b3684370152565b604051606091906128ef8382611367565b6002815291601f1901825f5b82811061290757505050565b6020906129126128a5565b828285010152016128fb565b91909161294060405161293081611347565b60018152600260208201526129e3565b9260405190612950606083611367565b6002825260405f5b8181106129cc5750506117bb939461296e6128de565b936129788461158f565b526129828361158f565b5061298b612aa7565b6129948561158f565b5261299e8461158f565b506129a88361159c565b526129b28261159c565b506129bc8361159c565b526129c68261159c565b50612bec565b6020906129d761288d565b82828701015201612958565b6129eb61288d565b5080511580612a9b575b612a82577f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd4760208251920151067f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47037f30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd478111610c5d5760405191612a7883611347565b8252602082015290565b50604051612a8f81611347565b5f81525f602082015290565b506020810151156129f5565b612aaf6128a5565b50604051612abc81611347565b7f198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c281527f1800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed6020820152604051612b1181611347565b7f090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b81527f12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa602082015260405191612a7883611347565b15612b6e57565b60405162461bcd60e51b815260206004820152601460248201527308498a67440d8cadccee8d040dad2e6dac2e8c6d60631b6044820152606490fd5b15612bb157565b60405162461bcd60e51b8152602060048201526013602482015272109314ce881c185a5c9a5b99c819985a5b1959606a1b6044820152606490fd5b612bf98151835114612b67565b8051612c0c612c0782611a48565b611a5e565b91612c1e612c1983611a48565b61195a565b935f5b838110612c505750505050612c4b60206001938193612c3e6113cc565b9485920160085afa612baa565b511490565b80612c5c600192611a48565b612c6682866115ac565b5151612c72828a6115ac565b526020612c7f83876115ac565b510151612c94612c8e836114fe565b8a6115ac565b52612c9f82856115ac565b515151612cae612c8e8361150c565b52612cc4612cbc83866115ac565b515160200190565b51612cd1612c8e8361151a565b526020612cde83866115ac565b51015151612cee612c8e83611528565b52612d1a612d14612d0d6020612d0486896115ac565b51015160200190565b5192611536565b896115ac565b5201612c2156fe9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00", +} + +// RegistryABI is the input ABI used to generate the binding from. +// Deprecated: Use RegistryMetaData.ABI instead. +var RegistryABI = RegistryMetaData.ABI + +// RegistryBin is the compiled bytecode used for deploying new contracts. +// Deprecated: Use RegistryMetaData.Bin instead. +var RegistryBin = RegistryMetaData.Bin + +// DeployRegistry deploys a new Ethereum contract, binding an instance of Registry to it. +func DeployRegistry(auth *bind.TransactOpts, backend bind.ContractBackend, nodeID common.Address, asset common.Address, networkId [32]byte, unbondingPeriod uint64, basePrice *big.Int, targetPrice *big.Int, owner_ common.Address) (common.Address, *types.Transaction, *Registry, error) { + parsed, err := RegistryMetaData.GetAbi() + if err != nil { + return common.Address{}, nil, nil, err + } + if parsed == nil { + return common.Address{}, nil, nil, errors.New("GetABI returned nil") + } + + address, tx, contract, err := bind.DeployContract(auth, *parsed, common.FromHex(RegistryBin), backend, nodeID, asset, networkId, unbondingPeriod, basePrice, targetPrice, owner_) + if err != nil { + return common.Address{}, nil, nil, err + } + return address, tx, &Registry{RegistryCaller: RegistryCaller{contract: contract}, RegistryTransactor: RegistryTransactor{contract: contract}, RegistryFilterer: RegistryFilterer{contract: contract}}, nil +} + +// Registry is an auto generated Go binding around an Ethereum contract. +type Registry struct { + RegistryCaller // Read-only binding to the contract + RegistryTransactor // Write-only binding to the contract + RegistryFilterer // Log filterer for contract events +} + +// RegistryCaller is an auto generated read-only Go binding around an Ethereum contract. +type RegistryCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// RegistryTransactor is an auto generated write-only Go binding around an Ethereum contract. +type RegistryTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// RegistryFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type RegistryFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// RegistrySession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type RegistrySession struct { + Contract *Registry // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// RegistryCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type RegistryCallerSession struct { + Contract *RegistryCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// RegistryTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type RegistryTransactorSession struct { + Contract *RegistryTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// RegistryRaw is an auto generated low-level Go binding around an Ethereum contract. +type RegistryRaw struct { + Contract *Registry // Generic contract binding to access the raw methods on +} + +// RegistryCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type RegistryCallerRaw struct { + Contract *RegistryCaller // Generic read-only contract binding to access the raw methods on +} + +// RegistryTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type RegistryTransactorRaw struct { + Contract *RegistryTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewRegistry creates a new instance of Registry, bound to a specific deployed contract. +func NewRegistry(address common.Address, backend bind.ContractBackend) (*Registry, error) { + contract, err := bindRegistry(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &Registry{RegistryCaller: RegistryCaller{contract: contract}, RegistryTransactor: RegistryTransactor{contract: contract}, RegistryFilterer: RegistryFilterer{contract: contract}}, nil +} + +// NewRegistryCaller creates a new read-only instance of Registry, bound to a specific deployed contract. +func NewRegistryCaller(address common.Address, caller bind.ContractCaller) (*RegistryCaller, error) { + contract, err := bindRegistry(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &RegistryCaller{contract: contract}, nil +} + +// NewRegistryTransactor creates a new write-only instance of Registry, bound to a specific deployed contract. +func NewRegistryTransactor(address common.Address, transactor bind.ContractTransactor) (*RegistryTransactor, error) { + contract, err := bindRegistry(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &RegistryTransactor{contract: contract}, nil +} + +// NewRegistryFilterer creates a new log filterer instance of Registry, bound to a specific deployed contract. +func NewRegistryFilterer(address common.Address, filterer bind.ContractFilterer) (*RegistryFilterer, error) { + contract, err := bindRegistry(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &RegistryFilterer{contract: contract}, nil +} + +// bindRegistry binds a generic wrapper to an already deployed contract. +func bindRegistry(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := RegistryMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Registry *RegistryRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Registry.Contract.RegistryCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Registry *RegistryRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Registry.Contract.RegistryTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Registry *RegistryRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Registry.Contract.RegistryTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Registry *RegistryCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Registry.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Registry *RegistryTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Registry.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Registry *RegistryTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Registry.Contract.contract.Transact(opts, method, params...) +} + +// ASSET is a free data retrieval call binding the contract method 0x4800d97f. +// +// Solidity: function ASSET() view returns(address) +func (_Registry *RegistryCaller) ASSET(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _Registry.contract.Call(opts, &out, "ASSET") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// ASSET is a free data retrieval call binding the contract method 0x4800d97f. +// +// Solidity: function ASSET() view returns(address) +func (_Registry *RegistrySession) ASSET() (common.Address, error) { + return _Registry.Contract.ASSET(&_Registry.CallOpts) +} + +// ASSET is a free data retrieval call binding the contract method 0x4800d97f. +// +// Solidity: function ASSET() view returns(address) +func (_Registry *RegistryCallerSession) ASSET() (common.Address, error) { + return _Registry.Contract.ASSET(&_Registry.CallOpts) +} + +// BASEPRICE is a free data retrieval call binding the contract method 0xf86325ed. +// +// Solidity: function BASE_PRICE() view returns(uint256) +func (_Registry *RegistryCaller) BASEPRICE(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _Registry.contract.Call(opts, &out, "BASE_PRICE") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// BASEPRICE is a free data retrieval call binding the contract method 0xf86325ed. +// +// Solidity: function BASE_PRICE() view returns(uint256) +func (_Registry *RegistrySession) BASEPRICE() (*big.Int, error) { + return _Registry.Contract.BASEPRICE(&_Registry.CallOpts) +} + +// BASEPRICE is a free data retrieval call binding the contract method 0xf86325ed. +// +// Solidity: function BASE_PRICE() view returns(uint256) +func (_Registry *RegistryCallerSession) BASEPRICE() (*big.Int, error) { + return _Registry.Contract.BASEPRICE(&_Registry.CallOpts) +} + +// MAXNODES is a free data retrieval call binding the contract method 0x8f16e1cd. +// +// Solidity: function MAX_NODES() view returns(uint256) +func (_Registry *RegistryCaller) MAXNODES(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _Registry.contract.Call(opts, &out, "MAX_NODES") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// MAXNODES is a free data retrieval call binding the contract method 0x8f16e1cd. +// +// Solidity: function MAX_NODES() view returns(uint256) +func (_Registry *RegistrySession) MAXNODES() (*big.Int, error) { + return _Registry.Contract.MAXNODES(&_Registry.CallOpts) +} + +// MAXNODES is a free data retrieval call binding the contract method 0x8f16e1cd. +// +// Solidity: function MAX_NODES() view returns(uint256) +func (_Registry *RegistryCallerSession) MAXNODES() (*big.Int, error) { + return _Registry.Contract.MAXNODES(&_Registry.CallOpts) +} + +// NETWORKID is a free data retrieval call binding the contract method 0x28ce8b31. +// +// Solidity: function NETWORK_ID() view returns(bytes32) +func (_Registry *RegistryCaller) NETWORKID(opts *bind.CallOpts) ([32]byte, error) { + var out []interface{} + err := _Registry.contract.Call(opts, &out, "NETWORK_ID") + + if err != nil { + return *new([32]byte), err + } + + out0 := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) + + return out0, err + +} + +// NETWORKID is a free data retrieval call binding the contract method 0x28ce8b31. +// +// Solidity: function NETWORK_ID() view returns(bytes32) +func (_Registry *RegistrySession) NETWORKID() ([32]byte, error) { + return _Registry.Contract.NETWORKID(&_Registry.CallOpts) +} + +// NETWORKID is a free data retrieval call binding the contract method 0x28ce8b31. +// +// Solidity: function NETWORK_ID() view returns(bytes32) +func (_Registry *RegistryCallerSession) NETWORKID() ([32]byte, error) { + return _Registry.Contract.NETWORKID(&_Registry.CallOpts) +} + +// NODEID is a free data retrieval call binding the contract method 0xef695be8. +// +// Solidity: function NODE_ID() view returns(address) +func (_Registry *RegistryCaller) NODEID(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _Registry.contract.Call(opts, &out, "NODE_ID") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// NODEID is a free data retrieval call binding the contract method 0xef695be8. +// +// Solidity: function NODE_ID() view returns(address) +func (_Registry *RegistrySession) NODEID() (common.Address, error) { + return _Registry.Contract.NODEID(&_Registry.CallOpts) +} + +// NODEID is a free data retrieval call binding the contract method 0xef695be8. +// +// Solidity: function NODE_ID() view returns(address) +func (_Registry *RegistryCallerSession) NODEID() (common.Address, error) { + return _Registry.Contract.NODEID(&_Registry.CallOpts) +} + +// TARGETPRICE is a free data retrieval call binding the contract method 0x7e99ce59. +// +// Solidity: function TARGET_PRICE() view returns(uint256) +func (_Registry *RegistryCaller) TARGETPRICE(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _Registry.contract.Call(opts, &out, "TARGET_PRICE") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// TARGETPRICE is a free data retrieval call binding the contract method 0x7e99ce59. +// +// Solidity: function TARGET_PRICE() view returns(uint256) +func (_Registry *RegistrySession) TARGETPRICE() (*big.Int, error) { + return _Registry.Contract.TARGETPRICE(&_Registry.CallOpts) +} + +// TARGETPRICE is a free data retrieval call binding the contract method 0x7e99ce59. +// +// Solidity: function TARGET_PRICE() view returns(uint256) +func (_Registry *RegistryCallerSession) TARGETPRICE() (*big.Int, error) { + return _Registry.Contract.TARGETPRICE(&_Registry.CallOpts) +} + +// UNBONDINGPERIOD is a free data retrieval call binding the contract method 0xd9a912ec. +// +// Solidity: function UNBONDING_PERIOD() view returns(uint64) +func (_Registry *RegistryCaller) UNBONDINGPERIOD(opts *bind.CallOpts) (uint64, error) { + var out []interface{} + err := _Registry.contract.Call(opts, &out, "UNBONDING_PERIOD") + + if err != nil { + return *new(uint64), err + } + + out0 := *abi.ConvertType(out[0], new(uint64)).(*uint64) + + return out0, err + +} + +// UNBONDINGPERIOD is a free data retrieval call binding the contract method 0xd9a912ec. +// +// Solidity: function UNBONDING_PERIOD() view returns(uint64) +func (_Registry *RegistrySession) UNBONDINGPERIOD() (uint64, error) { + return _Registry.Contract.UNBONDINGPERIOD(&_Registry.CallOpts) +} + +// UNBONDINGPERIOD is a free data retrieval call binding the contract method 0xd9a912ec. +// +// Solidity: function UNBONDING_PERIOD() view returns(uint64) +func (_Registry *RegistryCallerSession) UNBONDINGPERIOD() (uint64, error) { + return _Registry.Contract.UNBONDINGPERIOD(&_Registry.CallOpts) +} + +// WARMUPWINDOW is a free data retrieval call binding the contract method 0x0d420090. +// +// Solidity: function WARMUP_WINDOW() view returns(uint64) +func (_Registry *RegistryCaller) WARMUPWINDOW(opts *bind.CallOpts) (uint64, error) { + var out []interface{} + err := _Registry.contract.Call(opts, &out, "WARMUP_WINDOW") + + if err != nil { + return *new(uint64), err + } + + out0 := *abi.ConvertType(out[0], new(uint64)).(*uint64) + + return out0, err + +} + +// WARMUPWINDOW is a free data retrieval call binding the contract method 0x0d420090. +// +// Solidity: function WARMUP_WINDOW() view returns(uint64) +func (_Registry *RegistrySession) WARMUPWINDOW() (uint64, error) { + return _Registry.Contract.WARMUPWINDOW(&_Registry.CallOpts) +} + +// WARMUPWINDOW is a free data retrieval call binding the contract method 0x0d420090. +// +// Solidity: function WARMUP_WINDOW() view returns(uint64) +func (_Registry *RegistryCallerSession) WARMUPWINDOW() (uint64, error) { + return _Registry.Contract.WARMUPWINDOW(&_Registry.CallOpts) +} + +// ActiveCount is a free data retrieval call binding the contract method 0x4331ed1f. +// +// Solidity: function activeCount() view returns(uint32) +func (_Registry *RegistryCaller) ActiveCount(opts *bind.CallOpts) (uint32, error) { + var out []interface{} + err := _Registry.contract.Call(opts, &out, "activeCount") + + if err != nil { + return *new(uint32), err + } + + out0 := *abi.ConvertType(out[0], new(uint32)).(*uint32) + + return out0, err + +} + +// ActiveCount is a free data retrieval call binding the contract method 0x4331ed1f. +// +// Solidity: function activeCount() view returns(uint32) +func (_Registry *RegistrySession) ActiveCount() (uint32, error) { + return _Registry.Contract.ActiveCount(&_Registry.CallOpts) +} + +// ActiveCount is a free data retrieval call binding the contract method 0x4331ed1f. +// +// Solidity: function activeCount() view returns(uint32) +func (_Registry *RegistryCallerSession) ActiveCount() (uint32, error) { + return _Registry.Contract.ActiveCount(&_Registry.CallOpts) +} + +// FloorPrice is a free data retrieval call binding the contract method 0x9363c812. +// +// Solidity: function floorPrice() view returns(uint256) +func (_Registry *RegistryCaller) FloorPrice(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _Registry.contract.Call(opts, &out, "floorPrice") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// FloorPrice is a free data retrieval call binding the contract method 0x9363c812. +// +// Solidity: function floorPrice() view returns(uint256) +func (_Registry *RegistrySession) FloorPrice() (*big.Int, error) { + return _Registry.Contract.FloorPrice(&_Registry.CallOpts) +} + +// FloorPrice is a free data retrieval call binding the contract method 0x9363c812. +// +// Solidity: function floorPrice() view returns(uint256) +func (_Registry *RegistryCallerSession) FloorPrice() (*big.Int, error) { + return _Registry.Contract.FloorPrice(&_Registry.CallOpts) +} + +// GetNodeByBlsG2Hash is a free data retrieval call binding the contract method 0x18071936. +// +// Solidity: function getNodeByBlsG2Hash(bytes32 blsG2Hash) view returns(bytes32) +func (_Registry *RegistryCaller) GetNodeByBlsG2Hash(opts *bind.CallOpts, blsG2Hash [32]byte) ([32]byte, error) { + var out []interface{} + err := _Registry.contract.Call(opts, &out, "getNodeByBlsG2Hash", blsG2Hash) + + if err != nil { + return *new([32]byte), err + } + + out0 := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) + + return out0, err + +} + +// GetNodeByBlsG2Hash is a free data retrieval call binding the contract method 0x18071936. +// +// Solidity: function getNodeByBlsG2Hash(bytes32 blsG2Hash) view returns(bytes32) +func (_Registry *RegistrySession) GetNodeByBlsG2Hash(blsG2Hash [32]byte) ([32]byte, error) { + return _Registry.Contract.GetNodeByBlsG2Hash(&_Registry.CallOpts, blsG2Hash) +} + +// GetNodeByBlsG2Hash is a free data retrieval call binding the contract method 0x18071936. +// +// Solidity: function getNodeByBlsG2Hash(bytes32 blsG2Hash) view returns(bytes32) +func (_Registry *RegistryCallerSession) GetNodeByBlsG2Hash(blsG2Hash [32]byte) ([32]byte, error) { + return _Registry.Contract.GetNodeByBlsG2Hash(&_Registry.CallOpts, blsG2Hash) +} + +// GetNodeById is a free data retrieval call binding the contract method 0x8899cf50. +// +// Solidity: function getNodeById(bytes32 nodeId) view returns((address,uint64,uint64,uint64,uint32,uint32,uint256,uint256,uint256[2],uint256[4])) +func (_Registry *RegistryCaller) GetNodeById(opts *bind.CallOpts, nodeId [32]byte) (NodeRecord, error) { + var out []interface{} + err := _Registry.contract.Call(opts, &out, "getNodeById", nodeId) + + if err != nil { + return *new(NodeRecord), err + } + + out0 := *abi.ConvertType(out[0], new(NodeRecord)).(*NodeRecord) + + return out0, err + +} + +// GetNodeById is a free data retrieval call binding the contract method 0x8899cf50. +// +// Solidity: function getNodeById(bytes32 nodeId) view returns((address,uint64,uint64,uint64,uint32,uint32,uint256,uint256,uint256[2],uint256[4])) +func (_Registry *RegistrySession) GetNodeById(nodeId [32]byte) (NodeRecord, error) { + return _Registry.Contract.GetNodeById(&_Registry.CallOpts, nodeId) +} + +// GetNodeById is a free data retrieval call binding the contract method 0x8899cf50. +// +// Solidity: function getNodeById(bytes32 nodeId) view returns((address,uint64,uint64,uint64,uint32,uint32,uint256,uint256,uint256[2],uint256[4])) +func (_Registry *RegistryCallerSession) GetNodeById(nodeId [32]byte) (NodeRecord, error) { + return _Registry.Contract.GetNodeById(&_Registry.CallOpts, nodeId) +} + +// GetNodeId is a free data retrieval call binding the contract method 0x6ef67bae. +// +// Solidity: function getNodeId(uint32 tokenId) view returns(bytes32) +func (_Registry *RegistryCaller) GetNodeId(opts *bind.CallOpts, tokenId uint32) ([32]byte, error) { + var out []interface{} + err := _Registry.contract.Call(opts, &out, "getNodeId", tokenId) + + if err != nil { + return *new([32]byte), err + } + + out0 := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) + + return out0, err + +} + +// GetNodeId is a free data retrieval call binding the contract method 0x6ef67bae. +// +// Solidity: function getNodeId(uint32 tokenId) view returns(bytes32) +func (_Registry *RegistrySession) GetNodeId(tokenId uint32) ([32]byte, error) { + return _Registry.Contract.GetNodeId(&_Registry.CallOpts, tokenId) +} + +// GetNodeId is a free data retrieval call binding the contract method 0x6ef67bae. +// +// Solidity: function getNodeId(uint32 tokenId) view returns(bytes32) +func (_Registry *RegistryCallerSession) GetNodeId(tokenId uint32) ([32]byte, error) { + return _Registry.Contract.GetNodeId(&_Registry.CallOpts, tokenId) +} + +// GetNodeIds is a free data retrieval call binding the contract method 0xbdc43e92. +// +// Solidity: function getNodeIds(uint256 offset, uint256 limit) view returns(bytes32[]) +func (_Registry *RegistryCaller) GetNodeIds(opts *bind.CallOpts, offset *big.Int, limit *big.Int) ([][32]byte, error) { + var out []interface{} + err := _Registry.contract.Call(opts, &out, "getNodeIds", offset, limit) + + if err != nil { + return *new([][32]byte), err + } + + out0 := *abi.ConvertType(out[0], new([][32]byte)).(*[][32]byte) + + return out0, err + +} + +// GetNodeIds is a free data retrieval call binding the contract method 0xbdc43e92. +// +// Solidity: function getNodeIds(uint256 offset, uint256 limit) view returns(bytes32[]) +func (_Registry *RegistrySession) GetNodeIds(offset *big.Int, limit *big.Int) ([][32]byte, error) { + return _Registry.Contract.GetNodeIds(&_Registry.CallOpts, offset, limit) +} + +// GetNodeIds is a free data retrieval call binding the contract method 0xbdc43e92. +// +// Solidity: function getNodeIds(uint256 offset, uint256 limit) view returns(bytes32[]) +func (_Registry *RegistryCallerSession) GetNodeIds(offset *big.Int, limit *big.Int) ([][32]byte, error) { + return _Registry.Contract.GetNodeIds(&_Registry.CallOpts, offset, limit) +} + +// GetNodes is a free data retrieval call binding the contract method 0x038d67e8. +// +// Solidity: function getNodes(uint256 offset, uint256 limit) view returns((address,uint64,uint64,uint64,uint32,uint32,uint256,uint256,uint256[2],uint256[4])[]) +func (_Registry *RegistryCaller) GetNodes(opts *bind.CallOpts, offset *big.Int, limit *big.Int) ([]NodeRecord, error) { + var out []interface{} + err := _Registry.contract.Call(opts, &out, "getNodes", offset, limit) + + if err != nil { + return *new([]NodeRecord), err + } + + out0 := *abi.ConvertType(out[0], new([]NodeRecord)).(*[]NodeRecord) + + return out0, err + +} + +// GetNodes is a free data retrieval call binding the contract method 0x038d67e8. +// +// Solidity: function getNodes(uint256 offset, uint256 limit) view returns((address,uint64,uint64,uint64,uint32,uint32,uint256,uint256,uint256[2],uint256[4])[]) +func (_Registry *RegistrySession) GetNodes(offset *big.Int, limit *big.Int) ([]NodeRecord, error) { + return _Registry.Contract.GetNodes(&_Registry.CallOpts, offset, limit) +} + +// GetNodes is a free data retrieval call binding the contract method 0x038d67e8. +// +// Solidity: function getNodes(uint256 offset, uint256 limit) view returns((address,uint64,uint64,uint64,uint32,uint32,uint256,uint256,uint256[2],uint256[4])[]) +func (_Registry *RegistryCallerSession) GetNodes(offset *big.Int, limit *big.Int) ([]NodeRecord, error) { + return _Registry.Contract.GetNodes(&_Registry.CallOpts, offset, limit) +} + +// Liability is a free data retrieval call binding the contract method 0x705727b5. +// +// Solidity: function liability() view returns(uint256) +func (_Registry *RegistryCaller) Liability(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _Registry.contract.Call(opts, &out, "liability") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// Liability is a free data retrieval call binding the contract method 0x705727b5. +// +// Solidity: function liability() view returns(uint256) +func (_Registry *RegistrySession) Liability() (*big.Int, error) { + return _Registry.Contract.Liability(&_Registry.CallOpts) +} + +// Liability is a free data retrieval call binding the contract method 0x705727b5. +// +// Solidity: function liability() view returns(uint256) +func (_Registry *RegistryCallerSession) Liability() (*big.Int, error) { + return _Registry.Contract.Liability(&_Registry.CallOpts) +} + +// Owner is a free data retrieval call binding the contract method 0x8da5cb5b. +// +// Solidity: function owner() view returns(address) +func (_Registry *RegistryCaller) Owner(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _Registry.contract.Call(opts, &out, "owner") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// Owner is a free data retrieval call binding the contract method 0x8da5cb5b. +// +// Solidity: function owner() view returns(address) +func (_Registry *RegistrySession) Owner() (common.Address, error) { + return _Registry.Contract.Owner(&_Registry.CallOpts) +} + +// Owner is a free data retrieval call binding the contract method 0x8da5cb5b. +// +// Solidity: function owner() view returns(address) +func (_Registry *RegistryCallerSession) Owner() (common.Address, error) { + return _Registry.Contract.Owner(&_Registry.CallOpts) +} + +// Slasher is a free data retrieval call binding the contract method 0xb1344271. +// +// Solidity: function slasher() view returns(address) +func (_Registry *RegistryCaller) Slasher(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _Registry.contract.Call(opts, &out, "slasher") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// Slasher is a free data retrieval call binding the contract method 0xb1344271. +// +// Solidity: function slasher() view returns(address) +func (_Registry *RegistrySession) Slasher() (common.Address, error) { + return _Registry.Contract.Slasher(&_Registry.CallOpts) +} + +// Slasher is a free data retrieval call binding the contract method 0xb1344271. +// +// Solidity: function slasher() view returns(address) +func (_Registry *RegistryCallerSession) Slasher() (common.Address, error) { + return _Registry.Contract.Slasher(&_Registry.CallOpts) +} + +// TotalNodes is a free data retrieval call binding the contract method 0x9592d424. +// +// Solidity: function totalNodes() view returns(uint256) +func (_Registry *RegistryCaller) TotalNodes(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _Registry.contract.Call(opts, &out, "totalNodes") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// TotalNodes is a free data retrieval call binding the contract method 0x9592d424. +// +// Solidity: function totalNodes() view returns(uint256) +func (_Registry *RegistrySession) TotalNodes() (*big.Int, error) { + return _Registry.Contract.TotalNodes(&_Registry.CallOpts) +} + +// TotalNodes is a free data retrieval call binding the contract method 0x9592d424. +// +// Solidity: function totalNodes() view returns(uint256) +func (_Registry *RegistryCallerSession) TotalNodes() (*big.Int, error) { + return _Registry.Contract.TotalNodes(&_Registry.CallOpts) +} + +// Activate is a paid mutator transaction binding the contract method 0xad122111. +// +// Solidity: function activate(uint32 tokenId, uint256[2] blsPubkeyG1, uint256[4] blsPubkeyG2, uint256 operatorCollateral) returns(bytes32 nodeId) +func (_Registry *RegistryTransactor) Activate(opts *bind.TransactOpts, tokenId uint32, blsPubkeyG1 [2]*big.Int, blsPubkeyG2 [4]*big.Int, operatorCollateral *big.Int) (*types.Transaction, error) { + return _Registry.contract.Transact(opts, "activate", tokenId, blsPubkeyG1, blsPubkeyG2, operatorCollateral) +} + +// Activate is a paid mutator transaction binding the contract method 0xad122111. +// +// Solidity: function activate(uint32 tokenId, uint256[2] blsPubkeyG1, uint256[4] blsPubkeyG2, uint256 operatorCollateral) returns(bytes32 nodeId) +func (_Registry *RegistrySession) Activate(tokenId uint32, blsPubkeyG1 [2]*big.Int, blsPubkeyG2 [4]*big.Int, operatorCollateral *big.Int) (*types.Transaction, error) { + return _Registry.Contract.Activate(&_Registry.TransactOpts, tokenId, blsPubkeyG1, blsPubkeyG2, operatorCollateral) +} + +// Activate is a paid mutator transaction binding the contract method 0xad122111. +// +// Solidity: function activate(uint32 tokenId, uint256[2] blsPubkeyG1, uint256[4] blsPubkeyG2, uint256 operatorCollateral) returns(bytes32 nodeId) +func (_Registry *RegistryTransactorSession) Activate(tokenId uint32, blsPubkeyG1 [2]*big.Int, blsPubkeyG2 [4]*big.Int, operatorCollateral *big.Int) (*types.Transaction, error) { + return _Registry.Contract.Activate(&_Registry.TransactOpts, tokenId, blsPubkeyG1, blsPubkeyG2, operatorCollateral) +} + +// Fund is a paid mutator transaction binding the contract method 0x2db75d40. +// +// Solidity: function fund(uint32 tokenId, uint256 amount) returns() +func (_Registry *RegistryTransactor) Fund(opts *bind.TransactOpts, tokenId uint32, amount *big.Int) (*types.Transaction, error) { + return _Registry.contract.Transact(opts, "fund", tokenId, amount) +} + +// Fund is a paid mutator transaction binding the contract method 0x2db75d40. +// +// Solidity: function fund(uint32 tokenId, uint256 amount) returns() +func (_Registry *RegistrySession) Fund(tokenId uint32, amount *big.Int) (*types.Transaction, error) { + return _Registry.Contract.Fund(&_Registry.TransactOpts, tokenId, amount) +} + +// Fund is a paid mutator transaction binding the contract method 0x2db75d40. +// +// Solidity: function fund(uint32 tokenId, uint256 amount) returns() +func (_Registry *RegistryTransactorSession) Fund(tokenId uint32, amount *big.Int) (*types.Transaction, error) { + return _Registry.Contract.Fund(&_Registry.TransactOpts, tokenId, amount) +} + +// Register is a paid mutator transaction binding the contract method 0x7fdd1867. +// +// Solidity: function register(uint256[2] blsPubkeyG1, uint256[4] blsPubkeyG2, uint256 operatorCollateral) returns(uint32 tokenId, bytes32 nodeId) +func (_Registry *RegistryTransactor) Register(opts *bind.TransactOpts, blsPubkeyG1 [2]*big.Int, blsPubkeyG2 [4]*big.Int, operatorCollateral *big.Int) (*types.Transaction, error) { + return _Registry.contract.Transact(opts, "register", blsPubkeyG1, blsPubkeyG2, operatorCollateral) +} + +// Register is a paid mutator transaction binding the contract method 0x7fdd1867. +// +// Solidity: function register(uint256[2] blsPubkeyG1, uint256[4] blsPubkeyG2, uint256 operatorCollateral) returns(uint32 tokenId, bytes32 nodeId) +func (_Registry *RegistrySession) Register(blsPubkeyG1 [2]*big.Int, blsPubkeyG2 [4]*big.Int, operatorCollateral *big.Int) (*types.Transaction, error) { + return _Registry.Contract.Register(&_Registry.TransactOpts, blsPubkeyG1, blsPubkeyG2, operatorCollateral) +} + +// Register is a paid mutator transaction binding the contract method 0x7fdd1867. +// +// Solidity: function register(uint256[2] blsPubkeyG1, uint256[4] blsPubkeyG2, uint256 operatorCollateral) returns(uint32 tokenId, bytes32 nodeId) +func (_Registry *RegistryTransactorSession) Register(blsPubkeyG1 [2]*big.Int, blsPubkeyG2 [4]*big.Int, operatorCollateral *big.Int) (*types.Transaction, error) { + return _Registry.Contract.Register(&_Registry.TransactOpts, blsPubkeyG1, blsPubkeyG2, operatorCollateral) +} + +// Release is a paid mutator transaction binding the contract method 0xdda42b37. +// +// Solidity: function release(uint32 tokenId) returns() +func (_Registry *RegistryTransactor) Release(opts *bind.TransactOpts, tokenId uint32) (*types.Transaction, error) { + return _Registry.contract.Transact(opts, "release", tokenId) +} + +// Release is a paid mutator transaction binding the contract method 0xdda42b37. +// +// Solidity: function release(uint32 tokenId) returns() +func (_Registry *RegistrySession) Release(tokenId uint32) (*types.Transaction, error) { + return _Registry.Contract.Release(&_Registry.TransactOpts, tokenId) +} + +// Release is a paid mutator transaction binding the contract method 0xdda42b37. +// +// Solidity: function release(uint32 tokenId) returns() +func (_Registry *RegistryTransactorSession) Release(tokenId uint32) (*types.Transaction, error) { + return _Registry.Contract.Release(&_Registry.TransactOpts, tokenId) +} + +// SetSlasher is a paid mutator transaction binding the contract method 0xaabc2496. +// +// Solidity: function setSlasher(address newSlasher) returns() +func (_Registry *RegistryTransactor) SetSlasher(opts *bind.TransactOpts, newSlasher common.Address) (*types.Transaction, error) { + return _Registry.contract.Transact(opts, "setSlasher", newSlasher) +} + +// SetSlasher is a paid mutator transaction binding the contract method 0xaabc2496. +// +// Solidity: function setSlasher(address newSlasher) returns() +func (_Registry *RegistrySession) SetSlasher(newSlasher common.Address) (*types.Transaction, error) { + return _Registry.Contract.SetSlasher(&_Registry.TransactOpts, newSlasher) +} + +// SetSlasher is a paid mutator transaction binding the contract method 0xaabc2496. +// +// Solidity: function setSlasher(address newSlasher) returns() +func (_Registry *RegistryTransactorSession) SetSlasher(newSlasher common.Address) (*types.Transaction, error) { + return _Registry.Contract.SetSlasher(&_Registry.TransactOpts, newSlasher) +} + +// Slash is a paid mutator transaction binding the contract method 0x158ece27. +// +// Solidity: function slash(bytes32 nodeId, uint256 amount, address recipient) returns() +func (_Registry *RegistryTransactor) Slash(opts *bind.TransactOpts, nodeId [32]byte, amount *big.Int, recipient common.Address) (*types.Transaction, error) { + return _Registry.contract.Transact(opts, "slash", nodeId, amount, recipient) +} + +// Slash is a paid mutator transaction binding the contract method 0x158ece27. +// +// Solidity: function slash(bytes32 nodeId, uint256 amount, address recipient) returns() +func (_Registry *RegistrySession) Slash(nodeId [32]byte, amount *big.Int, recipient common.Address) (*types.Transaction, error) { + return _Registry.Contract.Slash(&_Registry.TransactOpts, nodeId, amount, recipient) +} + +// Slash is a paid mutator transaction binding the contract method 0x158ece27. +// +// Solidity: function slash(bytes32 nodeId, uint256 amount, address recipient) returns() +func (_Registry *RegistryTransactorSession) Slash(nodeId [32]byte, amount *big.Int, recipient common.Address) (*types.Transaction, error) { + return _Registry.Contract.Slash(&_Registry.TransactOpts, nodeId, amount, recipient) +} + +// TransferOwnership is a paid mutator transaction binding the contract method 0xf2fde38b. +// +// Solidity: function transferOwnership(address newOwner) returns() +func (_Registry *RegistryTransactor) TransferOwnership(opts *bind.TransactOpts, newOwner common.Address) (*types.Transaction, error) { + return _Registry.contract.Transact(opts, "transferOwnership", newOwner) +} + +// TransferOwnership is a paid mutator transaction binding the contract method 0xf2fde38b. +// +// Solidity: function transferOwnership(address newOwner) returns() +func (_Registry *RegistrySession) TransferOwnership(newOwner common.Address) (*types.Transaction, error) { + return _Registry.Contract.TransferOwnership(&_Registry.TransactOpts, newOwner) +} + +// TransferOwnership is a paid mutator transaction binding the contract method 0xf2fde38b. +// +// Solidity: function transferOwnership(address newOwner) returns() +func (_Registry *RegistryTransactorSession) TransferOwnership(newOwner common.Address) (*types.Transaction, error) { + return _Registry.Contract.TransferOwnership(&_Registry.TransactOpts, newOwner) +} + +// Unlock is a paid mutator transaction binding the contract method 0x67d93c81. +// +// Solidity: function unlock(uint32 tokenId) returns() +func (_Registry *RegistryTransactor) Unlock(opts *bind.TransactOpts, tokenId uint32) (*types.Transaction, error) { + return _Registry.contract.Transact(opts, "unlock", tokenId) +} + +// Unlock is a paid mutator transaction binding the contract method 0x67d93c81. +// +// Solidity: function unlock(uint32 tokenId) returns() +func (_Registry *RegistrySession) Unlock(tokenId uint32) (*types.Transaction, error) { + return _Registry.Contract.Unlock(&_Registry.TransactOpts, tokenId) +} + +// Unlock is a paid mutator transaction binding the contract method 0x67d93c81. +// +// Solidity: function unlock(uint32 tokenId) returns() +func (_Registry *RegistryTransactorSession) Unlock(tokenId uint32) (*types.Transaction, error) { + return _Registry.Contract.Unlock(&_Registry.TransactOpts, tokenId) +} + +// RegistryNodeActivatedIterator is returned from FilterNodeActivated and is used to iterate over the raw logs and unpacked data for NodeActivated events raised by the Registry contract. +type RegistryNodeActivatedIterator struct { + Event *RegistryNodeActivated // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *RegistryNodeActivatedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(RegistryNodeActivated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(RegistryNodeActivated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *RegistryNodeActivatedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *RegistryNodeActivatedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// RegistryNodeActivated represents a NodeActivated event raised by the Registry contract. +type RegistryNodeActivated struct { + Operator common.Address + NodeId [32]byte + TokenId uint32 + Collateral *big.Int + VestedAt uint64 + BlsPubkeyG2 [4]*big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterNodeActivated is a free log retrieval operation binding the contract event 0x58cee7261629c956b111bc684df727bd2e9b0f5d954e24b93908951c431cd13e. +// +// Solidity: event NodeActivated(address indexed operator, bytes32 indexed nodeId, uint32 indexed tokenId, uint256 collateral, uint64 vestedAt, uint256[4] blsPubkeyG2) +func (_Registry *RegistryFilterer) FilterNodeActivated(opts *bind.FilterOpts, operator []common.Address, nodeId [][32]byte, tokenId []uint32) (*RegistryNodeActivatedIterator, error) { + + var operatorRule []interface{} + for _, operatorItem := range operator { + operatorRule = append(operatorRule, operatorItem) + } + var nodeIdRule []interface{} + for _, nodeIdItem := range nodeId { + nodeIdRule = append(nodeIdRule, nodeIdItem) + } + var tokenIdRule []interface{} + for _, tokenIdItem := range tokenId { + tokenIdRule = append(tokenIdRule, tokenIdItem) + } + + logs, sub, err := _Registry.contract.FilterLogs(opts, "NodeActivated", operatorRule, nodeIdRule, tokenIdRule) + if err != nil { + return nil, err + } + return &RegistryNodeActivatedIterator{contract: _Registry.contract, event: "NodeActivated", logs: logs, sub: sub}, nil +} + +// WatchNodeActivated is a free log subscription operation binding the contract event 0x58cee7261629c956b111bc684df727bd2e9b0f5d954e24b93908951c431cd13e. +// +// Solidity: event NodeActivated(address indexed operator, bytes32 indexed nodeId, uint32 indexed tokenId, uint256 collateral, uint64 vestedAt, uint256[4] blsPubkeyG2) +func (_Registry *RegistryFilterer) WatchNodeActivated(opts *bind.WatchOpts, sink chan<- *RegistryNodeActivated, operator []common.Address, nodeId [][32]byte, tokenId []uint32) (event.Subscription, error) { + + var operatorRule []interface{} + for _, operatorItem := range operator { + operatorRule = append(operatorRule, operatorItem) + } + var nodeIdRule []interface{} + for _, nodeIdItem := range nodeId { + nodeIdRule = append(nodeIdRule, nodeIdItem) + } + var tokenIdRule []interface{} + for _, tokenIdItem := range tokenId { + tokenIdRule = append(tokenIdRule, tokenIdItem) + } + + logs, sub, err := _Registry.contract.WatchLogs(opts, "NodeActivated", operatorRule, nodeIdRule, tokenIdRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(RegistryNodeActivated) + if err := _Registry.contract.UnpackLog(event, "NodeActivated", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseNodeActivated is a log parse operation binding the contract event 0x58cee7261629c956b111bc684df727bd2e9b0f5d954e24b93908951c431cd13e. +// +// Solidity: event NodeActivated(address indexed operator, bytes32 indexed nodeId, uint32 indexed tokenId, uint256 collateral, uint64 vestedAt, uint256[4] blsPubkeyG2) +func (_Registry *RegistryFilterer) ParseNodeActivated(log types.Log) (*RegistryNodeActivated, error) { + event := new(RegistryNodeActivated) + if err := _Registry.contract.UnpackLog(event, "NodeActivated", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// RegistryNodeFundedIterator is returned from FilterNodeFunded and is used to iterate over the raw logs and unpacked data for NodeFunded events raised by the Registry contract. +type RegistryNodeFundedIterator struct { + Event *RegistryNodeFunded // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *RegistryNodeFundedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(RegistryNodeFunded) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(RegistryNodeFunded) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *RegistryNodeFundedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *RegistryNodeFundedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// RegistryNodeFunded represents a NodeFunded event raised by the Registry contract. +type RegistryNodeFunded struct { + Payer common.Address + NodeId [32]byte + TokenId uint32 + Amount *big.Int + TotalCollateral *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterNodeFunded is a free log retrieval operation binding the contract event 0x12341d30af78a74af3697daeaf7b1662bc9b723f6aa8a3402bf3d8b3f87a0772. +// +// Solidity: event NodeFunded(address indexed payer, bytes32 indexed nodeId, uint32 indexed tokenId, uint256 amount, uint256 totalCollateral) +func (_Registry *RegistryFilterer) FilterNodeFunded(opts *bind.FilterOpts, payer []common.Address, nodeId [][32]byte, tokenId []uint32) (*RegistryNodeFundedIterator, error) { + + var payerRule []interface{} + for _, payerItem := range payer { + payerRule = append(payerRule, payerItem) + } + var nodeIdRule []interface{} + for _, nodeIdItem := range nodeId { + nodeIdRule = append(nodeIdRule, nodeIdItem) + } + var tokenIdRule []interface{} + for _, tokenIdItem := range tokenId { + tokenIdRule = append(tokenIdRule, tokenIdItem) + } + + logs, sub, err := _Registry.contract.FilterLogs(opts, "NodeFunded", payerRule, nodeIdRule, tokenIdRule) + if err != nil { + return nil, err + } + return &RegistryNodeFundedIterator{contract: _Registry.contract, event: "NodeFunded", logs: logs, sub: sub}, nil +} + +// WatchNodeFunded is a free log subscription operation binding the contract event 0x12341d30af78a74af3697daeaf7b1662bc9b723f6aa8a3402bf3d8b3f87a0772. +// +// Solidity: event NodeFunded(address indexed payer, bytes32 indexed nodeId, uint32 indexed tokenId, uint256 amount, uint256 totalCollateral) +func (_Registry *RegistryFilterer) WatchNodeFunded(opts *bind.WatchOpts, sink chan<- *RegistryNodeFunded, payer []common.Address, nodeId [][32]byte, tokenId []uint32) (event.Subscription, error) { + + var payerRule []interface{} + for _, payerItem := range payer { + payerRule = append(payerRule, payerItem) + } + var nodeIdRule []interface{} + for _, nodeIdItem := range nodeId { + nodeIdRule = append(nodeIdRule, nodeIdItem) + } + var tokenIdRule []interface{} + for _, tokenIdItem := range tokenId { + tokenIdRule = append(tokenIdRule, tokenIdItem) + } + + logs, sub, err := _Registry.contract.WatchLogs(opts, "NodeFunded", payerRule, nodeIdRule, tokenIdRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(RegistryNodeFunded) + if err := _Registry.contract.UnpackLog(event, "NodeFunded", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseNodeFunded is a log parse operation binding the contract event 0x12341d30af78a74af3697daeaf7b1662bc9b723f6aa8a3402bf3d8b3f87a0772. +// +// Solidity: event NodeFunded(address indexed payer, bytes32 indexed nodeId, uint32 indexed tokenId, uint256 amount, uint256 totalCollateral) +func (_Registry *RegistryFilterer) ParseNodeFunded(log types.Log) (*RegistryNodeFunded, error) { + event := new(RegistryNodeFunded) + if err := _Registry.contract.UnpackLog(event, "NodeFunded", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// RegistryNodeReleasedIterator is returned from FilterNodeReleased and is used to iterate over the raw logs and unpacked data for NodeReleased events raised by the Registry contract. +type RegistryNodeReleasedIterator struct { + Event *RegistryNodeReleased // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *RegistryNodeReleasedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(RegistryNodeReleased) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(RegistryNodeReleased) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *RegistryNodeReleasedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *RegistryNodeReleasedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// RegistryNodeReleased represents a NodeReleased event raised by the Registry contract. +type RegistryNodeReleased struct { + Operator common.Address + NodeId [32]byte + TokenId uint32 + Collateral *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterNodeReleased is a free log retrieval operation binding the contract event 0x4f72a5ea49c0470a55beb3953816abf5c92fc73003b1049c241b133a0863208c. +// +// Solidity: event NodeReleased(address indexed operator, bytes32 indexed nodeId, uint32 indexed tokenId, uint256 collateral) +func (_Registry *RegistryFilterer) FilterNodeReleased(opts *bind.FilterOpts, operator []common.Address, nodeId [][32]byte, tokenId []uint32) (*RegistryNodeReleasedIterator, error) { + + var operatorRule []interface{} + for _, operatorItem := range operator { + operatorRule = append(operatorRule, operatorItem) + } + var nodeIdRule []interface{} + for _, nodeIdItem := range nodeId { + nodeIdRule = append(nodeIdRule, nodeIdItem) + } + var tokenIdRule []interface{} + for _, tokenIdItem := range tokenId { + tokenIdRule = append(tokenIdRule, tokenIdItem) + } + + logs, sub, err := _Registry.contract.FilterLogs(opts, "NodeReleased", operatorRule, nodeIdRule, tokenIdRule) + if err != nil { + return nil, err + } + return &RegistryNodeReleasedIterator{contract: _Registry.contract, event: "NodeReleased", logs: logs, sub: sub}, nil +} + +// WatchNodeReleased is a free log subscription operation binding the contract event 0x4f72a5ea49c0470a55beb3953816abf5c92fc73003b1049c241b133a0863208c. +// +// Solidity: event NodeReleased(address indexed operator, bytes32 indexed nodeId, uint32 indexed tokenId, uint256 collateral) +func (_Registry *RegistryFilterer) WatchNodeReleased(opts *bind.WatchOpts, sink chan<- *RegistryNodeReleased, operator []common.Address, nodeId [][32]byte, tokenId []uint32) (event.Subscription, error) { + + var operatorRule []interface{} + for _, operatorItem := range operator { + operatorRule = append(operatorRule, operatorItem) + } + var nodeIdRule []interface{} + for _, nodeIdItem := range nodeId { + nodeIdRule = append(nodeIdRule, nodeIdItem) + } + var tokenIdRule []interface{} + for _, tokenIdItem := range tokenId { + tokenIdRule = append(tokenIdRule, tokenIdItem) + } + + logs, sub, err := _Registry.contract.WatchLogs(opts, "NodeReleased", operatorRule, nodeIdRule, tokenIdRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(RegistryNodeReleased) + if err := _Registry.contract.UnpackLog(event, "NodeReleased", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseNodeReleased is a log parse operation binding the contract event 0x4f72a5ea49c0470a55beb3953816abf5c92fc73003b1049c241b133a0863208c. +// +// Solidity: event NodeReleased(address indexed operator, bytes32 indexed nodeId, uint32 indexed tokenId, uint256 collateral) +func (_Registry *RegistryFilterer) ParseNodeReleased(log types.Log) (*RegistryNodeReleased, error) { + event := new(RegistryNodeReleased) + if err := _Registry.contract.UnpackLog(event, "NodeReleased", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// RegistryNodeUnlockedIterator is returned from FilterNodeUnlocked and is used to iterate over the raw logs and unpacked data for NodeUnlocked events raised by the Registry contract. +type RegistryNodeUnlockedIterator struct { + Event *RegistryNodeUnlocked // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *RegistryNodeUnlockedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(RegistryNodeUnlocked) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(RegistryNodeUnlocked) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *RegistryNodeUnlockedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *RegistryNodeUnlockedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// RegistryNodeUnlocked represents a NodeUnlocked event raised by the Registry contract. +type RegistryNodeUnlocked struct { + Operator common.Address + NodeId [32]byte + AvailableAt uint64 + Raw types.Log // Blockchain specific contextual infos +} + +// FilterNodeUnlocked is a free log retrieval operation binding the contract event 0x0c833c7c9f5b9b8ed0085d7959eb025f59fa32a55b8d55223a819bc7c58db345. +// +// Solidity: event NodeUnlocked(address indexed operator, bytes32 indexed nodeId, uint64 availableAt) +func (_Registry *RegistryFilterer) FilterNodeUnlocked(opts *bind.FilterOpts, operator []common.Address, nodeId [][32]byte) (*RegistryNodeUnlockedIterator, error) { + + var operatorRule []interface{} + for _, operatorItem := range operator { + operatorRule = append(operatorRule, operatorItem) + } + var nodeIdRule []interface{} + for _, nodeIdItem := range nodeId { + nodeIdRule = append(nodeIdRule, nodeIdItem) + } + + logs, sub, err := _Registry.contract.FilterLogs(opts, "NodeUnlocked", operatorRule, nodeIdRule) + if err != nil { + return nil, err + } + return &RegistryNodeUnlockedIterator{contract: _Registry.contract, event: "NodeUnlocked", logs: logs, sub: sub}, nil +} + +// WatchNodeUnlocked is a free log subscription operation binding the contract event 0x0c833c7c9f5b9b8ed0085d7959eb025f59fa32a55b8d55223a819bc7c58db345. +// +// Solidity: event NodeUnlocked(address indexed operator, bytes32 indexed nodeId, uint64 availableAt) +func (_Registry *RegistryFilterer) WatchNodeUnlocked(opts *bind.WatchOpts, sink chan<- *RegistryNodeUnlocked, operator []common.Address, nodeId [][32]byte) (event.Subscription, error) { + + var operatorRule []interface{} + for _, operatorItem := range operator { + operatorRule = append(operatorRule, operatorItem) + } + var nodeIdRule []interface{} + for _, nodeIdItem := range nodeId { + nodeIdRule = append(nodeIdRule, nodeIdItem) + } + + logs, sub, err := _Registry.contract.WatchLogs(opts, "NodeUnlocked", operatorRule, nodeIdRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(RegistryNodeUnlocked) + if err := _Registry.contract.UnpackLog(event, "NodeUnlocked", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseNodeUnlocked is a log parse operation binding the contract event 0x0c833c7c9f5b9b8ed0085d7959eb025f59fa32a55b8d55223a819bc7c58db345. +// +// Solidity: event NodeUnlocked(address indexed operator, bytes32 indexed nodeId, uint64 availableAt) +func (_Registry *RegistryFilterer) ParseNodeUnlocked(log types.Log) (*RegistryNodeUnlocked, error) { + event := new(RegistryNodeUnlocked) + if err := _Registry.contract.UnpackLog(event, "NodeUnlocked", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// RegistryOwnershipTransferredIterator is returned from FilterOwnershipTransferred and is used to iterate over the raw logs and unpacked data for OwnershipTransferred events raised by the Registry contract. +type RegistryOwnershipTransferredIterator struct { + Event *RegistryOwnershipTransferred // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *RegistryOwnershipTransferredIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(RegistryOwnershipTransferred) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(RegistryOwnershipTransferred) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *RegistryOwnershipTransferredIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *RegistryOwnershipTransferredIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// RegistryOwnershipTransferred represents a OwnershipTransferred event raised by the Registry contract. +type RegistryOwnershipTransferred struct { + OldOwner common.Address + NewOwner common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterOwnershipTransferred is a free log retrieval operation binding the contract event 0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0. +// +// Solidity: event OwnershipTransferred(address indexed oldOwner, address indexed newOwner) +func (_Registry *RegistryFilterer) FilterOwnershipTransferred(opts *bind.FilterOpts, oldOwner []common.Address, newOwner []common.Address) (*RegistryOwnershipTransferredIterator, error) { + + var oldOwnerRule []interface{} + for _, oldOwnerItem := range oldOwner { + oldOwnerRule = append(oldOwnerRule, oldOwnerItem) + } + var newOwnerRule []interface{} + for _, newOwnerItem := range newOwner { + newOwnerRule = append(newOwnerRule, newOwnerItem) + } + + logs, sub, err := _Registry.contract.FilterLogs(opts, "OwnershipTransferred", oldOwnerRule, newOwnerRule) + if err != nil { + return nil, err + } + return &RegistryOwnershipTransferredIterator{contract: _Registry.contract, event: "OwnershipTransferred", logs: logs, sub: sub}, nil +} + +// WatchOwnershipTransferred is a free log subscription operation binding the contract event 0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0. +// +// Solidity: event OwnershipTransferred(address indexed oldOwner, address indexed newOwner) +func (_Registry *RegistryFilterer) WatchOwnershipTransferred(opts *bind.WatchOpts, sink chan<- *RegistryOwnershipTransferred, oldOwner []common.Address, newOwner []common.Address) (event.Subscription, error) { + + var oldOwnerRule []interface{} + for _, oldOwnerItem := range oldOwner { + oldOwnerRule = append(oldOwnerRule, oldOwnerItem) + } + var newOwnerRule []interface{} + for _, newOwnerItem := range newOwner { + newOwnerRule = append(newOwnerRule, newOwnerItem) + } + + logs, sub, err := _Registry.contract.WatchLogs(opts, "OwnershipTransferred", oldOwnerRule, newOwnerRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(RegistryOwnershipTransferred) + if err := _Registry.contract.UnpackLog(event, "OwnershipTransferred", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseOwnershipTransferred is a log parse operation binding the contract event 0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0. +// +// Solidity: event OwnershipTransferred(address indexed oldOwner, address indexed newOwner) +func (_Registry *RegistryFilterer) ParseOwnershipTransferred(log types.Log) (*RegistryOwnershipTransferred, error) { + event := new(RegistryOwnershipTransferred) + if err := _Registry.contract.UnpackLog(event, "OwnershipTransferred", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// RegistrySlashedIterator is returned from FilterSlashed and is used to iterate over the raw logs and unpacked data for Slashed events raised by the Registry contract. +type RegistrySlashedIterator struct { + Event *RegistrySlashed // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *RegistrySlashedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(RegistrySlashed) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(RegistrySlashed) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *RegistrySlashedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *RegistrySlashedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// RegistrySlashed represents a Slashed event raised by the Registry contract. +type RegistrySlashed struct { + NodeId [32]byte + Amount *big.Int + Recipient common.Address + FromOperator *big.Int + FromSponsor *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterSlashed is a free log retrieval operation binding the contract event 0x9a2bb3d9059142feaf2a6cbf5062a0047437076519c62ede693c2ec2240f1333. +// +// Solidity: event Slashed(bytes32 indexed nodeId, uint256 amount, address indexed recipient, uint256 fromOperator, uint256 fromSponsor) +func (_Registry *RegistryFilterer) FilterSlashed(opts *bind.FilterOpts, nodeId [][32]byte, recipient []common.Address) (*RegistrySlashedIterator, error) { + + var nodeIdRule []interface{} + for _, nodeIdItem := range nodeId { + nodeIdRule = append(nodeIdRule, nodeIdItem) + } + + var recipientRule []interface{} + for _, recipientItem := range recipient { + recipientRule = append(recipientRule, recipientItem) + } + + logs, sub, err := _Registry.contract.FilterLogs(opts, "Slashed", nodeIdRule, recipientRule) + if err != nil { + return nil, err + } + return &RegistrySlashedIterator{contract: _Registry.contract, event: "Slashed", logs: logs, sub: sub}, nil +} + +// WatchSlashed is a free log subscription operation binding the contract event 0x9a2bb3d9059142feaf2a6cbf5062a0047437076519c62ede693c2ec2240f1333. +// +// Solidity: event Slashed(bytes32 indexed nodeId, uint256 amount, address indexed recipient, uint256 fromOperator, uint256 fromSponsor) +func (_Registry *RegistryFilterer) WatchSlashed(opts *bind.WatchOpts, sink chan<- *RegistrySlashed, nodeId [][32]byte, recipient []common.Address) (event.Subscription, error) { + + var nodeIdRule []interface{} + for _, nodeIdItem := range nodeId { + nodeIdRule = append(nodeIdRule, nodeIdItem) + } + + var recipientRule []interface{} + for _, recipientItem := range recipient { + recipientRule = append(recipientRule, recipientItem) + } + + logs, sub, err := _Registry.contract.WatchLogs(opts, "Slashed", nodeIdRule, recipientRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(RegistrySlashed) + if err := _Registry.contract.UnpackLog(event, "Slashed", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseSlashed is a log parse operation binding the contract event 0x9a2bb3d9059142feaf2a6cbf5062a0047437076519c62ede693c2ec2240f1333. +// +// Solidity: event Slashed(bytes32 indexed nodeId, uint256 amount, address indexed recipient, uint256 fromOperator, uint256 fromSponsor) +func (_Registry *RegistryFilterer) ParseSlashed(log types.Log) (*RegistrySlashed, error) { + event := new(RegistrySlashed) + if err := _Registry.contract.UnpackLog(event, "Slashed", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// RegistrySlasherUpdatedIterator is returned from FilterSlasherUpdated and is used to iterate over the raw logs and unpacked data for SlasherUpdated events raised by the Registry contract. +type RegistrySlasherUpdatedIterator struct { + Event *RegistrySlasherUpdated // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *RegistrySlasherUpdatedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(RegistrySlasherUpdated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(RegistrySlasherUpdated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *RegistrySlasherUpdatedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *RegistrySlasherUpdatedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// RegistrySlasherUpdated represents a SlasherUpdated event raised by the Registry contract. +type RegistrySlasherUpdated struct { + OldSlasher common.Address + NewSlasher common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterSlasherUpdated is a free log retrieval operation binding the contract event 0xe0d49a54274423183dadecbdf239eaac6e06ba88320b26fe8cc5ec9d050a6395. +// +// Solidity: event SlasherUpdated(address indexed oldSlasher, address indexed newSlasher) +func (_Registry *RegistryFilterer) FilterSlasherUpdated(opts *bind.FilterOpts, oldSlasher []common.Address, newSlasher []common.Address) (*RegistrySlasherUpdatedIterator, error) { + + var oldSlasherRule []interface{} + for _, oldSlasherItem := range oldSlasher { + oldSlasherRule = append(oldSlasherRule, oldSlasherItem) + } + var newSlasherRule []interface{} + for _, newSlasherItem := range newSlasher { + newSlasherRule = append(newSlasherRule, newSlasherItem) + } + + logs, sub, err := _Registry.contract.FilterLogs(opts, "SlasherUpdated", oldSlasherRule, newSlasherRule) + if err != nil { + return nil, err + } + return &RegistrySlasherUpdatedIterator{contract: _Registry.contract, event: "SlasherUpdated", logs: logs, sub: sub}, nil +} + +// WatchSlasherUpdated is a free log subscription operation binding the contract event 0xe0d49a54274423183dadecbdf239eaac6e06ba88320b26fe8cc5ec9d050a6395. +// +// Solidity: event SlasherUpdated(address indexed oldSlasher, address indexed newSlasher) +func (_Registry *RegistryFilterer) WatchSlasherUpdated(opts *bind.WatchOpts, sink chan<- *RegistrySlasherUpdated, oldSlasher []common.Address, newSlasher []common.Address) (event.Subscription, error) { + + var oldSlasherRule []interface{} + for _, oldSlasherItem := range oldSlasher { + oldSlasherRule = append(oldSlasherRule, oldSlasherItem) + } + var newSlasherRule []interface{} + for _, newSlasherItem := range newSlasher { + newSlasherRule = append(newSlasherRule, newSlasherItem) + } + + logs, sub, err := _Registry.contract.WatchLogs(opts, "SlasherUpdated", oldSlasherRule, newSlasherRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(RegistrySlasherUpdated) + if err := _Registry.contract.UnpackLog(event, "SlasherUpdated", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseSlasherUpdated is a log parse operation binding the contract event 0xe0d49a54274423183dadecbdf239eaac6e06ba88320b26fe8cc5ec9d050a6395. +// +// Solidity: event SlasherUpdated(address indexed oldSlasher, address indexed newSlasher) +func (_Registry *RegistryFilterer) ParseSlasherUpdated(log types.Log) (*RegistrySlasherUpdated, error) { + event := new(RegistrySlasherUpdated) + if err := _Registry.contract.UnpackLog(event, "SlasherUpdated", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} diff --git a/pkg/blockchain/evm/registry_adapter.go b/pkg/blockchain/evm/registry_adapter.go new file mode 100644 index 0000000..a821bb2 --- /dev/null +++ b/pkg/blockchain/evm/registry_adapter.go @@ -0,0 +1,202 @@ +package evm + +import ( + "context" + "crypto/ecdsa" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + + "github.com/layer-3/clearnet-sdk/pkg/core" +) + +// RegistryAdapter wraps the Registry binding (plus the staking-token binding +// for collateral approvals) for node onboarding and registry queries. It +// implements core.RegistryReader and core.RegistryWriter. +type RegistryAdapter struct { + client *ethclient.Client + registry *Registry + registryAddr common.Address + token *MockERC20 + auth *bind.TransactOpts +} + +var ( + _ core.RegistryReader = (*RegistryAdapter)(nil) + _ core.RegistryWriter = (*RegistryAdapter)(nil) +) + +// NewRegistryAdapter binds the Registry at registryAddr and the staking token +// at tokenAddr over client, with a transactor for the given key. The token is +// needed because Lock/Fund approve collateral before the registry call. +func NewRegistryAdapter(ctx context.Context, client *ethclient.Client, registryAddr, tokenAddr common.Address, key *ecdsa.PrivateKey) (*RegistryAdapter, error) { + registry, err := NewRegistry(registryAddr, client) + if err != nil { + return nil, fmt.Errorf("load registry: %w", err) + } + token, err := NewMockERC20(tokenAddr, client) + if err != nil { + return nil, fmt.Errorf("load staking token: %w", err) + } + auth, err := newTransactor(ctx, client, key) + if err != nil { + return nil, err + } + return &RegistryAdapter{ + client: client, + registry: registry, + registryAddr: registryAddr, + token: token, + auth: auth, + }, nil +} + +// ─── Write ─────────────────────────────────────────────────────────────────── + +// Lock onboards a new operator: approves collateral, then calls +// Registry.register (which mints the NodeID NFT into escrow and activates it). +// Returns the freshly-minted tokenId. popSignature is accepted for +// source-compat but ignored on chain (ADR-008 2026-05-08). +func (a *RegistryAdapter) Lock(ctx context.Context, blsPubkeyG1 [2]*big.Int, blsPubkeyG2 [4]*big.Int, popSignature [2]*big.Int, maxPrice *big.Int) (uint32, error) { + _ = popSignature + + floor, err := a.registry.FloorPrice(&bind.CallOpts{Context: ctx}) + if err != nil { + return 0, fmt.Errorf("floor price: %w", err) + } + if maxPrice != nil && floor.Cmp(maxPrice) > 0 { + return 0, fmt.Errorf("price exceeds max") + } + collateral := new(big.Int).Mul(floor, big.NewInt(2)) + + tokenApproveTx, err := a.token.Approve(txOpts(a.auth, ctx), a.registryAddr, collateral) + if err != nil { + return 0, fmt.Errorf("approve staking token: %w", err) + } + if err := waitMined(ctx, a.client, tokenApproveTx); err != nil { + return 0, fmt.Errorf("approve staking token wait: %w", err) + } + + registerOpts := txOpts(a.auth, ctx) + registerOpts.GasLimit = 10_000_000 + registerTx, err := a.registry.Register(registerOpts, blsPubkeyG1, blsPubkeyG2, collateral) + if err != nil { + return 0, fmt.Errorf("register: %w", err) + } + receipt, err := bind.WaitMined(ctx, a.client, registerTx) + if err != nil { + return 0, fmt.Errorf("register wait: %w", err) + } + if receipt.Status == 0 { + return 0, fmt.Errorf("register reverted") + } + tokenId, _, err := parseRegistryActivationFromReceipt(a.registry, receipt) + if err != nil { + return 0, fmt.Errorf("parse NodeActivated: %w", err) + } + return tokenId, nil +} + +func (a *RegistryAdapter) Unlock(ctx context.Context, tokenId uint32) error { + tx, err := a.registry.Unlock(txOpts(a.auth, ctx), tokenId) + if err != nil { + return err + } + return waitMined(ctx, a.client, tx) +} + +func (a *RegistryAdapter) Release(ctx context.Context, tokenId uint32) error { + tx, err := a.registry.Release(txOpts(a.auth, ctx), tokenId) + if err != nil { + return err + } + return waitMined(ctx, a.client, tx) +} + +func (a *RegistryAdapter) Fund(ctx context.Context, tokenId uint32, amount *big.Int) error { + tokenApproveTx, err := a.token.Approve(txOpts(a.auth, ctx), a.registryAddr, amount) + if err != nil { + return fmt.Errorf("approve staking token for funding: %w", err) + } + if err := waitMined(ctx, a.client, tokenApproveTx); err != nil { + return fmt.Errorf("approve staking token wait for funding: %w", err) + } + + tx, err := a.registry.Fund(txOpts(a.auth, ctx), tokenId, amount) + if err != nil { + return err + } + return waitMined(ctx, a.client, tx) +} + +// ─── Read ──────────────────────────────────────────────────────────────────── + +func (a *RegistryAdapter) GetNodeByID(ctx context.Context, nodeID [32]byte) (*core.Slot, error) { + n, err := a.registry.GetNodeById(&bind.CallOpts{Context: ctx}, nodeID) + if err != nil { + return nil, err + } + slot := convertNodeRecord(n) + slot.ID = nodeID + return slot, nil +} + +func (a *RegistryAdapter) FloorPrice(ctx context.Context) (*big.Int, error) { + return a.registry.FloorPrice(&bind.CallOpts{Context: ctx}) +} + +func (a *RegistryAdapter) UnbondingPeriod(ctx context.Context) (uint64, error) { + return a.registry.UNBONDINGPERIOD(&bind.CallOpts{Context: ctx}) +} + +func (a *RegistryAdapter) GetNodeId(ctx context.Context, tokenId uint32) ([32]byte, error) { + return a.registry.GetNodeId(&bind.CallOpts{Context: ctx}, tokenId) +} + +func (a *RegistryAdapter) GetNodes(ctx context.Context, offset, limit *big.Int) ([]*core.Slot, error) { + nodes, err := a.registry.GetNodes(&bind.CallOpts{Context: ctx}, offset, limit) + if err != nil { + return nil, err + } + result := make([]*core.Slot, len(nodes)) + for i, n := range nodes { + slot := convertNodeRecord(n) + id, err := a.registry.GetNodeId(&bind.CallOpts{Context: ctx}, n.TokenId) + if err == nil { + slot.ID = id + } + result[i] = slot + } + return result, nil +} + +func (a *RegistryAdapter) GetActiveNodes(ctx context.Context, offset, limit *big.Int) ([]*core.Slot, error) { + nodes, err := a.registry.GetNodes(&bind.CallOpts{Context: ctx}, offset, limit) + if err != nil { + return nil, err + } + result := make([]*core.Slot, 0, len(nodes)) + for _, n := range nodes { + if n.DeactivatedAt != 0 { + continue + } + slot := convertNodeRecord(n) + id, err := a.registry.GetNodeId(&bind.CallOpts{Context: ctx}, n.TokenId) + if err == nil { + slot.ID = id + } + result = append(result, slot) + } + return result, nil +} + +func (a *RegistryAdapter) TotalNodes(ctx context.Context) (*big.Int, error) { + return a.registry.TotalNodes(&bind.CallOpts{Context: ctx}) +} + +func (a *RegistryAdapter) ActiveCount(ctx context.Context) (uint32, error) { + return a.registry.ActiveCount(&bind.CallOpts{Context: ctx}) +} diff --git a/pkg/blockchain/evm/token_adapter.go b/pkg/blockchain/evm/token_adapter.go new file mode 100644 index 0000000..6ad59ef --- /dev/null +++ b/pkg/blockchain/evm/token_adapter.go @@ -0,0 +1,43 @@ +package evm + +import ( + "context" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + + "github.com/layer-3/clearnet-sdk/pkg/core" +) + +// TokenAdapter reads ERC-20 balances. It binds the token contract per call +// from the address passed to BalanceOf, so one adapter serves any token. It +// implements core.TokenReader. +type TokenAdapter struct { + client *ethclient.Client +} + +var _ core.TokenReader = (*TokenAdapter)(nil) + +// NewTokenAdapter creates a read-only token adapter over client. +func NewTokenAdapter(client *ethclient.Client) *TokenAdapter { + return &TokenAdapter{client: client} +} + +// BalanceOf returns the ERC-20 balance of account (hex) for the token contract +// at token (hex). +func (a *TokenAdapter) BalanceOf(ctx context.Context, token string, account string) (*big.Int, error) { + if !common.IsHexAddress(token) { + return nil, fmt.Errorf("invalid token address %q", token) + } + if !common.IsHexAddress(account) { + return nil, fmt.Errorf("invalid account address %q", account) + } + erc20, err := NewMockERC20(common.HexToAddress(token), a.client) + if err != nil { + return nil, fmt.Errorf("bind token %s: %w", token, err) + } + return erc20.BalanceOf(&bind.CallOpts{Context: ctx}, common.HexToAddress(account)) +} diff --git a/pkg/blockchain/evm/vault_integration_test.go b/pkg/blockchain/evm/vault_integration_test.go new file mode 100644 index 0000000..f43a74b --- /dev/null +++ b/pkg/blockchain/evm/vault_integration_test.go @@ -0,0 +1,200 @@ +//go:build integration + +package evm + +import ( + "bytes" + "context" + "crypto/ecdsa" + "math/big" + "os" + "sort" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + gethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" + + "github.com/layer-3/clearnet-sdk/pkg/core" + "github.com/layer-3/clearnet-sdk/pkg/decimal" + "github.com/layer-3/clearnet-sdk/pkg/sign" +) + +// EVM full deposit + withdrawal flow against a real chain (the devnet anvil by +// default). Self-bootstrapping: deploys a fresh Custody vault whose signer set +// is N freshly-generated keys, then exercises the SDK depositor + the quorum +// withdrawal finalizer end-to-end. Build-tagged `integration`; run with: +// +// go test -tags integration ./pkg/blockchain/evm/ -run TestIntegrationEVM -v +// +// Env (defaults target `make devnet`): +// EVM_RPC_URL — default http://127.0.0.1:8545 +// EVM_DEPLOYER_KEY — hex privkey, funded; default anvil account 0 + +const ( + defaultAnvilRPC = "http://127.0.0.1:8545" + defaultAnvilDeployer = "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" + integrationSignerCount = 3 + integrationThreshold = 2 +) + +func envOr(key, def string) string { + if v := os.Getenv(key); v != "" { + return v + } + return def +} + +func TestIntegrationEVM_DepositAndWithdraw(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) + defer cancel() + + client, err := ethclient.Dial(envOr("EVM_RPC_URL", defaultAnvilRPC)) + if err != nil { + t.Fatalf("dial: %v", err) + } + defer client.Close() + + deployerKey, err := crypto.HexToECDSA(envOr("EVM_DEPLOYER_KEY", defaultAnvilDeployer)) + if err != nil { + t.Fatalf("parse deployer key: %v", err) + } + deployer := sign.NewKeySignerFromECDSA(deployerKey) + + // N vault signers, each a fresh key funded by the deployer for gas. + signers := make([]sign.Signer, integrationSignerCount) + signerAddrs := make([]common.Address, integrationSignerCount) + for i := range signers { + k, err := crypto.GenerateKey() + if err != nil { + t.Fatalf("gen signer key: %v", err) + } + signers[i] = sign.NewKeySignerFromECDSA(k) + signerAddrs[i] = crypto.PubkeyToAddress(k.PublicKey) + fundETH(ctx, t, client, deployerKey, signerAddrs[i], big.NewInt(1e18)) // 1 ETH for gas + } + // Custody's constructor requires initialSigners sorted ascending. + sort.Slice(signerAddrs, func(i, j int) bool { + return bytes.Compare(signerAddrs[i][:], signerAddrs[j][:]) < 0 + }) + + // Deploy a fresh Custody vault over the signer set. + depOpts, _, err := signerTransactOpts(ctx, client, deployer) + if err != nil { + t.Fatalf("deploy opts: %v", err) + } + custodyAddr, deployTx, _, err := DeployCustody(depOpts, client, signerAddrs, big.NewInt(integrationThreshold)) + if err != nil { + t.Fatalf("deploy custody: %v", err) + } + if err := waitMined(ctx, client, deployTx); err != nil { + t.Fatalf("deploy wait: %v", err) + } + t.Logf("deployed Custody at %s (signers=%d threshold=%d)", custodyAddr.Hex(), integrationSignerCount, integrationThreshold) + + // ── Deposit flow ────────────────────────────────────────────────────────── + depositor, err := NewDepositor(client, custodyAddr, deployer) + if err != nil { + t.Fatalf("NewDepositor: %v", err) + } + account := crypto.PubkeyToAddress(deployerKey.PublicKey) + const zeroAsset = "0x0000000000000000000000000000000000000000" // native ETH + depositAmt := decimal.NewFromInt(1_000_000_000_000) // 1e12 wei + depRef, err := depositor.Deposit(ctx, zeroAsset, depositAmt, account.Hex()) + if err != nil { + t.Fatalf("Deposit: %v", err) + } + t.Logf("deposit tx %s", depRef.Raw) + + // ── Withdrawal flow (the quorum runs in-process) ────────────────────────── + finalizers := make([]*WithdrawalFinalizer, len(signers)) + for i, s := range signers { + f, err := NewWithdrawalFinalizer(ctx, client, custodyAddr, s, FeeConfig{}) + if err != nil { + t.Fatalf("NewWithdrawalFinalizer %d: %v", i, err) + } + finalizers[i] = f + } + + var withdrawalID [32]byte + withdrawalID[0], withdrawalID[31] = 0x11, 0x22 + op := &core.WithdrawalOp{ + Recipient: signerAddrs[0].Hex(), + L1Asset: zeroAsset, + Amount: decimal.NewFromInt(400_000_000_000), // < deposited + } + + // 1. Pack (any node — here the first). + packed, err := finalizers[0].Pack(ctx, op, withdrawalID) + if err != nil { + t.Fatalf("Pack: %v", err) + } + // 2. Every node validates then signs. + sigs := make([][]byte, 0, len(finalizers)) + for i, f := range finalizers { + if err := f.Validate(ctx, packed, op, withdrawalID); err != nil { + t.Fatalf("Validate[%d]: %v", i, err) + } + s, err := f.Sign(ctx, packed) + if err != nil { + t.Fatalf("Sign[%d]: %v", i, err) + } + sigs = append(sigs, s) + } + // 3. Merge + Submit (a submitter node). + merged, err := finalizers[0].Merge(ctx, packed, sigs) + if err != nil { + t.Fatalf("Merge: %v", err) + } + wRef, err := finalizers[0].Submit(ctx, merged) + if err != nil { + t.Fatalf("Submit: %v", err) + } + t.Logf("withdrawal tx %s", wRef.Raw) + + // 4. Verify execution. + _, executed, err := finalizers[0].VerifyExecution(ctx, withdrawalID) + if err != nil { + t.Fatalf("VerifyExecution: %v", err) + } + if !executed { + t.Fatal("withdrawal not reported executed") + } +} + +// fundETH sends value from key to addr via a raw anvil tx and waits for it. +func fundETH(ctx context.Context, t *testing.T, client *ethclient.Client, key *ecdsa.PrivateKey, to common.Address, value *big.Int) { + t.Helper() + from := crypto.PubkeyToAddress(key.PublicKey) + nonce, err := client.PendingNonceAt(ctx, from) + if err != nil { + t.Fatalf("nonce: %v", err) + } + chainID, err := client.ChainID(ctx) + if err != nil { + t.Fatalf("chain id: %v", err) + } + gasPrice, err := client.SuggestGasPrice(ctx) + if err != nil { + t.Fatalf("gas price: %v", err) + } + tx := gethtypes.NewTx(&gethtypes.LegacyTx{ + Nonce: nonce, + To: &to, + Value: value, + Gas: 21000, + GasPrice: gasPrice, + }) + signed, err := gethtypes.SignTx(tx, gethtypes.LatestSignerForChainID(chainID), key) + if err != nil { + t.Fatalf("sign fund tx: %v", err) + } + if err := client.SendTransaction(ctx, signed); err != nil { + t.Fatalf("send fund tx: %v", err) + } + if err := waitMined(ctx, client, signed); err != nil { + t.Fatalf("fund wait: %v", err) + } +} diff --git a/pkg/blockchain/evm/withdrawal_finalizer.go b/pkg/blockchain/evm/withdrawal_finalizer.go new file mode 100644 index 0000000..6856956 --- /dev/null +++ b/pkg/blockchain/evm/withdrawal_finalizer.go @@ -0,0 +1,391 @@ +package evm + +import ( + "bytes" + "context" + "encoding/hex" + "encoding/json" + "fmt" + "math/big" + "sort" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" + + "github.com/layer-3/clearnet-sdk/pkg/core" + "github.com/layer-3/clearnet-sdk/pkg/sign" +) + +// executedLookupWindow bounds the eth_getLogs range when resolving the tx hash +// of an already-executed withdrawal. +const executedLookupWindow = uint64(50_000) + +// FeeConfig tunes the submit transaction's gas pricing. Zero values fall back +// to sensible defaults (no cap; 1.5x gas-limit margin). +type FeeConfig struct { + TipGwei float64 // EIP-1559 priority tip + CapGwei float64 // refuse to submit above this effective price (0 = no cap) + GasLimitMultiplier float64 // safety margin over eth_estimateGas (0 => 1.5) +} + +func (f FeeConfig) gasLimitMultiplier() float64 { + if f.GasLimitMultiplier <= 0 { + return 1.5 + } + return f.GasLimitMultiplier +} + +// WithdrawalFinalizer turns an authorized withdrawal into a Custody.execute +// call. It owns the node's signer (used both to sign the k-of-n digest and to +// submit the tx) and the vault address + chain id supplied at construction. It +// implements core.VaultWithdrawalFinalizer. +type WithdrawalFinalizer struct { + client *ethclient.Client + custody *Custody + vaultAddr common.Address + chainID uint64 + signer sign.Signer + signerAddr common.Address + fees FeeConfig +} + +var _ core.VaultWithdrawalFinalizer = (*WithdrawalFinalizer)(nil) + +// NewWithdrawalFinalizer binds the Custody vault at vaultAddr and reads the +// chain id from client. signer is this node's secp256k1 identity. +func NewWithdrawalFinalizer(ctx context.Context, client *ethclient.Client, vaultAddr common.Address, signer sign.Signer, fees FeeConfig) (*WithdrawalFinalizer, error) { + custody, err := NewCustody(vaultAddr, client) + if err != nil { + return nil, fmt.Errorf("load custody: %w", err) + } + chainID, err := client.ChainID(ctx) + if err != nil { + return nil, fmt.Errorf("get chain ID: %w", err) + } + addr, err := sign.EthAddress(signer) + if err != nil { + return nil, err + } + return &WithdrawalFinalizer{ + client: client, + custody: custody, + vaultAddr: vaultAddr, + chainID: chainID.Uint64(), + signer: signer, + signerAddr: addr, + fees: fees, + }, nil +} + +// evmPacked is the canonical withdrawal payload: enough to recompute the +// signing digest and to rebuild the execute() call. +type evmPacked struct { + To string `json:"to"` // recipient address (hex) + Asset string `json:"asset"` // asset address (hex); zero = ETH + Amount string `json:"amount"` // base units (decimal string) + WithdrawalID string `json:"withdrawalId"` // 32-byte hex +} + +// evmMerged is evmPacked plus the ordered, contract-ready signatures. +type evmMerged struct { + evmPacked + Sigs []string `json:"sigs"` // 65-byte sigs, sorted by signer, V ∈ {27,28}, hex +} + +// Pack returns the canonical JSON for the withdrawal. Pure — no chain access. +func (f *WithdrawalFinalizer) Pack(_ context.Context, op *core.WithdrawalOp, withdrawalID [32]byte) ([]byte, error) { + return json.Marshal(packedFromOp(op, withdrawalID)) +} + +// Validate re-derives the canonical payload from the op and asserts the packed +// bytes match it exactly — the defense against a Byzantine packer. +func (f *WithdrawalFinalizer) Validate(_ context.Context, packed []byte, op *core.WithdrawalOp, withdrawalID [32]byte) error { + var got evmPacked + if err := json.Unmarshal(packed, &got); err != nil { + return fmt.Errorf("decode packed: %w", err) + } + want := packedFromOp(op, withdrawalID) + if got != want { + return fmt.Errorf("packed withdrawal does not match op: got %+v want %+v", got, want) + } + return nil +} + +// Sign produces this node's 65-byte ECDSA signature (V ∈ {0,1}) over the +// withdrawal digest derived from the packed bytes. +func (f *WithdrawalFinalizer) Sign(ctx context.Context, packed []byte) ([]byte, error) { + var p evmPacked + if err := json.Unmarshal(packed, &p); err != nil { + return nil, fmt.Errorf("decode packed: %w", err) + } + digest, err := f.digest(p) + if err != nil { + return nil, err + } + return sign.SignEthDigest(ctx, f.signer, digest[:], f.signerAddr) +} + +// Merge filters the collected signatures against the live on-chain signer set, +// trims to the live threshold, orders them by signer address (Custody.sol +// requires ascending, no duplicates), shifts V to {27,28}, and returns the +// merged artifact. +func (f *WithdrawalFinalizer) Merge(ctx context.Context, packed []byte, signatures [][]byte) ([]byte, error) { + var p evmPacked + if err := json.Unmarshal(packed, &p); err != nil { + return nil, fmt.Errorf("decode packed: %w", err) + } + digest, err := f.digest(p) + if err != nil { + return nil, err + } + + liveSigners, liveThreshold, err := f.liveQuorum(ctx) + if err != nil { + return nil, err + } + authorized := make(map[common.Address]struct{}, len(liveSigners)) + for _, a := range liveSigners { + authorized[a] = struct{}{} + } + + type sigAddr struct { + sig []byte + addr common.Address + } + kept := make([]sigAddr, 0, len(signatures)) + seen := make(map[common.Address]struct{}) + for _, s := range signatures { + if len(s) != 65 { + return nil, fmt.Errorf("signature has wrong length %d", len(s)) + } + pub, err := crypto.SigToPub(digest[:], s) + if err != nil { + return nil, fmt.Errorf("recover signer: %w", err) + } + addr := crypto.PubkeyToAddress(*pub) + if _, ok := authorized[addr]; !ok { + continue // not in the live signer set + } + if _, dup := seen[addr]; dup { + continue + } + seen[addr] = struct{}{} + kept = append(kept, sigAddr{sig: s, addr: addr}) + } + if len(kept) < liveThreshold { + return nil, fmt.Errorf("only %d of %d authorized signatures", len(kept), liveThreshold) + } + // Custody.sol's _verifySignatures stops at `threshold` and rejects extras. + kept = kept[:liveThreshold] + // Ascending uint160 order == bytes order over [20]byte. + sort.Slice(kept, func(i, j int) bool { return bytes.Compare(kept[i].addr[:], kept[j].addr[:]) < 0 }) + + merged := evmMerged{evmPacked: p, Sigs: make([]string, len(kept))} + for i, k := range kept { + cp := make([]byte, 65) + copy(cp, k.sig) + if cp[64] < 27 { + cp[64] += 27 // shift V {0,1} -> {27,28} at the contract boundary + } + merged.Sigs[i] = hex.EncodeToString(cp) + } + return json.Marshal(merged) +} + +// Submit broadcasts the merged artifact via Custody.execute and returns the tx +// reference. Idempotent: if the withdrawal is already executed it returns the +// prior tx hash without re-submitting. +func (f *WithdrawalFinalizer) Submit(ctx context.Context, merged []byte) (core.TxRef, error) { + var m evmMerged + if err := json.Unmarshal(merged, &m); err != nil { + return core.TxRef{}, fmt.Errorf("decode merged: %w", err) + } + wid, err := decodeHex32(m.WithdrawalID) + if err != nil { + return core.TxRef{}, err + } + if txHash, executed, err := f.VerifyExecution(ctx, wid); err != nil { + return core.TxRef{}, err + } else if executed { + return core.TxRef{Hash: txHash, Raw: common.Hash(txHash).Hex()}, nil + } + + to := common.HexToAddress(m.To) + asset := common.HexToAddress(m.Asset) + amount, ok := new(big.Int).SetString(m.Amount, 10) + if !ok { + return core.TxRef{}, fmt.Errorf("bad amount %q", m.Amount) + } + sigs := make([][]byte, len(m.Sigs)) + for i, s := range m.Sigs { + b, err := hex.DecodeString(s) + if err != nil { + return core.TxRef{}, fmt.Errorf("decode sig %d: %w", i, err) + } + sigs[i] = b + } + + opts, _, err := signerTransactOpts(ctx, f.client, f.signer) + if err != nil { + return core.TxRef{}, err + } + if err := f.applyFees(ctx, opts); err != nil { + return core.TxRef{}, err + } + if err := f.estimateGas(ctx, opts, to, asset, amount, wid, sigs); err != nil { + return core.TxRef{}, err + } + tx, err := f.custody.Execute(opts, to, asset, amount, wid, sigs) + if err != nil { + return core.TxRef{}, fmt.Errorf("execute: %w", err) + } + return core.TxRef{Hash: tx.Hash(), Raw: tx.Hash().Hex()}, nil +} + +// VerifyExecution reads Custody.executed(id) and, when set, looks up the +// Executed event's tx hash within the lookback window. +func (f *WithdrawalFinalizer) VerifyExecution(ctx context.Context, withdrawalID [32]byte) ([32]byte, bool, error) { + executed, err := f.custody.Executed(&bind.CallOpts{Context: ctx}, withdrawalID) + if err != nil { + return [32]byte{}, false, fmt.Errorf("check executed: %w", err) + } + if !executed { + return [32]byte{}, false, nil + } + head, err := f.client.BlockNumber(ctx) + if err != nil { + return [32]byte{}, true, nil // executed; hash unknown + } + var from uint64 + if head > executedLookupWindow { + from = head - executedLookupWindow + } + it, err := f.custody.FilterExecuted(&bind.FilterOpts{Context: ctx, Start: from, End: &head}, [][32]byte{withdrawalID}, nil) + if err != nil { + return [32]byte{}, true, nil + } + defer it.Close() + if it.Next() { + return it.Event.Raw.TxHash, true, nil + } + return [32]byte{}, true, nil +} + +// --- helpers --- + +func packedFromOp(op *core.WithdrawalOp, withdrawalID [32]byte) evmPacked { + return evmPacked{ + To: common.HexToAddress(op.Recipient).Hex(), + Asset: common.HexToAddress(op.L1Asset).Hex(), + Amount: op.Amount.BigInt().String(), + WithdrawalID: hex.EncodeToString(withdrawalID[:]), + } +} + +// digest computes the Custody.execute signing digest: +// keccak256(abi.encode(chainId, vault, to, asset, amount, withdrawalId)). +func (f *WithdrawalFinalizer) digest(p evmPacked) (common.Hash, error) { + amount, ok := new(big.Int).SetString(p.Amount, 10) + if !ok { + return common.Hash{}, fmt.Errorf("bad amount %q", p.Amount) + } + wid, err := decodeHex32(p.WithdrawalID) + if err != nil { + return common.Hash{}, err + } + return crypto.Keccak256Hash( + common.LeftPadBytes(new(big.Int).SetUint64(f.chainID).Bytes(), 32), + common.LeftPadBytes(f.vaultAddr.Bytes(), 32), + common.LeftPadBytes(common.HexToAddress(p.To).Bytes(), 32), + common.LeftPadBytes(common.HexToAddress(p.Asset).Bytes(), 32), + common.LeftPadBytes(amount.Bytes(), 32), + wid[:], + ), nil +} + +func (f *WithdrawalFinalizer) liveQuorum(ctx context.Context) ([]common.Address, int, error) { + signers, err := f.custody.Signers(&bind.CallOpts{Context: ctx}) + if err != nil { + return nil, 0, fmt.Errorf("read signers: %w", err) + } + thr, err := f.custody.Threshold(&bind.CallOpts{Context: ctx}) + if err != nil { + return nil, 0, fmt.Errorf("read threshold: %w", err) + } + if !thr.IsInt64() || thr.Int64() <= 0 || thr.Int64() > int64(len(signers)) { + return nil, 0, fmt.Errorf("on-chain threshold %s out of range for %d signers", thr, len(signers)) + } + return signers, int(thr.Int64()), nil +} + +func (f *WithdrawalFinalizer) applyFees(ctx context.Context, opts *bind.TransactOpts) error { + tip := gweiToWei(f.fees.TipGwei) + cap := gweiToWei(f.fees.CapGwei) + head, err := f.client.HeaderByNumber(ctx, nil) + if err != nil { + return fmt.Errorf("fee head: %w", err) + } + if head.BaseFee == nil { + price, err := f.client.SuggestGasPrice(ctx) + if err != nil { + return fmt.Errorf("suggest gas price: %w", err) + } + if cap.Sign() > 0 && price.Cmp(cap) > 0 { + return fmt.Errorf("gas price %s exceeds cap", price) + } + opts.GasPrice = price + return nil + } + maxFee := new(big.Int).Add(new(big.Int).Mul(head.BaseFee, big.NewInt(2)), tip) + if cap.Sign() > 0 && maxFee.Cmp(cap) > 0 { + return fmt.Errorf("max fee %s exceeds cap", maxFee) + } + opts.GasTipCap = tip + opts.GasFeeCap = maxFee + return nil +} + +func (f *WithdrawalFinalizer) estimateGas(ctx context.Context, opts *bind.TransactOpts, to, asset common.Address, amount *big.Int, withdrawalID [32]byte, sigs [][]byte) error { + abi, err := CustodyMetaData.GetAbi() + if err != nil { + return fmt.Errorf("parse ABI: %w", err) + } + data, err := abi.Pack("execute", to, asset, amount, withdrawalID, sigs) + if err != nil { + return fmt.Errorf("pack execute calldata: %w", err) + } + est, err := f.client.EstimateGas(ctx, ethereum.CallMsg{ + From: f.signerAddr, + To: &f.vaultAddr, + Data: data, + GasTipCap: opts.GasTipCap, + GasFeeCap: opts.GasFeeCap, + GasPrice: opts.GasPrice, + }) + if err != nil { + return fmt.Errorf("estimate gas: %w", err) + } + opts.GasLimit = uint64(float64(est) * f.fees.gasLimitMultiplier()) + return nil +} + +func gweiToWei(g float64) *big.Int { + if g <= 0 { + return new(big.Int) + } + wei, _ := new(big.Float).Mul(big.NewFloat(g), big.NewFloat(1e9)).Int(nil) + return wei +} + +func decodeHex32(s string) ([32]byte, error) { + b, err := hex.DecodeString(s) + if err != nil || len(b) != 32 { + return [32]byte{}, fmt.Errorf("bad 32-byte hex %q (len=%d): %v", s, len(b), err) + } + var out [32]byte + copy(out[:], b) + return out, nil +} diff --git a/pkg/blockchain/evm/yellowtoken_abi.go b/pkg/blockchain/evm/yellowtoken_abi.go new file mode 100644 index 0000000..5156130 --- /dev/null +++ b/pkg/blockchain/evm/yellowtoken_abi.go @@ -0,0 +1,1077 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package evm + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// YellowTokenMetaData contains all meta data concerning the YellowToken contract. +var YellowTokenMetaData = &bind.MetaData{ + ABI: "[{\"type\":\"constructor\",\"inputs\":[{\"name\":\"treasury\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"DOMAIN_SEPARATOR\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"SUPPLY_CAP\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"allowance\",\"inputs\":[{\"name\":\"owner\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"spender\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"approve\",\"inputs\":[{\"name\":\"spender\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"value\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"balanceOf\",\"inputs\":[{\"name\":\"account\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"decimals\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint8\",\"internalType\":\"uint8\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"eip712Domain\",\"inputs\":[],\"outputs\":[{\"name\":\"fields\",\"type\":\"bytes1\",\"internalType\":\"bytes1\"},{\"name\":\"name\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"version\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"chainId\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"verifyingContract\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"salt\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"extensions\",\"type\":\"uint256[]\",\"internalType\":\"uint256[]\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"name\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"string\",\"internalType\":\"string\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"nonces\",\"inputs\":[{\"name\":\"owner\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"permit\",\"inputs\":[{\"name\":\"owner\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"spender\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"value\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"deadline\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"v\",\"type\":\"uint8\",\"internalType\":\"uint8\"},{\"name\":\"r\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"s\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"symbol\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"string\",\"internalType\":\"string\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"totalSupply\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"transfer\",\"inputs\":[{\"name\":\"to\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"value\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"transferFrom\",\"inputs\":[{\"name\":\"from\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"to\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"value\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"stateMutability\":\"nonpayable\"},{\"type\":\"event\",\"name\":\"Approval\",\"inputs\":[{\"name\":\"owner\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"spender\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"value\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"EIP712DomainChanged\",\"inputs\":[],\"anonymous\":false},{\"type\":\"event\",\"name\":\"Transfer\",\"inputs\":[{\"name\":\"from\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"to\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"value\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"}],\"anonymous\":false},{\"type\":\"error\",\"name\":\"ECDSAInvalidSignature\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ECDSAInvalidSignatureLength\",\"inputs\":[{\"name\":\"length\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]},{\"type\":\"error\",\"name\":\"ECDSAInvalidSignatureS\",\"inputs\":[{\"name\":\"s\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}]},{\"type\":\"error\",\"name\":\"ERC20InsufficientAllowance\",\"inputs\":[{\"name\":\"spender\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"allowance\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"needed\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]},{\"type\":\"error\",\"name\":\"ERC20InsufficientBalance\",\"inputs\":[{\"name\":\"sender\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"balance\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"needed\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]},{\"type\":\"error\",\"name\":\"ERC20InvalidApprover\",\"inputs\":[{\"name\":\"approver\",\"type\":\"address\",\"internalType\":\"address\"}]},{\"type\":\"error\",\"name\":\"ERC20InvalidReceiver\",\"inputs\":[{\"name\":\"receiver\",\"type\":\"address\",\"internalType\":\"address\"}]},{\"type\":\"error\",\"name\":\"ERC20InvalidSender\",\"inputs\":[{\"name\":\"sender\",\"type\":\"address\",\"internalType\":\"address\"}]},{\"type\":\"error\",\"name\":\"ERC20InvalidSpender\",\"inputs\":[{\"name\":\"spender\",\"type\":\"address\",\"internalType\":\"address\"}]},{\"type\":\"error\",\"name\":\"ERC2612ExpiredSignature\",\"inputs\":[{\"name\":\"deadline\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]},{\"type\":\"error\",\"name\":\"ERC2612InvalidSigner\",\"inputs\":[{\"name\":\"signer\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"owner\",\"type\":\"address\",\"internalType\":\"address\"}]},{\"type\":\"error\",\"name\":\"InvalidAccountNonce\",\"inputs\":[{\"name\":\"account\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"currentNonce\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]},{\"type\":\"error\",\"name\":\"InvalidAddress\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"InvalidShortString\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"StringTooLong\",\"inputs\":[{\"name\":\"str\",\"type\":\"string\",\"internalType\":\"string\"}]}]", + Bin: "0x61016080604052346105045760208161140980380380916100208285610508565b83398101031261050457516001600160a01b038116908190036105045760405161004b604082610508565b60068152602081016559656c6c6f7760d01b81526040519061006e604083610508565b600682526559656c6c6f7760d01b602083015260405192610090604085610508565b600684526559454c4c4f5760d01b6020850152604051936100b2604086610508565b60018552603160f81b60208601908152845190946001600160401b0382116103ff5760035490600182811c921680156104fa575b60208310146103e15781601f849311610484575b50602090601f831160011461041e575f92610413575b50508160011b915f199060031b1c1916176003555b8051906001600160401b0382116103ff5760045490600182811c921680156103f5575b60208310146103e15781601f84931161036b575b50602090601f8311600114610305575f926102fa575b50508160011b915f199060031b1c1916176004555b6101908161052b565b6101205261019d846106be565b61014052519020918260e05251902080610100524660a0526040519060208201927f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f8452604083015260608201524660808201523060a082015260a0815261020660c082610508565b5190206080523060c05280156102eb576002546b204fce5e3e2502611000000081018091116102d757600255805f525f60205260405f206b204fce5e3e2502611000000081540190555f7fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef60206040516b204fce5e3e250261100000008152a3604051610c069081610803823960805181610925015260a051816109e2015260c051816108ef015260e051816109740152610100518161099a0152610120518161037a015261014051816103a30152f35b634e487b7160e01b5f52601160045260245ffd5b63e6c4247b60e01b5f5260045ffd5b015190505f80610172565b60045f9081528281209350601f198516905b818110610353575090846001959493921061033b575b505050811b01600455610187565b01515f1960f88460031b161c191690555f808061032d565b92936020600181928786015181550195019301610317565b8281111561015c5760045f52909150601f830160051c7f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b602085106103d9575b849392601f0160051c82900391015f5b8281106103c957505061015c565b5f818301558594506001016103bb565b5f91506103ab565b634e487b7160e01b5f52602260045260245ffd5b91607f1691610148565b634e487b7160e01b5f52604160045260245ffd5b015190505f80610110565b60035f9081528281209350601f198516905b81811061046c5750908460019594939210610454575b505050811b01600355610125565b01515f1960f88460031b161c191690555f8080610446565b92936020600181928786015181550195019301610430565b828111156100fa5760035f52909150601f830160051c7fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b602085106104f2575b849392601f0160051c82900391015f5b8281106104e25750506100fa565b5f818301558594506001016104d4565b5f91506104c4565b91607f16916100e6565b5f80fd5b601f909101601f19168101906001600160401b038211908210176103ff57604052565b908151602081105f146105a5575090601f815111610565576020815191015160208210610556571790565b5f198260200360031b1b161790565b604460209160405192839163305a27a960e01b83528160048401528051918291826024860152018484015e5f828201840152601f01601f19168101030190fd5b6001600160401b0381116103ff57600554600181811c911680156106b4575b60208210146103e157601f8111610675575b50602092601f821160011461061457928192935f92610609575b50508160011b915f199060031b1c19161760055560ff90565b015190505f806105f0565b601f1982169360055f52805f20915f5b86811061065d5750836001959610610645575b505050811b0160055560ff90565b01515f1960f88460031b161c191690555f8080610637565b91926020600181928685015181550194019201610624565b818111156105d65760055f5260205f20601f80840160051c809201920160051c03905f5b8281106106a75750506105d6565b5f82820155600101610699565b90607f16906105c4565b908151602081105f146106e9575090601f815111610565576020815191015160208210610556571790565b6001600160401b0381116103ff57600654600181811c911680156107f8575b60208210146103e157601f81116107b9575b50602092601f821160011461075857928192935f9261074d575b50508160011b915f199060031b1c19161760065560ff90565b015190505f80610734565b601f1982169360065f52805f20915f5b8681106107a15750836001959610610789575b505050811b0160065560ff90565b01515f1960f88460031b161c191690555f808061077b565b91926020600181928685015181550194019201610768565b8181111561071a5760065f5260205f20601f80840160051c809201920160051c03905f5b8281106107eb57505061071a565b5f828201556001016107dd565b90607f169061070856fe6080806040526004361015610012575f80fd5b5f3560e01c90816306fdde031461064e57508063095ea7b3146106285780630cfccc831461060257806318160ddd146105e557806323b872dd14610506578063313ce567146104eb5780633644e515146104c957806370a08231146104925780637ecebe001461045a57806384b0196e1461036257806395d89b4114610280578063a9059cbb1461024f578063d505accf1461010a5763dd62ed3e146100b6575f80fd5b34610106576040366003190112610106576100cf610714565b6100d761072a565b6001600160a01b039182165f908152600160209081526040808320949093168252928352819020549051908152f35b5f80fd5b346101065760e036600319011261010657610123610714565b61012b61072a565b604435906064359260843560ff811681036101065784421161023c576101ff6102089160018060a01b03841696875f52600760205260405f20908154916001830190556040519060208201927f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c984528a604084015260018060a01b038916606084015289608084015260a083015260c082015260c081526101cd60e0826107f9565b5190206101d86108ec565b906040519161190160f01b83526002830152602282015260c43591604260a4359220610b05565b90929192610b92565b6001600160a01b031684810361022557506102239350610a08565b005b84906325c0072360e11b5f5260045260245260445ffd5b8463313c898160e11b5f5260045260245ffd5b346101065760403660031901126101065761027561026b610714565b602435903361082f565b602060405160018152f35b34610106575f366003190112610106576040515f6004546102a081610740565b808452906001811690811561033e57506001146102e0575b6102dc836102c8818503826107f9565b6040519182916020835260208301906106f0565b0390f35b60045f9081527f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b939250905b808210610324575090915081016020016102c86102b8565b91926001816020925483858801015201910190929161030c565b60ff191660208086019190915291151560051b840190910191506102c890506102b8565b34610106575f366003190112610106576103fe61039e7f0000000000000000000000000000000000000000000000000000000000000000610a6b565b6103c77f0000000000000000000000000000000000000000000000000000000000000000610ace565b602061040c604051926103da83856107f9565b5f84525f368137604051958695600f60f81b875260e08588015260e08701906106f0565b9085820360408701526106f0565b4660608501523060808501525f60a085015283810360c08501528180845192838152019301915f5b82811061044357505050500390f35b835185528695509381019392810192600101610434565b34610106576020366003190112610106576001600160a01b0361047b610714565b165f526007602052602060405f2054604051908152f35b34610106576020366003190112610106576001600160a01b036104b3610714565b165f525f602052602060405f2054604051908152f35b34610106575f3660031901126101065760206104e36108ec565b604051908152f35b34610106575f36600319011261010657602060405160128152f35b346101065760603660031901126101065761051f610714565b61052761072a565b6001600160a01b0382165f818152600160209081526040808320338452909152902054909260443592915f198110610565575b50610275935061082f565b8381106105ca5784156105b75733156105a457610275945f52600160205260405f2060018060a01b0333165f526020528360405f20910390558461055a565b634a1406b160e11b5f525f60045260245ffd5b63e602df0560e01b5f525f60045260245ffd5b8390637dc7a0d960e11b5f523360045260245260445260645ffd5b34610106575f366003190112610106576020600254604051908152f35b34610106575f3660031901126101065760206040516b204fce5e3e250261100000008152f35b3461010657604036600319011261010657610275610644610714565b6024359033610a08565b34610106575f366003190112610106575f60035461066b81610740565b808452906001811690811561033e5750600114610692576102dc836102c8818503826107f9565b60035f9081527fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b939250905b8082106106d6575090915081016020016102c86102b8565b9192600181602092548385880101520191019092916106be565b805180835260209291819084018484015e5f828201840152601f01601f1916010190565b600435906001600160a01b038216820361010657565b602435906001600160a01b038216820361010657565b90600182811c9216801561076e575b602083101461075a57565b634e487b7160e01b5f52602260045260245ffd5b91607f169161074f565b5f929181549161078783610740565b80835292600181169081156107dc57506001146107a357505050565b5f9081526020812093945091925b8383106107c2575060209250010190565b6001816020929493945483858701015201910191906107b1565b915050602093945060ff929192191683830152151560051b010190565b90601f8019910116810190811067ffffffffffffffff82111761081b57604052565b634e487b7160e01b5f52604160045260245ffd5b6001600160a01b03169081156108d9576001600160a01b03169182156108c657815f525f60205260405f20548181106108ad57817fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef92602092855f525f84520360405f2055845f525f825260405f20818154019055604051908152a3565b8263391434e360e21b5f5260045260245260445260645ffd5b63ec442f0560e01b5f525f60045260245ffd5b634b637e8f60e11b5f525f60045260245ffd5b307f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031614806109df575b15610947577f000000000000000000000000000000000000000000000000000000000000000090565b60405160208101907f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f82527f000000000000000000000000000000000000000000000000000000000000000060408201527f000000000000000000000000000000000000000000000000000000000000000060608201524660808201523060a082015260a081526109d960c0826107f9565b51902090565b507f0000000000000000000000000000000000000000000000000000000000000000461461091e565b6001600160a01b03169081156105b7576001600160a01b03169182156105a45760207f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92591835f526001825260405f20855f5282528060405f2055604051908152a3565b60ff8114610ab15760ff811690601f8211610aa25760405191610a8f6040846107f9565b6020808452838101919036833783525290565b632cd44ac360e21b5f5260045ffd5b50604051610acb81610ac4816005610778565b03826107f9565b90565b60ff8114610af25760ff811690601f8211610aa25760405191610a8f6040846107f9565b50604051610acb81610ac4816006610778565b91907f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08411610b87579160209360809260ff5f9560405194855216868401526040830152606082015282805260015afa15610b7c575f516001600160a01b03811615610b7257905f905f90565b505f906001905f90565b6040513d5f823e3d90fd5b5050505f9160039190565b6004811015610bf25780610ba4575050565b60018103610bbb5763f645eedf60e01b5f5260045ffd5b60028103610bd6575063fce698f760e01b5f5260045260245ffd5b600314610be05750565b6335e2f38360e21b5f5260045260245ffd5b634e487b7160e01b5f52602160045260245ffd", +} + +// YellowTokenABI is the input ABI used to generate the binding from. +// Deprecated: Use YellowTokenMetaData.ABI instead. +var YellowTokenABI = YellowTokenMetaData.ABI + +// YellowTokenBin is the compiled bytecode used for deploying new contracts. +// Deprecated: Use YellowTokenMetaData.Bin instead. +var YellowTokenBin = YellowTokenMetaData.Bin + +// DeployYellowToken deploys a new Ethereum contract, binding an instance of YellowToken to it. +func DeployYellowToken(auth *bind.TransactOpts, backend bind.ContractBackend, treasury common.Address) (common.Address, *types.Transaction, *YellowToken, error) { + parsed, err := YellowTokenMetaData.GetAbi() + if err != nil { + return common.Address{}, nil, nil, err + } + if parsed == nil { + return common.Address{}, nil, nil, errors.New("GetABI returned nil") + } + + address, tx, contract, err := bind.DeployContract(auth, *parsed, common.FromHex(YellowTokenBin), backend, treasury) + if err != nil { + return common.Address{}, nil, nil, err + } + return address, tx, &YellowToken{YellowTokenCaller: YellowTokenCaller{contract: contract}, YellowTokenTransactor: YellowTokenTransactor{contract: contract}, YellowTokenFilterer: YellowTokenFilterer{contract: contract}}, nil +} + +// YellowToken is an auto generated Go binding around an Ethereum contract. +type YellowToken struct { + YellowTokenCaller // Read-only binding to the contract + YellowTokenTransactor // Write-only binding to the contract + YellowTokenFilterer // Log filterer for contract events +} + +// YellowTokenCaller is an auto generated read-only Go binding around an Ethereum contract. +type YellowTokenCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// YellowTokenTransactor is an auto generated write-only Go binding around an Ethereum contract. +type YellowTokenTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// YellowTokenFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type YellowTokenFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// YellowTokenSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type YellowTokenSession struct { + Contract *YellowToken // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// YellowTokenCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type YellowTokenCallerSession struct { + Contract *YellowTokenCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// YellowTokenTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type YellowTokenTransactorSession struct { + Contract *YellowTokenTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// YellowTokenRaw is an auto generated low-level Go binding around an Ethereum contract. +type YellowTokenRaw struct { + Contract *YellowToken // Generic contract binding to access the raw methods on +} + +// YellowTokenCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type YellowTokenCallerRaw struct { + Contract *YellowTokenCaller // Generic read-only contract binding to access the raw methods on +} + +// YellowTokenTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type YellowTokenTransactorRaw struct { + Contract *YellowTokenTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewYellowToken creates a new instance of YellowToken, bound to a specific deployed contract. +func NewYellowToken(address common.Address, backend bind.ContractBackend) (*YellowToken, error) { + contract, err := bindYellowToken(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &YellowToken{YellowTokenCaller: YellowTokenCaller{contract: contract}, YellowTokenTransactor: YellowTokenTransactor{contract: contract}, YellowTokenFilterer: YellowTokenFilterer{contract: contract}}, nil +} + +// NewYellowTokenCaller creates a new read-only instance of YellowToken, bound to a specific deployed contract. +func NewYellowTokenCaller(address common.Address, caller bind.ContractCaller) (*YellowTokenCaller, error) { + contract, err := bindYellowToken(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &YellowTokenCaller{contract: contract}, nil +} + +// NewYellowTokenTransactor creates a new write-only instance of YellowToken, bound to a specific deployed contract. +func NewYellowTokenTransactor(address common.Address, transactor bind.ContractTransactor) (*YellowTokenTransactor, error) { + contract, err := bindYellowToken(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &YellowTokenTransactor{contract: contract}, nil +} + +// NewYellowTokenFilterer creates a new log filterer instance of YellowToken, bound to a specific deployed contract. +func NewYellowTokenFilterer(address common.Address, filterer bind.ContractFilterer) (*YellowTokenFilterer, error) { + contract, err := bindYellowToken(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &YellowTokenFilterer{contract: contract}, nil +} + +// bindYellowToken binds a generic wrapper to an already deployed contract. +func bindYellowToken(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := YellowTokenMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_YellowToken *YellowTokenRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _YellowToken.Contract.YellowTokenCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_YellowToken *YellowTokenRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _YellowToken.Contract.YellowTokenTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_YellowToken *YellowTokenRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _YellowToken.Contract.YellowTokenTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_YellowToken *YellowTokenCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _YellowToken.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_YellowToken *YellowTokenTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _YellowToken.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_YellowToken *YellowTokenTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _YellowToken.Contract.contract.Transact(opts, method, params...) +} + +// DOMAINSEPARATOR is a free data retrieval call binding the contract method 0x3644e515. +// +// Solidity: function DOMAIN_SEPARATOR() view returns(bytes32) +func (_YellowToken *YellowTokenCaller) DOMAINSEPARATOR(opts *bind.CallOpts) ([32]byte, error) { + var out []interface{} + err := _YellowToken.contract.Call(opts, &out, "DOMAIN_SEPARATOR") + + if err != nil { + return *new([32]byte), err + } + + out0 := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) + + return out0, err + +} + +// DOMAINSEPARATOR is a free data retrieval call binding the contract method 0x3644e515. +// +// Solidity: function DOMAIN_SEPARATOR() view returns(bytes32) +func (_YellowToken *YellowTokenSession) DOMAINSEPARATOR() ([32]byte, error) { + return _YellowToken.Contract.DOMAINSEPARATOR(&_YellowToken.CallOpts) +} + +// DOMAINSEPARATOR is a free data retrieval call binding the contract method 0x3644e515. +// +// Solidity: function DOMAIN_SEPARATOR() view returns(bytes32) +func (_YellowToken *YellowTokenCallerSession) DOMAINSEPARATOR() ([32]byte, error) { + return _YellowToken.Contract.DOMAINSEPARATOR(&_YellowToken.CallOpts) +} + +// SUPPLYCAP is a free data retrieval call binding the contract method 0x0cfccc83. +// +// Solidity: function SUPPLY_CAP() view returns(uint256) +func (_YellowToken *YellowTokenCaller) SUPPLYCAP(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _YellowToken.contract.Call(opts, &out, "SUPPLY_CAP") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// SUPPLYCAP is a free data retrieval call binding the contract method 0x0cfccc83. +// +// Solidity: function SUPPLY_CAP() view returns(uint256) +func (_YellowToken *YellowTokenSession) SUPPLYCAP() (*big.Int, error) { + return _YellowToken.Contract.SUPPLYCAP(&_YellowToken.CallOpts) +} + +// SUPPLYCAP is a free data retrieval call binding the contract method 0x0cfccc83. +// +// Solidity: function SUPPLY_CAP() view returns(uint256) +func (_YellowToken *YellowTokenCallerSession) SUPPLYCAP() (*big.Int, error) { + return _YellowToken.Contract.SUPPLYCAP(&_YellowToken.CallOpts) +} + +// Allowance is a free data retrieval call binding the contract method 0xdd62ed3e. +// +// Solidity: function allowance(address owner, address spender) view returns(uint256) +func (_YellowToken *YellowTokenCaller) Allowance(opts *bind.CallOpts, owner common.Address, spender common.Address) (*big.Int, error) { + var out []interface{} + err := _YellowToken.contract.Call(opts, &out, "allowance", owner, spender) + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// Allowance is a free data retrieval call binding the contract method 0xdd62ed3e. +// +// Solidity: function allowance(address owner, address spender) view returns(uint256) +func (_YellowToken *YellowTokenSession) Allowance(owner common.Address, spender common.Address) (*big.Int, error) { + return _YellowToken.Contract.Allowance(&_YellowToken.CallOpts, owner, spender) +} + +// Allowance is a free data retrieval call binding the contract method 0xdd62ed3e. +// +// Solidity: function allowance(address owner, address spender) view returns(uint256) +func (_YellowToken *YellowTokenCallerSession) Allowance(owner common.Address, spender common.Address) (*big.Int, error) { + return _YellowToken.Contract.Allowance(&_YellowToken.CallOpts, owner, spender) +} + +// BalanceOf is a free data retrieval call binding the contract method 0x70a08231. +// +// Solidity: function balanceOf(address account) view returns(uint256) +func (_YellowToken *YellowTokenCaller) BalanceOf(opts *bind.CallOpts, account common.Address) (*big.Int, error) { + var out []interface{} + err := _YellowToken.contract.Call(opts, &out, "balanceOf", account) + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// BalanceOf is a free data retrieval call binding the contract method 0x70a08231. +// +// Solidity: function balanceOf(address account) view returns(uint256) +func (_YellowToken *YellowTokenSession) BalanceOf(account common.Address) (*big.Int, error) { + return _YellowToken.Contract.BalanceOf(&_YellowToken.CallOpts, account) +} + +// BalanceOf is a free data retrieval call binding the contract method 0x70a08231. +// +// Solidity: function balanceOf(address account) view returns(uint256) +func (_YellowToken *YellowTokenCallerSession) BalanceOf(account common.Address) (*big.Int, error) { + return _YellowToken.Contract.BalanceOf(&_YellowToken.CallOpts, account) +} + +// Decimals is a free data retrieval call binding the contract method 0x313ce567. +// +// Solidity: function decimals() view returns(uint8) +func (_YellowToken *YellowTokenCaller) Decimals(opts *bind.CallOpts) (uint8, error) { + var out []interface{} + err := _YellowToken.contract.Call(opts, &out, "decimals") + + if err != nil { + return *new(uint8), err + } + + out0 := *abi.ConvertType(out[0], new(uint8)).(*uint8) + + return out0, err + +} + +// Decimals is a free data retrieval call binding the contract method 0x313ce567. +// +// Solidity: function decimals() view returns(uint8) +func (_YellowToken *YellowTokenSession) Decimals() (uint8, error) { + return _YellowToken.Contract.Decimals(&_YellowToken.CallOpts) +} + +// Decimals is a free data retrieval call binding the contract method 0x313ce567. +// +// Solidity: function decimals() view returns(uint8) +func (_YellowToken *YellowTokenCallerSession) Decimals() (uint8, error) { + return _YellowToken.Contract.Decimals(&_YellowToken.CallOpts) +} + +// Eip712Domain is a free data retrieval call binding the contract method 0x84b0196e. +// +// Solidity: function eip712Domain() view returns(bytes1 fields, string name, string version, uint256 chainId, address verifyingContract, bytes32 salt, uint256[] extensions) +func (_YellowToken *YellowTokenCaller) Eip712Domain(opts *bind.CallOpts) (struct { + Fields [1]byte + Name string + Version string + ChainId *big.Int + VerifyingContract common.Address + Salt [32]byte + Extensions []*big.Int +}, error) { + var out []interface{} + err := _YellowToken.contract.Call(opts, &out, "eip712Domain") + + outstruct := new(struct { + Fields [1]byte + Name string + Version string + ChainId *big.Int + VerifyingContract common.Address + Salt [32]byte + Extensions []*big.Int + }) + if err != nil { + return *outstruct, err + } + + outstruct.Fields = *abi.ConvertType(out[0], new([1]byte)).(*[1]byte) + outstruct.Name = *abi.ConvertType(out[1], new(string)).(*string) + outstruct.Version = *abi.ConvertType(out[2], new(string)).(*string) + outstruct.ChainId = *abi.ConvertType(out[3], new(*big.Int)).(**big.Int) + outstruct.VerifyingContract = *abi.ConvertType(out[4], new(common.Address)).(*common.Address) + outstruct.Salt = *abi.ConvertType(out[5], new([32]byte)).(*[32]byte) + outstruct.Extensions = *abi.ConvertType(out[6], new([]*big.Int)).(*[]*big.Int) + + return *outstruct, err + +} + +// Eip712Domain is a free data retrieval call binding the contract method 0x84b0196e. +// +// Solidity: function eip712Domain() view returns(bytes1 fields, string name, string version, uint256 chainId, address verifyingContract, bytes32 salt, uint256[] extensions) +func (_YellowToken *YellowTokenSession) Eip712Domain() (struct { + Fields [1]byte + Name string + Version string + ChainId *big.Int + VerifyingContract common.Address + Salt [32]byte + Extensions []*big.Int +}, error) { + return _YellowToken.Contract.Eip712Domain(&_YellowToken.CallOpts) +} + +// Eip712Domain is a free data retrieval call binding the contract method 0x84b0196e. +// +// Solidity: function eip712Domain() view returns(bytes1 fields, string name, string version, uint256 chainId, address verifyingContract, bytes32 salt, uint256[] extensions) +func (_YellowToken *YellowTokenCallerSession) Eip712Domain() (struct { + Fields [1]byte + Name string + Version string + ChainId *big.Int + VerifyingContract common.Address + Salt [32]byte + Extensions []*big.Int +}, error) { + return _YellowToken.Contract.Eip712Domain(&_YellowToken.CallOpts) +} + +// Name is a free data retrieval call binding the contract method 0x06fdde03. +// +// Solidity: function name() view returns(string) +func (_YellowToken *YellowTokenCaller) Name(opts *bind.CallOpts) (string, error) { + var out []interface{} + err := _YellowToken.contract.Call(opts, &out, "name") + + if err != nil { + return *new(string), err + } + + out0 := *abi.ConvertType(out[0], new(string)).(*string) + + return out0, err + +} + +// Name is a free data retrieval call binding the contract method 0x06fdde03. +// +// Solidity: function name() view returns(string) +func (_YellowToken *YellowTokenSession) Name() (string, error) { + return _YellowToken.Contract.Name(&_YellowToken.CallOpts) +} + +// Name is a free data retrieval call binding the contract method 0x06fdde03. +// +// Solidity: function name() view returns(string) +func (_YellowToken *YellowTokenCallerSession) Name() (string, error) { + return _YellowToken.Contract.Name(&_YellowToken.CallOpts) +} + +// Nonces is a free data retrieval call binding the contract method 0x7ecebe00. +// +// Solidity: function nonces(address owner) view returns(uint256) +func (_YellowToken *YellowTokenCaller) Nonces(opts *bind.CallOpts, owner common.Address) (*big.Int, error) { + var out []interface{} + err := _YellowToken.contract.Call(opts, &out, "nonces", owner) + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// Nonces is a free data retrieval call binding the contract method 0x7ecebe00. +// +// Solidity: function nonces(address owner) view returns(uint256) +func (_YellowToken *YellowTokenSession) Nonces(owner common.Address) (*big.Int, error) { + return _YellowToken.Contract.Nonces(&_YellowToken.CallOpts, owner) +} + +// Nonces is a free data retrieval call binding the contract method 0x7ecebe00. +// +// Solidity: function nonces(address owner) view returns(uint256) +func (_YellowToken *YellowTokenCallerSession) Nonces(owner common.Address) (*big.Int, error) { + return _YellowToken.Contract.Nonces(&_YellowToken.CallOpts, owner) +} + +// Symbol is a free data retrieval call binding the contract method 0x95d89b41. +// +// Solidity: function symbol() view returns(string) +func (_YellowToken *YellowTokenCaller) Symbol(opts *bind.CallOpts) (string, error) { + var out []interface{} + err := _YellowToken.contract.Call(opts, &out, "symbol") + + if err != nil { + return *new(string), err + } + + out0 := *abi.ConvertType(out[0], new(string)).(*string) + + return out0, err + +} + +// Symbol is a free data retrieval call binding the contract method 0x95d89b41. +// +// Solidity: function symbol() view returns(string) +func (_YellowToken *YellowTokenSession) Symbol() (string, error) { + return _YellowToken.Contract.Symbol(&_YellowToken.CallOpts) +} + +// Symbol is a free data retrieval call binding the contract method 0x95d89b41. +// +// Solidity: function symbol() view returns(string) +func (_YellowToken *YellowTokenCallerSession) Symbol() (string, error) { + return _YellowToken.Contract.Symbol(&_YellowToken.CallOpts) +} + +// TotalSupply is a free data retrieval call binding the contract method 0x18160ddd. +// +// Solidity: function totalSupply() view returns(uint256) +func (_YellowToken *YellowTokenCaller) TotalSupply(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _YellowToken.contract.Call(opts, &out, "totalSupply") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// TotalSupply is a free data retrieval call binding the contract method 0x18160ddd. +// +// Solidity: function totalSupply() view returns(uint256) +func (_YellowToken *YellowTokenSession) TotalSupply() (*big.Int, error) { + return _YellowToken.Contract.TotalSupply(&_YellowToken.CallOpts) +} + +// TotalSupply is a free data retrieval call binding the contract method 0x18160ddd. +// +// Solidity: function totalSupply() view returns(uint256) +func (_YellowToken *YellowTokenCallerSession) TotalSupply() (*big.Int, error) { + return _YellowToken.Contract.TotalSupply(&_YellowToken.CallOpts) +} + +// Approve is a paid mutator transaction binding the contract method 0x095ea7b3. +// +// Solidity: function approve(address spender, uint256 value) returns(bool) +func (_YellowToken *YellowTokenTransactor) Approve(opts *bind.TransactOpts, spender common.Address, value *big.Int) (*types.Transaction, error) { + return _YellowToken.contract.Transact(opts, "approve", spender, value) +} + +// Approve is a paid mutator transaction binding the contract method 0x095ea7b3. +// +// Solidity: function approve(address spender, uint256 value) returns(bool) +func (_YellowToken *YellowTokenSession) Approve(spender common.Address, value *big.Int) (*types.Transaction, error) { + return _YellowToken.Contract.Approve(&_YellowToken.TransactOpts, spender, value) +} + +// Approve is a paid mutator transaction binding the contract method 0x095ea7b3. +// +// Solidity: function approve(address spender, uint256 value) returns(bool) +func (_YellowToken *YellowTokenTransactorSession) Approve(spender common.Address, value *big.Int) (*types.Transaction, error) { + return _YellowToken.Contract.Approve(&_YellowToken.TransactOpts, spender, value) +} + +// Permit is a paid mutator transaction binding the contract method 0xd505accf. +// +// Solidity: function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) returns() +func (_YellowToken *YellowTokenTransactor) Permit(opts *bind.TransactOpts, owner common.Address, spender common.Address, value *big.Int, deadline *big.Int, v uint8, r [32]byte, s [32]byte) (*types.Transaction, error) { + return _YellowToken.contract.Transact(opts, "permit", owner, spender, value, deadline, v, r, s) +} + +// Permit is a paid mutator transaction binding the contract method 0xd505accf. +// +// Solidity: function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) returns() +func (_YellowToken *YellowTokenSession) Permit(owner common.Address, spender common.Address, value *big.Int, deadline *big.Int, v uint8, r [32]byte, s [32]byte) (*types.Transaction, error) { + return _YellowToken.Contract.Permit(&_YellowToken.TransactOpts, owner, spender, value, deadline, v, r, s) +} + +// Permit is a paid mutator transaction binding the contract method 0xd505accf. +// +// Solidity: function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) returns() +func (_YellowToken *YellowTokenTransactorSession) Permit(owner common.Address, spender common.Address, value *big.Int, deadline *big.Int, v uint8, r [32]byte, s [32]byte) (*types.Transaction, error) { + return _YellowToken.Contract.Permit(&_YellowToken.TransactOpts, owner, spender, value, deadline, v, r, s) +} + +// Transfer is a paid mutator transaction binding the contract method 0xa9059cbb. +// +// Solidity: function transfer(address to, uint256 value) returns(bool) +func (_YellowToken *YellowTokenTransactor) Transfer(opts *bind.TransactOpts, to common.Address, value *big.Int) (*types.Transaction, error) { + return _YellowToken.contract.Transact(opts, "transfer", to, value) +} + +// Transfer is a paid mutator transaction binding the contract method 0xa9059cbb. +// +// Solidity: function transfer(address to, uint256 value) returns(bool) +func (_YellowToken *YellowTokenSession) Transfer(to common.Address, value *big.Int) (*types.Transaction, error) { + return _YellowToken.Contract.Transfer(&_YellowToken.TransactOpts, to, value) +} + +// Transfer is a paid mutator transaction binding the contract method 0xa9059cbb. +// +// Solidity: function transfer(address to, uint256 value) returns(bool) +func (_YellowToken *YellowTokenTransactorSession) Transfer(to common.Address, value *big.Int) (*types.Transaction, error) { + return _YellowToken.Contract.Transfer(&_YellowToken.TransactOpts, to, value) +} + +// TransferFrom is a paid mutator transaction binding the contract method 0x23b872dd. +// +// Solidity: function transferFrom(address from, address to, uint256 value) returns(bool) +func (_YellowToken *YellowTokenTransactor) TransferFrom(opts *bind.TransactOpts, from common.Address, to common.Address, value *big.Int) (*types.Transaction, error) { + return _YellowToken.contract.Transact(opts, "transferFrom", from, to, value) +} + +// TransferFrom is a paid mutator transaction binding the contract method 0x23b872dd. +// +// Solidity: function transferFrom(address from, address to, uint256 value) returns(bool) +func (_YellowToken *YellowTokenSession) TransferFrom(from common.Address, to common.Address, value *big.Int) (*types.Transaction, error) { + return _YellowToken.Contract.TransferFrom(&_YellowToken.TransactOpts, from, to, value) +} + +// TransferFrom is a paid mutator transaction binding the contract method 0x23b872dd. +// +// Solidity: function transferFrom(address from, address to, uint256 value) returns(bool) +func (_YellowToken *YellowTokenTransactorSession) TransferFrom(from common.Address, to common.Address, value *big.Int) (*types.Transaction, error) { + return _YellowToken.Contract.TransferFrom(&_YellowToken.TransactOpts, from, to, value) +} + +// YellowTokenApprovalIterator is returned from FilterApproval and is used to iterate over the raw logs and unpacked data for Approval events raised by the YellowToken contract. +type YellowTokenApprovalIterator struct { + Event *YellowTokenApproval // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *YellowTokenApprovalIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(YellowTokenApproval) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(YellowTokenApproval) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *YellowTokenApprovalIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *YellowTokenApprovalIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// YellowTokenApproval represents a Approval event raised by the YellowToken contract. +type YellowTokenApproval struct { + Owner common.Address + Spender common.Address + Value *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterApproval is a free log retrieval operation binding the contract event 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925. +// +// Solidity: event Approval(address indexed owner, address indexed spender, uint256 value) +func (_YellowToken *YellowTokenFilterer) FilterApproval(opts *bind.FilterOpts, owner []common.Address, spender []common.Address) (*YellowTokenApprovalIterator, error) { + + var ownerRule []interface{} + for _, ownerItem := range owner { + ownerRule = append(ownerRule, ownerItem) + } + var spenderRule []interface{} + for _, spenderItem := range spender { + spenderRule = append(spenderRule, spenderItem) + } + + logs, sub, err := _YellowToken.contract.FilterLogs(opts, "Approval", ownerRule, spenderRule) + if err != nil { + return nil, err + } + return &YellowTokenApprovalIterator{contract: _YellowToken.contract, event: "Approval", logs: logs, sub: sub}, nil +} + +// WatchApproval is a free log subscription operation binding the contract event 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925. +// +// Solidity: event Approval(address indexed owner, address indexed spender, uint256 value) +func (_YellowToken *YellowTokenFilterer) WatchApproval(opts *bind.WatchOpts, sink chan<- *YellowTokenApproval, owner []common.Address, spender []common.Address) (event.Subscription, error) { + + var ownerRule []interface{} + for _, ownerItem := range owner { + ownerRule = append(ownerRule, ownerItem) + } + var spenderRule []interface{} + for _, spenderItem := range spender { + spenderRule = append(spenderRule, spenderItem) + } + + logs, sub, err := _YellowToken.contract.WatchLogs(opts, "Approval", ownerRule, spenderRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(YellowTokenApproval) + if err := _YellowToken.contract.UnpackLog(event, "Approval", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseApproval is a log parse operation binding the contract event 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925. +// +// Solidity: event Approval(address indexed owner, address indexed spender, uint256 value) +func (_YellowToken *YellowTokenFilterer) ParseApproval(log types.Log) (*YellowTokenApproval, error) { + event := new(YellowTokenApproval) + if err := _YellowToken.contract.UnpackLog(event, "Approval", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// YellowTokenEIP712DomainChangedIterator is returned from FilterEIP712DomainChanged and is used to iterate over the raw logs and unpacked data for EIP712DomainChanged events raised by the YellowToken contract. +type YellowTokenEIP712DomainChangedIterator struct { + Event *YellowTokenEIP712DomainChanged // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *YellowTokenEIP712DomainChangedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(YellowTokenEIP712DomainChanged) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(YellowTokenEIP712DomainChanged) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *YellowTokenEIP712DomainChangedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *YellowTokenEIP712DomainChangedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// YellowTokenEIP712DomainChanged represents a EIP712DomainChanged event raised by the YellowToken contract. +type YellowTokenEIP712DomainChanged struct { + Raw types.Log // Blockchain specific contextual infos +} + +// FilterEIP712DomainChanged is a free log retrieval operation binding the contract event 0x0a6387c9ea3628b88a633bb4f3b151770f70085117a15f9bf3787cda53f13d31. +// +// Solidity: event EIP712DomainChanged() +func (_YellowToken *YellowTokenFilterer) FilterEIP712DomainChanged(opts *bind.FilterOpts) (*YellowTokenEIP712DomainChangedIterator, error) { + + logs, sub, err := _YellowToken.contract.FilterLogs(opts, "EIP712DomainChanged") + if err != nil { + return nil, err + } + return &YellowTokenEIP712DomainChangedIterator{contract: _YellowToken.contract, event: "EIP712DomainChanged", logs: logs, sub: sub}, nil +} + +// WatchEIP712DomainChanged is a free log subscription operation binding the contract event 0x0a6387c9ea3628b88a633bb4f3b151770f70085117a15f9bf3787cda53f13d31. +// +// Solidity: event EIP712DomainChanged() +func (_YellowToken *YellowTokenFilterer) WatchEIP712DomainChanged(opts *bind.WatchOpts, sink chan<- *YellowTokenEIP712DomainChanged) (event.Subscription, error) { + + logs, sub, err := _YellowToken.contract.WatchLogs(opts, "EIP712DomainChanged") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(YellowTokenEIP712DomainChanged) + if err := _YellowToken.contract.UnpackLog(event, "EIP712DomainChanged", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseEIP712DomainChanged is a log parse operation binding the contract event 0x0a6387c9ea3628b88a633bb4f3b151770f70085117a15f9bf3787cda53f13d31. +// +// Solidity: event EIP712DomainChanged() +func (_YellowToken *YellowTokenFilterer) ParseEIP712DomainChanged(log types.Log) (*YellowTokenEIP712DomainChanged, error) { + event := new(YellowTokenEIP712DomainChanged) + if err := _YellowToken.contract.UnpackLog(event, "EIP712DomainChanged", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// YellowTokenTransferIterator is returned from FilterTransfer and is used to iterate over the raw logs and unpacked data for Transfer events raised by the YellowToken contract. +type YellowTokenTransferIterator struct { + Event *YellowTokenTransfer // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *YellowTokenTransferIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(YellowTokenTransfer) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(YellowTokenTransfer) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *YellowTokenTransferIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *YellowTokenTransferIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// YellowTokenTransfer represents a Transfer event raised by the YellowToken contract. +type YellowTokenTransfer struct { + From common.Address + To common.Address + Value *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterTransfer is a free log retrieval operation binding the contract event 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef. +// +// Solidity: event Transfer(address indexed from, address indexed to, uint256 value) +func (_YellowToken *YellowTokenFilterer) FilterTransfer(opts *bind.FilterOpts, from []common.Address, to []common.Address) (*YellowTokenTransferIterator, error) { + + var fromRule []interface{} + for _, fromItem := range from { + fromRule = append(fromRule, fromItem) + } + var toRule []interface{} + for _, toItem := range to { + toRule = append(toRule, toItem) + } + + logs, sub, err := _YellowToken.contract.FilterLogs(opts, "Transfer", fromRule, toRule) + if err != nil { + return nil, err + } + return &YellowTokenTransferIterator{contract: _YellowToken.contract, event: "Transfer", logs: logs, sub: sub}, nil +} + +// WatchTransfer is a free log subscription operation binding the contract event 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef. +// +// Solidity: event Transfer(address indexed from, address indexed to, uint256 value) +func (_YellowToken *YellowTokenFilterer) WatchTransfer(opts *bind.WatchOpts, sink chan<- *YellowTokenTransfer, from []common.Address, to []common.Address) (event.Subscription, error) { + + var fromRule []interface{} + for _, fromItem := range from { + fromRule = append(fromRule, fromItem) + } + var toRule []interface{} + for _, toItem := range to { + toRule = append(toRule, toItem) + } + + logs, sub, err := _YellowToken.contract.WatchLogs(opts, "Transfer", fromRule, toRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(YellowTokenTransfer) + if err := _YellowToken.contract.UnpackLog(event, "Transfer", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseTransfer is a log parse operation binding the contract event 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef. +// +// Solidity: event Transfer(address indexed from, address indexed to, uint256 value) +func (_YellowToken *YellowTokenFilterer) ParseTransfer(log types.Log) (*YellowTokenTransfer, error) { + event := new(YellowTokenTransfer) + if err := _YellowToken.contract.UnpackLog(event, "Transfer", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} diff --git a/pkg/blockchain/xrpl/depositor.go b/pkg/blockchain/xrpl/depositor.go new file mode 100644 index 0000000..e50581b --- /dev/null +++ b/pkg/blockchain/xrpl/depositor.go @@ -0,0 +1,87 @@ +package xrpl + +import ( + "context" + "fmt" + + "github.com/Peersyst/xrpl-go/xrpl/rpc" + "github.com/Peersyst/xrpl-go/xrpl/transaction" + "github.com/Peersyst/xrpl-go/xrpl/transaction/types" + + "github.com/layer-3/clearnet-sdk/pkg/core" + "github.com/layer-3/clearnet-sdk/pkg/decimal" + "github.com/layer-3/clearnet-sdk/pkg/sign" +) + +// Depositor sends a tagged Payment from the depositor's account (the key the +// sign.Signer holds) to the vault, crediting a clearnet account via the +// DestinationTag. It implements core.VaultDepositor. Native XRP and issued +// currencies ("CUR.rIssuer") are both supported. +type Depositor struct { + client *rpc.Client + vaultAddress string + signer sign.Signer + id xrplIdentity +} + +var _ core.VaultDepositor = (*Depositor)(nil) + +// NewDepositor builds the XRPL depositor against the rippled JSON-RPC at rpcURL. +func NewDepositor(rpcURL, vaultAddress string, signer sign.Signer) (*Depositor, error) { + cfg, err := rpc.NewClientConfig(rpcURL) + if err != nil { + return nil, fmt.Errorf("xrpl: create rpc config: %w", err) + } + id, err := deriveIdentity(signer) + if err != nil { + return nil, err + } + return &Depositor{client: rpc.NewClient(cfg), vaultAddress: vaultAddress, signer: signer, id: id}, nil +} + +// DepositorAddress returns the depositor's classic r-address. +func (d *Depositor) DepositorAddress() string { return d.id.classicAddress } + +// Deposit sends `amount` of `asset` to the vault, crediting `account` via its +// DestinationTag. asset is "" / "XRP" for native or "CUR.rIssuer" for an issued +// currency; account must be of the form xrpl- (the tag the watcher credits). +func (d *Depositor) Deposit(ctx context.Context, asset string, amount decimal.Decimal, account string) (core.TxRef, error) { + tag, err := parseDepositTag(account) + if err != nil { + return core.TxRef{}, err + } + xrplAmount, err := currencyAmount(asset, amount) + if err != nil { + return core.TxRef{}, err + } + + payment := transaction.Payment{ + BaseTx: transaction.BaseTx{Account: types.Address(d.id.classicAddress)}, + Destination: types.Address(d.vaultAddress), + Amount: xrplAmount, + } + flatTx := payment.Flatten() + flatTx["DestinationTag"] = tag + if err := d.client.Autofill(&flatTx); err != nil { + return core.TxRef{}, fmt.Errorf("xrpl: autofill: %w", err) + } + + blob, err := signSingle(ctx, d.signer, d.id, flatTx) + if err != nil { + return core.TxRef{}, err + } + hash, err := computeTxHash(blob) + if err != nil { + return core.TxRef{}, err + } + result, err := d.client.SubmitTxBlob(blob, false) + if err != nil { + return core.TxRef{}, fmt.Errorf("xrpl: submit: %w", err) + } + switch result.EngineResult { + case "tesSUCCESS", "terQUEUED": + return core.TxRef{Hash: hash, Raw: hashHex(hash)}, nil + default: + return core.TxRef{}, fmt.Errorf("xrpl: deposit rejected: %s - %s", result.EngineResult, result.EngineResultMessage) + } +} diff --git a/pkg/blockchain/xrpl/vault_integration_test.go b/pkg/blockchain/xrpl/vault_integration_test.go new file mode 100644 index 0000000..c03046c --- /dev/null +++ b/pkg/blockchain/xrpl/vault_integration_test.go @@ -0,0 +1,309 @@ +//go:build integration + +package xrpl + +import ( + "bytes" + "context" + "crypto/ed25519" + "crypto/rand" + "encoding/hex" + "encoding/json" + "fmt" + "net/http" + "os" + "strings" + "testing" + "time" + + xrplkeypairs "github.com/Peersyst/xrpl-go/keypairs" + "github.com/Peersyst/xrpl-go/xrpl/rpc" + "github.com/Peersyst/xrpl-go/xrpl/transaction" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/layer-3/clearnet-sdk/pkg/core" + "github.com/layer-3/clearnet-sdk/pkg/decimal" + "github.com/layer-3/clearnet-sdk/pkg/sign" +) + +// XRPL full deposit + withdrawal flow against a standalone rippled (the devnet +// by default). Self-provisioning: each run funds a fresh vault account from the +// genesis master, configures its SignerList over fresh signer keys, creates a +// Ticket, then runs deposit + the quorum withdrawal. Standalone rippled does +// not auto-close ledgers, so the harness calls `ledger_accept` after each +// submit. Re-running is a clean run (fresh accounts); only the genesis master +// persists. +// +// Build-tagged `integration`. Default node http://127.0.0.1:5005; override via +// XRPL_RPC_URL. +// +// NOTE: this is the least-validated integration test — standalone provisioning +// (ledger_accept cadence, SignerListSet/TicketCreate encoding) may need +// iteration against a live node. + +const ( + defaultXRPLRPC = "http://127.0.0.1:5005" + genesisSeed = "snoPBrXtMeMyMHUVTgbuqAfg1SUTb" // rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh, ~100B XRP + xrplSignerCount = 3 + xrplQuorum = 2 + depositTag = 42 +) + +func TestIntegrationXRPL_DepositAndWithdraw(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) + defer cancel() + + url := xrplEnv("XRPL_RPC_URL", defaultXRPLRPC) + cfg, err := rpc.NewClientConfig(url) + if err != nil { + t.Fatalf("rpc config: %v", err) + } + h := &xrplHarness{url: url, client: rpc.NewClient(cfg), http: &http.Client{Timeout: 30 * time.Second}} + + master := masterSigner(t) + masterID := mustIdentity(t, master) + + // Fresh accounts this run. + vault := genEd25519(t) + vaultID := mustIdentity(t, vault) + depositor := genEd25519(t) + depID := mustIdentity(t, depositor) + recipient := genEd25519(t) + recID := mustIdentity(t, recipient) + signers := make([]sign.Signer, xrplSignerCount) + signerAddrs := make([]string, xrplSignerCount) + for i := range signers { + signers[i] = genEd25519(t) + signerAddrs[i] = mustIdentity(t, signers[i]).classicAddress + } + + // ── Setup ───────────────────────────────────────────────────────────────── + h.fund(ctx, t, master, masterID, vaultID.classicAddress, "1000000000") // 1000 XRP + h.fund(ctx, t, master, masterID, depID.classicAddress, "1000000000") // 1000 XRP + h.signerListSet(ctx, t, vault, vaultID, signerAddrs, xrplQuorum) + ticketSeq := h.ticketCreate(ctx, t, vault, vaultID) + t.Logf("vault %s signer-list set (quorum %d), ticket %d", vaultID.classicAddress, xrplQuorum, ticketSeq) + + // ── Deposit flow ────────────────────────────────────────────────────────── + dep, err := NewDepositor(url, vaultID.classicAddress, depositor) + if err != nil { + t.Fatalf("NewDepositor: %v", err) + } + depRef, err := dep.Deposit(ctx, "XRP", decimal.NewFromInt(100_000_000), fmt.Sprintf("xrpl-%d", depositTag)) // 100 XRP + if err != nil { + t.Fatalf("Deposit: %v", err) + } + h.ledgerAccept(ctx, t) + t.Logf("deposit tx %s (from %s)", depRef.Raw, dep.DepositorAddress()) + + // ── Withdrawal flow (quorum in-process) ─────────────────────────────────── + finalizers := make([]*WithdrawalFinalizer, len(signers)) + for i, s := range signers { + f, err := NewWithdrawalFinalizer(url, vaultID.classicAddress, xrplQuorum, s, fixedTicket(ticketSeq)) + if err != nil { + t.Fatalf("NewWithdrawalFinalizer %d: %v", i, err) + } + finalizers[i] = f + } + + var wid [32]byte + wid[0], wid[31] = 0x12, 0x34 + op := &core.WithdrawalOp{Recipient: recID.classicAddress, L1Asset: "XRP", Amount: decimal.NewFromInt(50_000_000)} // 50 XRP + + packed, err := finalizers[0].Pack(ctx, op, wid) + if err != nil { + t.Fatalf("Pack: %v", err) + } + blobs := make([][]byte, 0, len(finalizers)) + for i, f := range finalizers { + if err := f.Validate(ctx, packed, op, wid); err != nil { + t.Fatalf("Validate[%d]: %v", i, err) + } + b, err := f.Sign(ctx, packed) + if err != nil { + t.Fatalf("Sign[%d]: %v", i, err) + } + blobs = append(blobs, b) + } + merged, err := finalizers[0].Merge(ctx, packed, blobs) + if err != nil { + t.Fatalf("Merge: %v", err) + } + ref, err := finalizers[0].Submit(ctx, merged) + if err != nil { + t.Fatalf("Submit: %v", err) + } + h.ledgerAccept(ctx, t) + t.Logf("withdrawal tx %s", ref.Raw) + + if _, executed, err := finalizers[0].VerifyExecution(ctx, wid); err != nil { + t.Fatalf("VerifyExecution: %v", err) + } else if !executed { + t.Fatal("withdrawal not reported executed") + } +} + +type fixedTicket uint32 + +func (f fixedTicket) TicketFor(context.Context, [32]byte) (uint32, error) { return uint32(f), nil } + +// ── harness ─────────────────────────────────────────────────────────────────── + +type xrplHarness struct { + url string + client *rpc.Client + http *http.Client +} + +// submit autofills, single-signs with the given account, submits, and accepts a +// ledger so the tx validates before the next call reads account state. +func (h *xrplHarness) submit(ctx context.Context, t *testing.T, s sign.Signer, id xrplIdentity, flatTx transaction.FlatTransaction) { + t.Helper() + if err := h.client.Autofill(&flatTx); err != nil { + t.Fatalf("autofill: %v", err) + } + blob, err := signSingle(ctx, s, id, flatTx) + if err != nil { + t.Fatalf("sign setup tx: %v", err) + } + res, err := h.client.SubmitTxBlob(blob, false) + if err != nil { + t.Fatalf("submit setup tx: %v", err) + } + if !strings.HasPrefix(res.EngineResult, "tes") && !strings.HasPrefix(res.EngineResult, "ter") { + t.Fatalf("setup tx rejected: %s - %s", res.EngineResult, res.EngineResultMessage) + } + h.ledgerAccept(ctx, t) +} + +func (h *xrplHarness) fund(ctx context.Context, t *testing.T, s sign.Signer, id xrplIdentity, dest, drops string) { + t.Helper() + h.submit(ctx, t, s, id, transaction.FlatTransaction{ + "TransactionType": "Payment", + "Account": id.classicAddress, + "Destination": dest, + "Amount": drops, + }) +} + +func (h *xrplHarness) signerListSet(ctx context.Context, t *testing.T, s sign.Signer, id xrplIdentity, signerAddrs []string, quorum int) { + t.Helper() + entries := make([]any, len(signerAddrs)) + for i, a := range signerAddrs { + entries[i] = map[string]any{"SignerEntry": map[string]any{"Account": a, "SignerWeight": 1}} + } + h.submit(ctx, t, s, id, transaction.FlatTransaction{ + "TransactionType": "SignerListSet", + "Account": id.classicAddress, + "SignerQuorum": quorum, + "SignerEntries": entries, + }) +} + +// ticketCreate creates one Ticket on the account and returns its sequence. +func (h *xrplHarness) ticketCreate(ctx context.Context, t *testing.T, s sign.Signer, id xrplIdentity) uint32 { + t.Helper() + h.submit(ctx, t, s, id, transaction.FlatTransaction{ + "TransactionType": "TicketCreate", + "Account": id.classicAddress, + "TicketCount": 1, + }) + // Read the created Ticket's sequence from account_objects. + var resp struct { + Result struct { + AccountObjects []struct { + LedgerEntryType string `json:"LedgerEntryType"` + TicketSequence uint32 `json:"TicketSequence"` + } `json:"account_objects"` + } `json:"result"` + } + h.rawRPC(ctx, t, "account_objects", map[string]any{"account": id.classicAddress, "type": "ticket"}, &resp) + for _, o := range resp.Result.AccountObjects { + if o.LedgerEntryType == "Ticket" { + return o.TicketSequence + } + } + t.Fatal("no Ticket object found after TicketCreate") + return 0 +} + +func (h *xrplHarness) ledgerAccept(ctx context.Context, t *testing.T) { + t.Helper() + h.rawRPC(ctx, t, "ledger_accept", nil, nil) +} + +// rawRPC posts a rippled JSON-RPC method (params wrapped in the single-element +// array rippled expects) and optionally unmarshals the full envelope into out. +func (h *xrplHarness) rawRPC(ctx context.Context, t *testing.T, method string, params map[string]any, out any) { + t.Helper() + p := []any{} + if params != nil { + p = append(p, params) + } + body, _ := json.Marshal(map[string]any{"method": method, "params": p}) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, h.url, bytes.NewReader(body)) + if err != nil { + t.Fatalf("rawRPC %s: %v", method, err) + } + req.Header.Set("Content-Type", "application/json") + resp, err := h.http.Do(req) + if err != nil { + t.Fatalf("rawRPC %s: %v", method, err) + } + defer resp.Body.Close() + if out != nil { + if err := json.NewDecoder(resp.Body).Decode(out); err != nil { + t.Fatalf("rawRPC %s decode: %v", method, err) + } + } +} + +// ── key helpers ─────────────────────────────────────────────────────────────── + +func genEd25519(t *testing.T) sign.Signer { + t.Helper() + _, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatalf("gen ed25519: %v", err) + } + ks, err := sign.NewKeySignerFromEd25519(priv) + if err != nil { + t.Fatalf("ed25519 signer: %v", err) + } + return ks +} + +// masterSigner derives the genesis (master) secp256k1 key from its family seed. +func masterSigner(t *testing.T) sign.Signer { + t.Helper() + privHex, _, err := xrplkeypairs.DeriveKeypair(genesisSeed, false) + if err != nil { + t.Fatalf("derive genesis keypair: %v", err) + } + // secp256k1 derivation hex carries a "00" prefix over the 32-byte scalar. + raw, err := hex.DecodeString(strings.TrimPrefix(strings.ToUpper(privHex), "00")) + if err != nil || len(raw) != 32 { + t.Fatalf("decode genesis scalar (len=%d): %v", len(raw), err) + } + k, err := crypto.ToECDSA(raw) + if err != nil { + t.Fatalf("genesis scalar to ECDSA: %v", err) + } + return sign.NewKeySignerFromECDSA(k) +} + +func mustIdentity(t *testing.T, s sign.Signer) xrplIdentity { + t.Helper() + id, err := deriveIdentity(s) + if err != nil { + t.Fatalf("derive identity: %v", err) + } + return id +} + +func xrplEnv(key, def string) string { + if v := os.Getenv(key); v != "" { + return v + } + return def +} diff --git a/pkg/blockchain/xrpl/wire.go b/pkg/blockchain/xrpl/wire.go new file mode 100644 index 0000000..96fa603 --- /dev/null +++ b/pkg/blockchain/xrpl/wire.go @@ -0,0 +1,330 @@ +// Package xrpl implements the XRP Ledger custody vault via Peersyst/xrpl-go: +// a depositor that sends tagged Payments, and a multi-sign withdrawal finalizer +// over a SignerList-configured vault account. Both take a sign.Signer; neither +// holds persistence or a mesh. +package xrpl + +import ( + "bytes" + "context" + "encoding/hex" + "encoding/json" + "fmt" + "sort" + "strconv" + "strings" + + addresscodec "github.com/Peersyst/xrpl-go/address-codec" + binarycodec "github.com/Peersyst/xrpl-go/binary-codec" + xrplcrypto "github.com/Peersyst/xrpl-go/pkg/crypto" + "github.com/Peersyst/xrpl-go/xrpl/transaction" + "github.com/Peersyst/xrpl-go/xrpl/transaction/types" + + "github.com/layer-3/clearnet-sdk/pkg/core" + "github.com/layer-3/clearnet-sdk/pkg/decimal" + "github.com/layer-3/clearnet-sdk/pkg/sign" +) + +// maxAcceptableFeeDrops caps the Fee a node will sign on a canonical Payment. +const maxAcceptableFeeDrops uint64 = 1_000_000 + +// canonicalAllowedFields is the allowlist of top-level keys a node accepts on a +// canonical Payment flatTx before signing. +var canonicalAllowedFields = map[string]struct{}{ + "TransactionType": {}, "Account": {}, "Destination": {}, "Amount": {}, + "InvoiceID": {}, "TicketSequence": {}, "Sequence": {}, "Fee": {}, + "SigningPubKey": {}, "Flags": {}, +} + +// parseDepositTag extracts the XRPL DestinationTag from a crediting account. +// +// The custody deposit watcher derives the credited account FROM the tag — +// `core.UserURI("xrpl-" + tag)` — so the tag is the primary identifier, not a +// hash of anything. The account therefore must be of the form `xrpl-` +// (optionally as the last segment of a yellow:// URI); this reverses that +// mapping to recover the uint32 tag the depositor must set. +func parseDepositTag(account string) (uint32, error) { + seg := account + if i := strings.LastIndex(seg, "/"); i >= 0 { + seg = seg[i+1:] + } + rest, ok := strings.CutPrefix(strings.ToLower(seg), "xrpl-") + if !ok { + return 0, fmt.Errorf("xrpl: account %q must be of the form xrpl- (or yellow://.../user/xrpl-)", account) + } + n, err := strconv.ParseUint(rest, 10, 32) + if err != nil { + return 0, fmt.Errorf("xrpl: bad deposit tag in account %q: %w", account, err) + } + return uint32(n), nil +} + +// xrplIdentity is a signer's XRPL classic address + signing pubkey hex. +type xrplIdentity struct { + classicAddress string + signingPubKeyHex string +} + +// deriveIdentity maps a sign.Signer's public key to its XRPL identity. +func deriveIdentity(s sign.Signer) (xrplIdentity, error) { + pub := s.PublicKey() + var xrplPub []byte + switch s.Algorithm() { + case sign.AlgSecp256k1: + if len(pub) != 33 { + return xrplIdentity{}, fmt.Errorf("xrpl: secp256k1 pubkey must be 33-byte compressed, got %d", len(pub)) + } + xrplPub = pub + case sign.AlgEd25519: + if len(pub) != 32 { + return xrplIdentity{}, fmt.Errorf("xrpl: ed25519 pubkey must be 32 bytes, got %d", len(pub)) + } + // XRPL ed25519 pubkeys take a 0xED prefix. + xrplPub = append([]byte{0xED}, pub...) + default: + return xrplIdentity{}, fmt.Errorf("xrpl: unsupported signer algorithm %q", s.Algorithm()) + } + pubHex := strings.ToUpper(hex.EncodeToString(xrplPub)) + addr, err := addresscodec.EncodeClassicAddressFromPublicKeyHex(pubHex) + if err != nil { + return xrplIdentity{}, fmt.Errorf("xrpl: derive classic address: %w", err) + } + return xrplIdentity{classicAddress: addr, signingPubKeyHex: pubHex}, nil +} + +// signDigest runs the algorithm-specific signing primitive over the +// codec-encoded tx bytes. +func signDigest(ctx context.Context, s sign.Signer, encodedHex string) ([]byte, error) { + raw, err := hex.DecodeString(encodedHex) + if err != nil { + return nil, fmt.Errorf("xrpl: decode encoded tx: %w", err) + } + switch s.Algorithm() { + case sign.AlgSecp256k1: + return s.Sign(ctx, xrplcrypto.Sha512Half(raw)) + case sign.AlgEd25519: + return s.Sign(ctx, raw) + default: + return nil, fmt.Errorf("xrpl: unsupported algorithm %q", s.Algorithm()) + } +} + +// signMultisig produces this node's multi-sign blob for tx. +func signMultisig(ctx context.Context, s sign.Signer, id xrplIdentity, tx transaction.FlatTransaction) (string, error) { + tx["SigningPubKey"] = "" + encoded, err := binarycodec.EncodeForMultisigning(tx, id.classicAddress) + if err != nil { + return "", fmt.Errorf("xrpl: EncodeForMultisigning: %w", err) + } + sigBytes, err := signDigest(ctx, s, encoded) + if err != nil { + return "", fmt.Errorf("xrpl: sign: %w", err) + } + inner := types.Signer{SignerData: types.SignerData{ + Account: types.Address(id.classicAddress), + TxnSignature: strings.ToUpper(hex.EncodeToString(sigBytes)), + SigningPubKey: id.signingPubKeyHex, + }} + tx["Signers"] = []any{inner.Flatten()} + return binarycodec.Encode(tx) +} + +// signSingle signs tx as a single-signer transaction and returns the submittable blob. +func signSingle(ctx context.Context, s sign.Signer, id xrplIdentity, tx transaction.FlatTransaction) (string, error) { + tx["SigningPubKey"] = id.signingPubKeyHex + encoded, err := binarycodec.EncodeForSigning(tx) + if err != nil { + return "", fmt.Errorf("xrpl: EncodeForSigning: %w", err) + } + sigBytes, err := signDigest(ctx, s, encoded) + if err != nil { + return "", fmt.Errorf("xrpl: sign: %w", err) + } + tx["TxnSignature"] = strings.ToUpper(hex.EncodeToString(sigBytes)) + return binarycodec.Encode(tx) +} + +// buildAmount converts a WithdrawalOp into an XRPL CurrencyAmount. +func buildAmount(op *core.WithdrawalOp) (types.CurrencyAmount, error) { + return currencyAmount(op.L1Asset, op.Amount) +} + +// currencyAmount maps an asset key + decimal amount to an XRPL CurrencyAmount: +// +// "" / "XRP" — native XRP; amount is drops (integer). +// "CUR.rIssuer" / "CUR:rIssuer" — issued currency; amount is a decimal value. +func currencyAmount(asset string, amount decimal.Decimal) (types.CurrencyAmount, error) { + l1 := strings.TrimSpace(asset) + if l1 == "" || strings.EqualFold(l1, "XRP") { + drops := amount.BigInt() + if !drops.IsUint64() { + return nil, fmt.Errorf("xrpl: xrp amount %s overflows uint64 drops", drops.String()) + } + return types.XRPCurrencyAmount(drops.Uint64()), nil + } + var currency, issuer string + for _, sep := range []string{".", ":"} { + if i := strings.Index(l1, sep); i > 0 { + currency, issuer = l1[:i], l1[i+1:] + break + } + } + if currency == "" || issuer == "" { + return nil, fmt.Errorf("xrpl: invalid asset %q: expected \"XRP\" or \"CUR.rIssuer\"", l1) + } + return types.IssuedCurrencyAmount{Issuer: types.Address(issuer), Currency: currency, Value: amount.String()}, nil +} + +// validateCanonical asserts the canonical flatTx matches the op. +func validateCanonical(flat transaction.FlatTransaction, op *core.WithdrawalOp, withdrawalID [32]byte, vault string) error { + if asString(flat["TransactionType"]) != "Payment" { + return fmt.Errorf("xrpl canonical: wrong TransactionType %v", flat["TransactionType"]) + } + if !strings.EqualFold(asString(flat["Account"]), vault) { + return fmt.Errorf("xrpl canonical: Account %v != vault %s", flat["Account"], vault) + } + if !strings.EqualFold(asString(flat["Destination"]), op.Recipient) { + return fmt.Errorf("xrpl canonical: Destination %v != op.Recipient %s", flat["Destination"], op.Recipient) + } + wantAmount, err := buildAmount(op) + if err != nil { + return fmt.Errorf("xrpl canonical: build expected Amount: %w", err) + } + if err := amountEqual(flat["Amount"], wantAmount); err != nil { + return fmt.Errorf("xrpl canonical: Amount mismatch: %w", err) + } + wantInvoice := strings.ToUpper(hex.EncodeToString(withdrawalID[:])) + if !strings.EqualFold(asString(flat["InvoiceID"]), wantInvoice) { + return fmt.Errorf("xrpl canonical: InvoiceID %v != withdrawalID %s", flat["InvoiceID"], wantInvoice) + } + if _, ok := uint32Field(flat["TicketSequence"]); !ok { + return fmt.Errorf("xrpl canonical: missing or invalid TicketSequence %v", flat["TicketSequence"]) + } + if seq, ok := uint32Field(flat["Sequence"]); !ok || seq != 0 { + return fmt.Errorf("xrpl canonical: Sequence must be 0 on Tickets path, got %v", flat["Sequence"]) + } + fee, ok := uint32Field(flat["Fee"]) + if !ok { + return fmt.Errorf("xrpl canonical: missing or invalid Fee %v", flat["Fee"]) + } + if uint64(fee) > maxAcceptableFeeDrops { + return fmt.Errorf("xrpl canonical: Fee %d drops exceeds ceiling %d", fee, maxAcceptableFeeDrops) + } + for k := range flat { + if _, ok := canonicalAllowedFields[k]; !ok { + return fmt.Errorf("xrpl canonical: unexpected field %q", k) + } + } + return nil +} + +func asString(v any) string { + if s, ok := v.(string); ok { + return s + } + return "" +} + +func amountEqual(got any, want types.CurrencyAmount) error { + switch w := want.(type) { + case types.XRPCurrencyAmount: + if asString(got) != w.String() { + return fmt.Errorf("XRP amount %v != %s", got, w.String()) + } + return nil + case types.IssuedCurrencyAmount: + gotMap, ok := got.(map[string]any) + if !ok { + return fmt.Errorf("issued amount must be an object, got %v (%T)", got, got) + } + if !strings.EqualFold(asString(gotMap["issuer"]), string(w.Issuer)) { + return fmt.Errorf("issuer %v != %s", gotMap["issuer"], w.Issuer) + } + if !strings.EqualFold(asString(gotMap["currency"]), w.Currency) { + return fmt.Errorf("currency %v != %s", gotMap["currency"], w.Currency) + } + if asString(gotMap["value"]) != w.Value { + return fmt.Errorf("value %v != %s", gotMap["value"], w.Value) + } + return nil + default: + return fmt.Errorf("unsupported expected amount type %T", want) + } +} + +func uint32Field(raw any) (uint32, bool) { + switch v := raw.(type) { + case json.Number: + n, err := v.Int64() + if err != nil || n < 0 || uint64(n) > uint64(^uint32(0)) { + return 0, false + } + return uint32(n), true + case float64: + if v < 0 || v > float64(^uint32(0)) { + return 0, false + } + return uint32(v), true + case int: + if v < 0 || uint64(v) > uint64(^uint32(0)) { + return 0, false + } + return uint32(v), true + case uint32: + return v, true + case uint64: + if v > uint64(^uint32(0)) { + return 0, false + } + return uint32(v), true + case string: + n, err := strconv.ParseUint(v, 10, 32) + if err != nil { + return 0, false + } + return uint32(n), true + default: + return 0, false + } +} + +// canonicalJSON encodes a FlatTransaction with sorted keys. +func canonicalJSON(flatTx transaction.FlatTransaction) ([]byte, error) { + keys := make([]string, 0, len(flatTx)) + for k := range flatTx { + keys = append(keys, k) + } + sort.Strings(keys) + var buf bytes.Buffer + buf.WriteByte('{') + for i, k := range keys { + if i > 0 { + buf.WriteByte(',') + } + kb, _ := json.Marshal(k) + buf.Write(kb) + buf.WriteByte(':') + vb, err := json.Marshal(flatTx[k]) + if err != nil { + return nil, fmt.Errorf("xrpl: encode key %q: %w", k, err) + } + buf.Write(vb) + } + buf.WriteByte('}') + return buf.Bytes(), nil +} + +// hashHex is the uppercase hex of a 32-byte tx hash (XRPL's display form). +func hashHex(h [32]byte) string { return strings.ToUpper(hex.EncodeToString(h[:])) } + +// computeTxHash computes the XRPL transaction hash from a tx blob hex string. +func computeTxHash(txBlobHex string) ([32]byte, error) { + blobBytes, err := hex.DecodeString(txBlobHex) + if err != nil { + return [32]byte{}, fmt.Errorf("xrpl: decode tx blob: %w", err) + } + buf := append([]byte{0x54, 0x58, 0x4E, 0x00}, blobBytes...) + var hash [32]byte + copy(hash[:], xrplcrypto.Sha512Half(buf)) + return hash, nil +} diff --git a/pkg/blockchain/xrpl/withdrawal_finalizer.go b/pkg/blockchain/xrpl/withdrawal_finalizer.go new file mode 100644 index 0000000..feec189 --- /dev/null +++ b/pkg/blockchain/xrpl/withdrawal_finalizer.go @@ -0,0 +1,188 @@ +package xrpl + +import ( + "context" + "encoding/hex" + "encoding/json" + "fmt" + "strings" + + "github.com/Peersyst/xrpl-go/xrpl" + "github.com/Peersyst/xrpl-go/xrpl/queries/account" + "github.com/Peersyst/xrpl-go/xrpl/rpc" + "github.com/Peersyst/xrpl-go/xrpl/transaction" + "github.com/Peersyst/xrpl-go/xrpl/transaction/types" + + "github.com/layer-3/clearnet-sdk/pkg/core" + "github.com/layer-3/clearnet-sdk/pkg/sign" +) + +// executionScanPages bounds how many account_tx pages VerifyExecution reads. +const executionScanPages = 5 + +// TicketProvider supplies the Ticket sequence that authorizes a withdrawal. +// Custody backs this with its ticket pool/store; tests and simpler clients back +// it with a fixed or create-then-return ticket. +type TicketProvider interface { + TicketFor(ctx context.Context, withdrawalID [32]byte) (uint32, error) +} + +// WithdrawalFinalizer is the XRPL multi-sign vault withdrawal path. It owns the +// node's signer and a TicketProvider; the quorum's blobs are merged off-mesh by +// the caller. It implements core.VaultWithdrawalFinalizer. +type WithdrawalFinalizer struct { + client *rpc.Client + vaultAddress string + threshold int + signer sign.Signer + id xrplIdentity + tickets TicketProvider +} + +var _ core.VaultWithdrawalFinalizer = (*WithdrawalFinalizer)(nil) + +// NewWithdrawalFinalizer builds the XRPL vault finalizer. threshold is the +// SignerQuorum; tickets authorizes each withdrawal's TicketSequence. +func NewWithdrawalFinalizer(rpcURL, vaultAddress string, threshold int, signer sign.Signer, tickets TicketProvider) (*WithdrawalFinalizer, error) { + cfg, err := rpc.NewClientConfig(rpcURL) + if err != nil { + return nil, fmt.Errorf("xrpl: create rpc config: %w", err) + } + id, err := deriveIdentity(signer) + if err != nil { + return nil, err + } + return &WithdrawalFinalizer{ + client: rpc.NewClient(cfg), + vaultAddress: vaultAddress, + threshold: threshold, + signer: signer, + id: id, + tickets: tickets, + }, nil +} + +// Pack binds a Ticket and builds the autofilled multi-sign Payment, returning +// its sorted-key JSON. +func (f *WithdrawalFinalizer) Pack(ctx context.Context, op *core.WithdrawalOp, withdrawalID [32]byte) ([]byte, error) { + amount, err := buildAmount(op) + if err != nil { + return nil, err + } + ticket, err := f.tickets.TicketFor(ctx, withdrawalID) + if err != nil { + return nil, fmt.Errorf("xrpl: ticket: %w", err) + } + payment := transaction.Payment{ + BaseTx: transaction.BaseTx{ + Account: types.Address(f.vaultAddress), + Sequence: 0, + TicketSequence: ticket, + }, + Destination: types.Address(op.Recipient), + Amount: amount, + InvoiceID: types.Hash256(strings.ToUpper(hex.EncodeToString(withdrawalID[:]))), + } + flatTx := payment.Flatten() + flatTx["Sequence"] = uint32(0) + if err := f.client.AutofillMultisigned(&flatTx, uint64(f.threshold)); err != nil { + return nil, fmt.Errorf("xrpl: autofill: %w", err) + } + flatTx["Sequence"] = uint32(0) + delete(flatTx, "LastLedgerSequence") + return canonicalJSON(flatTx) +} + +// Validate re-derives the trust-bound shape from the op and asserts the packed +// flatTx matches. +func (f *WithdrawalFinalizer) Validate(_ context.Context, packed []byte, op *core.WithdrawalOp, withdrawalID [32]byte) error { + var flat transaction.FlatTransaction + if err := json.Unmarshal(packed, &flat); err != nil { + return fmt.Errorf("xrpl: decode packed: %w", err) + } + return validateCanonical(flat, op, withdrawalID, f.vaultAddress) +} + +// Sign multi-signs the packed Payment and returns this node's blob. +func (f *WithdrawalFinalizer) Sign(ctx context.Context, packed []byte) ([]byte, error) { + var flat transaction.FlatTransaction + if err := json.Unmarshal(packed, &flat); err != nil { + return nil, fmt.Errorf("xrpl: decode packed: %w", err) + } + blob, err := signMultisig(ctx, f.signer, f.id, flat) + if err != nil { + return nil, err + } + return []byte(blob), nil +} + +// Merge combines the collected multi-sign blobs into one submittable blob. +// Exactly `threshold` signatures are included: Pack autofilled the multi-sign +// fee for that count (base × (1 + threshold)), so including extras would +// under-pay (telINSUF_FEE_P) and waste fee. Any threshold of the SignerList's +// members satisfies the quorum. +func (f *WithdrawalFinalizer) Merge(_ context.Context, _ []byte, signatures [][]byte) ([]byte, error) { + if len(signatures) < f.threshold { + return nil, fmt.Errorf("xrpl: have %d signatures, need %d", len(signatures), f.threshold) + } + blobs := make([]string, 0, f.threshold) + for _, s := range signatures[:f.threshold] { + blobs = append(blobs, string(s)) + } + final, err := xrpl.Multisign(blobs...) + if err != nil { + return nil, fmt.Errorf("xrpl: combine signatures: %w", err) + } + return []byte(final), nil +} + +// Submit broadcasts the merged blob and returns the tx reference. +func (f *WithdrawalFinalizer) Submit(_ context.Context, merged []byte) (core.TxRef, error) { + result, err := f.client.SubmitMultisigned(string(merged), false) + if err != nil { + return core.TxRef{}, fmt.Errorf("xrpl: submit_multisigned: %w", err) + } + switch result.EngineResult { + case "tesSUCCESS", "terQUEUED": + hash, err := computeTxHash(result.TxBlob) + if err != nil { + return core.TxRef{}, err + } + return core.TxRef{Hash: hash, Raw: hashHex(hash)}, nil + default: + return core.TxRef{}, fmt.Errorf("xrpl: submit rejected: %s - %s", result.EngineResult, result.EngineResultMessage) + } +} + +// VerifyExecution scans the vault's recent account_tx for a Payment whose +// InvoiceID equals the withdrawalID, returning its tx hash + true. +func (f *WithdrawalFinalizer) VerifyExecution(ctx context.Context, withdrawalID [32]byte) ([32]byte, bool, error) { + want := strings.ToUpper(hex.EncodeToString(withdrawalID[:])) + var marker any + for page := 0; page < executionScanPages; page++ { + resp, err := f.client.GetAccountTransactions(&account.TransactionsRequest{ + Account: types.Address(f.vaultAddress), + Limit: 100, + Marker: marker, + }) + if err != nil { + return [32]byte{}, false, fmt.Errorf("xrpl verify: account_tx: %w", err) + } + for _, tx := range resp.Transactions { + if strings.EqualFold(asString(tx.Tx["InvoiceID"]), want) { + h, err := hex.DecodeString(string(tx.Hash)) + if err != nil || len(h) != 32 { + return [32]byte{}, true, nil // executed; hash unparseable + } + var out [32]byte + copy(out[:], h) + return out, true, nil + } + } + if resp.Marker == nil { + break + } + marker = resp.Marker + } + return [32]byte{}, false, nil +} diff --git a/pkg/core/block.go b/pkg/core/block.go index 5f4f7f6..5a4767c 100644 --- a/pkg/core/block.go +++ b/pkg/core/block.go @@ -1,8 +1,8 @@ package core import ( - "errors" "bytes" + "errors" "fmt" "math/big" "sort" diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go new file mode 100644 index 0000000..e2f17a3 --- /dev/null +++ b/pkg/core/blockchain.go @@ -0,0 +1,130 @@ +package core + +import ( + "context" + "math/big" + + "github.com/ethereum/go-ethereum/common" + + "github.com/layer-3/clearnet-sdk/pkg/decimal" +) + +// TxRef identifies a submitted L1 transaction across chains: Hash is the +// canonical 32-byte tx hash / id; Raw is an optional chain-native reference +// (e.g. a hex txid) for chains where the string form is primary. +type TxRef struct { + Hash [32]byte + Raw string +} + +// Chain-agnostic adapter interfaces for L1 interactions, split by concern: the +// custody vault (deposit/withdraw), the node registry, token balances, the +// fraud adjudicator, and the testnet faucet. Each per-chain implementation +// (e.g. pkg/blockchain/evm) provides a separate adapter struct per role rather +// than one monolith, so the centralized registry/faucet paths stay decoupled +// from the vault money path. + +// WithdrawalFraudEvidence is the frozen two-object Slasher evidence shape +// (protocol/security.md §11): challenged signed withdrawal Block bytes plus +// the immediate pre-withdrawal signed header and one balance SMT proof. +type WithdrawalFraudEvidence struct { + ChallengedObject []byte + AnchorHeader []byte + AnchorSignature []byte + EntryIndex uint64 + SMTProof [][32]byte + SMTBitmask *big.Int + BalanceKey [32]byte + ProvenBalance *big.Int +} + +// FraudEvidenceSubmitter provides write access to the on-chain Slasher. +type FraudEvidenceSubmitter interface { + SubmitWithdrawalFraudEvidence(ctx context.Context, evidence WithdrawalFraudEvidence) error +} + +// VaultDepositor moves funds into the L1 vault. The implementation owns the +// depositor's signing identity (a sign.Signer supplied at construction) and +// executes the deposit on its chain: a contract call (EVM), a funding tx to a +// derived address (BTC), or a tagged Payment (XRPL). It expects only the asset, +// amount, and crediting clearnet account. +type VaultDepositor interface { + Deposit(ctx context.Context, asset string, amount decimal.Decimal, account string) (TxRef, error) +} + +// VaultWithdrawalFinalizer turns an authorized withdrawal into an on-chain +// release, as a sequence each custody node runs over a caller-orchestrated +// quorum. The implementation owns the node's signer and the chain-specific +// authorization (vault address, signer set, fee policy, ticket source) supplied +// at construction. +// +// - Pack returns the canonical bytes to be signed for this withdrawal. +// - Validate re-derives the trust-bound shape from the op and asserts the +// packed bytes match — the defense against a Byzantine packer; every node +// runs it before Sign. +// - Sign produces this node's signature over the packed bytes. +// - Merge combines the packed bytes with the collected quorum signatures into +// a submittable artifact. +// - Submit broadcasts the merged artifact. +// - VerifyExecution reads canonical chain state to answer "already executed?" +// for the retry/finalize loop. +type VaultWithdrawalFinalizer interface { + Pack(ctx context.Context, op *WithdrawalOp, withdrawalID [32]byte) ([]byte, error) + Validate(ctx context.Context, packed []byte, op *WithdrawalOp, withdrawalID [32]byte) error + Sign(ctx context.Context, packed []byte) ([]byte, error) + Merge(ctx context.Context, packed []byte, signatures [][]byte) ([]byte, error) + Submit(ctx context.Context, merged []byte) (TxRef, error) + VerifyExecution(ctx context.Context, withdrawalID [32]byte) (txHash [32]byte, executed bool, err error) +} + +// RegistryReader provides read access to the L1 node registry. +// +// Naming follows the on-chain `IRegistry` surface: +// - `TotalNodes` / `GetNodes` enumerate active + unbonding; active-only +// callers use `ActiveCount` plus `GetActiveNodes` (which filters +// `deactivatedAt == 0` on the Go side). +// - `FloorPrice` is the activation-time floor at the next cardinality. +// - `GetNodeId(tokenId)` resolves an NFT to the currently-locked nodeId. +type RegistryReader interface { + GetNodeByID(ctx context.Context, nodeID [32]byte) (*Slot, error) + GetNodes(ctx context.Context, offset, limit *big.Int) ([]*Slot, error) + TotalNodes(ctx context.Context) (*big.Int, error) + GetActiveNodes(ctx context.Context, offset, limit *big.Int) ([]*Slot, error) + ActiveCount(ctx context.Context) (uint32, error) + FloorPrice(ctx context.Context) (*big.Int, error) + GetNodeId(ctx context.Context, tokenId uint32) ([32]byte, error) + UnbondingPeriod(ctx context.Context) (uint64, error) +} + +// RegistryWriter provides write access to the L1 node registry. +// +// `Lock` is a high-level onboarding helper: it calls `Registry.register`, +// which mints a NodeID NFT directly into Registry escrow and locks collateral. +// The `popSignature` parameter is accepted for source-compat but ignored on +// chain (ADR-008 2026-05-08: PoP verification moved to off-chain tooling). +type RegistryWriter interface { + Lock(ctx context.Context, blsPubkeyG1 [2]*big.Int, blsPubkeyG2 [4]*big.Int, popSignature [2]*big.Int, maxPrice *big.Int) (uint32, error) + Unlock(ctx context.Context, tokenId uint32) error + Release(ctx context.Context, tokenId uint32) error + Fund(ctx context.Context, tokenId uint32, amount *big.Int) error +} + +// TokenReader provides read access to ERC-20 token balances. token is the +// token contract address (hex); account is the holder address (hex). +type TokenReader interface { + BalanceOf(ctx context.Context, token string, account string) (*big.Int, error) +} + +// FaucetReader provides read access to the testnet faucet's parameters. +type FaucetReader interface { + DripAmount(ctx context.Context) (*big.Int, error) + Cooldown(ctx context.Context) (*big.Int, error) + Owner(ctx context.Context) (common.Address, error) + LastDrip(ctx context.Context, addr common.Address) (*big.Int, error) +} + +// FaucetWriter provides write access to the testnet faucet drip. +type FaucetWriter interface { + Drip(ctx context.Context) error + DripTo(ctx context.Context, recipient common.Address) error +} diff --git a/pkg/core/slot.go b/pkg/core/slot.go new file mode 100644 index 0000000..6531a2e --- /dev/null +++ b/pkg/core/slot.go @@ -0,0 +1,47 @@ +package core + +import "math/big" + +// SlotState is the lifecycle state of a Registry slot (ADR-005 §3.3). +type SlotState uint8 + +const ( + // SlotStateWarmup: slot is syncing state, not eligible to sign (§3.3). + SlotStateWarmup SlotState = iota + + // SlotStateActive: slot is fully operational and may sign blocks. + SlotStateActive + + // SlotStateUnbonding: slot has unregistered. Cannot sign new blocks but + // remains in the Kademlia tree for the full UnbondingWindow so fraud + // proofs may still be submitted against its collateral (ADR-005 §3.3). + SlotStateUnbonding + + // SlotStateEvicted: slot has been fully removed from the Kademlia tree. + SlotStateEvicted + + // SlotStateDisabled: slot missed heartbeats and is temporarily disabled (bitmask = 0). + SlotStateDisabled +) + +// Slot is a logical node: one per NFT locked on Registry. Carries the slot's +// public identity (NodeID, Label, BLSPubKey) plus its Registry position and +// economic/lifecycle metadata. +type Slot struct { + ID NodeID `json:"id"` + Label string `json:"label"` + BLSPubKey []byte `json:"bls_pubkey,omitempty"` + + // Registry position + TokenID *big.Int `json:"-"` + Index uint64 `json:"-"` // widened from uint32 for cbor-gen compat (Wave 2 pre) + + // Economic weight + lifecycle + Owner string `json:"-"` // EOA (hex) that owns the NFT per Registry.ownerOf(tokenId) + Collateral *big.Int `json:"-"` + ActivatedAt uint64 `json:"-"` + DeactivatedAt uint64 `json:"-"` + State SlotState `json:"-"` + LastHeartbeat int64 `json:"-"` + MissedBeats uint64 `json:"-"` // widened from uint32 for cbor-gen compat (Wave 2 pre) +} diff --git a/pkg/sign/ethereum.go b/pkg/sign/ethereum.go new file mode 100644 index 0000000..8051ddd --- /dev/null +++ b/pkg/sign/ethereum.go @@ -0,0 +1,108 @@ +package sign + +import ( + "context" + "errors" + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + + decred_ecdsa "github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa" +) + +// EthAddress derives the Ethereum address from a secp256k1 Signer. +func EthAddress(s Signer) (common.Address, error) { + if s.Algorithm() != AlgSecp256k1 { + return common.Address{}, fmt.Errorf("sign: EthAddress requires secp256k1, got %s", s.Algorithm()) + } + pk, err := crypto.DecompressPubkey(s.PublicKey()) + if err != nil { + return common.Address{}, fmt.Errorf("sign: decompress pubkey: %w", err) + } + return crypto.PubkeyToAddress(*pk), nil +} + +// SignEthDigest signs a 32-byte digest with a secp256k1 Signer and returns the +// 65-byte Ethereum form R || S || V with V ∈ {0,1} (what crypto.Sign produces +// and crypto.SigToPub expects). Callers that need Solidity's V ∈ {27,28} shift +// at the contract boundary. expectedAddr disambiguates the recovery id. +func SignEthDigest(ctx context.Context, s Signer, digest []byte, expectedAddr common.Address) ([]byte, error) { + if len(digest) != 32 { + return nil, fmt.Errorf("sign: SignEthDigest requires 32-byte digest, got %d", len(digest)) + } + der, err := s.Sign(ctx, digest) + if err != nil { + return nil, err + } + r, ss, err := derSigRS(der) + if err != nil { + return nil, fmt.Errorf("sign: parse DER: %w", err) + } + + sig := make([]byte, 65) + copy(sig[:32], r[:]) + copy(sig[32:64], ss[:]) + + for v := byte(0); v < 2; v++ { + sig[64] = v + recovered, err := crypto.SigToPub(digest, sig) + if err != nil { + continue + } + if crypto.PubkeyToAddress(*recovered) == expectedAddr { + return sig, nil + } + } + return nil, errors.New("sign: could not recover V from Ethereum signature") +} + +// normalizeLowSDER re-encodes a DER ECDSA signature with S in the lower half of +// the curve order. Required by EVM (EIP-2) and XRPL (canonical signatures); KMS +// backends may return high-S. +func normalizeLowSDER(der []byte) ([]byte, error) { + sig, err := decred_ecdsa.ParseDERSignature(der) + if err != nil { + return nil, fmt.Errorf("parse DER: %w", err) + } + return sig.Serialize(), nil +} + +// derSigRS extracts R, S as 32-byte big-endian arrays from a DER ECDSA +// signature, normalizing S to the lower half-order in the process. +func derSigRS(der []byte) (r, s [32]byte, err error) { + canonical, err := normalizeLowSDER(der) + if err != nil { + return r, s, err + } + if len(canonical) < 8 || canonical[0] != 0x30 { + return r, s, errors.New("DER: bad SEQUENCE header") + } + rest := canonical[2:] + rBytes, rest, err := parseDERInt(rest) + if err != nil { + return r, s, err + } + sBytes, _, err := parseDERInt(rest) + if err != nil { + return r, s, err + } + copy(r[32-len(rBytes):], rBytes) + copy(s[32-len(sBytes):], sBytes) + return r, s, nil +} + +func parseDERInt(b []byte) (val, rest []byte, err error) { + if len(b) < 2 || b[0] != 0x02 { + return nil, nil, errors.New("DER: bad INTEGER tag") + } + n := int(b[1]) + if n+2 > len(b) { + return nil, nil, errors.New("DER: integer length overflow") + } + v := b[2 : 2+n] + if len(v) > 0 && v[0] == 0x00 { + v = v[1:] + } + return v, b[2+n:], nil +} diff --git a/pkg/sign/key.go b/pkg/sign/key.go new file mode 100644 index 0000000..584fc4b --- /dev/null +++ b/pkg/sign/key.go @@ -0,0 +1,74 @@ +package sign + +import ( + "context" + "crypto/ecdsa" + "crypto/ed25519" + "fmt" + + "github.com/ethereum/go-ethereum/crypto" + + decred_secp "github.com/decred/dcrd/dcrec/secp256k1/v4" + decred_ecdsa "github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa" +) + +// KeySigner is a Signer backed by a raw private key held in memory — for +// clients, the CLI, and tests. Production prefers a KMS-backed Signer. +type KeySigner struct { + algorithm Algorithm + ecdsaKey *ecdsa.PrivateKey + ed25519Key ed25519.PrivateKey + publicKey []byte +} + +// NewKeySignerFromECDSA wraps a secp256k1 private key. +func NewKeySignerFromECDSA(key *ecdsa.PrivateKey) *KeySigner { + return &KeySigner{ + algorithm: AlgSecp256k1, + ecdsaKey: key, + publicKey: crypto.CompressPubkey(&key.PublicKey), + } +} + +// NewKeySignerFromEd25519 wraps an ed25519 private key. +func NewKeySignerFromEd25519(key ed25519.PrivateKey) (*KeySigner, error) { + if len(key) != ed25519.PrivateKeySize { + return nil, fmt.Errorf("ed25519 private key has wrong length %d (expected %d)", len(key), ed25519.PrivateKeySize) + } + pub := make([]byte, ed25519.PublicKeySize) + copy(pub, key.Public().(ed25519.PublicKey)) + return &KeySigner{ + algorithm: AlgEd25519, + ed25519Key: key, + publicKey: pub, + }, nil +} + +func (s *KeySigner) Algorithm() Algorithm { return s.algorithm } + +func (s *KeySigner) PublicKey() []byte { + out := make([]byte, len(s.publicKey)) + copy(out, s.publicKey) + return out +} + +func (s *KeySigner) Sign(_ context.Context, message []byte) ([]byte, error) { + switch s.algorithm { + case AlgSecp256k1: + if len(message) != 32 { + return nil, fmt.Errorf("secp256k1 sign expects 32-byte digest, got %d", len(message)) + } + // decred's Sign uses RFC6979 deterministic ECDSA and Serialize emits + // canonical low-S DER — matches the KMS output format. + priv := decred_secp.PrivKeyFromBytes(crypto.FromECDSA(s.ecdsaKey)) + return decred_ecdsa.Sign(priv, message).Serialize(), nil + + case AlgEd25519: + return ed25519.Sign(s.ed25519Key, message), nil + + default: + return nil, ErrUnsupportedAlgorithm + } +} + +func (s *KeySigner) Close() error { return nil } diff --git a/pkg/sign/sign.go b/pkg/sign/sign.go new file mode 100644 index 0000000..712af83 --- /dev/null +++ b/pkg/sign/sign.go @@ -0,0 +1,40 @@ +// Package sign abstracts signing identities behind a pluggable, algorithm-aware +// interface — raw in-memory keys for clients/CLI/tests, KMS backends in +// production (custody's AWS/GCP signers satisfy this interface unchanged). The +// blockchain adapters take a Signer at construction and apply the chain-specific +// framing (EVM keccak + recovery id, BTC DER + sighash byte, XRPL +// encode-for-multisigning) themselves. +package sign + +import ( + "context" + "errors" +) + +// Algorithm names the curve/scheme. The Sign output format is +// algorithm-specific: +// - AlgSecp256k1: DER ECDSA, low-S normalized; caller passes a 32-byte digest. +// - AlgEd25519: raw 64-byte signature; caller passes the message. +type Algorithm string + +const ( + AlgSecp256k1 Algorithm = "secp256k1" + AlgEd25519 Algorithm = "ed25519" +) + +// ErrUnsupportedAlgorithm is returned for an algorithm a backend can't handle. +var ErrUnsupportedAlgorithm = errors.New("sign: unsupported algorithm") + +// Signer is implemented by every signing backend. Implementations are safe for +// concurrent Sign calls; construct once, share, Close at shutdown. +type Signer interface { + Algorithm() Algorithm + + // PublicKey returns the raw public key bytes: + // - secp256k1: 33-byte compressed SEC1 (0x02/0x03 || X) + // - ed25519: 32-byte raw + PublicKey() []byte + + Sign(ctx context.Context, message []byte) ([]byte, error) + Close() error +} From 76eb1d098e8d41a91a674a831a2ae0d6c272c875 Mon Sep 17 00:00:00 2001 From: Anton Filonenko Date: Tue, 16 Jun 2026 13:52:46 +0300 Subject: [PATCH 03/15] feat: add Solana support and make on-chain commitment configurable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - pkg/blockchain/sol: VaultDepositor (native SOL + SPL) and VaultWithdrawalFinalizer (native SOL) over the custody Anchor program — an ed25519 quorum signs a digest, verified on-chain via the Ed25519 precompile; fee payer separate from the quorum signers - generated program bindings (pkg/blockchain/sol/custody) emitted from the vendored Anchor IDL by `go generate` via an anchor-go-backed idl_refresher (the Solana parallel of the EVM abi_refresher); IDL + program binary vendored under sol/artifacts - on-chain read/submit commitment is configurable (default Finalized; devnet/tests use Confirmed to avoid waiting for finality) - devnet: solana-test-validator service with the program preloaded upgradeable at its fixed id + a readiness probe; self-provisioning deposit + withdrawal integration test (toolchain-free at test time) - evm: WithdrawalFinalizer.Submit now waits for the execute tx to mine Co-Authored-By: Claude Fable 5 --- devnet/README.md | 11 +- devnet/docker-compose.yml | 29 + devnet/sol-upgrade-authority.json | 1 + devnet/wait/main.go | 2 + go.mod | 38 +- go.sum | 138 ++- pkg/blockchain/evm/withdrawal_finalizer.go | 5 + pkg/blockchain/sol/artifacts/README.md | 46 + pkg/blockchain/sol/artifacts/custody.json | 1040 ++++++++++++++++++ pkg/blockchain/sol/artifacts/custody.so | Bin 0 -> 367320 bytes pkg/blockchain/sol/custody/accounts.go | 69 ++ pkg/blockchain/sol/custody/constants.go | 4 + pkg/blockchain/sol/custody/discriminators.go | 26 + pkg/blockchain/sol/custody/doc.go | 7 + pkg/blockchain/sol/custody/errors.go | 4 + pkg/blockchain/sol/custody/events.go | 93 ++ pkg/blockchain/sol/custody/fetchers.go | 4 + pkg/blockchain/sol/custody/instructions.go | 357 ++++++ pkg/blockchain/sol/custody/program-id.go | 8 + pkg/blockchain/sol/custody/types.go | 427 +++++++ pkg/blockchain/sol/depositor.go | 132 +++ pkg/blockchain/sol/digest.go | 37 + pkg/blockchain/sol/generate.go | 9 + pkg/blockchain/sol/idl_refresher/main.go | 97 ++ pkg/blockchain/sol/program.go | 168 +++ pkg/blockchain/sol/vault_integration_test.go | 258 +++++ pkg/blockchain/sol/withdrawal_finalizer.go | 379 +++++++ 27 files changed, 3371 insertions(+), 18 deletions(-) create mode 100644 devnet/sol-upgrade-authority.json create mode 100644 pkg/blockchain/sol/artifacts/README.md create mode 100644 pkg/blockchain/sol/artifacts/custody.json create mode 100755 pkg/blockchain/sol/artifacts/custody.so create mode 100644 pkg/blockchain/sol/custody/accounts.go create mode 100644 pkg/blockchain/sol/custody/constants.go create mode 100644 pkg/blockchain/sol/custody/discriminators.go create mode 100644 pkg/blockchain/sol/custody/doc.go create mode 100644 pkg/blockchain/sol/custody/errors.go create mode 100644 pkg/blockchain/sol/custody/events.go create mode 100644 pkg/blockchain/sol/custody/fetchers.go create mode 100644 pkg/blockchain/sol/custody/instructions.go create mode 100644 pkg/blockchain/sol/custody/program-id.go create mode 100644 pkg/blockchain/sol/custody/types.go create mode 100644 pkg/blockchain/sol/depositor.go create mode 100644 pkg/blockchain/sol/digest.go create mode 100644 pkg/blockchain/sol/generate.go create mode 100644 pkg/blockchain/sol/idl_refresher/main.go create mode 100644 pkg/blockchain/sol/program.go create mode 100644 pkg/blockchain/sol/vault_integration_test.go create mode 100644 pkg/blockchain/sol/withdrawal_finalizer.go diff --git a/devnet/README.md b/devnet/README.md index a43bd0f..c201cf1 100644 --- a/devnet/README.md +++ b/devnet/README.md @@ -10,7 +10,7 @@ VerifyExecution` itself, so no p2p mesh is needed. ## Run ```sh -make devnet # anvil + bitcoind(regtest) + rippled(standalone); blocks until all answer RPC +make devnet # anvil + bitcoind + rippled + solana-test-validator; blocks until all answer RPC make integration # go test -tags integration ./pkg/blockchain/... make devnet-down ``` @@ -35,6 +35,15 @@ wallet, the XRPL genesis master). `SignerListSet`s the vault over fresh signer keys, `TicketCreate`s a ticket, then deposits and runs the quorum withdrawal. Standalone rippled does not auto-close ledgers, so the test calls `ledger_accept` after each submit. +- **Solana** — the validator preloads the custody program **upgradeable** at its + fixed id (`--upgradeable-program`), upgrade authority = the vendored + `devnet/sol-upgrade-authority.json`. The test airdrop-funds the authority + + depositor, `Initialize`s the Config once (idempotent; gated on the upgrade + authority), deposits native SOL, then runs the quorum withdrawal. The Config + PDA is a singleton, so the signer set is **fixed** across runs and only the + withdrawalID is fresh — re-runs stay clean without a validator restart. The + validator image is multi-arch (no `platform:` pin — the Agave validator needs + AVX, which isn't emulable on Apple silicon). ## Optional overrides diff --git a/devnet/docker-compose.yml b/devnet/docker-compose.yml index 9a4191f..b5fc427 100644 --- a/devnet/docker-compose.yml +++ b/devnet/docker-compose.yml @@ -28,6 +28,35 @@ services: ports: - "18443:18443" + # Solana validator with the custody program preloaded upgradeable at its fixed + # id, upgrade authority = the vendored devnet key (so the test's Initialize, + # gated on the upgrade authority, succeeds). NO platform pin: the Agave + # validator needs AVX (not emulable on Apple silicon); beeman's image is + # multi-arch and pulls the native arm64 build. + solana: + image: beeman/solana-test-validator:3.1.14 + # The Agave validator uses io_uring; Docker Desktop's default seccomp + # profile blocks io_uring_setup (EPERM) even though the VM kernel supports + # it, so the validator aborts. Allow it. + # TODO: replace `unconfined` with a scoped profile (Docker default + + # io_uring_setup/enter/register) committed at devnet/seccomp.json, so we + # don't drop all syscall filtering for the validator container. + security_opt: + - seccomp=unconfined + command: + - solana-test-validator + - --reset + - --quiet + - --upgradeable-program + - "98eVpih8X9CAcgU9bzNB9V7VtkRrnFZUmqzEnsq7cfmg" + - /artifacts/custody.so + - "FdnkxpbVrjX9LGTXhQv5z1wkKc1sppJEKLaaVhQc2Z1f" + volumes: + - ../pkg/blockchain/sol/artifacts/custody.so:/artifacts/custody.so:ro + ports: + - "8899:8899" + - "8900:8900" + rippled: image: xrpllabsofficial/xrpld:latest platform: linux/amd64 diff --git a/devnet/sol-upgrade-authority.json b/devnet/sol-upgrade-authority.json new file mode 100644 index 0000000..1b6956a --- /dev/null +++ b/devnet/sol-upgrade-authority.json @@ -0,0 +1 @@ +[203,153,124,228,11,213,175,145,240,141,1,21,109,98,159,137,119,169,88,42,214,87,227,7,120,38,81,127,84,163,237,228,217,112,67,201,246,128,40,98,207,41,196,91,232,32,202,225,23,4,219,235,110,238,181,69,63,176,155,85,188,247,224,94] \ No newline at end of file diff --git a/devnet/wait/main.go b/devnet/wait/main.go index 8adabac..5acc53f 100644 --- a/devnet/wait/main.go +++ b/devnet/wait/main.go @@ -29,6 +29,8 @@ func main() { body: `{"jsonrpc":"1.0","id":1,"method":"getblockchaininfo","params":[]}`}, {name: "rippled", url: envOr("XRPL_RPC_URL", "http://127.0.0.1:5005"), body: `{"method":"server_info","params":[{}]}`}, + {name: "solana", url: envOr("SOL_RPC_URL", "http://127.0.0.1:8899"), + body: `{"jsonrpc":"2.0","id":1,"method":"getHealth"}`}, } deadline := time.Now().Add(90 * time.Second) diff --git a/go.mod b/go.mod index e6be6fa..565024c 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,9 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 github.com/ethereum/go-ethereum v1.17.3 github.com/fxamacker/cbor/v2 v2.9.1 + github.com/gagliardetto/anchor-go v1.0.0 + github.com/gagliardetto/binary v0.8.0 + github.com/gagliardetto/solana-go v1.21.0 github.com/ipfs/go-cid v0.0.6 github.com/whyrusleeping/cbor-gen v0.3.1 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 @@ -20,50 +23,77 @@ require ( github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect github.com/StackExchange/wmi v1.2.1 // indirect + github.com/benbjohnson/clock v1.3.5 // indirect github.com/bits-and-blooms/bitset v1.24.4 // indirect + github.com/blendle/zapdriver v1.3.1 // indirect github.com/bsv-blockchain/go-sdk v1.2.9 // indirect github.com/btcsuite/btcd/btcec/v2 v2.3.5 // indirect github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect + github.com/dave/jennifer v1.7.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/deckarep/golang-set/v2 v2.6.0 // indirect github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect github.com/decred/dcrd/crypto/ripemd160 v1.0.2 // indirect github.com/ethereum/c-kzg-4844/v2 v2.1.6 // indirect - github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/gagliardetto/treeout v0.1.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect + github.com/goccy/go-json v0.10.6 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/gorilla/websocket v1.5.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect github.com/holiman/uint256 v1.3.2 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kcalvinalvin/anet v0.0.0-20251112173137-d8ddc1f6dbee // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.0.9 // indirect + github.com/logrusorgru/aurora v2.0.3+incompatible // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 // indirect github.com/minio/sha256-simd v1.0.0 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/mr-tron/base58 v1.1.3 // indirect + github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect + github.com/mr-tron/base58 v1.2.0 // indirect github.com/multiformats/go-base32 v0.0.3 // indirect github.com/multiformats/go-base36 v0.1.0 // indirect github.com/multiformats/go-multibase v0.0.3 // indirect github.com/multiformats/go-multihash v0.0.13 // indirect github.com/multiformats/go-varint v0.0.5 // indirect + github.com/oasisprotocol/curve25519-voi v0.0.0-20251114093237-2ab5a27a1729 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/streamingfast/logging v0.0.0-20250404134358-92b15d2fbd2e // indirect github.com/supranational/blst v0.3.16 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + github.com/tidwall/sjson v1.2.5 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect + github.com/tyler-smith/go-bip39 v1.1.0 // indirect github.com/ugorji/go/codec v1.2.11 // indirect github.com/x448/float16 v0.8.4 // indirect + go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.40.0 // indirect go.opentelemetry.io/otel/metric v1.40.0 // indirect go.opentelemetry.io/otel/trace v1.40.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/ratelimit v0.3.1 // indirect + go.uber.org/zap v1.27.0 // indirect golang.org/x/crypto v0.48.0 // indirect + golang.org/x/mod v0.33.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.41.0 // indirect + golang.org/x/term v0.40.0 // indirect + golang.org/x/time v0.11.0 // indirect ) diff --git a/go.sum b/go.sum index f00d0b7..50c736a 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= +github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0= github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= @@ -10,10 +12,15 @@ github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDO github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= +github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE= github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/blendle/zapdriver v1.3.1 h1:C3dydBOWYRiOk+B8X9IVZ5IOe+7cl+tGOexN4QqHfpE= +github.com/blendle/zapdriver v1.3.1/go.mod h1:mdXfREi6u5MArG4j9fewC+FGnXaBR+T4Ox4J2u4eHCc= github.com/bsv-blockchain/go-sdk v1.2.9 h1:LwFzuts+J5X7A+ECx0LNowtUgIahCkNNlXckdiEMSDk= github.com/bsv-blockchain/go-sdk v1.2.9/go.mod h1:KiHWa/hblo3Bzr+IsX11v0sn1E6elGbNX0VXl5mOq6E= github.com/btcsuite/btcd v0.25.0 h1:JPbjwvHGpSywBRuorFFqTjaVP4y6Qw69XJ1nQ6MyWJM= @@ -48,6 +55,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/crate-crypto/go-eth-kzg v1.5.0 h1:FYRiJMJG2iv+2Dy3fi14SVGjcPteZ5HAAUe4YWlJygc= github.com/crate-crypto/go-eth-kzg v1.5.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= +github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo= +github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -71,12 +80,22 @@ github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJ github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= github.com/ethereum/go-ethereum v1.17.3 h1:Ev/sQHH+UdKZHWjuVzhu2pxhi/sXaPZl23Q+Q5LDd4Q= github.com/ethereum/go-ethereum v1.17.3/go.mod h1:f2EhRwqewIZkGoQekywI2Y2RZAMTSavLNkD9qItFy1A= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ= github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/gagliardetto/anchor-go v1.0.0 h1:YNt9I/9NOrNzz5uuzfzByAcbp39Ft07w63iPqC/wi34= +github.com/gagliardetto/anchor-go v1.0.0/go.mod h1:X6c9bx9JnmwNiyy8hmV5pAsq1c/zzPvkdzeq9/qmlCg= +github.com/gagliardetto/binary v0.8.0 h1:U9ahc45v9HW0d15LoN++vIXSJyqR/pWw8DDlhd7zvxg= +github.com/gagliardetto/binary v0.8.0/go.mod h1:2tfj51g5o9dnvsc+fL3Jxr22MuWzYXwx9wEoN0XQ7/c= +github.com/gagliardetto/solana-go v1.21.0 h1:ZswwwDOFuD/7Hk/k3b6dyLetmvEJuyLmQU7xIziCZgo= +github.com/gagliardetto/solana-go v1.21.0/go.mod h1:coSlnlih2oLWNbkVDeTiMvodhnheX/oaJ7h53mei4e8= +github.com/gagliardetto/treeout v0.1.4 h1:ozeYerrLCmCubo1TcIjFiOWTTGteOOHND1twdFpgwaw= +github.com/gagliardetto/treeout v0.1.4/go.mod h1:loUefvXTrlRG5rYmJmExNryyBRh8f89VZhmMOyCyqok= github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI= github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= @@ -89,6 +108,8 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= +github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -108,8 +129,8 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grafana/pyroscope-go v1.2.7 h1:VWBBlqxjyR0Cwk2W6UrE8CdcdD80GOFNutj0Kb1T8ac= github.com/grafana/pyroscope-go v1.2.7/go.mod h1:o/bpSLiJYYP6HQtvcoVKiE9s5RiNgjYTj1DhiddP2Pc= github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og= @@ -140,21 +161,28 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kcalvinalvin/anet v0.0.0-20251112173137-d8ddc1f6dbee h1:FPP9HDkBbPyniu+u7FHZg+kKFX1WW0gxOGteJ0h3AJk= github.com/kcalvinalvin/anet v0.0.0-20251112173137-d8ddc1f6dbee/go.mod h1:N6sz6HwJAenJ6d+/xmSl0ikfV05ZrVGmjt1ryy/WOtE= -github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= -github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= +github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= @@ -166,17 +194,23 @@ github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8Rv github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 h1:mPMvm6X6tf4w8y7j9YIt6V9jfWhL6QlbEc7CCmeQlWk= +github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1/go.mod h1:ye2e/VUEtE2BHE+G/QcKkcLQVAEJoYRFj5VUOQatCRE= github.com/mr-tron/base58 v1.1.0/go.mod h1:xcD2VGqlgYjBdcBLw+TuYLr8afG+Hj8g2eTVqeSzSU8= -github.com/mr-tron/base58 v1.1.3 h1:v+sk57XuaCKGXpWtVBX8YJzO7hMGx4Aajh4TQbdEFdc= github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= +github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/multiformats/go-base32 v0.0.3 h1:tw5+NhuwaOjJCC5Pp82QuXbrmLzWg7uxlMFp8Nq/kkI= github.com/multiformats/go-base32 v0.0.3/go.mod h1:pLiuGC8y0QR3Ue4Zug5UzK9LjgbkL8NSQj0zQ5Nz/AA= github.com/multiformats/go-base36 v0.1.0 h1:JR6TyF7JjGd3m6FbLU2cOxhC0Li8z8dLNGQ89tUg4F4= @@ -187,6 +221,10 @@ github.com/multiformats/go-multihash v0.0.13 h1:06x+mk/zj1FoMsgNejLpy6QTvJqlSt/B github.com/multiformats/go-multihash v0.0.13/go.mod h1:VdAWLKTwram9oKAatUcLxBNUjdtcVwxObEQBtRfuyjc= github.com/multiformats/go-varint v0.0.5 h1:XVZwSo04Cs3j/jS0uAEPpT3JY6DzMcVLLoWOSnCxOjg= github.com/multiformats/go-varint v0.0.5/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= +github.com/oasisprotocol/curve25519-voi v0.0.0-20251114093237-2ab5a27a1729 h1:yfQ2sO9WJXUAIUR+g7NUkxJSKCAFJcR5sUDu+ZmjTZI= +github.com/oasisprotocol/curve25519-voi v0.0.0-20251114093237-2ab5a27a1729/go.mod h1:hVoHR2EVESiICEMbg137etN/Lx+lSrHPTD39Z/uE+2s= +github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQmzR3rNLYGGz4g/UgFcjb28p/viDM= @@ -201,6 +239,7 @@ github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM= github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -223,20 +262,39 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091/go.mod h1:VlduQ80JcGJSargkRU4Sg9Xo63wZD/l8A5NC/Uo1/uU= +github.com/streamingfast/logging v0.0.0-20250404134358-92b15d2fbd2e h1:qGVGDR2/bXLyR498un1hvhDQPUJ/m14JBRTJz+c67Bc= +github.com/streamingfast/logging v0.0.0-20250404134358-92b15d2fbd2e/go.mod h1:VlduQ80JcGJSargkRU4Sg9Xo63wZD/l8A5NC/Uo1/uU= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE= github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/test-go/testify v1.1.4 h1:Tf9lntrKUMHiXQ07qBScBTSA0dhYQlu83hswqelv1iE= +github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= +github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= @@ -247,6 +305,9 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= +go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= @@ -257,38 +318,89 @@ go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ7 go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0= +go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= -golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= -golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/blockchain/evm/withdrawal_finalizer.go b/pkg/blockchain/evm/withdrawal_finalizer.go index 6856956..7418aa0 100644 --- a/pkg/blockchain/evm/withdrawal_finalizer.go +++ b/pkg/blockchain/evm/withdrawal_finalizer.go @@ -242,6 +242,11 @@ func (f *WithdrawalFinalizer) Submit(ctx context.Context, merged []byte) (core.T if err != nil { return core.TxRef{}, fmt.Errorf("execute: %w", err) } + // Block until mined so the returned ref corresponds to an executed + // withdrawal (and a subsequent VerifyExecution observes it). + if err := waitMined(ctx, f.client, tx); err != nil { + return core.TxRef{}, err + } return core.TxRef{Hash: tx.Hash(), Raw: tx.Hash().Hex()}, nil } diff --git a/pkg/blockchain/sol/artifacts/README.md b/pkg/blockchain/sol/artifacts/README.md new file mode 100644 index 0000000..7c569d7 --- /dev/null +++ b/pkg/blockchain/sol/artifacts/README.md @@ -0,0 +1,46 @@ +# Solana program artifacts + +Vendored artifacts for the custody Anchor program: + +- **`custody.json`** — the Anchor **IDL** (Solana's ABI analog). Source of truth + for the generated `../custody` bindings: `idl_refresher` reads this and emits + the Go client via anchor-go's generator library (the Solana parallel of the + EVM `abi_refresher` driving abigen). A program change shows up here as a + reviewable IDL diff. +- **`custody.so`** — the compiled BPF program (the bytecode analog). Used by the + integration-test devnet, which preloads it into `solana-test-validator` at the + fixed program id — no on-chain deploy or Anchor toolchain needed at test time. + +Program id (fixed, `declare_id!`): `98eVpih8X9CAcgU9bzNB9V7VtkRrnFZUmqzEnsq7cfmg`. + +## Regenerate the bindings (common case) + +```sh +make generate # go generate ./... → idl_refresher (+ evm abi_refresher, cbor-gen) +``` + +or directly: + +```sh +go generate ./pkg/blockchain/sol/... +``` + +Rewrites `../custody/*.go` from `custody.json`. Commit the result. + +## Refresh the artifacts (only when the program changes) + +Both files come from `anchor build` in the repo that owns the Rust source +(`clearnet/contracts/solana`, Anchor 0.31). Requires the Solana + Anchor +toolchain (`solana` / `cargo-build-sbf` + `anchor` 0.31 via avm) — needed only +to refresh, never to run the tests: + +```sh +cd ../clearnet/contracts/solana +anchor build +cp target/idl/custody.json /pkg/blockchain/sol/artifacts/custody.json +cp target/deploy/custody.so /pkg/blockchain/sol/artifacts/custody.so +# then, in the SDK: +make generate +``` + +Review the `custody.json` + generated `../custody/*.go` diffs together. diff --git a/pkg/blockchain/sol/artifacts/custody.json b/pkg/blockchain/sol/artifacts/custody.json new file mode 100644 index 0000000..3e9f1c3 --- /dev/null +++ b/pkg/blockchain/sol/artifacts/custody.json @@ -0,0 +1,1040 @@ +{ + "address": "98eVpih8X9CAcgU9bzNB9V7VtkRrnFZUmqzEnsq7cfmg", + "metadata": { + "name": "custody", + "version": "0.1.0", + "spec": "0.1.0", + "description": "Yellow custody Solana vault program — analogue of Custody.sol" + }, + "instructions": [ + { + "name": "deposit_sol", + "docs": [ + "Native SOL deposit crediting the 20-byte clearnet `account`." + ], + "discriminator": [ + 108, + 81, + 78, + 117, + 125, + 155, + 56, + 200 + ], + "accounts": [ + { + "name": "depositor", + "writable": true, + "signer": true + }, + { + "name": "vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116 + ] + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "account", + "type": { + "array": [ + "u8", + 20 + ] + } + }, + { + "name": "amount", + "type": "u64" + } + ] + }, + { + "name": "deposit_spl", + "docs": [ + "SPL token deposit crediting the 20-byte clearnet `account`." + ], + "discriminator": [ + 224, + 0, + 198, + 175, + 198, + 47, + 105, + 204 + ], + "accounts": [ + { + "name": "depositor", + "writable": true, + "signer": true + }, + { + "name": "mint" + }, + { + "name": "depositor_ata", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "depositor" + }, + { + "kind": "const", + "value": [ + 6, + 221, + 246, + 225, + 215, + 101, + 161, + 147, + 217, + 203, + 225, + 70, + 206, + 235, + 121, + 172, + 28, + 180, + 133, + 237, + 95, + 91, + 55, + 145, + 58, + 140, + 245, + 133, + 126, + 255, + 0, + 169 + ] + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "vault", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116 + ] + } + ] + } + }, + { + "name": "vault_ata", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "vault" + }, + { + "kind": "const", + "value": [ + 6, + 221, + 246, + 225, + 215, + 101, + 161, + 147, + 217, + 203, + 225, + 70, + 206, + 235, + 121, + 172, + 28, + 180, + 133, + 237, + 95, + 91, + 55, + 145, + 58, + 140, + 245, + 133, + 126, + 255, + 0, + 169 + ] + }, + { + "kind": "account", + "path": "mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "token_program", + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "account", + "type": { + "array": [ + "u8", + 20 + ] + } + }, + { + "name": "amount", + "type": "u64" + } + ] + }, + { + "name": "execute", + "docs": [ + "Execute a quorum-authorized withdrawal (native or SPL)." + ], + "discriminator": [ + 130, + 221, + 242, + 154, + 13, + 193, + 189, + 29 + ], + "accounts": [ + { + "name": "fee_payer", + "docs": [ + "Pays the WithdrawalRecord rent and the transaction fee. Authorizes no", + "custody action — distinct from the provider custody signers." + ], + "writable": true, + "signer": true + }, + { + "name": "config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 111, + 110, + 102, + 105, + 103 + ] + } + ] + } + }, + { + "name": "vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116 + ] + } + ] + } + }, + { + "name": "withdrawal", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 119, + 105, + 116, + 104, + 100, + 114, + 97, + 119, + 97, + 108 + ] + }, + { + "kind": "arg", + "path": "withdrawal_id" + } + ] + } + }, + { + "name": "recipient", + "docs": [ + "for SPL it is only used to derive the recipient ATA. Pinned to `to`." + ], + "writable": true + }, + { + "name": "instructions", + "docs": [ + "address constraint plus `load_instruction_at_checked` both verify it." + ], + "address": "Sysvar1nstructions1111111111111111111111111" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "to", + "type": "pubkey" + }, + { + "name": "mint", + "type": "pubkey" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "withdrawal_id", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "sig_ix_index", + "type": "u8" + } + ] + }, + { + "name": "initialize", + "docs": [ + "One-time setup of the Config PDA (signer set, threshold, chain_id)." + ], + "discriminator": [ + 175, + 175, + 109, + 31, + 13, + 152, + 155, + 237 + ], + "accounts": [ + { + "name": "config", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 111, + 110, + 102, + 105, + 103 + ] + } + ] + } + }, + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "program", + "address": "98eVpih8X9CAcgU9bzNB9V7VtkRrnFZUmqzEnsq7cfmg" + }, + { + "name": "program_data" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "signers", + "type": { + "vec": "pubkey" + } + }, + { + "name": "threshold", + "type": "u8" + }, + { + "name": "chain_id", + "type": "u64" + } + ] + }, + { + "name": "update_signers", + "docs": [ + "In-place signer rotation (ADR-013 Flow A)." + ], + "discriminator": [ + 228, + 82, + 68, + 150, + 92, + 66, + 140, + 174 + ], + "accounts": [ + { + "name": "config", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 111, + 110, + 102, + 105, + 103 + ] + } + ] + } + }, + { + "name": "instructions", + "address": "Sysvar1nstructions1111111111111111111111111" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "new_signers", + "type": { + "vec": "pubkey" + } + }, + { + "name": "new_threshold", + "type": "u8" + }, + { + "name": "sig_ix_index", + "type": "u8" + } + ] + } + ], + "accounts": [ + { + "name": "Config", + "discriminator": [ + 155, + 12, + 170, + 224, + 30, + 250, + 204, + 130 + ] + }, + { + "name": "WithdrawalRecord", + "discriminator": [ + 88, + 59, + 154, + 202, + 216, + 210, + 211, + 237 + ] + } + ], + "events": [ + { + "name": "Deposited", + "discriminator": [ + 111, + 141, + 26, + 45, + 161, + 35, + 100, + 57 + ] + }, + { + "name": "Executed", + "discriminator": [ + 8, + 232, + 139, + 132, + 197, + 45, + 29, + 164 + ] + }, + { + "name": "SignersUpdated", + "discriminator": [ + 71, + 177, + 47, + 237, + 194, + 42, + 20, + 105 + ] + } + ], + "errors": [ + { + "code": 6000, + "name": "NotUpgradeAuthority", + "msg": "initializer must be the program upgrade authority" + }, + { + "code": 6001, + "name": "ZeroAccount", + "msg": "clearnet account must not be the zero address" + }, + { + "code": 6002, + "name": "InvalidThreshold", + "msg": "threshold must be > 0 and <= number of signers" + }, + { + "code": 6003, + "name": "TooFewSigners", + "msg": "signer set must have at least 3 members" + }, + { + "code": 6004, + "name": "TooManySigners", + "msg": "signer set exceeds MAX_SIGNERS" + }, + { + "code": 6005, + "name": "DefaultSigner", + "msg": "signer set contains the default pubkey" + }, + { + "code": 6006, + "name": "SignersNotAscending", + "msg": "signers/pubkeys must be strictly ascending (byte-wise)" + }, + { + "code": 6007, + "name": "ZeroAmount", + "msg": "amount must be > 0" + }, + { + "code": 6008, + "name": "InvalidRecipient", + "msg": "recipient must not be the default pubkey and must match `to`" + }, + { + "code": 6009, + "name": "VaultAtaMismatch", + "msg": "vault ATA does not match the derived associated token address" + }, + { + "code": 6010, + "name": "RecipientAtaMismatch", + "msg": "recipient ATA does not match the derived associated token address" + }, + { + "code": 6011, + "name": "MissingTokenAccounts", + "msg": "SPL withdrawal requires token_program, vault_ata, recipient_ata in remaining accounts" + }, + { + "code": 6012, + "name": "WrongTokenProgram", + "msg": "token_program account is not the SPL Token program" + }, + { + "code": 6013, + "name": "SigIxLoad", + "msg": "failed to load the Ed25519 companion instruction" + }, + { + "code": 6014, + "name": "SigIxProgram", + "msg": "companion instruction is not the Ed25519 precompile" + }, + { + "code": 6015, + "name": "SigIxMalformed", + "msg": "Ed25519 instruction data is malformed" + }, + { + "code": 6016, + "name": "SigIxOffsets", + "msg": "Ed25519 offsets are not self-referencing (instruction_index != u16::MAX)" + }, + { + "code": 6017, + "name": "SigIxMsgSize", + "msg": "Ed25519 message size is not 32 bytes" + }, + { + "code": 6018, + "name": "SigIxDigest", + "msg": "Ed25519 signed message does not equal the recomputed digest" + }, + { + "code": 6019, + "name": "SignerNotAuthorized", + "msg": "a verified signer is not in the authorized config set" + }, + { + "code": 6020, + "name": "QuorumTooSmall", + "msg": "fewer than threshold authorized signatures" + } + ], + "types": [ + { + "name": "Config", + "docs": [ + "Config PDA — seeds `[\"config\"]`. The on-chain analogue of `Custody.sol`'s", + "signer set + threshold + nonce. `signers` is kept strictly ascending so a", + "linear membership scan in the Ed25519 verifier is unambiguous." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "signers", + "type": { + "vec": "pubkey" + } + }, + { + "name": "threshold", + "type": "u8" + }, + { + "name": "signer_nonce", + "type": "u64" + }, + { + "name": "chain_id", + "type": "u64" + }, + { + "name": "bump", + "type": "u8" + } + ] + } + }, + { + "name": "Deposited", + "docs": [ + "Emitted by `deposit_sol` / `deposit_spl` via `emit_cpi!`. The custody", + "watcher decodes these from the self-CPI inner instructions and turns each", + "into a `chains.DepositEvent`. `mint == Pubkey::default()` denotes native", + "SOL (the analogue of EVM's `asset == address(0)`). `account` is the 20-byte", + "clearnet account the deposit credits." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "depositor", + "type": "pubkey" + }, + { + "name": "account", + "type": { + "array": [ + "u8", + 20 + ] + } + }, + { + "name": "mint", + "type": "pubkey" + }, + { + "name": "amount", + "type": "u64" + } + ] + } + }, + { + "name": "Executed", + "docs": [ + "Emitted by `execute` via `emit_cpi!`. The watcher writes a ledger row keyed", + "by `withdrawal_id` (the reconciliation key against `signing_wal`)." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "withdrawal_id", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "to", + "type": "pubkey" + }, + { + "name": "mint", + "type": "pubkey" + }, + { + "name": "amount", + "type": "u64" + } + ] + } + }, + { + "name": "SignersUpdated", + "docs": [ + "Emitted by `update_signers` via `emit_cpi!`." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "signers", + "type": { + "vec": "pubkey" + } + }, + { + "name": "threshold", + "type": "u8" + }, + { + "name": "signer_nonce", + "type": "u64" + } + ] + } + }, + { + "name": "WithdrawalRecord", + "docs": [ + "WithdrawalRecord PDA — seeds `[\"withdrawal\", withdrawal_id]`. Its mere", + "existence is the replay guard: `execute` creates it with `init`, so a", + "second execution of the same `withdrawal_id` fails at account creation.", + "The analogue of `Custody.sol`'s `executed[withdrawalId]` storage slot;", + "never closed (closing would re-enable replay)." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "to", + "type": "pubkey" + }, + { + "name": "mint", + "type": "pubkey" + }, + { + "name": "amount", + "type": "u64" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/pkg/blockchain/sol/artifacts/custody.so b/pkg/blockchain/sol/artifacts/custody.so new file mode 100755 index 0000000000000000000000000000000000000000..b5668340aed1352a09d761b5b196452073d62903 GIT binary patch literal 367320 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≦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 literal 0 HcmV?d00001 diff --git a/pkg/blockchain/sol/custody/accounts.go b/pkg/blockchain/sol/custody/accounts.go new file mode 100644 index 0000000..d4876b1 --- /dev/null +++ b/pkg/blockchain/sol/custody/accounts.go @@ -0,0 +1,69 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains parsers for the accounts defined in the IDL. + +package custody + +import ( + "fmt" + binary "github.com/gagliardetto/binary" +) + +func ParseAnyAccount(accountData []byte) (any, error) { + decoder := binary.NewBorshDecoder(accountData) + discriminator, err := decoder.ReadDiscriminator() + if err != nil { + return nil, fmt.Errorf("failed to peek account discriminator: %w", err) + } + switch discriminator { + case Account_Config: + value := new(Config) + err := value.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal account as Config: %w", err) + } + return value, nil + case Account_WithdrawalRecord: + value := new(WithdrawalRecord) + err := value.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal account as WithdrawalRecord: %w", err) + } + return value, nil + default: + return nil, fmt.Errorf("unknown discriminator: %s", binary.FormatDiscriminator(discriminator)) + } +} + +func ParseAccount_Config(accountData []byte) (*Config, error) { + decoder := binary.NewBorshDecoder(accountData) + discriminator, err := decoder.ReadDiscriminator() + if err != nil { + return nil, fmt.Errorf("failed to peek discriminator: %w", err) + } + if discriminator != Account_Config { + return nil, fmt.Errorf("expected discriminator %v, got %s", Account_Config, binary.FormatDiscriminator(discriminator)) + } + acc := new(Config) + err = acc.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal account of type Config: %w", err) + } + return acc, nil +} + +func ParseAccount_WithdrawalRecord(accountData []byte) (*WithdrawalRecord, error) { + decoder := binary.NewBorshDecoder(accountData) + discriminator, err := decoder.ReadDiscriminator() + if err != nil { + return nil, fmt.Errorf("failed to peek discriminator: %w", err) + } + if discriminator != Account_WithdrawalRecord { + return nil, fmt.Errorf("expected discriminator %v, got %s", Account_WithdrawalRecord, binary.FormatDiscriminator(discriminator)) + } + acc := new(WithdrawalRecord) + err = acc.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal account of type WithdrawalRecord: %w", err) + } + return acc, nil +} diff --git a/pkg/blockchain/sol/custody/constants.go b/pkg/blockchain/sol/custody/constants.go new file mode 100644 index 0000000..a5c0fea --- /dev/null +++ b/pkg/blockchain/sol/custody/constants.go @@ -0,0 +1,4 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains constants. + +package custody diff --git a/pkg/blockchain/sol/custody/discriminators.go b/pkg/blockchain/sol/custody/discriminators.go new file mode 100644 index 0000000..ee8dc96 --- /dev/null +++ b/pkg/blockchain/sol/custody/discriminators.go @@ -0,0 +1,26 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains the discriminators for accounts and events defined in the IDL. + +package custody + +// Account discriminators +var ( + Account_Config = [8]byte{155, 12, 170, 224, 30, 250, 204, 130} + Account_WithdrawalRecord = [8]byte{88, 59, 154, 202, 216, 210, 211, 237} +) + +// Event discriminators +var ( + Event_Deposited = [8]byte{111, 141, 26, 45, 161, 35, 100, 57} + Event_Executed = [8]byte{8, 232, 139, 132, 197, 45, 29, 164} + Event_SignersUpdated = [8]byte{71, 177, 47, 237, 194, 42, 20, 105} +) + +// Instruction discriminators +var ( + Instruction_DepositSol = [8]byte{108, 81, 78, 117, 125, 155, 56, 200} + Instruction_DepositSpl = [8]byte{224, 0, 198, 175, 198, 47, 105, 204} + Instruction_Execute = [8]byte{130, 221, 242, 154, 13, 193, 189, 29} + Instruction_Initialize = [8]byte{175, 175, 109, 31, 13, 152, 155, 237} + Instruction_UpdateSigners = [8]byte{228, 82, 68, 150, 92, 66, 140, 174} +) diff --git a/pkg/blockchain/sol/custody/doc.go b/pkg/blockchain/sol/custody/doc.go new file mode 100644 index 0000000..eadf7f1 --- /dev/null +++ b/pkg/blockchain/sol/custody/doc.go @@ -0,0 +1,7 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains documentation and example usage for the generated code. + +package custody + +// No documentation available from the IDL. +// Please refer to the IDL source or the program documentation for more information. diff --git a/pkg/blockchain/sol/custody/errors.go b/pkg/blockchain/sol/custody/errors.go new file mode 100644 index 0000000..095c2d3 --- /dev/null +++ b/pkg/blockchain/sol/custody/errors.go @@ -0,0 +1,4 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains errors. + +package custody diff --git a/pkg/blockchain/sol/custody/events.go b/pkg/blockchain/sol/custody/events.go new file mode 100644 index 0000000..a170a46 --- /dev/null +++ b/pkg/blockchain/sol/custody/events.go @@ -0,0 +1,93 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains parsers for the events defined in the IDL. + +package custody + +import ( + "fmt" + binary "github.com/gagliardetto/binary" +) + +func ParseAnyEvent(eventData []byte) (any, error) { + decoder := binary.NewBorshDecoder(eventData) + discriminator, err := decoder.ReadDiscriminator() + if err != nil { + return nil, fmt.Errorf("failed to peek event discriminator: %w", err) + } + switch discriminator { + case Event_Deposited: + value := new(Deposited) + err := value.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal event as Deposited: %w", err) + } + return value, nil + case Event_Executed: + value := new(Executed) + err := value.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal event as Executed: %w", err) + } + return value, nil + case Event_SignersUpdated: + value := new(SignersUpdated) + err := value.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal event as SignersUpdated: %w", err) + } + return value, nil + default: + return nil, fmt.Errorf("unknown discriminator: %s", binary.FormatDiscriminator(discriminator)) + } +} + +func ParseEvent_Deposited(eventData []byte) (*Deposited, error) { + decoder := binary.NewBorshDecoder(eventData) + discriminator, err := decoder.ReadDiscriminator() + if err != nil { + return nil, fmt.Errorf("failed to peek discriminator: %w", err) + } + if discriminator != Event_Deposited { + return nil, fmt.Errorf("expected discriminator %v, got %s", Event_Deposited, binary.FormatDiscriminator(discriminator)) + } + event := new(Deposited) + err = event.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal event of type Deposited: %w", err) + } + return event, nil +} + +func ParseEvent_Executed(eventData []byte) (*Executed, error) { + decoder := binary.NewBorshDecoder(eventData) + discriminator, err := decoder.ReadDiscriminator() + if err != nil { + return nil, fmt.Errorf("failed to peek discriminator: %w", err) + } + if discriminator != Event_Executed { + return nil, fmt.Errorf("expected discriminator %v, got %s", Event_Executed, binary.FormatDiscriminator(discriminator)) + } + event := new(Executed) + err = event.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal event of type Executed: %w", err) + } + return event, nil +} + +func ParseEvent_SignersUpdated(eventData []byte) (*SignersUpdated, error) { + decoder := binary.NewBorshDecoder(eventData) + discriminator, err := decoder.ReadDiscriminator() + if err != nil { + return nil, fmt.Errorf("failed to peek discriminator: %w", err) + } + if discriminator != Event_SignersUpdated { + return nil, fmt.Errorf("expected discriminator %v, got %s", Event_SignersUpdated, binary.FormatDiscriminator(discriminator)) + } + event := new(SignersUpdated) + err = event.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal event of type SignersUpdated: %w", err) + } + return event, nil +} diff --git a/pkg/blockchain/sol/custody/fetchers.go b/pkg/blockchain/sol/custody/fetchers.go new file mode 100644 index 0000000..012a12c --- /dev/null +++ b/pkg/blockchain/sol/custody/fetchers.go @@ -0,0 +1,4 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains fetcher functions. + +package custody diff --git a/pkg/blockchain/sol/custody/instructions.go b/pkg/blockchain/sol/custody/instructions.go new file mode 100644 index 0000000..2f50ba9 --- /dev/null +++ b/pkg/blockchain/sol/custody/instructions.go @@ -0,0 +1,357 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains instructions. + +package custody + +import ( + "bytes" + "fmt" + errors "github.com/gagliardetto/anchor-go/errors" + binary "github.com/gagliardetto/binary" + solanago "github.com/gagliardetto/solana-go" +) + +// Builds a "deposit_sol" instruction. +// Native SOL deposit crediting the 20-byte clearnet `account`. +func NewDepositSolInstruction( + // Params: + accountParam [20]uint8, + amountParam uint64, + + // Accounts: + depositorAccount solanago.PublicKey, + vaultAccount solanago.PublicKey, + systemProgramAccount solanago.PublicKey, + eventAuthorityAccount solanago.PublicKey, + programAccount solanago.PublicKey, +) (solanago.Instruction, error) { + buf__ := new(bytes.Buffer) + enc__ := binary.NewBorshEncoder(buf__) + + // Encode the instruction discriminator. + err := enc__.WriteBytes(Instruction_DepositSol[:], false) + if err != nil { + return nil, fmt.Errorf("failed to write instruction discriminator: %w", err) + } + { + // Serialize `accountParam`: + err = enc__.Encode(accountParam) + if err != nil { + return nil, errors.NewField("accountParam", err) + } + // Serialize `amountParam`: + err = enc__.Encode(amountParam) + if err != nil { + return nil, errors.NewField("amountParam", err) + } + } + accounts__ := solanago.AccountMetaSlice{} + + // Add the accounts to the instruction. + { + // Account 0 "depositor": Writable, Signer, Required + accounts__.Append(solanago.NewAccountMeta(depositorAccount, true, true)) + // Account 1 "vault": Writable, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(vaultAccount, true, false)) + // Account 2 "system_program": Read-only, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(systemProgramAccount, false, false)) + // Account 3 "event_authority": Read-only, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(eventAuthorityAccount, false, false)) + // Account 4 "program": Read-only, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(programAccount, false, false)) + } + + // Create the instruction. + return solanago.NewInstruction( + ProgramID, + accounts__, + buf__.Bytes(), + ), nil +} + +// Builds a "deposit_spl" instruction. +// SPL token deposit crediting the 20-byte clearnet `account`. +func NewDepositSplInstruction( + // Params: + accountParam [20]uint8, + amountParam uint64, + + // Accounts: + depositorAccount solanago.PublicKey, + mintAccount solanago.PublicKey, + depositorAtaAccount solanago.PublicKey, + vaultAccount solanago.PublicKey, + vaultAtaAccount solanago.PublicKey, + tokenProgramAccount solanago.PublicKey, + associatedTokenProgramAccount solanago.PublicKey, + eventAuthorityAccount solanago.PublicKey, + programAccount solanago.PublicKey, +) (solanago.Instruction, error) { + buf__ := new(bytes.Buffer) + enc__ := binary.NewBorshEncoder(buf__) + + // Encode the instruction discriminator. + err := enc__.WriteBytes(Instruction_DepositSpl[:], false) + if err != nil { + return nil, fmt.Errorf("failed to write instruction discriminator: %w", err) + } + { + // Serialize `accountParam`: + err = enc__.Encode(accountParam) + if err != nil { + return nil, errors.NewField("accountParam", err) + } + // Serialize `amountParam`: + err = enc__.Encode(amountParam) + if err != nil { + return nil, errors.NewField("amountParam", err) + } + } + accounts__ := solanago.AccountMetaSlice{} + + // Add the accounts to the instruction. + { + // Account 0 "depositor": Writable, Signer, Required + accounts__.Append(solanago.NewAccountMeta(depositorAccount, true, true)) + // Account 1 "mint": Read-only, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(mintAccount, false, false)) + // Account 2 "depositor_ata": Writable, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(depositorAtaAccount, true, false)) + // Account 3 "vault": Read-only, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(vaultAccount, false, false)) + // Account 4 "vault_ata": Writable, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(vaultAtaAccount, true, false)) + // Account 5 "token_program": Read-only, Non-signer, Required, Address: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA + accounts__.Append(solanago.NewAccountMeta(tokenProgramAccount, false, false)) + // Account 6 "associated_token_program": Read-only, Non-signer, Required, Address: ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL + accounts__.Append(solanago.NewAccountMeta(associatedTokenProgramAccount, false, false)) + // Account 7 "event_authority": Read-only, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(eventAuthorityAccount, false, false)) + // Account 8 "program": Read-only, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(programAccount, false, false)) + } + + // Create the instruction. + return solanago.NewInstruction( + ProgramID, + accounts__, + buf__.Bytes(), + ), nil +} + +// Builds a "execute" instruction. +// Execute a quorum-authorized withdrawal (native or SPL). +func NewExecuteInstruction( + // Params: + toParam solanago.PublicKey, + mintParam solanago.PublicKey, + amountParam uint64, + withdrawalIdParam [32]uint8, + sigIxIndexParam uint8, + + // Accounts: + feePayerAccount solanago.PublicKey, + configAccount solanago.PublicKey, + vaultAccount solanago.PublicKey, + withdrawalAccount solanago.PublicKey, + recipientAccount solanago.PublicKey, + instructionsAccount solanago.PublicKey, + systemProgramAccount solanago.PublicKey, + eventAuthorityAccount solanago.PublicKey, + programAccount solanago.PublicKey, +) (solanago.Instruction, error) { + buf__ := new(bytes.Buffer) + enc__ := binary.NewBorshEncoder(buf__) + + // Encode the instruction discriminator. + err := enc__.WriteBytes(Instruction_Execute[:], false) + if err != nil { + return nil, fmt.Errorf("failed to write instruction discriminator: %w", err) + } + { + // Serialize `toParam`: + err = enc__.Encode(toParam) + if err != nil { + return nil, errors.NewField("toParam", err) + } + // Serialize `mintParam`: + err = enc__.Encode(mintParam) + if err != nil { + return nil, errors.NewField("mintParam", err) + } + // Serialize `amountParam`: + err = enc__.Encode(amountParam) + if err != nil { + return nil, errors.NewField("amountParam", err) + } + // Serialize `withdrawalIdParam`: + err = enc__.Encode(withdrawalIdParam) + if err != nil { + return nil, errors.NewField("withdrawalIdParam", err) + } + // Serialize `sigIxIndexParam`: + err = enc__.Encode(sigIxIndexParam) + if err != nil { + return nil, errors.NewField("sigIxIndexParam", err) + } + } + accounts__ := solanago.AccountMetaSlice{} + + // Add the accounts to the instruction. + { + // Account 0 "fee_payer": Writable, Signer, Required + // Pays the WithdrawalRecord rent and the transaction fee. Authorizes no + // custody action — distinct from the provider custody signers. + accounts__.Append(solanago.NewAccountMeta(feePayerAccount, true, true)) + // Account 1 "config": Read-only, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(configAccount, false, false)) + // Account 2 "vault": Writable, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(vaultAccount, true, false)) + // Account 3 "withdrawal": Writable, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(withdrawalAccount, true, false)) + // Account 4 "recipient": Writable, Non-signer, Required + // for SPL it is only used to derive the recipient ATA. Pinned to `to`. + accounts__.Append(solanago.NewAccountMeta(recipientAccount, true, false)) + // Account 5 "instructions": Read-only, Non-signer, Required, Address: Sysvar1nstructions1111111111111111111111111 + // address constraint plus `load_instruction_at_checked` both verify it. + accounts__.Append(solanago.NewAccountMeta(instructionsAccount, false, false)) + // Account 6 "system_program": Read-only, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(systemProgramAccount, false, false)) + // Account 7 "event_authority": Read-only, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(eventAuthorityAccount, false, false)) + // Account 8 "program": Read-only, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(programAccount, false, false)) + } + + // Create the instruction. + return solanago.NewInstruction( + ProgramID, + accounts__, + buf__.Bytes(), + ), nil +} + +// Builds a "initialize" instruction. +// One-time setup of the Config PDA (signer set, threshold, chain_id). +func NewInitializeInstruction( + // Params: + signersParam []solanago.PublicKey, + thresholdParam uint8, + chainIdParam uint64, + + // Accounts: + configAccount solanago.PublicKey, + payerAccount solanago.PublicKey, + programAccount solanago.PublicKey, + programDataAccount solanago.PublicKey, + systemProgramAccount solanago.PublicKey, +) (solanago.Instruction, error) { + buf__ := new(bytes.Buffer) + enc__ := binary.NewBorshEncoder(buf__) + + // Encode the instruction discriminator. + err := enc__.WriteBytes(Instruction_Initialize[:], false) + if err != nil { + return nil, fmt.Errorf("failed to write instruction discriminator: %w", err) + } + { + // Serialize `signersParam`: + err = enc__.Encode(signersParam) + if err != nil { + return nil, errors.NewField("signersParam", err) + } + // Serialize `thresholdParam`: + err = enc__.Encode(thresholdParam) + if err != nil { + return nil, errors.NewField("thresholdParam", err) + } + // Serialize `chainIdParam`: + err = enc__.Encode(chainIdParam) + if err != nil { + return nil, errors.NewField("chainIdParam", err) + } + } + accounts__ := solanago.AccountMetaSlice{} + + // Add the accounts to the instruction. + { + // Account 0 "config": Writable, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(configAccount, true, false)) + // Account 1 "payer": Writable, Signer, Required + accounts__.Append(solanago.NewAccountMeta(payerAccount, true, true)) + // Account 2 "program": Read-only, Non-signer, Required, Address: 98eVpih8X9CAcgU9bzNB9V7VtkRrnFZUmqzEnsq7cfmg + accounts__.Append(solanago.NewAccountMeta(programAccount, false, false)) + // Account 3 "program_data": Read-only, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(programDataAccount, false, false)) + // Account 4 "system_program": Read-only, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(systemProgramAccount, false, false)) + } + + // Create the instruction. + return solanago.NewInstruction( + ProgramID, + accounts__, + buf__.Bytes(), + ), nil +} + +// Builds a "update_signers" instruction. +// In-place signer rotation (ADR-013 Flow A). +func NewUpdateSignersInstruction( + // Params: + newSignersParam []solanago.PublicKey, + newThresholdParam uint8, + sigIxIndexParam uint8, + + // Accounts: + configAccount solanago.PublicKey, + instructionsAccount solanago.PublicKey, + eventAuthorityAccount solanago.PublicKey, + programAccount solanago.PublicKey, +) (solanago.Instruction, error) { + buf__ := new(bytes.Buffer) + enc__ := binary.NewBorshEncoder(buf__) + + // Encode the instruction discriminator. + err := enc__.WriteBytes(Instruction_UpdateSigners[:], false) + if err != nil { + return nil, fmt.Errorf("failed to write instruction discriminator: %w", err) + } + { + // Serialize `newSignersParam`: + err = enc__.Encode(newSignersParam) + if err != nil { + return nil, errors.NewField("newSignersParam", err) + } + // Serialize `newThresholdParam`: + err = enc__.Encode(newThresholdParam) + if err != nil { + return nil, errors.NewField("newThresholdParam", err) + } + // Serialize `sigIxIndexParam`: + err = enc__.Encode(sigIxIndexParam) + if err != nil { + return nil, errors.NewField("sigIxIndexParam", err) + } + } + accounts__ := solanago.AccountMetaSlice{} + + // Add the accounts to the instruction. + { + // Account 0 "config": Writable, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(configAccount, true, false)) + // Account 1 "instructions": Read-only, Non-signer, Required, Address: Sysvar1nstructions1111111111111111111111111 + accounts__.Append(solanago.NewAccountMeta(instructionsAccount, false, false)) + // Account 2 "event_authority": Read-only, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(eventAuthorityAccount, false, false)) + // Account 3 "program": Read-only, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(programAccount, false, false)) + } + + // Create the instruction. + return solanago.NewInstruction( + ProgramID, + accounts__, + buf__.Bytes(), + ), nil +} diff --git a/pkg/blockchain/sol/custody/program-id.go b/pkg/blockchain/sol/custody/program-id.go new file mode 100644 index 0000000..57360f0 --- /dev/null +++ b/pkg/blockchain/sol/custody/program-id.go @@ -0,0 +1,8 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains the program ID. + +package custody + +import solanago "github.com/gagliardetto/solana-go" + +var ProgramID = solanago.MustPublicKeyFromBase58("98eVpih8X9CAcgU9bzNB9V7VtkRrnFZUmqzEnsq7cfmg") diff --git a/pkg/blockchain/sol/custody/types.go b/pkg/blockchain/sol/custody/types.go new file mode 100644 index 0000000..0827ef1 --- /dev/null +++ b/pkg/blockchain/sol/custody/types.go @@ -0,0 +1,427 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains parsers for the types defined in the IDL. + +package custody + +import ( + "bytes" + "fmt" + errors "github.com/gagliardetto/anchor-go/errors" + binary "github.com/gagliardetto/binary" + solanago "github.com/gagliardetto/solana-go" +) + +// Config PDA — seeds `["config"]`. The on-chain analogue of `Custody.sol`'s +// signer set + threshold + nonce. `signers` is kept strictly ascending so a +// linear membership scan in the Ed25519 verifier is unambiguous. +type Config struct { + Signers []solanago.PublicKey `json:"signers"` + Threshold uint8 `json:"threshold"` + SignerNonce uint64 `json:"signerNonce"` + ChainId uint64 `json:"chainId"` + Bump uint8 `json:"bump"` +} + +func (obj Config) MarshalWithEncoder(encoder *binary.Encoder) (err error) { + // Serialize `Signers`: + err = encoder.Encode(obj.Signers) + if err != nil { + return errors.NewField("Signers", err) + } + // Serialize `Threshold`: + err = encoder.Encode(obj.Threshold) + if err != nil { + return errors.NewField("Threshold", err) + } + // Serialize `SignerNonce`: + err = encoder.Encode(obj.SignerNonce) + if err != nil { + return errors.NewField("SignerNonce", err) + } + // Serialize `ChainId`: + err = encoder.Encode(obj.ChainId) + if err != nil { + return errors.NewField("ChainId", err) + } + // Serialize `Bump`: + err = encoder.Encode(obj.Bump) + if err != nil { + return errors.NewField("Bump", err) + } + return nil +} + +func (obj Config) Marshal() ([]byte, error) { + buf := bytes.NewBuffer(nil) + encoder := binary.NewBorshEncoder(buf) + err := obj.MarshalWithEncoder(encoder) + if err != nil { + return nil, fmt.Errorf("error while encoding Config: %w", err) + } + return buf.Bytes(), nil +} + +func (obj *Config) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { + // Deserialize `Signers`: + err = decoder.Decode(&obj.Signers) + if err != nil { + return errors.NewField("Signers", err) + } + // Deserialize `Threshold`: + err = decoder.Decode(&obj.Threshold) + if err != nil { + return errors.NewField("Threshold", err) + } + // Deserialize `SignerNonce`: + err = decoder.Decode(&obj.SignerNonce) + if err != nil { + return errors.NewField("SignerNonce", err) + } + // Deserialize `ChainId`: + err = decoder.Decode(&obj.ChainId) + if err != nil { + return errors.NewField("ChainId", err) + } + // Deserialize `Bump`: + err = decoder.Decode(&obj.Bump) + if err != nil { + return errors.NewField("Bump", err) + } + return nil +} + +func (obj *Config) Unmarshal(buf []byte) error { + err := obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) + if err != nil { + return fmt.Errorf("error while unmarshaling Config: %w", err) + } + return nil +} + +func UnmarshalConfig(buf []byte) (*Config, error) { + obj := new(Config) + err := obj.Unmarshal(buf) + if err != nil { + return nil, err + } + return obj, nil +} + +// Emitted by `deposit_sol` / `deposit_spl` via `emit_cpi!`. The custody +// watcher decodes these from the self-CPI inner instructions and turns each +// into a `chains.DepositEvent`. `mint == Pubkey::default()` denotes native +// SOL (the analogue of EVM's `asset == address(0)`). `account` is the 20-byte +// clearnet account the deposit credits. +type Deposited struct { + Depositor solanago.PublicKey `json:"depositor"` + Account [20]uint8 `json:"account"` + Mint solanago.PublicKey `json:"mint"` + Amount uint64 `json:"amount"` +} + +func (obj Deposited) MarshalWithEncoder(encoder *binary.Encoder) (err error) { + // Serialize `Depositor`: + err = encoder.Encode(obj.Depositor) + if err != nil { + return errors.NewField("Depositor", err) + } + // Serialize `Account`: + err = encoder.Encode(obj.Account) + if err != nil { + return errors.NewField("Account", err) + } + // Serialize `Mint`: + err = encoder.Encode(obj.Mint) + if err != nil { + return errors.NewField("Mint", err) + } + // Serialize `Amount`: + err = encoder.Encode(obj.Amount) + if err != nil { + return errors.NewField("Amount", err) + } + return nil +} + +func (obj Deposited) Marshal() ([]byte, error) { + buf := bytes.NewBuffer(nil) + encoder := binary.NewBorshEncoder(buf) + err := obj.MarshalWithEncoder(encoder) + if err != nil { + return nil, fmt.Errorf("error while encoding Deposited: %w", err) + } + return buf.Bytes(), nil +} + +func (obj *Deposited) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { + // Deserialize `Depositor`: + err = decoder.Decode(&obj.Depositor) + if err != nil { + return errors.NewField("Depositor", err) + } + // Deserialize `Account`: + err = decoder.Decode(&obj.Account) + if err != nil { + return errors.NewField("Account", err) + } + // Deserialize `Mint`: + err = decoder.Decode(&obj.Mint) + if err != nil { + return errors.NewField("Mint", err) + } + // Deserialize `Amount`: + err = decoder.Decode(&obj.Amount) + if err != nil { + return errors.NewField("Amount", err) + } + return nil +} + +func (obj *Deposited) Unmarshal(buf []byte) error { + err := obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) + if err != nil { + return fmt.Errorf("error while unmarshaling Deposited: %w", err) + } + return nil +} + +func UnmarshalDeposited(buf []byte) (*Deposited, error) { + obj := new(Deposited) + err := obj.Unmarshal(buf) + if err != nil { + return nil, err + } + return obj, nil +} + +// Emitted by `execute` via `emit_cpi!`. The watcher writes a ledger row keyed +// by `withdrawal_id` (the reconciliation key against `signing_wal`). +type Executed struct { + WithdrawalId [32]uint8 `json:"withdrawalId"` + To solanago.PublicKey `json:"to"` + Mint solanago.PublicKey `json:"mint"` + Amount uint64 `json:"amount"` +} + +func (obj Executed) MarshalWithEncoder(encoder *binary.Encoder) (err error) { + // Serialize `WithdrawalId`: + err = encoder.Encode(obj.WithdrawalId) + if err != nil { + return errors.NewField("WithdrawalId", err) + } + // Serialize `To`: + err = encoder.Encode(obj.To) + if err != nil { + return errors.NewField("To", err) + } + // Serialize `Mint`: + err = encoder.Encode(obj.Mint) + if err != nil { + return errors.NewField("Mint", err) + } + // Serialize `Amount`: + err = encoder.Encode(obj.Amount) + if err != nil { + return errors.NewField("Amount", err) + } + return nil +} + +func (obj Executed) Marshal() ([]byte, error) { + buf := bytes.NewBuffer(nil) + encoder := binary.NewBorshEncoder(buf) + err := obj.MarshalWithEncoder(encoder) + if err != nil { + return nil, fmt.Errorf("error while encoding Executed: %w", err) + } + return buf.Bytes(), nil +} + +func (obj *Executed) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { + // Deserialize `WithdrawalId`: + err = decoder.Decode(&obj.WithdrawalId) + if err != nil { + return errors.NewField("WithdrawalId", err) + } + // Deserialize `To`: + err = decoder.Decode(&obj.To) + if err != nil { + return errors.NewField("To", err) + } + // Deserialize `Mint`: + err = decoder.Decode(&obj.Mint) + if err != nil { + return errors.NewField("Mint", err) + } + // Deserialize `Amount`: + err = decoder.Decode(&obj.Amount) + if err != nil { + return errors.NewField("Amount", err) + } + return nil +} + +func (obj *Executed) Unmarshal(buf []byte) error { + err := obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) + if err != nil { + return fmt.Errorf("error while unmarshaling Executed: %w", err) + } + return nil +} + +func UnmarshalExecuted(buf []byte) (*Executed, error) { + obj := new(Executed) + err := obj.Unmarshal(buf) + if err != nil { + return nil, err + } + return obj, nil +} + +// Emitted by `update_signers` via `emit_cpi!`. +type SignersUpdated struct { + Signers []solanago.PublicKey `json:"signers"` + Threshold uint8 `json:"threshold"` + SignerNonce uint64 `json:"signerNonce"` +} + +func (obj SignersUpdated) MarshalWithEncoder(encoder *binary.Encoder) (err error) { + // Serialize `Signers`: + err = encoder.Encode(obj.Signers) + if err != nil { + return errors.NewField("Signers", err) + } + // Serialize `Threshold`: + err = encoder.Encode(obj.Threshold) + if err != nil { + return errors.NewField("Threshold", err) + } + // Serialize `SignerNonce`: + err = encoder.Encode(obj.SignerNonce) + if err != nil { + return errors.NewField("SignerNonce", err) + } + return nil +} + +func (obj SignersUpdated) Marshal() ([]byte, error) { + buf := bytes.NewBuffer(nil) + encoder := binary.NewBorshEncoder(buf) + err := obj.MarshalWithEncoder(encoder) + if err != nil { + return nil, fmt.Errorf("error while encoding SignersUpdated: %w", err) + } + return buf.Bytes(), nil +} + +func (obj *SignersUpdated) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { + // Deserialize `Signers`: + err = decoder.Decode(&obj.Signers) + if err != nil { + return errors.NewField("Signers", err) + } + // Deserialize `Threshold`: + err = decoder.Decode(&obj.Threshold) + if err != nil { + return errors.NewField("Threshold", err) + } + // Deserialize `SignerNonce`: + err = decoder.Decode(&obj.SignerNonce) + if err != nil { + return errors.NewField("SignerNonce", err) + } + return nil +} + +func (obj *SignersUpdated) Unmarshal(buf []byte) error { + err := obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) + if err != nil { + return fmt.Errorf("error while unmarshaling SignersUpdated: %w", err) + } + return nil +} + +func UnmarshalSignersUpdated(buf []byte) (*SignersUpdated, error) { + obj := new(SignersUpdated) + err := obj.Unmarshal(buf) + if err != nil { + return nil, err + } + return obj, nil +} + +// WithdrawalRecord PDA — seeds `["withdrawal", withdrawal_id]`. Its mere +// existence is the replay guard: `execute` creates it with `init`, so a +// second execution of the same `withdrawal_id` fails at account creation. +// The analogue of `Custody.sol`'s `executed[withdrawalId]` storage slot; +// never closed (closing would re-enable replay). +type WithdrawalRecord struct { + To solanago.PublicKey `json:"to"` + Mint solanago.PublicKey `json:"mint"` + Amount uint64 `json:"amount"` +} + +func (obj WithdrawalRecord) MarshalWithEncoder(encoder *binary.Encoder) (err error) { + // Serialize `To`: + err = encoder.Encode(obj.To) + if err != nil { + return errors.NewField("To", err) + } + // Serialize `Mint`: + err = encoder.Encode(obj.Mint) + if err != nil { + return errors.NewField("Mint", err) + } + // Serialize `Amount`: + err = encoder.Encode(obj.Amount) + if err != nil { + return errors.NewField("Amount", err) + } + return nil +} + +func (obj WithdrawalRecord) Marshal() ([]byte, error) { + buf := bytes.NewBuffer(nil) + encoder := binary.NewBorshEncoder(buf) + err := obj.MarshalWithEncoder(encoder) + if err != nil { + return nil, fmt.Errorf("error while encoding WithdrawalRecord: %w", err) + } + return buf.Bytes(), nil +} + +func (obj *WithdrawalRecord) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { + // Deserialize `To`: + err = decoder.Decode(&obj.To) + if err != nil { + return errors.NewField("To", err) + } + // Deserialize `Mint`: + err = decoder.Decode(&obj.Mint) + if err != nil { + return errors.NewField("Mint", err) + } + // Deserialize `Amount`: + err = decoder.Decode(&obj.Amount) + if err != nil { + return errors.NewField("Amount", err) + } + return nil +} + +func (obj *WithdrawalRecord) Unmarshal(buf []byte) error { + err := obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) + if err != nil { + return fmt.Errorf("error while unmarshaling WithdrawalRecord: %w", err) + } + return nil +} + +func UnmarshalWithdrawalRecord(buf []byte) (*WithdrawalRecord, error) { + obj := new(WithdrawalRecord) + err := obj.Unmarshal(buf) + if err != nil { + return nil, err + } + return obj, nil +} diff --git a/pkg/blockchain/sol/depositor.go b/pkg/blockchain/sol/depositor.go new file mode 100644 index 0000000..80e575f --- /dev/null +++ b/pkg/blockchain/sol/depositor.go @@ -0,0 +1,132 @@ +package sol + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "strings" + + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" + + "github.com/layer-3/clearnet-sdk/pkg/blockchain/sol/custody" + "github.com/layer-3/clearnet-sdk/pkg/core" + "github.com/layer-3/clearnet-sdk/pkg/decimal" + "github.com/layer-3/clearnet-sdk/pkg/sign" +) + +// Depositor moves funds into the custody vault on Solana, signed by the +// depositor's own ed25519 key. It implements core.VaultDepositor. Native SOL +// and SPL tokens are both supported. The deposit credits the 20-byte clearnet +// account encoded in `account` (hex). +type Depositor struct { + client *rpc.Client + programID solana.PublicKey + vaultPDA solana.PublicKey + eventAuth solana.PublicKey + signer sign.Signer + depositorPub solana.PublicKey + commitment rpc.CommitmentType +} + +var _ core.VaultDepositor = (*Depositor)(nil) + +// NewDepositor builds the Solana depositor over the JSON-RPC at rpcURL. signer +// is the depositor's ed25519 key (it pays + funds). commitment is the level the +// deposit tx's blockhash + preflight use; empty → CommitmentFinalized (see the +// NOTE on the withdrawal finalizer's Config.Commitment for the test tradeoff). +func NewDepositor(rpcURL string, programID solana.PublicKey, signer sign.Signer, commitment rpc.CommitmentType) (*Depositor, error) { + pub, err := solanaPub(signer) + if err != nil { + return nil, err + } + if commitment == "" { + commitment = rpc.CommitmentFinalized + } + return &Depositor{ + client: rpc.New(rpcURL), + programID: programID, + vaultPDA: VaultPDA(programID), + eventAuth: eventAuthorityPDA(programID), + signer: signer, + depositorPub: pub, + commitment: commitment, + }, nil +} + +// DepositorAddress returns the depositor's Solana address. +func (d *Depositor) DepositorAddress() string { return d.depositorPub.String() } + +// Deposit transfers `amount` of `asset` into the vault, crediting clearnet +// `account` (20-byte hex). asset is "" / "SOL" for native or a base58 mint. +func (d *Depositor) Deposit(ctx context.Context, asset string, amount decimal.Decimal, account string) (core.TxRef, error) { + acct, err := parseClearnetAccount(account) + if err != nil { + return core.TxRef{}, err + } + amt := amount.BigInt() + if !amt.IsUint64() || amt.Sign() <= 0 { + return core.TxRef{}, fmt.Errorf("sol: amount %s not a positive uint64", amount.String()) + } + lamports := amt.Uint64() + mint, err := resolveMint(asset) + if err != nil { + return core.TxRef{}, err + } + + var ix solana.Instruction + if mint.IsZero() { + ix, err = custody.NewDepositSolInstruction( + acct, lamports, + d.depositorPub, d.vaultPDA, solana.SystemProgramID, d.eventAuth, d.programID, + ) + } else { + depositorATA, _, e := solana.FindAssociatedTokenAddress(d.depositorPub, mint) + if e != nil { + return core.TxRef{}, fmt.Errorf("sol: depositor ATA: %w", e) + } + vaultATA, _, e := solana.FindAssociatedTokenAddress(d.vaultPDA, mint) + if e != nil { + return core.TxRef{}, fmt.Errorf("sol: vault ATA: %w", e) + } + ix, err = custody.NewDepositSplInstruction( + acct, lamports, + d.depositorPub, mint, depositorATA, d.vaultPDA, vaultATA, + solana.TokenProgramID, solana.SPLAssociatedTokenAccountProgramID, d.eventAuth, d.programID, + ) + } + if err != nil { + return core.TxRef{}, fmt.Errorf("sol: build deposit ix: %w", err) + } + + sig, err := signAndSend(ctx, d.client, []solana.Instruction{ix}, d.depositorPub, d.signer, d.commitment) + if err != nil { + return core.TxRef{}, err + } + return txRef(sig), nil +} + +// parseClearnetAccount decodes a 20-byte clearnet account address from hex +// (optionally a yellow://.../user/ URI's last segment). +func parseClearnetAccount(account string) ([20]byte, error) { + seg := account + if i := strings.LastIndex(seg, "/"); i >= 0 { + seg = seg[i+1:] + } + seg = strings.TrimPrefix(strings.ToLower(seg), "0x") + b, err := hex.DecodeString(seg) + if err != nil || len(b) != 20 { + return [20]byte{}, fmt.Errorf("sol: account %q must be a 20-byte hex address (len=%d): %v", account, len(b), err) + } + var out [20]byte + copy(out[:], b) + return out, nil +} + +// txRef builds a core.TxRef from a Solana signature: Hash = sha256(sig) (the +// 32-byte receipt form clearnet uses), Raw = the base58 signature. +func txRef(sig solana.Signature) core.TxRef { + h := sha256.Sum256(sig[:]) + return core.TxRef{Hash: h, Raw: sig.String()} +} diff --git a/pkg/blockchain/sol/digest.go b/pkg/blockchain/sol/digest.go new file mode 100644 index 0000000..b0b5812 --- /dev/null +++ b/pkg/blockchain/sol/digest.go @@ -0,0 +1,37 @@ +package sol + +import ( + "crypto/sha256" + "encoding/binary" + + "github.com/gagliardetto/solana-go" +) + +// withdrawDomain is mirrored byte-for-byte by the Anchor program +// (programs/custody/src/digest.rs). +const withdrawDomain = "YELLOW_CUSTODY_SOL_WITHDRAW_V1" + +// WithdrawDigest computes the 32-byte digest the providers sign for a +// withdrawal, matching the program's `withdraw_digest`: +// +// sha256(WITHDRAW_DOMAIN ‖ chainID(BE) ‖ programID ‖ vault +// ‖ to ‖ mint ‖ amount(BE) ‖ withdrawalID) +// +// mint == the zero pubkey denotes native SOL. +func WithdrawDigest(chainID uint64, programID, vault, to, mint solana.PublicKey, amount uint64, withdrawalID [32]byte) [32]byte { + var u8 [8]byte + h := sha256.New() + h.Write([]byte(withdrawDomain)) + binary.BigEndian.PutUint64(u8[:], chainID) + h.Write(u8[:]) + h.Write(programID[:]) + h.Write(vault[:]) + h.Write(to[:]) + h.Write(mint[:]) + binary.BigEndian.PutUint64(u8[:], amount) + h.Write(u8[:]) + h.Write(withdrawalID[:]) + var out [32]byte + copy(out[:], h.Sum(nil)) + return out +} diff --git a/pkg/blockchain/sol/generate.go b/pkg/blockchain/sol/generate.go new file mode 100644 index 0000000..9a7fab3 --- /dev/null +++ b/pkg/blockchain/sol/generate.go @@ -0,0 +1,9 @@ +// Package sol implements the chain-agnostic adapter interfaces (see pkg/core) +// against Solana, over the custody Anchor program. The program bindings in +// ./custody are generated; the depositor/withdrawal-finalizer adapters and the +// Ed25519-precompile / digest helpers are hand-written on top of them. +package sol + +// Regenerate the ./custody program bindings from the vendored Anchor IDL in +// ./artifacts (see ./artifacts/README.md to refresh the IDL itself). +//go:generate go run ./idl_refresher diff --git a/pkg/blockchain/sol/idl_refresher/main.go b/pkg/blockchain/sol/idl_refresher/main.go new file mode 100644 index 0000000..8e50337 --- /dev/null +++ b/pkg/blockchain/sol/idl_refresher/main.go @@ -0,0 +1,97 @@ +// Command idl_refresher regenerates the Solana program bindings (the +// pkg/blockchain/sol/custody package) from the vendored Anchor IDL using +// anchor-go's generator library directly — the Solana analog of the EVM +// abi_refresher (which drives go-ethereum's abigen). No external binary, no AI +// in the loop. +// +// The vendored artifacts/custody.json (the Anchor IDL — Solana's ABI analog) +// is committed; a contract change shows up as a reviewable IDL diff. Regenerate +// with: +// +// go generate ./pkg/blockchain/sol/... # or: go run ./pkg/blockchain/sol/idl_refresher +// +// Refreshing the IDL itself (only when the program changes) is `anchor build` +// in the repo that owns the Rust source — see artifacts/README.md. +package main + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + + "github.com/gagliardetto/anchor-go/generator" + "github.com/gagliardetto/anchor-go/idl" + "github.com/gagliardetto/solana-go" +) + +// programID is the custody program's fixed on-chain id (declare_id!). +const programID = "98eVpih8X9CAcgU9bzNB9V7VtkRrnFZUmqzEnsq7cfmg" + +// generated bindings package name + its module import path. +const ( + pkgName = "custody" + modPath = "github.com/layer-3/clearnet-sdk/pkg/blockchain/sol/custody" +) + +func main() { + solDir := packageDir() + idlPath := filepath.Join(solDir, "artifacts", "custody.json") + outDir := filepath.Join(solDir, "custody") + + parsed, err := idl.ParseFromFilepath(idlPath) + if err != nil { + fmt.Fprintf(os.Stderr, "idl_refresher: parse %s: %v\n", idlPath, err) + os.Exit(1) + } + pid := solana.MustPublicKeyFromBase58(programID) + + out, err := generator.NewGenerator(parsed, &generator.GeneratorOptions{ + OutputDir: outDir, + Package: pkgName, + ModPath: modPath, + ProgramId: &pid, + ProgramName: pkgName, + SkipGoMod: true, // bindings live inside the SDK module + }).Generate() + if err != nil { + fmt.Fprintf(os.Stderr, "idl_refresher: generate: %v\n", err) + os.Exit(1) + } + + if err := os.MkdirAll(outDir, 0o755); err != nil { + fmt.Fprintf(os.Stderr, "idl_refresher: mkdir %s: %v\n", outDir, err) + os.Exit(1) + } + for _, file := range out.Files { + // Skip the generated placeholder test — it pulls heavy test deps + // (gomega/goleak) into the SDK for no benefit. + if file.Name == "tests_test.go" { + continue + } + path := filepath.Join(outDir, file.Name) + f, err := os.Create(path) + if err != nil { + fmt.Fprintf(os.Stderr, "idl_refresher: create %s: %v\n", path, err) + os.Exit(1) + } + if err := file.File.Render(f); err != nil { + f.Close() + fmt.Fprintf(os.Stderr, "idl_refresher: render %s: %v\n", path, err) + os.Exit(1) + } + f.Close() + fmt.Printf("idl_refresher: wrote custody/%s\n", file.Name) + } +} + +// packageDir returns the pkg/blockchain/sol directory, resolved from this +// source file so the working directory is irrelevant. +func packageDir() string { + _, file, _, ok := runtime.Caller(0) + if !ok { + panic("idl_refresher: runtime.Caller failed") + } + // file = /idl_refresher/main.go + return filepath.Dir(filepath.Dir(file)) +} diff --git a/pkg/blockchain/sol/program.go b/pkg/blockchain/sol/program.go new file mode 100644 index 0000000..aaf9871 --- /dev/null +++ b/pkg/blockchain/sol/program.go @@ -0,0 +1,168 @@ +package sol + +import ( + "context" + "encoding/binary" + "fmt" + + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" + + "github.com/layer-3/clearnet-sdk/pkg/blockchain/sol/custody" + "github.com/layer-3/clearnet-sdk/pkg/sign" +) + +// signAndSend builds a legacy transaction over the instructions, signs it with +// the single fee-payer (the quorum's signatures ride inside the Ed25519 +// instruction, not the tx signers), and broadcasts it. +func signAndSend(ctx context.Context, client *rpc.Client, instructions []solana.Instruction, payerPub solana.PublicKey, payer sign.Signer, commitment rpc.CommitmentType) (solana.Signature, error) { + if commitment == "" { + commitment = rpc.CommitmentFinalized + } + // The blockhash and the preflight simulation use the same commitment as the + // caller's reads — a mismatch (e.g. a finalized blockhash while funds were + // only just confirmed) evaluates the tx against stale state and misses the + // credit / can't find the fresh blockhash. + bh, err := client.GetLatestBlockhash(ctx, commitment) + if err != nil { + return solana.Signature{}, fmt.Errorf("sol: latest blockhash: %w", err) + } + tx, err := solana.NewTransaction(instructions, bh.Value.Blockhash, solana.TransactionPayer(payerPub)) + if err != nil { + return solana.Signature{}, fmt.Errorf("sol: build tx: %w", err) + } + msg, err := tx.Message.MarshalBinary() + if err != nil { + return solana.Signature{}, fmt.Errorf("sol: marshal message: %w", err) + } + sigBytes, err := payer.Sign(ctx, msg) + if err != nil { + return solana.Signature{}, fmt.Errorf("sol: sign tx: %w", err) + } + var sig solana.Signature + copy(sig[:], sigBytes) + tx.Signatures = []solana.Signature{sig} + if _, err := client.SendTransactionWithOpts(ctx, tx, rpc.TransactionOpts{PreflightCommitment: commitment}); err != nil { + return solana.Signature{}, fmt.Errorf("sol: send tx: %w", err) + } + return sig, nil +} + +// solanaPub maps a sign.Signer's ed25519 public key to a Solana pubkey. +func solanaPub(s sign.Signer) (solana.PublicKey, error) { + pub := s.PublicKey() + if s.Algorithm() != sign.AlgEd25519 || len(pub) != 32 { + return solana.PublicKey{}, fmt.Errorf("sol: signer must be ed25519 with a 32-byte key, got %s/%d", s.Algorithm(), len(pub)) + } + return solana.PublicKeyFromBytes(pub), nil +} + +// ed25519ProgramID is Solana's native Ed25519 signature-verification precompile. +var ed25519ProgramID = solana.MustPublicKeyFromBase58("Ed25519SigVerify111111111111111111111111111") + +// PDA seed prefixes — must match the Anchor program (programs/custody/src). +var ( + seedConfig = []byte("config") + seedVault = []byte("vault") + seedWithdrawal = []byte("withdrawal") + seedEventAuthority = []byte("__event_authority") +) + +// ConfigPDA / VaultPDA / WithdrawalPDA / eventAuthorityPDA derive the program's +// deterministic accounts. +func ConfigPDA(programID solana.PublicKey) solana.PublicKey { + pk, _, _ := solana.FindProgramAddress([][]byte{seedConfig}, programID) + return pk +} + +func VaultPDA(programID solana.PublicKey) solana.PublicKey { + pk, _, _ := solana.FindProgramAddress([][]byte{seedVault}, programID) + return pk +} + +func WithdrawalPDA(programID solana.PublicKey, withdrawalID [32]byte) solana.PublicKey { + pk, _, _ := solana.FindProgramAddress([][]byte{seedWithdrawal, withdrawalID[:]}, programID) + return pk +} + +func eventAuthorityPDA(programID solana.PublicKey) solana.PublicKey { + pk, _, _ := solana.FindProgramAddress([][]byte{seedEventAuthority}, programID) + return pk +} + +// BuildEd25519Instruction frames the quorum's signatures for the native +// Ed25519SigVerify precompile, all offsets self-referencing (instruction index +// 0xFFFF) so the verified data cannot be smuggled from another instruction. +// pubkeys/sigs are parallel (32-byte / 64-byte); message is the 32-byte digest. +func BuildEd25519Instruction(pubkeys, sigs [][]byte, message []byte) (solana.Instruction, error) { + n := len(pubkeys) + if n == 0 || n != len(sigs) { + return nil, fmt.Errorf("sol: ed25519 needs matching non-empty pubkeys/sigs (%d/%d)", n, len(sigs)) + } + if len(message) != 32 { + return nil, fmt.Errorf("sol: ed25519 message must be 32 bytes, got %d", len(message)) + } + const ( + header = 2 + offsetSize = 14 + selfRef = uint16(0xFFFF) + ) + msgOffset := header + n*offsetSize + pubkeysStart := msgOffset + 32 + sigsStart := pubkeysStart + n*32 + + data := make([]byte, sigsStart+n*64) + data[0] = byte(n) + data[1] = 0 + put16 := func(at int, v uint16) { binary.LittleEndian.PutUint16(data[at:], v) } + copy(data[msgOffset:], message) + for i := 0; i < n; i++ { + if len(pubkeys[i]) != 32 { + return nil, fmt.Errorf("sol: pubkey %d not 32 bytes", i) + } + if len(sigs[i]) != 64 { + return nil, fmt.Errorf("sol: signature %d not 64 bytes", i) + } + pkOff := pubkeysStart + i*32 + sigOff := sigsStart + i*64 + copy(data[pkOff:], pubkeys[i]) + copy(data[sigOff:], sigs[i]) + + base := header + i*offsetSize + put16(base+0, uint16(sigOff)) + put16(base+2, selfRef) + put16(base+4, uint16(pkOff)) + put16(base+6, selfRef) + put16(base+8, uint16(msgOffset)) + put16(base+10, 32) + put16(base+12, selfRef) + } + return solana.NewInstruction(ed25519ProgramID, nil, data), nil +} + +// fetchConfig reads the on-chain Config account (the live signer set + quorum) +// at the given commitment. +func fetchConfig(ctx context.Context, client *rpc.Client, programID solana.PublicKey, commitment rpc.CommitmentType) (*custody.Config, error) { + info, err := client.GetAccountInfoWithOpts(ctx, ConfigPDA(programID), &rpc.GetAccountInfoOpts{Commitment: commitment}) + if err != nil { + return nil, fmt.Errorf("sol: read config: %w", err) + } + if info == nil || info.Value == nil { + return nil, fmt.Errorf("sol: config account not found (program not initialized?)") + } + return custody.ParseAccount_Config(info.Value.Data.GetBinary()) +} + +// resolveMint maps an asset string to its mint; the zero pubkey is native SOL. +func resolveMint(l1Asset string) (solana.PublicKey, error) { + switch l1Asset { + case "", "native", "SOL", "sol": + return solana.PublicKey{}, nil + default: + mint, err := solana.PublicKeyFromBase58(l1Asset) + if err != nil { + return solana.PublicKey{}, fmt.Errorf("sol: l1_asset %q is not a base58 mint: %w", l1Asset, err) + } + return mint, nil + } +} diff --git a/pkg/blockchain/sol/vault_integration_test.go b/pkg/blockchain/sol/vault_integration_test.go new file mode 100644 index 0000000..58128f5 --- /dev/null +++ b/pkg/blockchain/sol/vault_integration_test.go @@ -0,0 +1,258 @@ +//go:build integration + +package sol + +import ( + "context" + "crypto/ed25519" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "os" + "path/filepath" + "sort" + "testing" + "time" + + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" + + "github.com/layer-3/clearnet-sdk/pkg/blockchain/sol/custody" + "github.com/layer-3/clearnet-sdk/pkg/core" + "github.com/layer-3/clearnet-sdk/pkg/decimal" + "github.com/layer-3/clearnet-sdk/pkg/sign" +) + +// Solana full deposit + withdrawal flow against the devnet validator (which +// preloads the custody program upgradeable at its fixed id, upgrade authority = +// devnet/sol-upgrade-authority.json). Self-provisioning: airdrop-funds the +// authority + depositor, Initializes the Config once (idempotent), deposits +// native SOL, then runs the quorum withdrawal with a fresh withdrawalID. +// +// Unlike EVM, the Config PDA is a singleton, so the signer set is FIXED across +// runs (derived from fixed seeds) and only the withdrawalID is fresh per run — +// re-runs stay clean without restarting the validator. Build-tagged +// `integration`; defaults target `make devnet` (override via SOL_RPC_URL). + +const ( + solChainID = 1002 + solSignerCount = 3 + solThreshold = 2 +) + +func TestIntegrationSOL_DepositAndWithdraw(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) + defer cancel() + + rpcURL := solEnv("SOL_RPC_URL", "http://127.0.0.1:8899") + client := rpc.New(rpcURL) + programID := custody.ProgramID + + // Upgrade authority (vendored) doubles as Initialize payer + withdrawal fee payer. + authority := loadAuthority(t) + authorityPub, err := solanaPub(authority) + if err != nil { + t.Fatalf("authority pub: %v", err) + } + + // Fixed signer set (Config is a singleton — same keys every run). + signers := make([]sign.Signer, solSignerCount) + for i := range signers { + signers[i] = fixedEd25519(t, fmt.Sprintf("clearnet-sdk/sol-itest/signer/%d", i)) + } + signerPubs := make([]solana.PublicKey, solSignerCount) + for i, s := range signers { + p, _ := solanaPub(s) + signerPubs[i] = p + } + // Program requires the on-chain signer set strictly ascending by raw bytes. + sort.Slice(signerPubs, func(i, j int) bool { + a, b := signerPubs[i], signerPubs[j] + return string(a[:]) < string(b[:]) + }) + + depositor := fixedEd25519(t, "clearnet-sdk/sol-itest/depositor") + depositorPub, _ := solanaPub(depositor) + + // Fund authority + depositor. + airdrop(ctx, t, client, authorityPub, 5*solana.LAMPORTS_PER_SOL) + airdrop(ctx, t, client, depositorPub, 5*solana.LAMPORTS_PER_SOL) + + // Initialize the Config once (idempotent — skip if it already exists). + if _, err := fetchConfig(ctx, client, programID, rpc.CommitmentConfirmed); err != nil { + programData, _, e := solana.FindProgramAddress([][]byte{programID[:]}, solana.BPFLoaderUpgradeableProgramID) + if e != nil { + t.Fatalf("program-data PDA: %v", e) + } + ix, e := custody.NewInitializeInstruction( + signerPubs, uint8(solThreshold), uint64(solChainID), + ConfigPDA(programID), authorityPub, programID, programData, solana.SystemProgramID, + ) + if e != nil { + t.Fatalf("build initialize: %v", e) + } + if _, e := signAndSend(ctx, client, []solana.Instruction{ix}, authorityPub, authority, rpc.CommitmentConfirmed); e != nil { + t.Fatalf("initialize: %v", e) + } + waitConfig(ctx, t, client, programID) + t.Logf("initialized Config (signers=%d threshold=%d)", solSignerCount, solThreshold) + } else { + t.Logf("Config already initialized; reusing") + } + + // ── Deposit flow ────────────────────────────────────────────────────────── + dep, err := NewDepositor(rpcURL, programID, depositor, rpc.CommitmentConfirmed) + if err != nil { + t.Fatalf("NewDepositor: %v", err) + } + const account = "00000000000000000000000000000000000000a1" // 20-byte clearnet addr + depRef, err := dep.Deposit(ctx, "SOL", decimal.NewFromInt(100_000_000), account) + if err != nil { + t.Fatalf("Deposit: %v", err) + } + t.Logf("deposit tx %s (from %s)", depRef.Raw, dep.DepositorAddress()) + // The depositor fire-and-forwards; wait until the vault PDA actually holds + // the funds before withdrawing. + waitBalance(ctx, t, client, VaultPDA(programID), 100_000_000) + + // ── Withdrawal flow (quorum in-process) ─────────────────────────────────── + finalizers := make([]*WithdrawalFinalizer, solSignerCount) + for i, s := range signers { + f, e := NewWithdrawalFinalizer(rpcURL, programID, s, authority, Config{ChainID: solChainID, Commitment: rpc.CommitmentConfirmed}) + if e != nil { + t.Fatalf("NewWithdrawalFinalizer %d: %v", i, e) + } + finalizers[i] = f + } + + var wid [32]byte + if _, err := rand.Read(wid[:]); err != nil { + t.Fatalf("rand wid: %v", err) + } + recipient := fixedEd25519(t, "clearnet-sdk/sol-itest/recipient/"+hex.EncodeToString(wid[:4])) + recipientPub, _ := solanaPub(recipient) + op := &core.WithdrawalOp{Recipient: recipientPub.String(), L1Asset: "SOL", Amount: decimal.NewFromInt(40_000_000)} + + packed, err := finalizers[0].Pack(ctx, op, wid) + if err != nil { + t.Fatalf("Pack: %v", err) + } + shares := make([][]byte, 0, len(finalizers)) + for i, f := range finalizers { + if err := f.Validate(ctx, packed, op, wid); err != nil { + t.Fatalf("Validate[%d]: %v", i, err) + } + s, e := f.Sign(ctx, packed) + if e != nil { + t.Fatalf("Sign[%d]: %v", i, e) + } + shares = append(shares, s) + } + merged, err := finalizers[0].Merge(ctx, packed, shares) + if err != nil { + t.Fatalf("Merge: %v", err) + } + ref, err := finalizers[0].Submit(ctx, merged) + if err != nil { + t.Fatalf("Submit: %v", err) + } + t.Logf("withdrawal tx %s", ref.Raw) + + if _, executed, err := finalizers[0].VerifyExecution(ctx, wid); err != nil { + t.Fatalf("VerifyExecution: %v", err) + } else if !executed { + t.Fatal("withdrawal not reported executed") + } +} + +// --- helpers --- + +func solEnv(key, def string) string { + if v := os.Getenv(key); v != "" { + return v + } + return def +} + +// loadAuthority reads the vendored upgrade-authority keypair (a 64-byte Solana +// keypair JSON) as an ed25519 signer. +func loadAuthority(t *testing.T) sign.Signer { + t.Helper() + // repo-root devnet/sol-upgrade-authority.json, from pkg/blockchain/sol. + path := filepath.Join("..", "..", "..", "devnet", "sol-upgrade-authority.json") + raw, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read authority keypair: %v", err) + } + var b []byte + if err := json.Unmarshal(raw, &b); err != nil { + t.Fatalf("parse authority keypair: %v", err) + } + if len(b) != ed25519.PrivateKeySize { + t.Fatalf("authority keypair is %d bytes, want %d", len(b), ed25519.PrivateKeySize) + } + ks, err := sign.NewKeySignerFromEd25519(ed25519.PrivateKey(b)) + if err != nil { + t.Fatalf("authority signer: %v", err) + } + return ks +} + +func fixedEd25519(t *testing.T, seedStr string) sign.Signer { + t.Helper() + seed := sha256.Sum256([]byte(seedStr)) + ks, err := sign.NewKeySignerFromEd25519(ed25519.NewKeyFromSeed(seed[:])) + if err != nil { + t.Fatalf("ed25519 from seed: %v", err) + } + return ks +} + +func airdrop(ctx context.Context, t *testing.T, client *rpc.Client, pub solana.PublicKey, lamports uint64) { + t.Helper() + if _, err := client.RequestAirdrop(ctx, pub, lamports, rpc.CommitmentConfirmed); err != nil { + t.Fatalf("airdrop %s: %v", pub, err) + } + deadline := time.Now().Add(30 * time.Second) + for { + bal, err := client.GetBalance(ctx, pub, rpc.CommitmentConfirmed) + if err == nil && bal != nil && bal.Value > 0 { + return + } + if time.Now().After(deadline) { + t.Fatalf("airdrop to %s not credited in time", pub) + } + time.Sleep(time.Second) + } +} + +func waitBalance(ctx context.Context, t *testing.T, client *rpc.Client, pub solana.PublicKey, min uint64) { + t.Helper() + deadline := time.Now().Add(30 * time.Second) + for { + bal, err := client.GetBalance(ctx, pub, rpc.CommitmentConfirmed) + if err == nil && bal != nil && bal.Value >= min { + return + } + if time.Now().After(deadline) { + t.Fatalf("balance of %s did not reach %d in time", pub, min) + } + time.Sleep(time.Second) + } +} + +func waitConfig(ctx context.Context, t *testing.T, client *rpc.Client, programID solana.PublicKey) { + t.Helper() + deadline := time.Now().Add(30 * time.Second) + for { + if _, err := fetchConfig(ctx, client, programID, rpc.CommitmentConfirmed); err == nil { + return + } + if time.Now().After(deadline) { + t.Fatal("Config not visible after initialize") + } + time.Sleep(time.Second) + } +} diff --git a/pkg/blockchain/sol/withdrawal_finalizer.go b/pkg/blockchain/sol/withdrawal_finalizer.go new file mode 100644 index 0000000..ddeb1b2 --- /dev/null +++ b/pkg/blockchain/sol/withdrawal_finalizer.go @@ -0,0 +1,379 @@ +package sol + +import ( + "bytes" + "context" + "encoding/hex" + "encoding/json" + "fmt" + "sort" + "time" + + "github.com/gagliardetto/solana-go" + computebudget "github.com/gagliardetto/solana-go/programs/compute-budget" + "github.com/gagliardetto/solana-go/rpc" + + "github.com/layer-3/clearnet-sdk/pkg/blockchain/sol/custody" + "github.com/layer-3/clearnet-sdk/pkg/core" + "github.com/layer-3/clearnet-sdk/pkg/sign" +) + +// shareLen is a per-signer share: ed25519 pubkey (32) ‖ signature (64). +const shareLen = 96 + +const ( + defaultComputeUnitLimit = uint32(200_000) + confirmPollInterval = 1 * time.Second + confirmTimeout = 30 * time.Second +) + +// Config tunes the submit transaction. +type Config struct { + ChainID uint64 + ComputeUnitLimit uint32 // 0 → 200k + ComputeUnitPrice uint64 // micro-lamports per CU (priority fee) + // Commitment is the level at which on-chain reads (Config, the Withdrawal + // PDA) are observed. Empty → CommitmentFinalized. + // + // NOTE: production should use CommitmentFinalized — a withdrawal is only + // truly settled once finalized (no rollback). CommitmentConfirmed is a + // devnet/test speed tradeoff: it observes results in ~1-2 slots instead of + // waiting ~32 for finality, cutting the local flow from ~16s to a few. + Commitment rpc.CommitmentType +} + +// WithdrawalFinalizer executes a withdrawal against the custody Anchor program: +// a digest signed by the ed25519 quorum, verified on-chain via the Ed25519 +// precompile. The node's signer contributes one share; a separate fee-payer +// signer pays + submits. It implements core.VaultWithdrawalFinalizer. Native +// SOL only for now (SPL execute needs the program's remaining-accounts). +type WithdrawalFinalizer struct { + client *rpc.Client + programID solana.PublicKey + chainID uint64 + vaultPDA solana.PublicKey + configPDA solana.PublicKey + eventAuth solana.PublicKey + cuLimit uint32 + cuPrice uint64 + commitment rpc.CommitmentType + signer sign.Signer + nodePub solana.PublicKey + feePayer sign.Signer + feePayerPub solana.PublicKey +} + +var _ core.VaultWithdrawalFinalizer = (*WithdrawalFinalizer)(nil) + +// NewWithdrawalFinalizer builds the finalizer. signer is this node's ed25519 +// custody key (contributes a quorum share); feePayer is a distinct ed25519 key +// that pays for and submits the execute transaction. +func NewWithdrawalFinalizer(rpcURL string, programID solana.PublicKey, signer, feePayer sign.Signer, cfg Config) (*WithdrawalFinalizer, error) { + nodePub, err := solanaPub(signer) + if err != nil { + return nil, err + } + payerPub, err := solanaPub(feePayer) + if err != nil { + return nil, fmt.Errorf("sol: fee payer: %w", err) + } + limit := cfg.ComputeUnitLimit + if limit == 0 { + limit = defaultComputeUnitLimit + } + commitment := cfg.Commitment + if commitment == "" { + commitment = rpc.CommitmentFinalized + } + return &WithdrawalFinalizer{ + client: rpc.New(rpcURL), + programID: programID, + chainID: cfg.ChainID, + vaultPDA: VaultPDA(programID), + configPDA: ConfigPDA(programID), + eventAuth: eventAuthorityPDA(programID), + cuLimit: limit, + cuPrice: cfg.ComputeUnitPrice, + commitment: commitment, + signer: signer, + nodePub: nodePub, + feePayer: feePayer, + feePayerPub: payerPub, + }, nil +} + +type solPacked struct { + To string `json:"to"` // recipient (base58) + Mint string `json:"mint"` // mint (base58); zero pubkey = native SOL + Amount uint64 `json:"amount"` // base units / lamports + WithdrawalID string `json:"withdrawalId"` // 32-byte hex +} + +type solMerged struct { + solPacked + Pubkeys []string `json:"pubkeys"` // ordered ed25519 pubkeys (base58) + Sigs []string `json:"sigs"` // parallel signatures (hex) +} + +// Pack resolves the withdrawal target and returns the canonical JSON. Pure. +func (f *WithdrawalFinalizer) Pack(_ context.Context, op *core.WithdrawalOp, withdrawalID [32]byte) ([]byte, error) { + p, err := f.packedFromOp(op, withdrawalID) + if err != nil { + return nil, err + } + return json.Marshal(p) +} + +// Validate re-derives the canonical payload from the op and asserts a match. +func (f *WithdrawalFinalizer) Validate(_ context.Context, packed []byte, op *core.WithdrawalOp, withdrawalID [32]byte) error { + var got solPacked + if err := json.Unmarshal(packed, &got); err != nil { + return fmt.Errorf("sol: decode packed: %w", err) + } + want, err := f.packedFromOp(op, withdrawalID) + if err != nil { + return err + } + if got != want { + return fmt.Errorf("sol: packed withdrawal does not match op: got %+v want %+v", got, want) + } + return nil +} + +// Sign returns this node's share: nodePubkey(32) ‖ ed25519 signature(64) over +// the withdrawal digest. +func (f *WithdrawalFinalizer) Sign(ctx context.Context, packed []byte) ([]byte, error) { + digest, err := f.digestFromPacked(packed) + if err != nil { + return nil, err + } + sig, err := f.signer.Sign(ctx, digest[:]) + if err != nil { + return nil, fmt.Errorf("sol: sign digest: %w", err) + } + if len(sig) != 64 { + return nil, fmt.Errorf("sol: ed25519 signature must be 64 bytes, got %d", len(sig)) + } + share := make([]byte, shareLen) + copy(share[:32], f.nodePub[:]) + copy(share[32:], sig) + return share, nil +} + +// Merge filters the shares against the live on-chain signer set, orders + trims +// to the quorum, and returns the merged artifact. +func (f *WithdrawalFinalizer) Merge(ctx context.Context, packed []byte, shares [][]byte) ([]byte, error) { + var p solPacked + if err := json.Unmarshal(packed, &p); err != nil { + return nil, fmt.Errorf("sol: decode packed: %w", err) + } + cfg, err := fetchConfig(ctx, f.client, f.programID, f.commitment) + if err != nil { + return nil, err + } + pubkeys, sigs, err := assembleQuorum(shares, cfg.Signers, int(cfg.Threshold)) + if err != nil { + return nil, err + } + m := solMerged{solPacked: p, Pubkeys: make([]string, len(pubkeys)), Sigs: make([]string, len(sigs))} + for i := range pubkeys { + m.Pubkeys[i] = solana.PublicKeyFromBytes(pubkeys[i]).String() + m.Sigs[i] = hex.EncodeToString(sigs[i]) + } + return json.Marshal(m) +} + +// Submit assembles the Ed25519-precompile + execute transaction and broadcasts +// it (fee-payer signed), then waits for the Withdrawal PDA to appear. +func (f *WithdrawalFinalizer) Submit(ctx context.Context, merged []byte) (core.TxRef, error) { + var m solMerged + if err := json.Unmarshal(merged, &m); err != nil { + return core.TxRef{}, fmt.Errorf("sol: decode merged: %w", err) + } + to, mint, amount, wid, err := decodePacked(m.solPacked) + if err != nil { + return core.TxRef{}, err + } + if !mint.IsZero() { + return core.TxRef{}, fmt.Errorf("sol: SPL withdrawal not yet supported (native SOL only)") + } + + pubkeys := make([][]byte, len(m.Pubkeys)) + sigs := make([][]byte, len(m.Sigs)) + for i := range m.Pubkeys { + pk, e := solana.PublicKeyFromBase58(m.Pubkeys[i]) + if e != nil { + return core.TxRef{}, fmt.Errorf("sol: bad merged pubkey: %w", e) + } + pubkeys[i] = pk[:] + b, e := hex.DecodeString(m.Sigs[i]) + if e != nil { + return core.TxRef{}, fmt.Errorf("sol: bad merged sig: %w", e) + } + sigs[i] = b + } + + digest := WithdrawDigest(f.chainID, f.programID, f.vaultPDA, to, mint, amount, wid) + ed25519Ix, err := BuildEd25519Instruction(pubkeys, sigs, digest[:]) + if err != nil { + return core.TxRef{}, err + } + // Leading instructions before the Ed25519 companion: the two compute-budget + // instructions. sigIxIndex (which execute introspects) is their count. + leading := []solana.Instruction{ + computebudget.NewSetComputeUnitLimitInstruction(f.cuLimit).Build(), + computebudget.NewSetComputeUnitPriceInstruction(f.cuPrice).Build(), + } + sigIxIndex := uint8(len(leading)) + execIx, err := custody.NewExecuteInstruction( + to, mint, amount, wid, sigIxIndex, + f.feePayerPub, f.configPDA, f.vaultPDA, WithdrawalPDA(f.programID, wid), + to, solana.SysVarInstructionsPubkey, solana.SystemProgramID, f.eventAuth, f.programID, + ) + if err != nil { + return core.TxRef{}, fmt.Errorf("sol: build execute ix: %w", err) + } + instructions := append(leading, ed25519Ix, execIx) + + sig, err := signAndSend(ctx, f.client, instructions, f.feePayerPub, f.feePayer, f.commitment) + if err != nil { + // A peer may have already landed it. + if h, executed, verr := f.VerifyExecution(ctx, wid); verr == nil && executed { + return core.TxRef{Hash: h}, nil + } + return core.TxRef{}, err + } + if err := f.waitExecuted(ctx, wid); err != nil { + return core.TxRef{}, err + } + return txRef(sig), nil +} + +// VerifyExecution reports whether the Withdrawal PDA exists (the on-chain +// executed flag). The tx hash is not recoverable from the PDA alone, so a zero +// hash is returned with executed=true. +func (f *WithdrawalFinalizer) VerifyExecution(ctx context.Context, withdrawalID [32]byte) ([32]byte, bool, error) { + info, err := f.client.GetAccountInfoWithOpts(ctx, WithdrawalPDA(f.programID, withdrawalID), &rpc.GetAccountInfoOpts{Commitment: f.commitment}) + if err != nil { + // solana-go returns an error for a missing account; treat as not-found. + if err == rpc.ErrNotFound { + return [32]byte{}, false, nil + } + return [32]byte{}, false, nil + } + if info == nil || info.Value == nil { + return [32]byte{}, false, nil + } + return [32]byte{}, true, nil +} + +func (f *WithdrawalFinalizer) waitExecuted(ctx context.Context, withdrawalID [32]byte) error { + deadline := time.Now().Add(confirmTimeout) + for { + if _, executed, _ := f.VerifyExecution(ctx, withdrawalID); executed { + return nil + } + if time.Now().After(deadline) { + return fmt.Errorf("sol: withdrawal %x not executed within %s", withdrawalID, confirmTimeout) + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(confirmPollInterval): + } + } +} + +// --- helpers --- + +func (f *WithdrawalFinalizer) packedFromOp(op *core.WithdrawalOp, withdrawalID [32]byte) (solPacked, error) { + to, err := solana.PublicKeyFromBase58(op.Recipient) + if err != nil { + return solPacked{}, fmt.Errorf("sol: recipient %q not base58: %w", op.Recipient, err) + } + mint, err := resolveMint(op.L1Asset) + if err != nil { + return solPacked{}, err + } + amt := op.Amount.BigInt() + if !amt.IsUint64() || amt.Sign() <= 0 { + return solPacked{}, fmt.Errorf("sol: amount %s not a positive uint64", op.Amount.String()) + } + return solPacked{ + To: to.String(), + Mint: mint.String(), + Amount: amt.Uint64(), + WithdrawalID: hex.EncodeToString(withdrawalID[:]), + }, nil +} + +func (f *WithdrawalFinalizer) digestFromPacked(packed []byte) ([32]byte, error) { + var p solPacked + if err := json.Unmarshal(packed, &p); err != nil { + return [32]byte{}, fmt.Errorf("sol: decode packed: %w", err) + } + to, mint, amount, wid, err := decodePacked(p) + if err != nil { + return [32]byte{}, err + } + return WithdrawDigest(f.chainID, f.programID, f.vaultPDA, to, mint, amount, wid), nil +} + +func decodePacked(p solPacked) (to, mint solana.PublicKey, amount uint64, wid [32]byte, err error) { + if to, err = solana.PublicKeyFromBase58(p.To); err != nil { + return + } + if mint, err = solana.PublicKeyFromBase58(p.Mint); err != nil { + return + } + b, e := hex.DecodeString(p.WithdrawalID) + if e != nil || len(b) != 32 { + err = fmt.Errorf("sol: bad withdrawalID %q", p.WithdrawalID) + return + } + copy(wid[:], b) + amount = p.Amount + return +} + +// assembleQuorum filters shares to the authorized signer set, dedups, orders by +// pubkey ascending (the program's verifier walks them in order), and trims to +// the threshold. +func assembleQuorum(shares [][]byte, authorized []solana.PublicKey, threshold int) (pubkeys, sigs [][]byte, err error) { + auth := make(map[solana.PublicKey]struct{}, len(authorized)) + for _, s := range authorized { + auth[s] = struct{}{} + } + type item struct { + pub solana.PublicKey + sig []byte + } + seen := make(map[solana.PublicKey]struct{}) + var items []item + for _, sh := range shares { + if len(sh) != shareLen { + continue + } + var pub solana.PublicKey + copy(pub[:], sh[:32]) + if _, ok := auth[pub]; !ok { + continue + } + if _, dup := seen[pub]; dup { + continue + } + seen[pub] = struct{}{} + items = append(items, item{pub: pub, sig: append([]byte(nil), sh[32:96]...)}) + } + if len(items) < threshold { + return nil, nil, fmt.Errorf("sol: only %d of %d authorized shares", len(items), threshold) + } + sort.Slice(items, func(i, j int) bool { return bytes.Compare(items[i].pub[:], items[j].pub[:]) < 0 }) + items = items[:threshold] + for _, it := range items { + pk := it.pub + pubkeys = append(pubkeys, append([]byte(nil), pk[:]...)) + sigs = append(sigs, it.sig) + } + return pubkeys, sigs, nil +} From 80537ae44326b97364cf11ec4044e0e23bba1665 Mon Sep 17 00:00:00 2001 From: Anton Filonenko Date: Tue, 16 Jun 2026 14:41:00 +0300 Subject: [PATCH 04/15] feat(blockchain): add deposit verification, BTC client, XRPL helpers Extend the chain-agnostic adapter surface: - core.DepositStatus tri-state + VaultDepositor.VerifyDeposit per chain (EVM receipt depth, BTC confirmations, XRPL validated flag, Solana commitment ladder). - Concrete btc.Client (bitcoind JSON-RPC) with typed RPCError; the withdrawal idempotency check now branches on the error code. - Export the XRPL wire helpers (BuildAmount, CanonicalJSON, DeriveIdentity, ValidateCanonical) and the Identity type for non-finalizer callers. - Add xrpl.LedgerTicketProvider, a client-backed TicketProvider. Fold VaultWithdrawalFinalizer.Merge into Submit(packed, signatures), dropping the unused merged-bytes intermediate, and rename VaultDepositor.Deposit to SubmitDeposit to pair with VerifyDeposit. Co-Authored-By: Claude Fable 5 --- pkg/blockchain/btc/client.go | 212 ++++++++++++++++++ pkg/blockchain/btc/depositor.go | 29 ++- pkg/blockchain/btc/rpc.go | 15 +- pkg/blockchain/btc/vault_integration_test.go | 178 ++------------- pkg/blockchain/btc/withdrawal_finalizer.go | 14 +- pkg/blockchain/evm/depositor.go | 40 +++- pkg/blockchain/evm/vault_integration_test.go | 10 +- pkg/blockchain/evm/withdrawal_finalizer.go | 64 +++--- pkg/blockchain/sol/depositor.go | 42 +++- pkg/blockchain/sol/vault_integration_test.go | 8 +- pkg/blockchain/sol/withdrawal_finalizer.go | 62 ++--- pkg/blockchain/xrpl/depositor.go | 37 ++- pkg/blockchain/xrpl/ticket.go | 69 ++++++ pkg/blockchain/xrpl/vault_integration_test.go | 42 ++-- pkg/blockchain/xrpl/wire.go | 48 ++-- pkg/blockchain/xrpl/withdrawal_finalizer.go | 31 +-- pkg/core/blockchain.go | 47 +++- 17 files changed, 602 insertions(+), 346 deletions(-) create mode 100644 pkg/blockchain/btc/client.go create mode 100644 pkg/blockchain/xrpl/ticket.go diff --git a/pkg/blockchain/btc/client.go b/pkg/blockchain/btc/client.go new file mode 100644 index 0000000..76bbd90 --- /dev/null +++ b/pkg/blockchain/btc/client.go @@ -0,0 +1,212 @@ +package btc + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "time" +) + +// Client is a concrete bitcoind JSON-RPC client implementing RPC. Wallet-scoped +// calls (ListUnspent) route to /wallet/; the rest hit the node root. +// It carries only the read + broadcast surface the adapters need — wallet +// provisioning (createwallet, mining, funding) is deliberately out of scope. +type Client struct { + url string + wallet string + user string + pass string + http *http.Client +} + +var _ RPC = (*Client)(nil) + +// Option configures a Client. +type Option func(*Client) + +// WithHTTPClient overrides the default *http.Client (30s timeout). +func WithHTTPClient(h *http.Client) Option { + return func(c *Client) { c.http = h } +} + +// NewClient builds a bitcoind RPC client at url with basic-auth user/pass. +// wallet is the wallet name wallet-scoped RPCs route to (may be empty if the +// node has a single default wallet loaded). +func NewClient(url, user, pass, wallet string, opts ...Option) *Client { + c := &Client{ + url: url, + wallet: wallet, + user: user, + pass: pass, + http: &http.Client{Timeout: 30 * time.Second}, + } + for _, opt := range opts { + opt(c) + } + return c +} + +// RPCError is a typed bitcoind JSON-RPC error response. The Code lets callers +// branch on outcome (e.g. already-in-chain) without string matching. +type RPCError struct { + Code int + Message string +} + +func (e *RPCError) Error() string { + return fmt.Sprintf("bitcoind rpc error %d: %s", e.Code, e.Message) +} + +func (c *Client) post(ctx context.Context, endpoint, method string, params []any, out any) error { + body, _ := json.Marshal(map[string]any{"jsonrpc": "1.0", "id": "sdk", "method": method, "params": params}) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body)) + if err != nil { + return err + } + req.SetBasicAuth(c.user, c.pass) + req.Header.Set("Content-Type", "application/json") + resp, err := c.http.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + var env struct { + Result json.RawMessage `json:"result"` + Error *struct { + Code int `json:"code"` + Message string `json:"message"` + } `json:"error"` + } + if err := json.NewDecoder(resp.Body).Decode(&env); err != nil { + return fmt.Errorf("btc rpc %s: decode response: %w", method, err) + } + if env.Error != nil { + return &RPCError{Code: env.Error.Code, Message: env.Error.Message} + } + if out != nil { + return json.Unmarshal(env.Result, out) + } + return nil +} + +func (c *Client) call(ctx context.Context, method string, params []any, out any) error { + return c.post(ctx, c.url, method, params, out) +} + +func (c *Client) walletCall(ctx context.Context, method string, params []any, out any) error { + return c.post(ctx, c.url+"/wallet/"+c.wallet, method, params, out) +} + +// ListUnspent returns the vault UTXOs at addrs with at least minConf confirmations. +func (c *Client) ListUnspent(ctx context.Context, minConf int, addrs []string) ([]Unspent, error) { + var raw []struct { + TxID string `json:"txid"` + Vout uint32 `json:"vout"` + Amount float64 `json:"amount"` + Confirmations int64 `json:"confirmations"` + ScriptPubKey string `json:"scriptPubKey"` + } + if err := c.walletCall(ctx, "listunspent", []any{minConf, 9999999, addrs}, &raw); err != nil { + return nil, err + } + out := make([]Unspent, len(raw)) + for i, u := range raw { + out[i] = Unspent{TxID: u.TxID, Vout: u.Vout, AmountSats: btcToSats(u.Amount), Confirmations: u.Confirmations, ScriptPubKey: u.ScriptPubKey} + } + return out, nil +} + +// GetTxOut returns the unspent output txid:vout, or nil if it is spent/unknown. +func (c *Client) GetTxOut(ctx context.Context, txid string, vout uint32, includeMempool bool) (*TxOut, error) { + var raw *struct { + Confirmations int64 `json:"confirmations"` + Value float64 `json:"value"` + ScriptPubKey struct { + Hex string `json:"hex"` + } `json:"scriptPubKey"` + } + if err := c.call(ctx, "gettxout", []any{txid, vout, includeMempool}, &raw); err != nil { + return nil, err + } + if raw == nil { + return nil, nil + } + return &TxOut{AmountSats: btcToSats(raw.Value), ScriptPubKey: raw.ScriptPubKey.Hex, Confirmations: raw.Confirmations}, nil +} + +// SendRawTransaction broadcasts hexTx and returns its txid. +func (c *Client) SendRawTransaction(ctx context.Context, hexTx string) (string, error) { + var txid string + return txid, c.call(ctx, "sendrawtransaction", []any{hexTx}, &txid) +} + +// EstimateSmartFeeSatPerVByte returns the node's fee estimate for confTarget +// blocks, falling back to fallbackRate when the node cannot estimate (e.g. on +// regtest). +func (c *Client) EstimateSmartFeeSatPerVByte(ctx context.Context, confTarget int, fallbackRate int64) (int64, error) { + var raw struct { + FeeRate float64 `json:"feerate"` + } + if err := c.call(ctx, "estimatesmartfee", []any{confTarget}, &raw); err != nil || raw.FeeRate <= 0 { + return fallbackRate, nil + } + rate := int64(raw.FeeRate*1e8/1000 + 0.5) + if rate < 1 { + rate = fallbackRate + } + return rate, nil +} + +// GetBlockCount returns the height of the most-work fully-validated chain. +func (c *Client) GetBlockCount(ctx context.Context) (int64, error) { + var n int64 + return n, c.call(ctx, "getblockcount", []any{}, &n) +} + +// GetBlockHash returns the block hash at the given height. +func (c *Client) GetBlockHash(ctx context.Context, height int64) (string, error) { + var h string + return h, c.call(ctx, "getblockhash", []any{height}, &h) +} + +// GetBlockTxids returns the txids in the block (verbosity 1). +func (c *Client) GetBlockTxids(ctx context.Context, blockHash string) ([]string, error) { + var raw struct { + Tx []string `json:"tx"` + } + if err := c.call(ctx, "getblock", []any{blockHash, 1}, &raw); err != nil { + return nil, err + } + return raw.Tx, nil +} + +// GetRawTransaction returns the decoded transaction (verbose=true). Requires the +// node to find the tx: txindex=1, or the tx unspent/in mempool. +func (c *Client) GetRawTransaction(ctx context.Context, txid string) (*RawTx, error) { + var raw *struct { + TxID string `json:"txid"` + Confirmations int64 `json:"confirmations"` + Vout []struct { + Value float64 `json:"value"` + ScriptPubKey struct { + Hex string `json:"hex"` + } `json:"scriptPubKey"` + } `json:"vout"` + } + if err := c.call(ctx, "getrawtransaction", []any{txid, true}, &raw); err != nil { + return nil, err + } + if raw == nil { + return nil, nil + } + out := &RawTx{TxID: raw.TxID, Confirmations: raw.Confirmations, Vouts: make([]RawVout, len(raw.Vout))} + for i, vo := range raw.Vout { + out.Vouts[i] = RawVout{ValueSats: btcToSats(vo.Value), ScriptPubKeyHex: vo.ScriptPubKey.Hex} + } + return out, nil +} + +// btcToSats converts a BTC float amount to integer satoshis. +func btcToSats(v float64) int64 { return int64(v*1e8 + 0.5) } diff --git a/pkg/blockchain/btc/depositor.go b/pkg/blockchain/btc/depositor.go index a1d5eb4..c3cecea 100644 --- a/pkg/blockchain/btc/depositor.go +++ b/pkg/blockchain/btc/depositor.go @@ -3,6 +3,7 @@ package btc import ( "context" "encoding/hex" + "errors" "fmt" "sort" "strings" @@ -62,10 +63,10 @@ func NewDepositor(net *chaincfg.Params, rpc RPC, signer sign.Signer, vaultPubkey // DepositorAddress returns the depositor's own P2WPKH funding address. func (d *Depositor) DepositorAddress() string { return d.depositAddr.EncodeAddress() } -// Deposit sends `amount` satoshis from the depositor's wallet to the per-account +// SubmitDeposit sends `amount` satoshis from the depositor's wallet to the per-account // deposit address for `account`. asset must be native BTC ("" or "BTC"). Builds, // signs (P2WPKH), and broadcasts the funding tx. -func (d *Depositor) Deposit(ctx context.Context, asset string, amount decimal.Decimal, account string) (core.TxRef, error) { +func (d *Depositor) SubmitDeposit(ctx context.Context, asset string, amount decimal.Decimal, account string) (core.TxRef, error) { if a := strings.ToUpper(strings.TrimSpace(asset)); a != "" && a != "BTC" { return core.TxRef{}, fmt.Errorf("btc: only native BTC deposits supported, got asset %q", asset) } @@ -122,6 +123,30 @@ func (d *Depositor) Deposit(ctx context.Context, asset string, amount decimal.De return core.TxRef{Hash: hash, Raw: txid}, nil } +// VerifyDeposit reports the on-chain status of the deposit tx in ref (matched +// by txid, ref.Raw). Requires the node to resolve the tx (txindex=1, or the tx +// unspent / in the mempool). A tx the node has never seen — or one reorged out +// and dropped — reads as DepositAbsent; a mempool tx (0 confs) is DepositPending +// until it is mined with at least max(1, minConf) confirmations (a deposit is +// only Confirmed once on chain, consistent with the other chains). +func (d *Depositor) VerifyDeposit(ctx context.Context, ref core.TxRef, minConf uint64) (core.DepositStatus, error) { + raw, err := d.rpc.GetRawTransaction(ctx, ref.Raw) + if err != nil { + var rpcErr *RPCError + if errors.As(err, &rpcErr) && rpcErr.Code == -5 { // RPC_INVALID_ADDRESS_OR_KEY: unknown tx + return core.DepositAbsent, nil + } + return core.DepositAbsent, fmt.Errorf("btc: getrawtransaction: %w", err) + } + if raw == nil { + return core.DepositAbsent, nil + } + if raw.Confirmations > 0 && raw.Confirmations >= int64(minConf) { + return core.DepositConfirmed, nil + } + return core.DepositPending, nil +} + // depositorUTXOs filters unspent outputs to the depositor's own address and // returns the UTXO set plus a per-outpoint amount/script index for signing. func depositorUTXOs(unspent []Unspent, myAddr string, net *chaincfg.Params) ([]UTXO, map[wire.OutPoint]int64, error) { diff --git a/pkg/blockchain/btc/rpc.go b/pkg/blockchain/btc/rpc.go index baec153..eb8d5e3 100644 --- a/pkg/blockchain/btc/rpc.go +++ b/pkg/blockchain/btc/rpc.go @@ -2,6 +2,7 @@ package btc import ( "context" + "errors" "strings" ) @@ -53,10 +54,18 @@ type RawVout struct { // isAlreadyKnown reports whether a SendRawTransaction error means the tx (or a // prior attempt spending the same inputs) is already in the chain/mempool — the -// UTXO-model analogue of EVM's executed[withdrawalID] guard. Matched on the -// error text since the concrete RPC client (and its typed error) is caller- -// supplied. +// UTXO-model analogue of EVM's executed[withdrawalID] guard. It prefers the +// typed bitcoind error code (RPC_VERIFY_ALREADY_IN_CHAIN = -27, +// RPC_VERIFY_ERROR = -25 for spent/missing inputs) when the caller supplies +// *Client, and falls back to error-text matching for other RPC implementations. func isAlreadyKnown(err error) bool { + var rpcErr *RPCError + if errors.As(err, &rpcErr) { + switch rpcErr.Code { + case -27, -25: + return true + } + } msg := strings.ToLower(err.Error()) return strings.Contains(msg, "already in block chain") || strings.Contains(msg, "txn-already-known") || diff --git a/pkg/blockchain/btc/vault_integration_test.go b/pkg/blockchain/btc/vault_integration_test.go index 117db9c..7d04799 100644 --- a/pkg/blockchain/btc/vault_integration_test.go +++ b/pkg/blockchain/btc/vault_integration_test.go @@ -3,11 +3,7 @@ package btc import ( - "bytes" "context" - "encoding/json" - "fmt" - "net/http" "os" "strings" "testing" @@ -41,13 +37,12 @@ func TestIntegrationBTC_DepositAndWithdraw(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) defer cancel() - node := &bitcoindRPC{ - url: btcEnv("BTC_RPC_URL", defaultBTCRPC), - wallet: btcWallet, - user: btcEnv("BTC_RPC_USER", "sdk"), - pass: btcEnv("BTC_RPC_PASS", "sdk"), - http: &http.Client{Timeout: 30 * time.Second}, - } + node := &bitcoindRPC{Client: NewClient( + btcEnv("BTC_RPC_URL", defaultBTCRPC), + btcEnv("BTC_RPC_USER", "sdk"), + btcEnv("BTC_RPC_PASS", "sdk"), + btcWallet, + )} net := &chaincfg.RegressionNetParams // ── Setup: wallet + mined funds ─────────────────────────────────────────── @@ -83,7 +78,7 @@ func TestIntegrationBTC_DepositAndWithdraw(t *testing.T) { node.generateToAddress(ctx, t, 1, miner) // ── Deposit flow ────────────────────────────────────────────────────────── - depRef, err := depositor.Deposit(ctx, "BTC", decimal.NewFromInt(20_000_000), account) // 0.2 BTC + depRef, err := depositor.SubmitDeposit(ctx, "BTC", decimal.NewFromInt(20_000_000), account) // 0.2 BTC if err != nil { t.Fatalf("Deposit: %v", err) } @@ -122,11 +117,7 @@ func TestIntegrationBTC_DepositAndWithdraw(t *testing.T) { } shares = append(shares, s) } - merged, err := finalizers[0].Merge(ctx, packed, shares) - if err != nil { - t.Fatalf("Merge: %v", err) - } - ref, err := finalizers[0].Submit(ctx, merged) + ref, err := finalizers[0].Submit(ctx, packed, shares) if err != nil { t.Fatalf("Submit: %v", err) } @@ -156,153 +147,14 @@ func btcEnv(key, def string) string { return def } -// ── minimal bitcoind JSON-RPC client (implements btc.RPC + test setup) ──────── +// ── test harness: a *Client plus regtest provisioning helpers ───────────────── +// bitcoindRPC embeds the SDK's concrete *Client (which provides the RPC +// surface) and adds regtest-only setup calls (wallet, mining, funding) that the +// shipped client deliberately omits. The setup helpers reach the embedded +// client's unexported call/walletCall directly (same package). type bitcoindRPC struct { - url string // node endpoint - wallet string // wallet name (wallet RPCs route to /wallet/) - user string - pass string - http *http.Client -} - -var _ RPC = (*bitcoindRPC)(nil) - -func (c *bitcoindRPC) post(ctx context.Context, endpoint, method string, params []any, out any) error { - body, _ := json.Marshal(map[string]any{"jsonrpc": "1.0", "id": "sdk", "method": method, "params": params}) - req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body)) - if err != nil { - return err - } - req.SetBasicAuth(c.user, c.pass) - req.Header.Set("Content-Type", "application/json") - resp, err := c.http.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - var env struct { - Result json.RawMessage `json:"result"` - Error *struct { - Code int `json:"code"` - Message string `json:"message"` - } `json:"error"` - } - if err := json.NewDecoder(resp.Body).Decode(&env); err != nil { - return err - } - if env.Error != nil { - return fmt.Errorf("rpc %s: %d %s", method, env.Error.Code, env.Error.Message) - } - if out != nil { - return json.Unmarshal(env.Result, out) - } - return nil -} - -func (c *bitcoindRPC) call(ctx context.Context, method string, params []any, out any) error { - return c.post(ctx, c.url, method, params, out) -} - -func (c *bitcoindRPC) walletCall(ctx context.Context, method string, params []any, out any) error { - return c.post(ctx, c.url+"/wallet/"+c.wallet, method, params, out) -} - -// --- btc.RPC interface --- - -func (c *bitcoindRPC) ListUnspent(ctx context.Context, minConf int, addrs []string) ([]Unspent, error) { - var raw []struct { - TxID string `json:"txid"` - Vout uint32 `json:"vout"` - Amount float64 `json:"amount"` - Confirmations int64 `json:"confirmations"` - ScriptPubKey string `json:"scriptPubKey"` - } - if err := c.walletCall(ctx, "listunspent", []any{minConf, 9999999, addrs}, &raw); err != nil { - return nil, err - } - out := make([]Unspent, len(raw)) - for i, u := range raw { - out[i] = Unspent{TxID: u.TxID, Vout: u.Vout, AmountSats: btcToSats(u.Amount), Confirmations: u.Confirmations, ScriptPubKey: u.ScriptPubKey} - } - return out, nil -} - -func (c *bitcoindRPC) GetTxOut(ctx context.Context, txid string, vout uint32, includeMempool bool) (*TxOut, error) { - var raw *struct { - Confirmations int64 `json:"confirmations"` - Value float64 `json:"value"` - ScriptPubKey struct { - Hex string `json:"hex"` - } `json:"scriptPubKey"` - } - if err := c.call(ctx, "gettxout", []any{txid, vout, includeMempool}, &raw); err != nil { - return nil, err - } - if raw == nil { - return nil, nil - } - return &TxOut{AmountSats: btcToSats(raw.Value), ScriptPubKey: raw.ScriptPubKey.Hex, Confirmations: raw.Confirmations}, nil -} - -func (c *bitcoindRPC) SendRawTransaction(ctx context.Context, hexTx string) (string, error) { - var txid string - return txid, c.call(ctx, "sendrawtransaction", []any{hexTx}, &txid) -} - -func (c *bitcoindRPC) EstimateSmartFeeSatPerVByte(ctx context.Context, confTarget int, fallbackRate int64) (int64, error) { - var raw struct { - FeeRate float64 `json:"feerate"` - } - if err := c.call(ctx, "estimatesmartfee", []any{confTarget}, &raw); err != nil || raw.FeeRate <= 0 { - return fallbackRate, nil // regtest has no fee estimate - } - rate := int64(raw.FeeRate*1e8/1000 + 0.5) - if rate < 1 { - rate = fallbackRate - } - return rate, nil -} - -func (c *bitcoindRPC) GetBlockCount(ctx context.Context) (int64, error) { - var n int64 - return n, c.call(ctx, "getblockcount", []any{}, &n) -} - -func (c *bitcoindRPC) GetBlockHash(ctx context.Context, height int64) (string, error) { - var h string - return h, c.call(ctx, "getblockhash", []any{height}, &h) -} - -func (c *bitcoindRPC) GetBlockTxids(ctx context.Context, blockHash string) ([]string, error) { - var raw struct { - Tx []string `json:"tx"` - } - if err := c.call(ctx, "getblock", []any{blockHash, 1}, &raw); err != nil { - return nil, err - } - return raw.Tx, nil -} - -func (c *bitcoindRPC) GetRawTransaction(ctx context.Context, txid string) (*RawTx, error) { - var raw struct { - TxID string `json:"txid"` - Vout []struct { - Value float64 `json:"value"` - ScriptPubKey struct { - Hex string `json:"hex"` - } `json:"scriptPubKey"` - } `json:"vout"` - } - // verbose=true; blockhash omitted (txindex=1 on the devnet node). - if err := c.call(ctx, "getrawtransaction", []any{txid, true}, &raw); err != nil { - return nil, err - } - out := &RawTx{TxID: raw.TxID, Vouts: make([]RawVout, len(raw.Vout))} - for i, vo := range raw.Vout { - out.Vouts[i] = RawVout{ValueSats: btcToSats(vo.Value), ScriptPubKeyHex: vo.ScriptPubKey.Hex} - } - return out, nil + *Client } // --- test-only setup helpers --- @@ -352,5 +204,3 @@ func (c *bitcoindRPC) sendToAddress(ctx context.Context, t *testing.T, addr stri t.Fatalf("sendtoaddress: %v", err) } } - -func btcToSats(v float64) int64 { return int64(v*1e8 + 0.5) } diff --git a/pkg/blockchain/btc/withdrawal_finalizer.go b/pkg/blockchain/btc/withdrawal_finalizer.go index 5624bbe..7975c5a 100644 --- a/pkg/blockchain/btc/withdrawal_finalizer.go +++ b/pkg/blockchain/btc/withdrawal_finalizer.go @@ -262,10 +262,10 @@ func (f *WithdrawalFinalizer) Sign(ctx context.Context, packed []byte) ([]byte, return json.Marshal(SigShare{PubKey: hex.EncodeToString(f.signerPub), Sigs: sigs}) } -// Merge assembles the witness for every input from the collected shares (the +// merge assembles the witness for every input from the collected shares (the // threshold lowest by redeem-script key position) and returns the fully-signed // tx serialization. -func (f *WithdrawalFinalizer) Merge(ctx context.Context, packed []byte, shares [][]byte) ([]byte, error) { +func (f *WithdrawalFinalizer) merge(ctx context.Context, packed []byte, shares [][]byte) ([]byte, error) { tx, err := deserializeTx(packed) if err != nil { return nil, fmt.Errorf("btc merge: %w", err) @@ -316,9 +316,13 @@ func (f *WithdrawalFinalizer) Merge(ctx context.Context, packed []byte, shares [ return serializeTx(tx) } -// Submit broadcasts the merged signed tx, returning its hash. Idempotent on an -// already-known/spent reply. -func (f *WithdrawalFinalizer) Submit(ctx context.Context, merged []byte) (core.TxRef, error) { +// Submit assembles the witnesses from the collected shares and broadcasts the +// signed tx, returning its hash. Idempotent on an already-known/spent reply. +func (f *WithdrawalFinalizer) Submit(ctx context.Context, packed []byte, shares [][]byte) (core.TxRef, error) { + merged, err := f.merge(ctx, packed, shares) + if err != nil { + return core.TxRef{}, err + } tx, err := deserializeTx(merged) if err != nil { return core.TxRef{}, fmt.Errorf("btc submit: %w", err) diff --git a/pkg/blockchain/evm/depositor.go b/pkg/blockchain/evm/depositor.go index c3427ea..eef2cdb 100644 --- a/pkg/blockchain/evm/depositor.go +++ b/pkg/blockchain/evm/depositor.go @@ -2,9 +2,12 @@ package evm import ( "context" + "errors" "fmt" + "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" "github.com/layer-3/clearnet-sdk/pkg/core" @@ -33,11 +36,11 @@ func NewDepositor(client *ethclient.Client, custodyAddr common.Address, signer s return &Depositor{client: client, custody: custody, custodyAddr: custodyAddr, signer: signer}, nil } -// Deposit credits `account` with `amount` of `asset`. For an ERC-20 (asset is a +// SubmitDeposit credits `account` with `amount` of `asset`. For an ERC-20 (asset is a // non-zero hex address) it approves the vault then calls // Custody.deposit(account, asset, amount); for the zero address it sends native // ETH with msg.value == amount. Blocks until the deposit tx mines. -func (d *Depositor) Deposit(ctx context.Context, asset string, amount decimal.Decimal, account string) (core.TxRef, error) { +func (d *Depositor) SubmitDeposit(ctx context.Context, asset string, amount decimal.Decimal, account string) (core.TxRef, error) { assetAddr, err := depositAssetAddress(asset) if err != nil { return core.TxRef{}, err @@ -91,3 +94,36 @@ func (d *Depositor) Deposit(ctx context.Context, asset string, amount decimal.De } return core.TxRef{Hash: tx.Hash(), Raw: tx.Hash().Hex()}, nil } + +// VerifyDeposit reports the on-chain status of the deposit tx in ref. A reverted +// deposit (receipt status != 1) reads as DepositAbsent since it credited +// nothing. Confirmations are counted inclusively (head - blockNumber + 1). +func (d *Depositor) VerifyDeposit(ctx context.Context, ref core.TxRef, minConf uint64) (core.DepositStatus, error) { + hash := common.Hash(ref.Hash) + receipt, err := d.client.TransactionReceipt(ctx, hash) + if err != nil { + if errors.Is(err, ethereum.NotFound) { + // No receipt — maybe still pending in the mempool. + if _, isPending, perr := d.client.TransactionByHash(ctx, hash); perr == nil && isPending { + return core.DepositPending, nil + } + return core.DepositAbsent, nil + } + return core.DepositAbsent, fmt.Errorf("evm: tx receipt: %w", err) + } + if receipt.Status != types.ReceiptStatusSuccessful { + return core.DepositAbsent, nil + } + head, err := d.client.BlockNumber(ctx) + if err != nil { + return core.DepositPending, fmt.Errorf("evm: block number: %w", err) + } + var confs uint64 + if bn := receipt.BlockNumber.Uint64(); head >= bn { + confs = head - bn + 1 + } + if confs >= minConf { + return core.DepositConfirmed, nil + } + return core.DepositPending, nil +} diff --git a/pkg/blockchain/evm/vault_integration_test.go b/pkg/blockchain/evm/vault_integration_test.go index f43a74b..777168f 100644 --- a/pkg/blockchain/evm/vault_integration_test.go +++ b/pkg/blockchain/evm/vault_integration_test.go @@ -102,7 +102,7 @@ func TestIntegrationEVM_DepositAndWithdraw(t *testing.T) { account := crypto.PubkeyToAddress(deployerKey.PublicKey) const zeroAsset = "0x0000000000000000000000000000000000000000" // native ETH depositAmt := decimal.NewFromInt(1_000_000_000_000) // 1e12 wei - depRef, err := depositor.Deposit(ctx, zeroAsset, depositAmt, account.Hex()) + depRef, err := depositor.SubmitDeposit(ctx, zeroAsset, depositAmt, account.Hex()) if err != nil { t.Fatalf("Deposit: %v", err) } @@ -143,12 +143,8 @@ func TestIntegrationEVM_DepositAndWithdraw(t *testing.T) { } sigs = append(sigs, s) } - // 3. Merge + Submit (a submitter node). - merged, err := finalizers[0].Merge(ctx, packed, sigs) - if err != nil { - t.Fatalf("Merge: %v", err) - } - wRef, err := finalizers[0].Submit(ctx, merged) + // 3. Submit (a submitter node merges the quorum and broadcasts). + wRef, err := finalizers[0].Submit(ctx, packed, sigs) if err != nil { t.Fatalf("Submit: %v", err) } diff --git a/pkg/blockchain/evm/withdrawal_finalizer.go b/pkg/blockchain/evm/withdrawal_finalizer.go index 7418aa0..7aad0ee 100644 --- a/pkg/blockchain/evm/withdrawal_finalizer.go +++ b/pkg/blockchain/evm/withdrawal_finalizer.go @@ -89,12 +89,6 @@ type evmPacked struct { WithdrawalID string `json:"withdrawalId"` // 32-byte hex } -// evmMerged is evmPacked plus the ordered, contract-ready signatures. -type evmMerged struct { - evmPacked - Sigs []string `json:"sigs"` // 65-byte sigs, sorted by signer, V ∈ {27,28}, hex -} - // Pack returns the canonical JSON for the withdrawal. Pure — no chain access. func (f *WithdrawalFinalizer) Pack(_ context.Context, op *core.WithdrawalOp, withdrawalID [32]byte) ([]byte, error) { return json.Marshal(packedFromOp(op, withdrawalID)) @@ -128,15 +122,11 @@ func (f *WithdrawalFinalizer) Sign(ctx context.Context, packed []byte) ([]byte, return sign.SignEthDigest(ctx, f.signer, digest[:], f.signerAddr) } -// Merge filters the collected signatures against the live on-chain signer set, +// merge filters the collected signatures against the live on-chain signer set, // trims to the live threshold, orders them by signer address (Custody.sol -// requires ascending, no duplicates), shifts V to {27,28}, and returns the -// merged artifact. -func (f *WithdrawalFinalizer) Merge(ctx context.Context, packed []byte, signatures [][]byte) ([]byte, error) { - var p evmPacked - if err := json.Unmarshal(packed, &p); err != nil { - return nil, fmt.Errorf("decode packed: %w", err) - } +// requires ascending, no duplicates), and shifts V to {27,28}. It returns the +// contract-ready signature list. +func (f *WithdrawalFinalizer) merge(ctx context.Context, p evmPacked, signatures [][]byte) ([][]byte, error) { digest, err := f.digest(p) if err != nil { return nil, err @@ -183,27 +173,28 @@ func (f *WithdrawalFinalizer) Merge(ctx context.Context, packed []byte, signatur // Ascending uint160 order == bytes order over [20]byte. sort.Slice(kept, func(i, j int) bool { return bytes.Compare(kept[i].addr[:], kept[j].addr[:]) < 0 }) - merged := evmMerged{evmPacked: p, Sigs: make([]string, len(kept))} + sigs := make([][]byte, len(kept)) for i, k := range kept { cp := make([]byte, 65) copy(cp, k.sig) if cp[64] < 27 { cp[64] += 27 // shift V {0,1} -> {27,28} at the contract boundary } - merged.Sigs[i] = hex.EncodeToString(cp) + sigs[i] = cp } - return json.Marshal(merged) + return sigs, nil } -// Submit broadcasts the merged artifact via Custody.execute and returns the tx -// reference. Idempotent: if the withdrawal is already executed it returns the -// prior tx hash without re-submitting. -func (f *WithdrawalFinalizer) Submit(ctx context.Context, merged []byte) (core.TxRef, error) { - var m evmMerged - if err := json.Unmarshal(merged, &m); err != nil { - return core.TxRef{}, fmt.Errorf("decode merged: %w", err) - } - wid, err := decodeHex32(m.WithdrawalID) +// Submit merges the collected signatures into a contract-ready quorum and +// broadcasts it via Custody.execute, returning the tx reference. Idempotent: if +// the withdrawal is already executed it returns the prior tx hash without +// re-submitting. +func (f *WithdrawalFinalizer) Submit(ctx context.Context, packed []byte, signatures [][]byte) (core.TxRef, error) { + var p evmPacked + if err := json.Unmarshal(packed, &p); err != nil { + return core.TxRef{}, fmt.Errorf("decode packed: %w", err) + } + wid, err := decodeHex32(p.WithdrawalID) if err != nil { return core.TxRef{}, err } @@ -213,19 +204,16 @@ func (f *WithdrawalFinalizer) Submit(ctx context.Context, merged []byte) (core.T return core.TxRef{Hash: txHash, Raw: common.Hash(txHash).Hex()}, nil } - to := common.HexToAddress(m.To) - asset := common.HexToAddress(m.Asset) - amount, ok := new(big.Int).SetString(m.Amount, 10) - if !ok { - return core.TxRef{}, fmt.Errorf("bad amount %q", m.Amount) + sigs, err := f.merge(ctx, p, signatures) + if err != nil { + return core.TxRef{}, err } - sigs := make([][]byte, len(m.Sigs)) - for i, s := range m.Sigs { - b, err := hex.DecodeString(s) - if err != nil { - return core.TxRef{}, fmt.Errorf("decode sig %d: %w", i, err) - } - sigs[i] = b + + to := common.HexToAddress(p.To) + asset := common.HexToAddress(p.Asset) + amount, ok := new(big.Int).SetString(p.Amount, 10) + if !ok { + return core.TxRef{}, fmt.Errorf("bad amount %q", p.Amount) } opts, _, err := signerTransactOpts(ctx, f.client, f.signer) diff --git a/pkg/blockchain/sol/depositor.go b/pkg/blockchain/sol/depositor.go index 80e575f..19f6f5e 100644 --- a/pkg/blockchain/sol/depositor.go +++ b/pkg/blockchain/sol/depositor.go @@ -4,6 +4,7 @@ import ( "context" "crypto/sha256" "encoding/hex" + "errors" "fmt" "strings" @@ -58,9 +59,9 @@ func NewDepositor(rpcURL string, programID solana.PublicKey, signer sign.Signer, // DepositorAddress returns the depositor's Solana address. func (d *Depositor) DepositorAddress() string { return d.depositorPub.String() } -// Deposit transfers `amount` of `asset` into the vault, crediting clearnet +// SubmitDeposit transfers `amount` of `asset` into the vault, crediting clearnet // `account` (20-byte hex). asset is "" / "SOL" for native or a base58 mint. -func (d *Depositor) Deposit(ctx context.Context, asset string, amount decimal.Decimal, account string) (core.TxRef, error) { +func (d *Depositor) SubmitDeposit(ctx context.Context, asset string, amount decimal.Decimal, account string) (core.TxRef, error) { acct, err := parseClearnetAccount(account) if err != nil { return core.TxRef{}, err @@ -107,6 +108,43 @@ func (d *Depositor) Deposit(ctx context.Context, asset string, amount decimal.De return txRef(sig), nil } +// VerifyDeposit reports the on-chain status of the deposit tx in ref (matched by +// signature, ref.Raw). minConf maps onto Solana's commitment ladder, which has +// no numeric depth: minConf 0 accepts the optimistic "confirmed" level (~1-2 +// slots), while minConf >= 1 requires "finalized" (irreversible). A failed tx +// reads as DepositAbsent (it credited nothing). +func (d *Depositor) VerifyDeposit(ctx context.Context, ref core.TxRef, minConf uint64) (core.DepositStatus, error) { + sig, err := solana.SignatureFromBase58(ref.Raw) + if err != nil { + return core.DepositAbsent, fmt.Errorf("sol: bad signature %q: %w", ref.Raw, err) + } + out, err := d.client.GetSignatureStatuses(ctx, true, sig) + if err != nil { + if errors.Is(err, rpc.ErrNotFound) { + return core.DepositAbsent, nil + } + return core.DepositAbsent, fmt.Errorf("sol: signature status: %w", err) + } + if len(out.Value) == 0 || out.Value[0] == nil { + return core.DepositAbsent, nil + } + st := out.Value[0] + if st.Err != nil { + return core.DepositAbsent, nil + } + switch st.ConfirmationStatus { + case rpc.ConfirmationStatusFinalized: + return core.DepositConfirmed, nil + case rpc.ConfirmationStatusConfirmed: + if minConf == 0 { + return core.DepositConfirmed, nil + } + return core.DepositPending, nil + default: + return core.DepositPending, nil + } +} + // parseClearnetAccount decodes a 20-byte clearnet account address from hex // (optionally a yellow://.../user/ URI's last segment). func parseClearnetAccount(account string) ([20]byte, error) { diff --git a/pkg/blockchain/sol/vault_integration_test.go b/pkg/blockchain/sol/vault_integration_test.go index 58128f5..c599008 100644 --- a/pkg/blockchain/sol/vault_integration_test.go +++ b/pkg/blockchain/sol/vault_integration_test.go @@ -108,7 +108,7 @@ func TestIntegrationSOL_DepositAndWithdraw(t *testing.T) { t.Fatalf("NewDepositor: %v", err) } const account = "00000000000000000000000000000000000000a1" // 20-byte clearnet addr - depRef, err := dep.Deposit(ctx, "SOL", decimal.NewFromInt(100_000_000), account) + depRef, err := dep.SubmitDeposit(ctx, "SOL", decimal.NewFromInt(100_000_000), account) if err != nil { t.Fatalf("Deposit: %v", err) } @@ -150,11 +150,7 @@ func TestIntegrationSOL_DepositAndWithdraw(t *testing.T) { } shares = append(shares, s) } - merged, err := finalizers[0].Merge(ctx, packed, shares) - if err != nil { - t.Fatalf("Merge: %v", err) - } - ref, err := finalizers[0].Submit(ctx, merged) + ref, err := finalizers[0].Submit(ctx, packed, shares) if err != nil { t.Fatalf("Submit: %v", err) } diff --git a/pkg/blockchain/sol/withdrawal_finalizer.go b/pkg/blockchain/sol/withdrawal_finalizer.go index ddeb1b2..841291e 100644 --- a/pkg/blockchain/sol/withdrawal_finalizer.go +++ b/pkg/blockchain/sol/withdrawal_finalizer.go @@ -109,12 +109,6 @@ type solPacked struct { WithdrawalID string `json:"withdrawalId"` // 32-byte hex } -type solMerged struct { - solPacked - Pubkeys []string `json:"pubkeys"` // ordered ed25519 pubkeys (base58) - Sigs []string `json:"sigs"` // parallel signatures (hex) -} - // Pack resolves the withdrawal target and returns the canonical JSON. Pure. func (f *WithdrawalFinalizer) Pack(_ context.Context, op *core.WithdrawalOp, withdrawalID [32]byte) ([]byte, error) { p, err := f.packedFromOp(op, withdrawalID) @@ -160,37 +154,25 @@ func (f *WithdrawalFinalizer) Sign(ctx context.Context, packed []byte) ([]byte, return share, nil } -// Merge filters the shares against the live on-chain signer set, orders + trims -// to the quorum, and returns the merged artifact. -func (f *WithdrawalFinalizer) Merge(ctx context.Context, packed []byte, shares [][]byte) ([]byte, error) { - var p solPacked - if err := json.Unmarshal(packed, &p); err != nil { - return nil, fmt.Errorf("sol: decode packed: %w", err) - } +// merge filters the shares against the live on-chain signer set and orders + +// trims them to the quorum, returning the parallel ed25519 pubkeys / signatures. +func (f *WithdrawalFinalizer) merge(ctx context.Context, shares [][]byte) (pubkeys, sigs [][]byte, err error) { cfg, err := fetchConfig(ctx, f.client, f.programID, f.commitment) if err != nil { - return nil, err - } - pubkeys, sigs, err := assembleQuorum(shares, cfg.Signers, int(cfg.Threshold)) - if err != nil { - return nil, err + return nil, nil, err } - m := solMerged{solPacked: p, Pubkeys: make([]string, len(pubkeys)), Sigs: make([]string, len(sigs))} - for i := range pubkeys { - m.Pubkeys[i] = solana.PublicKeyFromBytes(pubkeys[i]).String() - m.Sigs[i] = hex.EncodeToString(sigs[i]) - } - return json.Marshal(m) + return assembleQuorum(shares, cfg.Signers, int(cfg.Threshold)) } -// Submit assembles the Ed25519-precompile + execute transaction and broadcasts -// it (fee-payer signed), then waits for the Withdrawal PDA to appear. -func (f *WithdrawalFinalizer) Submit(ctx context.Context, merged []byte) (core.TxRef, error) { - var m solMerged - if err := json.Unmarshal(merged, &m); err != nil { - return core.TxRef{}, fmt.Errorf("sol: decode merged: %w", err) - } - to, mint, amount, wid, err := decodePacked(m.solPacked) +// Submit filters + orders the collected shares against the live signer set, +// assembles the Ed25519-precompile + execute transaction, and broadcasts it +// (fee-payer signed), then waits for the Withdrawal PDA to appear. +func (f *WithdrawalFinalizer) Submit(ctx context.Context, packed []byte, shares [][]byte) (core.TxRef, error) { + var p solPacked + if err := json.Unmarshal(packed, &p); err != nil { + return core.TxRef{}, fmt.Errorf("sol: decode packed: %w", err) + } + to, mint, amount, wid, err := decodePacked(p) if err != nil { return core.TxRef{}, err } @@ -198,19 +180,9 @@ func (f *WithdrawalFinalizer) Submit(ctx context.Context, merged []byte) (core.T return core.TxRef{}, fmt.Errorf("sol: SPL withdrawal not yet supported (native SOL only)") } - pubkeys := make([][]byte, len(m.Pubkeys)) - sigs := make([][]byte, len(m.Sigs)) - for i := range m.Pubkeys { - pk, e := solana.PublicKeyFromBase58(m.Pubkeys[i]) - if e != nil { - return core.TxRef{}, fmt.Errorf("sol: bad merged pubkey: %w", e) - } - pubkeys[i] = pk[:] - b, e := hex.DecodeString(m.Sigs[i]) - if e != nil { - return core.TxRef{}, fmt.Errorf("sol: bad merged sig: %w", e) - } - sigs[i] = b + pubkeys, sigs, err := f.merge(ctx, shares) + if err != nil { + return core.TxRef{}, err } digest := WithdrawDigest(f.chainID, f.programID, f.vaultPDA, to, mint, amount, wid) diff --git a/pkg/blockchain/xrpl/depositor.go b/pkg/blockchain/xrpl/depositor.go index e50581b..2ffbb2c 100644 --- a/pkg/blockchain/xrpl/depositor.go +++ b/pkg/blockchain/xrpl/depositor.go @@ -3,7 +3,9 @@ package xrpl import ( "context" "fmt" + "strings" + "github.com/Peersyst/xrpl-go/xrpl/queries/transactions" "github.com/Peersyst/xrpl-go/xrpl/rpc" "github.com/Peersyst/xrpl-go/xrpl/transaction" "github.com/Peersyst/xrpl-go/xrpl/transaction/types" @@ -21,7 +23,7 @@ type Depositor struct { client *rpc.Client vaultAddress string signer sign.Signer - id xrplIdentity + id Identity } var _ core.VaultDepositor = (*Depositor)(nil) @@ -32,7 +34,7 @@ func NewDepositor(rpcURL, vaultAddress string, signer sign.Signer) (*Depositor, if err != nil { return nil, fmt.Errorf("xrpl: create rpc config: %w", err) } - id, err := deriveIdentity(signer) + id, err := DeriveIdentity(signer) if err != nil { return nil, err } @@ -40,12 +42,12 @@ func NewDepositor(rpcURL, vaultAddress string, signer sign.Signer) (*Depositor, } // DepositorAddress returns the depositor's classic r-address. -func (d *Depositor) DepositorAddress() string { return d.id.classicAddress } +func (d *Depositor) DepositorAddress() string { return d.id.ClassicAddress } -// Deposit sends `amount` of `asset` to the vault, crediting `account` via its +// SubmitDeposit sends `amount` of `asset` to the vault, crediting `account` via its // DestinationTag. asset is "" / "XRP" for native or "CUR.rIssuer" for an issued // currency; account must be of the form xrpl- (the tag the watcher credits). -func (d *Depositor) Deposit(ctx context.Context, asset string, amount decimal.Decimal, account string) (core.TxRef, error) { +func (d *Depositor) SubmitDeposit(ctx context.Context, asset string, amount decimal.Decimal, account string) (core.TxRef, error) { tag, err := parseDepositTag(account) if err != nil { return core.TxRef{}, err @@ -56,7 +58,7 @@ func (d *Depositor) Deposit(ctx context.Context, asset string, amount decimal.De } payment := transaction.Payment{ - BaseTx: transaction.BaseTx{Account: types.Address(d.id.classicAddress)}, + BaseTx: transaction.BaseTx{Account: types.Address(d.id.ClassicAddress)}, Destination: types.Address(d.vaultAddress), Amount: xrplAmount, } @@ -85,3 +87,26 @@ func (d *Depositor) Deposit(ctx context.Context, asset string, amount decimal.De return core.TxRef{}, fmt.Errorf("xrpl: deposit rejected: %s - %s", result.EngineResult, result.EngineResultMessage) } } + +// VerifyDeposit reports the on-chain status of the deposit tx in ref (matched by +// hash, ref.Raw). XRPL finality is binary — a validated transaction cannot be +// reorged — so minConf is not a depth here: a validated tx is DepositConfirmed, +// one found but not yet validated is DepositPending, and an unknown hash +// (never submitted, or dropped before validation) is DepositAbsent. +func (d *Depositor) VerifyDeposit(_ context.Context, ref core.TxRef, _ uint64) (core.DepositStatus, error) { + res, err := d.client.Request(&transactions.TxRequest{Transaction: ref.Raw}) + if err != nil { + if strings.Contains(err.Error(), "txnNotFound") { + return core.DepositAbsent, nil + } + return core.DepositAbsent, fmt.Errorf("xrpl: tx lookup: %w", err) + } + var tx transactions.TxResponse + if err := res.GetResult(&tx); err != nil { + return core.DepositAbsent, fmt.Errorf("xrpl: decode tx: %w", err) + } + if tx.Validated { + return core.DepositConfirmed, nil + } + return core.DepositPending, nil +} diff --git a/pkg/blockchain/xrpl/ticket.go b/pkg/blockchain/xrpl/ticket.go new file mode 100644 index 0000000..a6aa8ae --- /dev/null +++ b/pkg/blockchain/xrpl/ticket.go @@ -0,0 +1,69 @@ +package xrpl + +import ( + "context" + "fmt" + "sort" + + "github.com/Peersyst/xrpl-go/xrpl/queries/account" + "github.com/Peersyst/xrpl-go/xrpl/rpc" + "github.com/Peersyst/xrpl-go/xrpl/transaction/types" +) + +// LedgerTicketProvider is a TicketProvider that hands out Tickets already +// provisioned on the vault account, read live from the ledger via +// account_objects. It does not create Tickets (that needs signing authority +// over the vault, which the caller orchestrates); it surfaces the ones that +// exist. +// +// TicketFor returns the lowest available TicketSequence and is stateless: it +// does not reserve the Ticket it returns, so two concurrent withdrawals can be +// handed the same one (the second submit then fails tefNO_TICKET). Callers that +// run withdrawals concurrently must layer their own reservation/pool on top — +// this is the simple single-flight building block. +type LedgerTicketProvider struct { + client *rpc.Client + account types.Address +} + +var _ TicketProvider = (*LedgerTicketProvider)(nil) + +// NewLedgerTicketProvider builds a provider reading Tickets owned by +// vaultAddress over the JSON-RPC at rpcURL. +func NewLedgerTicketProvider(rpcURL, vaultAddress string) (*LedgerTicketProvider, error) { + cfg, err := rpc.NewClientConfig(rpcURL) + if err != nil { + return nil, fmt.Errorf("xrpl: create rpc config: %w", err) + } + return &LedgerTicketProvider{ + client: rpc.NewClient(cfg), + account: types.Address(vaultAddress), + }, nil +} + +// TicketFor returns the lowest TicketSequence currently owned by the vault. The +// withdrawalID is ignored — any of the account's Tickets authorizes any +// withdrawal. Errors if the account owns no Tickets. +func (p *LedgerTicketProvider) TicketFor(_ context.Context, _ [32]byte) (uint32, error) { + resp, err := p.client.GetAccountObjects(&account.ObjectsRequest{ + Account: p.account, + Type: account.TicketObject, + }) + if err != nil { + return 0, fmt.Errorf("xrpl: account_objects: %w", err) + } + seqs := make([]uint32, 0, len(resp.AccountObjects)) + for _, obj := range resp.AccountObjects { + if asString(obj["LedgerEntryType"]) != "Ticket" { + continue + } + if seq, ok := uint32Field(obj["TicketSequence"]); ok { + seqs = append(seqs, seq) + } + } + if len(seqs) == 0 { + return 0, fmt.Errorf("xrpl: account %s owns no tickets", p.account) + } + sort.Slice(seqs, func(i, j int) bool { return seqs[i] < seqs[j] }) + return seqs[0], nil +} diff --git a/pkg/blockchain/xrpl/vault_integration_test.go b/pkg/blockchain/xrpl/vault_integration_test.go index c03046c..c026d92 100644 --- a/pkg/blockchain/xrpl/vault_integration_test.go +++ b/pkg/blockchain/xrpl/vault_integration_test.go @@ -74,22 +74,22 @@ func TestIntegrationXRPL_DepositAndWithdraw(t *testing.T) { signerAddrs := make([]string, xrplSignerCount) for i := range signers { signers[i] = genEd25519(t) - signerAddrs[i] = mustIdentity(t, signers[i]).classicAddress + signerAddrs[i] = mustIdentity(t, signers[i]).ClassicAddress } // ── Setup ───────────────────────────────────────────────────────────────── - h.fund(ctx, t, master, masterID, vaultID.classicAddress, "1000000000") // 1000 XRP - h.fund(ctx, t, master, masterID, depID.classicAddress, "1000000000") // 1000 XRP + h.fund(ctx, t, master, masterID, vaultID.ClassicAddress, "1000000000") // 1000 XRP + h.fund(ctx, t, master, masterID, depID.ClassicAddress, "1000000000") // 1000 XRP h.signerListSet(ctx, t, vault, vaultID, signerAddrs, xrplQuorum) ticketSeq := h.ticketCreate(ctx, t, vault, vaultID) - t.Logf("vault %s signer-list set (quorum %d), ticket %d", vaultID.classicAddress, xrplQuorum, ticketSeq) + t.Logf("vault %s signer-list set (quorum %d), ticket %d", vaultID.ClassicAddress, xrplQuorum, ticketSeq) // ── Deposit flow ────────────────────────────────────────────────────────── - dep, err := NewDepositor(url, vaultID.classicAddress, depositor) + dep, err := NewDepositor(url, vaultID.ClassicAddress, depositor) if err != nil { t.Fatalf("NewDepositor: %v", err) } - depRef, err := dep.Deposit(ctx, "XRP", decimal.NewFromInt(100_000_000), fmt.Sprintf("xrpl-%d", depositTag)) // 100 XRP + depRef, err := dep.SubmitDeposit(ctx, "XRP", decimal.NewFromInt(100_000_000), fmt.Sprintf("xrpl-%d", depositTag)) // 100 XRP if err != nil { t.Fatalf("Deposit: %v", err) } @@ -99,7 +99,7 @@ func TestIntegrationXRPL_DepositAndWithdraw(t *testing.T) { // ── Withdrawal flow (quorum in-process) ─────────────────────────────────── finalizers := make([]*WithdrawalFinalizer, len(signers)) for i, s := range signers { - f, err := NewWithdrawalFinalizer(url, vaultID.classicAddress, xrplQuorum, s, fixedTicket(ticketSeq)) + f, err := NewWithdrawalFinalizer(url, vaultID.ClassicAddress, xrplQuorum, s, fixedTicket(ticketSeq)) if err != nil { t.Fatalf("NewWithdrawalFinalizer %d: %v", i, err) } @@ -108,7 +108,7 @@ func TestIntegrationXRPL_DepositAndWithdraw(t *testing.T) { var wid [32]byte wid[0], wid[31] = 0x12, 0x34 - op := &core.WithdrawalOp{Recipient: recID.classicAddress, L1Asset: "XRP", Amount: decimal.NewFromInt(50_000_000)} // 50 XRP + op := &core.WithdrawalOp{Recipient: recID.ClassicAddress, L1Asset: "XRP", Amount: decimal.NewFromInt(50_000_000)} // 50 XRP packed, err := finalizers[0].Pack(ctx, op, wid) if err != nil { @@ -125,11 +125,7 @@ func TestIntegrationXRPL_DepositAndWithdraw(t *testing.T) { } blobs = append(blobs, b) } - merged, err := finalizers[0].Merge(ctx, packed, blobs) - if err != nil { - t.Fatalf("Merge: %v", err) - } - ref, err := finalizers[0].Submit(ctx, merged) + ref, err := finalizers[0].Submit(ctx, packed, blobs) if err != nil { t.Fatalf("Submit: %v", err) } @@ -157,7 +153,7 @@ type xrplHarness struct { // submit autofills, single-signs with the given account, submits, and accepts a // ledger so the tx validates before the next call reads account state. -func (h *xrplHarness) submit(ctx context.Context, t *testing.T, s sign.Signer, id xrplIdentity, flatTx transaction.FlatTransaction) { +func (h *xrplHarness) submit(ctx context.Context, t *testing.T, s sign.Signer, id Identity, flatTx transaction.FlatTransaction) { t.Helper() if err := h.client.Autofill(&flatTx); err != nil { t.Fatalf("autofill: %v", err) @@ -176,17 +172,17 @@ func (h *xrplHarness) submit(ctx context.Context, t *testing.T, s sign.Signer, i h.ledgerAccept(ctx, t) } -func (h *xrplHarness) fund(ctx context.Context, t *testing.T, s sign.Signer, id xrplIdentity, dest, drops string) { +func (h *xrplHarness) fund(ctx context.Context, t *testing.T, s sign.Signer, id Identity, dest, drops string) { t.Helper() h.submit(ctx, t, s, id, transaction.FlatTransaction{ "TransactionType": "Payment", - "Account": id.classicAddress, + "Account": id.ClassicAddress, "Destination": dest, "Amount": drops, }) } -func (h *xrplHarness) signerListSet(ctx context.Context, t *testing.T, s sign.Signer, id xrplIdentity, signerAddrs []string, quorum int) { +func (h *xrplHarness) signerListSet(ctx context.Context, t *testing.T, s sign.Signer, id Identity, signerAddrs []string, quorum int) { t.Helper() entries := make([]any, len(signerAddrs)) for i, a := range signerAddrs { @@ -194,18 +190,18 @@ func (h *xrplHarness) signerListSet(ctx context.Context, t *testing.T, s sign.Si } h.submit(ctx, t, s, id, transaction.FlatTransaction{ "TransactionType": "SignerListSet", - "Account": id.classicAddress, + "Account": id.ClassicAddress, "SignerQuorum": quorum, "SignerEntries": entries, }) } // ticketCreate creates one Ticket on the account and returns its sequence. -func (h *xrplHarness) ticketCreate(ctx context.Context, t *testing.T, s sign.Signer, id xrplIdentity) uint32 { +func (h *xrplHarness) ticketCreate(ctx context.Context, t *testing.T, s sign.Signer, id Identity) uint32 { t.Helper() h.submit(ctx, t, s, id, transaction.FlatTransaction{ "TransactionType": "TicketCreate", - "Account": id.classicAddress, + "Account": id.ClassicAddress, "TicketCount": 1, }) // Read the created Ticket's sequence from account_objects. @@ -217,7 +213,7 @@ func (h *xrplHarness) ticketCreate(ctx context.Context, t *testing.T, s sign.Sig } `json:"account_objects"` } `json:"result"` } - h.rawRPC(ctx, t, "account_objects", map[string]any{"account": id.classicAddress, "type": "ticket"}, &resp) + h.rawRPC(ctx, t, "account_objects", map[string]any{"account": id.ClassicAddress, "type": "ticket"}, &resp) for _, o := range resp.Result.AccountObjects { if o.LedgerEntryType == "Ticket" { return o.TicketSequence @@ -292,9 +288,9 @@ func masterSigner(t *testing.T) sign.Signer { return sign.NewKeySignerFromECDSA(k) } -func mustIdentity(t *testing.T, s sign.Signer) xrplIdentity { +func mustIdentity(t *testing.T, s sign.Signer) Identity { t.Helper() - id, err := deriveIdentity(s) + id, err := DeriveIdentity(s) if err != nil { t.Fatalf("derive identity: %v", err) } diff --git a/pkg/blockchain/xrpl/wire.go b/pkg/blockchain/xrpl/wire.go index 96fa603..bac2ac0 100644 --- a/pkg/blockchain/xrpl/wire.go +++ b/pkg/blockchain/xrpl/wire.go @@ -59,37 +59,37 @@ func parseDepositTag(account string) (uint32, error) { return uint32(n), nil } -// xrplIdentity is a signer's XRPL classic address + signing pubkey hex. -type xrplIdentity struct { - classicAddress string - signingPubKeyHex string +// Identity is a signer's XRPL classic address + signing pubkey hex. +type Identity struct { + ClassicAddress string + SigningPubKeyHex string } -// deriveIdentity maps a sign.Signer's public key to its XRPL identity. -func deriveIdentity(s sign.Signer) (xrplIdentity, error) { +// DeriveIdentity maps a sign.Signer's public key to its XRPL identity. +func DeriveIdentity(s sign.Signer) (Identity, error) { pub := s.PublicKey() var xrplPub []byte switch s.Algorithm() { case sign.AlgSecp256k1: if len(pub) != 33 { - return xrplIdentity{}, fmt.Errorf("xrpl: secp256k1 pubkey must be 33-byte compressed, got %d", len(pub)) + return Identity{}, fmt.Errorf("xrpl: secp256k1 pubkey must be 33-byte compressed, got %d", len(pub)) } xrplPub = pub case sign.AlgEd25519: if len(pub) != 32 { - return xrplIdentity{}, fmt.Errorf("xrpl: ed25519 pubkey must be 32 bytes, got %d", len(pub)) + return Identity{}, fmt.Errorf("xrpl: ed25519 pubkey must be 32 bytes, got %d", len(pub)) } // XRPL ed25519 pubkeys take a 0xED prefix. xrplPub = append([]byte{0xED}, pub...) default: - return xrplIdentity{}, fmt.Errorf("xrpl: unsupported signer algorithm %q", s.Algorithm()) + return Identity{}, fmt.Errorf("xrpl: unsupported signer algorithm %q", s.Algorithm()) } pubHex := strings.ToUpper(hex.EncodeToString(xrplPub)) addr, err := addresscodec.EncodeClassicAddressFromPublicKeyHex(pubHex) if err != nil { - return xrplIdentity{}, fmt.Errorf("xrpl: derive classic address: %w", err) + return Identity{}, fmt.Errorf("xrpl: derive classic address: %w", err) } - return xrplIdentity{classicAddress: addr, signingPubKeyHex: pubHex}, nil + return Identity{ClassicAddress: addr, SigningPubKeyHex: pubHex}, nil } // signDigest runs the algorithm-specific signing primitive over the @@ -110,9 +110,9 @@ func signDigest(ctx context.Context, s sign.Signer, encodedHex string) ([]byte, } // signMultisig produces this node's multi-sign blob for tx. -func signMultisig(ctx context.Context, s sign.Signer, id xrplIdentity, tx transaction.FlatTransaction) (string, error) { +func signMultisig(ctx context.Context, s sign.Signer, id Identity, tx transaction.FlatTransaction) (string, error) { tx["SigningPubKey"] = "" - encoded, err := binarycodec.EncodeForMultisigning(tx, id.classicAddress) + encoded, err := binarycodec.EncodeForMultisigning(tx, id.ClassicAddress) if err != nil { return "", fmt.Errorf("xrpl: EncodeForMultisigning: %w", err) } @@ -121,17 +121,17 @@ func signMultisig(ctx context.Context, s sign.Signer, id xrplIdentity, tx transa return "", fmt.Errorf("xrpl: sign: %w", err) } inner := types.Signer{SignerData: types.SignerData{ - Account: types.Address(id.classicAddress), + Account: types.Address(id.ClassicAddress), TxnSignature: strings.ToUpper(hex.EncodeToString(sigBytes)), - SigningPubKey: id.signingPubKeyHex, + SigningPubKey: id.SigningPubKeyHex, }} tx["Signers"] = []any{inner.Flatten()} return binarycodec.Encode(tx) } // signSingle signs tx as a single-signer transaction and returns the submittable blob. -func signSingle(ctx context.Context, s sign.Signer, id xrplIdentity, tx transaction.FlatTransaction) (string, error) { - tx["SigningPubKey"] = id.signingPubKeyHex +func signSingle(ctx context.Context, s sign.Signer, id Identity, tx transaction.FlatTransaction) (string, error) { + tx["SigningPubKey"] = id.SigningPubKeyHex encoded, err := binarycodec.EncodeForSigning(tx) if err != nil { return "", fmt.Errorf("xrpl: EncodeForSigning: %w", err) @@ -144,8 +144,8 @@ func signSingle(ctx context.Context, s sign.Signer, id xrplIdentity, tx transact return binarycodec.Encode(tx) } -// buildAmount converts a WithdrawalOp into an XRPL CurrencyAmount. -func buildAmount(op *core.WithdrawalOp) (types.CurrencyAmount, error) { +// BuildAmount converts a WithdrawalOp into an XRPL CurrencyAmount. +func BuildAmount(op *core.WithdrawalOp) (types.CurrencyAmount, error) { return currencyAmount(op.L1Asset, op.Amount) } @@ -175,8 +175,8 @@ func currencyAmount(asset string, amount decimal.Decimal) (types.CurrencyAmount, return types.IssuedCurrencyAmount{Issuer: types.Address(issuer), Currency: currency, Value: amount.String()}, nil } -// validateCanonical asserts the canonical flatTx matches the op. -func validateCanonical(flat transaction.FlatTransaction, op *core.WithdrawalOp, withdrawalID [32]byte, vault string) error { +// ValidateCanonical asserts the canonical flatTx matches the op. +func ValidateCanonical(flat transaction.FlatTransaction, op *core.WithdrawalOp, withdrawalID [32]byte, vault string) error { if asString(flat["TransactionType"]) != "Payment" { return fmt.Errorf("xrpl canonical: wrong TransactionType %v", flat["TransactionType"]) } @@ -186,7 +186,7 @@ func validateCanonical(flat transaction.FlatTransaction, op *core.WithdrawalOp, if !strings.EqualFold(asString(flat["Destination"]), op.Recipient) { return fmt.Errorf("xrpl canonical: Destination %v != op.Recipient %s", flat["Destination"], op.Recipient) } - wantAmount, err := buildAmount(op) + wantAmount, err := BuildAmount(op) if err != nil { return fmt.Errorf("xrpl canonical: build expected Amount: %w", err) } @@ -288,8 +288,8 @@ func uint32Field(raw any) (uint32, bool) { } } -// canonicalJSON encodes a FlatTransaction with sorted keys. -func canonicalJSON(flatTx transaction.FlatTransaction) ([]byte, error) { +// CanonicalJSON encodes a FlatTransaction with sorted keys. +func CanonicalJSON(flatTx transaction.FlatTransaction) ([]byte, error) { keys := make([]string, 0, len(flatTx)) for k := range flatTx { keys = append(keys, k) diff --git a/pkg/blockchain/xrpl/withdrawal_finalizer.go b/pkg/blockchain/xrpl/withdrawal_finalizer.go index feec189..9e989c0 100644 --- a/pkg/blockchain/xrpl/withdrawal_finalizer.go +++ b/pkg/blockchain/xrpl/withdrawal_finalizer.go @@ -35,7 +35,7 @@ type WithdrawalFinalizer struct { vaultAddress string threshold int signer sign.Signer - id xrplIdentity + id Identity tickets TicketProvider } @@ -48,7 +48,7 @@ func NewWithdrawalFinalizer(rpcURL, vaultAddress string, threshold int, signer s if err != nil { return nil, fmt.Errorf("xrpl: create rpc config: %w", err) } - id, err := deriveIdentity(signer) + id, err := DeriveIdentity(signer) if err != nil { return nil, err } @@ -65,7 +65,7 @@ func NewWithdrawalFinalizer(rpcURL, vaultAddress string, threshold int, signer s // Pack binds a Ticket and builds the autofilled multi-sign Payment, returning // its sorted-key JSON. func (f *WithdrawalFinalizer) Pack(ctx context.Context, op *core.WithdrawalOp, withdrawalID [32]byte) ([]byte, error) { - amount, err := buildAmount(op) + amount, err := BuildAmount(op) if err != nil { return nil, err } @@ -90,7 +90,7 @@ func (f *WithdrawalFinalizer) Pack(ctx context.Context, op *core.WithdrawalOp, w } flatTx["Sequence"] = uint32(0) delete(flatTx, "LastLedgerSequence") - return canonicalJSON(flatTx) + return CanonicalJSON(flatTx) } // Validate re-derives the trust-bound shape from the op and asserts the packed @@ -100,7 +100,7 @@ func (f *WithdrawalFinalizer) Validate(_ context.Context, packed []byte, op *cor if err := json.Unmarshal(packed, &flat); err != nil { return fmt.Errorf("xrpl: decode packed: %w", err) } - return validateCanonical(flat, op, withdrawalID, f.vaultAddress) + return ValidateCanonical(flat, op, withdrawalID, f.vaultAddress) } // Sign multi-signs the packed Payment and returns this node's blob. @@ -116,14 +116,14 @@ func (f *WithdrawalFinalizer) Sign(ctx context.Context, packed []byte) ([]byte, return []byte(blob), nil } -// Merge combines the collected multi-sign blobs into one submittable blob. +// merge combines the collected multi-sign blobs into one submittable blob. // Exactly `threshold` signatures are included: Pack autofilled the multi-sign // fee for that count (base × (1 + threshold)), so including extras would // under-pay (telINSUF_FEE_P) and waste fee. Any threshold of the SignerList's // members satisfies the quorum. -func (f *WithdrawalFinalizer) Merge(_ context.Context, _ []byte, signatures [][]byte) ([]byte, error) { +func (f *WithdrawalFinalizer) merge(signatures [][]byte) (string, error) { if len(signatures) < f.threshold { - return nil, fmt.Errorf("xrpl: have %d signatures, need %d", len(signatures), f.threshold) + return "", fmt.Errorf("xrpl: have %d signatures, need %d", len(signatures), f.threshold) } blobs := make([]string, 0, f.threshold) for _, s := range signatures[:f.threshold] { @@ -131,14 +131,19 @@ func (f *WithdrawalFinalizer) Merge(_ context.Context, _ []byte, signatures [][] } final, err := xrpl.Multisign(blobs...) if err != nil { - return nil, fmt.Errorf("xrpl: combine signatures: %w", err) + return "", fmt.Errorf("xrpl: combine signatures: %w", err) } - return []byte(final), nil + return final, nil } -// Submit broadcasts the merged blob and returns the tx reference. -func (f *WithdrawalFinalizer) Submit(_ context.Context, merged []byte) (core.TxRef, error) { - result, err := f.client.SubmitMultisigned(string(merged), false) +// Submit combines the collected multi-sign blobs and broadcasts the result, +// returning the tx reference. +func (f *WithdrawalFinalizer) Submit(_ context.Context, _ []byte, signatures [][]byte) (core.TxRef, error) { + merged, err := f.merge(signatures) + if err != nil { + return core.TxRef{}, err + } + result, err := f.client.SubmitMultisigned(merged, false) if err != nil { return core.TxRef{}, fmt.Errorf("xrpl: submit_multisigned: %w", err) } diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index e2f17a3..2dea867 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -43,13 +43,48 @@ type FraudEvidenceSubmitter interface { SubmitWithdrawalFraudEvidence(ctx context.Context, evidence WithdrawalFraudEvidence) error } +// DepositStatus is the tri-state result of VerifyDeposit, distinguishing a +// deposit that settled, one that is still in flight, and one that never landed +// (or was dropped/reorged out). +type DepositStatus int + +const ( + // DepositAbsent: no matching deposit transaction is on chain or in the + // mempool (never broadcast, dropped, reorged out, or reverted/failed). + DepositAbsent DepositStatus = iota + // DepositPending: the deposit transaction is observed but not yet final — + // in the mempool, or with fewer than the requested confirmations. + DepositPending + // DepositConfirmed: the deposit transaction is final to the requested depth. + DepositConfirmed +) + +func (s DepositStatus) String() string { + switch s { + case DepositAbsent: + return "absent" + case DepositPending: + return "pending" + case DepositConfirmed: + return "confirmed" + default: + return "unknown" + } +} + // VaultDepositor moves funds into the L1 vault. The implementation owns the // depositor's signing identity (a sign.Signer supplied at construction) and // executes the deposit on its chain: a contract call (EVM), a funding tx to a // derived address (BTC), or a tagged Payment (XRPL). It expects only the asset, // amount, and crediting clearnet account. type VaultDepositor interface { - Deposit(ctx context.Context, asset string, amount decimal.Decimal, account string) (TxRef, error) + SubmitDeposit(ctx context.Context, asset string, amount decimal.Decimal, account string) (TxRef, error) + // VerifyDeposit reports whether the deposit identified by ref (a TxRef + // returned by SubmitDeposit) is present and final on chain — a pure read for + // replay/audit. minConf is the confirmation depth required for + // DepositConfirmed; chains with no numeric depth (Solana) map it onto a + // commitment level instead. + VerifyDeposit(ctx context.Context, ref TxRef, minConf uint64) (DepositStatus, error) } // VaultWithdrawalFinalizer turns an authorized withdrawal into an on-chain @@ -63,17 +98,17 @@ type VaultDepositor interface { // packed bytes match — the defense against a Byzantine packer; every node // runs it before Sign. // - Sign produces this node's signature over the packed bytes. -// - Merge combines the packed bytes with the collected quorum signatures into -// a submittable artifact. -// - Submit broadcasts the merged artifact. +// - Submit merges the packed bytes with the collected quorum signatures into +// a submittable artifact and broadcasts it. It filters the signatures +// against the live on-chain signer set and is idempotent against a +// withdrawal a peer has already executed. // - VerifyExecution reads canonical chain state to answer "already executed?" // for the retry/finalize loop. type VaultWithdrawalFinalizer interface { Pack(ctx context.Context, op *WithdrawalOp, withdrawalID [32]byte) ([]byte, error) Validate(ctx context.Context, packed []byte, op *WithdrawalOp, withdrawalID [32]byte) error Sign(ctx context.Context, packed []byte) ([]byte, error) - Merge(ctx context.Context, packed []byte, signatures [][]byte) ([]byte, error) - Submit(ctx context.Context, merged []byte) (TxRef, error) + Submit(ctx context.Context, packed []byte, signatures [][]byte) (TxRef, error) VerifyExecution(ctx context.Context, withdrawalID [32]byte) (txHash [32]byte, executed bool, err error) } From be89488c96ac85fc478d8eb3ef6ba7f1c67ec965 Mon Sep 17 00:00:00 2001 From: Anton Filonenko Date: Tue, 16 Jun 2026 18:02:12 +0300 Subject: [PATCH 05/15] feat(blockchain): add signer rotation across EVM, BTC, XRPL, Solana Add core.SignerRotationFinalizer (Pack/Validate/Sign/Submit/VerifyRotation) and a per-chain implementation: - EVM: updateSigners over the live quorum; rotation digest commits to chainId, vault, "updateSigners", keccak(newSigners,newThreshold) and the on-chain signerNonce, golden-tested against the contract. - Solana: update_signers with the ed25519 quorum verified via the Ed25519 precompile; digest binds the signers commitment + program nonce. - XRPL: multi-signed SignerListSet; replay defense is the account sequence. - BTC: no in-place form, so rotation is a sweep of every vault UTXO into the newly-derived vault, behind the same interface via a VaultStore seam that pivots on confirmation. Share the EVM quorum-signature merge (mergeQuorumSigs/fetchLiveQuorum) and the XRPL multisign combine between the withdrawal and rotation paths, and add abiutil.AddressArr for the rotation digest. Per-chain rotation integration tests run on the devnet. Co-Authored-By: Claude Fable 5 --- pkg/abiutil/types.go | 4 + pkg/blockchain/btc/rotation_finalizer.go | 286 +++++++++++++++ pkg/blockchain/btc/vault_integration_test.go | 101 +++++- pkg/blockchain/evm/artifacts/Custody.abi | 13 + pkg/blockchain/evm/artifacts/Custody.bin | 2 +- pkg/blockchain/evm/custody_abi.go | 35 +- pkg/blockchain/evm/quorum.go | 86 +++++ pkg/blockchain/evm/rotation_digest.go | 57 +++ pkg/blockchain/evm/rotation_digest_test.go | 71 ++++ pkg/blockchain/evm/rotation_finalizer.go | 300 ++++++++++++++++ pkg/blockchain/evm/vault_integration_test.go | 46 +++ pkg/blockchain/evm/withdrawal_finalizer.go | 82 +---- pkg/blockchain/sol/digest.go | 43 ++- pkg/blockchain/sol/rotation_finalizer.go | 339 ++++++++++++++++++ pkg/blockchain/sol/vault_integration_test.go | 116 ++++++ pkg/blockchain/xrpl/rotation_finalizer.go | 184 ++++++++++ pkg/blockchain/xrpl/vault_integration_test.go | 45 +++ pkg/blockchain/xrpl/wire.go | 106 ++++++ pkg/blockchain/xrpl/withdrawal_finalizer.go | 27 +- pkg/core/blockchain.go | 37 ++ 20 files changed, 1877 insertions(+), 103 deletions(-) create mode 100644 pkg/blockchain/btc/rotation_finalizer.go create mode 100644 pkg/blockchain/evm/quorum.go create mode 100644 pkg/blockchain/evm/rotation_digest.go create mode 100644 pkg/blockchain/evm/rotation_digest_test.go create mode 100644 pkg/blockchain/evm/rotation_finalizer.go create mode 100644 pkg/blockchain/sol/rotation_finalizer.go create mode 100644 pkg/blockchain/xrpl/rotation_finalizer.go diff --git a/pkg/abiutil/types.go b/pkg/abiutil/types.go index 01fcf22..62096c1 100644 --- a/pkg/abiutil/types.go +++ b/pkg/abiutil/types.go @@ -22,6 +22,9 @@ var ( // Fixed-size array types used by the BLS signature ABI encoding. Uint256Arr2 abi.Type Uint256Arr4 abi.Type + + // Dynamic array type used by the signer-rotation digest. + AddressArr abi.Type ) func init() { @@ -37,6 +40,7 @@ func init() { Bytes = must("bytes") Uint256Arr2 = must("uint256[2]") Uint256Arr4 = must("uint256[4]") + AddressArr = must("address[]") } func must(t string) abi.Type { diff --git a/pkg/blockchain/btc/rotation_finalizer.go b/pkg/blockchain/btc/rotation_finalizer.go new file mode 100644 index 0000000..f27ac3e --- /dev/null +++ b/pkg/blockchain/btc/rotation_finalizer.go @@ -0,0 +1,286 @@ +package btc + +import ( + "bytes" + "context" + "encoding/hex" + "fmt" + "sort" + + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + + "github.com/layer-3/clearnet-sdk/pkg/core" + "github.com/layer-3/clearnet-sdk/pkg/sign" +) + +// VaultStore is the seam that lets BTC rotation fit the in-place +// SignerRotationFinalizer interface. A P2WSH vault's address is a function of +// its signer set, so rotation is a sweep into a newly-derived vault, after which +// the daemon must pivot to that vault. Current supplies the active vault (the +// source of UTXOs to sweep and the set that authorizes the sweep); Pivot adopts +// the new vault once the sweep confirms. +// +// Pivot must be idempotent: it is called by every node from VerifyRotation when +// the sweep is observed, and re-adopting the already-current vault is a no-op. +type VaultStore interface { + Current(ctx context.Context) (pubkeys [][]byte, threshold int, err error) + Pivot(ctx context.Context, pubkeys [][]byte, threshold int) error +} + +// RotationFinalizer rotates a BTC P2WSH vault by sweeping every old-vault UTXO +// into the vault derived from the new signer set. It implements +// core.SignerRotationFinalizer. The signing/merge/UTXO machinery is the +// withdrawal path (it is mechanically a withdrawal whose inputs are all +// old-vault UTXOs and whose single output is the new vault); the extra pieces +// are the sweep build and the post-confirmation pivot via the VaultStore. +type RotationFinalizer struct { + net *chaincfg.Params + rpc RPC + signer sign.Signer + store VaultStore + cfg Config + accounts []string // per-account deposit URIs whose UTXOs must also be swept +} + +var _ core.SignerRotationFinalizer = (*RotationFinalizer)(nil) + +// NewRotationFinalizer builds the BTC rotation finalizer. signer is this node's +// vault key; store supplies the current vault and receives the pivot. accountURIs +// are the per-account deposit accounts whose tagged-address UTXOs must be +// included in the sweep (the base vault is always swept) — undeclared accounts' +// UTXOs would be stranded under the old vault. +func NewRotationFinalizer(net *chaincfg.Params, rpc RPC, signer sign.Signer, store VaultStore, cfg Config, accountURIs ...string) (*RotationFinalizer, error) { + if signer.Algorithm() != sign.AlgSecp256k1 { + return nil, fmt.Errorf("btc: rotation signer must be secp256k1, got %s", signer.Algorithm()) + } + return &RotationFinalizer{ + net: net, + rpc: rpc, + signer: signer, + store: store, + cfg: cfg, + accounts: accountURIs, + }, nil +} + +// currentVault builds a withdrawal finalizer over the current vault (from the +// store), registering the deposit accounts so its spend-script set covers the +// base vault plus every tagged deposit address to sweep. It provides the shared +// UTXO/sign/merge machinery. +func (f *RotationFinalizer) currentVault(ctx context.Context) (*WithdrawalFinalizer, error) { + pubkeys, threshold, err := f.store.Current(ctx) + if err != nil { + return nil, fmt.Errorf("btc: read current vault: %w", err) + } + cur, err := NewWithdrawalFinalizer(f.net, f.rpc, f.signer, pubkeys, threshold, f.cfg) + if err != nil { + return nil, fmt.Errorf("btc: build current vault: %w", err) + } + if err := cur.RegisterDepositAccounts(f.accounts...); err != nil { + return nil, err + } + return cur, nil +} + +// newVaultAddress derives the destination vault address + pkScript from the +// incoming signer set. +func (f *RotationFinalizer) newVaultAddress(newSigners []string, newThreshold int) (btcutil.Address, []byte, [][]byte, error) { + pubkeys, err := parseVaultPubkeys(newSigners) + if err != nil { + return nil, nil, nil, err + } + redeem, err := RedeemScript(newThreshold, pubkeys) + if err != nil { + return nil, nil, nil, err + } + addr, err := VaultAddress(redeem, f.net) + if err != nil { + return nil, nil, nil, fmt.Errorf("btc: derive new vault address: %w", err) + } + pk, err := PkScript(addr) + if err != nil { + return nil, nil, nil, fmt.Errorf("btc: new vault pkScript: %w", err) + } + return addr, pk, pubkeys, nil +} + +// Pack lists every current-vault UTXO and builds the unsigned sweep: all of them +// as inputs, a single output paying the new vault the total minus fee. +func (f *RotationFinalizer) Pack(ctx context.Context, newSigners []string, newThreshold int) ([]byte, error) { + cur, err := f.currentVault(ctx) + if err != nil { + return nil, err + } + newVault, _, _, err := f.newVaultAddress(newSigners, newThreshold) + if err != nil { + return nil, err + } + unspent, err := f.rpc.ListUnspent(ctx, int(f.cfg.ConfirmationDepth), cur.watchAddresses()) + if err != nil { + return nil, fmt.Errorf("btc: list vault utxos: %w", err) + } + utxos, err := cur.toUTXOs(unspent) + if err != nil { + return nil, err + } + if len(utxos) == 0 { + return nil, fmt.Errorf("btc: vault has no UTXOs to sweep") + } + feeRate, err := f.rpc.EstimateSmartFeeSatPerVByte(ctx, f.cfg.FeeConfTarget, f.cfg.FallbackFeeRate) + if err != nil { + return nil, fmt.Errorf("btc: estimate fee: %w", err) + } + tx, err := buildSweepTx(utxos, newVault, feeRate) + if err != nil { + return nil, err + } + return serializeTx(tx) +} + +// Validate re-derives the new vault and asserts the packed sweep pays exactly it +// from a single output, consumes only current-vault UTXOs, and keeps the implied +// fee within the ceiling. +func (f *RotationFinalizer) Validate(ctx context.Context, packed []byte, newSigners []string, newThreshold int) error { + _, newVaultScript, _, err := f.newVaultAddress(newSigners, newThreshold) + if err != nil { + return err + } + tx, err := deserializeTx(packed) + if err != nil { + return fmt.Errorf("btc rotation validate: %w", err) + } + if len(tx.TxOut) != 1 { + return fmt.Errorf("btc rotation validate: expected 1 output, got %d", len(tx.TxOut)) + } + if !bytes.Equal(tx.TxOut[0].PkScript, newVaultScript) { + return fmt.Errorf("btc rotation validate: output not paid to the new vault") + } + cur, err := f.currentVault(ctx) + if err != nil { + return err + } + totalIn, err := cur.sumValidatedInputs(ctx, tx) + if err != nil { + return err + } + fee := totalIn - tx.TxOut[0].Value + if fee < 0 { + return fmt.Errorf("btc rotation validate: output exceeds inputs (fee %d)", fee) + } + if cap := EstimateFeeSats(len(tx.TxIn), 1, f.cfg.FeeCapSatPerVByte); f.cfg.FeeCapSatPerVByte > 0 && fee > cap { + return fmt.Errorf("btc rotation validate: fee %d exceeds ceiling %d", fee, cap) + } + return nil +} + +// Sign produces this node's per-input signatures over the sweep, delegating to +// the current-vault signing machinery. +func (f *RotationFinalizer) Sign(ctx context.Context, packed []byte) ([]byte, error) { + cur, err := f.currentVault(ctx) + if err != nil { + return nil, err + } + return cur.Sign(ctx, packed) +} + +// Submit assembles the witnesses from the collected shares and broadcasts the +// sweep. Idempotent by the UTXO model: if the sweep already landed, its inputs +// are spent and the rebroadcast is rejected as already-known/missing-inputs, in +// which case the original tx hash is returned. +func (f *RotationFinalizer) Submit(ctx context.Context, packed []byte, shares [][]byte) (core.TxRef, error) { + cur, err := f.currentVault(ctx) + if err != nil { + return core.TxRef{}, err + } + merged, err := cur.merge(ctx, packed, shares) + if err != nil { + return core.TxRef{}, err + } + tx, err := deserializeTx(merged) + if err != nil { + return core.TxRef{}, fmt.Errorf("btc rotation submit: %w", err) + } + hash := [32]byte(tx.TxHash()) + txid := hashToTxid(hash) + if _, err := f.rpc.SendRawTransaction(ctx, hex.EncodeToString(merged)); err != nil { + if isAlreadyKnown(err) { + return core.TxRef{Hash: hash, Raw: txid}, nil + } + return core.TxRef{}, fmt.Errorf("btc rotation submit: sendrawtransaction: %w", err) + } + return core.TxRef{Hash: hash, Raw: txid}, nil +} + +// VerifyRotation reports whether the sweep landed — the new vault holds at least +// one confirmed UTXO — and, when so, pivots the store to the new vault. Binary; +// the sweep tx hash is not recovered here, so a zero hash is returned with +// done=true. Note: a vault with nothing to sweep cannot be observed as rotated. +func (f *RotationFinalizer) VerifyRotation(ctx context.Context, newSigners []string, newThreshold int) ([32]byte, bool, error) { + newVault, _, newPubkeys, err := f.newVaultAddress(newSigners, newThreshold) + if err != nil { + return [32]byte{}, false, err + } + unspent, err := f.rpc.ListUnspent(ctx, int(f.cfg.ConfirmationDepth), []string{newVault.EncodeAddress()}) + if err != nil { + return [32]byte{}, false, fmt.Errorf("btc rotation verify: list new vault utxos: %w", err) + } + if len(unspent) == 0 { + return [32]byte{}, false, nil + } + if err := f.store.Pivot(ctx, newPubkeys, newThreshold); err != nil { + return [32]byte{}, false, fmt.Errorf("btc rotation verify: pivot: %w", err) + } + return [32]byte{}, true, nil +} + +// buildSweepTx builds the unsigned sweep: every UTXO as an input, a single +// output paying newVault the total minus the estimated fee. +func buildSweepTx(utxos []UTXO, newVault btcutil.Address, feeRate int64) (*wire.MsgTx, error) { + ordered := make([]UTXO, len(utxos)) + copy(ordered, utxos) + sort.Slice(ordered, func(i, j int) bool { + if c := compareHash(ordered[i].TxID[:], ordered[j].TxID[:]); c != 0 { + return c < 0 + } + return ordered[i].Vout < ordered[j].Vout + }) + + var total int64 + tx := wire.NewMsgTx(wire.TxVersion) + for _, u := range ordered { + op := wire.NewOutPoint(&u.TxID, u.Vout) + tx.AddTxIn(wire.NewTxIn(op, nil, nil)) + total += u.Amount + } + fee := EstimateFeeSats(len(ordered), 1, feeRate) + out := total - fee + if out < dustThresholdSats { + return nil, fmt.Errorf("btc: sweep output %d below dust after fee %d (total %d)", out, fee, total) + } + script, err := txscript.PayToAddrScript(newVault) + if err != nil { + return nil, fmt.Errorf("btc: new vault script: %w", err) + } + tx.AddTxOut(wire.NewTxOut(out, script)) + return tx, nil +} + +// parseVaultPubkeys decodes the incoming signer set (33-byte compressed pubkey +// hex) for vault derivation. Ordering is handled by RedeemScript (BIP-67). +func parseVaultPubkeys(newSigners []string) ([][]byte, error) { + if len(newSigners) == 0 { + return nil, fmt.Errorf("btc: empty new signer set") + } + out := make([][]byte, 0, len(newSigners)) + for _, s := range newSigners { + b, err := hex.DecodeString(s) + if err != nil || len(b) != 33 { + return nil, fmt.Errorf("btc: signer %q must be a 33-byte compressed pubkey hex", s) + } + out = append(out, b) + } + return out, nil +} diff --git a/pkg/blockchain/btc/vault_integration_test.go b/pkg/blockchain/btc/vault_integration_test.go index 7d04799..3eb2919 100644 --- a/pkg/blockchain/btc/vault_integration_test.go +++ b/pkg/blockchain/btc/vault_integration_test.go @@ -4,8 +4,10 @@ package btc import ( "context" + "encoding/hex" "os" "strings" + "sync" "testing" "time" @@ -70,10 +72,23 @@ func TestIntegrationBTC_DepositAndWithdraw(t *testing.T) { t.Fatalf("DepositAddress: %v", err) } - // Watch the depositor + deposit addresses so listunspent/gettxout see them, - // then fund the depositor from the node wallet. + // Base vault address — the withdrawal pays change here, and the rotation + // sweep later spends it; watch it up front (rescan=false) so the change UTXO + // is tracked from creation. + baseRedeem, err := RedeemScript(btcThreshold, pubkeys) + if err != nil { + t.Fatalf("base redeem: %v", err) + } + baseVault, err := VaultAddress(baseRedeem, net) + if err != nil { + t.Fatalf("base vault: %v", err) + } + + // Watch the depositor + deposit + base-vault addresses so listunspent/gettxout + // see them, then fund the depositor from the node wallet. node.importAddress(ctx, t, depositor.DepositorAddress()) node.importAddress(ctx, t, depositAddr.EncodeAddress()) + node.importAddress(ctx, t, baseVault.EncodeAddress()) node.sendToAddress(ctx, t, depositor.DepositorAddress(), 1.0) // 1 BTC node.generateToAddress(ctx, t, 1, miner) @@ -129,6 +144,88 @@ func TestIntegrationBTC_DepositAndWithdraw(t *testing.T) { } else if !executed { t.Fatal("withdrawal not reported executed") } + + // ── Rotation flow (sweep the vault into a new vault; current signers sign) ─ + newSigners := make([]sign.Signer, btcSignerCount) + newPubkeys := make([][]byte, btcSignerCount) + newPubHex := make([]string, btcSignerCount) + for i := range newSigners { + newSigners[i] = genSecpSigner(t) + newPubkeys[i] = newSigners[i].PublicKey() + newPubHex[i] = hex.EncodeToString(newPubkeys[i]) + } + // Watch the new vault so listunspent sees the swept output. + newRedeem, err := RedeemScript(btcThreshold, newPubkeys) + if err != nil { + t.Fatalf("new redeem: %v", err) + } + newVaultAddr, err := VaultAddress(newRedeem, net) + if err != nil { + t.Fatalf("new vault addr: %v", err) + } + node.importAddress(ctx, t, newVaultAddr.EncodeAddress()) + + store := &memVaultStore{pubkeys: pubkeys, threshold: btcThreshold} + rotators := make([]*RotationFinalizer, btcSignerCount) + for i, s := range signers { + r, err := NewRotationFinalizer(net, node, s, store, cfg, account) + if err != nil { + t.Fatalf("NewRotationFinalizer %d: %v", i, err) + } + rotators[i] = r + } + + rPacked, err := rotators[0].Pack(ctx, newPubHex, btcThreshold) + if err != nil { + t.Fatalf("rotation Pack: %v", err) + } + rShares := make([][]byte, 0, len(rotators)) + for i, r := range rotators { + if err := r.Validate(ctx, rPacked, newPubHex, btcThreshold); err != nil { + t.Fatalf("rotation Validate[%d]: %v", i, err) + } + s, err := r.Sign(ctx, rPacked) + if err != nil { + t.Fatalf("rotation Sign[%d]: %v", i, err) + } + rShares = append(rShares, s) + } + rRef, err := rotators[0].Submit(ctx, rPacked, rShares) + if err != nil { + t.Fatalf("rotation Submit: %v", err) + } + node.generateToAddress(ctx, t, 1, miner) // confirm the sweep + t.Logf("rotation sweep tx %s", rRef.Raw) + + if _, done, err := rotators[0].VerifyRotation(ctx, newPubHex, btcThreshold); err != nil { + t.Fatalf("VerifyRotation: %v", err) + } else if !done { + t.Fatal("rotation not reported done") + } + // VerifyRotation pivots the store on success. + if cur, _, _ := store.Current(ctx); len(cur) != btcSignerCount { + t.Fatalf("store not pivoted: %d pubkeys", len(cur)) + } +} + +// memVaultStore is an in-memory btc.VaultStore for the rotation test. +type memVaultStore struct { + mu sync.Mutex + pubkeys [][]byte + threshold int +} + +func (s *memVaultStore) Current(context.Context) ([][]byte, int, error) { + s.mu.Lock() + defer s.mu.Unlock() + return s.pubkeys, s.threshold, nil +} + +func (s *memVaultStore) Pivot(_ context.Context, pubkeys [][]byte, threshold int) error { + s.mu.Lock() + defer s.mu.Unlock() + s.pubkeys, s.threshold = pubkeys, threshold + return nil } func genSecpSigner(t *testing.T) sign.Signer { diff --git a/pkg/blockchain/evm/artifacts/Custody.abi b/pkg/blockchain/evm/artifacts/Custody.abi index 6d5ebf7..a5d08b1 100644 --- a/pkg/blockchain/evm/artifacts/Custody.abi +++ b/pkg/blockchain/evm/artifacts/Custody.abi @@ -113,6 +113,19 @@ ], "stateMutability": "view" }, + { + "type": "function", + "name": "signerNonce", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, { "type": "function", "name": "signers", diff --git a/pkg/blockchain/evm/artifacts/Custody.bin b/pkg/blockchain/evm/artifacts/Custody.bin index 0ef1520..d383e92 100644 --- a/pkg/blockchain/evm/artifacts/Custody.bin +++ b/pkg/blockchain/evm/artifacts/Custody.bin @@ -1 +1 @@ -0x60806040523461039c5761136c80380380610019816103a0565b928339810160408282031261039c5781516001600160401b03811161039c5782019181601f8401121561039c578251926001600160401b03841161029a578360051b9260206100698186016103a0565b8096815201916020839582010191821161039c57602001915b81831061037c575050506020015160017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055801561033757808351106102f35760038351106102ae575f5b83518110156101fc576001600160a01b036100e882866103c5565b5116156101b75780610126575b6001906001600160a01b0361010a82876103c5565b51165f528160205260405f208260ff19825416179055016100cd565b6001600160a01b0361013882866103c5565b51165f1982018281116101a3576001600160a01b039061015890876103c5565b5116106100f557606460405162461bcd60e51b815260206004820152602060248201527f5369676e657273206d75737420626520736f7274656420617363656e64696e676044820152fd5b634e487b7160e01b5f52601160045260245ffd5b60405162461bcd60e51b815260206004820152601360248201527f5a65726f2061646472657373207369676e6572000000000000000000000000006044820152606490fd5b509151906001600160401b03821161029a5768010000000000000000821161029a576003548260035580831061026f575b5060035f5260205f205f5b8381106102525784600255604051610f7e90816103ee8239f35b82516001600160a01b031681830155602090920191600101610238565b60035f52828060205f20019103905f5b82811061028d57505061022d565b5f8282015560010161027f565b634e487b7160e01b5f52604160045260245ffd5b60405162461bcd60e51b815260206004820152601760248201527f4e656564206174206c656173742033207369676e6572730000000000000000006044820152606490fd5b606460405162461bcd60e51b815260206004820152602060248201527f4e6f7420656e6f756768207369676e65727320666f72207468726573686f6c646044820152fd5b60405162461bcd60e51b815260206004820152601a60248201527f5468726573686f6c64206d75737420626520706f7369746976650000000000006044820152606490fd5b82516001600160a01b038116810361039c57815260209283019201610082565b5f80fd5b6040519190601f01601f191682016001600160401b0381118382101761029a57604052565b80518210156103d95760209160051b010190565b634e487b7160e01b5f52603260045260245ffdfe608080604052600436101561001c575b50361561001a575f80fd5b005b5f3560e01c9081630e2411ac1461066957508063191d0a49146103d557806342cde4e8146103b857806346f0975a146103035780637df73e27146102c65780638340f549146100a75763a9fcfb3314610075575f61000f565b346100a35760203660031901126100a3576004355f525f602052602060ff60405f2054166040519015158152f35b5f80fd5b60603660031901126100a3576100bb610aad565b6100c3610ac3565b604435916100cf610dc4565b6001600160a01b0316918215610292576100ea811515610b85565b6001600160a01b038216806101ad57508034036101735761014a7f4174a9435a04d04d274c76779cad136a41fde6937c56241c09ab9d3c7064a1a9915b604080516001600160a01b03909516855260208501919091523393918291820190565b0390a360017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055005b60405162461bcd60e51b815260206004820152601260248201527108aa89040ecc2d8eaca40dad2e6dac2e8c6d60731b6044820152606490fd5b3461024d576040516323b872dd60e01b5f5233600452306024528260445260205f60648180865af19060015f511482161561022c575b6040525f6060521561021a575061014a7f4174a9435a04d04d274c76779cad136a41fde6937c56241c09ab9d3c7064a1a991610127565b635274afe760e01b5f5260045260245ffd5b90600181151661024457823b15153d151616906101e3565b503d5f823e3d90fd5b60405162461bcd60e51b815260206004820152601b60248201527f4554482073656e742077697468204552433230206465706f73697400000000006044820152606490fd5b60405162461bcd60e51b815260206004820152600c60248201526b16995c9bc81858d8dbdd5b9d60a21b6044820152606490fd5b346100a35760203660031901126100a3576001600160a01b036102e7610aad565b165f526001602052602060ff60405f2054166040519015158152f35b346100a3575f3660031901126100a3576040518060206003549283815201809260035f525f516020610f5e5f395f51905f52905f5b818110610399575050508161034e910382610b2b565b604051918291602083019060208452518091526040830191905f5b818110610377575050500390f35b82516001600160a01b0316845285945060209384019390920191600101610369565b82546001600160a01b0316845260209093019260019283019201610338565b346100a3575f3660031901126100a3576020600254604051908152f35b346100a35760a03660031901126100a3576103ee610aad565b6103f6610ac3565b6064359060443560843567ffffffffffffffff81116100a35761041d903690600401610a7c565b9490610427610dc4565b845f525f60205260ff60405f205416610631576001600160a01b0382169586156105fb576104a49061045a851515610b85565b604051926020840146815230604086015289606086015260018060a01b038816948560808201528760a08201528960c082015260c0815261049c60e082610b2b565b519020610bdb565b845f525f60205260405f20600160ff1982541617905580155f1461057e57505f80808481945af13d15610579573d6104db81610bbf565b906104e96040519283610b2b565b81525f60203d92013e5b1561053e577fe57dd573634102b6cae74aab341f709f6fc3ae2bdc0a35f9a47a85f45b677a21915b604080516001600160a01b0390921682526020820192909252908190810161014a565b60405162461bcd60e51b8152602060048201526013602482015272115512081d1c985b9cd9995c8819985a5b1959606a1b6044820152606490fd5b6104f3565b90505f9291925060405163a9059cbb60e01b5f52856004528360245260205f60448180865af19060015f51148216156105e3575b6040521561021a5750907fe57dd573634102b6cae74aab341f709f6fc3ae2bdc0a35f9a47a85f45b677a219161051b565b90600181151661024457823b15153d151616906105b2565b60405162461bcd60e51b815260206004820152600e60248201526d16995c9bc81c9958da5c1a595b9d60921b6044820152606490fd5b60405162461bcd60e51b815260206004820152601060248201526f105b1c9958591e48195e1958dd5d195960821b6044820152606490fd5b346100a35760603660031901126100a35760043567ffffffffffffffff81116100a35761069a903690600401610a7c565b6024359260443567ffffffffffffffff81116100a3576106be903690600401610a7c565b90918515610a3a57508483106109f657600383106109b15761074c916040516020810190610700816106f28a898b87610ad9565b03601f198101835282610b2b565b519020604051602081019146835230604083015260806060830152600d60a08301526c7570646174655369676e65727360981b60c0830152608082015260c0815261049c60e082610b2b565b5f5b600354811015610790575f516020610f5e5f395f51905f528101546001600160a01b03165f908152600160208190526040909120805460ff191690550161074e565b50905f5b828110610881575067ffffffffffffffff821161086d5768010000000000000000821161086d57816003548160035580821061083f575b50508060035f525f5b838110610817575050610812837feb4dc7fab86d67670d7a4d7443a38860da1aa053f26529c8f41cc68e5d6a93369460025560405193849384610ad9565b0390a1005b600190602061082584610b71565b930192815f516020610f5e5f395f51905f520155016107d4565b035f5b818110610851578391506107cb565b5f8482015f516020610f5e5f395f51905f520155600101610842565b634e487b7160e01b5f52604160045260245ffd5b6001600160a01b0361089c610897838686610b4d565b610b71565b161561097657806108dc575b6001906001600160a01b036108c1610897838787610b4d565b165f528160205260405f208260ff1982541617905501610794565b6108ea610897828585610b4d565b5f198201828111610962576001600160a01b039061090d90610897908787610b4d565b166001600160a01b03909116116108a857606460405162461bcd60e51b815260206004820152602060248201527f5369676e657273206d75737420626520736f7274656420617363656e64696e676044820152fd5b634e487b7160e01b5f52601160045260245ffd5b60405162461bcd60e51b81526020600482015260136024820152722d32b9379030b2323932b9b99039b4b3b732b960691b6044820152606490fd5b60405162461bcd60e51b815260206004820152601760248201527f4e656564206174206c656173742033207369676e6572730000000000000000006044820152606490fd5b606460405162461bcd60e51b815260206004820152602060248201527f4e6f7420656e6f756768207369676e65727320666f72207468726573686f6c646044820152fd5b62461bcd60e51b815260206004820152601a60248201527f5468726573686f6c64206d75737420626520706f7369746976650000000000006044820152606490fd5b9181601f840112156100a35782359167ffffffffffffffff83116100a3576020808501948460051b0101116100a357565b600435906001600160a01b03821682036100a357565b602435906001600160a01b03821682036100a357565b6040808252810183905293929160608501905f905b808210610b0057505060209150930152565b909183356001600160a01b03811691908290036100a357908152602093840193019160010190610aee565b90601f8019910116810190811067ffffffffffffffff82111761086d57604052565b9190811015610b5d5760051b0190565b634e487b7160e01b5f52603260045260245ffd5b356001600160a01b03811681036100a35790565b15610b8c57565b60405162461bcd60e51b815260206004820152600b60248201526a16995c9bc8185b5bdd5b9d60aa1b6044820152606490fd5b67ffffffffffffffff811161086d57601f01601f191660200190565b9060025490818410610d8d575f948592835b86881015610d39578760051b840135601e19853603018112156100a35784019081359167ffffffffffffffff83116100a357602081019083360382136100a357610c3684610bbf565b90610c446040519283610b2b565b84825260208536920101116100a3575f602085610c7696610c6d95838601378301015288610e22565b90939193610e5c565b6001600160a01b038281169116811115610ce8575f52600160205260ff60405f20541615610cb457935f198114610962576001978801970193610bed565b60405162461bcd60e51b815260206004820152600c60248201526b2737ba10309039b4b3b732b960a11b6044820152606490fd5b60405162461bcd60e51b815260206004820152602360248201527f5369676e617475726573206e6f74206f726465726564206f72206475706c696360448201526261746560e81b6064820152608490fd5b509450945050905010610d4857565b60405162461bcd60e51b815260206004820152601d60248201527f496e73756666696369656e742076616c6964207369676e6174757265730000006044820152606490fd5b60405162461bcd60e51b815260206004820152600f60248201526e10995b1bddc81d1a1c995cda1bdb19608a1b6044820152606490fd5b60027f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f005414610e135760027f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055565b633ee5aeb560e01b5f5260045ffd5b8151919060418303610e5257610e4b9250602082015190606060408401519301515f1a90610ed0565b9192909190565b50505f9160029190565b6004811015610ebc5780610e6e575050565b60018103610e855763f645eedf60e01b5f5260045ffd5b60028103610ea0575063fce698f760e01b5f5260045260245ffd5b600314610eaa5750565b6335e2f38360e21b5f5260045260245ffd5b634e487b7160e01b5f52602160045260245ffd5b91907f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08411610f52579160209360809260ff5f9560405194855216868401526040830152606082015282805260015afa15610f47575f516001600160a01b03811615610f3d57905f905f90565b505f906001905f90565b6040513d5f823e3d90fd5b5050505f916003919056fec2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b +60806040523461039c576113a580380380610019816103a0565b928339810160408282031261039c5781516001600160401b03811161039c5782019181601f8401121561039c578251926001600160401b03841161029a578360051b9260206100698186016103a0565b8096815201916020839582010191821161039c57602001915b81831061037c575050506020015160017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055801561033757808351106102f35760038351106102ae575f5b83518110156101fc576001600160a01b036100e882866103c5565b5116156101b75780610126575b6001906001600160a01b0361010a82876103c5565b51165f528160205260405f208260ff19825416179055016100cd565b6001600160a01b0361013882866103c5565b51165f1982018281116101a3576001600160a01b039061015890876103c5565b5116106100f557606460405162461bcd60e51b815260206004820152602060248201527f5369676e657273206d75737420626520736f7274656420617363656e64696e676044820152fd5b634e487b7160e01b5f52601160045260245ffd5b60405162461bcd60e51b815260206004820152601360248201527f5a65726f2061646472657373207369676e6572000000000000000000000000006044820152606490fd5b509151906001600160401b03821161029a5768010000000000000000821161029a576004548260045580831061026f575b5060045f5260205f205f5b8381106102525784600255604051610fb790816103ee8239f35b82516001600160a01b031681830155602090920191600101610238565b60045f52828060205f20019103905f5b82811061028d57505061022d565b5f8282015560010161027f565b634e487b7160e01b5f52604160045260245ffd5b60405162461bcd60e51b815260206004820152601760248201527f4e656564206174206c656173742033207369676e6572730000000000000000006044820152606490fd5b606460405162461bcd60e51b815260206004820152602060248201527f4e6f7420656e6f756768207369676e65727320666f72207468726573686f6c646044820152fd5b60405162461bcd60e51b815260206004820152601a60248201527f5468726573686f6c64206d75737420626520706f7369746976650000000000006044820152606490fd5b82516001600160a01b038116810361039c57815260209283019201610082565b5f80fd5b6040519190601f01601f191682016001600160401b0381118382101761029a57604052565b80518210156103d95760209160051b010190565b634e487b7160e01b5f52603260045260245ffdfe608080604052600436101561001c575b50361561001a575f80fd5b005b5f3560e01c9081630ce8d62214610a9b575080630e2411ac14610674578063191d0a49146103e057806342cde4e8146103c357806346f0975a1461030e5780637df73e27146102d15780638340f549146100b25763a9fcfb3314610080575f61000f565b346100ae5760203660031901126100ae576004355f525f602052602060ff60405f2054166040519015158152f35b5f80fd5b60603660031901126100ae576100c6610ae6565b6100ce610afc565b604435916100da610dfd565b6001600160a01b031691821561029d576100f5811515610bbe565b6001600160a01b038216806101b8575080340361017e576101557f4174a9435a04d04d274c76779cad136a41fde6937c56241c09ab9d3c7064a1a9915b604080516001600160a01b03909516855260208501919091523393918291820190565b0390a360017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055005b60405162461bcd60e51b815260206004820152601260248201527108aa89040ecc2d8eaca40dad2e6dac2e8c6d60731b6044820152606490fd5b34610258576040516323b872dd60e01b5f5233600452306024528260445260205f60648180865af19060015f5114821615610237575b6040525f6060521561022557506101557f4174a9435a04d04d274c76779cad136a41fde6937c56241c09ab9d3c7064a1a991610132565b635274afe760e01b5f5260045260245ffd5b90600181151661024f57823b15153d151616906101ee565b503d5f823e3d90fd5b60405162461bcd60e51b815260206004820152601b60248201527f4554482073656e742077697468204552433230206465706f73697400000000006044820152606490fd5b60405162461bcd60e51b815260206004820152600c60248201526b16995c9bc81858d8dbdd5b9d60a21b6044820152606490fd5b346100ae5760203660031901126100ae576001600160a01b036102f2610ae6565b165f526001602052602060ff60405f2054166040519015158152f35b346100ae575f3660031901126100ae576040518060206004549283815201809260045f525f516020610f975f395f51905f52905f5b8181106103a45750505081610359910382610b64565b604051918291602083019060208452518091526040830191905f5b818110610382575050500390f35b82516001600160a01b0316845285945060209384019390920191600101610374565b82546001600160a01b0316845260209093019260019283019201610343565b346100ae575f3660031901126100ae576020600254604051908152f35b346100ae5760a03660031901126100ae576103f9610ae6565b610401610afc565b6064359060443560843567ffffffffffffffff81116100ae57610428903690600401610ab5565b9490610432610dfd565b845f525f60205260ff60405f20541661063c576001600160a01b038216958615610606576104af90610465851515610bbe565b604051926020840146815230604086015289606086015260018060a01b038816948560808201528760a08201528960c082015260c081526104a760e082610b64565b519020610c14565b845f525f60205260405f20600160ff1982541617905580155f1461058957505f80808481945af13d15610584573d6104e681610bf8565b906104f46040519283610b64565b81525f60203d92013e5b15610549577fe57dd573634102b6cae74aab341f709f6fc3ae2bdc0a35f9a47a85f45b677a21915b604080516001600160a01b03909216825260208201929092529081908101610155565b60405162461bcd60e51b8152602060048201526013602482015272115512081d1c985b9cd9995c8819985a5b1959606a1b6044820152606490fd5b6104fe565b90505f9291925060405163a9059cbb60e01b5f52856004528360245260205f60448180865af19060015f51148216156105ee575b604052156102255750907fe57dd573634102b6cae74aab341f709f6fc3ae2bdc0a35f9a47a85f45b677a2191610526565b90600181151661024f57823b15153d151616906105bd565b60405162461bcd60e51b815260206004820152600e60248201526d16995c9bc81c9958da5c1a595b9d60921b6044820152606490fd5b60405162461bcd60e51b815260206004820152601060248201526f105b1c9958591e48195e1958dd5d195960821b6044820152606490fd5b346100ae5760603660031901126100ae5760043567ffffffffffffffff81116100ae576106a5903690600401610ab5565b906024359160443567ffffffffffffffff81116100ae576106ca903690600401610ab5565b908415610a5657848310610a1257600383106109cd57610764600192604051602081019061070c816106fe8b8a8c87610b12565b03601f198101835282610b64565b5190209260035493604051602081019146835230604083015260a06060830152600d60c08301526c7570646174655369676e65727360981b60e083015260808201528560a082015260e081526104a761010082610b64565b016003555f5b6004548110156107ac575f516020610f975f395f51905f528101546001600160a01b03165f908152600160208190526040909120805460ff191690550161076a565b50905f5b82811061089d575067ffffffffffffffff82116108895768010000000000000000821161088957816004548160045580821061085b575b50508060045f525f5b83811061083357505061082e837feb4dc7fab86d67670d7a4d7443a38860da1aa053f26529c8f41cc68e5d6a93369460025560405193849384610b12565b0390a1005b600190602061084184610baa565b930192815f516020610f975f395f51905f520155016107f0565b035f5b81811061086d578391506107e7565b5f8482015f516020610f975f395f51905f52015560010161085e565b634e487b7160e01b5f52604160045260245ffd5b6001600160a01b036108b86108b3838686610b86565b610baa565b161561099257806108f8575b6001906001600160a01b036108dd6108b3838787610b86565b165f528160205260405f208260ff19825416179055016107b0565b6109066108b3828585610b86565b5f19820182811161097e576001600160a01b0390610929906108b3908787610b86565b166001600160a01b03909116116108c457606460405162461bcd60e51b815260206004820152602060248201527f5369676e657273206d75737420626520736f7274656420617363656e64696e676044820152fd5b634e487b7160e01b5f52601160045260245ffd5b60405162461bcd60e51b81526020600482015260136024820152722d32b9379030b2323932b9b99039b4b3b732b960691b6044820152606490fd5b60405162461bcd60e51b815260206004820152601760248201527f4e656564206174206c656173742033207369676e6572730000000000000000006044820152606490fd5b606460405162461bcd60e51b815260206004820152602060248201527f4e6f7420656e6f756768207369676e65727320666f72207468726573686f6c646044820152fd5b60405162461bcd60e51b815260206004820152601a60248201527f5468726573686f6c64206d75737420626520706f7369746976650000000000006044820152606490fd5b346100ae575f3660031901126100ae576020906003548152f35b9181601f840112156100ae5782359167ffffffffffffffff83116100ae576020808501948460051b0101116100ae57565b600435906001600160a01b03821682036100ae57565b602435906001600160a01b03821682036100ae57565b6040808252810183905293929160608501905f905b808210610b3957505060209150930152565b909183356001600160a01b03811691908290036100ae57908152602093840193019160010190610b27565b90601f8019910116810190811067ffffffffffffffff82111761088957604052565b9190811015610b965760051b0190565b634e487b7160e01b5f52603260045260245ffd5b356001600160a01b03811681036100ae5790565b15610bc557565b60405162461bcd60e51b815260206004820152600b60248201526a16995c9bc8185b5bdd5b9d60aa1b6044820152606490fd5b67ffffffffffffffff811161088957601f01601f191660200190565b9060025490818410610dc6575f948592835b86881015610d72578760051b840135601e19853603018112156100ae5784019081359167ffffffffffffffff83116100ae57602081019083360382136100ae57610c6f84610bf8565b90610c7d6040519283610b64565b84825260208536920101116100ae575f602085610caf96610ca695838601378301015288610e5b565b90939193610e95565b6001600160a01b038281169116811115610d21575f52600160205260ff60405f20541615610ced57935f19811461097e576001978801970193610c26565b60405162461bcd60e51b815260206004820152600c60248201526b2737ba10309039b4b3b732b960a11b6044820152606490fd5b60405162461bcd60e51b815260206004820152602360248201527f5369676e617475726573206e6f74206f726465726564206f72206475706c696360448201526261746560e81b6064820152608490fd5b509450945050905010610d8157565b60405162461bcd60e51b815260206004820152601d60248201527f496e73756666696369656e742076616c6964207369676e6174757265730000006044820152606490fd5b60405162461bcd60e51b815260206004820152600f60248201526e10995b1bddc81d1a1c995cda1bdb19608a1b6044820152606490fd5b60027f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f005414610e4c5760027f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055565b633ee5aeb560e01b5f5260045ffd5b8151919060418303610e8b57610e849250602082015190606060408401519301515f1a90610f09565b9192909190565b50505f9160029190565b6004811015610ef55780610ea7575050565b60018103610ebe5763f645eedf60e01b5f5260045ffd5b60028103610ed9575063fce698f760e01b5f5260045260245ffd5b600314610ee35750565b6335e2f38360e21b5f5260045260245ffd5b634e487b7160e01b5f52602160045260245ffd5b91907f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08411610f8b579160209360809260ff5f9560405194855216868401526040830152606082015282805260015afa15610f80575f516001600160a01b03811615610f7657905f905f90565b505f906001905f90565b6040513d5f823e3d90fd5b5050505f916003919056fe8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b \ No newline at end of file diff --git a/pkg/blockchain/evm/custody_abi.go b/pkg/blockchain/evm/custody_abi.go index 0ced375..53285f6 100644 --- a/pkg/blockchain/evm/custody_abi.go +++ b/pkg/blockchain/evm/custody_abi.go @@ -31,8 +31,8 @@ var ( // CustodyMetaData contains all meta data concerning the Custody contract. var CustodyMetaData = &bind.MetaData{ - ABI: "[{\"type\":\"constructor\",\"inputs\":[{\"name\":\"initialSigners\",\"type\":\"address[]\",\"internalType\":\"address[]\"},{\"name\":\"initialThreshold\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"nonpayable\"},{\"type\":\"receive\",\"stateMutability\":\"payable\"},{\"type\":\"function\",\"name\":\"deposit\",\"inputs\":[{\"name\":\"account\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"asset\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"amount\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[],\"stateMutability\":\"payable\"},{\"type\":\"function\",\"name\":\"execute\",\"inputs\":[{\"name\":\"to\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"asset\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"amount\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"withdrawalId\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"signatures\",\"type\":\"bytes[]\",\"internalType\":\"bytes[]\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"executed\",\"inputs\":[{\"name\":\"withdrawalId\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"isSigner\",\"inputs\":[{\"name\":\"addr\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"signers\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address[]\",\"internalType\":\"address[]\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"threshold\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"updateSigners\",\"inputs\":[{\"name\":\"newSigners\",\"type\":\"address[]\",\"internalType\":\"address[]\"},{\"name\":\"newThreshold\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"signatures\",\"type\":\"bytes[]\",\"internalType\":\"bytes[]\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"event\",\"name\":\"Deposited\",\"inputs\":[{\"name\":\"depositor\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"account\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"asset\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"},{\"name\":\"amount\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"Executed\",\"inputs\":[{\"name\":\"withdrawalId\",\"type\":\"bytes32\",\"indexed\":true,\"internalType\":\"bytes32\"},{\"name\":\"to\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"asset\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"},{\"name\":\"amount\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"SignersUpdated\",\"inputs\":[{\"name\":\"newSigners\",\"type\":\"address[]\",\"indexed\":false,\"internalType\":\"address[]\"},{\"name\":\"newThreshold\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"}],\"anonymous\":false},{\"type\":\"error\",\"name\":\"ECDSAInvalidSignature\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ECDSAInvalidSignatureLength\",\"inputs\":[{\"name\":\"length\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]},{\"type\":\"error\",\"name\":\"ECDSAInvalidSignatureS\",\"inputs\":[{\"name\":\"s\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}]},{\"type\":\"error\",\"name\":\"ReentrancyGuardReentrantCall\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"SafeERC20FailedOperation\",\"inputs\":[{\"name\":\"token\",\"type\":\"address\",\"internalType\":\"address\"}]}]", - Bin: "0x60806040523461039c5761136c80380380610019816103a0565b928339810160408282031261039c5781516001600160401b03811161039c5782019181601f8401121561039c578251926001600160401b03841161029a578360051b9260206100698186016103a0565b8096815201916020839582010191821161039c57602001915b81831061037c575050506020015160017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055801561033757808351106102f35760038351106102ae575f5b83518110156101fc576001600160a01b036100e882866103c5565b5116156101b75780610126575b6001906001600160a01b0361010a82876103c5565b51165f528160205260405f208260ff19825416179055016100cd565b6001600160a01b0361013882866103c5565b51165f1982018281116101a3576001600160a01b039061015890876103c5565b5116106100f557606460405162461bcd60e51b815260206004820152602060248201527f5369676e657273206d75737420626520736f7274656420617363656e64696e676044820152fd5b634e487b7160e01b5f52601160045260245ffd5b60405162461bcd60e51b815260206004820152601360248201527f5a65726f2061646472657373207369676e6572000000000000000000000000006044820152606490fd5b509151906001600160401b03821161029a5768010000000000000000821161029a576003548260035580831061026f575b5060035f5260205f205f5b8381106102525784600255604051610f7e90816103ee8239f35b82516001600160a01b031681830155602090920191600101610238565b60035f52828060205f20019103905f5b82811061028d57505061022d565b5f8282015560010161027f565b634e487b7160e01b5f52604160045260245ffd5b60405162461bcd60e51b815260206004820152601760248201527f4e656564206174206c656173742033207369676e6572730000000000000000006044820152606490fd5b606460405162461bcd60e51b815260206004820152602060248201527f4e6f7420656e6f756768207369676e65727320666f72207468726573686f6c646044820152fd5b60405162461bcd60e51b815260206004820152601a60248201527f5468726573686f6c64206d75737420626520706f7369746976650000000000006044820152606490fd5b82516001600160a01b038116810361039c57815260209283019201610082565b5f80fd5b6040519190601f01601f191682016001600160401b0381118382101761029a57604052565b80518210156103d95760209160051b010190565b634e487b7160e01b5f52603260045260245ffdfe608080604052600436101561001c575b50361561001a575f80fd5b005b5f3560e01c9081630e2411ac1461066957508063191d0a49146103d557806342cde4e8146103b857806346f0975a146103035780637df73e27146102c65780638340f549146100a75763a9fcfb3314610075575f61000f565b346100a35760203660031901126100a3576004355f525f602052602060ff60405f2054166040519015158152f35b5f80fd5b60603660031901126100a3576100bb610aad565b6100c3610ac3565b604435916100cf610dc4565b6001600160a01b0316918215610292576100ea811515610b85565b6001600160a01b038216806101ad57508034036101735761014a7f4174a9435a04d04d274c76779cad136a41fde6937c56241c09ab9d3c7064a1a9915b604080516001600160a01b03909516855260208501919091523393918291820190565b0390a360017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055005b60405162461bcd60e51b815260206004820152601260248201527108aa89040ecc2d8eaca40dad2e6dac2e8c6d60731b6044820152606490fd5b3461024d576040516323b872dd60e01b5f5233600452306024528260445260205f60648180865af19060015f511482161561022c575b6040525f6060521561021a575061014a7f4174a9435a04d04d274c76779cad136a41fde6937c56241c09ab9d3c7064a1a991610127565b635274afe760e01b5f5260045260245ffd5b90600181151661024457823b15153d151616906101e3565b503d5f823e3d90fd5b60405162461bcd60e51b815260206004820152601b60248201527f4554482073656e742077697468204552433230206465706f73697400000000006044820152606490fd5b60405162461bcd60e51b815260206004820152600c60248201526b16995c9bc81858d8dbdd5b9d60a21b6044820152606490fd5b346100a35760203660031901126100a3576001600160a01b036102e7610aad565b165f526001602052602060ff60405f2054166040519015158152f35b346100a3575f3660031901126100a3576040518060206003549283815201809260035f525f516020610f5e5f395f51905f52905f5b818110610399575050508161034e910382610b2b565b604051918291602083019060208452518091526040830191905f5b818110610377575050500390f35b82516001600160a01b0316845285945060209384019390920191600101610369565b82546001600160a01b0316845260209093019260019283019201610338565b346100a3575f3660031901126100a3576020600254604051908152f35b346100a35760a03660031901126100a3576103ee610aad565b6103f6610ac3565b6064359060443560843567ffffffffffffffff81116100a35761041d903690600401610a7c565b9490610427610dc4565b845f525f60205260ff60405f205416610631576001600160a01b0382169586156105fb576104a49061045a851515610b85565b604051926020840146815230604086015289606086015260018060a01b038816948560808201528760a08201528960c082015260c0815261049c60e082610b2b565b519020610bdb565b845f525f60205260405f20600160ff1982541617905580155f1461057e57505f80808481945af13d15610579573d6104db81610bbf565b906104e96040519283610b2b565b81525f60203d92013e5b1561053e577fe57dd573634102b6cae74aab341f709f6fc3ae2bdc0a35f9a47a85f45b677a21915b604080516001600160a01b0390921682526020820192909252908190810161014a565b60405162461bcd60e51b8152602060048201526013602482015272115512081d1c985b9cd9995c8819985a5b1959606a1b6044820152606490fd5b6104f3565b90505f9291925060405163a9059cbb60e01b5f52856004528360245260205f60448180865af19060015f51148216156105e3575b6040521561021a5750907fe57dd573634102b6cae74aab341f709f6fc3ae2bdc0a35f9a47a85f45b677a219161051b565b90600181151661024457823b15153d151616906105b2565b60405162461bcd60e51b815260206004820152600e60248201526d16995c9bc81c9958da5c1a595b9d60921b6044820152606490fd5b60405162461bcd60e51b815260206004820152601060248201526f105b1c9958591e48195e1958dd5d195960821b6044820152606490fd5b346100a35760603660031901126100a35760043567ffffffffffffffff81116100a35761069a903690600401610a7c565b6024359260443567ffffffffffffffff81116100a3576106be903690600401610a7c565b90918515610a3a57508483106109f657600383106109b15761074c916040516020810190610700816106f28a898b87610ad9565b03601f198101835282610b2b565b519020604051602081019146835230604083015260806060830152600d60a08301526c7570646174655369676e65727360981b60c0830152608082015260c0815261049c60e082610b2b565b5f5b600354811015610790575f516020610f5e5f395f51905f528101546001600160a01b03165f908152600160208190526040909120805460ff191690550161074e565b50905f5b828110610881575067ffffffffffffffff821161086d5768010000000000000000821161086d57816003548160035580821061083f575b50508060035f525f5b838110610817575050610812837feb4dc7fab86d67670d7a4d7443a38860da1aa053f26529c8f41cc68e5d6a93369460025560405193849384610ad9565b0390a1005b600190602061082584610b71565b930192815f516020610f5e5f395f51905f520155016107d4565b035f5b818110610851578391506107cb565b5f8482015f516020610f5e5f395f51905f520155600101610842565b634e487b7160e01b5f52604160045260245ffd5b6001600160a01b0361089c610897838686610b4d565b610b71565b161561097657806108dc575b6001906001600160a01b036108c1610897838787610b4d565b165f528160205260405f208260ff1982541617905501610794565b6108ea610897828585610b4d565b5f198201828111610962576001600160a01b039061090d90610897908787610b4d565b166001600160a01b03909116116108a857606460405162461bcd60e51b815260206004820152602060248201527f5369676e657273206d75737420626520736f7274656420617363656e64696e676044820152fd5b634e487b7160e01b5f52601160045260245ffd5b60405162461bcd60e51b81526020600482015260136024820152722d32b9379030b2323932b9b99039b4b3b732b960691b6044820152606490fd5b60405162461bcd60e51b815260206004820152601760248201527f4e656564206174206c656173742033207369676e6572730000000000000000006044820152606490fd5b606460405162461bcd60e51b815260206004820152602060248201527f4e6f7420656e6f756768207369676e65727320666f72207468726573686f6c646044820152fd5b62461bcd60e51b815260206004820152601a60248201527f5468726573686f6c64206d75737420626520706f7369746976650000000000006044820152606490fd5b9181601f840112156100a35782359167ffffffffffffffff83116100a3576020808501948460051b0101116100a357565b600435906001600160a01b03821682036100a357565b602435906001600160a01b03821682036100a357565b6040808252810183905293929160608501905f905b808210610b0057505060209150930152565b909183356001600160a01b03811691908290036100a357908152602093840193019160010190610aee565b90601f8019910116810190811067ffffffffffffffff82111761086d57604052565b9190811015610b5d5760051b0190565b634e487b7160e01b5f52603260045260245ffd5b356001600160a01b03811681036100a35790565b15610b8c57565b60405162461bcd60e51b815260206004820152600b60248201526a16995c9bc8185b5bdd5b9d60aa1b6044820152606490fd5b67ffffffffffffffff811161086d57601f01601f191660200190565b9060025490818410610d8d575f948592835b86881015610d39578760051b840135601e19853603018112156100a35784019081359167ffffffffffffffff83116100a357602081019083360382136100a357610c3684610bbf565b90610c446040519283610b2b565b84825260208536920101116100a3575f602085610c7696610c6d95838601378301015288610e22565b90939193610e5c565b6001600160a01b038281169116811115610ce8575f52600160205260ff60405f20541615610cb457935f198114610962576001978801970193610bed565b60405162461bcd60e51b815260206004820152600c60248201526b2737ba10309039b4b3b732b960a11b6044820152606490fd5b60405162461bcd60e51b815260206004820152602360248201527f5369676e617475726573206e6f74206f726465726564206f72206475706c696360448201526261746560e81b6064820152608490fd5b509450945050905010610d4857565b60405162461bcd60e51b815260206004820152601d60248201527f496e73756666696369656e742076616c6964207369676e6174757265730000006044820152606490fd5b60405162461bcd60e51b815260206004820152600f60248201526e10995b1bddc81d1a1c995cda1bdb19608a1b6044820152606490fd5b60027f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f005414610e135760027f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055565b633ee5aeb560e01b5f5260045ffd5b8151919060418303610e5257610e4b9250602082015190606060408401519301515f1a90610ed0565b9192909190565b50505f9160029190565b6004811015610ebc5780610e6e575050565b60018103610e855763f645eedf60e01b5f5260045ffd5b60028103610ea0575063fce698f760e01b5f5260045260245ffd5b600314610eaa5750565b6335e2f38360e21b5f5260045260245ffd5b634e487b7160e01b5f52602160045260245ffd5b91907f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08411610f52579160209360809260ff5f9560405194855216868401526040830152606082015282805260015afa15610f47575f516001600160a01b03811615610f3d57905f905f90565b505f906001905f90565b6040513d5f823e3d90fd5b5050505f916003919056fec2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b", + ABI: "[{\"type\":\"constructor\",\"inputs\":[{\"name\":\"initialSigners\",\"type\":\"address[]\",\"internalType\":\"address[]\"},{\"name\":\"initialThreshold\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"nonpayable\"},{\"type\":\"receive\",\"stateMutability\":\"payable\"},{\"type\":\"function\",\"name\":\"deposit\",\"inputs\":[{\"name\":\"account\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"asset\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"amount\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[],\"stateMutability\":\"payable\"},{\"type\":\"function\",\"name\":\"execute\",\"inputs\":[{\"name\":\"to\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"asset\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"amount\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"withdrawalId\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"signatures\",\"type\":\"bytes[]\",\"internalType\":\"bytes[]\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"executed\",\"inputs\":[{\"name\":\"withdrawalId\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"isSigner\",\"inputs\":[{\"name\":\"addr\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"signerNonce\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"signers\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address[]\",\"internalType\":\"address[]\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"threshold\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"updateSigners\",\"inputs\":[{\"name\":\"newSigners\",\"type\":\"address[]\",\"internalType\":\"address[]\"},{\"name\":\"newThreshold\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"signatures\",\"type\":\"bytes[]\",\"internalType\":\"bytes[]\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"event\",\"name\":\"Deposited\",\"inputs\":[{\"name\":\"depositor\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"account\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"asset\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"},{\"name\":\"amount\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"Executed\",\"inputs\":[{\"name\":\"withdrawalId\",\"type\":\"bytes32\",\"indexed\":true,\"internalType\":\"bytes32\"},{\"name\":\"to\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"asset\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"},{\"name\":\"amount\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"SignersUpdated\",\"inputs\":[{\"name\":\"newSigners\",\"type\":\"address[]\",\"indexed\":false,\"internalType\":\"address[]\"},{\"name\":\"newThreshold\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"}],\"anonymous\":false},{\"type\":\"error\",\"name\":\"ECDSAInvalidSignature\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ECDSAInvalidSignatureLength\",\"inputs\":[{\"name\":\"length\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]},{\"type\":\"error\",\"name\":\"ECDSAInvalidSignatureS\",\"inputs\":[{\"name\":\"s\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}]},{\"type\":\"error\",\"name\":\"ReentrancyGuardReentrantCall\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"SafeERC20FailedOperation\",\"inputs\":[{\"name\":\"token\",\"type\":\"address\",\"internalType\":\"address\"}]}]", + Bin: "0x60806040523461039c576113a580380380610019816103a0565b928339810160408282031261039c5781516001600160401b03811161039c5782019181601f8401121561039c578251926001600160401b03841161029a578360051b9260206100698186016103a0565b8096815201916020839582010191821161039c57602001915b81831061037c575050506020015160017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055801561033757808351106102f35760038351106102ae575f5b83518110156101fc576001600160a01b036100e882866103c5565b5116156101b75780610126575b6001906001600160a01b0361010a82876103c5565b51165f528160205260405f208260ff19825416179055016100cd565b6001600160a01b0361013882866103c5565b51165f1982018281116101a3576001600160a01b039061015890876103c5565b5116106100f557606460405162461bcd60e51b815260206004820152602060248201527f5369676e657273206d75737420626520736f7274656420617363656e64696e676044820152fd5b634e487b7160e01b5f52601160045260245ffd5b60405162461bcd60e51b815260206004820152601360248201527f5a65726f2061646472657373207369676e6572000000000000000000000000006044820152606490fd5b509151906001600160401b03821161029a5768010000000000000000821161029a576004548260045580831061026f575b5060045f5260205f205f5b8381106102525784600255604051610fb790816103ee8239f35b82516001600160a01b031681830155602090920191600101610238565b60045f52828060205f20019103905f5b82811061028d57505061022d565b5f8282015560010161027f565b634e487b7160e01b5f52604160045260245ffd5b60405162461bcd60e51b815260206004820152601760248201527f4e656564206174206c656173742033207369676e6572730000000000000000006044820152606490fd5b606460405162461bcd60e51b815260206004820152602060248201527f4e6f7420656e6f756768207369676e65727320666f72207468726573686f6c646044820152fd5b60405162461bcd60e51b815260206004820152601a60248201527f5468726573686f6c64206d75737420626520706f7369746976650000000000006044820152606490fd5b82516001600160a01b038116810361039c57815260209283019201610082565b5f80fd5b6040519190601f01601f191682016001600160401b0381118382101761029a57604052565b80518210156103d95760209160051b010190565b634e487b7160e01b5f52603260045260245ffdfe608080604052600436101561001c575b50361561001a575f80fd5b005b5f3560e01c9081630ce8d62214610a9b575080630e2411ac14610674578063191d0a49146103e057806342cde4e8146103c357806346f0975a1461030e5780637df73e27146102d15780638340f549146100b25763a9fcfb3314610080575f61000f565b346100ae5760203660031901126100ae576004355f525f602052602060ff60405f2054166040519015158152f35b5f80fd5b60603660031901126100ae576100c6610ae6565b6100ce610afc565b604435916100da610dfd565b6001600160a01b031691821561029d576100f5811515610bbe565b6001600160a01b038216806101b8575080340361017e576101557f4174a9435a04d04d274c76779cad136a41fde6937c56241c09ab9d3c7064a1a9915b604080516001600160a01b03909516855260208501919091523393918291820190565b0390a360017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055005b60405162461bcd60e51b815260206004820152601260248201527108aa89040ecc2d8eaca40dad2e6dac2e8c6d60731b6044820152606490fd5b34610258576040516323b872dd60e01b5f5233600452306024528260445260205f60648180865af19060015f5114821615610237575b6040525f6060521561022557506101557f4174a9435a04d04d274c76779cad136a41fde6937c56241c09ab9d3c7064a1a991610132565b635274afe760e01b5f5260045260245ffd5b90600181151661024f57823b15153d151616906101ee565b503d5f823e3d90fd5b60405162461bcd60e51b815260206004820152601b60248201527f4554482073656e742077697468204552433230206465706f73697400000000006044820152606490fd5b60405162461bcd60e51b815260206004820152600c60248201526b16995c9bc81858d8dbdd5b9d60a21b6044820152606490fd5b346100ae5760203660031901126100ae576001600160a01b036102f2610ae6565b165f526001602052602060ff60405f2054166040519015158152f35b346100ae575f3660031901126100ae576040518060206004549283815201809260045f525f516020610f975f395f51905f52905f5b8181106103a45750505081610359910382610b64565b604051918291602083019060208452518091526040830191905f5b818110610382575050500390f35b82516001600160a01b0316845285945060209384019390920191600101610374565b82546001600160a01b0316845260209093019260019283019201610343565b346100ae575f3660031901126100ae576020600254604051908152f35b346100ae5760a03660031901126100ae576103f9610ae6565b610401610afc565b6064359060443560843567ffffffffffffffff81116100ae57610428903690600401610ab5565b9490610432610dfd565b845f525f60205260ff60405f20541661063c576001600160a01b038216958615610606576104af90610465851515610bbe565b604051926020840146815230604086015289606086015260018060a01b038816948560808201528760a08201528960c082015260c081526104a760e082610b64565b519020610c14565b845f525f60205260405f20600160ff1982541617905580155f1461058957505f80808481945af13d15610584573d6104e681610bf8565b906104f46040519283610b64565b81525f60203d92013e5b15610549577fe57dd573634102b6cae74aab341f709f6fc3ae2bdc0a35f9a47a85f45b677a21915b604080516001600160a01b03909216825260208201929092529081908101610155565b60405162461bcd60e51b8152602060048201526013602482015272115512081d1c985b9cd9995c8819985a5b1959606a1b6044820152606490fd5b6104fe565b90505f9291925060405163a9059cbb60e01b5f52856004528360245260205f60448180865af19060015f51148216156105ee575b604052156102255750907fe57dd573634102b6cae74aab341f709f6fc3ae2bdc0a35f9a47a85f45b677a2191610526565b90600181151661024f57823b15153d151616906105bd565b60405162461bcd60e51b815260206004820152600e60248201526d16995c9bc81c9958da5c1a595b9d60921b6044820152606490fd5b60405162461bcd60e51b815260206004820152601060248201526f105b1c9958591e48195e1958dd5d195960821b6044820152606490fd5b346100ae5760603660031901126100ae5760043567ffffffffffffffff81116100ae576106a5903690600401610ab5565b906024359160443567ffffffffffffffff81116100ae576106ca903690600401610ab5565b908415610a5657848310610a1257600383106109cd57610764600192604051602081019061070c816106fe8b8a8c87610b12565b03601f198101835282610b64565b5190209260035493604051602081019146835230604083015260a06060830152600d60c08301526c7570646174655369676e65727360981b60e083015260808201528560a082015260e081526104a761010082610b64565b016003555f5b6004548110156107ac575f516020610f975f395f51905f528101546001600160a01b03165f908152600160208190526040909120805460ff191690550161076a565b50905f5b82811061089d575067ffffffffffffffff82116108895768010000000000000000821161088957816004548160045580821061085b575b50508060045f525f5b83811061083357505061082e837feb4dc7fab86d67670d7a4d7443a38860da1aa053f26529c8f41cc68e5d6a93369460025560405193849384610b12565b0390a1005b600190602061084184610baa565b930192815f516020610f975f395f51905f520155016107f0565b035f5b81811061086d578391506107e7565b5f8482015f516020610f975f395f51905f52015560010161085e565b634e487b7160e01b5f52604160045260245ffd5b6001600160a01b036108b86108b3838686610b86565b610baa565b161561099257806108f8575b6001906001600160a01b036108dd6108b3838787610b86565b165f528160205260405f208260ff19825416179055016107b0565b6109066108b3828585610b86565b5f19820182811161097e576001600160a01b0390610929906108b3908787610b86565b166001600160a01b03909116116108c457606460405162461bcd60e51b815260206004820152602060248201527f5369676e657273206d75737420626520736f7274656420617363656e64696e676044820152fd5b634e487b7160e01b5f52601160045260245ffd5b60405162461bcd60e51b81526020600482015260136024820152722d32b9379030b2323932b9b99039b4b3b732b960691b6044820152606490fd5b60405162461bcd60e51b815260206004820152601760248201527f4e656564206174206c656173742033207369676e6572730000000000000000006044820152606490fd5b606460405162461bcd60e51b815260206004820152602060248201527f4e6f7420656e6f756768207369676e65727320666f72207468726573686f6c646044820152fd5b60405162461bcd60e51b815260206004820152601a60248201527f5468726573686f6c64206d75737420626520706f7369746976650000000000006044820152606490fd5b346100ae575f3660031901126100ae576020906003548152f35b9181601f840112156100ae5782359167ffffffffffffffff83116100ae576020808501948460051b0101116100ae57565b600435906001600160a01b03821682036100ae57565b602435906001600160a01b03821682036100ae57565b6040808252810183905293929160608501905f905b808210610b3957505060209150930152565b909183356001600160a01b03811691908290036100ae57908152602093840193019160010190610b27565b90601f8019910116810190811067ffffffffffffffff82111761088957604052565b9190811015610b965760051b0190565b634e487b7160e01b5f52603260045260245ffd5b356001600160a01b03811681036100ae5790565b15610bc557565b60405162461bcd60e51b815260206004820152600b60248201526a16995c9bc8185b5bdd5b9d60aa1b6044820152606490fd5b67ffffffffffffffff811161088957601f01601f191660200190565b9060025490818410610dc6575f948592835b86881015610d72578760051b840135601e19853603018112156100ae5784019081359167ffffffffffffffff83116100ae57602081019083360382136100ae57610c6f84610bf8565b90610c7d6040519283610b64565b84825260208536920101116100ae575f602085610caf96610ca695838601378301015288610e5b565b90939193610e95565b6001600160a01b038281169116811115610d21575f52600160205260ff60405f20541615610ced57935f19811461097e576001978801970193610c26565b60405162461bcd60e51b815260206004820152600c60248201526b2737ba10309039b4b3b732b960a11b6044820152606490fd5b60405162461bcd60e51b815260206004820152602360248201527f5369676e617475726573206e6f74206f726465726564206f72206475706c696360448201526261746560e81b6064820152608490fd5b509450945050905010610d8157565b60405162461bcd60e51b815260206004820152601d60248201527f496e73756666696369656e742076616c6964207369676e6174757265730000006044820152606490fd5b60405162461bcd60e51b815260206004820152600f60248201526e10995b1bddc81d1a1c995cda1bdb19608a1b6044820152606490fd5b60027f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f005414610e4c5760027f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055565b633ee5aeb560e01b5f5260045ffd5b8151919060418303610e8b57610e849250602082015190606060408401519301515f1a90610f09565b9192909190565b50505f9160029190565b6004811015610ef55780610ea7575050565b60018103610ebe5763f645eedf60e01b5f5260045ffd5b60028103610ed9575063fce698f760e01b5f5260045260245ffd5b600314610ee35750565b6335e2f38360e21b5f5260045260245ffd5b634e487b7160e01b5f52602160045260245ffd5b91907f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08411610f8b579160209360809260ff5f9560405194855216868401526040830152606082015282805260015afa15610f80575f516001600160a01b03811615610f7657905f905f90565b505f906001905f90565b6040513d5f823e3d90fd5b5050505f916003919056fe8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b", } // CustodyABI is the input ABI used to generate the binding from. @@ -264,6 +264,37 @@ func (_Custody *CustodyCallerSession) IsSigner(addr common.Address) (bool, error return _Custody.Contract.IsSigner(&_Custody.CallOpts, addr) } +// SignerNonce is a free data retrieval call binding the contract method 0x0ce8d622. +// +// Solidity: function signerNonce() view returns(uint256) +func (_Custody *CustodyCaller) SignerNonce(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _Custody.contract.Call(opts, &out, "signerNonce") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// SignerNonce is a free data retrieval call binding the contract method 0x0ce8d622. +// +// Solidity: function signerNonce() view returns(uint256) +func (_Custody *CustodySession) SignerNonce() (*big.Int, error) { + return _Custody.Contract.SignerNonce(&_Custody.CallOpts) +} + +// SignerNonce is a free data retrieval call binding the contract method 0x0ce8d622. +// +// Solidity: function signerNonce() view returns(uint256) +func (_Custody *CustodyCallerSession) SignerNonce() (*big.Int, error) { + return _Custody.Contract.SignerNonce(&_Custody.CallOpts) +} + // Signers is a free data retrieval call binding the contract method 0x46f0975a. // // Solidity: function signers() view returns(address[]) diff --git a/pkg/blockchain/evm/quorum.go b/pkg/blockchain/evm/quorum.go new file mode 100644 index 0000000..a98dc83 --- /dev/null +++ b/pkg/blockchain/evm/quorum.go @@ -0,0 +1,86 @@ +package evm + +import ( + "bytes" + "context" + "fmt" + "sort" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +// fetchLiveQuorum reads the vault's current authorized signer set and threshold. +// The outgoing/current quorum is what authorizes both execute and updateSigners, +// so withdrawal and rotation both size and filter against it. +func fetchLiveQuorum(ctx context.Context, custody *Custody) ([]common.Address, int, error) { + signers, err := custody.Signers(&bind.CallOpts{Context: ctx}) + if err != nil { + return nil, 0, fmt.Errorf("read signers: %w", err) + } + thr, err := custody.Threshold(&bind.CallOpts{Context: ctx}) + if err != nil { + return nil, 0, fmt.Errorf("read threshold: %w", err) + } + if !thr.IsInt64() || thr.Int64() <= 0 || thr.Int64() > int64(len(signers)) { + return nil, 0, fmt.Errorf("on-chain threshold %s out of range for %d signers", thr, len(signers)) + } + return signers, int(thr.Int64()), nil +} + +// mergeQuorumSigs filters the collected signatures over digest against the live +// signer set, drops duplicates and unauthorized recoveries, trims to the live +// threshold, orders by signer address (Custody.sol requires ascending, no +// duplicates), and shifts V to {27,28}. It returns the contract-ready signature +// list. Both Custody.execute (withdrawal) and Custody.updateSigners (rotation) +// share this verification shape, differing only in the digest. +func mergeQuorumSigs(digest common.Hash, signatures [][]byte, liveSigners []common.Address, liveThreshold int) ([][]byte, error) { + authorized := make(map[common.Address]struct{}, len(liveSigners)) + for _, a := range liveSigners { + authorized[a] = struct{}{} + } + + type sigAddr struct { + sig []byte + addr common.Address + } + kept := make([]sigAddr, 0, len(signatures)) + seen := make(map[common.Address]struct{}) + for _, s := range signatures { + if len(s) != 65 { + return nil, fmt.Errorf("signature has wrong length %d", len(s)) + } + pub, err := crypto.SigToPub(digest[:], s) + if err != nil { + return nil, fmt.Errorf("recover signer: %w", err) + } + addr := crypto.PubkeyToAddress(*pub) + if _, ok := authorized[addr]; !ok { + continue // not in the live signer set + } + if _, dup := seen[addr]; dup { + continue + } + seen[addr] = struct{}{} + kept = append(kept, sigAddr{sig: s, addr: addr}) + } + if len(kept) < liveThreshold { + return nil, fmt.Errorf("only %d of %d authorized signatures", len(kept), liveThreshold) + } + // Custody.sol's _verifySignatures stops at `threshold` and rejects extras. + kept = kept[:liveThreshold] + // Ascending uint160 order == bytes order over [20]byte. + sort.Slice(kept, func(i, j int) bool { return bytes.Compare(kept[i].addr[:], kept[j].addr[:]) < 0 }) + + sigs := make([][]byte, len(kept)) + for i, k := range kept { + cp := make([]byte, 65) + copy(cp, k.sig) + if cp[64] < 27 { + cp[64] += 27 // shift V {0,1} -> {27,28} at the contract boundary + } + sigs[i] = cp + } + return sigs, nil +} diff --git a/pkg/blockchain/evm/rotation_digest.go b/pkg/blockchain/evm/rotation_digest.go new file mode 100644 index 0000000..c159918 --- /dev/null +++ b/pkg/blockchain/evm/rotation_digest.go @@ -0,0 +1,57 @@ +package evm + +import ( + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/layer-3/clearnet-sdk/pkg/abiutil" +) + +// rotationInnerArgs / rotationOuterArgs mirror Custody.sol's updateSigners +// digest layout. The inner hash commits to the dynamic newSigners array and the +// threshold; the outer commits to the chain id, vault, the "updateSigners" +// domain string, that inner hash, and the signer nonce. +var ( + rotationInnerArgs = abi.Arguments{{Type: abiutil.AddressArr}, {Type: abiutil.Uint256}} + rotationOuterArgs = abi.Arguments{ + {Type: abiutil.Uint256}, + {Type: abiutil.Address}, + {Type: abiutil.String}, + {Type: abiutil.Bytes32}, + {Type: abiutil.Uint256}, + } +) + +// ComputeRotationDigest mirrors Custody.sol's updateSigners digest: +// +// keccak256(abi.encode( +// block.chainid, address(this), "updateSigners", +// keccak256(abi.encode(newSigners, newThreshold)), +// signerNonce)) +// +// newSigners must be in the same (ascending) order the contract stores them; +// signerNonce is the on-chain Custody.signerNonce() — the rotation replay token. +func ComputeRotationDigest(chainID uint64, vault common.Address, newSigners []common.Address, newThreshold, signerNonce *big.Int) [32]byte { + innerEncoded, err := rotationInnerArgs.Pack(newSigners, newThreshold) + if err != nil { + // rotationInnerArgs is fixed; Pack only fails on a type mismatch. + panic(fmt.Errorf("evm: ComputeRotationDigest inner pack: %w", err)) + } + innerHash := crypto.Keccak256Hash(innerEncoded) + + outerEncoded, err := rotationOuterArgs.Pack( + new(big.Int).SetUint64(chainID), + vault, + "updateSigners", + innerHash, + new(big.Int).Set(signerNonce), + ) + if err != nil { + panic(fmt.Errorf("evm: ComputeRotationDigest outer pack: %w", err)) + } + return crypto.Keccak256Hash(outerEncoded) +} diff --git a/pkg/blockchain/evm/rotation_digest_test.go b/pkg/blockchain/evm/rotation_digest_test.go new file mode 100644 index 0000000..5595210 --- /dev/null +++ b/pkg/blockchain/evm/rotation_digest_test.go @@ -0,0 +1,71 @@ +package evm + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" +) + +// TestComputeRotationDigest_GoldenVector pins the Go implementation to the +// Solidity contract. The golden digest was produced by Custody.sol's exact +// keccak256(abi.encode(...)) for these inputs, so a divergence here means the Go +// digest no longer matches what updateSigners will verify on chain. +func TestComputeRotationDigest_GoldenVector(t *testing.T) { + chainID := uint64(31337) + vault := common.HexToAddress("0x0000000000000000000000000000000000AbC123") + signers := []common.Address{ + common.HexToAddress("0x0000000000000000000000000000000000000001"), + common.HexToAddress("0x0000000000000000000000000000000000000002"), + common.HexToAddress("0x0000000000000000000000000000000000000003"), + } + threshold := big.NewInt(2) + nonce := big.NewInt(7) + + want := common.HexToHash("0x96fbf07188f867ef8a2f43996500d7926c85d189dcf8951a03808d864853a6cd") + got := common.Hash(ComputeRotationDigest(chainID, vault, signers, threshold, nonce)) + if got != want { + t.Fatalf("rotation digest mismatch\nwant %s\ngot %s", want.Hex(), got.Hex()) + } +} + +// TestComputeRotationDigest_InputsDifferentiate guards against a bug where the +// digest ignores one of its inputs (e.g. dropping signerNonce) that the golden +// alone could miss — every field must change the digest. +func TestComputeRotationDigest_InputsDifferentiate(t *testing.T) { + base := struct { + chainID uint64 + vault common.Address + signers []common.Address + threshold *big.Int + nonce *big.Int + }{ + chainID: 1, + vault: common.HexToAddress("0x000000000000000000000000000000000000beef"), + signers: []common.Address{ + common.HexToAddress("0x0000000000000000000000000000000000000010"), + common.HexToAddress("0x0000000000000000000000000000000000000020"), + common.HexToAddress("0x0000000000000000000000000000000000000030"), + }, + threshold: big.NewInt(2), + nonce: big.NewInt(0), + } + d0 := ComputeRotationDigest(base.chainID, base.vault, base.signers, base.threshold, base.nonce) + + variants := map[string][32]byte{ + "chainID": ComputeRotationDigest(base.chainID+1, base.vault, base.signers, base.threshold, base.nonce), + "vault": ComputeRotationDigest(base.chainID, common.HexToAddress("0x000000000000000000000000000000000000bee0"), base.signers, base.threshold, base.nonce), + "threshold": ComputeRotationDigest(base.chainID, base.vault, base.signers, big.NewInt(3), base.nonce), + "nonce": ComputeRotationDigest(base.chainID, base.vault, base.signers, base.threshold, big.NewInt(1)), + } + signersChanged := make([]common.Address, len(base.signers)) + copy(signersChanged, base.signers) + signersChanged[0] = common.HexToAddress("0x0000000000000000000000000000000000000099") + variants["signers"] = ComputeRotationDigest(base.chainID, base.vault, signersChanged, base.threshold, base.nonce) + + for name, d := range variants { + if d == d0 { + t.Errorf("digest unchanged when %s changed — input not committed", name) + } + } +} diff --git a/pkg/blockchain/evm/rotation_finalizer.go b/pkg/blockchain/evm/rotation_finalizer.go new file mode 100644 index 0000000..7ed02a9 --- /dev/null +++ b/pkg/blockchain/evm/rotation_finalizer.go @@ -0,0 +1,300 @@ +package evm + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "math/big" + "sort" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + + "github.com/layer-3/clearnet-sdk/pkg/core" + "github.com/layer-3/clearnet-sdk/pkg/sign" +) + +// rotationLookupWindow bounds the eth_getLogs range when resolving the tx hash +// of an already-applied rotation. +const rotationLookupWindow = uint64(50_000) + +// RotationFinalizer rotates the Custody vault's signer set via updateSigners, +// authorized by the current (outgoing) k-of-n quorum. It is the rotation +// analogue of WithdrawalFinalizer and implements core.SignerRotationFinalizer. +// It owns the node's signer (signs the rotation digest and submits) and the +// vault address + chain id supplied at construction. +type RotationFinalizer struct { + client *ethclient.Client + custody *Custody + vaultAddr common.Address + chainID uint64 + signer sign.Signer + signerAddr common.Address + fees FeeConfig +} + +var _ core.SignerRotationFinalizer = (*RotationFinalizer)(nil) + +// NewRotationFinalizer binds the Custody vault at vaultAddr and reads the chain +// id from client. signer is this node's secp256k1 identity. +func NewRotationFinalizer(ctx context.Context, client *ethclient.Client, vaultAddr common.Address, signer sign.Signer, fees FeeConfig) (*RotationFinalizer, error) { + custody, err := NewCustody(vaultAddr, client) + if err != nil { + return nil, fmt.Errorf("load custody: %w", err) + } + chainID, err := client.ChainID(ctx) + if err != nil { + return nil, fmt.Errorf("get chain ID: %w", err) + } + addr, err := sign.EthAddress(signer) + if err != nil { + return nil, err + } + return &RotationFinalizer{ + client: client, + custody: custody, + vaultAddr: vaultAddr, + chainID: chainID.Uint64(), + signer: signer, + signerAddr: addr, + fees: fees, + }, nil +} + +// evmRotPacked is the canonical rotation payload: the new signer set (ascending) +// + threshold, and the signer nonce the digest is bound to. +type evmRotPacked struct { + NewSigners []string `json:"newSigners"` // ascending hex addresses + NewThreshold int `json:"newThreshold"` + SignerNonce string `json:"signerNonce"` // decimal +} + +// Pack reads the live signer nonce and returns the canonical JSON for rotating +// to newSigners / newThreshold (signers sorted ascending, as Custody requires). +func (f *RotationFinalizer) Pack(ctx context.Context, newSigners []string, newThreshold int) ([]byte, error) { + addrs, err := parseSignerAddresses(newSigners) + if err != nil { + return nil, err + } + if newThreshold <= 0 || newThreshold > len(addrs) { + return nil, fmt.Errorf("evm: threshold %d out of range for %d signers", newThreshold, len(addrs)) + } + nonce, err := f.custody.SignerNonce(&bind.CallOpts{Context: ctx}) + if err != nil { + return nil, fmt.Errorf("read signer nonce: %w", err) + } + p := evmRotPacked{NewSigners: addrsToHex(addrs), NewThreshold: newThreshold, SignerNonce: nonce.String()} + return json.Marshal(p) +} + +// Validate re-derives the rotation target from newSigners / newThreshold and +// asserts the packed payload matches, including a re-read of the live nonce to +// reject a packer that bound a stale or wrong signer nonce. +func (f *RotationFinalizer) Validate(ctx context.Context, packed []byte, newSigners []string, newThreshold int) error { + var got evmRotPacked + if err := json.Unmarshal(packed, &got); err != nil { + return fmt.Errorf("decode packed: %w", err) + } + addrs, err := parseSignerAddresses(newSigners) + if err != nil { + return err + } + want := evmRotPacked{NewSigners: addrsToHex(addrs), NewThreshold: newThreshold} + if got.NewThreshold != want.NewThreshold || !equalStrings(got.NewSigners, want.NewSigners) { + return fmt.Errorf("packed rotation does not match request") + } + nonce, err := f.custody.SignerNonce(&bind.CallOpts{Context: ctx}) + if err != nil { + return fmt.Errorf("read signer nonce: %w", err) + } + if got.SignerNonce != nonce.String() { + return fmt.Errorf("packed signer nonce %s != live %s", got.SignerNonce, nonce) + } + return nil +} + +// Sign produces this node's 65-byte ECDSA signature (V ∈ {0,1}) over the +// rotation digest derived from the packed bytes. +func (f *RotationFinalizer) Sign(ctx context.Context, packed []byte) ([]byte, error) { + digest, err := f.digestFromPacked(packed) + if err != nil { + return nil, err + } + return sign.SignEthDigest(ctx, f.signer, digest[:], f.signerAddr) +} + +// Submit merges the collected signatures against the live (outgoing) signer set +// and broadcasts updateSigners. Idempotent: if the rotation already applied it +// returns the prior tx hash without re-submitting. +func (f *RotationFinalizer) Submit(ctx context.Context, packed []byte, signatures [][]byte) (core.TxRef, error) { + var p evmRotPacked + if err := json.Unmarshal(packed, &p); err != nil { + return core.TxRef{}, fmt.Errorf("decode packed: %w", err) + } + addrs, err := parseSignerAddresses(p.NewSigners) + if err != nil { + return core.TxRef{}, err + } + if txHash, done, err := f.VerifyRotation(ctx, p.NewSigners, p.NewThreshold); err != nil { + return core.TxRef{}, err + } else if done { + return core.TxRef{Hash: txHash, Raw: common.Hash(txHash).Hex()}, nil + } + + digest, err := f.digestFromPacked(packed) + if err != nil { + return core.TxRef{}, err + } + liveSigners, liveThreshold, err := fetchLiveQuorum(ctx, f.custody) + if err != nil { + return core.TxRef{}, err + } + sigs, err := mergeQuorumSigs(common.Hash(digest), signatures, liveSigners, liveThreshold) + if err != nil { + return core.TxRef{}, err + } + + opts, _, err := signerTransactOpts(ctx, f.client, f.signer) + if err != nil { + return core.TxRef{}, err + } + if err := applyFees(ctx, f.client, f.fees, opts); err != nil { + return core.TxRef{}, err + } + tx, err := f.custody.UpdateSigners(opts, addrs, big.NewInt(int64(p.NewThreshold)), sigs) + if err != nil { + return core.TxRef{}, fmt.Errorf("updateSigners: %w", err) + } + if err := waitMined(ctx, f.client, tx); err != nil { + return core.TxRef{}, err + } + return core.TxRef{Hash: tx.Hash(), Raw: tx.Hash().Hex()}, nil +} + +// VerifyRotation reports whether the on-chain signer set now equals newSigners +// with the given threshold. When set, it resolves the SignersUpdated event's tx +// hash within the lookback window. +func (f *RotationFinalizer) VerifyRotation(ctx context.Context, newSigners []string, newThreshold int) ([32]byte, bool, error) { + addrs, err := parseSignerAddresses(newSigners) + if err != nil { + return [32]byte{}, false, err + } + live, err := f.custody.Signers(&bind.CallOpts{Context: ctx}) + if err != nil { + return [32]byte{}, false, fmt.Errorf("read signers: %w", err) + } + thr, err := f.custody.Threshold(&bind.CallOpts{Context: ctx}) + if err != nil { + return [32]byte{}, false, fmt.Errorf("read threshold: %w", err) + } + if !thr.IsInt64() || int(thr.Int64()) != newThreshold || !addrSetEqual(live, addrs) { + return [32]byte{}, false, nil + } + return f.lookupRotationTxHash(ctx, addrs), true, nil +} + +// --- helpers --- + +func (f *RotationFinalizer) digestFromPacked(packed []byte) ([32]byte, error) { + var p evmRotPacked + if err := json.Unmarshal(packed, &p); err != nil { + return [32]byte{}, fmt.Errorf("decode packed: %w", err) + } + addrs, err := parseSignerAddresses(p.NewSigners) + if err != nil { + return [32]byte{}, err + } + nonce, ok := new(big.Int).SetString(p.SignerNonce, 10) + if !ok { + return [32]byte{}, fmt.Errorf("bad signer nonce %q", p.SignerNonce) + } + return ComputeRotationDigest(f.chainID, f.vaultAddr, addrs, big.NewInt(int64(p.NewThreshold)), nonce), nil +} + +// lookupRotationTxHash finds the SignersUpdated event matching addrs within the +// lookback window; a zero hash (with done already established) is acceptable. +func (f *RotationFinalizer) lookupRotationTxHash(ctx context.Context, addrs []common.Address) [32]byte { + head, err := f.client.BlockNumber(ctx) + if err != nil { + return [32]byte{} + } + var from uint64 + if head > rotationLookupWindow { + from = head - rotationLookupWindow + } + it, err := f.custody.FilterSignersUpdated(&bind.FilterOpts{Context: ctx, Start: from, End: &head}) + if err != nil { + return [32]byte{} + } + defer it.Close() + var last [32]byte + for it.Next() { + if addrSetEqual(it.Event.NewSigners, addrs) { + last = it.Event.Raw.TxHash // keep the most recent match + } + } + return last +} + +// parseSignerAddresses validates and sorts the incoming hex addresses ascending +// (Custody requires ascending, no duplicates) — the order the digest binds. +func parseSignerAddresses(newSigners []string) ([]common.Address, error) { + if len(newSigners) == 0 { + return nil, fmt.Errorf("evm: empty new signer set") + } + out := make([]common.Address, 0, len(newSigners)) + seen := make(map[common.Address]struct{}, len(newSigners)) + for _, s := range newSigners { + if !common.IsHexAddress(s) { + return nil, fmt.Errorf("evm: signer %q is not a hex address", s) + } + a := common.HexToAddress(s) + if _, dup := seen[a]; dup { + return nil, fmt.Errorf("evm: duplicate signer %s", a) + } + seen[a] = struct{}{} + out = append(out, a) + } + sort.Slice(out, func(i, j int) bool { return bytes.Compare(out[i][:], out[j][:]) < 0 }) + return out, nil +} + +func addrsToHex(addrs []common.Address) []string { + out := make([]string, len(addrs)) + for i, a := range addrs { + out[i] = a.Hex() + } + return out +} + +// addrSetEqual reports whether two address slices hold the same set (order +// independent). Both are deduplicated by construction here. +func addrSetEqual(a, b []common.Address) bool { + if len(a) != len(b) { + return false + } + set := make(map[common.Address]struct{}, len(a)) + for _, x := range a { + set[x] = struct{}{} + } + for _, x := range b { + if _, ok := set[x]; !ok { + return false + } + } + return true +} + +func equalStrings(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} diff --git a/pkg/blockchain/evm/vault_integration_test.go b/pkg/blockchain/evm/vault_integration_test.go index 777168f..6874d91 100644 --- a/pkg/blockchain/evm/vault_integration_test.go +++ b/pkg/blockchain/evm/vault_integration_test.go @@ -158,6 +158,52 @@ func TestIntegrationEVM_DepositAndWithdraw(t *testing.T) { if !executed { t.Fatal("withdrawal not reported executed") } + + // ── Rotation flow (the current quorum authorizes the new signer set) ────── + newAddrs := make([]string, integrationSignerCount) + for i := range newAddrs { + k, err := crypto.GenerateKey() + if err != nil { + t.Fatalf("gen new signer key: %v", err) + } + newAddrs[i] = crypto.PubkeyToAddress(k.PublicKey).Hex() + } + + rotators := make([]*RotationFinalizer, len(signers)) + for i, s := range signers { + r, err := NewRotationFinalizer(ctx, client, custodyAddr, s, FeeConfig{}) + if err != nil { + t.Fatalf("NewRotationFinalizer %d: %v", i, err) + } + rotators[i] = r + } + + rPacked, err := rotators[0].Pack(ctx, newAddrs, integrationThreshold) + if err != nil { + t.Fatalf("rotation Pack: %v", err) + } + rSigs := make([][]byte, 0, len(rotators)) + for i, r := range rotators { + if err := r.Validate(ctx, rPacked, newAddrs, integrationThreshold); err != nil { + t.Fatalf("rotation Validate[%d]: %v", i, err) + } + s, err := r.Sign(ctx, rPacked) + if err != nil { + t.Fatalf("rotation Sign[%d]: %v", i, err) + } + rSigs = append(rSigs, s) + } + rRef, err := rotators[0].Submit(ctx, rPacked, rSigs) + if err != nil { + t.Fatalf("rotation Submit: %v", err) + } + t.Logf("rotation tx %s", rRef.Raw) + + if _, done, err := rotators[0].VerifyRotation(ctx, newAddrs, integrationThreshold); err != nil { + t.Fatalf("VerifyRotation: %v", err) + } else if !done { + t.Fatal("rotation not reported done") + } } // fundETH sends value from key to addr via a raw anvil tx and waits for it. diff --git a/pkg/blockchain/evm/withdrawal_finalizer.go b/pkg/blockchain/evm/withdrawal_finalizer.go index 7aad0ee..13e5e30 100644 --- a/pkg/blockchain/evm/withdrawal_finalizer.go +++ b/pkg/blockchain/evm/withdrawal_finalizer.go @@ -1,13 +1,11 @@ package evm import ( - "bytes" "context" "encoding/hex" "encoding/json" "fmt" "math/big" - "sort" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi/bind" @@ -131,58 +129,11 @@ func (f *WithdrawalFinalizer) merge(ctx context.Context, p evmPacked, signatures if err != nil { return nil, err } - - liveSigners, liveThreshold, err := f.liveQuorum(ctx) + liveSigners, liveThreshold, err := fetchLiveQuorum(ctx, f.custody) if err != nil { return nil, err } - authorized := make(map[common.Address]struct{}, len(liveSigners)) - for _, a := range liveSigners { - authorized[a] = struct{}{} - } - - type sigAddr struct { - sig []byte - addr common.Address - } - kept := make([]sigAddr, 0, len(signatures)) - seen := make(map[common.Address]struct{}) - for _, s := range signatures { - if len(s) != 65 { - return nil, fmt.Errorf("signature has wrong length %d", len(s)) - } - pub, err := crypto.SigToPub(digest[:], s) - if err != nil { - return nil, fmt.Errorf("recover signer: %w", err) - } - addr := crypto.PubkeyToAddress(*pub) - if _, ok := authorized[addr]; !ok { - continue // not in the live signer set - } - if _, dup := seen[addr]; dup { - continue - } - seen[addr] = struct{}{} - kept = append(kept, sigAddr{sig: s, addr: addr}) - } - if len(kept) < liveThreshold { - return nil, fmt.Errorf("only %d of %d authorized signatures", len(kept), liveThreshold) - } - // Custody.sol's _verifySignatures stops at `threshold` and rejects extras. - kept = kept[:liveThreshold] - // Ascending uint160 order == bytes order over [20]byte. - sort.Slice(kept, func(i, j int) bool { return bytes.Compare(kept[i].addr[:], kept[j].addr[:]) < 0 }) - - sigs := make([][]byte, len(kept)) - for i, k := range kept { - cp := make([]byte, 65) - copy(cp, k.sig) - if cp[64] < 27 { - cp[64] += 27 // shift V {0,1} -> {27,28} at the contract boundary - } - sigs[i] = cp - } - return sigs, nil + return mergeQuorumSigs(digest, signatures, liveSigners, liveThreshold) } // Submit merges the collected signatures into a contract-ready quorum and @@ -220,7 +171,7 @@ func (f *WithdrawalFinalizer) Submit(ctx context.Context, packed []byte, signatu if err != nil { return core.TxRef{}, err } - if err := f.applyFees(ctx, opts); err != nil { + if err := applyFees(ctx, f.client, f.fees, opts); err != nil { return core.TxRef{}, err } if err := f.estimateGas(ctx, opts, to, asset, amount, wid, sigs); err != nil { @@ -299,30 +250,17 @@ func (f *WithdrawalFinalizer) digest(p evmPacked) (common.Hash, error) { ), nil } -func (f *WithdrawalFinalizer) liveQuorum(ctx context.Context) ([]common.Address, int, error) { - signers, err := f.custody.Signers(&bind.CallOpts{Context: ctx}) - if err != nil { - return nil, 0, fmt.Errorf("read signers: %w", err) - } - thr, err := f.custody.Threshold(&bind.CallOpts{Context: ctx}) - if err != nil { - return nil, 0, fmt.Errorf("read threshold: %w", err) - } - if !thr.IsInt64() || thr.Int64() <= 0 || thr.Int64() > int64(len(signers)) { - return nil, 0, fmt.Errorf("on-chain threshold %s out of range for %d signers", thr, len(signers)) - } - return signers, int(thr.Int64()), nil -} - -func (f *WithdrawalFinalizer) applyFees(ctx context.Context, opts *bind.TransactOpts) error { - tip := gweiToWei(f.fees.TipGwei) - cap := gweiToWei(f.fees.CapGwei) - head, err := f.client.HeaderByNumber(ctx, nil) +// applyFees sets EIP-1559 (or legacy) gas pricing on opts from fees, refusing to +// exceed the configured cap. Shared by the withdrawal and rotation submits. +func applyFees(ctx context.Context, client *ethclient.Client, fees FeeConfig, opts *bind.TransactOpts) error { + tip := gweiToWei(fees.TipGwei) + cap := gweiToWei(fees.CapGwei) + head, err := client.HeaderByNumber(ctx, nil) if err != nil { return fmt.Errorf("fee head: %w", err) } if head.BaseFee == nil { - price, err := f.client.SuggestGasPrice(ctx) + price, err := client.SuggestGasPrice(ctx) if err != nil { return fmt.Errorf("suggest gas price: %w", err) } diff --git a/pkg/blockchain/sol/digest.go b/pkg/blockchain/sol/digest.go index b0b5812..f2dac6e 100644 --- a/pkg/blockchain/sol/digest.go +++ b/pkg/blockchain/sol/digest.go @@ -7,9 +7,12 @@ import ( "github.com/gagliardetto/solana-go" ) -// withdrawDomain is mirrored byte-for-byte by the Anchor program +// Domain separators, mirrored byte-for-byte by the Anchor program // (programs/custody/src/digest.rs). -const withdrawDomain = "YELLOW_CUSTODY_SOL_WITHDRAW_V1" +const ( + withdrawDomain = "YELLOW_CUSTODY_SOL_WITHDRAW_V1" + rotateDomain = "YELLOW_CUSTODY_SOL_ROTATE_V1" +) // WithdrawDigest computes the 32-byte digest the providers sign for a // withdrawal, matching the program's `withdraw_digest`: @@ -35,3 +38,39 @@ func WithdrawDigest(chainID uint64, programID, vault, to, mint solana.PublicKey, copy(out[:], h.Sum(nil)) return out } + +// SignersCommitment computes sha256(newSigners ‖ newThreshold), matching the +// program's `signers_commitment`. It is the payload the rotation digest binds. +func SignersCommitment(newSigners []solana.PublicKey, newThreshold uint8) [32]byte { + h := sha256.New() + for _, s := range newSigners { + h.Write(s[:]) + } + h.Write([]byte{newThreshold}) + var out [32]byte + copy(out[:], h.Sum(nil)) + return out +} + +// RotateDigest computes the 32-byte digest the providers sign for a signer +// rotation, matching the program's `rotate_digest`: +// +// sha256(ROTATE_DOMAIN ‖ chainID(BE) ‖ programID ‖ config +// ‖ signersCommitment ‖ signerNonce(BE)) +// +// signerNonce is the on-chain Config.SignerNonce — the rotation replay token. +func RotateDigest(chainID uint64, programID, config solana.PublicKey, commitment [32]byte, signerNonce uint64) [32]byte { + var u8 [8]byte + h := sha256.New() + h.Write([]byte(rotateDomain)) + binary.BigEndian.PutUint64(u8[:], chainID) + h.Write(u8[:]) + h.Write(programID[:]) + h.Write(config[:]) + h.Write(commitment[:]) + binary.BigEndian.PutUint64(u8[:], signerNonce) + h.Write(u8[:]) + var out [32]byte + copy(out[:], h.Sum(nil)) + return out +} diff --git a/pkg/blockchain/sol/rotation_finalizer.go b/pkg/blockchain/sol/rotation_finalizer.go new file mode 100644 index 0000000..c8049a8 --- /dev/null +++ b/pkg/blockchain/sol/rotation_finalizer.go @@ -0,0 +1,339 @@ +package sol + +import ( + "bytes" + "context" + "encoding/hex" + "encoding/json" + "fmt" + "sort" + "time" + + "github.com/gagliardetto/solana-go" + computebudget "github.com/gagliardetto/solana-go/programs/compute-budget" + "github.com/gagliardetto/solana-go/rpc" + + "github.com/layer-3/clearnet-sdk/pkg/blockchain/sol/custody" + "github.com/layer-3/clearnet-sdk/pkg/core" + "github.com/layer-3/clearnet-sdk/pkg/sign" +) + +// RotationFinalizer rotates the custody program's signer set via update_signers, +// authorized by the current (outgoing) ed25519 quorum and verified on-chain via +// the Ed25519 precompile — the rotation analogue of WithdrawalFinalizer. The +// node's signer contributes one share; a separate fee-payer pays + submits. It +// implements core.SignerRotationFinalizer. +type RotationFinalizer struct { + client *rpc.Client + programID solana.PublicKey + chainID uint64 + configPDA solana.PublicKey + eventAuth solana.PublicKey + cuLimit uint32 + cuPrice uint64 + commitment rpc.CommitmentType + signer sign.Signer + nodePub solana.PublicKey + feePayer sign.Signer + feePayerPub solana.PublicKey +} + +var _ core.SignerRotationFinalizer = (*RotationFinalizer)(nil) + +// NewRotationFinalizer builds the finalizer. signer is this node's ed25519 +// custody key (one quorum share); feePayer pays for and submits the +// update_signers transaction. cfg reuses the withdrawal Config (chain id, +// compute budget, commitment). +func NewRotationFinalizer(rpcURL string, programID solana.PublicKey, signer, feePayer sign.Signer, cfg Config) (*RotationFinalizer, error) { + nodePub, err := solanaPub(signer) + if err != nil { + return nil, err + } + payerPub, err := solanaPub(feePayer) + if err != nil { + return nil, fmt.Errorf("sol: fee payer: %w", err) + } + limit := cfg.ComputeUnitLimit + if limit == 0 { + limit = defaultComputeUnitLimit + } + commitment := cfg.Commitment + if commitment == "" { + commitment = rpc.CommitmentFinalized + } + return &RotationFinalizer{ + client: rpc.New(rpcURL), + programID: programID, + chainID: cfg.ChainID, + configPDA: ConfigPDA(programID), + eventAuth: eventAuthorityPDA(programID), + cuLimit: limit, + cuPrice: cfg.ComputeUnitPrice, + commitment: commitment, + signer: signer, + nodePub: nodePub, + feePayer: feePayer, + feePayerPub: payerPub, + }, nil +} + +// rotPacked is the canonical rotation payload: the new signer set + threshold +// and the signer nonce the digest is bound to. +type rotPacked struct { + NewSigners []string `json:"newSigners"` // base58, ascending + NewThreshold uint8 `json:"newThreshold"` + SignerNonce uint64 `json:"signerNonce"` +} + +// Pack reads the live signer nonce and returns the canonical JSON for rotating +// to newSigners / newThreshold. +func (f *RotationFinalizer) Pack(ctx context.Context, newSigners []string, newThreshold int) ([]byte, error) { + pubs, err := parseRotationSigners(newSigners) + if err != nil { + return nil, err + } + thr, err := checkThreshold(newThreshold, len(pubs)) + if err != nil { + return nil, err + } + cfg, err := fetchConfig(ctx, f.client, f.programID, f.commitment) + if err != nil { + return nil, err + } + p := rotPacked{NewSigners: make([]string, len(pubs)), NewThreshold: thr, SignerNonce: cfg.SignerNonce} + for i, pk := range pubs { + p.NewSigners[i] = pk.String() + } + return json.Marshal(p) +} + +// Validate re-derives the rotation target from newSigners / newThreshold, +// asserts the packed payload matches it, and re-reads the live nonce to reject a +// packer that bound a stale or wrong signer nonce. +func (f *RotationFinalizer) Validate(ctx context.Context, packed []byte, newSigners []string, newThreshold int) error { + var got rotPacked + if err := json.Unmarshal(packed, &got); err != nil { + return fmt.Errorf("sol: decode packed: %w", err) + } + pubs, err := parseRotationSigners(newSigners) + if err != nil { + return err + } + thr, err := checkThreshold(newThreshold, len(pubs)) + if err != nil { + return err + } + want := make([]string, len(pubs)) + for i, pk := range pubs { + want[i] = pk.String() + } + if got.NewThreshold != thr || !equalStrings(got.NewSigners, want) { + return fmt.Errorf("sol: packed rotation does not match request") + } + cfg, err := fetchConfig(ctx, f.client, f.programID, f.commitment) + if err != nil { + return err + } + if got.SignerNonce != cfg.SignerNonce { + return fmt.Errorf("sol: packed signer nonce %d != live %d", got.SignerNonce, cfg.SignerNonce) + } + return nil +} + +// Sign returns this node's share: nodePubkey(32) ‖ ed25519 signature(64) over +// the rotation digest. +func (f *RotationFinalizer) Sign(ctx context.Context, packed []byte) ([]byte, error) { + digest, err := f.digestFromPacked(packed) + if err != nil { + return nil, err + } + sig, err := f.signer.Sign(ctx, digest[:]) + if err != nil { + return nil, fmt.Errorf("sol: sign rotation digest: %w", err) + } + if len(sig) != 64 { + return nil, fmt.Errorf("sol: ed25519 signature must be 64 bytes, got %d", len(sig)) + } + share := make([]byte, shareLen) + copy(share[:32], f.nodePub[:]) + copy(share[32:], sig) + return share, nil +} + +// Submit filters the collected shares against the live (outgoing) signer set, +// assembles the Ed25519-precompile + update_signers transaction, and broadcasts +// it (fee-payer signed). Idempotent: if the rotation already applied it returns +// without re-submitting. +func (f *RotationFinalizer) Submit(ctx context.Context, packed []byte, shares [][]byte) (core.TxRef, error) { + var p rotPacked + if err := json.Unmarshal(packed, &p); err != nil { + return core.TxRef{}, fmt.Errorf("sol: decode packed: %w", err) + } + newPubs, err := parseRotationSigners(p.NewSigners) + if err != nil { + return core.TxRef{}, err + } + if _, done, _ := f.VerifyRotation(ctx, p.NewSigners, int(p.NewThreshold)); done { + return core.TxRef{}, nil + } + + cfg, err := fetchConfig(ctx, f.client, f.programID, f.commitment) + if err != nil { + return core.TxRef{}, err + } + pubkeys, sigs, err := assembleQuorum(shares, cfg.Signers, int(cfg.Threshold)) + if err != nil { + return core.TxRef{}, err + } + + commitment := SignersCommitment(newPubs, p.NewThreshold) + digest := RotateDigest(f.chainID, f.programID, f.configPDA, commitment, p.SignerNonce) + ed25519Ix, err := BuildEd25519Instruction(pubkeys, sigs, digest[:]) + if err != nil { + return core.TxRef{}, err + } + leading := []solana.Instruction{ + computebudget.NewSetComputeUnitLimitInstruction(f.cuLimit).Build(), + computebudget.NewSetComputeUnitPriceInstruction(f.cuPrice).Build(), + } + sigIxIndex := uint8(len(leading)) + updateIx, err := custody.NewUpdateSignersInstruction( + newPubs, p.NewThreshold, sigIxIndex, + f.configPDA, solana.SysVarInstructionsPubkey, f.eventAuth, f.programID, + ) + if err != nil { + return core.TxRef{}, fmt.Errorf("sol: build update_signers ix: %w", err) + } + instructions := append(leading, ed25519Ix, updateIx) + + sig, err := signAndSend(ctx, f.client, instructions, f.feePayerPub, f.feePayer, f.commitment) + if err != nil { + if _, done, verr := f.VerifyRotation(ctx, p.NewSigners, int(p.NewThreshold)); verr == nil && done { + return core.TxRef{}, nil + } + return core.TxRef{}, err + } + // Block until the Config reflects the new set, so the returned ref + // corresponds to an applied rotation (mirrors the withdrawal finalizer). + if err := f.waitRotated(ctx, p.NewSigners, int(p.NewThreshold)); err != nil { + return core.TxRef{}, err + } + return txRef(sig), nil +} + +func (f *RotationFinalizer) waitRotated(ctx context.Context, newSigners []string, newThreshold int) error { + deadline := time.Now().Add(confirmTimeout) + for { + if _, done, _ := f.VerifyRotation(ctx, newSigners, newThreshold); done { + return nil + } + if time.Now().After(deadline) { + return fmt.Errorf("sol: rotation not applied within %s", confirmTimeout) + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(confirmPollInterval): + } + } +} + +// VerifyRotation reports whether the on-chain Config now holds exactly the +// requested signer set + threshold. Binary — no tx hash is recoverable from the +// Config account, so a zero hash is returned with done=true. +func (f *RotationFinalizer) VerifyRotation(ctx context.Context, newSigners []string, newThreshold int) ([32]byte, bool, error) { + pubs, err := parseRotationSigners(newSigners) + if err != nil { + return [32]byte{}, false, err + } + thr, err := checkThreshold(newThreshold, len(pubs)) + if err != nil { + return [32]byte{}, false, err + } + cfg, err := fetchConfig(ctx, f.client, f.programID, f.commitment) + if err != nil { + return [32]byte{}, false, err + } + if cfg.Threshold != thr || len(cfg.Signers) != len(pubs) { + return [32]byte{}, false, nil + } + for i := range pubs { + if cfg.Signers[i] != pubs[i] { + return [32]byte{}, false, nil + } + } + return [32]byte{}, true, nil +} + +// --- helpers --- + +func (f *RotationFinalizer) digestFromPacked(packed []byte) ([32]byte, error) { + var p rotPacked + if err := json.Unmarshal(packed, &p); err != nil { + return [32]byte{}, fmt.Errorf("sol: decode packed: %w", err) + } + pubs, err := parseRotationSigners(p.NewSigners) + if err != nil { + return [32]byte{}, err + } + commitment := SignersCommitment(pubs, p.NewThreshold) + return RotateDigest(f.chainID, f.programID, f.configPDA, commitment, p.SignerNonce), nil +} + +// parseRotationSigners decodes the incoming signer set (base58 or 32-byte hex) +// into solana pubkeys sorted ascending — the order the program stores and the +// commitment binds. Rejects duplicates. +func parseRotationSigners(newSigners []string) ([]solana.PublicKey, error) { + if len(newSigners) == 0 { + return nil, fmt.Errorf("sol: empty new signer set") + } + out := make([]solana.PublicKey, 0, len(newSigners)) + seen := make(map[solana.PublicKey]struct{}, len(newSigners)) + for _, s := range newSigners { + pk, err := parsePubkey(s) + if err != nil { + return nil, err + } + if _, dup := seen[pk]; dup { + return nil, fmt.Errorf("sol: duplicate signer %s", pk) + } + seen[pk] = struct{}{} + out = append(out, pk) + } + sort.Slice(out, func(i, j int) bool { return bytes.Compare(out[i][:], out[j][:]) < 0 }) + return out, nil +} + +// parsePubkey accepts a 32-byte hex string or a base58 pubkey. +func parsePubkey(s string) (solana.PublicKey, error) { + if b, err := hex.DecodeString(s); err == nil && len(b) == 32 { + return solana.PublicKeyFromBytes(b), nil + } + pk, err := solana.PublicKeyFromBase58(s) + if err != nil { + return solana.PublicKey{}, fmt.Errorf("sol: signer %q is neither 32-byte hex nor base58: %w", s, err) + } + return pk, nil +} + +func checkThreshold(newThreshold, n int) (uint8, error) { + if newThreshold <= 0 || newThreshold > n { + return 0, fmt.Errorf("sol: threshold %d out of range for %d signers", newThreshold, n) + } + if n > 255 { + return 0, fmt.Errorf("sol: too many signers (%d)", n) + } + return uint8(newThreshold), nil +} + +func equalStrings(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} diff --git a/pkg/blockchain/sol/vault_integration_test.go b/pkg/blockchain/sol/vault_integration_test.go index c599008..bc1a92a 100644 --- a/pkg/blockchain/sol/vault_integration_test.go +++ b/pkg/blockchain/sol/vault_integration_test.go @@ -102,6 +102,12 @@ func TestIntegrationSOL_DepositAndWithdraw(t *testing.T) { t.Logf("Config already initialized; reusing") } + // Self-heal: a prior run that died mid-rotation may have left the singleton + // Config on the rotated set; restore the fixed set before deposit/withdraw. + rotCfg := Config{ChainID: solChainID, Commitment: rpc.CommitmentConfirmed} + rotatedSigners := solRotatedSigners(t) + ensureFixedSigners(ctx, t, rpcURL, programID, authority, rotCfg, signerPubs, rotatedSigners) + // ── Deposit flow ────────────────────────────────────────────────────────── dep, err := NewDepositor(rpcURL, programID, depositor, rpc.CommitmentConfirmed) if err != nil { @@ -161,6 +167,116 @@ func TestIntegrationSOL_DepositAndWithdraw(t *testing.T) { } else if !executed { t.Fatal("withdrawal not reported executed") } + + // ── Rotation flow ───────────────────────────────────────────────────────── + // Config is a singleton, so rotate to the (deterministic) rotated set and + // restore the original — both directions exercised. A run that dies between + // the two rotations is healed by ensureFixedSigners on the next run. + newPubs := pubsBase58(rotatedSigners) + origPubs := pubsBase58(signers) + runRotation(ctx, t, rpcURL, programID, authority, rotCfg, signers, newPubs, solThreshold) + t.Logf("rotated to fresh signer set") + runRotation(ctx, t, rpcURL, programID, authority, rotCfg, rotatedSigners, origPubs, solThreshold) + t.Logf("restored original signer set") +} + +// runRotation drives one rotation: `current` are the signers that authorize it, +// `target` the new on-chain set (base58). Fails the test on any step. +func runRotation(ctx context.Context, t *testing.T, rpcURL string, programID solana.PublicKey, feePayer sign.Signer, cfg Config, current []sign.Signer, target []string, threshold int) { + t.Helper() + rotators := make([]*RotationFinalizer, len(current)) + for i, s := range current { + r, err := NewRotationFinalizer(rpcURL, programID, s, feePayer, cfg) + if err != nil { + t.Fatalf("NewRotationFinalizer %d: %v", i, err) + } + rotators[i] = r + } + packed, err := rotators[0].Pack(ctx, target, threshold) + if err != nil { + t.Fatalf("rotation Pack: %v", err) + } + shares := make([][]byte, 0, len(rotators)) + for i, r := range rotators { + if err := r.Validate(ctx, packed, target, threshold); err != nil { + t.Fatalf("rotation Validate[%d]: %v", i, err) + } + s, err := r.Sign(ctx, packed) + if err != nil { + t.Fatalf("rotation Sign[%d]: %v", i, err) + } + shares = append(shares, s) + } + if _, err := rotators[0].Submit(ctx, packed, shares); err != nil { + t.Fatalf("rotation Submit: %v", err) + } + if _, done, err := rotators[0].VerifyRotation(ctx, target, threshold); err != nil { + t.Fatalf("VerifyRotation: %v", err) + } else if !done { + t.Fatal("rotation not reported done") + } +} + +// solRotatedSigners returns the deterministic "rotated" signer set the rotation +// flow rotates to (distinct from the fixed deposit/withdraw set). +func solRotatedSigners(t *testing.T) []sign.Signer { + t.Helper() + out := make([]sign.Signer, solSignerCount) + for i := range out { + out[i] = fixedEd25519(t, fmt.Sprintf("clearnet-sdk/sol-itest/rotated/%d", i)) + } + return out +} + +// pubsBase58 maps signers to their base58 pubkeys. +func pubsBase58(signers []sign.Signer) []string { + out := make([]string, len(signers)) + for i, s := range signers { + p, _ := solanaPub(s) + out[i] = p.String() + } + return out +} + +// ensureFixedSigners restores the singleton Config to the fixed signer set if a +// prior run left it on the rotated set. Both sets are deterministic, so recovery +// is a rotation from the rotated set (whose keys the test holds) back to fixed. +func ensureFixedSigners(ctx context.Context, t *testing.T, rpcURL string, programID solana.PublicKey, feePayer sign.Signer, cfg Config, fixedPubs []solana.PublicKey, rotated []sign.Signer) { + t.Helper() + conf, err := fetchConfig(ctx, rpc.New(rpcURL), programID, cfg.Commitment) + if err != nil { + return // not initialized; Initialize already set the fixed set + } + want := make([]string, len(fixedPubs)) + for i := range fixedPubs { + want[i] = fixedPubs[i].String() + } + if sameSignerSet(conf.Signers, want) { + return + } + if sameSignerSet(conf.Signers, pubsBase58(rotated)) { + runRotation(ctx, t, rpcURL, programID, feePayer, cfg, rotated, want, solThreshold) + t.Logf("self-healed Config back to the fixed signer set") + return + } + t.Fatal("Config in an unrecognized signer state; manual reset needed") +} + +// sameSignerSet reports whether the on-chain signer set equals want (as a set). +func sameSignerSet(got []solana.PublicKey, want []string) bool { + if len(got) != len(want) { + return false + } + set := make(map[string]struct{}, len(want)) + for _, w := range want { + set[w] = struct{}{} + } + for _, g := range got { + if _, ok := set[g.String()]; !ok { + return false + } + } + return true } // --- helpers --- diff --git a/pkg/blockchain/xrpl/rotation_finalizer.go b/pkg/blockchain/xrpl/rotation_finalizer.go new file mode 100644 index 0000000..796aabc --- /dev/null +++ b/pkg/blockchain/xrpl/rotation_finalizer.go @@ -0,0 +1,184 @@ +package xrpl + +import ( + "context" + "encoding/json" + "fmt" + "sort" + + "github.com/Peersyst/xrpl-go/xrpl/queries/account" + "github.com/Peersyst/xrpl-go/xrpl/rpc" + "github.com/Peersyst/xrpl-go/xrpl/transaction" + "github.com/Peersyst/xrpl-go/xrpl/transaction/types" + + "github.com/layer-3/clearnet-sdk/pkg/core" + "github.com/layer-3/clearnet-sdk/pkg/sign" +) + +// RotationFinalizer rotates the vault account's SignerList via a multi-signed +// SignerListSet — the rotation analogue of WithdrawalFinalizer. It owns the +// node's signer; the quorum's blobs are merged off-mesh by the caller. It +// implements core.SignerRotationFinalizer. +// +// Replay defense is the vault account's own Sequence (autofilled into the +// SignerListSet), so there is no separate nonce: a given Sequence applies once. +type RotationFinalizer struct { + client *rpc.Client + vaultAddress string + threshold int // current SignerQuorum — sizes the multi-sign fee + signer sign.Signer + id Identity +} + +var _ core.SignerRotationFinalizer = (*RotationFinalizer)(nil) + +// NewRotationFinalizer builds the XRPL rotation finalizer. threshold is the +// current SignerQuorum (used to size the multi-sign fee and trim the quorum); +// signer is one of the current SignerList members. +func NewRotationFinalizer(rpcURL, vaultAddress string, threshold int, signer sign.Signer) (*RotationFinalizer, error) { + cfg, err := rpc.NewClientConfig(rpcURL) + if err != nil { + return nil, fmt.Errorf("xrpl: create rpc config: %w", err) + } + id, err := DeriveIdentity(signer) + if err != nil { + return nil, err + } + return &RotationFinalizer{ + client: rpc.NewClient(cfg), + vaultAddress: vaultAddress, + threshold: threshold, + signer: signer, + id: id, + }, nil +} + +// Pack builds the autofilled multi-sign SignerListSet installing newSigners / +// newThreshold (each member weight 1), returning its sorted-key JSON. +func (f *RotationFinalizer) Pack(_ context.Context, newSigners []string, newThreshold int) ([]byte, error) { + entries, err := signerEntries(newSigners, newThreshold) + if err != nil { + return nil, err + } + flatTx := transaction.FlatTransaction{ + "TransactionType": "SignerListSet", + "Account": f.vaultAddress, + "SignerQuorum": uint32(newThreshold), + "SignerEntries": entries, + } + if err := f.client.AutofillMultisigned(&flatTx, uint64(f.threshold)); err != nil { + return nil, fmt.Errorf("xrpl: autofill: %w", err) + } + delete(flatTx, "LastLedgerSequence") + return CanonicalJSON(flatTx) +} + +// Validate asserts the packed SignerListSet rotates to exactly the requested set. +func (f *RotationFinalizer) Validate(_ context.Context, packed []byte, newSigners []string, newThreshold int) error { + var flat transaction.FlatTransaction + if err := json.Unmarshal(packed, &flat); err != nil { + return fmt.Errorf("xrpl: decode packed: %w", err) + } + return validateCanonicalRotation(flat, newSigners, newThreshold, f.vaultAddress) +} + +// Sign multi-signs the packed SignerListSet and returns this node's blob. +func (f *RotationFinalizer) Sign(ctx context.Context, packed []byte) ([]byte, error) { + var flat transaction.FlatTransaction + if err := json.Unmarshal(packed, &flat); err != nil { + return nil, fmt.Errorf("xrpl: decode packed: %w", err) + } + blob, err := signMultisig(ctx, f.signer, f.id, flat) + if err != nil { + return nil, err + } + return []byte(blob), nil +} + +// Submit combines the collected multi-sign blobs (trimmed to the current quorum) +// and broadcasts the SignerListSet, returning the tx reference. +func (f *RotationFinalizer) Submit(_ context.Context, _ []byte, signatures [][]byte) (core.TxRef, error) { + merged, err := combineMultisign(signatures, f.threshold) + if err != nil { + return core.TxRef{}, err + } + result, err := f.client.SubmitMultisigned(merged, false) + if err != nil { + return core.TxRef{}, fmt.Errorf("xrpl: submit_multisigned: %w", err) + } + switch result.EngineResult { + case "tesSUCCESS", "terQUEUED": + hash, err := computeTxHash(result.TxBlob) + if err != nil { + return core.TxRef{}, err + } + return core.TxRef{Hash: hash, Raw: hashHex(hash)}, nil + default: + return core.TxRef{}, fmt.Errorf("xrpl: rotation rejected: %s - %s", result.EngineResult, result.EngineResultMessage) + } +} + +// VerifyRotation reads the vault's on-chain SignerList and reports whether it now +// holds exactly newSigners with SignerQuorum == newThreshold. Binary; the tx +// hash is not recoverable from the SignerList object, so a zero hash is returned +// with done=true. +func (f *RotationFinalizer) VerifyRotation(_ context.Context, newSigners []string, newThreshold int) ([32]byte, bool, error) { + resp, err := f.client.GetAccountObjects(&account.ObjectsRequest{ + Account: types.Address(f.vaultAddress), + Type: account.SignerListObject, + }) + if err != nil { + return [32]byte{}, false, fmt.Errorf("xrpl rotation verify: account_objects: %w", err) + } + want := make(map[string]struct{}, len(newSigners)) + for _, s := range newSigners { + want[s] = struct{}{} + } + for _, obj := range resp.AccountObjects { + if asString(obj["LedgerEntryType"]) != "SignerList" { + continue + } + quorum, ok := uint32Field(obj["SignerQuorum"]) + if !ok || int(quorum) != newThreshold { + return [32]byte{}, false, nil + } + got, err := signerEntryAccounts(obj["SignerEntries"]) + if err != nil || len(got) != len(want) { + return [32]byte{}, false, nil + } + for a := range got { + if _, ok := want[a]; !ok { + return [32]byte{}, false, nil + } + } + return [32]byte{}, true, nil + } + return [32]byte{}, false, nil +} + +// signerEntries builds the SignerListSet SignerEntries (each member weight 1), +// sorted by account ascending for a deterministic canonical payload. Validates +// the set is non-empty, duplicate-free, and quorum-consistent. +func signerEntries(newSigners []string, newThreshold int) ([]any, error) { + if len(newSigners) == 0 { + return nil, fmt.Errorf("xrpl: empty new signer set") + } + if newThreshold <= 0 || newThreshold > len(newSigners) { + return nil, fmt.Errorf("xrpl: threshold %d out of range for %d signers", newThreshold, len(newSigners)) + } + seen := make(map[string]struct{}, len(newSigners)) + sorted := make([]string, 0, len(newSigners)) + for _, s := range newSigners { + if _, dup := seen[s]; dup { + return nil, fmt.Errorf("xrpl: duplicate signer %s", s) + } + seen[s] = struct{}{} + sorted = append(sorted, s) + } + sort.Strings(sorted) + entries := make([]any, len(sorted)) + for i, a := range sorted { + entries[i] = map[string]any{"SignerEntry": map[string]any{"Account": a, "SignerWeight": 1}} + } + return entries, nil +} diff --git a/pkg/blockchain/xrpl/vault_integration_test.go b/pkg/blockchain/xrpl/vault_integration_test.go index c026d92..ac2254a 100644 --- a/pkg/blockchain/xrpl/vault_integration_test.go +++ b/pkg/blockchain/xrpl/vault_integration_test.go @@ -137,6 +137,51 @@ func TestIntegrationXRPL_DepositAndWithdraw(t *testing.T) { } else if !executed { t.Fatal("withdrawal not reported executed") } + + // ── Rotation flow (current SignerList members authorize the new list) ───── + newSigners := make([]sign.Signer, xrplSignerCount) + newAddrs := make([]string, xrplSignerCount) + for i := range newSigners { + newSigners[i] = genEd25519(t) // SignerList members need not be funded accounts + newAddrs[i] = mustIdentity(t, newSigners[i]).ClassicAddress + } + + rotators := make([]*RotationFinalizer, len(signers)) + for i, s := range signers { + r, err := NewRotationFinalizer(url, vaultID.ClassicAddress, xrplQuorum, s) + if err != nil { + t.Fatalf("NewRotationFinalizer %d: %v", i, err) + } + rotators[i] = r + } + + rPacked, err := rotators[0].Pack(ctx, newAddrs, xrplQuorum) + if err != nil { + t.Fatalf("rotation Pack: %v", err) + } + rBlobs := make([][]byte, 0, len(rotators)) + for i, r := range rotators { + if err := r.Validate(ctx, rPacked, newAddrs, xrplQuorum); err != nil { + t.Fatalf("rotation Validate[%d]: %v", i, err) + } + b, err := r.Sign(ctx, rPacked) + if err != nil { + t.Fatalf("rotation Sign[%d]: %v", i, err) + } + rBlobs = append(rBlobs, b) + } + rRef, err := rotators[0].Submit(ctx, rPacked, rBlobs) + if err != nil { + t.Fatalf("rotation Submit: %v", err) + } + h.ledgerAccept(ctx, t) + t.Logf("rotation tx %s", rRef.Raw) + + if _, done, err := rotators[0].VerifyRotation(ctx, newAddrs, xrplQuorum); err != nil { + t.Fatalf("VerifyRotation: %v", err) + } else if !done { + t.Fatal("rotation not reported done") + } } type fixedTicket uint32 diff --git a/pkg/blockchain/xrpl/wire.go b/pkg/blockchain/xrpl/wire.go index bac2ac0..12388f8 100644 --- a/pkg/blockchain/xrpl/wire.go +++ b/pkg/blockchain/xrpl/wire.go @@ -17,6 +17,7 @@ import ( addresscodec "github.com/Peersyst/xrpl-go/address-codec" binarycodec "github.com/Peersyst/xrpl-go/binary-codec" xrplcrypto "github.com/Peersyst/xrpl-go/pkg/crypto" + "github.com/Peersyst/xrpl-go/xrpl" "github.com/Peersyst/xrpl-go/xrpl/transaction" "github.com/Peersyst/xrpl-go/xrpl/transaction/types" @@ -218,6 +219,91 @@ func ValidateCanonical(flat transaction.FlatTransaction, op *core.WithdrawalOp, return nil } +// rotationAllowedFields is the allowlist of top-level keys a node accepts on a +// canonical SignerListSet flatTx before signing. +var rotationAllowedFields = map[string]struct{}{ + "TransactionType": {}, "Account": {}, "SignerQuorum": {}, "SignerEntries": {}, + "Sequence": {}, "Fee": {}, "SigningPubKey": {}, "Flags": {}, +} + +// validateCanonicalRotation asserts the canonical SignerListSet flatTx rotates +// the vault to exactly newSigners / newThreshold (each entry weight 1, quorum == +// newThreshold), with a fee within the ceiling and no unexpected fields. +func validateCanonicalRotation(flat transaction.FlatTransaction, newSigners []string, newThreshold int, vault string) error { + if asString(flat["TransactionType"]) != "SignerListSet" { + return fmt.Errorf("xrpl rotation: wrong TransactionType %v", flat["TransactionType"]) + } + if !strings.EqualFold(asString(flat["Account"]), vault) { + return fmt.Errorf("xrpl rotation: Account %v != vault %s", flat["Account"], vault) + } + quorum, ok := uint32Field(flat["SignerQuorum"]) + if !ok || int(quorum) != newThreshold { + return fmt.Errorf("xrpl rotation: SignerQuorum %v != newThreshold %d", flat["SignerQuorum"], newThreshold) + } + gotSet, err := signerEntryAccounts(flat["SignerEntries"]) + if err != nil { + return err + } + wantSet := make(map[string]struct{}, len(newSigners)) + for _, s := range newSigners { + wantSet[s] = struct{}{} + } + if len(gotSet) != len(wantSet) { + return fmt.Errorf("xrpl rotation: %d signer entries != %d new signers", len(gotSet), len(wantSet)) + } + for a := range gotSet { + if _, ok := wantSet[a]; !ok { + return fmt.Errorf("xrpl rotation: unexpected signer entry %s", a) + } + } + fee, ok := uint32Field(flat["Fee"]) + if !ok { + return fmt.Errorf("xrpl rotation: missing or invalid Fee %v", flat["Fee"]) + } + if uint64(fee) > maxAcceptableFeeDrops { + return fmt.Errorf("xrpl rotation: Fee %d drops exceeds ceiling %d", fee, maxAcceptableFeeDrops) + } + for k := range flat { + if _, ok := rotationAllowedFields[k]; !ok { + return fmt.Errorf("xrpl rotation: unexpected field %q", k) + } + } + return nil +} + +// signerEntryAccounts extracts the set of member accounts from a SignerEntries +// value (each element is {"SignerEntry": {"Account": ..., "SignerWeight": ...}}), +// rejecting weights other than 1 and duplicate accounts. +func signerEntryAccounts(raw any) (map[string]struct{}, error) { + entries, ok := raw.([]any) + if !ok { + return nil, fmt.Errorf("xrpl rotation: SignerEntries not an array (%T)", raw) + } + out := make(map[string]struct{}, len(entries)) + for _, e := range entries { + wrapper, ok := e.(map[string]any) + if !ok { + return nil, fmt.Errorf("xrpl rotation: signer entry not an object") + } + inner, ok := wrapper["SignerEntry"].(map[string]any) + if !ok { + return nil, fmt.Errorf("xrpl rotation: missing SignerEntry object") + } + acct := asString(inner["Account"]) + if acct == "" { + return nil, fmt.Errorf("xrpl rotation: signer entry missing Account") + } + if w, ok := uint32Field(inner["SignerWeight"]); !ok || w != 1 { + return nil, fmt.Errorf("xrpl rotation: signer entry %s weight %v != 1", acct, inner["SignerWeight"]) + } + if _, dup := out[acct]; dup { + return nil, fmt.Errorf("xrpl rotation: duplicate signer entry %s", acct) + } + out[acct] = struct{}{} + } + return out, nil +} + func asString(v any) string { if s, ok := v.(string); ok { return s @@ -314,6 +400,26 @@ func CanonicalJSON(flatTx transaction.FlatTransaction) ([]byte, error) { return buf.Bytes(), nil } +// combineMultisign trims the collected multi-sign blobs to exactly `threshold` +// and combines them into one submittable blob. Pack autofilled the multi-sign +// fee for that count (base × (1 + threshold)), so including extras would +// under-pay (telINSUF_FEE_P) and waste fee; any threshold of the SignerList's +// members satisfies the quorum. Shared by the withdrawal and rotation submits. +func combineMultisign(signatures [][]byte, threshold int) (string, error) { + if len(signatures) < threshold { + return "", fmt.Errorf("xrpl: have %d signatures, need %d", len(signatures), threshold) + } + blobs := make([]string, 0, threshold) + for _, s := range signatures[:threshold] { + blobs = append(blobs, string(s)) + } + final, err := xrpl.Multisign(blobs...) + if err != nil { + return "", fmt.Errorf("xrpl: combine signatures: %w", err) + } + return final, nil +} + // hashHex is the uppercase hex of a 32-byte tx hash (XRPL's display form). func hashHex(h [32]byte) string { return strings.ToUpper(hex.EncodeToString(h[:])) } diff --git a/pkg/blockchain/xrpl/withdrawal_finalizer.go b/pkg/blockchain/xrpl/withdrawal_finalizer.go index 9e989c0..ec9b17f 100644 --- a/pkg/blockchain/xrpl/withdrawal_finalizer.go +++ b/pkg/blockchain/xrpl/withdrawal_finalizer.go @@ -7,7 +7,6 @@ import ( "fmt" "strings" - "github.com/Peersyst/xrpl-go/xrpl" "github.com/Peersyst/xrpl-go/xrpl/queries/account" "github.com/Peersyst/xrpl-go/xrpl/rpc" "github.com/Peersyst/xrpl-go/xrpl/transaction" @@ -116,30 +115,10 @@ func (f *WithdrawalFinalizer) Sign(ctx context.Context, packed []byte) ([]byte, return []byte(blob), nil } -// merge combines the collected multi-sign blobs into one submittable blob. -// Exactly `threshold` signatures are included: Pack autofilled the multi-sign -// fee for that count (base × (1 + threshold)), so including extras would -// under-pay (telINSUF_FEE_P) and waste fee. Any threshold of the SignerList's -// members satisfies the quorum. -func (f *WithdrawalFinalizer) merge(signatures [][]byte) (string, error) { - if len(signatures) < f.threshold { - return "", fmt.Errorf("xrpl: have %d signatures, need %d", len(signatures), f.threshold) - } - blobs := make([]string, 0, f.threshold) - for _, s := range signatures[:f.threshold] { - blobs = append(blobs, string(s)) - } - final, err := xrpl.Multisign(blobs...) - if err != nil { - return "", fmt.Errorf("xrpl: combine signatures: %w", err) - } - return final, nil -} - -// Submit combines the collected multi-sign blobs and broadcasts the result, -// returning the tx reference. +// Submit combines the collected multi-sign blobs (trimmed to the quorum) and +// broadcasts the result, returning the tx reference. func (f *WithdrawalFinalizer) Submit(_ context.Context, _ []byte, signatures [][]byte) (core.TxRef, error) { - merged, err := f.merge(signatures) + merged, err := combineMultisign(signatures, f.threshold) if err != nil { return core.TxRef{}, err } diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 2dea867..df22471 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -112,6 +112,43 @@ type VaultWithdrawalFinalizer interface { VerifyExecution(ctx context.Context, withdrawalID [32]byte) (txHash [32]byte, executed bool, err error) } +// SignerRotationFinalizer rotates the vault's authorized signer set. It is the +// same build→sign→merge→submit→verify shape as VaultWithdrawalFinalizer, but the +// signed payload commits to the new signer set + threshold + the chain's local +// replay token (EVM signerNonce, XRPL account sequence, Solana program nonce) +// rather than a withdrawal. Signature collection (mesh) and submitter selection +// stay with the caller; the implementation owns the node's signer and the +// chain-specific authorization supplied at construction. +// +// newSigners is the chain-native encoding of the incoming set — EVM/XRPL +// addresses, BTC 33-byte compressed pubkeys (hex), Solana ed25519 pubkeys (hex) +// — matching custody's RotationRequest. newThreshold is the new k-of-n quorum. +// +// In-place chains (EVM, XRPL, Solana) mutate on-chain signer state at a fixed +// vault address. BTC has no in-place form: its P2WSH vault address is a function +// of the signer set, so rotation is a sweep of every old-vault UTXO into the +// newly-derived vault. The BTC implementation hides that behind the same +// interface via a vault store supplied at construction (it pivots to the new +// vault on confirmation); to callers all four chains rotate identically. +// +// - Pack returns the canonical bytes to be signed for this rotation. +// - Validate re-derives the trust-bound shape and asserts the packed bytes +// match — the Byzantine-packer defense; every node runs it before Sign. +// - Sign produces this node's signature over the packed bytes. +// - Submit merges the collected signatures against the live (outgoing) signer +// set and broadcasts the rotation. Idempotent against an already-applied +// rotation. +// - VerifyRotation reads canonical chain state to answer "is the set now the +// requested one?" — binary (done or not), the signal each node uses to close +// the dual-sign window and drop the outgoing key. +type SignerRotationFinalizer interface { + Pack(ctx context.Context, newSigners []string, newThreshold int) ([]byte, error) + Validate(ctx context.Context, packed []byte, newSigners []string, newThreshold int) error + Sign(ctx context.Context, packed []byte) ([]byte, error) + Submit(ctx context.Context, packed []byte, signatures [][]byte) (TxRef, error) + VerifyRotation(ctx context.Context, newSigners []string, newThreshold int) (txHash [32]byte, done bool, err error) +} + // RegistryReader provides read access to the L1 node registry. // // Naming follows the on-chain `IRegistry` surface: From 6dfe2961b628c724afbb862cc66e219153a56286 Mon Sep 17 00:00:00 2001 From: Anton Filonenko Date: Wed, 17 Jun 2026 12:44:29 +0300 Subject: [PATCH 06/15] feat(p2p): add libp2p wire contract, auth, receipt, gossip helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the pkg/p2p protocol layer over a caller-supplied host.Host: - protocol: stream IDs + topic names pinned to one version, the AuthChallenge/AuthResponse/ReceiptAck wire structs with handwritten CBOR codecs (golden-frozen), and the Registrar interface. - auth: /ynp/auth handshake with operator (secp256k1 + allow-list) and passive (libp2p identity) roles; Server + Client. - receipt: burn/mint submission over a ReceiptHandler seam; Server with per-stream HandleBurnReceipt/HandleMintReceipt + Client. - pubsub: concrete FinalizedWithdrawal publish/subscribe. - gossip: generic publish/subscribe over any cborx payload — the type-parameterized alternative to pubsub. Servers take a host and register handlers; they never construct the Host. pubsub and gossip overlap by design — one is dropped before merge. --- go.mod | 86 +++++++++-- go.sum | 227 +++++++++++++++++++++++------ pkg/p2p/auth/auth.go | 67 +++++++++ pkg/p2p/auth/auth_test.go | 161 ++++++++++++++++++++ pkg/p2p/auth/client.go | 115 +++++++++++++++ pkg/p2p/auth/server.go | 170 +++++++++++++++++++++ pkg/p2p/gossip/follower.go | 156 ++++++++++++++++++++ pkg/p2p/gossip/gossip.go | 44 ++++++ pkg/p2p/gossip/gossip_test.go | 93 ++++++++++++ pkg/p2p/gossip/publisher.go | 80 ++++++++++ pkg/p2p/protocol/protocols.go | 71 +++++++++ pkg/p2p/protocol/protocols_test.go | 56 +++++++ pkg/p2p/protocol/registrar.go | 17 +++ pkg/p2p/protocol/wire.go | 226 ++++++++++++++++++++++++++++ pkg/p2p/protocol/wire_test.go | 133 +++++++++++++++++ pkg/p2p/pubsub/follower.go | 175 ++++++++++++++++++++++ pkg/p2p/pubsub/publisher.go | 95 ++++++++++++ pkg/p2p/pubsub/pubsub_test.go | 92 ++++++++++++ pkg/p2p/receipt/client.go | 93 ++++++++++++ pkg/p2p/receipt/interface.go | 22 +++ pkg/p2p/receipt/receipt_test.go | 152 +++++++++++++++++++ pkg/p2p/receipt/server.go | 124 ++++++++++++++++ 22 files changed, 2397 insertions(+), 58 deletions(-) create mode 100644 pkg/p2p/auth/auth.go create mode 100644 pkg/p2p/auth/auth_test.go create mode 100644 pkg/p2p/auth/client.go create mode 100644 pkg/p2p/auth/server.go create mode 100644 pkg/p2p/gossip/follower.go create mode 100644 pkg/p2p/gossip/gossip.go create mode 100644 pkg/p2p/gossip/gossip_test.go create mode 100644 pkg/p2p/gossip/publisher.go create mode 100644 pkg/p2p/protocol/protocols.go create mode 100644 pkg/p2p/protocol/protocols_test.go create mode 100644 pkg/p2p/protocol/registrar.go create mode 100644 pkg/p2p/protocol/wire.go create mode 100644 pkg/p2p/protocol/wire_test.go create mode 100644 pkg/p2p/pubsub/follower.go create mode 100644 pkg/p2p/pubsub/publisher.go create mode 100644 pkg/p2p/pubsub/pubsub_test.go create mode 100644 pkg/p2p/receipt/client.go create mode 100644 pkg/p2p/receipt/interface.go create mode 100644 pkg/p2p/receipt/receipt_test.go create mode 100644 pkg/p2p/receipt/server.go diff --git a/go.mod b/go.mod index 565024c..3602d92 100644 --- a/go.mod +++ b/go.mod @@ -14,16 +14,21 @@ require ( github.com/gagliardetto/anchor-go v1.0.0 github.com/gagliardetto/binary v0.8.0 github.com/gagliardetto/solana-go v1.21.0 - github.com/ipfs/go-cid v0.0.6 + github.com/ipfs/go-cid v0.5.0 + github.com/libp2p/go-libp2p v0.48.0 + github.com/libp2p/go-libp2p-pubsub v0.16.0 github.com/whyrusleeping/cbor-gen v0.3.1 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 ) require ( + filippo.io/bigmod v0.1.1-0.20260103110540-f8a47775ebe5 // indirect + filippo.io/keygen v0.0.0-20260114151900-8e2790ea4c5b // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect github.com/StackExchange/wmi v1.2.1 // indirect github.com/benbjohnson/clock v1.3.5 // indirect + github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.24.4 // indirect github.com/blendle/zapdriver v1.3.1 // indirect github.com/bsv-blockchain/go-sdk v1.2.9 // indirect @@ -33,42 +38,92 @@ require ( github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect github.com/dave/jennifer v1.7.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect github.com/deckarep/golang-set/v2 v2.6.0 // indirect github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect github.com/decred/dcrd/crypto/ripemd160 v1.0.2 // indirect + github.com/dunglas/httpsfv v1.1.0 // indirect github.com/ethereum/c-kzg-4844/v2 v2.1.6 // indirect github.com/fatih/color v1.18.0 // indirect + github.com/flynn/noise v1.1.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/gagliardetto/treeout v0.1.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/goccy/go-json v0.10.6 // indirect + github.com/gogo/protobuf v1.3.2 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/holiman/uint256 v1.3.2 // indirect + github.com/huin/goupnp v1.3.0 // indirect + github.com/jackpal/go-nat-pmp v1.0.2 // indirect + github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kcalvinalvin/anet v0.0.0-20251112173137-d8ddc1f6dbee // indirect github.com/klauspost/compress v1.18.0 // indirect - github.com/klauspost/cpuid/v2 v2.0.9 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/koron/go-ssdp v0.0.6 // indirect + github.com/libp2p/go-buffer-pool v0.1.0 // indirect + github.com/libp2p/go-flow-metrics v0.2.0 // indirect + github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect + github.com/libp2p/go-msgio v0.3.0 // indirect + github.com/libp2p/go-netroute v0.4.0 // indirect + github.com/libp2p/go-reuseport v0.4.0 // indirect + github.com/libp2p/go-yamux/v5 v5.0.1 // indirect github.com/logrusorgru/aurora v2.0.3+incompatible // indirect + github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 // indirect - github.com/minio/sha256-simd v1.0.0 // indirect + github.com/miekg/dns v1.1.66 // indirect + github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect + github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect + github.com/minio/sha256-simd v1.0.1 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect github.com/mr-tron/base58 v1.2.0 // indirect - github.com/multiformats/go-base32 v0.0.3 // indirect - github.com/multiformats/go-base36 v0.1.0 // indirect - github.com/multiformats/go-multibase v0.0.3 // indirect - github.com/multiformats/go-multihash v0.0.13 // indirect - github.com/multiformats/go-varint v0.0.5 // indirect + github.com/multiformats/go-base32 v0.1.0 // indirect + github.com/multiformats/go-base36 v0.2.0 // indirect + github.com/multiformats/go-multiaddr v0.16.0 // indirect + github.com/multiformats/go-multiaddr-dns v0.4.1 // indirect + github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect + github.com/multiformats/go-multibase v0.2.0 // indirect + github.com/multiformats/go-multicodec v0.9.1 // indirect + github.com/multiformats/go-multihash v0.2.3 // indirect + github.com/multiformats/go-multistream v0.6.1 // indirect + github.com/multiformats/go-varint v0.0.7 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/oasisprotocol/curve25519-voi v0.0.0-20251114093237-2ab5a27a1729 // indirect + github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect + github.com/pion/datachannel v1.5.10 // indirect + github.com/pion/dtls/v3 v3.1.2 // indirect + github.com/pion/ice/v4 v4.0.10 // indirect + github.com/pion/interceptor v0.1.40 // indirect + github.com/pion/logging v0.2.4 // indirect + github.com/pion/mdns/v2 v2.0.7 // indirect + github.com/pion/randutil v0.1.0 // indirect + github.com/pion/rtcp v1.2.16 // indirect + github.com/pion/rtp v1.8.19 // indirect + github.com/pion/sctp v1.8.39 // indirect + github.com/pion/sdp/v3 v3.0.18 // indirect + github.com/pion/srtp/v3 v3.0.6 // indirect + github.com/pion/stun/v3 v3.1.1 // indirect + github.com/pion/transport/v3 v3.0.7 // indirect + github.com/pion/transport/v4 v4.0.1 // indirect + github.com/pion/turn/v4 v4.0.2 // indirect + github.com/pion/webrtc/v4 v4.1.2 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.22.0 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.64.0 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.59.0 // indirect + github.com/quic-go/webtransport-go v0.10.0 // indirect github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/streamingfast/logging v0.0.0-20250404134358-92b15d2fbd2e // indirect @@ -81,19 +136,30 @@ require ( github.com/tklauser/numcpus v0.6.1 // indirect github.com/tyler-smith/go-bip39 v1.1.0 // indirect github.com/ugorji/go/codec v1.2.11 // indirect + github.com/wlynxg/anet v0.0.5 // indirect github.com/x448/float16 v0.8.4 // indirect go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.40.0 // indirect go.opentelemetry.io/otel/metric v1.40.0 // indirect go.opentelemetry.io/otel/trace v1.40.0 // indirect + go.uber.org/dig v1.19.0 // indirect + go.uber.org/fx v1.24.0 // indirect + go.uber.org/mock v0.5.2 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/ratelimit v0.3.1 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/crypto v0.48.0 // indirect + golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 // indirect golang.org/x/mod v0.33.0 // indirect + golang.org/x/net v0.50.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.41.0 // indirect + golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4 // indirect golang.org/x/term v0.40.0 // indirect - golang.org/x/time v0.11.0 // indirect + golang.org/x/text v0.34.0 // indirect + golang.org/x/time v0.12.0 // indirect + golang.org/x/tools v0.42.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + lukechampine.com/blake3 v1.4.1 // indirect ) diff --git a/go.sum b/go.sum index 50c736a..3f474c1 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +filippo.io/bigmod v0.1.1-0.20260103110540-f8a47775ebe5 h1:JA0fFr+kxpqTdxR9LOBiTWpGNchqmkcsgmdeJZRclZ0= +filippo.io/bigmod v0.1.1-0.20260103110540-f8a47775ebe5/go.mod h1:OjOXDNlClLblvXdwgFFOQFJEocLhhtai8vGLy0JCZlI= +filippo.io/keygen v0.0.0-20260114151900-8e2790ea4c5b h1:REI1FbdW71yO56Are4XAxD+OS/e+BQsB3gE4mZRQEXY= +filippo.io/keygen v0.0.0-20260114151900-8e2790ea4c5b/go.mod h1:9nnw1SlYHYuPSo/3wjQzNjSbeHlq2NsKo5iEtfJPWP0= github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0= github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= @@ -33,6 +37,8 @@ github.com/btcsuite/btcd/chaincfg/chainhash v1.2.0 h1:yMIg99+4aBvqfl/HzJRKfxTX9r github.com/btcsuite/btcd/chaincfg/chainhash v1.2.0/go.mod h1:Y72Ren9gfhlEvnwnT78BGcSNO2UMphTKLn9AorF+5rg= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/canonical/go-sp800.90a-drbg v0.0.0-20210314144037-6eeb1040d6c3 h1:oe6fCvaEpkhyW3qAicT0TnGtyht/UrgvOwMcEgLb7Aw= +github.com/canonical/go-sp800.90a-drbg v0.0.0-20210314144037-6eeb1040d6c3/go.mod h1:qdP0gaj0QtgX2RUZhnlVrceJ+Qln8aSlDyJwelLLFeM= github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -60,6 +66,8 @@ github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU= +github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U= github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= @@ -72,6 +80,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3h github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/deepmap/oapi-codegen v1.6.0 h1:w/d1ntwh91XI0b/8ja7+u5SvA4IFfM0UNNLmiDR1gg0= github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M= +github.com/dunglas/httpsfv v1.1.0 h1:Jw76nAyKWKZKFrpMMcL76y35tOpYHqQPzHQiwDvpe54= +github.com/dunglas/httpsfv v1.1.0/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg= github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= github.com/ethereum/c-kzg-4844/v2 v2.1.6 h1:xQymkKCT5E2Jiaoqf3v4wsNgjZLY0lRSkZn27fRjSls= @@ -84,6 +94,8 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= +github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg= +github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ= @@ -118,8 +130,6 @@ github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXe github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -139,6 +149,8 @@ github.com/graph-gophers/graphql-go v1.3.0 h1:Eb9x/q6MFpCLz7jBCiP/WTxjSDrYLR1QY4 github.com/graph-gophers/graphql-go v1.3.0/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db h1:IZUYC/xb3giYwBLMnr8d0TGTzPKFGNTCGgGLoyeX330= github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db/go.mod h1:xTEYN9KCHxuYHs+NmrmzFcnvHMzLLNiGFafCb1n3Mfg= github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= @@ -153,20 +165,25 @@ github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c h1:qSH github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU= github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= -github.com/ipfs/go-cid v0.0.6 h1:go0y+GcDOGeJIV01FeBsta4FHngoA4Wz7KMeLkXAhMs= -github.com/ipfs/go-cid v0.0.6/go.mod h1:6Ux9z5e+HpkQdckYoX1PG/6xqKspzlEIR5SDmgqgC/I= +github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg= +github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk= github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk= +github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kcalvinalvin/anet v0.0.0-20251112173137-d8ddc1f6dbee h1:FPP9HDkBbPyniu+u7FHZg+kKFX1WW0gxOGteJ0h3AJk= github.com/kcalvinalvin/anet v0.0.0-20251112173137-d8ddc1f6dbee/go.mod h1:N6sz6HwJAenJ6d+/xmSl0ikfV05ZrVGmjt1ryy/WOtE= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= -github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/koron/go-ssdp v0.0.6 h1:Jb0h04599eq/CY7rB5YEqPS83HmRfHP2azkxMN2rFtU= +github.com/koron/go-ssdp v0.0.6/go.mod h1:0R9LfRJGek1zWTjN3JUNlm5INCDYGpRDfAptnct63fI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -179,21 +196,50 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= +github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= +github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= +github.com/libp2p/go-flow-metrics v0.2.0 h1:EIZzjmeOE6c8Dav0sNv35vhZxATIXWZg6j/C08XmmDw= +github.com/libp2p/go-flow-metrics v0.2.0/go.mod h1:st3qqfu8+pMfh+9Mzqb2GTiwrAGjIPszEjZmtksN8Jc= +github.com/libp2p/go-libp2p v0.48.0 h1:h2BrLAgrj7X8bEN05K7qmrjpNHYA+6tnsGRdprjTnvo= +github.com/libp2p/go-libp2p v0.48.0/go.mod h1:Q1fBZNdmC2Hf82husCTfkKJVfHm2we5zk+NWmOGEmWk= +github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= +github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= +github.com/libp2p/go-libp2p-pubsub v0.16.0 h1:j7G2C8kJwkcAQqYR7Wmq3d75d3Sgw/N0Hhiv0dVx7OY= +github.com/libp2p/go-libp2p-pubsub v0.16.0/go.mod h1:lr4oE8bFgQaifRcoc2uWhWWiK6tPdOEKpUuR408GFN4= +github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= +github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg= +github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0= +github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM= +github.com/libp2p/go-netroute v0.4.0 h1:sZZx9hyANYUx9PZyqcgE/E1GUG3iEtTZHUEvdtXT7/Q= +github.com/libp2p/go-netroute v0.4.0/go.mod h1:Nkd5ShYgSMS5MUKy/MU2T57xFoOKvvLR92Lic48LEyA= +github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s= +github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU= +github.com/libp2p/go-yamux/v5 v5.0.1 h1:f0WoX/bEF2E8SbE4c/k1Mo+/9z0O4oC/hWEA+nfYRSg= +github.com/libp2p/go-yamux/v5 v5.0.1/go.mod h1:en+3cdX51U0ZslwRdRLrvQsdayFt3TSUKvBGErzpWbU= github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/marcopolo/simnet v0.0.4 h1:50Kx4hS9kFGSRIbrt9xUS3NJX33EyPqHVmpXvaKLqrY= +github.com/marcopolo/simnet v0.0.4/go.mod h1:tfQF1u2DmaB6WHODMtQaLtClEf3a296CKQLq5gAsIS0= +github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk= +github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= -github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 h1:lYpkrQH5ajf0OXOcUbGjvZxxijuBwbbmlSxLiuofa+g= +github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE= +github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE= +github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8= +github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c/go.mod h1:0SQS9kMwD2VsyFEB++InYyBJroV/FRmBgcydeSUcJms= +github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc= +github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b/go.mod h1:lxPUiZwKoFL8DUUmalo2yJJUCxbPKtm8OKfqr2/FTNU= +github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc h1:PTfri+PuQmWDqERdnNMiD9ZejrlswWrCpBEZgWOiTrc= +github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc/go.mod h1:cGKTAVKx4SxOuR/czcZ/E2RSJ3sfHs8FpHhQ5CWMf9s= github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ= github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= -github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= -github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= +github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -207,51 +253,103 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 h1:mPMvm6X6tf4w8y7j9YIt6V9jfWhL6QlbEc7CCmeQlWk= github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1/go.mod h1:ye2e/VUEtE2BHE+G/QcKkcLQVAEJoYRFj5VUOQatCRE= -github.com/mr-tron/base58 v1.1.0/go.mod h1:xcD2VGqlgYjBdcBLw+TuYLr8afG+Hj8g2eTVqeSzSU8= -github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= -github.com/multiformats/go-base32 v0.0.3 h1:tw5+NhuwaOjJCC5Pp82QuXbrmLzWg7uxlMFp8Nq/kkI= -github.com/multiformats/go-base32 v0.0.3/go.mod h1:pLiuGC8y0QR3Ue4Zug5UzK9LjgbkL8NSQj0zQ5Nz/AA= -github.com/multiformats/go-base36 v0.1.0 h1:JR6TyF7JjGd3m6FbLU2cOxhC0Li8z8dLNGQ89tUg4F4= -github.com/multiformats/go-base36 v0.1.0/go.mod h1:kFGE83c6s80PklsHO9sRn2NCoffoRdUUOENyW/Vv6sM= -github.com/multiformats/go-multibase v0.0.3 h1:l/B6bJDQjvQ5G52jw4QGSYeOTZoAwIO77RblWplfIqk= -github.com/multiformats/go-multibase v0.0.3/go.mod h1:5+1R4eQrT3PkYZ24C3W2Ue2tPwIdYQD509ZjSb5y9Oc= -github.com/multiformats/go-multihash v0.0.13 h1:06x+mk/zj1FoMsgNejLpy6QTvJqlSt/BhLEy87zidlc= -github.com/multiformats/go-multihash v0.0.13/go.mod h1:VdAWLKTwram9oKAatUcLxBNUjdtcVwxObEQBtRfuyjc= -github.com/multiformats/go-varint v0.0.5 h1:XVZwSo04Cs3j/jS0uAEPpT3JY6DzMcVLLoWOSnCxOjg= -github.com/multiformats/go-varint v0.0.5/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= +github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= +github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= +github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= +github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= +github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo= +github.com/multiformats/go-multiaddr v0.16.0 h1:oGWEVKioVQcdIOBlYM8BH1rZDWOGJSqr9/BKl6zQ4qc= +github.com/multiformats/go-multiaddr v0.16.0/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0= +github.com/multiformats/go-multiaddr-dns v0.4.1 h1:whi/uCLbDS3mSEUMb1MsoT4uzUeZB0N32yzufqS0i5M= +github.com/multiformats/go-multiaddr-dns v0.4.1/go.mod h1:7hfthtB4E4pQwirrz+J0CcDUfbWzTqEzVyYKKIKpgkc= +github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= +github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= +github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= +github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= +github.com/multiformats/go-multicodec v0.9.1 h1:x/Fuxr7ZuR4jJV4Os5g444F7xC4XmyUaT/FWtE+9Zjo= +github.com/multiformats/go-multicodec v0.9.1/go.mod h1:LLWNMtyV5ithSBUo3vFIMaeDy+h3EbkMTek1m+Fybbo= +github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew= +github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= +github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= +github.com/multiformats/go-multistream v0.6.1 h1:4aoX5v6T+yWmc2raBHsTvzmFhOI8WVOer28DeBBEYdQ= +github.com/multiformats/go-multistream v0.6.1/go.mod h1:ksQf6kqHAb6zIsyw7Zm+gAuVo57Qbq84E27YlYqavqw= +github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= +github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/oasisprotocol/curve25519-voi v0.0.0-20251114093237-2ab5a27a1729 h1:yfQ2sO9WJXUAIUR+g7NUkxJSKCAFJcR5sUDu+ZmjTZI= github.com/oasisprotocol/curve25519-voi v0.0.0-20251114093237-2ab5a27a1729/go.mod h1:hVoHR2EVESiICEMbg137etN/Lx+lSrHPTD39Z/uE+2s= -github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQmzR3rNLYGGz4g/UgFcjb28p/viDM= github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= -github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= -github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= -github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= -github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= +github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= +github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= +github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= +github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc= +github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo= +github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4= +github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= +github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4= +github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic= +github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= +github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= +github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM= +github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA= +github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= +github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo= +github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo= +github.com/pion/rtp v1.8.19 h1:jhdO/3XhL/aKm/wARFVmvTfq0lC/CvN1xwYKmduly3c= +github.com/pion/rtp v1.8.19/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk= +github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE= +github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE= +github.com/pion/sdp/v3 v3.0.18 h1:l0bAXazKHpepazVdp+tPYnrsy9dfh7ZbT8DxesH5ZnI= +github.com/pion/sdp/v3 v3.0.18/go.mod h1:ZREGo6A9ZygQ9XkqAj5xYCQtQpif0i6Pa81HOiAdqQ8= +github.com/pion/srtp/v3 v3.0.6 h1:E2gyj1f5X10sB/qILUGIkL4C2CqK269Xq167PbGCc/4= +github.com/pion/srtp/v3 v3.0.6/go.mod h1:BxvziG3v/armJHAaJ87euvkhHqWe9I7iiOy50K2QkhY= +github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= -github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c= -github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= -github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM= -github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= +github.com/pion/stun/v3 v3.1.1 h1:CkQxveJ4xGQjulGSROXbXq94TAWu8gIX2dT+ePhUkqw= +github.com/pion/stun/v3 v3.1.1/go.mod h1:qC1DfmcCTQjl9PBaMa5wSn3x9IPmKxSdcCsxBcDBndM= +github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q= +github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= +github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= +github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= +github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o= +github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM= +github.com/pion/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps= +github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs= +github.com/pion/webrtc/v4 v4.1.2 h1:mpuUo/EJ1zMNKGE79fAdYNFZBX790KE7kQQpLMjjR54= +github.com/pion/webrtc/v4 v4.1.2/go.mod h1:xsCXiNAmMEjIdFxAYU0MbB3RwRieJsegSB2JZsGN+8U= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.15.0 h1:5fCgGYogn0hFdhyhLbw7hEsWxufKtY9klyvdNfFlFhM= -github.com/prometheus/client_golang v1.15.0/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= -github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= -github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= -github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= -github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= -github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= -github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4= +github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/quic-go/webtransport-go v0.10.0 h1:LqXXPOXuETY5Xe8ITdGisBzTYmUOy5eSj+9n4hLTjHI= +github.com/quic-go/webtransport-go v0.10.0/go.mod h1:LeGIXr5BQKE3UsynwVBeQrU1TPrbh73MGoC6jd+V7ow= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= @@ -301,10 +399,14 @@ github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= github.com/whyrusleeping/cbor-gen v0.3.1 h1:82ioxmhEYut7LBVGhGq8xoRkXPLElVuh5mV67AFfdv0= github.com/whyrusleeping/cbor-gen v0.3.1/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= +github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= +github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= @@ -322,9 +424,15 @@ go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4= +go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg= +go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -338,30 +446,42 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= -golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= -golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 h1:bsqhLWFR6G6xiQcb+JoGqdKdRU6WzPWmK8E0jxTjzo4= +golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= -golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -373,6 +493,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4 h1:bTLqdHv7xrGlFbvf5/TXNxy/iUwwdkjhqQTJDjW7aj0= +golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4/go.mod h1:g5NllXBEermZrmR51cJDQxmJUHUOfRAaNyWBM+R+548= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= @@ -381,20 +503,27 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= -golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= -golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -404,3 +533,5 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= +lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= diff --git a/pkg/p2p/auth/auth.go b/pkg/p2p/auth/auth.go new file mode 100644 index 0000000..78feb6e --- /dev/null +++ b/pkg/p2p/auth/auth.go @@ -0,0 +1,67 @@ +// Package auth implements the /ynp/auth/1.0.0 libp2p handshake that lets a node +// prove who it is before a peer accepts its restricted streams (notably the +// burn/mint receipt protocols). +// +// The handshake is a single round trip: the Server sends a random nonce +// (AuthChallenge); the Client signs it and returns an AuthResponse. Two roles +// share the wire: +// +// - Operator — the client signs keccak256(nonce) with a secp256k1 operator +// key and sets Address. The server ecrecovers, matches the recovered +// address to the claimed one, and checks it against an operator allow-list. +// Proves the peer holds a key on the signer set; the gate for receipt +// streams. +// +// - Passive — the client leaves Address empty and signs a domain-separated +// nonce with its libp2p identity key. The server verifies against the +// connection's remote public key. Proves the peer controls the libp2p +// identity it dials from — no operator key, no allow-list — for +// gateway-safe / read paths. +// +// The package owns no host.Host: Server.Register installs a handler on a +// caller-built host (Server satisfies protocol.Registrar), and Client dials +// over one. +package auth + +// maxAuthEnvelope caps a single challenge/response envelope read. +const maxAuthEnvelope = 64 << 10 // 64 KiB + +// passiveAuthDomain separates passive-auth signatures from any other use of the +// libp2p identity key. It is part of the wire contract — both sides must agree +// on these exact bytes. +var passiveAuthDomain = []byte("ynp/libp2p-passive-auth/v1") + +// Role is the outcome class of a successful handshake. +type Role uint8 + +const ( + // RolePassive means the peer proved control of its libp2p identity only. + RolePassive Role = 1 + // RoleOperator means the peer proved control of an allow-listed operator key. + RoleOperator Role = 2 +) + +func (r Role) String() string { + switch r { + case RolePassive: + return "passive" + case RoleOperator: + return "operator" + default: + return "unknown" + } +} + +// Result holds the outcome of a successful authentication. +type Result struct { + // Address is the recovered 0x-prefixed operator address; empty for passive. + Address string + Role Role +} + +func passiveAuthMessage(nonce [32]byte) []byte { + msg := make([]byte, 0, len(passiveAuthDomain)+len(nonce)) + msg = append(msg, passiveAuthDomain...) + msg = append(msg, nonce[:]...) + return msg +} diff --git a/pkg/p2p/auth/auth_test.go b/pkg/p2p/auth/auth_test.go new file mode 100644 index 0000000..e06f7d0 --- /dev/null +++ b/pkg/p2p/auth/auth_test.go @@ -0,0 +1,161 @@ +package auth + +import ( + "context" + "crypto/ecdsa" + "strings" + "testing" + "time" + + "github.com/ethereum/go-ethereum/crypto" + libp2p "github.com/libp2p/go-libp2p" + libp2pcrypto "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/core/peer" + + "github.com/layer-3/clearnet-sdk/pkg/sign" +) + +func TestAuth_Operator(t *testing.T) { + srv, cli := newPair(t, nil) + + signer := sign.NewKeySignerFromECDSA(mustKey(t)) + addr, err := sign.EthAddress(signer) + if err != nil { + t.Fatal(err) + } + + results := make(chan Result, 1) + NewServer(AllowList{strings.ToLower(addr.Hex()): {}}, func(_ network.Conn, r Result) { + results <- r + }, nil).Register(srv) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := NewClient(ClientOpts{Signer: signer}).Authenticate(ctx, cli, srv.ID()); err != nil { + t.Fatalf("Authenticate: %v", err) + } + + select { + case r := <-results: + if r.Role != RoleOperator { + t.Errorf("role = %v, want operator", r.Role) + } + if !strings.EqualFold(r.Address, addr.Hex()) { + t.Errorf("address = %s, want %s", r.Address, addr.Hex()) + } + case <-time.After(3 * time.Second): + t.Fatal("server never reported a successful auth") + } +} + +func TestAuth_OperatorRejectedByAllowList(t *testing.T) { + srv, cli := newPair(t, nil) + + signer := sign.NewKeySignerFromECDSA(mustKey(t)) + // Allow-list holds a different address, so this operator is rejected. + other := sign.NewKeySignerFromECDSA(mustKey(t)) + otherAddr, _ := sign.EthAddress(other) + + results := make(chan Result, 1) + NewServer(AllowList{strings.ToLower(otherAddr.Hex()): {}}, func(_ network.Conn, r Result) { + results <- r + }, nil).Register(srv) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + // The client side returns nil — it does not wait for the server's verdict; + // rejection is observed by the server never invoking onAuth. + if err := NewClient(ClientOpts{Signer: signer}).Authenticate(ctx, cli, srv.ID()); err != nil { + t.Fatalf("Authenticate: %v", err) + } + select { + case r := <-results: + t.Fatalf("expected rejection, but server authenticated %+v", r) + case <-time.After(time.Second): + // expected: no callback + } +} + +func TestAuth_Passive(t *testing.T) { + priv, _, err := libp2pcrypto.GenerateKeyPair(libp2pcrypto.Ed25519, -1) + if err != nil { + t.Fatal(err) + } + srv := newHost(t, nil) + cli := newHost(t, priv) // client identity must match the passive signing key + connect(t, cli, srv) + + results := make(chan Result, 1) + // Empty allow-list: operator gate disabled, but passive does not consult it. + NewServer(nil, func(_ network.Conn, r Result) { results <- r }, nil).Register(srv) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := NewClient(ClientOpts{IdentityKey: priv}).Authenticate(ctx, cli, srv.ID()); err != nil { + t.Fatalf("Authenticate: %v", err) + } + select { + case r := <-results: + if r.Role != RolePassive { + t.Errorf("role = %v, want passive", r.Role) + } + if r.Address != "" { + t.Errorf("address = %q, want empty for passive", r.Address) + } + case <-time.After(3 * time.Second): + t.Fatal("server never reported a successful passive auth") + } +} + +func TestParseAllowListCSV(t *testing.T) { + a := ParseAllowListCSV("0x1111111111111111111111111111111111111111, garbage ,0x2222222222222222222222222222222222222222") + if len(a) != 2 { + t.Fatalf("len = %d, want 2 (malformed dropped)", len(a)) + } +} + +// ── helpers ─────────────────────────────────────────────────────────────── + +func mustKey(t *testing.T) *ecdsa.PrivateKey { + t.Helper() + k, err := crypto.GenerateKey() + if err != nil { + t.Fatal(err) + } + return k +} + +// newPair builds a server + client host (client identity optional) and connects +// them. +func newPair(t *testing.T, cliKey libp2pcrypto.PrivKey) (srv, cli host.Host) { + t.Helper() + srv = newHost(t, nil) + cli = newHost(t, cliKey) + connect(t, cli, srv) + return srv, cli +} + +func newHost(t *testing.T, identity libp2pcrypto.PrivKey) host.Host { + t.Helper() + opts := []libp2p.Option{libp2p.ListenAddrStrings("/ip4/127.0.0.1/tcp/0")} + if identity != nil { + opts = append(opts, libp2p.Identity(identity)) + } + h, err := libp2p.New(opts...) + if err != nil { + t.Fatalf("libp2p new: %v", err) + } + t.Cleanup(func() { _ = h.Close() }) + return h +} + +func connect(t *testing.T, from, to host.Host) { + t.Helper() + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := from.Connect(ctx, peer.AddrInfo{ID: to.ID(), Addrs: to.Addrs()}); err != nil { + t.Fatalf("connect: %v", err) + } +} diff --git a/pkg/p2p/auth/client.go b/pkg/p2p/auth/client.go new file mode 100644 index 0000000..e90ff1e --- /dev/null +++ b/pkg/p2p/auth/client.go @@ -0,0 +1,115 @@ +package auth + +import ( + "context" + "fmt" + "io" + "time" + + ethcrypto "github.com/ethereum/go-ethereum/crypto" + libp2pcrypto "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/core/protocol" + + "github.com/layer-3/clearnet-sdk/pkg/cborx" + p2pproto "github.com/layer-3/clearnet-sdk/pkg/p2p/protocol" + "github.com/layer-3/clearnet-sdk/pkg/sign" +) + +// clientTimeout bounds a single Authenticate attempt. +const clientTimeout = 10 * time.Second + +// ClientOpts selects the role for Authenticate. Exactly one of Signer +// (operator) or IdentityKey (passive) must be set; Signer takes precedence if +// both are present. +type ClientOpts struct { + // Signer is a secp256k1 operator key. When set, runs operator auth. + Signer sign.Signer + // IdentityKey is the libp2p identity private key. Used for passive auth + // when Signer is nil. + IdentityKey libp2pcrypto.PrivKey +} + +// Client runs the dialing side of the auth handshake. It mirrors Server: +// Server.HandleAuth verifies what Client.Authenticate produces. +type Client struct { + opts ClientOpts +} + +// NewClient returns a Client configured by opts. +func NewClient(opts ClientOpts) *Client { + return &Client{opts: opts} +} + +// Authenticate runs the handshake against pid over h. With opts.Signer it +// performs operator auth; otherwise passive auth with opts.IdentityKey. The +// remote's HandleAuth marks us authenticated on success. +func (c *Client) Authenticate(ctx context.Context, h host.Host, pid peer.ID) error { + if pid == "" { + return fmt.Errorf("auth: empty peer id") + } + if c.opts.Signer == nil && c.opts.IdentityKey == nil { + return fmt.Errorf("auth: ClientOpts needs a Signer or an IdentityKey") + } + + ctx, cancel := context.WithTimeout(ctx, clientTimeout) + defer cancel() + + s, err := h.NewStream(ctx, pid, protocol.ID(p2pproto.ProtocolAuth)) + if err != nil { + return fmt.Errorf("open auth stream to %s: %w", pid.ShortString(), err) + } + defer s.Close() + + if c.opts.Signer != nil { + return c.requestOperator(ctx, s) + } + return c.requestPassive(s) +} + +func (c *Client) requestOperator(ctx context.Context, s network.Stream) error { + addr, err := sign.EthAddress(c.opts.Signer) + if err != nil { + return fmt.Errorf("operator address: %w", err) + } + return respond(s, func(nonce [32]byte) (p2pproto.AuthResponse, error) { + nonceHash := ethcrypto.Keccak256(nonce[:]) + sig, err := sign.SignEthDigest(ctx, c.opts.Signer, nonceHash, addr) + if err != nil { + return p2pproto.AuthResponse{}, fmt.Errorf("sign nonce: %w", err) + } + return p2pproto.AuthResponse{Signature: sig, Address: addr.Hex()}, nil + }) +} + +func (c *Client) requestPassive(s network.Stream) error { + return respond(s, func(nonce [32]byte) (p2pproto.AuthResponse, error) { + sig, err := c.opts.IdentityKey.Sign(passiveAuthMessage(nonce)) + if err != nil { + return p2pproto.AuthResponse{}, fmt.Errorf("sign passive nonce: %w", err) + } + return p2pproto.AuthResponse{Signature: sig}, nil + }) +} + +// respond reads the challenge, builds the response, and writes it back. +func respond(s network.Stream, build func([32]byte) (p2pproto.AuthResponse, error)) error { + var challenge p2pproto.AuthChallenge + var v cborx.Version + if err := cborx.ReadEnvelope(io.LimitReader(s, maxAuthEnvelope), &v, &challenge); err != nil { + return fmt.Errorf("read challenge: %w", err) + } + if v != cborx.V1 { + return fmt.Errorf("unsupported auth wire version: 0x%02x", byte(v)) + } + resp, err := build(challenge.Nonce) + if err != nil { + return err + } + if err := cborx.WriteEnvelope(s, cborx.V1, &resp); err != nil { + return fmt.Errorf("send response: %w", err) + } + return nil +} diff --git a/pkg/p2p/auth/server.go b/pkg/p2p/auth/server.go new file mode 100644 index 0000000..043cc01 --- /dev/null +++ b/pkg/p2p/auth/server.go @@ -0,0 +1,170 @@ +package auth + +import ( + "crypto/rand" + "fmt" + "io" + "log/slog" + "strings" + + "github.com/ethereum/go-ethereum/common" + ethcrypto "github.com/ethereum/go-ethereum/crypto" + libp2pcrypto "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/core/protocol" + + "github.com/layer-3/clearnet-sdk/pkg/cborx" + p2pproto "github.com/layer-3/clearnet-sdk/pkg/p2p/protocol" +) + +// AllowList is the operator allow-list: a set of lowercased hex addresses. A +// nil or empty AllowList disables the operator gate — any well-formed operator +// signature is accepted (useful for early devnet). Passive auth is never gated +// by the allow-list. +type AllowList map[string]struct{} + +// ParseAllowListCSV turns "0xabc..,0xdef.." into an AllowList. Empty input +// yields an empty set (gate disabled). Malformed entries are skipped. +func ParseAllowListCSV(s string) AllowList { + out := AllowList{} + for _, raw := range strings.Split(s, ",") { + raw = strings.TrimSpace(raw) + if raw == "" || !common.IsHexAddress(raw) { + continue + } + out[strings.ToLower(common.HexToAddress(raw).Hex())] = struct{}{} + } + return out +} + +func (a AllowList) normalize() AllowList { + out := make(AllowList, len(a)) + for k := range a { + if !common.IsHexAddress(k) { + continue + } + out[strings.ToLower(common.HexToAddress(k).Hex())] = struct{}{} + } + return out +} + +func (a AllowList) permits(addr common.Address) bool { + if len(a) == 0 { + return true + } + _, ok := a[strings.ToLower(addr.Hex())] + return ok +} + +// Server handles inbound auth streams: it issues a nonce, verifies the response +// as operator or passive, and reports each success via an onAuth callback. +type Server struct { + allow AllowList // normalized + onAuth func(network.Conn, Result) + logger *slog.Logger +} + +var _ p2pproto.Registrar = (*Server)(nil) + +// NewServer returns a Server gated by allow (nil/empty disables the operator +// gate). onAuth, if non-nil, is invoked with the connection and Result after +// each successful handshake — the caller binds whatever "authenticated" means +// in its world (e.g. marking the connection so receipt streams pass their +// gate). The connection is passed (not just the peer ID) so the caller can key +// auth state per-connection, matching libp2p's connection lifetime. +func NewServer(allow AllowList, onAuth func(network.Conn, Result), logger *slog.Logger) *Server { + if logger == nil { + logger = slog.Default() + } + log := logger.With("component", "p2p-auth-server", "protocol", p2pproto.ProtocolAuth) + clean := allow.normalize() + if len(allow) > 0 && len(clean) == 0 { + log.Error("auth: every allow-list entry is malformed; gate is EMPTY and bypassed", "raw_entries", len(allow)) + } else if len(allow) > len(clean) { + log.Warn("auth: dropped malformed allow-list entries", "raw", len(allow), "accepted", len(clean)) + } + return &Server{allow: clean, onAuth: onAuth, logger: log} +} + +// Register installs the auth stream handler on h. +func (s *Server) Register(h host.Host) { + h.SetStreamHandler(protocol.ID(p2pproto.ProtocolAuth), s.HandleAuth) +} + +// HandleAuth is the stream handler for /ynp/auth/1.0.0. +func (s *Server) HandleAuth(stream network.Stream) { + defer stream.Close() + conn := stream.Conn() + res, err := s.verify(stream, conn.RemotePublicKey()) + if err != nil { + s.logger.Debug("auth handshake failed", "peer", conn.RemotePeer().ShortString(), "error", err) + return + } + s.logger.Info("peer authenticated", "peer", conn.RemotePeer().ShortString(), "address", res.Address, "role", res.Role.String()) + if s.onAuth != nil { + s.onAuth(conn, res) + } +} + +// verify runs the server side of one handshake on stream: generate a nonce, +// send the challenge, read the response, verify it as operator or passive. +// remotePub is the connection's remote libp2p key, used for passive auth. +func (s *Server) verify(stream network.Stream, remotePub libp2pcrypto.PubKey) (Result, error) { + var challenge p2pproto.AuthChallenge + if _, err := rand.Read(challenge.Nonce[:]); err != nil { + return Result{}, fmt.Errorf("generate nonce: %w", err) + } + if err := cborx.WriteEnvelope(stream, cborx.V1, &challenge); err != nil { + return Result{}, fmt.Errorf("send challenge: %w", err) + } + + var resp p2pproto.AuthResponse + var v cborx.Version + if err := cborx.ReadEnvelope(io.LimitReader(stream, maxAuthEnvelope), &v, &resp); err != nil { + return Result{}, fmt.Errorf("read response: %w", err) + } + if v != cborx.V1 { + return Result{}, fmt.Errorf("unsupported auth wire version: 0x%02x", byte(v)) + } + + // Empty Address ⇒ passive auth proven against the libp2p identity key. + if resp.Address == "" { + if err := verifyPassive(remotePub, challenge.Nonce, resp.Signature); err != nil { + return Result{}, err + } + return Result{Role: RolePassive}, nil + } + + // Operator auth: recover the signer of keccak256(nonce) and gate it. + if len(resp.Signature) != 65 { + return Result{}, fmt.Errorf("operator signature must be 65 bytes, got %d", len(resp.Signature)) + } + nonceHash := ethcrypto.Keccak256(challenge.Nonce[:]) + pub, err := ethcrypto.SigToPub(nonceHash, resp.Signature) + if err != nil { + return Result{}, fmt.Errorf("ecrecover: %w", err) + } + recovered := ethcrypto.PubkeyToAddress(*pub) + if !strings.EqualFold(resp.Address, recovered.Hex()) { + return Result{}, fmt.Errorf("address mismatch: recovered %s, claimed %s", recovered.Hex(), resp.Address) + } + if !s.allow.permits(recovered) { + return Result{}, fmt.Errorf("operator %s not in allow-list", recovered.Hex()) + } + return Result{Address: recovered.Hex(), Role: RoleOperator}, nil +} + +func verifyPassive(pub libp2pcrypto.PubKey, nonce [32]byte, sig []byte) error { + if pub == nil { + return fmt.Errorf("missing remote libp2p public key") + } + ok, err := pub.Verify(passiveAuthMessage(nonce), sig) + if err != nil { + return fmt.Errorf("verify passive auth: %w", err) + } + if !ok { + return fmt.Errorf("passive auth signature mismatch") + } + return nil +} diff --git a/pkg/p2p/gossip/follower.go b/pkg/p2p/gossip/follower.go new file mode 100644 index 0000000..f16f46b --- /dev/null +++ b/pkg/p2p/gossip/follower.go @@ -0,0 +1,156 @@ +package gossip + +import ( + "bytes" + "context" + "fmt" + "log/slog" + "sync" + + pubsub "github.com/libp2p/go-libp2p-pubsub" + "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/core/peer" + + "github.com/layer-3/clearnet-sdk/pkg/cborx" +) + +// Metrics captures Follower counters for ops dashboards. +type Metrics struct { + mu sync.Mutex + Delivered uint64 // payloads handed to the handler + DecodeErrors uint64 // envelope/CBOR decode failures + OversizeDrops uint64 // dropped for exceeding the size cap +} + +// Snapshot returns a copy of the current counters. Safe for concurrent use. +func (m *Metrics) Snapshot() Metrics { + m.mu.Lock() + defer m.mu.Unlock() + return Metrics{Delivered: m.Delivered, DecodeErrors: m.DecodeErrors, OversizeDrops: m.OversizeDrops} +} + +func (m *Metrics) inc(field *uint64) { + m.mu.Lock() + *field++ + m.mu.Unlock() +} + +// Follower subscribes to a topic on a caller-owned host and forwards each +// decoded value of type T to a Handler. +type Follower[T any, M message[T]] struct { + host host.Host + sub *pubsub.Subscription + topic *pubsub.Topic + name string + logger *slog.Logger + metrics *Metrics + + handlerMu sync.RWMutex + handler Handler[T] +} + +// NewFollower joins and subscribes to topic on h. A size-validator is attached +// before joining, so oversized messages are rejected by GossipSub and never +// reach the consume loop. Call Run to start consuming; register a handler with +// SetHandler before (or shortly after) Run. The caller owns h and its +// connectivity. +func NewFollower[T any, M message[T]](ctx context.Context, h host.Host, topic string, logger *slog.Logger) (*Follower[T, M], error) { + if logger == nil { + logger = slog.Default() + } + ps, err := pubsub.NewGossipSub(ctx, h) + if err != nil { + return nil, fmt.Errorf("gossipsub: %w", err) + } + f := &Follower[T, M]{ + host: h, + name: topic, + logger: logger.With("component", "p2p-gossip-follower", "topic", topic), + metrics: &Metrics{}, + } + if err := ps.RegisterTopicValidator(topic, f.validateSize); err != nil { + return nil, fmt.Errorf("register validator %s: %w", topic, err) + } + t, err := ps.Join(topic) + if err != nil { + return nil, fmt.Errorf("join %s: %w", topic, err) + } + sub, err := t.Subscribe() + if err != nil { + _ = t.Close() + return nil, fmt.Errorf("subscribe %s: %w", topic, err) + } + f.topic = t + f.sub = sub + f.logger.Info("gossip follower started", "peer_id", h.ID().String()) + return f, nil +} + +// SetHandler installs the handler invoked for each decoded value. Safe to call +// concurrently with Run. +func (f *Follower[T, M]) SetHandler(h Handler[T]) { + f.handlerMu.Lock() + f.handler = h + f.handlerMu.Unlock() +} + +// Metrics returns the Follower's counters handle. +func (f *Follower[T, M]) Metrics() *Metrics { return f.metrics } + +// PeerID returns the host's libp2p peer ID. +func (f *Follower[T, M]) PeerID() peer.ID { return f.host.ID() } + +// Run consumes the subscription until ctx is cancelled or the subscription +// closes. It blocks; run it in a goroutine. +func (f *Follower[T, M]) Run(ctx context.Context) { + for { + msg, err := f.sub.Next(ctx) + if err != nil { + if ctx.Err() == nil { + f.logger.Debug("subscription closed", "error", err) + } + return + } + if msg.ReceivedFrom == f.host.ID() { + continue + } + f.handle(msg) + } +} + +// Close cancels the subscription and leaves the topic. It does not close the +// host — the caller owns that. +func (f *Follower[T, M]) Close() error { + f.sub.Cancel() + return f.topic.Close() +} + +func (f *Follower[T, M]) validateSize(_ context.Context, from peer.ID, msg *pubsub.Message) bool { + if len(msg.Data) > maxMessageBytes { + f.metrics.inc(&f.metrics.OversizeDrops) + f.logger.Warn("dropping oversize message", "from", from.ShortString(), "bytes", len(msg.Data)) + return false + } + return true +} + +func (f *Follower[T, M]) handle(msg *pubsub.Message) { + var v T + var ver cborx.Version + // M(&v) converts *T to the constrained pointer type, which satisfies + // cborx's CBORUnmarshaler — decode in place into v. + if err := cborx.ReadEnvelopeStrict(bytes.NewReader(msg.Data), &ver, M(&v)); err != nil { + f.metrics.inc(&f.metrics.DecodeErrors) + f.logger.Warn("decode payload failed", "error", err) + return + } + f.handlerMu.RLock() + h := f.handler + f.handlerMu.RUnlock() + if h == nil { + f.logger.Warn("no handler registered; dropping payload") + return + } + h(&v) + f.metrics.inc(&f.metrics.Delivered) +} diff --git a/pkg/p2p/gossip/gossip.go b/pkg/p2p/gossip/gossip.go new file mode 100644 index 0000000..9abc7ac --- /dev/null +++ b/pkg/p2p/gossip/gossip.go @@ -0,0 +1,44 @@ +// Package gossip is a generic GossipSub publish/subscribe toolset over any +// cborx-envelope payload. It is the type-parameterized alternative to the +// concrete pkg/p2p/pubsub (which bakes in *core.FinalizedWithdrawal): a +// Publisher[T]/Follower[T] works for any T whose cbor-gen codec lives on *T. +// +// Both ship side by side so the design can be chosen at review — generic reuse +// here vs. a concrete, no-generics surface in pubsub. Like pubsub, gossip is +// host-taking: the caller builds and owns the libp2p host and its connectivity; +// gossip owns only the GossipSub instance, topic, and subscription. +// +// Usage (constraint type inference fills the pointer type, so callers name only +// the value type): +// +// pub, _ := gossip.NewPublisher[core.FinalizedWithdrawal](ctx, h, topic, nil) +// _ = pub.Publish(ctx, &core.FinalizedWithdrawal{...}) +// +// fol, _ := gossip.NewFollower[core.FinalizedWithdrawal](ctx, h, topic, nil) +// fol.SetHandler(func(fw *core.FinalizedWithdrawal) { ... }) +// go fol.Run(ctx) +package gossip + +import ( + cbg "github.com/whyrusleeping/cbor-gen" +) + +// maxMessageBytes caps the raw size of an inbound message before any CBOR +// allocation, keeping a malicious publisher from forcing large allocations per +// message. Matches the concrete pubsub follower's cap. +const maxMessageBytes = 128 * 1024 + +// message constrains *T to the cborx codec interfaces. cbor-gen emits +// Marshal/Unmarshal on the pointer receiver, so the constraint is expressed on +// *T. Its single type term (*T) gives the constraint a core type, which lets +// constraint type inference deduce M from T alone — callers write +// NewPublisher[T](...) without naming the pointer type. +type message[T any] interface { + *T + cbg.CBORMarshaler + cbg.CBORUnmarshaler +} + +// Handler receives each decoded payload. The Follower calls it synchronously on +// the consume goroutine; a slow handler backs up incoming messages. +type Handler[T any] func(v *T) diff --git a/pkg/p2p/gossip/gossip_test.go b/pkg/p2p/gossip/gossip_test.go new file mode 100644 index 0000000..e0605fe --- /dev/null +++ b/pkg/p2p/gossip/gossip_test.go @@ -0,0 +1,93 @@ +package gossip + +import ( + "context" + "testing" + "time" + + libp2p "github.com/libp2p/go-libp2p" + "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/core/peer" + + "github.com/layer-3/clearnet-sdk/pkg/core" + p2pproto "github.com/layer-3/clearnet-sdk/pkg/p2p/protocol" +) + +// TestGossip_FinalizedWithdrawal shows the generic toolset carrying a concrete +// payload: callers name only the value type (core.FinalizedWithdrawal) and +// constraint type inference supplies the *T pointer type to Publisher/Follower. +func TestGossip_FinalizedWithdrawal(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + + hPub := newHost(t) + hSub := newHost(t) + connect(t, hPub, hSub) + + follower, err := NewFollower[core.FinalizedWithdrawal](ctx, hSub, p2pproto.TopicWithdrawals, nil) + if err != nil { + t.Fatalf("NewFollower: %v", err) + } + defer follower.Close() + + got := make(chan *core.FinalizedWithdrawal, 1) + follower.SetHandler(func(fw *core.FinalizedWithdrawal) { got <- fw }) + go follower.Run(ctx) + + pub, err := NewPublisher[core.FinalizedWithdrawal](ctx, hPub, p2pproto.TopicWithdrawals, nil) + if err != nil { + t.Fatalf("NewPublisher: %v", err) + } + defer pub.Close() + + if err := pub.WaitForPeers(ctx, 1, 10*time.Second); err != nil { + t.Fatalf("WaitForPeers: %v", err) + } + + want := &core.FinalizedWithdrawal{EntryIndex: 7} + want.WithdrawalID[0], want.WithdrawalID[31] = 0xF1, 0x7A + + ticker := time.NewTicker(300 * time.Millisecond) + defer ticker.Stop() + deadline := time.After(10 * time.Second) + for { + if err := pub.Publish(ctx, want); err != nil { + t.Fatalf("publish: %v", err) + } + select { + case fw := <-got: + if fw.WithdrawalID != want.WithdrawalID || fw.EntryIndex != want.EntryIndex { + t.Fatalf("delivered %+v, want %+v", fw.Header(), want.Header()) + } + if m := follower.Metrics().Snapshot(); m.Delivered != 1 { + t.Errorf("Delivered = %d, want 1", m.Delivered) + } + return + case <-ticker.C: + continue + case <-deadline: + t.Fatal("withdrawal never delivered") + } + } +} + +// ── helpers ─────────────────────────────────────────────────────────────── + +func newHost(t *testing.T) host.Host { + t.Helper() + h, err := libp2p.New(libp2p.ListenAddrStrings("/ip4/127.0.0.1/tcp/0")) + if err != nil { + t.Fatalf("libp2p new: %v", err) + } + t.Cleanup(func() { _ = h.Close() }) + return h +} + +func connect(t *testing.T, from, to host.Host) { + t.Helper() + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := from.Connect(ctx, peer.AddrInfo{ID: to.ID(), Addrs: to.Addrs()}); err != nil { + t.Fatalf("connect: %v", err) + } +} diff --git a/pkg/p2p/gossip/publisher.go b/pkg/p2p/gossip/publisher.go new file mode 100644 index 0000000..bc86028 --- /dev/null +++ b/pkg/p2p/gossip/publisher.go @@ -0,0 +1,80 @@ +package gossip + +import ( + "bytes" + "context" + "fmt" + "log/slog" + "time" + + pubsub "github.com/libp2p/go-libp2p-pubsub" + "github.com/libp2p/go-libp2p/core/host" + + "github.com/layer-3/clearnet-sdk/pkg/cborx" +) + +// Publisher joins a GossipSub topic and publishes values of type T. It does not +// subscribe: GossipSub only propagates once at least one subscriber is in the +// mesh, so a publisher-only node relies on its peers subscribing. +type Publisher[T any, M message[T]] struct { + topic *pubsub.Topic + name string + logger *slog.Logger +} + +// NewPublisher joins topic on h. The caller owns h and must keep it alive for +// the Publisher's lifetime. +func NewPublisher[T any, M message[T]](ctx context.Context, h host.Host, topic string, logger *slog.Logger) (*Publisher[T, M], error) { + if logger == nil { + logger = slog.Default() + } + ps, err := pubsub.NewGossipSub(ctx, h) + if err != nil { + return nil, fmt.Errorf("gossipsub: %w", err) + } + t, err := ps.Join(topic) + if err != nil { + return nil, fmt.Errorf("join %s: %w", topic, err) + } + return &Publisher[T, M]{ + topic: t, + name: topic, + logger: logger.With("component", "p2p-gossip-publisher", "topic", topic), + }, nil +} + +// Publish emits v on the topic using the cborx V1 envelope. v is *T. +func (p *Publisher[T, M]) Publish(ctx context.Context, v M) error { + var buf bytes.Buffer + if err := cborx.WriteEnvelope(&buf, cborx.V1, v); err != nil { + return fmt.Errorf("encode payload: %w", err) + } + return p.topic.Publish(ctx, buf.Bytes()) +} + +// Topic returns the joined topic name. +func (p *Publisher[T, M]) Topic() string { return p.name } + +// WaitForPeers blocks until at least minPeers subscribers have joined the topic +// mesh, ctx is cancelled, or timeout elapses. +func (p *Publisher[T, M]) WaitForPeers(ctx context.Context, minPeers int, timeout time.Duration) error { + t := time.NewTimer(timeout) + defer t.Stop() + tick := time.NewTicker(500 * time.Millisecond) + defer tick.Stop() + for { + if len(p.topic.ListPeers()) >= minPeers { + return nil + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-t.C: + return fmt.Errorf("only %d/%d peers joined within %s", len(p.topic.ListPeers()), minPeers, timeout) + case <-tick.C: + } + } +} + +// Close leaves the topic. It does not close the host — the caller owns that. +func (p *Publisher[T, M]) Close() error { return p.topic.Close() } diff --git a/pkg/p2p/protocol/protocols.go b/pkg/p2p/protocol/protocols.go new file mode 100644 index 0000000..94a333f --- /dev/null +++ b/pkg/p2p/protocol/protocols.go @@ -0,0 +1,71 @@ +// Package protocol defines the canonical libp2p wire contract: the stream +// protocol identifiers, the GossipSub topic names, and the framed message +// structs that travel over them. +// +// Every identifier is pinned to a single protocol version (1.0.0). A node only +// speaks to peers that dial the exact same string, so the version is the wire +// compatibility boundary — bump it deliberately, never per-stream. +// +// The package is transport-agnostic: it owns the names and the bytes, never a +// host.Host. The auth/receipt/pubsub helpers under pkg/p2p take a caller-built +// host and register handlers for these identifiers. +package protocol + +import "fmt" + +// Stream protocol identifiers. Each names a libp2p request/response or one-shot +// stream; the body is a cborx envelope (auth) or frame (receipt) of the typed +// payload. Only the identifiers a shared SDK consumer drives are exported here; +// the cluster-internal streams (transfer, heartbeat, identify, …) stay in the +// node that owns them. +// +// TODO(sdk): revisit whether other streams belong here. The rule today is +// "move a channel iff a consumer of the SDK could legitimately speak it" — so +// only the three shared custody↔clearnet streams (auth, burn/mint receipt) +// moved, and the ~14 cluster-internal streams (transfer, swap, shardsync, +// heartbeat, identify, peerexchange, signature, …) stayed in node.go. If a +// future consumer needs to dial one of those (and its wire body is extracted +// to the SDK), promote its identifier here and pin the version. +const ( + // ProtocolAuth carries the nonce-challenge authentication handshake: the + // server sends an AuthChallenge, the peer returns a signed AuthResponse. + ProtocolAuth = "/ynp/auth/1.0.0" + + // ProtocolBurnReceipt carries a signed BurnReceipt — the attestation that + // the L1 execute() of a finalized withdrawal landed — and a ReceiptAck. + ProtocolBurnReceipt = "/ynp/burnreceipt/1.0.0" + + // ProtocolMintReceipt carries a signed MintReceipt — the attestation that + // an L1 deposit confirmed — and a ReceiptAck. + ProtocolMintReceipt = "/ynp/mintreceipt/1.0.0" +) + +// GossipSub topic names. The topic name encodes the payload type; a subscriber +// dispatches on the topic and decodes the body as the cborx envelope of that +// payload directly (no inner message wrapper). +const ( + // TopicBlocks fans out sealed *core.Block values. + TopicBlocks = "/clearnet/blocks.v1" + + // TopicTransfers fans out a batched []core.Event of non-pool events. + TopicTransfers = "/clearnet/transfers.v1" + + // TopicWithdrawals fans out *core.FinalizedWithdrawal notifications. + TopicWithdrawals = "/clearnet/withdrawals.v1" + + // TopicChallenges fans out fraud-challenge submissions. + TopicChallenges = "/clearnet/challenges.v1" +) + +// PoolTopic returns the canonical GossipSub topic for a pool anchor. Format: +// /clearnet/pool/.v1. The payload is a batched []core.Event of pool +// events (Swap, LiquidityAdded/Removed, Repeg, PoolCreated). +func PoolTopic(anchor [32]byte) string { + return fmt.Sprintf("/clearnet/pool/%x.v1", anchor) +} + +// PoolTopicHex is the string-anchor variant for call sites that already carry +// the anchor as hex (no 0x prefix, lowercase, 64 chars). +func PoolTopicHex(anchorHex string) string { + return fmt.Sprintf("/clearnet/pool/%s.v1", anchorHex) +} diff --git a/pkg/p2p/protocol/protocols_test.go b/pkg/p2p/protocol/protocols_test.go new file mode 100644 index 0000000..096be24 --- /dev/null +++ b/pkg/p2p/protocol/protocols_test.go @@ -0,0 +1,56 @@ +package protocol + +import "testing" + +// Protocol identifiers and topics are the wire-compatibility boundary. Freeze +// the exact strings so a stray edit can't silently fork the version. +func TestProtocolIDsFrozen(t *testing.T) { + cases := map[string]string{ + "auth": ProtocolAuth, + "burnreceipt": ProtocolBurnReceipt, + "mintreceipt": ProtocolMintReceipt, + } + want := map[string]string{ + "auth": "/ynp/auth/1.0.0", + "burnreceipt": "/ynp/burnreceipt/1.0.0", + "mintreceipt": "/ynp/mintreceipt/1.0.0", + } + for k, got := range cases { + if got != want[k] { + t.Errorf("%s ID = %q, want %q", k, got, want[k]) + } + } +} + +func TestTopicsFrozen(t *testing.T) { + cases := map[string]string{ + "blocks": TopicBlocks, + "transfers": TopicTransfers, + "withdrawals": TopicWithdrawals, + "challenges": TopicChallenges, + } + want := map[string]string{ + "blocks": "/clearnet/blocks.v1", + "transfers": "/clearnet/transfers.v1", + "withdrawals": "/clearnet/withdrawals.v1", + "challenges": "/clearnet/challenges.v1", + } + for k, got := range cases { + if got != want[k] { + t.Errorf("%s topic = %q, want %q", k, got, want[k]) + } + } +} + +func TestPoolTopic(t *testing.T) { + var anchor [32]byte + anchor[0], anchor[31] = 0xab, 0xcd + got := PoolTopic(anchor) + want := "/clearnet/pool/ab000000000000000000000000000000000000000000000000000000000000cd.v1" + if got != want { + t.Errorf("PoolTopic = %q, want %q", got, want) + } + if hexVariant := PoolTopicHex("ab000000000000000000000000000000000000000000000000000000000000cd"); hexVariant != want { + t.Errorf("PoolTopicHex = %q, want %q", hexVariant, want) + } +} diff --git a/pkg/p2p/protocol/registrar.go b/pkg/p2p/protocol/registrar.go new file mode 100644 index 0000000..e4871d8 --- /dev/null +++ b/pkg/p2p/protocol/registrar.go @@ -0,0 +1,17 @@ +package protocol + +import "github.com/libp2p/go-libp2p/core/host" + +// Registrar is implemented by every p2p server: it installs that server's +// stream handlers on a caller-owned host. Keeping the contract here lets the +// wiring layer treat auth/receipt/… servers uniformly — register a slice of +// Registrars against one host without knowing their concrete types. +// +// GossipSub helpers (publish/subscribe over topics) deliberately do NOT +// implement Registrar: they register no stream handlers, which is the line +// between a stream protocol and a broadcast topic. +type Registrar interface { + // Register installs the server's stream handlers on h. The caller owns h + // and its lifecycle; Register only attaches handlers. + Register(h host.Host) +} diff --git a/pkg/p2p/protocol/wire.go b/pkg/p2p/protocol/wire.go new file mode 100644 index 0000000..e891ef3 --- /dev/null +++ b/pkg/p2p/protocol/wire.go @@ -0,0 +1,226 @@ +package protocol + +import ( + "fmt" + "io" + + cbg "github.com/whyrusleeping/cbor-gen" +) + +// TODO(sdk): migrate these handwritten CBOR codecs to cbor-gen (pkg/core/gen) +// once wire_test.go's golden vectors freeze the encoding. They are kept +// handwritten for now so the extraction is a byte-for-byte port of the +// established wire with no generator wiring. +// +// TODO(sdk): consider centralizing every p2p communication structure here — +// the auth challenge/response and receipt ack already live in this file, but +// the receipt bodies (core.BurnReceipt/MintReceipt) and topic event payloads +// (core.FinalizedWithdrawal, []core.Event, …) are defined elsewhere. Gathering +// the wire surface into one place with a consistent naming scheme — as +// erc7824/nitrolite does in pkg/rpc/api.go (structs) + pkg/rpc/methods.go +// (protocol/topic identifiers) — would make the full p2p contract readable at +// a glance. Blocked on deciding whether the shared core.* types should move or +// be re-exported, since they are also consumed off the wire. + +// AuthChallenge is sent by the server (entry node) to a connecting peer at the +// start of the auth handshake: 32 random bytes scoped to a single attempt. +// +// Wire encoding: cborx V1 envelope wrapping a 1-tuple (Nonce [32]byte). +type AuthChallenge struct { + Nonce [32]byte +} + +// AuthResponse is the peer's reply after signing the nonce. +// +// Wire encoding: cborx V1 envelope wrapping a 2-tuple (Signature []byte, +// Address string). Operator auth sets Address and signs keccak256(Nonce) with +// the operator secp256k1 key (raw v=0/1 form). Passive auth leaves Address +// empty and signs a domain-separated nonce with the libp2p identity key; the +// Address field is then carried empty. +type AuthResponse struct { + Signature []byte + Address string +} + +// ReceiptAck is the server's response to a burn/mint receipt submission. +// +// Accepted is true when the server persisted the receipt or recognized it as a +// duplicate of an already-persisted one (the receipt path is idempotent on the +// natural keys the clearing layer de-dupes by, so retries are safe). Reason +// carries a short diagnostic when Accepted is false; empty otherwise. +// +// Wire encoding: cborx V1 frame wrapping a 2-tuple (Accepted bool, Reason +// string). +type ReceiptAck struct { + Accepted bool + Reason string +} + +var lengthBufAuthChallenge = []byte{0x81} // CBOR array, 1 element + +// MarshalCBOR writes AuthChallenge as a 1-element CBOR array. +func (t *AuthChallenge) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + cw := cbg.NewCborWriter(w) + if _, err := cw.Write(lengthBufAuthChallenge); err != nil { + return err + } + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, 32); err != nil { + return err + } + _, err := cw.Write(t.Nonce[:]) + return err +} + +// UnmarshalCBOR reads AuthChallenge from a 1-element CBOR array. +func (t *AuthChallenge) UnmarshalCBOR(r io.Reader) error { + *t = AuthChallenge{} + cr := cbg.NewCborReader(r) + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajArray || extra != 1 { + return fmt.Errorf("AuthChallenge: expected 1-element CBOR array, got maj=%d extra=%d", maj, extra) + } + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajByteString || extra != 32 { + return fmt.Errorf("AuthChallenge.Nonce: expected 32-byte string") + } + if _, err := io.ReadFull(cr, t.Nonce[:]); err != nil { + return err + } + return nil +} + +var lengthBufAuthResponse = []byte{0x82} // CBOR array, 2 elements + +// MarshalCBOR writes AuthResponse as a 2-element CBOR array. +func (t *AuthResponse) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + cw := cbg.NewCborWriter(w) + if _, err := cw.Write(lengthBufAuthResponse); err != nil { + return err + } + if len(t.Signature) > cbg.MaxLength { + return fmt.Errorf("AuthResponse.Signature too long") + } + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.Signature))); err != nil { + return err + } + if _, err := cw.Write(t.Signature); err != nil { + return err + } + if len(t.Address) > cbg.MaxLength { + return fmt.Errorf("AuthResponse.Address too long") + } + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Address))); err != nil { + return err + } + _, err := cw.WriteString(t.Address) + return err +} + +// UnmarshalCBOR reads AuthResponse from a 2-element CBOR array. +func (t *AuthResponse) UnmarshalCBOR(r io.Reader) error { + *t = AuthResponse{} + cr := cbg.NewCborReader(r) + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajArray || extra != 2 { + return fmt.Errorf("AuthResponse: expected 2-element CBOR array") + } + // Signature (byte string). + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajByteString { + return fmt.Errorf("AuthResponse.Signature: expected byte string") + } + if extra > 1024 { + return fmt.Errorf("AuthResponse.Signature: implausibly large (%d)", extra) + } + t.Signature = make([]byte, extra) + if _, err := io.ReadFull(cr, t.Signature); err != nil { + return err + } + // Address (text string). + addr, err := cbg.ReadString(cr) + if err != nil { + return fmt.Errorf("AuthResponse.Address: %w", err) + } + t.Address = addr + return nil +} + +var lengthBufReceiptAck = []byte{0x82} // CBOR array, 2 elements + +// MarshalCBOR writes ReceiptAck as a 2-element CBOR array. +func (t *ReceiptAck) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + cw := cbg.NewCborWriter(w) + if _, err := cw.Write(lengthBufReceiptAck); err != nil { + return err + } + if err := cbg.WriteBool(cw, t.Accepted); err != nil { + return err + } + if len(t.Reason) > cbg.MaxLength { + return fmt.Errorf("ReceiptAck.Reason too long (%d)", len(t.Reason)) + } + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Reason))); err != nil { + return err + } + _, err := cw.WriteString(t.Reason) + return err +} + +// UnmarshalCBOR reads ReceiptAck from a 2-element CBOR array. +func (t *ReceiptAck) UnmarshalCBOR(r io.Reader) error { + *t = ReceiptAck{} + cr := cbg.NewCborReader(r) + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajArray || extra != 2 { + return fmt.Errorf("ReceiptAck: expected 2-element CBOR array") + } + // Accepted (CBOR simple value: 20 = false, 21 = true). + bmaj, bminor, err := cr.ReadHeader() + if err != nil { + return err + } + if bmaj != cbg.MajOther { + return fmt.Errorf("ReceiptAck.Accepted: expected bool, got major %d", bmaj) + } + switch bminor { + case 20: + t.Accepted = false + case 21: + t.Accepted = true + default: + return fmt.Errorf("ReceiptAck.Accepted: unexpected minor %d", bminor) + } + reason, err := cbg.ReadString(cr) + if err != nil { + return fmt.Errorf("ReceiptAck.Reason: %w", err) + } + t.Reason = reason + return nil +} diff --git a/pkg/p2p/protocol/wire_test.go b/pkg/p2p/protocol/wire_test.go new file mode 100644 index 0000000..c29838a --- /dev/null +++ b/pkg/p2p/protocol/wire_test.go @@ -0,0 +1,133 @@ +package protocol + +import ( + "bytes" + "encoding/hex" + "strings" + "testing" +) + +// Golden CBOR vectors freeze the wire bytes. Both clearnet and custody must +// encode these structs identically; a mismatch means the wire forked. +func TestWireGoldens(t *testing.T) { + var nonce [32]byte + for i := range nonce { + nonce[i] = byte(i) + } + + tests := []struct { + name string + marshal func() ([]byte, error) + wantHex string + }{ + { + name: "AuthChallenge", + marshal: func() ([]byte, error) { + var buf bytes.Buffer + err := (&AuthChallenge{Nonce: nonce}).MarshalCBOR(&buf) + return buf.Bytes(), err + }, + // 81 = array(1); 5820 = byte string len 32; then the nonce bytes. + wantHex: "815820" + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + }, + { + name: "AuthResponse", + marshal: func() ([]byte, error) { + var buf bytes.Buffer + err := (&AuthResponse{ + Signature: []byte{0xde, 0xad, 0xbe, 0xef}, + Address: "0x" + strings.Repeat("1", 40), + }).MarshalCBOR(&buf) + return buf.Bytes(), err + }, + // 82 = array(2); 44 deadbeef = bstr len 4; 782a = tstr len 42; 3078 = "0x"; then 40×'1'. + wantHex: "8244deadbeef782a3078" + strings.Repeat("31", 40), + }, + { + name: "ReceiptAck true/ok", + marshal: func() ([]byte, error) { + var buf bytes.Buffer + err := (&ReceiptAck{Accepted: true, Reason: "ok"}).MarshalCBOR(&buf) + return buf.Bytes(), err + }, + // 82 = array(2); f5 = true; 626f6b = tstr "ok". + wantHex: "82f5626f6b", + }, + { + name: "ReceiptAck false/empty", + marshal: func() ([]byte, error) { + var buf bytes.Buffer + err := (&ReceiptAck{Accepted: false, Reason: ""}).MarshalCBOR(&buf) + return buf.Bytes(), err + }, + // 82 = array(2); f4 = false; 60 = tstr len 0. + wantHex: "82f460", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got, err := tc.marshal() + if err != nil { + t.Fatalf("marshal: %v", err) + } + if gotHex := hex.EncodeToString(got); gotHex != tc.wantHex { + t.Errorf("bytes = %s\n want = %s", gotHex, tc.wantHex) + } + }) + } +} + +func TestWireRoundTrip(t *testing.T) { + var nonce [32]byte + nonce[0], nonce[31] = 0x11, 0x22 + + t.Run("AuthChallenge", func(t *testing.T) { + in := &AuthChallenge{Nonce: nonce} + var buf bytes.Buffer + if err := in.MarshalCBOR(&buf); err != nil { + t.Fatal(err) + } + var out AuthChallenge + if err := out.UnmarshalCBOR(&buf); err != nil { + t.Fatal(err) + } + if out.Nonce != in.Nonce { + t.Errorf("nonce mismatch") + } + }) + + t.Run("AuthResponse", func(t *testing.T) { + in := &AuthResponse{Signature: bytes.Repeat([]byte{0x7}, 65), Address: "0xDeadBeef"} + var buf bytes.Buffer + if err := in.MarshalCBOR(&buf); err != nil { + t.Fatal(err) + } + var out AuthResponse + if err := out.UnmarshalCBOR(&buf); err != nil { + t.Fatal(err) + } + if !bytes.Equal(out.Signature, in.Signature) || out.Address != in.Address { + t.Errorf("round-trip mismatch: %+v", out) + } + }) + + t.Run("ReceiptAck", func(t *testing.T) { + for _, in := range []*ReceiptAck{ + {Accepted: true, Reason: ""}, + {Accepted: false, Reason: "rejected: bad signature"}, + } { + var buf bytes.Buffer + if err := in.MarshalCBOR(&buf); err != nil { + t.Fatal(err) + } + var out ReceiptAck + if err := out.UnmarshalCBOR(&buf); err != nil { + t.Fatal(err) + } + if out != *in { + t.Errorf("round-trip mismatch: got %+v want %+v", out, *in) + } + } + }) +} diff --git a/pkg/p2p/pubsub/follower.go b/pkg/p2p/pubsub/follower.go new file mode 100644 index 0000000..abb413d --- /dev/null +++ b/pkg/p2p/pubsub/follower.go @@ -0,0 +1,175 @@ +package pubsub + +import ( + "bytes" + "context" + "fmt" + "log/slog" + "sync" + + pubsub "github.com/libp2p/go-libp2p-pubsub" + "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/core/peer" + + "github.com/layer-3/clearnet-sdk/pkg/cborx" + "github.com/layer-3/clearnet-sdk/pkg/core" +) + +// maxFinalizedWithdrawalBytes caps the raw size of an inbound message before +// any CBOR allocation. A realistic envelope is a few KB (one Block + the BLS +// aggregate); 128 KiB leaves headroom for the largest plausible Block while +// keeping a malicious publisher from forcing megabyte allocations per message. +const maxFinalizedWithdrawalBytes = 128 * 1024 + +// WithdrawalHandler receives each FinalizedWithdrawal decoded from the topic. +// The Follower calls it synchronously on the consume goroutine; a slow handler +// backs up incoming messages. +type WithdrawalHandler func(fw *core.FinalizedWithdrawal) + +// Metrics captures Follower counters for ops dashboards. +type Metrics struct { + mu sync.Mutex + DeliveredWithdrawals uint64 // handed to the handler + DecodeErrors uint64 // envelope/CBOR decode failures + OversizeDrops uint64 // dropped for exceeding the size cap +} + +// Snapshot returns a copy of the current counters. Safe for concurrent use. +func (m *Metrics) Snapshot() Metrics { + m.mu.Lock() + defer m.mu.Unlock() + return Metrics{ + DeliveredWithdrawals: m.DeliveredWithdrawals, + DecodeErrors: m.DecodeErrors, + OversizeDrops: m.OversizeDrops, + } +} + +func (m *Metrics) inc(field *uint64) { + m.mu.Lock() + *field++ + m.mu.Unlock() +} + +// Follower subscribes to a topic on a caller-owned host and forwards each +// decoded FinalizedWithdrawal to a handler. +type Follower struct { + host host.Host + sub *pubsub.Subscription + topic *pubsub.Topic + name string + logger *slog.Logger + metrics *Metrics + + handlerMu sync.RWMutex + handler WithdrawalHandler +} + +// NewFollower joins and subscribes to topic on h. A size-validator is attached +// before joining, so oversized messages are rejected by GossipSub and never +// reach the consume loop. Call Run to start consuming; register a handler with +// SetHandler before (or shortly after) Run — messages arriving with no handler +// are dropped with a warning. The caller owns h and its connectivity. +func NewFollower(ctx context.Context, h host.Host, topic string, logger *slog.Logger) (*Follower, error) { + if logger == nil { + logger = slog.Default() + } + ps, err := pubsub.NewGossipSub(ctx, h) + if err != nil { + return nil, fmt.Errorf("gossipsub: %w", err) + } + f := &Follower{ + host: h, + name: topic, + logger: logger.With("component", "p2p-pubsub-follower", "topic", topic), + metrics: &Metrics{}, + } + // Register the validator before Join so it is attached from the first + // message. Oversized messages are rejected by GossipSub itself. + if err := ps.RegisterTopicValidator(topic, f.validateSize); err != nil { + return nil, fmt.Errorf("register validator %s: %w", topic, err) + } + t, err := ps.Join(topic) + if err != nil { + return nil, fmt.Errorf("join %s: %w", topic, err) + } + sub, err := t.Subscribe() + if err != nil { + _ = t.Close() + return nil, fmt.Errorf("subscribe %s: %w", topic, err) + } + f.topic = t + f.sub = sub + f.logger.Info("pubsub follower started", "peer_id", h.ID().String()) + return f, nil +} + +// SetHandler installs the handler invoked for each decoded withdrawal. Safe to +// call concurrently with Run. +func (f *Follower) SetHandler(h WithdrawalHandler) { + f.handlerMu.Lock() + f.handler = h + f.handlerMu.Unlock() +} + +// Metrics returns the Follower's counters handle. +func (f *Follower) Metrics() *Metrics { return f.metrics } + +// PeerID returns the host's libp2p peer ID. +func (f *Follower) PeerID() peer.ID { return f.host.ID() } + +// Run consumes the subscription until ctx is cancelled or the subscription +// closes. It blocks; run it in a goroutine. +func (f *Follower) Run(ctx context.Context) { + for { + msg, err := f.sub.Next(ctx) + if err != nil { + if ctx.Err() == nil { + f.logger.Debug("subscription closed", "error", err) + } + return + } + // Skip our own messages. + if msg.ReceivedFrom == f.host.ID() { + continue + } + f.handle(msg) + } +} + +// Close cancels the subscription and leaves the topic. It does not close the +// host — the caller owns that. +func (f *Follower) Close() error { + f.sub.Cancel() + return f.topic.Close() +} + +// validateSize is the GossipSub topic validator: it rejects oversized messages +// before they propagate or reach the consume loop. +func (f *Follower) validateSize(_ context.Context, from peer.ID, msg *pubsub.Message) bool { + if len(msg.Data) > maxFinalizedWithdrawalBytes { + f.metrics.inc(&f.metrics.OversizeDrops) + f.logger.Warn("dropping oversize message", "from", from.ShortString(), "bytes", len(msg.Data)) + return false + } + return true +} + +func (f *Follower) handle(msg *pubsub.Message) { + var fw core.FinalizedWithdrawal + var v cborx.Version + if err := cborx.ReadEnvelopeStrict(bytes.NewReader(msg.Data), &v, &fw); err != nil { + f.metrics.inc(&f.metrics.DecodeErrors) + f.logger.Warn("decode finalized withdrawal failed", "error", err) + return + } + f.handlerMu.RLock() + h := f.handler + f.handlerMu.RUnlock() + if h == nil { + f.logger.Warn("no handler registered; dropping withdrawal") + return + } + h(&fw) + f.metrics.inc(&f.metrics.DeliveredWithdrawals) +} diff --git a/pkg/p2p/pubsub/publisher.go b/pkg/p2p/pubsub/publisher.go new file mode 100644 index 0000000..d492b71 --- /dev/null +++ b/pkg/p2p/pubsub/publisher.go @@ -0,0 +1,95 @@ +// Package pubsub provides GossipSub publish/subscribe helpers for the clearing +// layer's broadcast topics. A Publisher joins a topic and emits typed payloads; +// a Follower subscribes and forwards decoded payloads to a handler. +// +// Both are host-taking: the caller builds and owns the libp2p host (identity, +// listen addresses, resource limits) and is responsible for connectivity +// (dialing seed peers, peer discovery). These helpers own only the GossipSub +// instance, the topic, and — for the Follower — the subscription. Close +// releases those, never the host. +package pubsub + +import ( + "bytes" + "context" + "fmt" + "log/slog" + "time" + + pubsub "github.com/libp2p/go-libp2p-pubsub" + "github.com/libp2p/go-libp2p/core/host" + + "github.com/layer-3/clearnet-sdk/pkg/cborx" + "github.com/layer-3/clearnet-sdk/pkg/core" +) + +// Publisher joins a GossipSub topic and publishes typed payloads to it. It does +// not subscribe: GossipSub only propagates once at least one subscriber is in +// the mesh, so a publisher-only node relies on its peers subscribing. +type Publisher struct { + host host.Host + topic *pubsub.Topic + name string + logger *slog.Logger +} + +// NewPublisher joins topic on h and returns a Publisher. The caller owns h and +// must keep it alive for the Publisher's lifetime. +func NewPublisher(ctx context.Context, h host.Host, topic string, logger *slog.Logger) (*Publisher, error) { + if logger == nil { + logger = slog.Default() + } + ps, err := pubsub.NewGossipSub(ctx, h) + if err != nil { + return nil, fmt.Errorf("gossipsub: %w", err) + } + t, err := ps.Join(topic) + if err != nil { + return nil, fmt.Errorf("join %s: %w", topic, err) + } + return &Publisher{ + host: h, + topic: t, + name: topic, + logger: logger.With("component", "p2p-pubsub-publisher", "topic", topic), + }, nil +} + +// PublishFinalizedWithdrawal emits a single FinalizedWithdrawal on the topic +// using the cborx V1 envelope. +func (p *Publisher) PublishFinalizedWithdrawal(ctx context.Context, fw *core.FinalizedWithdrawal) error { + var buf bytes.Buffer + if err := cborx.WriteEnvelope(&buf, cborx.V1, fw); err != nil { + return fmt.Errorf("encode finalized withdrawal: %w", err) + } + return p.topic.Publish(ctx, buf.Bytes()) +} + +// Topic returns the joined topic name. +func (p *Publisher) Topic() string { return p.name } + +// WaitForPeers blocks until at least minPeers subscribers have joined the topic +// mesh, ctx is cancelled, or timeout elapses. GossipSub forwards only once mesh +// links form; publishing before then silently drops, so a publisher-only node +// should wait before its first broadcast. +func (p *Publisher) WaitForPeers(ctx context.Context, minPeers int, timeout time.Duration) error { + t := time.NewTimer(timeout) + defer t.Stop() + tick := time.NewTicker(500 * time.Millisecond) + defer tick.Stop() + for { + if len(p.topic.ListPeers()) >= minPeers { + return nil + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-t.C: + return fmt.Errorf("only %d/%d peers joined within %s", len(p.topic.ListPeers()), minPeers, timeout) + case <-tick.C: + } + } +} + +// Close leaves the topic. It does not close the host — the caller owns that. +func (p *Publisher) Close() error { return p.topic.Close() } diff --git a/pkg/p2p/pubsub/pubsub_test.go b/pkg/p2p/pubsub/pubsub_test.go new file mode 100644 index 0000000..0c99165 --- /dev/null +++ b/pkg/p2p/pubsub/pubsub_test.go @@ -0,0 +1,92 @@ +package pubsub + +import ( + "context" + "testing" + "time" + + libp2p "github.com/libp2p/go-libp2p" + "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/core/peer" + + "github.com/layer-3/clearnet-sdk/pkg/core" + p2pproto "github.com/layer-3/clearnet-sdk/pkg/p2p/protocol" +) + +func TestPubSub_PublishDeliver(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + + hPub := newHost(t) + hSub := newHost(t) + connect(t, hPub, hSub) + + follower, err := NewFollower(ctx, hSub, p2pproto.TopicWithdrawals, nil) + if err != nil { + t.Fatalf("NewFollower: %v", err) + } + defer follower.Close() + + got := make(chan *core.FinalizedWithdrawal, 1) + follower.SetHandler(func(fw *core.FinalizedWithdrawal) { got <- fw }) + go follower.Run(ctx) + + pub, err := NewPublisher(ctx, hPub, p2pproto.TopicWithdrawals, nil) + if err != nil { + t.Fatalf("NewPublisher: %v", err) + } + defer pub.Close() + + // Wait for the subscriber to appear in the publisher's topic mesh. + if err := pub.WaitForPeers(ctx, 1, 10*time.Second); err != nil { + t.Fatalf("WaitForPeers: %v", err) + } + + want := &core.FinalizedWithdrawal{EntryIndex: 7} + want.WithdrawalID[0], want.WithdrawalID[31] = 0xF1, 0x7A + + // GossipSub mesh links may still be forming; republish until delivered. + ticker := time.NewTicker(300 * time.Millisecond) + defer ticker.Stop() + deadline := time.After(10 * time.Second) + for { + if err := pub.PublishFinalizedWithdrawal(ctx, want); err != nil { + t.Fatalf("publish: %v", err) + } + select { + case fw := <-got: + if fw.WithdrawalID != want.WithdrawalID || fw.EntryIndex != want.EntryIndex { + t.Fatalf("delivered %+v, want %+v", fw.Header(), want.Header()) + } + if m := follower.Metrics().Snapshot(); m.DeliveredWithdrawals != 1 { + t.Errorf("DeliveredWithdrawals = %d, want 1", m.DeliveredWithdrawals) + } + return + case <-ticker.C: + continue + case <-deadline: + t.Fatal("withdrawal never delivered") + } + } +} + +// ── helpers ─────────────────────────────────────────────────────────────── + +func newHost(t *testing.T) host.Host { + t.Helper() + h, err := libp2p.New(libp2p.ListenAddrStrings("/ip4/127.0.0.1/tcp/0")) + if err != nil { + t.Fatalf("libp2p new: %v", err) + } + t.Cleanup(func() { _ = h.Close() }) + return h +} + +func connect(t *testing.T, from, to host.Host) { + t.Helper() + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := from.Connect(ctx, peer.AddrInfo{ID: to.ID(), Addrs: to.Addrs()}); err != nil { + t.Fatalf("connect: %v", err) + } +} diff --git a/pkg/p2p/receipt/client.go b/pkg/p2p/receipt/client.go new file mode 100644 index 0000000..ea92c85 --- /dev/null +++ b/pkg/p2p/receipt/client.go @@ -0,0 +1,93 @@ +package receipt + +import ( + "bytes" + "context" + "fmt" + "io" + "log/slog" + "time" + + "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/core/protocol" + + cbg "github.com/whyrusleeping/cbor-gen" + + "github.com/layer-3/clearnet-sdk/pkg/cborx" + "github.com/layer-3/clearnet-sdk/pkg/core" + p2pproto "github.com/layer-3/clearnet-sdk/pkg/p2p/protocol" +) + +const defaultTimeout = 15 * time.Second + +// Client submits burn/mint receipts to a single peer over a caller-owned +// host.Host. It is a stateless convenience: the caller is responsible for +// making peerID reachable (adding it to the peerstore and/or dialing) before +// the first send. +type Client struct { + host host.Host + peerID peer.ID + timeout time.Duration + logger *slog.Logger +} + +// NewClient creates a Client that submits to peerID over h. +func NewClient(h host.Host, peerID peer.ID, logger *slog.Logger) *Client { + if logger == nil { + logger = slog.Default() + } + return &Client{ + host: h, + peerID: peerID, + timeout: defaultTimeout, + logger: logger.With("component", "p2p-receipt-client"), + } +} + +// SendBurnReceipt writes r on /ynp/burnreceipt/1.0.0 and returns the ack. +// Transport-level errors (timeout, stream, decode) come back as a non-nil +// error; an Accepted=false ack returns without error so the caller decides +// whether to retry. +func (c *Client) SendBurnReceipt(ctx context.Context, r *core.BurnReceipt) (p2pproto.ReceiptAck, error) { + return c.submit(ctx, p2pproto.ProtocolBurnReceipt, r) +} + +// SendMintReceipt writes r on /ynp/mintreceipt/1.0.0. Same error semantics as +// SendBurnReceipt. +func (c *Client) SendMintReceipt(ctx context.Context, r *core.MintReceipt) (p2pproto.ReceiptAck, error) { + return c.submit(ctx, p2pproto.ProtocolMintReceipt, r) +} + +// submit is the shared transport path for both receipt kinds. +func (c *Client) submit(ctx context.Context, proto string, body cbg.CBORMarshaler) (p2pproto.ReceiptAck, error) { + if c.peerID == "" { + return p2pproto.ReceiptAck{}, fmt.Errorf("receipt: no peer configured") + } + ctx, cancel := context.WithTimeout(ctx, c.timeout) + defer cancel() + + s, err := c.host.NewStream(ctx, c.peerID, protocol.ID(proto)) + if err != nil { + return p2pproto.ReceiptAck{}, fmt.Errorf("open stream %s: %w", proto, err) + } + defer s.Close() + + var reqBuf bytes.Buffer + if err := cborx.WriteFrame(&reqBuf, cborx.V1, body); err != nil { + return p2pproto.ReceiptAck{}, fmt.Errorf("encode receipt: %w", err) + } + if _, err := s.Write(reqBuf.Bytes()); err != nil { + return p2pproto.ReceiptAck{}, fmt.Errorf("write receipt: %w", err) + } + if err := s.CloseWrite(); err != nil { + return p2pproto.ReceiptAck{}, fmt.Errorf("close write: %w", err) + } + + var ack p2pproto.ReceiptAck + var v cborx.Version + if err := cborx.ReadFrame(io.LimitReader(s, maxReceiptBytes), cborx.MaxControlFrame, &v, &ack); err != nil { + return p2pproto.ReceiptAck{}, fmt.Errorf("decode ack: %w", err) + } + return ack, nil +} diff --git a/pkg/p2p/receipt/interface.go b/pkg/p2p/receipt/interface.go new file mode 100644 index 0000000..96676e8 --- /dev/null +++ b/pkg/p2p/receipt/interface.go @@ -0,0 +1,22 @@ +package receipt + +import ( + "context" + + "github.com/layer-3/clearnet-sdk/pkg/core" + p2pproto "github.com/layer-3/clearnet-sdk/pkg/p2p/protocol" +) + +// ReceiptHandler is the business seam a consumer implements to process inbound +// receipts. The Server decodes the wire frame and calls the matching method; +// the returned ReceiptAck is sent back to the peer. A non-nil error is +// delivered to the peer as Accepted=false with the error string. +// +// Implementations must be idempotent on the clearing layer's natural de-dupe +// keys (BurnReceipt: BlockHash+EntryIndex; MintReceipt: ChainID+L1TxHash+ +// LogIndex) so client retries are safe. A consumer that handles only one kind +// still implements both methods — return a reject ack for the unhandled one. +type ReceiptHandler interface { + OnBurnReceipt(ctx context.Context, r *core.BurnReceipt) (p2pproto.ReceiptAck, error) + OnMintReceipt(ctx context.Context, r *core.MintReceipt) (p2pproto.ReceiptAck, error) +} diff --git a/pkg/p2p/receipt/receipt_test.go b/pkg/p2p/receipt/receipt_test.go new file mode 100644 index 0000000..278caf6 --- /dev/null +++ b/pkg/p2p/receipt/receipt_test.go @@ -0,0 +1,152 @@ +package receipt + +import ( + "context" + "math/big" + "testing" + "time" + + libp2p "github.com/libp2p/go-libp2p" + "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/core/protocol" + + "github.com/layer-3/clearnet-sdk/pkg/core" + p2pproto "github.com/layer-3/clearnet-sdk/pkg/p2p/protocol" +) + +// testHandler is a ReceiptHandler backed by per-test closures. +type testHandler struct { + burn func(context.Context, *core.BurnReceipt) (p2pproto.ReceiptAck, error) + mint func(context.Context, *core.MintReceipt) (p2pproto.ReceiptAck, error) +} + +func (h testHandler) OnBurnReceipt(ctx context.Context, r *core.BurnReceipt) (p2pproto.ReceiptAck, error) { + return h.burn(ctx, r) +} + +func (h testHandler) OnMintReceipt(ctx context.Context, r *core.MintReceipt) (p2pproto.ReceiptAck, error) { + return h.mint(ctx, r) +} + +func TestReceipt_BurnRoundTrip(t *testing.T) { + srv, cli := newPair(t) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + var got *core.BurnReceipt + NewServer(testHandler{ + burn: func(_ context.Context, r *core.BurnReceipt) (p2pproto.ReceiptAck, error) { + got = r + return p2pproto.ReceiptAck{Accepted: true}, nil + }, + }, nil).Register(srv) + + want := &core.BurnReceipt{Signatures: [][]byte{{0x1, 0x2}}} + want.WithdrawalID[0] = 0xBE + want.L1TxHash[31] = 0xEF + + ack, err := NewClient(cli, srv.ID(), nil).SendBurnReceipt(ctx, want) + if err != nil { + t.Fatalf("SendBurnReceipt: %v", err) + } + if !ack.Accepted { + t.Fatalf("ack not accepted: %+v", ack) + } + if got == nil || got.WithdrawalID != want.WithdrawalID || got.L1TxHash != want.L1TxHash { + t.Fatalf("server received %+v, want %+v", got, want) + } +} + +func TestReceipt_MintRejected(t *testing.T) { + srv, cli := newPair(t) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + NewServer(testHandler{ + mint: func(_ context.Context, _ *core.MintReceipt) (p2pproto.ReceiptAck, error) { + return p2pproto.ReceiptAck{Accepted: false, Reason: "duplicate"}, nil + }, + }, nil).Register(srv) + + ack, err := NewClient(cli, srv.ID(), nil).SendMintReceipt(ctx, &core.MintReceipt{ + ChainID: 1, Account: "yellow://x", Asset: "ETH", Amount: big.NewInt(5), + }) + if err != nil { + t.Fatalf("SendMintReceipt: %v", err) + } + if ack.Accepted || ack.Reason != "duplicate" { + t.Fatalf("ack = %+v, want rejected/duplicate", ack) + } +} + +func TestReceipt_HandlerError(t *testing.T) { + srv, cli := newPair(t) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + NewServer(testHandler{ + burn: func(_ context.Context, _ *core.BurnReceipt) (p2pproto.ReceiptAck, error) { + return p2pproto.ReceiptAck{}, context.Canceled // any error + }, + }, nil).Register(srv) + + ack, err := NewClient(cli, srv.ID(), nil).SendBurnReceipt(ctx, &core.BurnReceipt{}) + if err != nil { + t.Fatalf("transport error: %v", err) + } + if ack.Accepted || ack.Reason == "" { + t.Fatalf("expected Accepted=false with a reason, got %+v", ack) + } +} + +// TestReceipt_HandleBurnReceiptDirect wires a single Handle* method (not via +// Register) to show it is independently registrable — the unit any consumer +// can mount under its own protocol ID or test in isolation. +func TestReceipt_HandleBurnReceiptDirect(t *testing.T) { + srv, cli := newPair(t) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + called := make(chan struct{}, 1) + s := NewServer(testHandler{ + burn: func(_ context.Context, _ *core.BurnReceipt) (p2pproto.ReceiptAck, error) { + called <- struct{}{} + return p2pproto.ReceiptAck{Accepted: true}, nil + }, + }, nil) + srv.SetStreamHandler(protocol.ID(p2pproto.ProtocolBurnReceipt), s.HandleBurnReceipt) + + if _, err := NewClient(cli, srv.ID(), nil).SendBurnReceipt(ctx, &core.BurnReceipt{}); err != nil { + t.Fatalf("SendBurnReceipt: %v", err) + } + select { + case <-called: + case <-time.After(3 * time.Second): + t.Fatal("HandleBurnReceipt never invoked") + } +} + +// ── helpers ─────────────────────────────────────────────────────────────── + +func newPair(t *testing.T) (srv, cli host.Host) { + t.Helper() + srv = newHost(t) + cli = newHost(t) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := cli.Connect(ctx, peer.AddrInfo{ID: srv.ID(), Addrs: srv.Addrs()}); err != nil { + t.Fatalf("connect: %v", err) + } + return srv, cli +} + +func newHost(t *testing.T) host.Host { + t.Helper() + h, err := libp2p.New(libp2p.ListenAddrStrings("/ip4/127.0.0.1/tcp/0")) + if err != nil { + t.Fatalf("libp2p new: %v", err) + } + t.Cleanup(func() { _ = h.Close() }) + return h +} diff --git a/pkg/p2p/receipt/server.go b/pkg/p2p/receipt/server.go new file mode 100644 index 0000000..cd73f71 --- /dev/null +++ b/pkg/p2p/receipt/server.go @@ -0,0 +1,124 @@ +// Package receipt implements the libp2p request/response stream protocols for +// burn/mint receipt submission. A client writes a cborx-framed receipt; the +// Server decodes it, hands it to a ReceiptHandler, and writes back a +// protocol.ReceiptAck. +// +// Two protocols share one shape, differing only in protocol ID and payload: +// +// /ynp/burnreceipt/1.0.0 — request *core.BurnReceipt, response *protocol.ReceiptAck +// /ynp/mintreceipt/1.0.0 — request *core.MintReceipt, response *protocol.ReceiptAck +// +// The package never builds a host.Host: Register installs handlers on a +// caller-supplied host (Server satisfies protocol.Registrar), and Client dials +// over one. +package receipt + +import ( + "bytes" + "context" + "fmt" + "io" + "log/slog" + "time" + + "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/core/protocol" + + "github.com/layer-3/clearnet-sdk/pkg/cborx" + "github.com/layer-3/clearnet-sdk/pkg/core" + p2pproto "github.com/layer-3/clearnet-sdk/pkg/p2p/protocol" +) + +// Per-stream guards. A CBOR receipt is hundreds of bytes; the cap stops a hung +// or hostile peer from streaming the server out of memory, and the deadline +// bounds a single request end to end. +const ( + maxReceiptBytes = 64 * 1024 + streamReadDeadline = 10 * time.Second +) + +// Server handles inbound burn/mint receipt streams, delegating each decoded +// receipt to a ReceiptHandler. +type Server struct { + handler ReceiptHandler + logger *slog.Logger +} + +var _ p2pproto.Registrar = (*Server)(nil) + +// NewServer returns a Server that delegates to handler. +func NewServer(handler ReceiptHandler, logger *slog.Logger) *Server { + if logger == nil { + logger = slog.Default() + } + return &Server{handler: handler, logger: logger.With("component", "p2p-receipt-server")} +} + +// Register installs both receipt stream handlers on h. +func (s *Server) Register(h host.Host) { + h.SetStreamHandler(protocol.ID(p2pproto.ProtocolBurnReceipt), s.HandleBurnReceipt) + h.SetStreamHandler(protocol.ID(p2pproto.ProtocolMintReceipt), s.HandleMintReceipt) +} + +// HandleBurnReceipt is the stream handler for /ynp/burnreceipt/1.0.0. +func (s *Server) HandleBurnReceipt(stream network.Stream) { + s.serve(stream, p2pproto.ProtocolBurnReceipt, func(ctx context.Context, r io.Reader) (p2pproto.ReceiptAck, error) { + var receipt core.BurnReceipt + var v cborx.Version + if err := cborx.ReadFrame(r, cborx.MaxControlFrame, &v, &receipt); err != nil { + return p2pproto.ReceiptAck{}, fmt.Errorf("decode: %w", err) + } + return s.handler.OnBurnReceipt(ctx, &receipt) + }) +} + +// HandleMintReceipt is the stream handler for /ynp/mintreceipt/1.0.0. +func (s *Server) HandleMintReceipt(stream network.Stream) { + s.serve(stream, p2pproto.ProtocolMintReceipt, func(ctx context.Context, r io.Reader) (p2pproto.ReceiptAck, error) { + var receipt core.MintReceipt + var v cborx.Version + if err := cborx.ReadFrame(r, cborx.MaxControlFrame, &v, &receipt); err != nil { + return p2pproto.ReceiptAck{}, fmt.Errorf("decode: %w", err) + } + return s.handler.OnMintReceipt(ctx, &receipt) + }) +} + +// serve runs one deadline-bounded request: decode via dispatch, write the ack. +// The per-call context is derived from Background and bounded by the same +// deadline as the stream read — the Server holds no context of its own. +func (s *Server) serve( + stream network.Stream, + proto string, + dispatch func(context.Context, io.Reader) (p2pproto.ReceiptAck, error), +) { + defer stream.Close() + log := s.logger.With("protocol", proto) + + ctx, cancel := context.WithTimeout(context.Background(), streamReadDeadline) + defer cancel() + if err := stream.SetReadDeadline(time.Now().Add(streamReadDeadline)); err != nil { + log.Warn("set read deadline failed", "error", err) + return + } + + ack, err := dispatch(ctx, io.LimitReader(stream, maxReceiptBytes)) + if err != nil { + log.Warn("handler error", "error", err) + writeAck(stream, p2pproto.ReceiptAck{Accepted: false, Reason: err.Error()}, log) + return + } + writeAck(stream, ack, log) +} + +func writeAck(stream network.Stream, ack p2pproto.ReceiptAck, log *slog.Logger) { + var buf bytes.Buffer + if err := cborx.WriteFrame(&buf, cborx.V1, &ack); err != nil { + log.Warn("encode ack failed", "error", err) + return + } + if _, err := stream.Write(buf.Bytes()); err != nil { + log.Warn("write ack failed", "error", err) + } +} From b0ff63d6c7c527752bfc2ffc2a59155f48ead93f Mon Sep 17 00:00:00 2001 From: Anton Filonenko Date: Wed, 17 Jun 2026 13:00:52 +0300 Subject: [PATCH 07/15] refactor(log): adopt structured Logger interface, drop slog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds pkg/log — a structured, context-aware Logger interface with zap, noop, and span (OpenTelemetry) implementations — and routes every logging call through it. - pkg/p2p (auth/receipt/pubsub/gossip): loggers take log.Logger; nil defaults to a no-op so the library is silent unless a logger is injected. - evm BLSPubkeyCache: replace global slog calls with an injected logger field, defaulting to no-op, settable via SetLogger; the constructor signature is unchanged. --- go.mod | 7 +- go.sum | 2 + pkg/blockchain/evm/bls_cache.go | 27 ++++-- pkg/log/context.go | 40 ++++++++ pkg/log/doc.go | 123 ++++++++++++++++++++++++ pkg/log/noop_logger.go | 44 +++++++++ pkg/log/otel_ser.go | 161 +++++++++++++++++++++++++++++++ pkg/log/span_logger.go | 123 ++++++++++++++++++++++++ pkg/log/types.go | 71 ++++++++++++++ pkg/log/zap_logger.go | 163 ++++++++++++++++++++++++++++++++ pkg/p2p/auth/server.go | 16 ++-- pkg/p2p/gossip/follower.go | 10 +- pkg/p2p/gossip/publisher.go | 10 +- pkg/p2p/pubsub/follower.go | 10 +- pkg/p2p/pubsub/publisher.go | 10 +- pkg/p2p/receipt/client.go | 10 +- pkg/p2p/receipt/server.go | 26 ++--- 17 files changed, 797 insertions(+), 56 deletions(-) create mode 100644 pkg/log/context.go create mode 100644 pkg/log/doc.go create mode 100644 pkg/log/noop_logger.go create mode 100644 pkg/log/otel_ser.go create mode 100644 pkg/log/span_logger.go create mode 100644 pkg/log/types.go create mode 100644 pkg/log/zap_logger.go diff --git a/go.mod b/go.mod index 3602d92..389ae9e 100644 --- a/go.mod +++ b/go.mod @@ -15,9 +15,13 @@ require ( github.com/gagliardetto/binary v0.8.0 github.com/gagliardetto/solana-go v1.21.0 github.com/ipfs/go-cid v0.5.0 + github.com/jsternberg/zap-logfmt v1.3.0 github.com/libp2p/go-libp2p v0.48.0 github.com/libp2p/go-libp2p-pubsub v0.16.0 github.com/whyrusleeping/cbor-gen v0.3.1 + go.opentelemetry.io/otel v1.40.0 + go.opentelemetry.io/otel/trace v1.40.0 + go.uber.org/zap v1.27.0 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 ) @@ -140,15 +144,12 @@ require ( github.com/x448/float16 v0.8.4 // indirect go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel v1.40.0 // indirect go.opentelemetry.io/otel/metric v1.40.0 // indirect - go.opentelemetry.io/otel/trace v1.40.0 // indirect go.uber.org/dig v1.19.0 // indirect go.uber.org/fx v1.24.0 // indirect go.uber.org/mock v0.5.2 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/ratelimit v0.3.1 // indirect - go.uber.org/zap v1.27.0 // indirect golang.org/x/crypto v0.48.0 // indirect golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 // indirect golang.org/x/mod v0.33.0 // indirect diff --git a/go.sum b/go.sum index 3f474c1..3d6eba8 100644 --- a/go.sum +++ b/go.sum @@ -173,6 +173,8 @@ github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABo github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jsternberg/zap-logfmt v1.3.0 h1:z1n1AOHVVydOOVuyphbOKyR4NICDQFiJMn1IK5hVQ5Y= +github.com/jsternberg/zap-logfmt v1.3.0/go.mod h1:N3DENp9WNmCZxvkBD/eReWwz1149BK6jEN9cQ4fNwZE= github.com/kcalvinalvin/anet v0.0.0-20251112173137-d8ddc1f6dbee h1:FPP9HDkBbPyniu+u7FHZg+kKFX1WW0gxOGteJ0h3AJk= github.com/kcalvinalvin/anet v0.0.0-20251112173137-d8ddc1f6dbee/go.mod h1:N6sz6HwJAenJ6d+/xmSl0ikfV05ZrVGmjt1ryy/WOtE= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= diff --git a/pkg/blockchain/evm/bls_cache.go b/pkg/blockchain/evm/bls_cache.go index 8da7922..fad282f 100644 --- a/pkg/blockchain/evm/bls_cache.go +++ b/pkg/blockchain/evm/bls_cache.go @@ -3,7 +3,6 @@ package evm import ( "context" "fmt" - "log/slog" "math/big" "sync" "time" @@ -16,6 +15,7 @@ import ( "github.com/ethereum/go-ethereum/ethclient" "github.com/layer-3/clearnet-sdk/pkg/core" + "github.com/layer-3/clearnet-sdk/pkg/log" ) // BLSPubkeyCacheSize is the expected G2 serialization length (ADR-008 / ISSUE-035 @@ -81,6 +81,9 @@ type BLSPubkeyCache struct { // committing a NodeActivated / NodeReleased event into the cache. confirmations uint64 + // logger defaults to a no-op; set with SetLogger. + logger log.Logger + mu sync.RWMutex // keys is the forward index: nodeId → 128-byte G2 pubkey. keys map[core.NodeID][]byte @@ -120,11 +123,21 @@ func NewBLSPubkeyCache(client *ethclient.Client, registryAddr common.Address, re registryAddr: registryAddr, reg: reg, confirmations: confirmations, + logger: log.NewNoopLogger(), keys: make(map[core.NodeID][]byte), byPubkey: make(map[string]core.NodeID), } } +// SetLogger sets the cache's logger (defaults to a no-op). Call before Backfill +// or Watch; not safe to call concurrently with them. +func (c *BLSPubkeyCache) SetLogger(l log.Logger) { + if l == nil { + l = log.NewNoopLogger() + } + c.logger = l +} + // assignLocked writes (id → pubkey) into both indices. Caller MUST hold the // write lock. pubkey is expected to be exactly BLSPubkeyCacheSize bytes; the // caller already validated length. If id previously mapped to a different @@ -272,7 +285,7 @@ func (c *BLSPubkeyCache) Backfill(ctx context.Context) error { } c.mu.Unlock() - slog.Info("BLSPubkeyCache backfill complete", + c.logger.Info("BLSPubkeyCache backfill complete", "entries", c.Size(), "watermark", startBlock, "total_nodes", total.String(), @@ -344,7 +357,7 @@ func (c *BLSPubkeyCache) Watch(ctx context.Context) error { return nil case <-ticker.C: if err := c.pollOnce(ctx); err != nil { - slog.Debug("BLSPubkeyCache poll failed", "error", err) + c.logger.Debug("BLSPubkeyCache poll failed", "error", err) } } } @@ -413,7 +426,7 @@ func (c *BLSPubkeyCache) applyLog(l types.Log) { // [32..64) uint64 vestedAt (right-padded) // [64..192) uint256[4] blsPubkeyG2 — [x_im, x_re, y_im, y_re] if len(l.Data) < 192 { - slog.Warn("NodeActivated log has short data", "len", len(l.Data), "tx", l.TxHash.Hex()) + c.logger.Warn("NodeActivated log has short data", "len", len(l.Data), "tx", l.TxHash.Hex()) return } pubkey := make([]byte, BLSPubkeyCacheSize) @@ -421,7 +434,7 @@ func (c *BLSPubkeyCache) applyLog(l types.Log) { c.mu.Lock() c.assignLocked(nodeID, pubkey) c.mu.Unlock() - slog.Debug("BLSPubkeyCache: NodeActivated committed", + c.logger.Debug("BLSPubkeyCache: NodeActivated committed", "node", fmt.Sprintf("%x", nodeID[:8]), "block", l.BlockNumber, ) @@ -429,7 +442,7 @@ func (c *BLSPubkeyCache) applyLog(l types.Log) { c.mu.Lock() c.removeLocked(nodeID) c.mu.Unlock() - slog.Debug("BLSPubkeyCache: NodeReleased committed", + c.logger.Debug("BLSPubkeyCache: NodeReleased committed", "node", fmt.Sprintf("%x", nodeID[:8]), "block", l.BlockNumber, ) @@ -451,7 +464,7 @@ func (c *BLSPubkeyCache) LookupWithRefresh(ctx context.Context, id core.NodeID) } rec, err := c.reg.GetNodeById(&bind.CallOpts{Context: ctx}, [32]byte(id)) if err != nil { - slog.Debug("BLSPubkeyCache: cold-miss lookup failed", "error", err, "node", fmt.Sprintf("%x", id[:8])) + c.logger.Debug("BLSPubkeyCache: cold-miss lookup failed", "error", err, "node", fmt.Sprintf("%x", id[:8])) return nil, false } if g2Zero(rec.BlsPubkeyG2) { diff --git a/pkg/log/context.go b/pkg/log/context.go new file mode 100644 index 0000000..826092f --- /dev/null +++ b/pkg/log/context.go @@ -0,0 +1,40 @@ +package log + +import ( + "context" + + "go.opentelemetry.io/otel/trace" +) + +type contextKey struct{} + +var loggerContextKey = contextKey{} + +// SetContextLogger attaches the provided logger to the context. +// If the context contains a valid OpenTelemetry span, the logger is wrapped with a SpanLogger +// that automatically records log events to the span. If logger is nil, a NoopLogger is used. +func SetContextLogger(ctx context.Context, lg Logger) context.Context { + if lg == nil { + lg = NewNoopLogger() + } + + span := trace.SpanFromContext(ctx) + if !span.SpanContext().IsValid() { + // If there's no valid span, we don't need to create a spanLogger. + return context.WithValue(ctx, loggerContextKey, lg) + } + + ser := NewOtelSpanEventRecorder(span) + lg = NewSpanLogger(lg, ser) + + return context.WithValue(ctx, loggerContextKey, lg) +} + +// FromContext retrieves the logger stored in the context. +// If no logger is found in the context, it returns a NoopLogger as a safe default. +func FromContext(ctx context.Context) Logger { + if l, ok := ctx.Value(loggerContextKey).(Logger); ok { + return l + } + return NewNoopLogger() +} diff --git a/pkg/log/doc.go b/pkg/log/doc.go new file mode 100644 index 0000000..8c1bbaf --- /dev/null +++ b/pkg/log/doc.go @@ -0,0 +1,123 @@ +// Package log provides a structured, context-aware logging system with distributed tracing support. +// +// The package is designed around explicit dependency injection and context propagation, +// avoiding global state and encouraging clean, testable code. +// +// # Core Types +// +// The package centers around the Logger interface, which provides structured logging methods: +// +// type Logger interface { +// Debug(msg string, keysAndValues ...any) +// Info(msg string, keysAndValues ...any) +// Warn(msg string, keysAndValues ...any) +// Error(msg string, keysAndValues ...any) +// Fatal(msg string, keysAndValues ...any) +// WithKV(key string, value any) Logger +// GetAllKV() []any +// WithName(name string) Logger +// Name() string +// AddCallerSkip(skip int) Logger +// } +// +// Three implementations are provided: +// +// - ZapLogger: A production-ready logger based on Uber's zap library +// - NoopLogger: A logger that discards all messages (useful for testing) +// - SpanLogger: A decorator that records logs to both a wrapped logger and a trace span +// +// # Basic Usage +// +// Create a logger and use it directly: +// +// conf := log.Config{ +// Format: "json", +// Level: log.LevelInfo, +// Output: "stderr", +// } +// logger := log.NewZapLogger(conf) +// logger.Info("Application started", "version", "1.0.0") +// +// # Context Integration +// +// The package provides context-aware logging with automatic span integration: +// +// // Store logger in context +// ctx = log.SetContextLogger(ctx, logger) +// +// // Retrieve logger from context +// logger := log.FromContext(ctx) +// +// When SetContextLogger is called with a context containing a valid OpenTelemetry span, +// the logger is automatically wrapped with a SpanLogger that records events to both +// the logger output and the trace span. +// +// # Structured Logging +// +// All logging methods accept key-value pairs for structured data: +// +// logger.Info("User action", +// "userID", user.ID, +// "action", "login", +// "ip", request.RemoteAddr, +// ) +// +// # Logger Enrichment +// +// Create derived loggers with additional context: +// +// // Add a name hierarchy +// serviceLogger := logger.WithName("auth-service") +// +// // Add persistent key-value pairs +// userLogger := serviceLogger.WithKV("userID", userID) +// +// # OpenTelemetry Integration +// +// The package seamlessly integrates with OpenTelemetry tracing. When a logger is set +// in a context with an active span, log events are automatically recorded as span events: +// +// ctx, span := tracer.Start(ctx, "operation") +// defer span.End() +// +// ctx = log.SetContextLogger(ctx, logger) +// log.FromContext(ctx).Info("Operation started") // Recorded in both log and span +// +// Error and Fatal level logs are recorded as span errors with appropriate status codes. +// +// # Using AddCallerSkip for Helper Functions +// +// When you wrap logging calls in helper functions, use AddCallerSkip(1) to ensure +// the log output reports the correct source line from your application code, +// not the helper itself. +// +// func handleError(logger log.Logger, err error) { +// // Skip this helper frame so the log points to the real caller +// logger.AddCallerSkip(1).Error("operation failed", "err", err) +// } +// +// func doSomething(logger log.Logger) { +// err := someOperation() +// if err != nil { +// handleError(logger, err) // Log will point here, not inside handleError +// } +// } +// +// # Testing +// +// For unit tests, use NoopLogger to avoid log output: +// +// func TestSomething(t *testing.T) { +// logger := log.NewNoopLogger() +// service := NewService(logger) +// // ... test service +// } +// +// # Environment Configuration +// +// The Config struct supports environment variables: +// +// - LOG_FORMAT: Output format (console, logfmt, json) +// - LOG_LEVEL: Minimum log level (debug, info, warn, error, fatal) +// - LOG_OUTPUT: Output destination (stderr, stdout, or file path) +package log diff --git a/pkg/log/noop_logger.go b/pkg/log/noop_logger.go new file mode 100644 index 0000000..e399477 --- /dev/null +++ b/pkg/log/noop_logger.go @@ -0,0 +1,44 @@ +package log + +var _ Logger = NoopLogger{} + +// NoopLogger is a logger implementation that discards all log messages. +// It implements the Logger interface but performs no actual logging operations. +// This is useful for testing or when logging needs to be disabled. +type NoopLogger struct{} + +// NewNoopLogger creates a new NoopLogger instance. +// All logging operations on the returned logger will be silently discarded. +func NewNoopLogger() Logger { + return NoopLogger{} +} + +// Debug implements Logger.Debug but performs no operation. +func (n NoopLogger) Debug(msg string, keysAndValues ...any) {} + +// Info implements Logger.Info but performs no operation. +func (n NoopLogger) Info(msg string, keysAndValues ...any) {} + +// Warn implements Logger.Warn but performs no operation. +func (n NoopLogger) Warn(msg string, keysAndValues ...any) {} + +// Error implements Logger.Error but performs no operation. +func (n NoopLogger) Error(msg string, keysAndValues ...any) {} + +// Fatal implements Logger.Fatal but performs no operation. +func (n NoopLogger) Fatal(msg string, keysAndValues ...any) {} + +// WithKV implements Logger.WithKV but returns the same NoopLogger instance. +func (n NoopLogger) WithKV(key string, value any) Logger { return n } + +// GetAllKV implements Logger.GetAllKV and returns an empty slice. +func (n NoopLogger) GetAllKV() []any { return []any{} } + +// WithName implements Logger.WithName but returns the same NoopLogger instance. +func (n NoopLogger) WithName(name string) Logger { return n } + +// Name implements Logger.Name and always returns "noop". +func (n NoopLogger) Name() string { return "noop" } + +// AddCallerSkip implements Logger.AddCallerSkip but returns the same NoopLogger instance. +func (n NoopLogger) AddCallerSkip(skip int) Logger { return n } diff --git a/pkg/log/otel_ser.go b/pkg/log/otel_ser.go new file mode 100644 index 0000000..4029967 --- /dev/null +++ b/pkg/log/otel_ser.go @@ -0,0 +1,161 @@ +package log + +import ( + "fmt" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" +) + +var _ SpanEventRecorder = &OtelSpanEventRecorder{} + +const ( + // Used when a value is missing for a key in attribute pairs + missingAttributeValue = "MISSING" + // Used as the key when an invalid (non-string) key is encountered + invalidAttributeKey = "invalidKeysAndValues" +) + +// OtelSpanEventRecorder is a SpanEventRecorder implementation that records +// events to an OpenTelemetry span. It converts log messages and their +// associated key-value pairs into span events and attributes. +type OtelSpanEventRecorder struct { + span trace.Span +} + +// NewOtelSpanEventRecorder creates a new OtelSpanEventRecorder that will +// record events to the provided OpenTelemetry span. +func NewOtelSpanEventRecorder(span trace.Span) *OtelSpanEventRecorder { + return &OtelSpanEventRecorder{ + span: span, + } +} + +// TraceID returns the trace ID of the span as a string. +func (ser *OtelSpanEventRecorder) TraceID() string { + return ser.span.SpanContext().TraceID().String() +} + +// SpanID returns the span ID of the span as a string. +func (ser *OtelSpanEventRecorder) SpanID() string { + return ser.span.SpanContext().SpanID().String() +} + +// RecordEvent records an event to the span with the given name and attributes. +// The keysAndValues are converted to OpenTelemetry attributes. +func (ser *OtelSpanEventRecorder) RecordEvent(name string, keysAndValues ...any) { + ser.span.AddEvent(name, trace.WithAttributes(kvToOtelAttributes(keysAndValues...)...)) +} + +// RecordError records an error event to the span with the given name and attributes. +// It also sets the span status to error. +func (ser *OtelSpanEventRecorder) RecordError(name string, keysAndValues ...any) { + ser.span.AddEvent(name, trace.WithAttributes(kvToOtelAttributes(keysAndValues...)...)) + ser.span.SetStatus(codes.Error, name) +} + +func kvToOtelAttributes(keysAndValues ...any) []attribute.KeyValue { + if len(keysAndValues)%2 != 0 { + keysAndValues = append(keysAndValues, missingAttributeValue) + } + + attributes := make([]attribute.KeyValue, 0, len(keysAndValues)/2) + for i := 0; i < len(keysAndValues); i += 2 { + var key string + s, keyIsStr := keysAndValues[i].(string) + if keyIsStr { + key = s + } else { + attributes = append(attributes, attribute.String( + invalidAttributeKey, + fmt.Sprint(keysAndValues[i:]), + )) + break + } + + var keyValue attribute.KeyValue + switch v := keysAndValues[i+1].(type) { + case bool: + keyValue = attribute.Bool(key, v) + case int: + keyValue = attribute.Int(key, v) + case int16, int32, int64, uint8, uint16, uint32: + keyValue = attribute.Int64(key, toInt64(v)) + case float32, float64: + keyValue = attribute.Float64(key, toFloat64(v)) + case fmt.Stringer: + keyValue = attribute.String(key, v.String()) + default: + keyValue = attribute.String(key, fmt.Sprint(v)) + } + + attributes = append(attributes, keyValue) + } + + return attributes +} + +// toInt64 converts various integer types to int64 for use in attributes. +func toInt64(value any) int64 { + switch v := value.(type) { + case int: + return int64(v) + case int8: + return int64(v) + case int16: + return int64(v) + case int32: + return int64(v) + case int64: + return v + case uint: + return int64(v) + case uint8: + return int64(v) + case uint16: + return int64(v) + case uint32: + return int64(v) + case uint64: + return int64(v) + case float32: + return int64(v) + case float64: + return int64(v) + default: + return 0 + } +} + +// toFloat64 converts various float types to float64 for use in attributes. +func toFloat64(value any) float64 { + switch v := value.(type) { + case int: + return float64(v) + case int8: + return float64(v) + case int16: + return float64(v) + case int32: + return float64(v) + case int64: + return float64(v) + case uint: + return float64(v) + case uint8: + return float64(v) + case uint16: + return float64(v) + case uint32: + return float64(v) + case uint64: + return float64(v) + case float32: + return float64(v) + case float64: + return v + default: + return 0 + } +} diff --git a/pkg/log/span_logger.go b/pkg/log/span_logger.go new file mode 100644 index 0000000..cfef89a --- /dev/null +++ b/pkg/log/span_logger.go @@ -0,0 +1,123 @@ +package log + +var _ Logger = SpanLogger{} + +// SpanLogger is a logger that wraps another logger and additionally records +// log events to a span using a SpanEventRecorder. +// This allows log messages to be correlated with distributed traces. +type SpanLogger struct { + lg Logger + ser SpanEventRecorder +} + +// NewSpanLogger creates a new SpanLogger that wraps the provided logger +// and records events to the given SpanEventRecorder. +// The wrapped logger's caller skip is incremented by 1 to account for the SpanLogger wrapper. +func NewSpanLogger(lg Logger, ser SpanEventRecorder) Logger { + return &SpanLogger{ + lg: lg.AddCallerSkip(1), // Skip the spanLogger's own call stack frame + ser: ser, + } +} + +// Debug logs a debug message to both the wrapped logger and the span. +func (sl SpanLogger) Debug(msg string, keysAndValues ...any) { + sl.ser.RecordEvent(msg, sl.withLogContext(LevelDebug, keysAndValues)...) + sl.lg.Debug(msg, sl.withTraceContext(keysAndValues)...) +} + +// Info logs an info message to both the wrapped logger and the span. +func (sl SpanLogger) Info(msg string, keysAndValues ...any) { + sl.ser.RecordEvent(msg, sl.withLogContext(LevelInfo, keysAndValues)...) + sl.lg.Info(msg, sl.withTraceContext(keysAndValues)...) +} + +// Warn logs a warning message to both the wrapped logger and the span. +func (sl SpanLogger) Warn(msg string, keysAndValues ...any) { + sl.ser.RecordEvent(msg, sl.withLogContext(LevelWarn, keysAndValues)...) + sl.lg.Warn(msg, sl.withTraceContext(keysAndValues)...) +} + +// Error logs an error message to both the wrapped logger and the span. +// The error is recorded as an error event in the span. +func (sl SpanLogger) Error(msg string, keysAndValues ...any) { + sl.ser.RecordError(msg, sl.withLogContext(LevelError, keysAndValues)...) + sl.lg.Error(msg, sl.withTraceContext(keysAndValues)...) +} + +// Fatal logs a fatal message to both the wrapped logger and the span. +// The error is recorded as an error event in the span. +func (sl SpanLogger) Fatal(msg string, keysAndValues ...any) { + sl.ser.RecordError(msg, sl.withLogContext(LevelFatal, keysAndValues)...) + sl.lg.Fatal(msg, sl.withTraceContext(keysAndValues)...) +} + +// WithKV returns a new SpanLogger with the key-value pair added to the wrapped logger. +// The SpanEventRecorder remains the same. +func (sl SpanLogger) WithKV(key string, value any) Logger { + return SpanLogger{ + lg: sl.lg.WithKV(key, value), + ser: sl.ser, + } +} + +// GetAllKV returns all key-value pairs from the wrapped logger. +func (sl SpanLogger) GetAllKV() []any { + return sl.lg.GetAllKV() +} + +// WithName returns a new SpanLogger with the given name set on the wrapped logger. +// The SpanEventRecorder remains the same. +func (sl SpanLogger) WithName(name string) Logger { + return SpanLogger{ + lg: sl.lg.WithName(name), + ser: sl.ser, + } +} + +// Name returns the name of the wrapped logger. +func (sl SpanLogger) Name() string { + return sl.lg.Name() +} + +// AddCallerSkip returns a new SpanLogger with increased caller skip on the wrapped logger. +func (sl SpanLogger) AddCallerSkip(skip int) Logger { + return SpanLogger{ + lg: sl.lg.AddCallerSkip(skip), + ser: sl.ser, + } +} + +// withTraceContext appends the current trace and span IDs to the provided slice of key-value pairs. +// It returns a new slice containing the trace information followed by the original keys and values. +// This is useful for ensuring that log entries include trace context for distributed tracing. +func (sl SpanLogger) withTraceContext(keysAndValues []any) []any { + logKeysAndValues := append([]any{ + "traceId", sl.ser.TraceID(), + "spanId", sl.ser.SpanID(), + }, keysAndValues...) + + return logKeysAndValues +} + +// withLogContext constructs a combined slice of key-value pairs for logging. +// It prepends the log level and component name, appends all key-value pairs +// from the underlying logger, and finally appends any additional key-value +// pairs provided as input. This ensures that all log entries include consistent +// context information. +// +// Parameters: +// - level: The log level to associate with the log entry. +// - keysAndValues: Additional key-value pairs to include in the log entry. +// +// Returns: +// - A slice of key-value pairs representing the complete log context. +func (sl SpanLogger) withLogContext(level Level, keysAndValues []any) []any { + fullKeysAndValues := append([]any{ + "level", string(level), + "component", sl.lg.Name(), + }, sl.lg.GetAllKV()...) + fullKeysAndValues = append(fullKeysAndValues, keysAndValues...) + + return fullKeysAndValues +} diff --git a/pkg/log/types.go b/pkg/log/types.go new file mode 100644 index 0000000..0b9b084 --- /dev/null +++ b/pkg/log/types.go @@ -0,0 +1,71 @@ +package log + +// Logger is a logger interface. +type Logger interface { + // Debug logs a message for low-level debugging. + // Use for detailed information useful during development. + // keysAndValues lets you add structured context (e.g., "user", id). + Debug(msg string, keysAndValues ...any) + // Info logs general information about application progress. + // Use for routine events or state changes. + // keysAndValues lets you add structured context (e.g., "module", name). + Info(msg string, keysAndValues ...any) + // Warn logs a message for unexpected situations that aren't errors. + // Use when something might be wrong but the app can continue. + // keysAndValues lets you add structured context (e.g., "attempt", n). + Warn(msg string, keysAndValues ...any) + // Error logs an error that prevents normal operation. + // Use for failures or problems that need attention. + // keysAndValues lets you add structured context (e.g., "error", err). + Error(msg string, keysAndValues ...any) + // Fatal logs a critical error and may terminate the program. + // Use for unrecoverable failures. + // keysAndValues lets you add structured context (e.g., "reason", reason). + Fatal(msg string, keysAndValues ...any) + // WithKV returns a logger with an extra key-value pair for all future logs. + // Use to add persistent context (e.g., component, request ID). + WithKV(key string, value any) Logger + // GetAllKV returns all persistent key-value pairs for this logger. + // Use to inspect logger context. + GetAllKV() []any + // WithName returns a logger with a specific name (e.g., module or component). + // Use to identify the source of logs. + WithName(name string) Logger + // Name returns the logger's name. + Name() string + // AddCallerSkip returns a logger that skips extra stack frames when reporting log source. + // Use when wrapping the logger in helpers; returns itself if unsupported. + AddCallerSkip(skip int) Logger +} + +// Level represents the severity level of a log message. +// It can be used to filter log output based on importance. +type Level string + +const ( + // LevelDebug is the most verbose level, used for debugging purposes. + LevelDebug Level = "debug" + // LevelInfo is used for informational messages. + LevelInfo Level = "info" + // LevelWarn is used for warning messages that indicate potential issues. + LevelWarn Level = "warn" + // LevelError is used for error messages that indicate something went wrong. + LevelError Level = "error" + // LevelFatal is used for fatal errors that typically cause the program to exit. + LevelFatal Level = "fatal" +) + +// SpanEventRecorder is an interface for recording events and errors to a span. +type SpanEventRecorder interface { + // TraceID returns the trace ID of the span. + TraceID() string + // SpanID returns the span ID of the span. + SpanID() string + + // RecordEvent records an event to the span. + // keysAndValues are treated as key-value pairs (e.g., "key1", value1, "key2", value2). + RecordEvent(name string, keysAndValues ...any) + // RecordError records an error to the span. + // keysAndValues are treated as key-value pairs (e.g., "key1", value1, "key2", value2). + RecordError(name string, keysAndValues ...any) +} diff --git a/pkg/log/zap_logger.go b/pkg/log/zap_logger.go new file mode 100644 index 0000000..dfd4038 --- /dev/null +++ b/pkg/log/zap_logger.go @@ -0,0 +1,163 @@ +package log + +import ( + "os" + "path/filepath" + "time" + + zaplogfmt "github.com/jsternberg/zap-logfmt" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +var _ Logger = &ZapLogger{} + +// ZapLogger is a logger implementation backed by Uber's zap logger. +// It provides structured logging with high performance and supports +// various output formats and destinations. +type ZapLogger struct { + lg *zap.SugaredLogger + keysAndValues []any +} + +// Config is used to configure the ZapLogger. +// It supports environment variable configuration with default values. +type Config struct { + Format string `env:"LOG_FORMAT" env-default:"console"` // console, logfmt or json + Level Level `env:"LOG_LEVEL" env-default:"info"` // debug, info, warn, error, fatal, trace + Output string `env:"LOG_OUTPUT" env-default:"stderr"` // stderr, stdout or file path +} + +// NewZapLogger creates a new ZapLogger with the given configuration. +// It supports multiple output formats (console, logfmt, json) and destinations (stderr, stdout, file). +// Additional write syncers can be provided to write logs to multiple destinations. +func NewZapLogger(conf Config, extraWriters ...zapcore.WriteSyncer) Logger { + // Create a production encoder config and customize time format. + encCfg := zap.NewProductionEncoderConfig() + encCfg.EncodeTime = func(ts time.Time, encoder zapcore.PrimitiveArrayEncoder) { + encoder.AppendString(ts.UTC().Format(time.RFC3339)) + } + + // Choose the encoder based on the config. + var encoder zapcore.Encoder + switch conf.Format { + case "logfmt": + encoder = zaplogfmt.NewEncoder(encCfg) + case "json": + encoder = zapcore.NewJSONEncoder(encCfg) + default: + encoder = zapcore.NewConsoleEncoder(encCfg) + } + + var ws zapcore.WriteSyncer + if conf.Output == "" || conf.Output == "stderr" { + ws = zapcore.Lock(os.Stderr) + } else if conf.Output == "stdout" { + ws = zapcore.Lock(os.Stdout) + } else { + dir := filepath.Dir(conf.Output) + err1 := os.MkdirAll(dir, 0755) // 0755 gives read/write/execute permissions to the owner, and read/execute permissions to others + + // Open the specified file; fallback to stderr on error. + file, err2 := os.OpenFile(conf.Output, os.O_RDWR|os.O_CREATE, 0666) + if err1 != nil || err2 != nil { + ws = zapcore.Lock(os.Stdout) + } else { + ws = zapcore.AddSync(file) + } + } + wss := zapcore.NewMultiWriteSyncer(append(extraWriters, ws)...) + + // Build the core. + core := zapcore.NewCore(encoder, wss, toZapLogLevel(conf.Level)) + // Create a SugaredLogger; AddCallerSkip(2) skips wrapper methods in the call stack. + zl := zap.New(core, zap.AddCaller(), zap.AddCallerSkip(2)).Sugar() + + return &ZapLogger{ + lg: zl, + } +} + +// Debug logs a message at debug level. +func (l *ZapLogger) Debug(msg string, keysAndValues ...any) { + l.log(LevelDebug, msg, keysAndValues...) +} + +// Info logs a message at info level. +func (l *ZapLogger) Info(msg string, keysAndValues ...any) { + l.log(LevelInfo, msg, keysAndValues...) +} + +// Warn logs a message at warn level. +func (l *ZapLogger) Warn(msg string, keysAndValues ...any) { + l.log(LevelWarn, msg, keysAndValues...) +} + +// Error logs a message at error level. +func (l *ZapLogger) Error(msg string, keysAndValues ...any) { + l.log(LevelError, msg, keysAndValues...) +} + +// Fatal logs a message at fatal level. +func (l *ZapLogger) Fatal(msg string, keysAndValues ...any) { + l.log(LevelFatal, msg, keysAndValues...) +} + +func (l *ZapLogger) log(level Level, msg string, keysAndValues ...any) { + l.lg.Logw(toZapLogLevel(level), msg, keysAndValues...) +} + +// WithKV returns a new ZapLogger with the key-value pair added to all future log messages. +func (l *ZapLogger) WithKV(key string, value any) Logger { + return &ZapLogger{ + lg: l.lg.With(key, value), + keysAndValues: append(l.keysAndValues, key, value), + } +} + +// GetAllKV returns all key-value pairs that have been added to this logger instance. +func (l *ZapLogger) GetAllKV() []any { + return l.keysAndValues +} + +// WithName returns a new ZapLogger with the given name. +// The name is added to the logger hierarchy separated by dots. +func (l *ZapLogger) WithName(name string) Logger { + return &ZapLogger{ + lg: l.lg.Named(name), + keysAndValues: l.keysAndValues, + } +} + +// Name returns the current name of the logger. +func (l *ZapLogger) Name() string { + return l.lg.Desugar().Name() +} + +// AddCallerSkip returns a new ZapLogger that skips additional stack frames when determining the caller. +func (l *ZapLogger) AddCallerSkip(skip int) Logger { + return &ZapLogger{ + lg: l.lg.WithOptions(zap.AddCallerSkip(skip)), + keysAndValues: l.keysAndValues, + } +} + +func toZapLogLevel(logLevel Level) zapcore.Level { + var zapLevel zapcore.Level + switch logLevel { + case LevelDebug: + zapLevel = zapcore.DebugLevel + case LevelInfo: + zapLevel = zapcore.InfoLevel + case LevelWarn: + zapLevel = zapcore.WarnLevel + case LevelError: + zapLevel = zapcore.ErrorLevel + case LevelFatal: + zapLevel = zapcore.FatalLevel + default: + zapLevel = zapcore.InfoLevel + } + + return zapLevel +} diff --git a/pkg/p2p/auth/server.go b/pkg/p2p/auth/server.go index 043cc01..7491637 100644 --- a/pkg/p2p/auth/server.go +++ b/pkg/p2p/auth/server.go @@ -4,7 +4,6 @@ import ( "crypto/rand" "fmt" "io" - "log/slog" "strings" "github.com/ethereum/go-ethereum/common" @@ -15,6 +14,7 @@ import ( "github.com/libp2p/go-libp2p/core/protocol" "github.com/layer-3/clearnet-sdk/pkg/cborx" + "github.com/layer-3/clearnet-sdk/pkg/log" p2pproto "github.com/layer-3/clearnet-sdk/pkg/p2p/protocol" ) @@ -62,7 +62,7 @@ func (a AllowList) permits(addr common.Address) bool { type Server struct { allow AllowList // normalized onAuth func(network.Conn, Result) - logger *slog.Logger + logger log.Logger } var _ p2pproto.Registrar = (*Server)(nil) @@ -73,18 +73,18 @@ var _ p2pproto.Registrar = (*Server)(nil) // in its world (e.g. marking the connection so receipt streams pass their // gate). The connection is passed (not just the peer ID) so the caller can key // auth state per-connection, matching libp2p's connection lifetime. -func NewServer(allow AllowList, onAuth func(network.Conn, Result), logger *slog.Logger) *Server { +func NewServer(allow AllowList, onAuth func(network.Conn, Result), logger log.Logger) *Server { if logger == nil { - logger = slog.Default() + logger = log.NewNoopLogger() } - log := logger.With("component", "p2p-auth-server", "protocol", p2pproto.ProtocolAuth) + lg := logger.WithName("p2p-auth-server").WithKV("protocol", p2pproto.ProtocolAuth) clean := allow.normalize() if len(allow) > 0 && len(clean) == 0 { - log.Error("auth: every allow-list entry is malformed; gate is EMPTY and bypassed", "raw_entries", len(allow)) + lg.Error("auth: every allow-list entry is malformed; gate is EMPTY and bypassed", "raw_entries", len(allow)) } else if len(allow) > len(clean) { - log.Warn("auth: dropped malformed allow-list entries", "raw", len(allow), "accepted", len(clean)) + lg.Warn("auth: dropped malformed allow-list entries", "raw", len(allow), "accepted", len(clean)) } - return &Server{allow: clean, onAuth: onAuth, logger: log} + return &Server{allow: clean, onAuth: onAuth, logger: lg} } // Register installs the auth stream handler on h. diff --git a/pkg/p2p/gossip/follower.go b/pkg/p2p/gossip/follower.go index f16f46b..048f338 100644 --- a/pkg/p2p/gossip/follower.go +++ b/pkg/p2p/gossip/follower.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "fmt" - "log/slog" "sync" pubsub "github.com/libp2p/go-libp2p-pubsub" @@ -12,6 +11,7 @@ import ( "github.com/libp2p/go-libp2p/core/peer" "github.com/layer-3/clearnet-sdk/pkg/cborx" + "github.com/layer-3/clearnet-sdk/pkg/log" ) // Metrics captures Follower counters for ops dashboards. @@ -42,7 +42,7 @@ type Follower[T any, M message[T]] struct { sub *pubsub.Subscription topic *pubsub.Topic name string - logger *slog.Logger + logger log.Logger metrics *Metrics handlerMu sync.RWMutex @@ -54,9 +54,9 @@ type Follower[T any, M message[T]] struct { // reach the consume loop. Call Run to start consuming; register a handler with // SetHandler before (or shortly after) Run. The caller owns h and its // connectivity. -func NewFollower[T any, M message[T]](ctx context.Context, h host.Host, topic string, logger *slog.Logger) (*Follower[T, M], error) { +func NewFollower[T any, M message[T]](ctx context.Context, h host.Host, topic string, logger log.Logger) (*Follower[T, M], error) { if logger == nil { - logger = slog.Default() + logger = log.NewNoopLogger() } ps, err := pubsub.NewGossipSub(ctx, h) if err != nil { @@ -65,7 +65,7 @@ func NewFollower[T any, M message[T]](ctx context.Context, h host.Host, topic st f := &Follower[T, M]{ host: h, name: topic, - logger: logger.With("component", "p2p-gossip-follower", "topic", topic), + logger: logger.WithName("p2p-gossip-follower").WithKV("topic", topic), metrics: &Metrics{}, } if err := ps.RegisterTopicValidator(topic, f.validateSize); err != nil { diff --git a/pkg/p2p/gossip/publisher.go b/pkg/p2p/gossip/publisher.go index bc86028..d29f882 100644 --- a/pkg/p2p/gossip/publisher.go +++ b/pkg/p2p/gossip/publisher.go @@ -4,13 +4,13 @@ import ( "bytes" "context" "fmt" - "log/slog" "time" pubsub "github.com/libp2p/go-libp2p-pubsub" "github.com/libp2p/go-libp2p/core/host" "github.com/layer-3/clearnet-sdk/pkg/cborx" + "github.com/layer-3/clearnet-sdk/pkg/log" ) // Publisher joins a GossipSub topic and publishes values of type T. It does not @@ -19,14 +19,14 @@ import ( type Publisher[T any, M message[T]] struct { topic *pubsub.Topic name string - logger *slog.Logger + logger log.Logger } // NewPublisher joins topic on h. The caller owns h and must keep it alive for // the Publisher's lifetime. -func NewPublisher[T any, M message[T]](ctx context.Context, h host.Host, topic string, logger *slog.Logger) (*Publisher[T, M], error) { +func NewPublisher[T any, M message[T]](ctx context.Context, h host.Host, topic string, logger log.Logger) (*Publisher[T, M], error) { if logger == nil { - logger = slog.Default() + logger = log.NewNoopLogger() } ps, err := pubsub.NewGossipSub(ctx, h) if err != nil { @@ -39,7 +39,7 @@ func NewPublisher[T any, M message[T]](ctx context.Context, h host.Host, topic s return &Publisher[T, M]{ topic: t, name: topic, - logger: logger.With("component", "p2p-gossip-publisher", "topic", topic), + logger: logger.WithName("p2p-gossip-publisher").WithKV("topic", topic), }, nil } diff --git a/pkg/p2p/pubsub/follower.go b/pkg/p2p/pubsub/follower.go index abb413d..0b83ede 100644 --- a/pkg/p2p/pubsub/follower.go +++ b/pkg/p2p/pubsub/follower.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "fmt" - "log/slog" "sync" pubsub "github.com/libp2p/go-libp2p-pubsub" @@ -13,6 +12,7 @@ import ( "github.com/layer-3/clearnet-sdk/pkg/cborx" "github.com/layer-3/clearnet-sdk/pkg/core" + "github.com/layer-3/clearnet-sdk/pkg/log" ) // maxFinalizedWithdrawalBytes caps the raw size of an inbound message before @@ -58,7 +58,7 @@ type Follower struct { sub *pubsub.Subscription topic *pubsub.Topic name string - logger *slog.Logger + logger log.Logger metrics *Metrics handlerMu sync.RWMutex @@ -70,9 +70,9 @@ type Follower struct { // reach the consume loop. Call Run to start consuming; register a handler with // SetHandler before (or shortly after) Run — messages arriving with no handler // are dropped with a warning. The caller owns h and its connectivity. -func NewFollower(ctx context.Context, h host.Host, topic string, logger *slog.Logger) (*Follower, error) { +func NewFollower(ctx context.Context, h host.Host, topic string, logger log.Logger) (*Follower, error) { if logger == nil { - logger = slog.Default() + logger = log.NewNoopLogger() } ps, err := pubsub.NewGossipSub(ctx, h) if err != nil { @@ -81,7 +81,7 @@ func NewFollower(ctx context.Context, h host.Host, topic string, logger *slog.Lo f := &Follower{ host: h, name: topic, - logger: logger.With("component", "p2p-pubsub-follower", "topic", topic), + logger: logger.WithName("p2p-pubsub-follower").WithKV("topic", topic), metrics: &Metrics{}, } // Register the validator before Join so it is attached from the first diff --git a/pkg/p2p/pubsub/publisher.go b/pkg/p2p/pubsub/publisher.go index d492b71..45bdbaa 100644 --- a/pkg/p2p/pubsub/publisher.go +++ b/pkg/p2p/pubsub/publisher.go @@ -13,7 +13,6 @@ import ( "bytes" "context" "fmt" - "log/slog" "time" pubsub "github.com/libp2p/go-libp2p-pubsub" @@ -21,6 +20,7 @@ import ( "github.com/layer-3/clearnet-sdk/pkg/cborx" "github.com/layer-3/clearnet-sdk/pkg/core" + "github.com/layer-3/clearnet-sdk/pkg/log" ) // Publisher joins a GossipSub topic and publishes typed payloads to it. It does @@ -30,14 +30,14 @@ type Publisher struct { host host.Host topic *pubsub.Topic name string - logger *slog.Logger + logger log.Logger } // NewPublisher joins topic on h and returns a Publisher. The caller owns h and // must keep it alive for the Publisher's lifetime. -func NewPublisher(ctx context.Context, h host.Host, topic string, logger *slog.Logger) (*Publisher, error) { +func NewPublisher(ctx context.Context, h host.Host, topic string, logger log.Logger) (*Publisher, error) { if logger == nil { - logger = slog.Default() + logger = log.NewNoopLogger() } ps, err := pubsub.NewGossipSub(ctx, h) if err != nil { @@ -51,7 +51,7 @@ func NewPublisher(ctx context.Context, h host.Host, topic string, logger *slog.L host: h, topic: t, name: topic, - logger: logger.With("component", "p2p-pubsub-publisher", "topic", topic), + logger: logger.WithName("p2p-pubsub-publisher").WithKV("topic", topic), }, nil } diff --git a/pkg/p2p/receipt/client.go b/pkg/p2p/receipt/client.go index ea92c85..91c9c31 100644 --- a/pkg/p2p/receipt/client.go +++ b/pkg/p2p/receipt/client.go @@ -5,7 +5,6 @@ import ( "context" "fmt" "io" - "log/slog" "time" "github.com/libp2p/go-libp2p/core/host" @@ -16,6 +15,7 @@ import ( "github.com/layer-3/clearnet-sdk/pkg/cborx" "github.com/layer-3/clearnet-sdk/pkg/core" + "github.com/layer-3/clearnet-sdk/pkg/log" p2pproto "github.com/layer-3/clearnet-sdk/pkg/p2p/protocol" ) @@ -29,19 +29,19 @@ type Client struct { host host.Host peerID peer.ID timeout time.Duration - logger *slog.Logger + logger log.Logger } // NewClient creates a Client that submits to peerID over h. -func NewClient(h host.Host, peerID peer.ID, logger *slog.Logger) *Client { +func NewClient(h host.Host, peerID peer.ID, logger log.Logger) *Client { if logger == nil { - logger = slog.Default() + logger = log.NewNoopLogger() } return &Client{ host: h, peerID: peerID, timeout: defaultTimeout, - logger: logger.With("component", "p2p-receipt-client"), + logger: logger.WithName("p2p-receipt-client"), } } diff --git a/pkg/p2p/receipt/server.go b/pkg/p2p/receipt/server.go index cd73f71..ad84a69 100644 --- a/pkg/p2p/receipt/server.go +++ b/pkg/p2p/receipt/server.go @@ -18,7 +18,6 @@ import ( "context" "fmt" "io" - "log/slog" "time" "github.com/libp2p/go-libp2p/core/host" @@ -27,6 +26,7 @@ import ( "github.com/layer-3/clearnet-sdk/pkg/cborx" "github.com/layer-3/clearnet-sdk/pkg/core" + "github.com/layer-3/clearnet-sdk/pkg/log" p2pproto "github.com/layer-3/clearnet-sdk/pkg/p2p/protocol" ) @@ -42,17 +42,17 @@ const ( // receipt to a ReceiptHandler. type Server struct { handler ReceiptHandler - logger *slog.Logger + logger log.Logger } var _ p2pproto.Registrar = (*Server)(nil) // NewServer returns a Server that delegates to handler. -func NewServer(handler ReceiptHandler, logger *slog.Logger) *Server { +func NewServer(handler ReceiptHandler, logger log.Logger) *Server { if logger == nil { - logger = slog.Default() + logger = log.NewNoopLogger() } - return &Server{handler: handler, logger: logger.With("component", "p2p-receipt-server")} + return &Server{handler: handler, logger: logger.WithName("p2p-receipt-server")} } // Register installs both receipt stream handlers on h. @@ -94,31 +94,31 @@ func (s *Server) serve( dispatch func(context.Context, io.Reader) (p2pproto.ReceiptAck, error), ) { defer stream.Close() - log := s.logger.With("protocol", proto) + lg := s.logger.WithKV("protocol", proto) ctx, cancel := context.WithTimeout(context.Background(), streamReadDeadline) defer cancel() if err := stream.SetReadDeadline(time.Now().Add(streamReadDeadline)); err != nil { - log.Warn("set read deadline failed", "error", err) + lg.Warn("set read deadline failed", "error", err) return } ack, err := dispatch(ctx, io.LimitReader(stream, maxReceiptBytes)) if err != nil { - log.Warn("handler error", "error", err) - writeAck(stream, p2pproto.ReceiptAck{Accepted: false, Reason: err.Error()}, log) + lg.Warn("handler error", "error", err) + writeAck(stream, p2pproto.ReceiptAck{Accepted: false, Reason: err.Error()}, lg) return } - writeAck(stream, ack, log) + writeAck(stream, ack, lg) } -func writeAck(stream network.Stream, ack p2pproto.ReceiptAck, log *slog.Logger) { +func writeAck(stream network.Stream, ack p2pproto.ReceiptAck, logger log.Logger) { var buf bytes.Buffer if err := cborx.WriteFrame(&buf, cborx.V1, &ack); err != nil { - log.Warn("encode ack failed", "error", err) + logger.Warn("encode ack failed", "error", err) return } if _, err := stream.Write(buf.Bytes()); err != nil { - log.Warn("write ack failed", "error", err) + logger.Warn("write ack failed", "error", err) } } From 5f189df3498ad2f1616596634a153a4ea928c140 Mon Sep 17 00:00:00 2001 From: Anton Filonenko Date: Wed, 17 Jun 2026 14:11:01 +0300 Subject: [PATCH 08/15] feat(blockchain): mark BTC rotation sweep with OP_RETURN(opID) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The BTC rotation sweep now emits the watcher-recognizable wire: output 0 pays the new vault, the final output is a zero-value OP_RETURN carrying the rotation's operation id. A watcher can attribute the landed sweep to a specific rotation by that marker and pivot the vault — a single unmarked output could not be distinguished from any other vault spend. SignerRotationFinalizer.Pack/Validate take an opID [32]byte. BTC embeds it in the sweep; EVM, XRPL, and Solana bind rotation replay on-chain (signerNonce / account sequence / program nonce) and accept opID only to keep one uniform signature. VerifyRotation stays keyed on the new signer set, so it remains a direct state read on every chain. --- pkg/blockchain/btc/rotation_finalizer.go | 47 +++++++---- pkg/blockchain/btc/rotation_finalizer_test.go | 83 +++++++++++++++++++ pkg/blockchain/btc/vault_integration_test.go | 6 +- pkg/blockchain/evm/rotation_finalizer.go | 6 +- pkg/blockchain/evm/vault_integration_test.go | 6 +- pkg/blockchain/sol/rotation_finalizer.go | 8 +- pkg/blockchain/sol/vault_integration_test.go | 6 +- pkg/blockchain/xrpl/rotation_finalizer.go | 8 +- pkg/blockchain/xrpl/vault_integration_test.go | 6 +- pkg/core/blockchain.go | 15 +++- 10 files changed, 158 insertions(+), 33 deletions(-) create mode 100644 pkg/blockchain/btc/rotation_finalizer_test.go diff --git a/pkg/blockchain/btc/rotation_finalizer.go b/pkg/blockchain/btc/rotation_finalizer.go index f27ac3e..c710aac 100644 --- a/pkg/blockchain/btc/rotation_finalizer.go +++ b/pkg/blockchain/btc/rotation_finalizer.go @@ -108,8 +108,10 @@ func (f *RotationFinalizer) newVaultAddress(newSigners []string, newThreshold in } // Pack lists every current-vault UTXO and builds the unsigned sweep: all of them -// as inputs, a single output paying the new vault the total minus fee. -func (f *RotationFinalizer) Pack(ctx context.Context, newSigners []string, newThreshold int) ([]byte, error) { +// as inputs, output 0 paying the new vault the total minus fee, and a final +// zero-value OP_RETURN carrying opID so an external watcher can attribute the +// landed sweep to this rotation (and pivot the vault). +func (f *RotationFinalizer) Pack(ctx context.Context, opID [32]byte, newSigners []string, newThreshold int) ([]byte, error) { cur, err := f.currentVault(ctx) if err != nil { return nil, err @@ -133,7 +135,7 @@ func (f *RotationFinalizer) Pack(ctx context.Context, newSigners []string, newTh if err != nil { return nil, fmt.Errorf("btc: estimate fee: %w", err) } - tx, err := buildSweepTx(utxos, newVault, feeRate) + tx, err := buildSweepTx(utxos, newVault, opID, feeRate) if err != nil { return nil, err } @@ -141,9 +143,10 @@ func (f *RotationFinalizer) Pack(ctx context.Context, newSigners []string, newTh } // Validate re-derives the new vault and asserts the packed sweep pays exactly it -// from a single output, consumes only current-vault UTXOs, and keeps the implied -// fee within the ceiling. -func (f *RotationFinalizer) Validate(ctx context.Context, packed []byte, newSigners []string, newThreshold int) error { +// from output 0, carries the OP_RETURN(opID) marker as its final output, +// consumes only current-vault UTXOs, and keeps the implied fee within the +// ceiling. +func (f *RotationFinalizer) Validate(ctx context.Context, opID [32]byte, packed []byte, newSigners []string, newThreshold int) error { _, newVaultScript, _, err := f.newVaultAddress(newSigners, newThreshold) if err != nil { return err @@ -152,11 +155,18 @@ func (f *RotationFinalizer) Validate(ctx context.Context, packed []byte, newSign if err != nil { return fmt.Errorf("btc rotation validate: %w", err) } - if len(tx.TxOut) != 1 { - return fmt.Errorf("btc rotation validate: expected 1 output, got %d", len(tx.TxOut)) + if len(tx.TxOut) != 2 { + return fmt.Errorf("btc rotation validate: expected 2 outputs (new vault + OP_RETURN), got %d", len(tx.TxOut)) } if !bytes.Equal(tx.TxOut[0].PkScript, newVaultScript) { - return fmt.Errorf("btc rotation validate: output not paid to the new vault") + return fmt.Errorf("btc rotation validate: output 0 not paid to the new vault") + } + wantMarker, err := txscript.NullDataScript(opID[:]) + if err != nil { + return fmt.Errorf("btc rotation validate: opID marker script: %w", err) + } + if tx.TxOut[1].Value != 0 || !bytes.Equal(tx.TxOut[1].PkScript, wantMarker) { + return fmt.Errorf("btc rotation validate: final output is not OP_RETURN(opID)") } cur, err := f.currentVault(ctx) if err != nil { @@ -170,7 +180,7 @@ func (f *RotationFinalizer) Validate(ctx context.Context, packed []byte, newSign if fee < 0 { return fmt.Errorf("btc rotation validate: output exceeds inputs (fee %d)", fee) } - if cap := EstimateFeeSats(len(tx.TxIn), 1, f.cfg.FeeCapSatPerVByte); f.cfg.FeeCapSatPerVByte > 0 && fee > cap { + if cap := EstimateFeeSats(len(tx.TxIn), 2, f.cfg.FeeCapSatPerVByte); f.cfg.FeeCapSatPerVByte > 0 && fee > cap { return fmt.Errorf("btc rotation validate: fee %d exceeds ceiling %d", fee, cap) } return nil @@ -236,9 +246,12 @@ func (f *RotationFinalizer) VerifyRotation(ctx context.Context, newSigners []str return [32]byte{}, true, nil } -// buildSweepTx builds the unsigned sweep: every UTXO as an input, a single -// output paying newVault the total minus the estimated fee. -func buildSweepTx(utxos []UTXO, newVault btcutil.Address, feeRate int64) (*wire.MsgTx, error) { +// buildSweepTx builds the unsigned sweep: every UTXO as an input, output 0 +// paying newVault the total minus the estimated fee, and a final zero-value +// OP_RETURN carrying opID (the rotation marker). The two-output shape — vault as +// output 0 plus the OP_RETURN — is what a watcher matches to attribute the +// landed sweep to this rotation. +func buildSweepTx(utxos []UTXO, newVault btcutil.Address, opID [32]byte, feeRate int64) (*wire.MsgTx, error) { ordered := make([]UTXO, len(utxos)) copy(ordered, utxos) sort.Slice(ordered, func(i, j int) bool { @@ -255,7 +268,7 @@ func buildSweepTx(utxos []UTXO, newVault btcutil.Address, feeRate int64) (*wire. tx.AddTxIn(wire.NewTxIn(op, nil, nil)) total += u.Amount } - fee := EstimateFeeSats(len(ordered), 1, feeRate) + fee := EstimateFeeSats(len(ordered), 2, feeRate) out := total - fee if out < dustThresholdSats { return nil, fmt.Errorf("btc: sweep output %d below dust after fee %d (total %d)", out, fee, total) @@ -265,6 +278,12 @@ func buildSweepTx(utxos []UTXO, newVault btcutil.Address, feeRate int64) (*wire. return nil, fmt.Errorf("btc: new vault script: %w", err) } tx.AddTxOut(wire.NewTxOut(out, script)) + + marker, err := txscript.NullDataScript(opID[:]) + if err != nil { + return nil, fmt.Errorf("btc: opID OP_RETURN script: %w", err) + } + tx.AddTxOut(wire.NewTxOut(0, marker)) return tx, nil } diff --git a/pkg/blockchain/btc/rotation_finalizer_test.go b/pkg/blockchain/btc/rotation_finalizer_test.go new file mode 100644 index 0000000..a9d65e5 --- /dev/null +++ b/pkg/blockchain/btc/rotation_finalizer_test.go @@ -0,0 +1,83 @@ +package btc + +import ( + "bytes" + "testing" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/layer-3/clearnet-sdk/pkg/sign" +) + +// TestBuildSweepTx_OpReturnMarker pins the rotation sweep wire a watcher matches +// on: output 0 pays the new vault, the final output is a zero-value OP_RETURN +// carrying opID. (Devnet-free; the full flow is exercised by the integration +// test.) +func TestBuildSweepTx_OpReturnMarker(t *testing.T) { + net := &chaincfg.RegressionNetParams + + pubs := make([][]byte, 2) + for i := range pubs { + k, err := crypto.GenerateKey() + if err != nil { + t.Fatalf("gen key: %v", err) + } + pubs[i] = sign.NewKeySignerFromECDSA(k).PublicKey() + } + redeem, err := RedeemScript(2, pubs) + if err != nil { + t.Fatalf("RedeemScript: %v", err) + } + vault, err := VaultAddress(redeem, net) + if err != nil { + t.Fatalf("VaultAddress: %v", err) + } + + var txid chainhash.Hash + txid[0] = 0x11 + utxos := []UTXO{{TxID: txid, Vout: 0, Amount: 1_000_000}} + + var opID [32]byte + opID[0], opID[31] = 0xAB, 0xCD + + tx, err := buildSweepTx(utxos, vault, opID, 5) + if err != nil { + t.Fatalf("buildSweepTx: %v", err) + } + + if len(tx.TxOut) != 2 { + t.Fatalf("outputs = %d, want 2 (vault + OP_RETURN)", len(tx.TxOut)) + } + + // Output 0: the new vault, total minus fee. + vaultScript, err := txscript.PayToAddrScript(vault) + if err != nil { + t.Fatalf("vault script: %v", err) + } + if !bytes.Equal(tx.TxOut[0].PkScript, vaultScript) { + t.Error("output 0 is not the new vault script") + } + if tx.TxOut[0].Value <= 0 || tx.TxOut[0].Value >= 1_000_000 { + t.Errorf("output 0 value = %d, want 0 < v < total", tx.TxOut[0].Value) + } + + // Output 1: zero-value OP_RETURN(opID). + marker, err := txscript.NullDataScript(opID[:]) + if err != nil { + t.Fatalf("marker script: %v", err) + } + if tx.TxOut[1].Value != 0 { + t.Errorf("OP_RETURN value = %d, want 0", tx.TxOut[1].Value) + } + if !bytes.Equal(tx.TxOut[1].PkScript, marker) { + t.Error("output 1 is not OP_RETURN(opID)") + } + + // Inputs: every UTXO consumed. + if len(tx.TxIn) != len(utxos) { + t.Errorf("inputs = %d, want %d", len(tx.TxIn), len(utxos)) + } +} diff --git a/pkg/blockchain/btc/vault_integration_test.go b/pkg/blockchain/btc/vault_integration_test.go index 3eb2919..856d171 100644 --- a/pkg/blockchain/btc/vault_integration_test.go +++ b/pkg/blockchain/btc/vault_integration_test.go @@ -175,13 +175,15 @@ func TestIntegrationBTC_DepositAndWithdraw(t *testing.T) { rotators[i] = r } - rPacked, err := rotators[0].Pack(ctx, newPubHex, btcThreshold) + var rotID [32]byte + rotID[0], rotID[31] = 0xB7, 0x7A + rPacked, err := rotators[0].Pack(ctx, rotID, newPubHex, btcThreshold) if err != nil { t.Fatalf("rotation Pack: %v", err) } rShares := make([][]byte, 0, len(rotators)) for i, r := range rotators { - if err := r.Validate(ctx, rPacked, newPubHex, btcThreshold); err != nil { + if err := r.Validate(ctx, rotID, rPacked, newPubHex, btcThreshold); err != nil { t.Fatalf("rotation Validate[%d]: %v", i, err) } s, err := r.Sign(ctx, rPacked) diff --git a/pkg/blockchain/evm/rotation_finalizer.go b/pkg/blockchain/evm/rotation_finalizer.go index 7ed02a9..5e3b858 100644 --- a/pkg/blockchain/evm/rotation_finalizer.go +++ b/pkg/blockchain/evm/rotation_finalizer.go @@ -73,7 +73,9 @@ type evmRotPacked struct { // Pack reads the live signer nonce and returns the canonical JSON for rotating // to newSigners / newThreshold (signers sorted ascending, as Custody requires). -func (f *RotationFinalizer) Pack(ctx context.Context, newSigners []string, newThreshold int) ([]byte, error) { +// opID is ignored: EVM binds rotation replay to the on-chain Custody signerNonce, +// so the operation identity is not embedded in the payload. +func (f *RotationFinalizer) Pack(ctx context.Context, _ [32]byte, newSigners []string, newThreshold int) ([]byte, error) { addrs, err := parseSignerAddresses(newSigners) if err != nil { return nil, err @@ -92,7 +94,7 @@ func (f *RotationFinalizer) Pack(ctx context.Context, newSigners []string, newTh // Validate re-derives the rotation target from newSigners / newThreshold and // asserts the packed payload matches, including a re-read of the live nonce to // reject a packer that bound a stale or wrong signer nonce. -func (f *RotationFinalizer) Validate(ctx context.Context, packed []byte, newSigners []string, newThreshold int) error { +func (f *RotationFinalizer) Validate(ctx context.Context, _ [32]byte, packed []byte, newSigners []string, newThreshold int) error { var got evmRotPacked if err := json.Unmarshal(packed, &got); err != nil { return fmt.Errorf("decode packed: %w", err) diff --git a/pkg/blockchain/evm/vault_integration_test.go b/pkg/blockchain/evm/vault_integration_test.go index 6874d91..992d32f 100644 --- a/pkg/blockchain/evm/vault_integration_test.go +++ b/pkg/blockchain/evm/vault_integration_test.go @@ -178,13 +178,15 @@ func TestIntegrationEVM_DepositAndWithdraw(t *testing.T) { rotators[i] = r } - rPacked, err := rotators[0].Pack(ctx, newAddrs, integrationThreshold) + var rotID [32]byte + rotID[0], rotID[31] = 0xE0, 0x7A + rPacked, err := rotators[0].Pack(ctx, rotID, newAddrs, integrationThreshold) if err != nil { t.Fatalf("rotation Pack: %v", err) } rSigs := make([][]byte, 0, len(rotators)) for i, r := range rotators { - if err := r.Validate(ctx, rPacked, newAddrs, integrationThreshold); err != nil { + if err := r.Validate(ctx, rotID, rPacked, newAddrs, integrationThreshold); err != nil { t.Fatalf("rotation Validate[%d]: %v", i, err) } s, err := r.Sign(ctx, rPacked) diff --git a/pkg/blockchain/sol/rotation_finalizer.go b/pkg/blockchain/sol/rotation_finalizer.go index c8049a8..603a58a 100644 --- a/pkg/blockchain/sol/rotation_finalizer.go +++ b/pkg/blockchain/sol/rotation_finalizer.go @@ -86,8 +86,10 @@ type rotPacked struct { } // Pack reads the live signer nonce and returns the canonical JSON for rotating -// to newSigners / newThreshold. -func (f *RotationFinalizer) Pack(ctx context.Context, newSigners []string, newThreshold int) ([]byte, error) { +// to newSigners / newThreshold. opID is ignored: Solana binds rotation replay to +// the on-chain program signer nonce, so the operation identity is not embedded +// in the payload. +func (f *RotationFinalizer) Pack(ctx context.Context, _ [32]byte, newSigners []string, newThreshold int) ([]byte, error) { pubs, err := parseRotationSigners(newSigners) if err != nil { return nil, err @@ -110,7 +112,7 @@ func (f *RotationFinalizer) Pack(ctx context.Context, newSigners []string, newTh // Validate re-derives the rotation target from newSigners / newThreshold, // asserts the packed payload matches it, and re-reads the live nonce to reject a // packer that bound a stale or wrong signer nonce. -func (f *RotationFinalizer) Validate(ctx context.Context, packed []byte, newSigners []string, newThreshold int) error { +func (f *RotationFinalizer) Validate(ctx context.Context, _ [32]byte, packed []byte, newSigners []string, newThreshold int) error { var got rotPacked if err := json.Unmarshal(packed, &got); err != nil { return fmt.Errorf("sol: decode packed: %w", err) diff --git a/pkg/blockchain/sol/vault_integration_test.go b/pkg/blockchain/sol/vault_integration_test.go index bc1a92a..58aa1b1 100644 --- a/pkg/blockchain/sol/vault_integration_test.go +++ b/pkg/blockchain/sol/vault_integration_test.go @@ -192,13 +192,15 @@ func runRotation(ctx context.Context, t *testing.T, rpcURL string, programID sol } rotators[i] = r } - packed, err := rotators[0].Pack(ctx, target, threshold) + var rotID [32]byte + rotID[0], rotID[31] = 0x50, 0x7A + packed, err := rotators[0].Pack(ctx, rotID, target, threshold) if err != nil { t.Fatalf("rotation Pack: %v", err) } shares := make([][]byte, 0, len(rotators)) for i, r := range rotators { - if err := r.Validate(ctx, packed, target, threshold); err != nil { + if err := r.Validate(ctx, rotID, packed, target, threshold); err != nil { t.Fatalf("rotation Validate[%d]: %v", i, err) } s, err := r.Sign(ctx, packed) diff --git a/pkg/blockchain/xrpl/rotation_finalizer.go b/pkg/blockchain/xrpl/rotation_finalizer.go index 796aabc..55f7e07 100644 --- a/pkg/blockchain/xrpl/rotation_finalizer.go +++ b/pkg/blockchain/xrpl/rotation_finalizer.go @@ -54,8 +54,10 @@ func NewRotationFinalizer(rpcURL, vaultAddress string, threshold int, signer sig } // Pack builds the autofilled multi-sign SignerListSet installing newSigners / -// newThreshold (each member weight 1), returning its sorted-key JSON. -func (f *RotationFinalizer) Pack(_ context.Context, newSigners []string, newThreshold int) ([]byte, error) { +// newThreshold (each member weight 1), returning its sorted-key JSON. opID is +// ignored: XRPL binds rotation replay to the account Sequence (autofilled here), +// so the operation identity is not embedded in the payload. +func (f *RotationFinalizer) Pack(_ context.Context, _ [32]byte, newSigners []string, newThreshold int) ([]byte, error) { entries, err := signerEntries(newSigners, newThreshold) if err != nil { return nil, err @@ -74,7 +76,7 @@ func (f *RotationFinalizer) Pack(_ context.Context, newSigners []string, newThre } // Validate asserts the packed SignerListSet rotates to exactly the requested set. -func (f *RotationFinalizer) Validate(_ context.Context, packed []byte, newSigners []string, newThreshold int) error { +func (f *RotationFinalizer) Validate(_ context.Context, _ [32]byte, packed []byte, newSigners []string, newThreshold int) error { var flat transaction.FlatTransaction if err := json.Unmarshal(packed, &flat); err != nil { return fmt.Errorf("xrpl: decode packed: %w", err) diff --git a/pkg/blockchain/xrpl/vault_integration_test.go b/pkg/blockchain/xrpl/vault_integration_test.go index ac2254a..5765bce 100644 --- a/pkg/blockchain/xrpl/vault_integration_test.go +++ b/pkg/blockchain/xrpl/vault_integration_test.go @@ -155,13 +155,15 @@ func TestIntegrationXRPL_DepositAndWithdraw(t *testing.T) { rotators[i] = r } - rPacked, err := rotators[0].Pack(ctx, newAddrs, xrplQuorum) + var rotID [32]byte + rotID[0], rotID[31] = 0x4A, 0x7A + rPacked, err := rotators[0].Pack(ctx, rotID, newAddrs, xrplQuorum) if err != nil { t.Fatalf("rotation Pack: %v", err) } rBlobs := make([][]byte, 0, len(rotators)) for i, r := range rotators { - if err := r.Validate(ctx, rPacked, newAddrs, xrplQuorum); err != nil { + if err := r.Validate(ctx, rotID, rPacked, newAddrs, xrplQuorum); err != nil { t.Fatalf("rotation Validate[%d]: %v", i, err) } b, err := r.Sign(ctx, rPacked) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index df22471..2dd4847 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -124,6 +124,14 @@ type VaultWithdrawalFinalizer interface { // addresses, BTC 33-byte compressed pubkeys (hex), Solana ed25519 pubkeys (hex) // — matching custody's RotationRequest. newThreshold is the new k-of-n quorum. // +// opID is the caller's unique identifier for this rotation operation (custody's +// RotationRequest.RequestID). In-place chains bind replay protection on-chain +// (EVM signerNonce, XRPL account sequence, Solana program nonce) and accept opID +// only to keep one uniform signature — they do not embed it in the packed +// payload. BTC has no on-chain op record: it embeds opID as an OP_RETURN marker +// in the sweep so an external watcher can attribute the swept transaction to +// this rotation. Pass a zero opID when the caller has no watcher to satisfy. +// // In-place chains (EVM, XRPL, Solana) mutate on-chain signer state at a fixed // vault address. BTC has no in-place form: its P2WSH vault address is a function // of the signer set, so rotation is a sweep of every old-vault UTXO into the @@ -140,10 +148,11 @@ type VaultWithdrawalFinalizer interface { // rotation. // - VerifyRotation reads canonical chain state to answer "is the set now the // requested one?" — binary (done or not), the signal each node uses to close -// the dual-sign window and drop the outgoing key. +// the dual-sign window and drop the outgoing key. It is keyed on the new +// signer set (not opID), so it stays a direct state read on every chain. type SignerRotationFinalizer interface { - Pack(ctx context.Context, newSigners []string, newThreshold int) ([]byte, error) - Validate(ctx context.Context, packed []byte, newSigners []string, newThreshold int) error + Pack(ctx context.Context, opID [32]byte, newSigners []string, newThreshold int) ([]byte, error) + Validate(ctx context.Context, opID [32]byte, packed []byte, newSigners []string, newThreshold int) error Sign(ctx context.Context, packed []byte) ([]byte, error) Submit(ctx context.Context, packed []byte, signatures [][]byte) (TxRef, error) VerifyRotation(ctx context.Context, newSigners []string, newThreshold int) (txHash [32]byte, done bool, err error) From 810c537edfa5f466d345786886c80d95d3222282 Mon Sep 17 00:00:00 2001 From: Anton Filonenko Date: Wed, 17 Jun 2026 14:29:48 +0300 Subject: [PATCH 09/15] feat(blockchain): harden XRPL validation, add SOL SPL + ALT withdrawals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit XRPL: reject a present-but-non-zero (or non-numeric) Flags in both the withdrawal and rotation canonical validators, closing the tfPartialPayment underdelivery path on issued-currency withdrawals. The multi-sign combine now reads the vault's live SignerList and trims to the live SignerQuorum, dropping blobs from signers that have rotated off (or not yet on) and sizing the fee to the current quorum. Solana: support SPL-token withdrawals — an idempotent recipient-ATA creation ahead of the Ed25519 companion and the token remaining-accounts on execute (token program, vault ATA, recipient ATA), reusing the generated instruction's data encoding. Submit can emit a v0 transaction over a configured Address Lookup Table so large quorums stay within the 1232-byte packet limit. BTC: the withdrawal validator asserts the fixed fields the SIGHASH commits to (tx version, zero locktime, final input sequences), rejecting a non-final or RBF-signalling canonical tx before signing. Docs: the Custody (EVM) and custody-program (Solana) artifacts are now sourced from the custody repo, not clearnet. --- pkg/blockchain/btc/withdrawal_finalizer.go | 17 +++ pkg/blockchain/evm/artifacts/README.md | 15 ++- pkg/blockchain/sol/artifacts/README.md | 7 +- pkg/blockchain/sol/depositor.go | 2 +- pkg/blockchain/sol/program.go | 24 +++- pkg/blockchain/sol/rotation_finalizer.go | 2 +- pkg/blockchain/sol/vault_integration_test.go | 2 +- pkg/blockchain/sol/withdrawal_finalizer.go | 72 ++++++++-- pkg/blockchain/xrpl/rotation_finalizer.go | 2 +- pkg/blockchain/xrpl/wire.go | 130 +++++++++++++++++-- pkg/blockchain/xrpl/withdrawal_finalizer.go | 2 +- 11 files changed, 233 insertions(+), 42 deletions(-) diff --git a/pkg/blockchain/btc/withdrawal_finalizer.go b/pkg/blockchain/btc/withdrawal_finalizer.go index 7975c5a..55d0f8b 100644 --- a/pkg/blockchain/btc/withdrawal_finalizer.go +++ b/pkg/blockchain/btc/withdrawal_finalizer.go @@ -181,6 +181,23 @@ func (f *WithdrawalFinalizer) Validate(ctx context.Context, packed []byte, op *c if err != nil { return fmt.Errorf("btc validate: %w", err) } + // Assert the fixed fields the BIP-143 SIGHASH_ALL digest commits to, matching + // what BuildUnsignedTx produces: version wire.TxVersion, locktime 0, and final + // (non-RBF) input sequences. The sighash already binds these, so a Byzantine + // canonicalizer cannot make followers sign something inconsistent — but + // without the checks it could induce co-signing of a non-final or + // RBF-signalling tx and waste a signing round (ISS-006(b), griefing only). + if tx.Version != wire.TxVersion { + return fmt.Errorf("btc validate: unexpected tx version %d", tx.Version) + } + if tx.LockTime != 0 { + return fmt.Errorf("btc validate: non-zero locktime %d", tx.LockTime) + } + for i, in := range tx.TxIn { + if in.Sequence != wire.MaxTxInSequenceNum { + return fmt.Errorf("btc validate: input %d non-final sequence %d", i, in.Sequence) + } + } if n := len(tx.TxOut); n != 2 && n != 3 { return fmt.Errorf("btc validate: expected 2 or 3 outputs, got %d", n) } diff --git a/pkg/blockchain/evm/artifacts/README.md b/pkg/blockchain/evm/artifacts/README.md index 33f5b6e..d207704 100644 --- a/pkg/blockchain/evm/artifacts/README.md +++ b/pkg/blockchain/evm/artifacts/README.md @@ -28,13 +28,18 @@ This rewrites `../*_abi.go` from the `.abi`/`.bin` here. Commit the result. ## Refresh the .abi / .bin (only when a contract changes) -The files are produced by `forge build` in a repo that owns the Solidity -source (clearnet for Registry/YellowToken/etc.; custody for Custody). Build -there, then extract abi + bytecode into this directory. Foundry nests output -by source-file name, so the contract→json mapping matters (note YellowToken -lives in `Token.sol`): +The files are produced by `forge build` in the repo that owns each contract's +Solidity source. The source trees are split: the clearing contracts +(`Registry`, `YellowToken`, `MockERC20`, `NodeID`, `Faucet`, `Slasher`) live in +**clearnet** (`contracts/evm`); **`Custody` was moved out of clearnet and now +lives in custody** at `chains/evm/contract` (`src/Custody.sol`). Build each in +its own tree, then extract abi + bytecode into this directory. Foundry nests +output by source-file name, so the contract→json mapping matters (note +YellowToken lives in `Token.sol`): ```sh +# Clearing contracts from clearnet; refresh Custody from +# ../custody/chains/evm/contract/out (same jq extraction, different OUT tree). OUT=../clearnet/contracts/evm/out # a `forge build` output tree DEST=pkg/blockchain/evm/artifacts # this directory, from repo root diff --git a/pkg/blockchain/sol/artifacts/README.md b/pkg/blockchain/sol/artifacts/README.md index 7c569d7..0edda8e 100644 --- a/pkg/blockchain/sol/artifacts/README.md +++ b/pkg/blockchain/sol/artifacts/README.md @@ -29,13 +29,14 @@ Rewrites `../custody/*.go` from `custody.json`. Commit the result. ## Refresh the artifacts (only when the program changes) -Both files come from `anchor build` in the repo that owns the Rust source -(`clearnet/contracts/solana`, Anchor 0.31). Requires the Solana + Anchor +Both files come from `anchor build` in the repo that owns the Rust source. +That source now lives in **custody** at `chains/sol/contract` (Anchor 0.31) — +the custody program was moved out of clearnet. Requires the Solana + Anchor toolchain (`solana` / `cargo-build-sbf` + `anchor` 0.31 via avm) — needed only to refresh, never to run the tests: ```sh -cd ../clearnet/contracts/solana +cd ../custody/chains/sol/contract anchor build cp target/idl/custody.json /pkg/blockchain/sol/artifacts/custody.json cp target/deploy/custody.so /pkg/blockchain/sol/artifacts/custody.so diff --git a/pkg/blockchain/sol/depositor.go b/pkg/blockchain/sol/depositor.go index 19f6f5e..dae9794 100644 --- a/pkg/blockchain/sol/depositor.go +++ b/pkg/blockchain/sol/depositor.go @@ -101,7 +101,7 @@ func (d *Depositor) SubmitDeposit(ctx context.Context, asset string, amount deci return core.TxRef{}, fmt.Errorf("sol: build deposit ix: %w", err) } - sig, err := signAndSend(ctx, d.client, []solana.Instruction{ix}, d.depositorPub, d.signer, d.commitment) + sig, err := signAndSend(ctx, d.client, []solana.Instruction{ix}, d.depositorPub, d.signer, d.commitment, solana.PublicKey{}) if err != nil { return core.TxRef{}, err } diff --git a/pkg/blockchain/sol/program.go b/pkg/blockchain/sol/program.go index aaf9871..dcc9e9b 100644 --- a/pkg/blockchain/sol/program.go +++ b/pkg/blockchain/sol/program.go @@ -6,16 +6,20 @@ import ( "fmt" "github.com/gagliardetto/solana-go" + addresslookuptable "github.com/gagliardetto/solana-go/programs/address-lookup-table" "github.com/gagliardetto/solana-go/rpc" "github.com/layer-3/clearnet-sdk/pkg/blockchain/sol/custody" "github.com/layer-3/clearnet-sdk/pkg/sign" ) -// signAndSend builds a legacy transaction over the instructions, signs it with -// the single fee-payer (the quorum's signatures ride inside the Ed25519 -// instruction, not the tx signers), and broadcasts it. -func signAndSend(ctx context.Context, client *rpc.Client, instructions []solana.Instruction, payerPub solana.PublicKey, payer sign.Signer, commitment rpc.CommitmentType) (solana.Signature, error) { +// signAndSend builds a transaction over the instructions, signs it with the +// single fee-payer (the quorum's signatures ride inside the Ed25519 instruction, +// not the tx signers), and broadcasts it. When alt is non-zero it loads that +// Address Lookup Table and emits a v0 transaction, compressing the account list +// so large quorums (whose Ed25519 instruction is already large) still fit the +// 1232-byte packet limit; otherwise it emits a legacy transaction. +func signAndSend(ctx context.Context, client *rpc.Client, instructions []solana.Instruction, payerPub solana.PublicKey, payer sign.Signer, commitment rpc.CommitmentType, alt solana.PublicKey) (solana.Signature, error) { if commitment == "" { commitment = rpc.CommitmentFinalized } @@ -27,7 +31,17 @@ func signAndSend(ctx context.Context, client *rpc.Client, instructions []solana. if err != nil { return solana.Signature{}, fmt.Errorf("sol: latest blockhash: %w", err) } - tx, err := solana.NewTransaction(instructions, bh.Value.Blockhash, solana.TransactionPayer(payerPub)) + opts := []solana.TransactionOption{solana.TransactionPayer(payerPub)} + if !alt.IsZero() { + state, err := addresslookuptable.GetAddressLookupTable(ctx, client, alt) + if err != nil { + return solana.Signature{}, fmt.Errorf("sol: load ALT: %w", err) + } + opts = append(opts, solana.TransactionAddressTables(map[solana.PublicKey]solana.PublicKeySlice{ + alt: state.Addresses, + })) + } + tx, err := solana.NewTransaction(instructions, bh.Value.Blockhash, opts...) if err != nil { return solana.Signature{}, fmt.Errorf("sol: build tx: %w", err) } diff --git a/pkg/blockchain/sol/rotation_finalizer.go b/pkg/blockchain/sol/rotation_finalizer.go index 603a58a..4554487 100644 --- a/pkg/blockchain/sol/rotation_finalizer.go +++ b/pkg/blockchain/sol/rotation_finalizer.go @@ -208,7 +208,7 @@ func (f *RotationFinalizer) Submit(ctx context.Context, packed []byte, shares [] } instructions := append(leading, ed25519Ix, updateIx) - sig, err := signAndSend(ctx, f.client, instructions, f.feePayerPub, f.feePayer, f.commitment) + sig, err := signAndSend(ctx, f.client, instructions, f.feePayerPub, f.feePayer, f.commitment, solana.PublicKey{}) if err != nil { if _, done, verr := f.VerifyRotation(ctx, p.NewSigners, int(p.NewThreshold)); verr == nil && done { return core.TxRef{}, nil diff --git a/pkg/blockchain/sol/vault_integration_test.go b/pkg/blockchain/sol/vault_integration_test.go index 58aa1b1..75d0bd2 100644 --- a/pkg/blockchain/sol/vault_integration_test.go +++ b/pkg/blockchain/sol/vault_integration_test.go @@ -93,7 +93,7 @@ func TestIntegrationSOL_DepositAndWithdraw(t *testing.T) { if e != nil { t.Fatalf("build initialize: %v", e) } - if _, e := signAndSend(ctx, client, []solana.Instruction{ix}, authorityPub, authority, rpc.CommitmentConfirmed); e != nil { + if _, e := signAndSend(ctx, client, []solana.Instruction{ix}, authorityPub, authority, rpc.CommitmentConfirmed, solana.PublicKey{}); e != nil { t.Fatalf("initialize: %v", e) } waitConfig(ctx, t, client, programID) diff --git a/pkg/blockchain/sol/withdrawal_finalizer.go b/pkg/blockchain/sol/withdrawal_finalizer.go index 841291e..eae0a6e 100644 --- a/pkg/blockchain/sol/withdrawal_finalizer.go +++ b/pkg/blockchain/sol/withdrawal_finalizer.go @@ -10,6 +10,7 @@ import ( "time" "github.com/gagliardetto/solana-go" + associatedtokenaccount "github.com/gagliardetto/solana-go/programs/associated-token-account" computebudget "github.com/gagliardetto/solana-go/programs/compute-budget" "github.com/gagliardetto/solana-go/rpc" @@ -40,13 +41,19 @@ type Config struct { // devnet/test speed tradeoff: it observes results in ~1-2 slots instead of // waiting ~32 for finality, cutting the local flow from ~16s to a few. Commitment rpc.CommitmentType + // AddressLookupTable, when set, makes the submit emit a v0 transaction using + // this ALT. Required for large quorums: the Ed25519 instruction grows ~112 + // bytes per signer, so beyond ~8-9 signers a legacy transaction exceeds the + // 1232-byte packet limit. Zero → legacy transaction. + AddressLookupTable solana.PublicKey } // WithdrawalFinalizer executes a withdrawal against the custody Anchor program: // a digest signed by the ed25519 quorum, verified on-chain via the Ed25519 // precompile. The node's signer contributes one share; a separate fee-payer -// signer pays + submits. It implements core.VaultWithdrawalFinalizer. Native -// SOL only for now (SPL execute needs the program's remaining-accounts). +// signer pays + submits. It implements core.VaultWithdrawalFinalizer, for both +// native SOL and SPL tokens (the SPL path adds the recipient-ATA creation and +// the token remaining-accounts the program's execute expects). type WithdrawalFinalizer struct { client *rpc.Client programID solana.PublicKey @@ -57,6 +64,7 @@ type WithdrawalFinalizer struct { cuLimit uint32 cuPrice uint64 commitment rpc.CommitmentType + alt solana.PublicKey signer sign.Signer nodePub solana.PublicKey feePayer sign.Signer @@ -95,6 +103,7 @@ func NewWithdrawalFinalizer(rpcURL string, programID solana.PublicKey, signer, f cuLimit: limit, cuPrice: cfg.ComputeUnitPrice, commitment: commitment, + alt: cfg.AddressLookupTable, signer: signer, nodePub: nodePub, feePayer: feePayer, @@ -176,9 +185,6 @@ func (f *WithdrawalFinalizer) Submit(ctx context.Context, packed []byte, shares if err != nil { return core.TxRef{}, err } - if !mint.IsZero() { - return core.TxRef{}, fmt.Errorf("sol: SPL withdrawal not yet supported (native SOL only)") - } pubkeys, sigs, err := f.merge(ctx, shares) if err != nil { @@ -191,23 +197,26 @@ func (f *WithdrawalFinalizer) Submit(ctx context.Context, packed []byte, shares return core.TxRef{}, err } // Leading instructions before the Ed25519 companion: the two compute-budget - // instructions. sigIxIndex (which execute introspects) is their count. + // instructions, plus (SPL only) an idempotent recipient-ATA creation paid by + // the fee payer (the recipient may not have a token account yet). sigIxIndex + // — which execute introspects to find its Ed25519 companion — is the count of + // these leading instructions. leading := []solana.Instruction{ computebudget.NewSetComputeUnitLimitInstruction(f.cuLimit).Build(), computebudget.NewSetComputeUnitPriceInstruction(f.cuPrice).Build(), } + if !mint.IsZero() { + leading = append(leading, + associatedtokenaccount.NewCreateIdempotentInstruction(f.feePayerPub, to, mint).Build()) + } sigIxIndex := uint8(len(leading)) - execIx, err := custody.NewExecuteInstruction( - to, mint, amount, wid, sigIxIndex, - f.feePayerPub, f.configPDA, f.vaultPDA, WithdrawalPDA(f.programID, wid), - to, solana.SysVarInstructionsPubkey, solana.SystemProgramID, f.eventAuth, f.programID, - ) + execIx, err := f.buildExecuteIx(to, mint, amount, wid, sigIxIndex) if err != nil { - return core.TxRef{}, fmt.Errorf("sol: build execute ix: %w", err) + return core.TxRef{}, err } instructions := append(leading, ed25519Ix, execIx) - sig, err := signAndSend(ctx, f.client, instructions, f.feePayerPub, f.feePayer, f.commitment) + sig, err := signAndSend(ctx, f.client, instructions, f.feePayerPub, f.feePayer, f.commitment, f.alt) if err != nil { // A peer may have already landed it. if h, executed, verr := f.VerifyExecution(ctx, wid); verr == nil && executed { @@ -221,6 +230,43 @@ func (f *WithdrawalFinalizer) Submit(ctx context.Context, packed []byte, shares return txRef(sig), nil } +// buildExecuteIx builds the execute instruction. The native account list comes +// from the generated binding; for an SPL withdrawal it appends the token +// remaining-accounts the program's execute expects (token program, vault ATA, +// recipient ATA), reusing the binding's data encoding so the discriminator and +// arguments stay byte-exact. +func (f *WithdrawalFinalizer) buildExecuteIx(to, mint solana.PublicKey, amount uint64, wid [32]byte, sigIxIndex uint8) (solana.Instruction, error) { + execIx, err := custody.NewExecuteInstruction( + to, mint, amount, wid, sigIxIndex, + f.feePayerPub, f.configPDA, f.vaultPDA, WithdrawalPDA(f.programID, wid), + to, solana.SysVarInstructionsPubkey, solana.SystemProgramID, f.eventAuth, f.programID, + ) + if err != nil { + return nil, fmt.Errorf("sol: build execute ix: %w", err) + } + if mint.IsZero() { + return execIx, nil + } + vaultATA, _, err := solana.FindAssociatedTokenAddress(f.vaultPDA, mint) + if err != nil { + return nil, fmt.Errorf("sol: vault ATA: %w", err) + } + recipientATA, _, err := solana.FindAssociatedTokenAddress(to, mint) + if err != nil { + return nil, fmt.Errorf("sol: recipient ATA: %w", err) + } + data, err := execIx.Data() + if err != nil { + return nil, fmt.Errorf("sol: execute data: %w", err) + } + metas := append(execIx.Accounts(), + solana.NewAccountMeta(solana.TokenProgramID, false, false), + solana.NewAccountMeta(vaultATA, true, false), + solana.NewAccountMeta(recipientATA, true, false), + ) + return solana.NewInstruction(f.programID, metas, data), nil +} + // VerifyExecution reports whether the Withdrawal PDA exists (the on-chain // executed flag). The tx hash is not recoverable from the PDA alone, so a zero // hash is returned with executed=true. diff --git a/pkg/blockchain/xrpl/rotation_finalizer.go b/pkg/blockchain/xrpl/rotation_finalizer.go index 55f7e07..a0d8fb3 100644 --- a/pkg/blockchain/xrpl/rotation_finalizer.go +++ b/pkg/blockchain/xrpl/rotation_finalizer.go @@ -100,7 +100,7 @@ func (f *RotationFinalizer) Sign(ctx context.Context, packed []byte) ([]byte, er // Submit combines the collected multi-sign blobs (trimmed to the current quorum) // and broadcasts the SignerListSet, returning the tx reference. func (f *RotationFinalizer) Submit(_ context.Context, _ []byte, signatures [][]byte) (core.TxRef, error) { - merged, err := combineMultisign(signatures, f.threshold) + merged, err := combineLive(f.client, f.vaultAddress, signatures) if err != nil { return core.TxRef{}, err } diff --git a/pkg/blockchain/xrpl/wire.go b/pkg/blockchain/xrpl/wire.go index 12388f8..f3cf52d 100644 --- a/pkg/blockchain/xrpl/wire.go +++ b/pkg/blockchain/xrpl/wire.go @@ -18,6 +18,8 @@ import ( binarycodec "github.com/Peersyst/xrpl-go/binary-codec" xrplcrypto "github.com/Peersyst/xrpl-go/pkg/crypto" "github.com/Peersyst/xrpl-go/xrpl" + "github.com/Peersyst/xrpl-go/xrpl/queries/account" + "github.com/Peersyst/xrpl-go/xrpl/rpc" "github.com/Peersyst/xrpl-go/xrpl/transaction" "github.com/Peersyst/xrpl-go/xrpl/transaction/types" @@ -211,6 +213,16 @@ func ValidateCanonical(flat transaction.FlatTransaction, op *core.WithdrawalOp, if uint64(fee) > maxAcceptableFeeDrops { return fmt.Errorf("xrpl canonical: Fee %d drops exceeds ceiling %d", fee, maxAcceptableFeeDrops) } + // Flags is allowlisted (honest bodies omit it) but its value must be + // constrained: a non-zero Flags can carry tfPartialPayment, which on an + // issued-currency withdrawal lets the submitter deliver less than Amount + // while the ceremony still reports success (ISS-002). Reject any present + // Flags that is non-zero or non-numeric. + if raw, ok := flat["Flags"]; ok { + if v, ok := uint32Field(raw); !ok || v != 0 { + return fmt.Errorf("xrpl canonical: non-zero or invalid Flags not permitted: %v", raw) + } + } for k := range flat { if _, ok := canonicalAllowedFields[k]; !ok { return fmt.Errorf("xrpl canonical: unexpected field %q", k) @@ -263,6 +275,13 @@ func validateCanonicalRotation(flat transaction.FlatTransaction, newSigners []st if uint64(fee) > maxAcceptableFeeDrops { return fmt.Errorf("xrpl rotation: Fee %d drops exceeds ceiling %d", fee, maxAcceptableFeeDrops) } + // Flags is allowlisted but its value must be constrained; reject any present + // Flags that is non-zero or non-numeric (ISS-002). + if raw, ok := flat["Flags"]; ok { + if v, ok := uint32Field(raw); !ok || v != 0 { + return fmt.Errorf("xrpl rotation: non-zero or invalid Flags not permitted: %v", raw) + } + } for k := range flat { if _, ok := rotationAllowedFields[k]; !ok { return fmt.Errorf("xrpl rotation: unexpected field %q", k) @@ -400,19 +419,31 @@ func CanonicalJSON(flatTx transaction.FlatTransaction) ([]byte, error) { return buf.Bytes(), nil } -// combineMultisign trims the collected multi-sign blobs to exactly `threshold` -// and combines them into one submittable blob. Pack autofilled the multi-sign -// fee for that count (base × (1 + threshold)), so including extras would -// under-pay (telINSUF_FEE_P) and waste fee; any threshold of the SignerList's -// members satisfies the quorum. Shared by the withdrawal and rotation submits. -func combineMultisign(signatures [][]byte, threshold int) (string, error) { - if len(signatures) < threshold { - return "", fmt.Errorf("xrpl: have %d signatures, need %d", len(signatures), threshold) - } - blobs := make([]string, 0, threshold) - for _, s := range signatures[:threshold] { +// combineLive merges the collected multi-sign blobs, filtering them against the +// vault's live SignerList and trimming to the live SignerQuorum rather than a +// boot-time threshold. A blob signed by a peer whose key just rotated off (or +// hasn't rotated on yet) would make rippled reject the assembled tx +// (tefBAD_SIGNATURE), and a stale quorum would under- or over-fill the Signers +// array; reading the live list keeps both correct across a rotation. Pack +// autofilled the multi-sign fee for the quorum count, so trimming to exactly +// liveQuorum keeps the fee right. Shared by the withdrawal and rotation submits. +func combineLive(client *rpc.Client, vault string, signatures [][]byte) (string, error) { + blobs := make([]string, 0, len(signatures)) + for _, s := range signatures { blobs = append(blobs, string(s)) } + authorized, liveQuorum, err := fetchLiveSignerList(client, vault) + if err != nil { + return "", err + } + blobs, err = filterBlobsByAuthorized(blobs, authorized) + if err != nil { + return "", err + } + if len(blobs) < liveQuorum { + return "", fmt.Errorf("xrpl: only %d authorized signatures after live-SignerList filter, need %d", len(blobs), liveQuorum) + } + blobs = blobs[:liveQuorum] final, err := xrpl.Multisign(blobs...) if err != nil { return "", fmt.Errorf("xrpl: combine signatures: %w", err) @@ -420,6 +451,83 @@ func combineMultisign(signatures [][]byte, threshold int) (string, error) { return final, nil } +// fetchLiveSignerList reads the vault's live SignerList via account_info and +// returns the currently-authorized r-addresses plus the live SignerQuorum. +func fetchLiveSignerList(client *rpc.Client, vault string) (map[string]struct{}, int, error) { + resp, err := client.GetAccountInfo(&account.InfoRequest{ + Account: types.Address(vault), + SignerLists: true, + }) + if err != nil { + return nil, 0, fmt.Errorf("xrpl: account_info: %w", err) + } + if len(resp.SignerLists) == 0 { + return nil, 0, fmt.Errorf("xrpl: vault has no SignerList configured") + } + live := resp.SignerLists[0] + authorized := make(map[string]struct{}, len(live.SignerEntries)) + for _, e := range live.SignerEntries { + authorized[string(e.SignerEntry.Account)] = struct{}{} + } + quorum := int(live.SignerQuorum) + if quorum <= 0 || quorum > len(live.SignerEntries) { + return nil, 0, fmt.Errorf("xrpl: live SignerQuorum %d out of range for %d entries", quorum, len(live.SignerEntries)) + } + return authorized, quorum, nil +} + +// filterBlobsByAuthorized drops blobs whose inner signer is not in authorized, +// de-duping by signer account. +func filterBlobsByAuthorized(blobs []string, authorized map[string]struct{}) ([]string, error) { + out := make([]string, 0, len(blobs)) + seen := make(map[string]struct{}, len(blobs)) + for i, b := range blobs { + acct, err := blobSignerAccount(b) + if err != nil { + return nil, fmt.Errorf("xrpl: decode blob %d: %w", i, err) + } + if _, dup := seen[acct]; dup { + continue + } + if _, ok := authorized[acct]; !ok { + continue + } + seen[acct] = struct{}{} + out = append(out, b) + } + return out, nil +} + +// blobSignerAccount decodes a multi-sign blob (one signer's contribution) and +// returns the classic r-address of its inner Signer entry. +func blobSignerAccount(blob string) (string, error) { + decoded, err := binarycodec.Decode(blob) + if err != nil { + return "", err + } + signersAny, ok := decoded["Signers"] + if !ok { + return "", fmt.Errorf("blob missing Signers field") + } + signers, ok := signersAny.([]any) + if !ok || len(signers) == 0 { + return "", fmt.Errorf("blob Signers field not a non-empty array") + } + first, ok := signers[0].(map[string]any) + if !ok { + return "", fmt.Errorf("blob Signers[0] not an object") + } + inner, ok := first["Signer"].(map[string]any) + if !ok { + return "", fmt.Errorf("blob Signers[0].Signer not an object") + } + acct, ok := inner["Account"].(string) + if !ok || acct == "" { + return "", fmt.Errorf("blob Signers[0].Signer.Account missing") + } + return acct, nil +} + // hashHex is the uppercase hex of a 32-byte tx hash (XRPL's display form). func hashHex(h [32]byte) string { return strings.ToUpper(hex.EncodeToString(h[:])) } diff --git a/pkg/blockchain/xrpl/withdrawal_finalizer.go b/pkg/blockchain/xrpl/withdrawal_finalizer.go index ec9b17f..79fcfc7 100644 --- a/pkg/blockchain/xrpl/withdrawal_finalizer.go +++ b/pkg/blockchain/xrpl/withdrawal_finalizer.go @@ -118,7 +118,7 @@ func (f *WithdrawalFinalizer) Sign(ctx context.Context, packed []byte) ([]byte, // Submit combines the collected multi-sign blobs (trimmed to the quorum) and // broadcasts the result, returning the tx reference. func (f *WithdrawalFinalizer) Submit(_ context.Context, _ []byte, signatures [][]byte) (core.TxRef, error) { - merged, err := combineMultisign(signatures, f.threshold) + merged, err := combineLive(f.client, f.vaultAddress, signatures) if err != nil { return core.TxRef{}, err } From 6a8fbb2b10005f8a55bd8018b73d45018b018959 Mon Sep 17 00:00:00 2001 From: Anton Filonenko Date: Wed, 17 Jun 2026 14:52:39 +0300 Subject: [PATCH 10/15] test(blockchain): cover validation guards, SPL withdrawal; apply BTC fix Tests for the previously-uncovered logic plus the matching BTC fix: - BTC: factor the SIGHASH fixed-field checks into validateFixedTxFields and apply them in the rotation validator too (they were only on the withdrawal path); unit test for the version/locktime/sequence guard. - XRPL: unit tests for the Flags==0 guard on both the withdrawal and rotation validators (tfPartialPayment rejected), and for the live-SignerList blob filter using real multi-sign blobs. - Solana: unit test pinning the execute account shape (native vs the SPL remaining-accounts), and an SPL-withdrawal case in the integration test that mints to the vault ATA and asserts the recipient ATA is credited. --- pkg/blockchain/btc/rotation_finalizer.go | 3 + pkg/blockchain/btc/rotation_finalizer_test.go | 35 ++++ pkg/blockchain/btc/txbuild.go | 24 +++ pkg/blockchain/btc/withdrawal_finalizer.go | 18 +- pkg/blockchain/sol/vault_integration_test.go | 136 +++++++++++++ .../sol/withdrawal_finalizer_test.go | 74 +++++++ pkg/blockchain/xrpl/validate_test.go | 182 ++++++++++++++++++ 7 files changed, 456 insertions(+), 16 deletions(-) create mode 100644 pkg/blockchain/sol/withdrawal_finalizer_test.go create mode 100644 pkg/blockchain/xrpl/validate_test.go diff --git a/pkg/blockchain/btc/rotation_finalizer.go b/pkg/blockchain/btc/rotation_finalizer.go index c710aac..005f6a0 100644 --- a/pkg/blockchain/btc/rotation_finalizer.go +++ b/pkg/blockchain/btc/rotation_finalizer.go @@ -155,6 +155,9 @@ func (f *RotationFinalizer) Validate(ctx context.Context, opID [32]byte, packed if err != nil { return fmt.Errorf("btc rotation validate: %w", err) } + if err := validateFixedTxFields(tx); err != nil { + return fmt.Errorf("btc rotation validate: %w", err) + } if len(tx.TxOut) != 2 { return fmt.Errorf("btc rotation validate: expected 2 outputs (new vault + OP_RETURN), got %d", len(tx.TxOut)) } diff --git a/pkg/blockchain/btc/rotation_finalizer_test.go b/pkg/blockchain/btc/rotation_finalizer_test.go index a9d65e5..505f587 100644 --- a/pkg/blockchain/btc/rotation_finalizer_test.go +++ b/pkg/blockchain/btc/rotation_finalizer_test.go @@ -7,11 +7,46 @@ import ( "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" "github.com/ethereum/go-ethereum/crypto" "github.com/layer-3/clearnet-sdk/pkg/sign" ) +// TestValidateFixedTxFields covers the ISS-006(b) griefing guard shared by the +// withdrawal and rotation validators: the canonical tx must use the canonical +// version, a zero locktime, and final (non-RBF) input sequences. +func TestValidateFixedTxFields(t *testing.T) { + good := func() *wire.MsgTx { + tx := wire.NewMsgTx(wire.TxVersion) + in := wire.NewTxIn(&wire.OutPoint{Index: 0}, nil, nil) + in.Sequence = wire.MaxTxInSequenceNum + tx.AddTxIn(in) + return tx + } + if err := validateFixedTxFields(good()); err != nil { + t.Fatalf("canonical tx rejected: %v", err) + } + + badVersion := good() + badVersion.Version = wire.TxVersion + 1 + if err := validateFixedTxFields(badVersion); err == nil { + t.Error("non-canonical version accepted") + } + + badLock := good() + badLock.LockTime = 1 + if err := validateFixedTxFields(badLock); err == nil { + t.Error("non-zero locktime accepted") + } + + rbf := good() + rbf.TxIn[0].Sequence = wire.MaxTxInSequenceNum - 2 // RBF-signalling + if err := validateFixedTxFields(rbf); err == nil { + t.Error("RBF-signalling sequence accepted") + } +} + // TestBuildSweepTx_OpReturnMarker pins the rotation sweep wire a watcher matches // on: output 0 pays the new vault, the final output is a zero-value OP_RETURN // carrying opID. (Devnet-free; the full flow is exercised by the integration diff --git a/pkg/blockchain/btc/txbuild.go b/pkg/blockchain/btc/txbuild.go index 94c6596..ac06510 100644 --- a/pkg/blockchain/btc/txbuild.go +++ b/pkg/blockchain/btc/txbuild.go @@ -3,8 +3,32 @@ package btc import ( "fmt" "sort" + + "github.com/btcsuite/btcd/wire" ) +// validateFixedTxFields asserts the fixed fields the BIP-143 SIGHASH_ALL digest +// commits to, matching what the canonical builders produce: version +// wire.TxVersion, locktime 0, and final (non-RBF) input sequences. The sighash +// already binds these, so a Byzantine canonicalizer cannot make followers sign +// something inconsistent — but without the checks it could induce co-signing of +// a non-final or RBF-signalling tx and waste a signing round (ISS-006(b), +// griefing only). Shared by the withdrawal and rotation validators. +func validateFixedTxFields(tx *wire.MsgTx) error { + if tx.Version != wire.TxVersion { + return fmt.Errorf("unexpected tx version %d", tx.Version) + } + if tx.LockTime != 0 { + return fmt.Errorf("non-zero locktime %d", tx.LockTime) + } + for i, in := range tx.TxIn { + if in.Sequence != wire.MaxTxInSequenceNum { + return fmt.Errorf("input %d non-final sequence %d", i, in.Sequence) + } + } + return nil +} + // Witness/size constants for a P2WSH m-of-n input, used for fee estimation. const ( // p2wshInputVBytes is the vsize contribution of one signed P2WSH input diff --git a/pkg/blockchain/btc/withdrawal_finalizer.go b/pkg/blockchain/btc/withdrawal_finalizer.go index 55d0f8b..a219152 100644 --- a/pkg/blockchain/btc/withdrawal_finalizer.go +++ b/pkg/blockchain/btc/withdrawal_finalizer.go @@ -181,22 +181,8 @@ func (f *WithdrawalFinalizer) Validate(ctx context.Context, packed []byte, op *c if err != nil { return fmt.Errorf("btc validate: %w", err) } - // Assert the fixed fields the BIP-143 SIGHASH_ALL digest commits to, matching - // what BuildUnsignedTx produces: version wire.TxVersion, locktime 0, and final - // (non-RBF) input sequences. The sighash already binds these, so a Byzantine - // canonicalizer cannot make followers sign something inconsistent — but - // without the checks it could induce co-signing of a non-final or - // RBF-signalling tx and waste a signing round (ISS-006(b), griefing only). - if tx.Version != wire.TxVersion { - return fmt.Errorf("btc validate: unexpected tx version %d", tx.Version) - } - if tx.LockTime != 0 { - return fmt.Errorf("btc validate: non-zero locktime %d", tx.LockTime) - } - for i, in := range tx.TxIn { - if in.Sequence != wire.MaxTxInSequenceNum { - return fmt.Errorf("btc validate: input %d non-final sequence %d", i, in.Sequence) - } + if err := validateFixedTxFields(tx); err != nil { + return fmt.Errorf("btc validate: %w", err) } if n := len(tx.TxOut); n != 2 && n != 3 { return fmt.Errorf("btc validate: expected 2 or 3 outputs, got %d", n) diff --git a/pkg/blockchain/sol/vault_integration_test.go b/pkg/blockchain/sol/vault_integration_test.go index 75d0bd2..b8fef86 100644 --- a/pkg/blockchain/sol/vault_integration_test.go +++ b/pkg/blockchain/sol/vault_integration_test.go @@ -13,10 +13,14 @@ import ( "os" "path/filepath" "sort" + "strconv" "testing" "time" "github.com/gagliardetto/solana-go" + associatedtokenaccount "github.com/gagliardetto/solana-go/programs/associated-token-account" + "github.com/gagliardetto/solana-go/programs/system" + "github.com/gagliardetto/solana-go/programs/token" "github.com/gagliardetto/solana-go/rpc" "github.com/layer-3/clearnet-sdk/pkg/blockchain/sol/custody" @@ -168,6 +172,46 @@ func TestIntegrationSOL_DepositAndWithdraw(t *testing.T) { t.Fatal("withdrawal not reported executed") } + // ── SPL withdrawal flow ───────────────────────────────────────────────────── + // Create a 0-decimal mint, fund the vault's ATA, then withdraw under the same + // quorum. Exercises the SPL execute path: recipient-ATA creation + the token + // remaining-accounts. + authorityKey := loadAuthorityKey(t) + mint := setupSPLMintFundVault(ctx, t, client, authorityPub, authorityKey, programID, 100) + + var splWid [32]byte + if _, err := rand.Read(splWid[:]); err != nil { + t.Fatalf("rand spl wid: %v", err) + } + splRecipient := fixedEd25519(t, "clearnet-sdk/sol-itest/spl-recipient/"+hex.EncodeToString(splWid[:4])) + splRecipientPub, _ := solanaPub(splRecipient) + splOp := &core.WithdrawalOp{Recipient: splRecipientPub.String(), L1Asset: mint.String(), Amount: decimal.NewFromInt(40)} + + splPacked, err := finalizers[0].Pack(ctx, splOp, splWid) + if err != nil { + t.Fatalf("SPL Pack: %v", err) + } + splShares := make([][]byte, 0, len(finalizers)) + for i, f := range finalizers { + if err := f.Validate(ctx, splPacked, splOp, splWid); err != nil { + t.Fatalf("SPL Validate[%d]: %v", i, err) + } + s, e := f.Sign(ctx, splPacked) + if e != nil { + t.Fatalf("SPL Sign[%d]: %v", i, e) + } + splShares = append(splShares, s) + } + if _, err := finalizers[0].Submit(ctx, splPacked, splShares); err != nil { + t.Fatalf("SPL Submit: %v", err) + } + recipientATA, _, err := solana.FindAssociatedTokenAddress(splRecipientPub, mint) + if err != nil { + t.Fatalf("recipient ATA: %v", err) + } + waitTokenBalance(ctx, t, client, recipientATA, 40) + t.Logf("SPL withdrawal credited 40 tokens to %s", recipientATA) + // ── Rotation flow ───────────────────────────────────────────────────────── // Config is a singleton, so rotate to the (deterministic) rotated set and // restore the original — both directions exercised. A run that dies between @@ -314,6 +358,98 @@ func loadAuthority(t *testing.T) sign.Signer { return ks } +// loadAuthorityKey reads the vendored upgrade-authority keypair as a raw Solana +// private key (for signing token-setup txs that need more than the fee payer). +func loadAuthorityKey(t *testing.T) solana.PrivateKey { + t.Helper() + path := filepath.Join("..", "..", "..", "devnet", "sol-upgrade-authority.json") + raw, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read authority keypair: %v", err) + } + var b []byte + if err := json.Unmarshal(raw, &b); err != nil { + t.Fatalf("parse authority keypair: %v", err) + } + return solana.PrivateKey(b) +} + +// setupSPLMintFundVault creates a fresh 0-decimal mint (authority = mint +// authority), creates the vault's ATA, and mints `amount` tokens into it — so a +// subsequent SPL withdrawal has a funded vault to draw from. Returns the mint. +func setupSPLMintFundVault(ctx context.Context, t *testing.T, client *rpc.Client, authorityPub solana.PublicKey, authorityKey solana.PrivateKey, programID solana.PublicKey, amount uint64) solana.PublicKey { + t.Helper() + mintKP, err := solana.NewRandomPrivateKey() + if err != nil { + t.Fatalf("mint keypair: %v", err) + } + mintPub := mintKP.PublicKey() + + rent, err := client.GetMinimumBalanceForRentExemption(ctx, token.MINT_SIZE, rpc.CommitmentConfirmed) + if err != nil { + t.Fatalf("rent exemption: %v", err) + } + vaultATA, _, err := solana.FindAssociatedTokenAddress(VaultPDA(programID), mintPub) + if err != nil { + t.Fatalf("vault ATA: %v", err) + } + + ixs := []solana.Instruction{ + system.NewCreateAccountInstruction(rent, token.MINT_SIZE, solana.TokenProgramID, authorityPub, mintPub).Build(), + token.NewInitializeMint2Instruction(0, authorityPub, authorityPub, mintPub).Build(), + associatedtokenaccount.NewCreateInstruction(authorityPub, VaultPDA(programID), mintPub).Build(), + token.NewMintToInstruction(amount, mintPub, vaultATA, authorityPub, nil).Build(), + } + keys := map[solana.PublicKey]solana.PrivateKey{authorityPub: authorityKey, mintPub: mintKP} + sendMultiSigned(ctx, t, client, ixs, authorityPub, keys) + waitTokenBalance(ctx, t, client, vaultATA, amount) + return mintPub +} + +// sendMultiSigned builds, signs (with every key the message needs), and sends a +// transaction, then waits for the vault-ATA balance to reflect it via the +// caller's own wait. +func sendMultiSigned(ctx context.Context, t *testing.T, client *rpc.Client, ixs []solana.Instruction, payer solana.PublicKey, keys map[solana.PublicKey]solana.PrivateKey) { + t.Helper() + bh, err := client.GetLatestBlockhash(ctx, rpc.CommitmentConfirmed) + if err != nil { + t.Fatalf("blockhash: %v", err) + } + tx, err := solana.NewTransaction(ixs, bh.Value.Blockhash, solana.TransactionPayer(payer)) + if err != nil { + t.Fatalf("build tx: %v", err) + } + if _, err := tx.Sign(func(pub solana.PublicKey) *solana.PrivateKey { + if k, ok := keys[pub]; ok { + return &k + } + return nil + }); err != nil { + t.Fatalf("sign tx: %v", err) + } + if _, err := client.SendTransactionWithOpts(ctx, tx, rpc.TransactionOpts{PreflightCommitment: rpc.CommitmentConfirmed}); err != nil { + t.Fatalf("send tx: %v", err) + } +} + +// waitTokenBalance blocks until the SPL token account holds at least min. +func waitTokenBalance(ctx context.Context, t *testing.T, client *rpc.Client, account solana.PublicKey, min uint64) { + t.Helper() + deadline := time.Now().Add(30 * time.Second) + for { + res, err := client.GetTokenAccountBalance(ctx, account, rpc.CommitmentConfirmed) + if err == nil && res != nil && res.Value != nil { + if v, perr := strconv.ParseUint(res.Value.Amount, 10, 64); perr == nil && v >= min { + return + } + } + if time.Now().After(deadline) { + t.Fatalf("token account %s did not reach %d in time", account, min) + } + time.Sleep(time.Second) + } +} + func fixedEd25519(t *testing.T, seedStr string) sign.Signer { t.Helper() seed := sha256.Sum256([]byte(seedStr)) diff --git a/pkg/blockchain/sol/withdrawal_finalizer_test.go b/pkg/blockchain/sol/withdrawal_finalizer_test.go new file mode 100644 index 0000000..8737bb1 --- /dev/null +++ b/pkg/blockchain/sol/withdrawal_finalizer_test.go @@ -0,0 +1,74 @@ +package sol + +import ( + "crypto/ed25519" + "crypto/rand" + "testing" + + "github.com/gagliardetto/solana-go" + + "github.com/layer-3/clearnet-sdk/pkg/sign" +) + +func mustEd25519Signer(t *testing.T) sign.Signer { + t.Helper() + _, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatalf("gen ed25519: %v", err) + } + s, err := sign.NewKeySignerFromEd25519(priv) + if err != nil { + t.Fatalf("ed25519 signer: %v", err) + } + return s +} + +// TestBuildExecuteIx_SPLAccounts pins the account shape of the execute +// instruction: native SOL uses the program's base account list, while an SPL +// withdrawal appends the token remaining-accounts (token program, vault ATA, +// recipient ATA) the program expects. Devnet-free — buildExecuteIx is pure given +// the keys. +func TestBuildExecuteIx_SPLAccounts(t *testing.T) { + programID := solana.MustPublicKeyFromBase58("98eVpih8X9CAcgU9bzNB9V7VtkRrnFZUmqzEnsq7cfmg") + f, err := NewWithdrawalFinalizer("http://127.0.0.1:8899", programID, mustEd25519Signer(t), mustEd25519Signer(t), Config{ChainID: 1}) + if err != nil { + t.Fatalf("NewWithdrawalFinalizer: %v", err) + } + + to := solana.NewWallet().PublicKey() + mint := solana.NewWallet().PublicKey() + var wid [32]byte + wid[0], wid[31] = 0x5A, 0x01 + + // Native: base account list, no token accounts. + nativeIx, err := f.buildExecuteIx(to, solana.PublicKey{}, 100, wid, 2) + if err != nil { + t.Fatalf("native buildExecuteIx: %v", err) + } + base := len(nativeIx.Accounts()) + + // SPL: base + 3 token remaining-accounts. + splIx, err := f.buildExecuteIx(to, mint, 100, wid, 3) + if err != nil { + t.Fatalf("spl buildExecuteIx: %v", err) + } + splAccts := splIx.Accounts() + if len(splAccts) != base+3 { + t.Fatalf("SPL accounts = %d, want base+3 (%d)", len(splAccts), base+3) + } + + vaultATA, _, err := solana.FindAssociatedTokenAddress(f.vaultPDA, mint) + if err != nil { + t.Fatal(err) + } + recipientATA, _, err := solana.FindAssociatedTokenAddress(to, mint) + if err != nil { + t.Fatal(err) + } + want := []solana.PublicKey{solana.TokenProgramID, vaultATA, recipientATA} + for i, w := range want { + if got := splAccts[base+i].PublicKey; got != w { + t.Errorf("SPL remaining account %d = %s, want %s", i, got, w) + } + } +} diff --git a/pkg/blockchain/xrpl/validate_test.go b/pkg/blockchain/xrpl/validate_test.go new file mode 100644 index 0000000..792cb8e --- /dev/null +++ b/pkg/blockchain/xrpl/validate_test.go @@ -0,0 +1,182 @@ +package xrpl + +import ( + "context" + "crypto/ed25519" + "crypto/rand" + "encoding/hex" + "encoding/json" + "strings" + "testing" + + "github.com/Peersyst/xrpl-go/xrpl/transaction" + + "github.com/layer-3/clearnet-sdk/pkg/core" + "github.com/layer-3/clearnet-sdk/pkg/decimal" + "github.com/layer-3/clearnet-sdk/pkg/sign" +) + +// roundTrip marshals then unmarshals a flatTx, reproducing the numeric shape +// (float64) the real Validate path sees after decoding the packed bytes. +func roundTrip(t *testing.T, flat transaction.FlatTransaction) transaction.FlatTransaction { + t.Helper() + b, err := json.Marshal(flat) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var out transaction.FlatTransaction + if err := json.Unmarshal(b, &out); err != nil { + t.Fatalf("unmarshal: %v", err) + } + return out +} + +// TestValidateCanonical_FlagsRejected covers the ISS-002 guard: a present Flags +// must be numeric zero; a non-zero or non-numeric value (tfPartialPayment is +// 131072) is rejected so an issued-currency withdrawal cannot underdeliver. +func TestValidateCanonical_FlagsRejected(t *testing.T) { + const vault = "rVaULtAdd1111111111111111111111111" + op := &core.WithdrawalOp{Recipient: "rDeST1111111111111111111111111111", Amount: decimal.NewFromInt(1_000_000)} + amt, err := BuildAmount(op) + if err != nil { + t.Fatalf("BuildAmount: %v", err) + } + var wid [32]byte + wid[0], wid[31] = 0xAB, 0xCD + + base := func() transaction.FlatTransaction { + return transaction.FlatTransaction{ + "TransactionType": "Payment", + "Account": vault, + "Destination": op.Recipient, + "Amount": amt, + "InvoiceID": strings.ToUpper(hex.EncodeToString(wid[:])), + "TicketSequence": uint32(5), + "Sequence": uint32(0), + "Fee": uint32(100), + } + } + + if err := ValidateCanonical(roundTrip(t, base()), op, wid, vault); err != nil { + t.Fatalf("valid canonical rejected: %v", err) + } + + zero := base() + zero["Flags"] = uint32(0) + if err := ValidateCanonical(roundTrip(t, zero), op, wid, vault); err != nil { + t.Errorf("Flags=0 rejected: %v", err) + } + + partial := base() + partial["Flags"] = uint32(131072) // tfPartialPayment + if err := ValidateCanonical(roundTrip(t, partial), op, wid, vault); err == nil { + t.Error("tfPartialPayment Flags accepted") + } + + nonNumeric := base() + nonNumeric["Flags"] = "deadbeef" + if err := ValidateCanonical(roundTrip(t, nonNumeric), op, wid, vault); err == nil { + t.Error("non-numeric Flags accepted") + } +} + +// TestValidateCanonicalRotation_FlagsRejected is the SignerListSet analogue. +func TestValidateCanonicalRotation_FlagsRejected(t *testing.T) { + const vault = "rVaULtAdd1111111111111111111111111" + newSigners := []string{"rAAA1111111111111111111111111111aa", "rBBB1111111111111111111111111111bb"} + entries, err := signerEntries(newSigners, 2) + if err != nil { + t.Fatalf("signerEntries: %v", err) + } + + base := func() transaction.FlatTransaction { + return transaction.FlatTransaction{ + "TransactionType": "SignerListSet", + "Account": vault, + "SignerQuorum": uint32(2), + "SignerEntries": entries, + "Sequence": uint32(1), + "Fee": uint32(100), + } + } + + if err := validateCanonicalRotation(roundTrip(t, base()), newSigners, 2, vault); err != nil { + t.Fatalf("valid rotation rejected: %v", err) + } + + partial := base() + partial["Flags"] = uint32(131072) + if err := validateCanonicalRotation(roundTrip(t, partial), newSigners, 2, vault); err == nil { + t.Error("non-zero Flags accepted on rotation") + } +} + +// TestFilterBlobsByAuthorized covers the live-SignerList filter that combineLive +// applies: blobs from non-authorized signers are dropped, and a duplicate signer +// is kept once. Uses real multi-sign blobs (no devnet). +func TestFilterBlobsByAuthorized(t *testing.T) { + ctx := context.Background() + signerA, idA := newXRPLSigner(t) + signerB, idB := newXRPLSigner(t) + + blobA := multisignBlob(t, ctx, signerA, idA, idB.ClassicAddress) + blobB := multisignBlob(t, ctx, signerB, idB, idA.ClassicAddress) + + // blobSignerAccount recovers the inner signer's account. + if got, err := blobSignerAccount(blobA); err != nil || got != idA.ClassicAddress { + t.Fatalf("blobSignerAccount(blobA) = %q, %v; want %s", got, err, idA.ClassicAddress) + } + + // Only A authorized: B dropped. + authorized := map[string]struct{}{idA.ClassicAddress: {}} + out, err := filterBlobsByAuthorized([]string{blobA, blobB}, authorized) + if err != nil { + t.Fatalf("filter: %v", err) + } + if len(out) != 1 || out[0] != blobA { + t.Fatalf("filter kept %d blobs, want only blobA", len(out)) + } + + // Duplicate A kept once. + out, err = filterBlobsByAuthorized([]string{blobA, blobA}, map[string]struct{}{idA.ClassicAddress: {}}) + if err != nil { + t.Fatalf("filter dup: %v", err) + } + if len(out) != 1 { + t.Fatalf("dup signer kept %d times, want 1", len(out)) + } +} + +func newXRPLSigner(t *testing.T) (sign.Signer, Identity) { + t.Helper() + _, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatalf("gen ed25519: %v", err) + } + s, err := sign.NewKeySignerFromEd25519(priv) + if err != nil { + t.Fatalf("ed25519 signer: %v", err) + } + id, err := DeriveIdentity(s) + if err != nil { + t.Fatalf("DeriveIdentity: %v", err) + } + return s, id +} + +func multisignBlob(t *testing.T, ctx context.Context, s sign.Signer, id Identity, dest string) string { + t.Helper() + flat := transaction.FlatTransaction{ + "TransactionType": "Payment", + "Account": id.ClassicAddress, + "Destination": dest, + "Amount": "1000000", + "Fee": "100", + "Sequence": uint32(1), + } + blob, err := signMultisig(ctx, s, id, flat) + if err != nil { + t.Fatalf("signMultisig: %v", err) + } + return blob +} From b0a1c725d7bd5c21f1b8dce7a93c2ef6c93472e7 Mon Sep 17 00:00:00 2001 From: Anton Filonenko Date: Wed, 17 Jun 2026 15:08:24 +0300 Subject: [PATCH 11/15] feat(sol): expose VaultLookupAddresses; exercise SPL withdrawal over an ALT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VaultLookupAddresses returns the invariant accounts of the execute instruction — the lookup-table-eligible set that lets large quorums fit a v0 transaction. Centralizing it in the SDK keeps the table in lockstep with the instruction's account layout, instead of every consumer hand-copying the list. The SPL-withdrawal integration case now builds a real Address Lookup Table from VaultLookupAddresses and withdraws over it, exercising the v0/ALT submit path end to end. The table's recent_slot is taken at finalized commitment, the only slot guaranteed to already be in the SlotHashes sysvar (the current slot can equal the execution slot and is rejected as not recent). --- pkg/blockchain/sol/program.go | 28 ++++++++ pkg/blockchain/sol/vault_integration_test.go | 67 +++++++++++++++++--- 2 files changed, 87 insertions(+), 8 deletions(-) diff --git a/pkg/blockchain/sol/program.go b/pkg/blockchain/sol/program.go index dcc9e9b..e8d0419 100644 --- a/pkg/blockchain/sol/program.go +++ b/pkg/blockchain/sol/program.go @@ -104,6 +104,34 @@ func eventAuthorityPDA(programID solana.PublicKey) solana.PublicKey { return pk } +// VaultLookupAddresses returns the invariant accounts of the execute +// instruction — the ones that recur across every withdrawal and are therefore +// eligible to populate an Address Lookup Table, letting large quorums fit a v0 +// transaction (set the table via WithdrawalFinalizer's Config.AddressLookupTable). +// A zero mint returns the native-SOL set; a non-zero mint adds the token program +// and the vault's associated token account. Per-withdrawal accounts (the +// recipient, its token account, the Withdrawal PDA, the fee payer) vary each call +// and are intentionally excluded. +// +// Build the lookup table from this set so it stays in lockstep with the +// instruction's account layout (both live here, in the SDK). +func VaultLookupAddresses(programID, mint solana.PublicKey) []solana.PublicKey { + addrs := []solana.PublicKey{ + ConfigPDA(programID), + VaultPDA(programID), + eventAuthorityPDA(programID), + solana.SystemProgramID, + solana.SysVarInstructionsPubkey, + } + if !mint.IsZero() { + addrs = append(addrs, solana.TokenProgramID) + if vaultATA, _, err := solana.FindAssociatedTokenAddress(VaultPDA(programID), mint); err == nil { + addrs = append(addrs, vaultATA) + } + } + return addrs +} + // BuildEd25519Instruction frames the quorum's signatures for the native // Ed25519SigVerify precompile, all offsets self-referencing (instruction index // 0xFFFF) so the verified data cannot be smuggled from another instruction. diff --git a/pkg/blockchain/sol/vault_integration_test.go b/pkg/blockchain/sol/vault_integration_test.go index b8fef86..a80da52 100644 --- a/pkg/blockchain/sol/vault_integration_test.go +++ b/pkg/blockchain/sol/vault_integration_test.go @@ -18,6 +18,7 @@ import ( "time" "github.com/gagliardetto/solana-go" + addresslookuptable "github.com/gagliardetto/solana-go/programs/address-lookup-table" associatedtokenaccount "github.com/gagliardetto/solana-go/programs/associated-token-account" "github.com/gagliardetto/solana-go/programs/system" "github.com/gagliardetto/solana-go/programs/token" @@ -172,12 +173,26 @@ func TestIntegrationSOL_DepositAndWithdraw(t *testing.T) { t.Fatal("withdrawal not reported executed") } - // ── SPL withdrawal flow ───────────────────────────────────────────────────── - // Create a 0-decimal mint, fund the vault's ATA, then withdraw under the same - // quorum. Exercises the SPL execute path: recipient-ATA creation + the token - // remaining-accounts. + // ── SPL withdrawal flow (over a v0 transaction + ALT) ─────────────────────── + // Create a 0-decimal mint, fund the vault's ATA, build an Address Lookup Table + // over the static execute accounts, then withdraw under the same quorum with + // the ALT configured. Exercises the SPL execute path (recipient-ATA creation + + // token remaining-accounts) and the v0/ALT submit path together. authorityKey := loadAuthorityKey(t) mint := setupSPLMintFundVault(ctx, t, client, authorityPub, authorityKey, programID, 100) + // Build the ALT from the SDK's invariant account set — the same knowledge + // buildExecuteIx uses, so the table can't drift from the instruction layout. + alt := createActiveALT(ctx, t, client, authorityPub, authorityKey, VaultLookupAddresses(programID, mint)) + + splCfg := Config{ChainID: solChainID, Commitment: rpc.CommitmentConfirmed, AddressLookupTable: alt} + splFinalizers := make([]*WithdrawalFinalizer, solSignerCount) + for i, s := range signers { + f, e := NewWithdrawalFinalizer(rpcURL, programID, s, authority, splCfg) + if e != nil { + t.Fatalf("NewWithdrawalFinalizer (spl) %d: %v", i, e) + } + splFinalizers[i] = f + } var splWid [32]byte if _, err := rand.Read(splWid[:]); err != nil { @@ -187,12 +202,12 @@ func TestIntegrationSOL_DepositAndWithdraw(t *testing.T) { splRecipientPub, _ := solanaPub(splRecipient) splOp := &core.WithdrawalOp{Recipient: splRecipientPub.String(), L1Asset: mint.String(), Amount: decimal.NewFromInt(40)} - splPacked, err := finalizers[0].Pack(ctx, splOp, splWid) + splPacked, err := splFinalizers[0].Pack(ctx, splOp, splWid) if err != nil { t.Fatalf("SPL Pack: %v", err) } - splShares := make([][]byte, 0, len(finalizers)) - for i, f := range finalizers { + splShares := make([][]byte, 0, len(splFinalizers)) + for i, f := range splFinalizers { if err := f.Validate(ctx, splPacked, splOp, splWid); err != nil { t.Fatalf("SPL Validate[%d]: %v", i, err) } @@ -202,7 +217,7 @@ func TestIntegrationSOL_DepositAndWithdraw(t *testing.T) { } splShares = append(splShares, s) } - if _, err := finalizers[0].Submit(ctx, splPacked, splShares); err != nil { + if _, err := splFinalizers[0].Submit(ctx, splPacked, splShares); err != nil { t.Fatalf("SPL Submit: %v", err) } recipientATA, _, err := solana.FindAssociatedTokenAddress(splRecipientPub, mint) @@ -406,6 +421,42 @@ func setupSPLMintFundVault(ctx context.Context, t *testing.T, client *rpc.Client return mintPub } +// createActiveALT creates an Address Lookup Table, extends it with addresses, +// and waits until it is populated and old enough to use in a v0 transaction. +func createActiveALT(ctx context.Context, t *testing.T, client *rpc.Client, authorityPub solana.PublicKey, authorityKey solana.PrivateKey, addresses []solana.PublicKey) solana.PublicKey { + t.Helper() + // CreateLookupTable requires recent_slot to be a slot already in the + // SlotHashes sysvar — i.e. strictly in the past. The finalized slot always + // lags the execution slot, so it is safely present; the current (confirmed) + // slot can equal the execution slot and is rejected ("not a recent slot"). + slot, err := client.GetSlot(ctx, rpc.CommitmentFinalized) + if err != nil { + t.Fatalf("get slot: %v", err) + } + createIx, altAddr, err := addresslookuptable.NewCreateLookupTableInstruction(authorityPub, authorityPub, slot) + if err != nil { + t.Fatalf("create ALT ix: %v", err) + } + extendIx := addresslookuptable.NewExtendLookupTableInstruction(altAddr, authorityPub, authorityPub, addresses).Build() + sendMultiSigned(ctx, t, client, []solana.Instruction{createIx.Build(), extendIx}, authorityPub, map[solana.PublicKey]solana.PrivateKey{authorityPub: authorityKey}) + + // Wait until the table is readable with all addresses, then let it age a slot + // (a lookup table cannot be used in the same slot it was extended). + deadline := time.Now().Add(30 * time.Second) + for { + state, err := addresslookuptable.GetAddressLookupTable(ctx, client, altAddr) + if err == nil && state != nil && len(state.Addresses) >= len(addresses) { + break + } + if time.Now().After(deadline) { + t.Fatalf("ALT %s not populated in time", altAddr) + } + time.Sleep(time.Second) + } + time.Sleep(2 * time.Second) + return altAddr +} + // sendMultiSigned builds, signs (with every key the message needs), and sends a // transaction, then waits for the vault-ATA balance to reflect it via the // caller's own wait. From 59d06ad6e2bf0d1fffafa687a1b438cb768bb475 Mon Sep 17 00:00:00 2001 From: Anton Filonenko Date: Wed, 17 Jun 2026 15:21:41 +0300 Subject: [PATCH 12/15] ci: add unit + integration test workflows; pin devnet images MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reusable test-go (go test -race ./...) and test-integration (make devnet → make integration → make devnet-down) workflows, invoked by per-event callers on pull requests and pushes to master. Test-only — no lint, build, or publish steps. Pin every devnet image to its manifest-list digest so the integration job runs the exact versions the suite was validated against and a moving upstream tag cannot break CI out from under unrelated changes. --- .github/workflows/main-pr.yml | 17 +++++++++++++ .github/workflows/main-push.yml | 17 +++++++++++++ .github/workflows/test-go.yml | 25 ++++++++++++++++++ .github/workflows/test-integration.yml | 35 ++++++++++++++++++++++++++ devnet/docker-compose.yml | 8 +++--- 5 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/main-pr.yml create mode 100644 .github/workflows/main-push.yml create mode 100644 .github/workflows/test-go.yml create mode 100644 .github/workflows/test-integration.yml diff --git a/.github/workflows/main-pr.yml b/.github/workflows/main-pr.yml new file mode 100644 index 0000000..b8d7f2a --- /dev/null +++ b/.github/workflows/main-pr.yml @@ -0,0 +1,17 @@ +name: PR on master + +on: + pull_request: + branches: [master] + +permissions: + contents: read + +jobs: + unit: + name: Unit + uses: ./.github/workflows/test-go.yml + + integration: + name: Integration + uses: ./.github/workflows/test-integration.yml diff --git a/.github/workflows/main-push.yml b/.github/workflows/main-push.yml new file mode 100644 index 0000000..08b4b7e --- /dev/null +++ b/.github/workflows/main-push.yml @@ -0,0 +1,17 @@ +name: Push on master + +on: + push: + branches: [master] + +permissions: + contents: read + +jobs: + unit: + name: Unit + uses: ./.github/workflows/test-go.yml + + integration: + name: Integration + uses: ./.github/workflows/test-integration.yml diff --git a/.github/workflows/test-go.yml b/.github/workflows/test-go.yml new file mode 100644 index 0000000..9784c6c --- /dev/null +++ b/.github/workflows/test-go.yml @@ -0,0 +1,25 @@ +name: Test (Unit) + +# Reusable: runs the unit suite (go test -race ./...). Called by the per-event +# workflows (PR, push to master). +on: + workflow_call: + +permissions: + contents: read + +jobs: + unit: + name: Unit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Test + run: make test diff --git a/.github/workflows/test-integration.yml b/.github/workflows/test-integration.yml new file mode 100644 index 0000000..6455ec3 --- /dev/null +++ b/.github/workflows/test-integration.yml @@ -0,0 +1,35 @@ +name: Test (Integration) + +# Reusable: brings up the devnet (anvil + bitcoind + rippled + solana) and runs +# the integration suite against it. Called by the per-event workflows. +on: + workflow_call: + +permissions: + contents: read + +jobs: + integration: + name: Integration + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v6 + + # setup-go precedes `make devnet`: the readiness gate (devnet/wait) runs + # via `go run`, and the integration tests need the toolchain too. + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Bring up devnet + run: make devnet + + - name: Integration tests + run: make integration + + - name: Tear down devnet + if: always() + run: make devnet-down diff --git a/devnet/docker-compose.yml b/devnet/docker-compose.yml index b5fc427..d09d0ab 100644 --- a/devnet/docker-compose.yml +++ b/devnet/docker-compose.yml @@ -9,13 +9,13 @@ # these nodes (fresh keys/accounts/contract each run). See devnet/README.md. services: anvil: - image: ghcr.io/foundry-rs/foundry:latest + image: ghcr.io/foundry-rs/foundry:latest@sha256:8347b728d5d393dac1c018691b36f506d23b9dcd78341d40ea0fcb11c3a19cdd entrypoint: ["anvil", "--host", "0.0.0.0", "--chain-id", "31337"] ports: - "8545:8545" bitcoind: - image: ruimarinho/bitcoin-core:24 + image: ruimarinho/bitcoin-core:24@sha256:79dd32455cf8c268c63e5d0114cc9882a8857e942b1d17a6b8ec40a6d44e3981 command: - -regtest=1 - -server=1 @@ -34,7 +34,7 @@ services: # validator needs AVX (not emulable on Apple silicon); beeman's image is # multi-arch and pulls the native arm64 build. solana: - image: beeman/solana-test-validator:3.1.14 + image: beeman/solana-test-validator:3.1.14@sha256:92350b9cda62938aaca12b63ca81d5db217f6a96ff415215e0346904957cd5db # The Agave validator uses io_uring; Docker Desktop's default seccomp # profile blocks io_uring_setup (EPERM) even though the VM kernel supports # it, so the validator aborts. Allow it. @@ -58,7 +58,7 @@ services: - "8900:8900" rippled: - image: xrpllabsofficial/xrpld:latest + image: xrpllabsofficial/xrpld:latest@sha256:5bed71463491c098bdb7f057a95c5a77bb71c36ed6b5009ff8397ce076d0e22e platform: linux/amd64 command: ["-a", "--start"] volumes: From 6a8e411e3413737dc469c8da0bb718c7440ee51e Mon Sep 17 00:00:00 2001 From: Anton Filonenko Date: Wed, 17 Jun 2026 17:09:58 +0300 Subject: [PATCH 13/15] fix(p2p): tolerant ReceiptAck decode, auth deadline, logger KV aliasing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - protocol: ReceiptAck decoder accepts >=2 array elements and skips the trailing fields instead of requiring exactly 2. A real clearnode emits a wider (6-element) ack, which the strict reader rejected — this blocked the receipt client from talking to production. The encoder still writes the 2-element form, so the golden vector is unchanged. - auth: the server bounds the whole handshake with a deadline (challenge write + response read), matching the receipt server, so a stalled peer cannot pin a handler goroutine. - log: ZapLogger.WithKV clones the parent's key-value slice before appending; the bare append reused the parent's backing array, racing and corrupting sibling loggers' KV under fan-out. --- go.sum | 2 ++ pkg/log/zap_logger.go | 7 ++++++- pkg/log/zap_logger_test.go | 27 +++++++++++++++++++++++++++ pkg/p2p/auth/server.go | 11 +++++++++++ pkg/p2p/protocol/wire.go | 19 +++++++++++++++++-- pkg/p2p/protocol/wire_test.go | 17 +++++++++++++++++ 6 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 pkg/log/zap_logger_test.go diff --git a/go.sum b/go.sum index 3d6eba8..ea6803c 100644 --- a/go.sum +++ b/go.sum @@ -104,6 +104,8 @@ github.com/gagliardetto/anchor-go v1.0.0 h1:YNt9I/9NOrNzz5uuzfzByAcbp39Ft07w63iP github.com/gagliardetto/anchor-go v1.0.0/go.mod h1:X6c9bx9JnmwNiyy8hmV5pAsq1c/zzPvkdzeq9/qmlCg= github.com/gagliardetto/binary v0.8.0 h1:U9ahc45v9HW0d15LoN++vIXSJyqR/pWw8DDlhd7zvxg= github.com/gagliardetto/binary v0.8.0/go.mod h1:2tfj51g5o9dnvsc+fL3Jxr22MuWzYXwx9wEoN0XQ7/c= +github.com/gagliardetto/gofuzz v1.2.2 h1:XL/8qDMzcgvR4+CyRQW9UGdwPRPMHVJfqQ/uMvSUuQw= +github.com/gagliardetto/gofuzz v1.2.2/go.mod h1:bkH/3hYLZrMLbfYWA0pWzXmi5TTRZnu4pMGZBkqMKvY= github.com/gagliardetto/solana-go v1.21.0 h1:ZswwwDOFuD/7Hk/k3b6dyLetmvEJuyLmQU7xIziCZgo= github.com/gagliardetto/solana-go v1.21.0/go.mod h1:coSlnlih2oLWNbkVDeTiMvodhnheX/oaJ7h53mei4e8= github.com/gagliardetto/treeout v0.1.4 h1:ozeYerrLCmCubo1TcIjFiOWTTGteOOHND1twdFpgwaw= diff --git a/pkg/log/zap_logger.go b/pkg/log/zap_logger.go index dfd4038..2e5441b 100644 --- a/pkg/log/zap_logger.go +++ b/pkg/log/zap_logger.go @@ -109,9 +109,14 @@ func (l *ZapLogger) log(level Level, msg string, keysAndValues ...any) { // WithKV returns a new ZapLogger with the key-value pair added to all future log messages. func (l *ZapLogger) WithKV(key string, value any) Logger { + // Clone before appending: a bare append(l.keysAndValues, …) reuses the + // parent's backing array when it has spare capacity, so sibling WithKV calls + // off the same logger would race and corrupt each other's KV set (and the + // span attributes derived from GetAllKV). + kv := append(append([]any(nil), l.keysAndValues...), key, value) return &ZapLogger{ lg: l.lg.With(key, value), - keysAndValues: append(l.keysAndValues, key, value), + keysAndValues: kv, } } diff --git a/pkg/log/zap_logger_test.go b/pkg/log/zap_logger_test.go new file mode 100644 index 0000000..24488ff --- /dev/null +++ b/pkg/log/zap_logger_test.go @@ -0,0 +1,27 @@ +package log + +import "testing" + +// TestZapLogger_WithKV_NoAlias guards against the parent's KV backing array +// being shared with children: two WithKV calls off the same logger must not +// corrupt each other. Deterministic (no -race needed): the parent is given +// spare capacity so a bare append would write both children into the same slot. +func TestZapLogger_WithKV_NoAlias(t *testing.T) { + base := NewZapLogger(Config{Level: LevelError}).(*ZapLogger) + kv := make([]any, 2, 8) // len 2, spare cap → naive append would alias + kv[0], kv[1] = "base", "0" + base.keysAndValues = kv + + c1 := base.WithKV("x", "1").(*ZapLogger) + c2 := base.WithKV("y", "2").(*ZapLogger) + + if got := c1.GetAllKV(); len(got) != 4 || got[2] != "x" || got[3] != "1" { + t.Errorf("c1 KV = %v, want [base 0 x 1]", got) + } + if got := c2.GetAllKV(); len(got) != 4 || got[2] != "y" || got[3] != "2" { + t.Errorf("c2 KV = %v, want [base 0 y 2]", got) + } + if got := base.GetAllKV(); len(got) != 2 { + t.Errorf("base KV mutated: %v", got) + } +} diff --git a/pkg/p2p/auth/server.go b/pkg/p2p/auth/server.go index 7491637..85d9326 100644 --- a/pkg/p2p/auth/server.go +++ b/pkg/p2p/auth/server.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "strings" + "time" "github.com/ethereum/go-ethereum/common" ethcrypto "github.com/ethereum/go-ethereum/crypto" @@ -92,10 +93,20 @@ func (s *Server) Register(h host.Host) { h.SetStreamHandler(protocol.ID(p2pproto.ProtocolAuth), s.HandleAuth) } +// handshakeTimeout bounds one server-side handshake end to end (challenge write +// + response read), so a stalled peer cannot pin the handler goroutine. +const handshakeTimeout = 10 * time.Second + // HandleAuth is the stream handler for /ynp/auth/1.0.0. func (s *Server) HandleAuth(stream network.Stream) { defer stream.Close() conn := stream.Conn() + // Bound the whole handshake: the server writes the challenge then reads the + // response, so the deadline covers both directions (slowloris guard). + if err := stream.SetDeadline(time.Now().Add(handshakeTimeout)); err != nil { + s.logger.Debug("auth set deadline failed", "peer", conn.RemotePeer().ShortString(), "error", err) + return + } res, err := s.verify(stream, conn.RemotePublicKey()) if err != nil { s.logger.Debug("auth handshake failed", "peer", conn.RemotePeer().ShortString(), "error", err) diff --git a/pkg/p2p/protocol/wire.go b/pkg/p2p/protocol/wire.go index e891ef3..61312e1 100644 --- a/pkg/p2p/protocol/wire.go +++ b/pkg/p2p/protocol/wire.go @@ -4,6 +4,7 @@ import ( "fmt" "io" + "github.com/ipfs/go-cid" cbg "github.com/whyrusleeping/cbor-gen" ) @@ -198,8 +199,14 @@ func (t *ReceiptAck) UnmarshalCBOR(r io.Reader) error { if err != nil { return err } - if maj != cbg.MajArray || extra != 2 { - return fmt.Errorf("ReceiptAck: expected 2-element CBOR array") + // Accept >= 2 elements and skip any trailing fields: a real clearnode emits + // a wider ack (6 elements) and forward-compatible readers must not reject it. + // Only the first two — Accepted, Reason — are part of this contract. + if maj != cbg.MajArray { + return fmt.Errorf("ReceiptAck: expected CBOR array, got major %d", maj) + } + if extra < 2 { + return fmt.Errorf("ReceiptAck: expected >=2 elements, got %d", extra) } // Accepted (CBOR simple value: 20 = false, 21 = true). bmaj, bminor, err := cr.ReadHeader() @@ -222,5 +229,13 @@ func (t *ReceiptAck) UnmarshalCBOR(r io.Reader) error { return fmt.Errorf("ReceiptAck.Reason: %w", err) } t.Reason = reason + // Skip any trailing elements (ScanForLinks walks exactly one CBOR item per + // call, recursing into arrays/maps); the CID sink is a no-op. + noLink := func(cid.Cid) {} + for i := uint64(2); i < extra; i++ { + if err := cbg.ScanForLinks(cr, noLink); err != nil { + return fmt.Errorf("ReceiptAck: skip trailing element %d: %w", i, err) + } + } return nil } diff --git a/pkg/p2p/protocol/wire_test.go b/pkg/p2p/protocol/wire_test.go index c29838a..9c29414 100644 --- a/pkg/p2p/protocol/wire_test.go +++ b/pkg/p2p/protocol/wire_test.go @@ -112,6 +112,23 @@ func TestWireRoundTrip(t *testing.T) { } }) + t.Run("ReceiptAck wider ack", func(t *testing.T) { + // A real clearnode emits a 6-element ack; the reader must accept it and + // take only the first two fields (Accepted, Reason), skipping the rest. + // 86 = array(6); f5 = true; 626f6b = "ok"; then 4 trailing ints 0..3. + wire, err := hex.DecodeString("86f5626f6b00010203") + if err != nil { + t.Fatal(err) + } + var out ReceiptAck + if err := out.UnmarshalCBOR(bytes.NewReader(wire)); err != nil { + t.Fatalf("decode 6-element ack: %v", err) + } + if !out.Accepted || out.Reason != "ok" { + t.Errorf("got %+v, want {Accepted:true Reason:ok}", out) + } + }) + t.Run("ReceiptAck", func(t *testing.T) { for _, in := range []*ReceiptAck{ {Accepted: true, Reason: ""}, From 57056cfb9e4f9527a22e83a046c8576ceb6668a4 Mon Sep 17 00:00:00 2001 From: Anton Filonenko Date: Wed, 17 Jun 2026 17:15:47 +0300 Subject: [PATCH 14/15] refactor(p2p): keep the generic pubsub, drop the concrete one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve the side-by-side pubsub/gossip choice in favour of the generic, type-parameterized implementation: remove the concrete *core.FinalizedWithdrawal pubsub and rename gossip → pubsub. The package now serves any cborx-envelope payload (FinalizedWithdrawal included) via Publisher[T]/Follower[T]. --- pkg/p2p/gossip/follower.go | 156 ------------------ pkg/p2p/gossip/gossip_test.go | 93 ----------- pkg/p2p/gossip/publisher.go | 80 --------- pkg/p2p/pubsub/follower.go | 81 ++++----- pkg/p2p/pubsub/publisher.go | 47 ++---- .../{gossip/gossip.go => pubsub/pubsub.go} | 22 ++- pkg/p2p/pubsub/pubsub_test.go | 17 +- 7 files changed, 66 insertions(+), 430 deletions(-) delete mode 100644 pkg/p2p/gossip/follower.go delete mode 100644 pkg/p2p/gossip/gossip_test.go delete mode 100644 pkg/p2p/gossip/publisher.go rename pkg/p2p/{gossip/gossip.go => pubsub/pubsub.go} (59%) diff --git a/pkg/p2p/gossip/follower.go b/pkg/p2p/gossip/follower.go deleted file mode 100644 index 048f338..0000000 --- a/pkg/p2p/gossip/follower.go +++ /dev/null @@ -1,156 +0,0 @@ -package gossip - -import ( - "bytes" - "context" - "fmt" - "sync" - - pubsub "github.com/libp2p/go-libp2p-pubsub" - "github.com/libp2p/go-libp2p/core/host" - "github.com/libp2p/go-libp2p/core/peer" - - "github.com/layer-3/clearnet-sdk/pkg/cborx" - "github.com/layer-3/clearnet-sdk/pkg/log" -) - -// Metrics captures Follower counters for ops dashboards. -type Metrics struct { - mu sync.Mutex - Delivered uint64 // payloads handed to the handler - DecodeErrors uint64 // envelope/CBOR decode failures - OversizeDrops uint64 // dropped for exceeding the size cap -} - -// Snapshot returns a copy of the current counters. Safe for concurrent use. -func (m *Metrics) Snapshot() Metrics { - m.mu.Lock() - defer m.mu.Unlock() - return Metrics{Delivered: m.Delivered, DecodeErrors: m.DecodeErrors, OversizeDrops: m.OversizeDrops} -} - -func (m *Metrics) inc(field *uint64) { - m.mu.Lock() - *field++ - m.mu.Unlock() -} - -// Follower subscribes to a topic on a caller-owned host and forwards each -// decoded value of type T to a Handler. -type Follower[T any, M message[T]] struct { - host host.Host - sub *pubsub.Subscription - topic *pubsub.Topic - name string - logger log.Logger - metrics *Metrics - - handlerMu sync.RWMutex - handler Handler[T] -} - -// NewFollower joins and subscribes to topic on h. A size-validator is attached -// before joining, so oversized messages are rejected by GossipSub and never -// reach the consume loop. Call Run to start consuming; register a handler with -// SetHandler before (or shortly after) Run. The caller owns h and its -// connectivity. -func NewFollower[T any, M message[T]](ctx context.Context, h host.Host, topic string, logger log.Logger) (*Follower[T, M], error) { - if logger == nil { - logger = log.NewNoopLogger() - } - ps, err := pubsub.NewGossipSub(ctx, h) - if err != nil { - return nil, fmt.Errorf("gossipsub: %w", err) - } - f := &Follower[T, M]{ - host: h, - name: topic, - logger: logger.WithName("p2p-gossip-follower").WithKV("topic", topic), - metrics: &Metrics{}, - } - if err := ps.RegisterTopicValidator(topic, f.validateSize); err != nil { - return nil, fmt.Errorf("register validator %s: %w", topic, err) - } - t, err := ps.Join(topic) - if err != nil { - return nil, fmt.Errorf("join %s: %w", topic, err) - } - sub, err := t.Subscribe() - if err != nil { - _ = t.Close() - return nil, fmt.Errorf("subscribe %s: %w", topic, err) - } - f.topic = t - f.sub = sub - f.logger.Info("gossip follower started", "peer_id", h.ID().String()) - return f, nil -} - -// SetHandler installs the handler invoked for each decoded value. Safe to call -// concurrently with Run. -func (f *Follower[T, M]) SetHandler(h Handler[T]) { - f.handlerMu.Lock() - f.handler = h - f.handlerMu.Unlock() -} - -// Metrics returns the Follower's counters handle. -func (f *Follower[T, M]) Metrics() *Metrics { return f.metrics } - -// PeerID returns the host's libp2p peer ID. -func (f *Follower[T, M]) PeerID() peer.ID { return f.host.ID() } - -// Run consumes the subscription until ctx is cancelled or the subscription -// closes. It blocks; run it in a goroutine. -func (f *Follower[T, M]) Run(ctx context.Context) { - for { - msg, err := f.sub.Next(ctx) - if err != nil { - if ctx.Err() == nil { - f.logger.Debug("subscription closed", "error", err) - } - return - } - if msg.ReceivedFrom == f.host.ID() { - continue - } - f.handle(msg) - } -} - -// Close cancels the subscription and leaves the topic. It does not close the -// host — the caller owns that. -func (f *Follower[T, M]) Close() error { - f.sub.Cancel() - return f.topic.Close() -} - -func (f *Follower[T, M]) validateSize(_ context.Context, from peer.ID, msg *pubsub.Message) bool { - if len(msg.Data) > maxMessageBytes { - f.metrics.inc(&f.metrics.OversizeDrops) - f.logger.Warn("dropping oversize message", "from", from.ShortString(), "bytes", len(msg.Data)) - return false - } - return true -} - -func (f *Follower[T, M]) handle(msg *pubsub.Message) { - var v T - var ver cborx.Version - // M(&v) converts *T to the constrained pointer type, which satisfies - // cborx's CBORUnmarshaler — decode in place into v. - if err := cborx.ReadEnvelopeStrict(bytes.NewReader(msg.Data), &ver, M(&v)); err != nil { - f.metrics.inc(&f.metrics.DecodeErrors) - f.logger.Warn("decode payload failed", "error", err) - return - } - f.handlerMu.RLock() - h := f.handler - f.handlerMu.RUnlock() - if h == nil { - f.logger.Warn("no handler registered; dropping payload") - return - } - h(&v) - f.metrics.inc(&f.metrics.Delivered) -} diff --git a/pkg/p2p/gossip/gossip_test.go b/pkg/p2p/gossip/gossip_test.go deleted file mode 100644 index e0605fe..0000000 --- a/pkg/p2p/gossip/gossip_test.go +++ /dev/null @@ -1,93 +0,0 @@ -package gossip - -import ( - "context" - "testing" - "time" - - libp2p "github.com/libp2p/go-libp2p" - "github.com/libp2p/go-libp2p/core/host" - "github.com/libp2p/go-libp2p/core/peer" - - "github.com/layer-3/clearnet-sdk/pkg/core" - p2pproto "github.com/layer-3/clearnet-sdk/pkg/p2p/protocol" -) - -// TestGossip_FinalizedWithdrawal shows the generic toolset carrying a concrete -// payload: callers name only the value type (core.FinalizedWithdrawal) and -// constraint type inference supplies the *T pointer type to Publisher/Follower. -func TestGossip_FinalizedWithdrawal(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) - defer cancel() - - hPub := newHost(t) - hSub := newHost(t) - connect(t, hPub, hSub) - - follower, err := NewFollower[core.FinalizedWithdrawal](ctx, hSub, p2pproto.TopicWithdrawals, nil) - if err != nil { - t.Fatalf("NewFollower: %v", err) - } - defer follower.Close() - - got := make(chan *core.FinalizedWithdrawal, 1) - follower.SetHandler(func(fw *core.FinalizedWithdrawal) { got <- fw }) - go follower.Run(ctx) - - pub, err := NewPublisher[core.FinalizedWithdrawal](ctx, hPub, p2pproto.TopicWithdrawals, nil) - if err != nil { - t.Fatalf("NewPublisher: %v", err) - } - defer pub.Close() - - if err := pub.WaitForPeers(ctx, 1, 10*time.Second); err != nil { - t.Fatalf("WaitForPeers: %v", err) - } - - want := &core.FinalizedWithdrawal{EntryIndex: 7} - want.WithdrawalID[0], want.WithdrawalID[31] = 0xF1, 0x7A - - ticker := time.NewTicker(300 * time.Millisecond) - defer ticker.Stop() - deadline := time.After(10 * time.Second) - for { - if err := pub.Publish(ctx, want); err != nil { - t.Fatalf("publish: %v", err) - } - select { - case fw := <-got: - if fw.WithdrawalID != want.WithdrawalID || fw.EntryIndex != want.EntryIndex { - t.Fatalf("delivered %+v, want %+v", fw.Header(), want.Header()) - } - if m := follower.Metrics().Snapshot(); m.Delivered != 1 { - t.Errorf("Delivered = %d, want 1", m.Delivered) - } - return - case <-ticker.C: - continue - case <-deadline: - t.Fatal("withdrawal never delivered") - } - } -} - -// ── helpers ─────────────────────────────────────────────────────────────── - -func newHost(t *testing.T) host.Host { - t.Helper() - h, err := libp2p.New(libp2p.ListenAddrStrings("/ip4/127.0.0.1/tcp/0")) - if err != nil { - t.Fatalf("libp2p new: %v", err) - } - t.Cleanup(func() { _ = h.Close() }) - return h -} - -func connect(t *testing.T, from, to host.Host) { - t.Helper() - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - if err := from.Connect(ctx, peer.AddrInfo{ID: to.ID(), Addrs: to.Addrs()}); err != nil { - t.Fatalf("connect: %v", err) - } -} diff --git a/pkg/p2p/gossip/publisher.go b/pkg/p2p/gossip/publisher.go deleted file mode 100644 index d29f882..0000000 --- a/pkg/p2p/gossip/publisher.go +++ /dev/null @@ -1,80 +0,0 @@ -package gossip - -import ( - "bytes" - "context" - "fmt" - "time" - - pubsub "github.com/libp2p/go-libp2p-pubsub" - "github.com/libp2p/go-libp2p/core/host" - - "github.com/layer-3/clearnet-sdk/pkg/cborx" - "github.com/layer-3/clearnet-sdk/pkg/log" -) - -// Publisher joins a GossipSub topic and publishes values of type T. It does not -// subscribe: GossipSub only propagates once at least one subscriber is in the -// mesh, so a publisher-only node relies on its peers subscribing. -type Publisher[T any, M message[T]] struct { - topic *pubsub.Topic - name string - logger log.Logger -} - -// NewPublisher joins topic on h. The caller owns h and must keep it alive for -// the Publisher's lifetime. -func NewPublisher[T any, M message[T]](ctx context.Context, h host.Host, topic string, logger log.Logger) (*Publisher[T, M], error) { - if logger == nil { - logger = log.NewNoopLogger() - } - ps, err := pubsub.NewGossipSub(ctx, h) - if err != nil { - return nil, fmt.Errorf("gossipsub: %w", err) - } - t, err := ps.Join(topic) - if err != nil { - return nil, fmt.Errorf("join %s: %w", topic, err) - } - return &Publisher[T, M]{ - topic: t, - name: topic, - logger: logger.WithName("p2p-gossip-publisher").WithKV("topic", topic), - }, nil -} - -// Publish emits v on the topic using the cborx V1 envelope. v is *T. -func (p *Publisher[T, M]) Publish(ctx context.Context, v M) error { - var buf bytes.Buffer - if err := cborx.WriteEnvelope(&buf, cborx.V1, v); err != nil { - return fmt.Errorf("encode payload: %w", err) - } - return p.topic.Publish(ctx, buf.Bytes()) -} - -// Topic returns the joined topic name. -func (p *Publisher[T, M]) Topic() string { return p.name } - -// WaitForPeers blocks until at least minPeers subscribers have joined the topic -// mesh, ctx is cancelled, or timeout elapses. -func (p *Publisher[T, M]) WaitForPeers(ctx context.Context, minPeers int, timeout time.Duration) error { - t := time.NewTimer(timeout) - defer t.Stop() - tick := time.NewTicker(500 * time.Millisecond) - defer tick.Stop() - for { - if len(p.topic.ListPeers()) >= minPeers { - return nil - } - select { - case <-ctx.Done(): - return ctx.Err() - case <-t.C: - return fmt.Errorf("only %d/%d peers joined within %s", len(p.topic.ListPeers()), minPeers, timeout) - case <-tick.C: - } - } -} - -// Close leaves the topic. It does not close the host — the caller owns that. -func (p *Publisher[T, M]) Close() error { return p.topic.Close() } diff --git a/pkg/p2p/pubsub/follower.go b/pkg/p2p/pubsub/follower.go index 0b83ede..323e15d 100644 --- a/pkg/p2p/pubsub/follower.go +++ b/pkg/p2p/pubsub/follower.go @@ -11,38 +11,22 @@ import ( "github.com/libp2p/go-libp2p/core/peer" "github.com/layer-3/clearnet-sdk/pkg/cborx" - "github.com/layer-3/clearnet-sdk/pkg/core" "github.com/layer-3/clearnet-sdk/pkg/log" ) -// maxFinalizedWithdrawalBytes caps the raw size of an inbound message before -// any CBOR allocation. A realistic envelope is a few KB (one Block + the BLS -// aggregate); 128 KiB leaves headroom for the largest plausible Block while -// keeping a malicious publisher from forcing megabyte allocations per message. -const maxFinalizedWithdrawalBytes = 128 * 1024 - -// WithdrawalHandler receives each FinalizedWithdrawal decoded from the topic. -// The Follower calls it synchronously on the consume goroutine; a slow handler -// backs up incoming messages. -type WithdrawalHandler func(fw *core.FinalizedWithdrawal) - // Metrics captures Follower counters for ops dashboards. type Metrics struct { - mu sync.Mutex - DeliveredWithdrawals uint64 // handed to the handler - DecodeErrors uint64 // envelope/CBOR decode failures - OversizeDrops uint64 // dropped for exceeding the size cap + mu sync.Mutex + Delivered uint64 // payloads handed to the handler + DecodeErrors uint64 // envelope/CBOR decode failures + OversizeDrops uint64 // dropped for exceeding the size cap } // Snapshot returns a copy of the current counters. Safe for concurrent use. func (m *Metrics) Snapshot() Metrics { m.mu.Lock() defer m.mu.Unlock() - return Metrics{ - DeliveredWithdrawals: m.DeliveredWithdrawals, - DecodeErrors: m.DecodeErrors, - OversizeDrops: m.OversizeDrops, - } + return Metrics{Delivered: m.Delivered, DecodeErrors: m.DecodeErrors, OversizeDrops: m.OversizeDrops} } func (m *Metrics) inc(field *uint64) { @@ -52,8 +36,8 @@ func (m *Metrics) inc(field *uint64) { } // Follower subscribes to a topic on a caller-owned host and forwards each -// decoded FinalizedWithdrawal to a handler. -type Follower struct { +// decoded value of type T to a Handler. +type Follower[T any, M message[T]] struct { host host.Host sub *pubsub.Subscription topic *pubsub.Topic @@ -62,15 +46,15 @@ type Follower struct { metrics *Metrics handlerMu sync.RWMutex - handler WithdrawalHandler + handler Handler[T] } // NewFollower joins and subscribes to topic on h. A size-validator is attached // before joining, so oversized messages are rejected by GossipSub and never // reach the consume loop. Call Run to start consuming; register a handler with -// SetHandler before (or shortly after) Run — messages arriving with no handler -// are dropped with a warning. The caller owns h and its connectivity. -func NewFollower(ctx context.Context, h host.Host, topic string, logger log.Logger) (*Follower, error) { +// SetHandler before (or shortly after) Run. The caller owns h and its +// connectivity. +func NewFollower[T any, M message[T]](ctx context.Context, h host.Host, topic string, logger log.Logger) (*Follower[T, M], error) { if logger == nil { logger = log.NewNoopLogger() } @@ -78,14 +62,12 @@ func NewFollower(ctx context.Context, h host.Host, topic string, logger log.Logg if err != nil { return nil, fmt.Errorf("gossipsub: %w", err) } - f := &Follower{ + f := &Follower[T, M]{ host: h, name: topic, logger: logger.WithName("p2p-pubsub-follower").WithKV("topic", topic), metrics: &Metrics{}, } - // Register the validator before Join so it is attached from the first - // message. Oversized messages are rejected by GossipSub itself. if err := ps.RegisterTopicValidator(topic, f.validateSize); err != nil { return nil, fmt.Errorf("register validator %s: %w", topic, err) } @@ -104,23 +86,23 @@ func NewFollower(ctx context.Context, h host.Host, topic string, logger log.Logg return f, nil } -// SetHandler installs the handler invoked for each decoded withdrawal. Safe to -// call concurrently with Run. -func (f *Follower) SetHandler(h WithdrawalHandler) { +// SetHandler installs the handler invoked for each decoded value. Safe to call +// concurrently with Run. +func (f *Follower[T, M]) SetHandler(h Handler[T]) { f.handlerMu.Lock() f.handler = h f.handlerMu.Unlock() } // Metrics returns the Follower's counters handle. -func (f *Follower) Metrics() *Metrics { return f.metrics } +func (f *Follower[T, M]) Metrics() *Metrics { return f.metrics } // PeerID returns the host's libp2p peer ID. -func (f *Follower) PeerID() peer.ID { return f.host.ID() } +func (f *Follower[T, M]) PeerID() peer.ID { return f.host.ID() } // Run consumes the subscription until ctx is cancelled or the subscription // closes. It blocks; run it in a goroutine. -func (f *Follower) Run(ctx context.Context) { +func (f *Follower[T, M]) Run(ctx context.Context) { for { msg, err := f.sub.Next(ctx) if err != nil { @@ -129,7 +111,6 @@ func (f *Follower) Run(ctx context.Context) { } return } - // Skip our own messages. if msg.ReceivedFrom == f.host.ID() { continue } @@ -139,15 +120,13 @@ func (f *Follower) Run(ctx context.Context) { // Close cancels the subscription and leaves the topic. It does not close the // host — the caller owns that. -func (f *Follower) Close() error { +func (f *Follower[T, M]) Close() error { f.sub.Cancel() return f.topic.Close() } -// validateSize is the GossipSub topic validator: it rejects oversized messages -// before they propagate or reach the consume loop. -func (f *Follower) validateSize(_ context.Context, from peer.ID, msg *pubsub.Message) bool { - if len(msg.Data) > maxFinalizedWithdrawalBytes { +func (f *Follower[T, M]) validateSize(_ context.Context, from peer.ID, msg *pubsub.Message) bool { + if len(msg.Data) > maxMessageBytes { f.metrics.inc(&f.metrics.OversizeDrops) f.logger.Warn("dropping oversize message", "from", from.ShortString(), "bytes", len(msg.Data)) return false @@ -155,21 +134,23 @@ func (f *Follower) validateSize(_ context.Context, from peer.ID, msg *pubsub.Mes return true } -func (f *Follower) handle(msg *pubsub.Message) { - var fw core.FinalizedWithdrawal - var v cborx.Version - if err := cborx.ReadEnvelopeStrict(bytes.NewReader(msg.Data), &v, &fw); err != nil { +func (f *Follower[T, M]) handle(msg *pubsub.Message) { + var v T + var ver cborx.Version + // M(&v) converts *T to the constrained pointer type, which satisfies + // cborx's CBORUnmarshaler — decode in place into v. + if err := cborx.ReadEnvelopeStrict(bytes.NewReader(msg.Data), &ver, M(&v)); err != nil { f.metrics.inc(&f.metrics.DecodeErrors) - f.logger.Warn("decode finalized withdrawal failed", "error", err) + f.logger.Warn("decode payload failed", "error", err) return } f.handlerMu.RLock() h := f.handler f.handlerMu.RUnlock() if h == nil { - f.logger.Warn("no handler registered; dropping withdrawal") + f.logger.Warn("no handler registered; dropping payload") return } - h(&fw) - f.metrics.inc(&f.metrics.DeliveredWithdrawals) + h(&v) + f.metrics.inc(&f.metrics.Delivered) } diff --git a/pkg/p2p/pubsub/publisher.go b/pkg/p2p/pubsub/publisher.go index 45bdbaa..3694e08 100644 --- a/pkg/p2p/pubsub/publisher.go +++ b/pkg/p2p/pubsub/publisher.go @@ -1,12 +1,3 @@ -// Package pubsub provides GossipSub publish/subscribe helpers for the clearing -// layer's broadcast topics. A Publisher joins a topic and emits typed payloads; -// a Follower subscribes and forwards decoded payloads to a handler. -// -// Both are host-taking: the caller builds and owns the libp2p host (identity, -// listen addresses, resource limits) and is responsible for connectivity -// (dialing seed peers, peer discovery). These helpers own only the GossipSub -// instance, the topic, and — for the Follower — the subscription. Close -// releases those, never the host. package pubsub import ( @@ -19,23 +10,21 @@ import ( "github.com/libp2p/go-libp2p/core/host" "github.com/layer-3/clearnet-sdk/pkg/cborx" - "github.com/layer-3/clearnet-sdk/pkg/core" "github.com/layer-3/clearnet-sdk/pkg/log" ) -// Publisher joins a GossipSub topic and publishes typed payloads to it. It does -// not subscribe: GossipSub only propagates once at least one subscriber is in -// the mesh, so a publisher-only node relies on its peers subscribing. -type Publisher struct { - host host.Host +// Publisher joins a GossipSub topic and publishes values of type T. It does not +// subscribe: GossipSub only propagates once at least one subscriber is in the +// mesh, so a publisher-only node relies on its peers subscribing. +type Publisher[T any, M message[T]] struct { topic *pubsub.Topic name string logger log.Logger } -// NewPublisher joins topic on h and returns a Publisher. The caller owns h and -// must keep it alive for the Publisher's lifetime. -func NewPublisher(ctx context.Context, h host.Host, topic string, logger log.Logger) (*Publisher, error) { +// NewPublisher joins topic on h. The caller owns h and must keep it alive for +// the Publisher's lifetime. +func NewPublisher[T any, M message[T]](ctx context.Context, h host.Host, topic string, logger log.Logger) (*Publisher[T, M], error) { if logger == nil { logger = log.NewNoopLogger() } @@ -47,32 +36,28 @@ func NewPublisher(ctx context.Context, h host.Host, topic string, logger log.Log if err != nil { return nil, fmt.Errorf("join %s: %w", topic, err) } - return &Publisher{ - host: h, + return &Publisher[T, M]{ topic: t, name: topic, logger: logger.WithName("p2p-pubsub-publisher").WithKV("topic", topic), }, nil } -// PublishFinalizedWithdrawal emits a single FinalizedWithdrawal on the topic -// using the cborx V1 envelope. -func (p *Publisher) PublishFinalizedWithdrawal(ctx context.Context, fw *core.FinalizedWithdrawal) error { +// Publish emits v on the topic using the cborx V1 envelope. v is *T. +func (p *Publisher[T, M]) Publish(ctx context.Context, v M) error { var buf bytes.Buffer - if err := cborx.WriteEnvelope(&buf, cborx.V1, fw); err != nil { - return fmt.Errorf("encode finalized withdrawal: %w", err) + if err := cborx.WriteEnvelope(&buf, cborx.V1, v); err != nil { + return fmt.Errorf("encode payload: %w", err) } return p.topic.Publish(ctx, buf.Bytes()) } // Topic returns the joined topic name. -func (p *Publisher) Topic() string { return p.name } +func (p *Publisher[T, M]) Topic() string { return p.name } // WaitForPeers blocks until at least minPeers subscribers have joined the topic -// mesh, ctx is cancelled, or timeout elapses. GossipSub forwards only once mesh -// links form; publishing before then silently drops, so a publisher-only node -// should wait before its first broadcast. -func (p *Publisher) WaitForPeers(ctx context.Context, minPeers int, timeout time.Duration) error { +// mesh, ctx is cancelled, or timeout elapses. +func (p *Publisher[T, M]) WaitForPeers(ctx context.Context, minPeers int, timeout time.Duration) error { t := time.NewTimer(timeout) defer t.Stop() tick := time.NewTicker(500 * time.Millisecond) @@ -92,4 +77,4 @@ func (p *Publisher) WaitForPeers(ctx context.Context, minPeers int, timeout time } // Close leaves the topic. It does not close the host — the caller owns that. -func (p *Publisher) Close() error { return p.topic.Close() } +func (p *Publisher[T, M]) Close() error { return p.topic.Close() } diff --git a/pkg/p2p/gossip/gossip.go b/pkg/p2p/pubsub/pubsub.go similarity index 59% rename from pkg/p2p/gossip/gossip.go rename to pkg/p2p/pubsub/pubsub.go index 9abc7ac..82308c6 100644 --- a/pkg/p2p/gossip/gossip.go +++ b/pkg/p2p/pubsub/pubsub.go @@ -1,23 +1,21 @@ -// Package gossip is a generic GossipSub publish/subscribe toolset over any -// cborx-envelope payload. It is the type-parameterized alternative to the -// concrete pkg/p2p/pubsub (which bakes in *core.FinalizedWithdrawal): a -// Publisher[T]/Follower[T] works for any T whose cbor-gen codec lives on *T. +// Package pubsub is a generic GossipSub publish/subscribe toolset over any +// cborx-envelope payload: a Publisher[T]/Follower[T] works for any T whose +// cbor-gen codec lives on *T (e.g. *core.FinalizedWithdrawal). // -// Both ship side by side so the design can be chosen at review — generic reuse -// here vs. a concrete, no-generics surface in pubsub. Like pubsub, gossip is -// host-taking: the caller builds and owns the libp2p host and its connectivity; -// gossip owns only the GossipSub instance, topic, and subscription. +// It is host-taking: the caller builds and owns the libp2p host and its +// connectivity; this package owns only the GossipSub instance, topic, and +// subscription. // // Usage (constraint type inference fills the pointer type, so callers name only // the value type): // -// pub, _ := gossip.NewPublisher[core.FinalizedWithdrawal](ctx, h, topic, nil) +// pub, _ := pubsub.NewPublisher[core.FinalizedWithdrawal](ctx, h, topic, nil) // _ = pub.Publish(ctx, &core.FinalizedWithdrawal{...}) // -// fol, _ := gossip.NewFollower[core.FinalizedWithdrawal](ctx, h, topic, nil) +// fol, _ := pubsub.NewFollower[core.FinalizedWithdrawal](ctx, h, topic, nil) // fol.SetHandler(func(fw *core.FinalizedWithdrawal) { ... }) // go fol.Run(ctx) -package gossip +package pubsub import ( cbg "github.com/whyrusleeping/cbor-gen" @@ -25,7 +23,7 @@ import ( // maxMessageBytes caps the raw size of an inbound message before any CBOR // allocation, keeping a malicious publisher from forcing large allocations per -// message. Matches the concrete pubsub follower's cap. +// message. const maxMessageBytes = 128 * 1024 // message constrains *T to the cborx codec interfaces. cbor-gen emits diff --git a/pkg/p2p/pubsub/pubsub_test.go b/pkg/p2p/pubsub/pubsub_test.go index 0c99165..07249c8 100644 --- a/pkg/p2p/pubsub/pubsub_test.go +++ b/pkg/p2p/pubsub/pubsub_test.go @@ -13,7 +13,10 @@ import ( p2pproto "github.com/layer-3/clearnet-sdk/pkg/p2p/protocol" ) -func TestPubSub_PublishDeliver(t *testing.T) { +// TestPubSub_FinalizedWithdrawal shows the generic toolset carrying a concrete +// payload: callers name only the value type (core.FinalizedWithdrawal) and +// constraint type inference supplies the *T pointer type to Publisher/Follower. +func TestPubSub_FinalizedWithdrawal(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() @@ -21,7 +24,7 @@ func TestPubSub_PublishDeliver(t *testing.T) { hSub := newHost(t) connect(t, hPub, hSub) - follower, err := NewFollower(ctx, hSub, p2pproto.TopicWithdrawals, nil) + follower, err := NewFollower[core.FinalizedWithdrawal](ctx, hSub, p2pproto.TopicWithdrawals, nil) if err != nil { t.Fatalf("NewFollower: %v", err) } @@ -31,13 +34,12 @@ func TestPubSub_PublishDeliver(t *testing.T) { follower.SetHandler(func(fw *core.FinalizedWithdrawal) { got <- fw }) go follower.Run(ctx) - pub, err := NewPublisher(ctx, hPub, p2pproto.TopicWithdrawals, nil) + pub, err := NewPublisher[core.FinalizedWithdrawal](ctx, hPub, p2pproto.TopicWithdrawals, nil) if err != nil { t.Fatalf("NewPublisher: %v", err) } defer pub.Close() - // Wait for the subscriber to appear in the publisher's topic mesh. if err := pub.WaitForPeers(ctx, 1, 10*time.Second); err != nil { t.Fatalf("WaitForPeers: %v", err) } @@ -45,12 +47,11 @@ func TestPubSub_PublishDeliver(t *testing.T) { want := &core.FinalizedWithdrawal{EntryIndex: 7} want.WithdrawalID[0], want.WithdrawalID[31] = 0xF1, 0x7A - // GossipSub mesh links may still be forming; republish until delivered. ticker := time.NewTicker(300 * time.Millisecond) defer ticker.Stop() deadline := time.After(10 * time.Second) for { - if err := pub.PublishFinalizedWithdrawal(ctx, want); err != nil { + if err := pub.Publish(ctx, want); err != nil { t.Fatalf("publish: %v", err) } select { @@ -58,8 +59,8 @@ func TestPubSub_PublishDeliver(t *testing.T) { if fw.WithdrawalID != want.WithdrawalID || fw.EntryIndex != want.EntryIndex { t.Fatalf("delivered %+v, want %+v", fw.Header(), want.Header()) } - if m := follower.Metrics().Snapshot(); m.DeliveredWithdrawals != 1 { - t.Errorf("DeliveredWithdrawals = %d, want 1", m.DeliveredWithdrawals) + if m := follower.Metrics().Snapshot(); m.Delivered != 1 { + t.Errorf("Delivered = %d, want 1", m.Delivered) } return case <-ticker.C: From 26fcb57c8a6692c9eac4ae1eafbab26884e0683d Mon Sep 17 00:00:00 2001 From: Anton Filonenko Date: Wed, 17 Jun 2026 17:29:16 +0300 Subject: [PATCH 15/15] fix(bls,evm): validate BLS point deserialization and EVM addresses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - bls: DeserializeG1/G2 are acceptance-path decoders for untrusted input but set coordinates without checks. Reject non-canonical coordinates (>= field prime), off-curve points, and points outside the prime-order subgroup — the membership the on-chain precompile enforces. Without the subgroup check a crafted point could pass off-chain acceptance. - evm: packedFromOp rejects a recipient or asset that is not a well-formed hex address. common.HexToAddress silently zero-fills a malformed input, which would otherwise sign a withdrawal to the wrong (often zero) destination. --- pkg/blockchain/evm/withdrawal_finalizer.go | 24 ++++++-- .../evm/withdrawal_finalizer_test.go | 29 +++++++++ pkg/bls/bls.go | 50 +++++++++++++--- pkg/bls/deserialize_test.go | 59 +++++++++++++++++++ 4 files changed, 150 insertions(+), 12 deletions(-) create mode 100644 pkg/blockchain/evm/withdrawal_finalizer_test.go create mode 100644 pkg/bls/deserialize_test.go diff --git a/pkg/blockchain/evm/withdrawal_finalizer.go b/pkg/blockchain/evm/withdrawal_finalizer.go index 13e5e30..c78b3b1 100644 --- a/pkg/blockchain/evm/withdrawal_finalizer.go +++ b/pkg/blockchain/evm/withdrawal_finalizer.go @@ -89,7 +89,11 @@ type evmPacked struct { // Pack returns the canonical JSON for the withdrawal. Pure — no chain access. func (f *WithdrawalFinalizer) Pack(_ context.Context, op *core.WithdrawalOp, withdrawalID [32]byte) ([]byte, error) { - return json.Marshal(packedFromOp(op, withdrawalID)) + p, err := packedFromOp(op, withdrawalID) + if err != nil { + return nil, err + } + return json.Marshal(p) } // Validate re-derives the canonical payload from the op and asserts the packed @@ -99,7 +103,10 @@ func (f *WithdrawalFinalizer) Validate(_ context.Context, packed []byte, op *cor if err := json.Unmarshal(packed, &got); err != nil { return fmt.Errorf("decode packed: %w", err) } - want := packedFromOp(op, withdrawalID) + want, err := packedFromOp(op, withdrawalID) + if err != nil { + return err + } if got != want { return fmt.Errorf("packed withdrawal does not match op: got %+v want %+v", got, want) } @@ -220,13 +227,22 @@ func (f *WithdrawalFinalizer) VerifyExecution(ctx context.Context, withdrawalID // --- helpers --- -func packedFromOp(op *core.WithdrawalOp, withdrawalID [32]byte) evmPacked { +func packedFromOp(op *core.WithdrawalOp, withdrawalID [32]byte) (evmPacked, error) { + // common.HexToAddress silently zero-fills/truncates a malformed input, which + // would sign a withdrawal to the wrong (often zero) recipient. Reject + // anything that is not a well-formed hex address up front. + if !common.IsHexAddress(op.Recipient) { + return evmPacked{}, fmt.Errorf("evm: recipient %q is not a valid hex address", op.Recipient) + } + if !common.IsHexAddress(op.L1Asset) { + return evmPacked{}, fmt.Errorf("evm: l1 asset %q is not a valid hex address", op.L1Asset) + } return evmPacked{ To: common.HexToAddress(op.Recipient).Hex(), Asset: common.HexToAddress(op.L1Asset).Hex(), Amount: op.Amount.BigInt().String(), WithdrawalID: hex.EncodeToString(withdrawalID[:]), - } + }, nil } // digest computes the Custody.execute signing digest: diff --git a/pkg/blockchain/evm/withdrawal_finalizer_test.go b/pkg/blockchain/evm/withdrawal_finalizer_test.go new file mode 100644 index 0000000..774d6f1 --- /dev/null +++ b/pkg/blockchain/evm/withdrawal_finalizer_test.go @@ -0,0 +1,29 @@ +package evm + +import ( + "strings" + "testing" + + "github.com/layer-3/clearnet-sdk/pkg/core" + "github.com/layer-3/clearnet-sdk/pkg/decimal" +) + +// TestPackedFromOp_RejectsMalformedAddress guards M3: common.HexToAddress +// silently zero-fills a malformed address, so packedFromOp must reject a +// recipient or asset that is not a well-formed hex address instead of signing a +// withdrawal to the wrong destination. +func TestPackedFromOp_RejectsMalformedAddress(t *testing.T) { + var wid [32]byte + addr := "0x" + strings.Repeat("ab", 20) + asset := "0x" + strings.Repeat("cd", 20) + + if _, err := packedFromOp(&core.WithdrawalOp{Recipient: addr, L1Asset: asset, Amount: decimal.NewFromInt(1)}, wid); err != nil { + t.Fatalf("valid op rejected: %v", err) + } + if _, err := packedFromOp(&core.WithdrawalOp{Recipient: "not-an-address", L1Asset: asset, Amount: decimal.NewFromInt(1)}, wid); err == nil { + t.Error("malformed recipient accepted") + } + if _, err := packedFromOp(&core.WithdrawalOp{Recipient: addr, L1Asset: "0xzz", Amount: decimal.NewFromInt(1)}, wid); err == nil { + t.Error("malformed asset accepted") + } +} diff --git a/pkg/bls/bls.go b/pkg/bls/bls.go index f4d951e..4340946 100644 --- a/pkg/bls/bls.go +++ b/pkg/bls/bls.go @@ -259,14 +259,29 @@ func SerializeG1(p bn254.G1Affine) []byte { return buf } -// DeserializeG1 reads a G1 point from 64 bytes (X || Y, big-endian). +// DeserializeG1 reads a G1 point from 64 bytes (X || Y, big-endian). It is an +// acceptance-path decoder for untrusted input, so it fully validates the point: +// each coordinate must be a canonical field element (< P), and the point must be +// on the curve and in the prime-order subgroup. Skipping the subgroup check +// would admit small-subgroup points that break the signature scheme's security. func DeserializeG1(data []byte) (bn254.G1Affine, error) { if len(data) != 64 { return bn254.G1Affine{}, errors.New("invalid G1 data length: expected 64 bytes") } + x := new(big.Int).SetBytes(data[:32]) + y := new(big.Int).SetBytes(data[32:]) + if x.Cmp(fieldP) >= 0 || y.Cmp(fieldP) >= 0 { + return bn254.G1Affine{}, errors.New("bls: G1 coordinate not in field range") + } var pt bn254.G1Affine - pt.X.SetBigInt(new(big.Int).SetBytes(data[:32])) - pt.Y.SetBigInt(new(big.Int).SetBytes(data[32:])) + pt.X.SetBigInt(x) + pt.Y.SetBigInt(y) + if !pt.IsOnCurve() { + return bn254.G1Affine{}, errors.New("bls: G1 point not on curve") + } + if !pt.IsInSubGroup() { + return bn254.G1Affine{}, errors.New("bls: G1 point not in prime-order subgroup") + } return pt, nil } @@ -295,16 +310,35 @@ func SerializeG2(p bn254.G2Affine) []byte { return buf } -// DeserializeG2 reads a G2 point from 128 bytes. +// DeserializeG2 reads a G2 point from 128 bytes. Like DeserializeG1 it is an +// acceptance-path decoder: it rejects non-canonical coordinates (>= P) and +// points that are off-curve or outside the prime-order subgroup. The off-chain +// verifier must apply the same membership checks the on-chain precompile does, +// or a crafted pubkey/signature could pass off-chain acceptance. func DeserializeG2(data []byte) (bn254.G2Affine, error) { if len(data) != 128 { return bn254.G2Affine{}, errors.New("invalid G2 data length: expected 128 bytes") } + xa1 := new(big.Int).SetBytes(data[0:32]) + xa0 := new(big.Int).SetBytes(data[32:64]) + ya1 := new(big.Int).SetBytes(data[64:96]) + ya0 := new(big.Int).SetBytes(data[96:128]) + for _, c := range []*big.Int{xa1, xa0, ya1, ya0} { + if c.Cmp(fieldP) >= 0 { + return bn254.G2Affine{}, errors.New("bls: G2 coordinate not in field range") + } + } var pt bn254.G2Affine - pt.X.A1.SetBigInt(new(big.Int).SetBytes(data[0:32])) - pt.X.A0.SetBigInt(new(big.Int).SetBytes(data[32:64])) - pt.Y.A1.SetBigInt(new(big.Int).SetBytes(data[64:96])) - pt.Y.A0.SetBigInt(new(big.Int).SetBytes(data[96:128])) + pt.X.A1.SetBigInt(xa1) + pt.X.A0.SetBigInt(xa0) + pt.Y.A1.SetBigInt(ya1) + pt.Y.A0.SetBigInt(ya0) + if !pt.IsOnCurve() { + return bn254.G2Affine{}, errors.New("bls: G2 point not on curve") + } + if !pt.IsInSubGroup() { + return bn254.G2Affine{}, errors.New("bls: G2 point not in prime-order subgroup") + } return pt, nil } diff --git a/pkg/bls/deserialize_test.go b/pkg/bls/deserialize_test.go new file mode 100644 index 0000000..3803779 --- /dev/null +++ b/pkg/bls/deserialize_test.go @@ -0,0 +1,59 @@ +package bls + +import ( + "testing" + + "github.com/consensys/gnark-crypto/ecc/bn254" +) + +// TestDeserializeG1_Validation covers the acceptance-path checks: a valid point +// round-trips, while non-canonical (coordinate >= P) and off-curve encodings are +// rejected. (G1 has cofactor 1, so every on-curve point is in the subgroup — the +// subgroup check is exercised by the G2 case.) +func TestDeserializeG1_Validation(t *testing.T) { + _, _, g1, _ := bn254.Generators() + if _, err := DeserializeG1(SerializeG1(g1)); err != nil { + t.Fatalf("valid G1 rejected: %v", err) + } + + nonCanonical := make([]byte, 64) + fieldP.FillBytes(nonCanonical[:32]) // X == P + if _, err := DeserializeG1(nonCanonical); err == nil { + t.Error("G1 with coordinate == P accepted") + } + + offCurve := make([]byte, 64) + offCurve[31], offCurve[63] = 1, 1 // (1,1): 1 != 1+3, not on y^2 = x^3 + 3 + if _, err := DeserializeG1(offCurve); err == nil { + t.Error("off-curve G1 accepted") + } + + if _, err := DeserializeG1(make([]byte, 63)); err == nil { + t.Error("wrong-length G1 accepted") + } +} + +// TestDeserializeG2_Validation covers the same checks for G2, including the +// prime-order subgroup membership (G2 has cofactor > 1). +func TestDeserializeG2_Validation(t *testing.T) { + _, _, _, g2 := bn254.Generators() + if _, err := DeserializeG2(SerializeG2(g2)); err != nil { + t.Fatalf("valid G2 rejected: %v", err) + } + + nonCanonical := make([]byte, 128) + fieldP.FillBytes(nonCanonical[:32]) // X.A1 == P + if _, err := DeserializeG2(nonCanonical); err == nil { + t.Error("G2 with coordinate == P accepted") + } + + offCurve := make([]byte, 128) + offCurve[31], offCurve[63], offCurve[95], offCurve[127] = 1, 1, 1, 1 + if _, err := DeserializeG2(offCurve); err == nil { + t.Error("off-curve G2 accepted") + } + + if _, err := DeserializeG2(make([]byte, 127)); err == nil { + t.Error("wrong-length G2 accepted") + } +}