diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 53c90e5..4df1e3a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,3 +30,25 @@ jobs: - uses: golangci/golangci-lint-action@v6 with: version: latest + + verify-addresses: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.24' + - name: regenerate addresses from FWSS + run: go generate ./constants/... + - name: check for drift + run: | + if git diff --exit-code constants/addresses_generated.go; then + echo "addresses match on-chain state" + else + echo "::error::addresses_generated.go differs from on-chain FWSS state" + echo "run 'go generate ./constants/...' and commit the result" + echo "" + echo "if using old addresses intentionally, add [skip-address-verify] to commit message" + exit 1 + fi + if: ${{ !contains(github.event.head_commit.message, '[skip-address-verify]') }} diff --git a/constants/addresses.go b/constants/addresses.go index 09395a9..afb2b3d 100644 --- a/constants/addresses.go +++ b/constants/addresses.go @@ -1,3 +1,5 @@ +//go:generate go run ../internal/generate/addresses.go + package constants import ( @@ -7,7 +9,7 @@ import ( type Network string const ( - NetworkMainnet Network = "mainnet" + NetworkMainnet Network = "mainnet" NetworkCalibration Network = "calibration" ) @@ -16,17 +18,8 @@ const ( ChainIDCalibration int64 = 314159 ) +// static addresses not derived from FWSS var ( - WarmStorageAddresses = map[Network]common.Address{ - NetworkMainnet: common.HexToAddress("0x8408502033C418E1bbC97cE9ac48E5528F371A9f"), - NetworkCalibration: common.HexToAddress("0x02925630df557F957f70E112bA06e50965417CA0"), - } - - SPRegistryAddresses = map[Network]common.Address{ - NetworkMainnet: common.HexToAddress("0xf55dDbf63F1b55c3F1D4FA7e339a68AB7b64A5eB"), - NetworkCalibration: common.HexToAddress("0x839e5c9988e4e9977d40708d0094103c0839Ac9D"), - } - Multicall3Addresses = map[Network]common.Address{ NetworkMainnet: common.HexToAddress("0xcA11bde05977b3631167028862bE2a173976CA11"), NetworkCalibration: common.HexToAddress("0xcA11bde05977b3631167028862bE2a173976CA11"), @@ -36,16 +29,6 @@ var ( NetworkMainnet: common.HexToAddress("0x80B98d3aa09ffff255c3ba4A241111Ff1262F045"), NetworkCalibration: common.HexToAddress("0xb3042734b608a1B16e9e86B374A3f3e389B4cDf0"), } - - PaymentsAddresses = map[Network]common.Address{ - NetworkMainnet: common.HexToAddress("0x23b1e018F08BB982348b15a86ee926eEBf7F4DAa"), - NetworkCalibration: common.HexToAddress("0x09a0fDc2723fAd1A7b8e3e00eE5DF73841df55a0"), - } - - WarmStorageStateViewAddresses = map[Network]common.Address{ - NetworkMainnet: common.HexToAddress("0x9e4e6699d8F67dFc883d6b0A7344Bd56F7E80B46"), - NetworkCalibration: common.HexToAddress("0xA5D87b04086B1d591026cCE10255351B5AA4689B"), - } ) var RPCURLs = map[Network]string{ @@ -67,3 +50,9 @@ var USDFCAddressesByChainID = map[int64]common.Address{ ChainIDMainnet: common.HexToAddress("0x80B98d3aa09ffff255c3ba4A241111Ff1262F045"), ChainIDCalibration: common.HexToAddress("0xb3042734b608a1B16e9e86B374A3f3e389B4cDf0"), } + +// WarmStorageAddresses aliases the FWSS addresses (root of trust) +var WarmStorageAddresses = map[Network]common.Address{ + NetworkMainnet: FWSSAddressMainnet, + NetworkCalibration: FWSSAddressCalibration, +} diff --git a/constants/addresses_generated.go b/constants/addresses_generated.go new file mode 100644 index 0000000..d0590c0 --- /dev/null +++ b/constants/addresses_generated.go @@ -0,0 +1,40 @@ +// Code generated by go generate; DO NOT EDIT. +// Source: FWSS contracts on mainnet and calibration + +package constants + +import "github.com/ethereum/go-ethereum/common" + +// root of trust - FWSS contract addresses +var ( + FWSSAddressMainnet = common.HexToAddress("0x8408502033C418E1bbC97cE9ac48E5528F371A9f") + FWSSAddressCalibration = common.HexToAddress("0x02925630df557F957f70E112bA06e50965417CA0") +) + +// derived addresses - read from FWSS contracts +var ( + PaymentsAddresses = map[Network]common.Address{ + NetworkMainnet: common.HexToAddress("0x23b1e018F08BB982348b15a86ee926eEBf7F4DAa"), + NetworkCalibration: common.HexToAddress("0x09a0fDc2723fAd1A7b8e3e00eE5DF73841df55a0"), + } + + WarmStorageStateViewAddresses = map[Network]common.Address{ + NetworkMainnet: common.HexToAddress("0x638a4986332bF9B889E5D7435B966C5ecdE077Fa"), + NetworkCalibration: common.HexToAddress("0x53d235D474585EC102ccaB7e0cdcE951dD00f716"), + } + + PDPVerifierAddresses = map[Network]common.Address{ + NetworkMainnet: common.HexToAddress("0xBADd0B92C1c71d02E7d520f64c0876538fa2557F"), + NetworkCalibration: common.HexToAddress("0x85e366Cf9DD2c0aE37E963d9556F5f4718d6417C"), + } + + SPRegistryAddresses = map[Network]common.Address{ + NetworkMainnet: common.HexToAddress("0xf55dDbf63F1b55c3F1D4FA7e339a68AB7b64A5eB"), + NetworkCalibration: common.HexToAddress("0x839e5c9988e4e9977d40708d0094103c0839Ac9D"), + } + + SessionKeyRegistryAddresses = map[Network]common.Address{ + NetworkMainnet: common.HexToAddress("0x74FD50525A958aF5d484601E252271f9625231aB"), + NetworkCalibration: common.HexToAddress("0x518411c2062E119Aaf7A8B12A2eDf9a939347655"), + } +) diff --git a/constants/addresses_integration_test.go b/constants/addresses_integration_test.go new file mode 100644 index 0000000..a1f6b7f --- /dev/null +++ b/constants/addresses_integration_test.go @@ -0,0 +1,105 @@ +//go:build integration + +package constants + +import ( + "context" + "math/big" + "strings" + "testing" + "time" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" +) + +// minimal ABIs - Payments.accounts is the most reliable test +var paymentsABI = `[{"name":"accounts","type":"function","inputs":[{"name":"token","type":"address"},{"name":"owner","type":"address"}],"outputs":[{"name":"balance","type":"uint256"},{"name":"lockedBalance","type":"uint256"}],"stateMutability":"view"}]` + +type contractTest struct { + name string + address common.Address +} + +func TestGeneratedAddresses_MainnetContracts(t *testing.T) { + testContracts(t, NetworkMainnet, RPCURLs[NetworkMainnet]) +} + +func TestGeneratedAddresses_CalibrationContracts(t *testing.T) { + testContracts(t, NetworkCalibration, RPCURLs[NetworkCalibration]) +} + +func testContracts(t *testing.T, network Network, rpcURL string) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + client, err := ethclient.DialContext(ctx, rpcURL) + if err != nil { + t.Fatalf("failed to connect to %s: %v", network, err) + } + defer client.Close() + + // verify we can reach the network + chainID, err := client.ChainID(ctx) + if err != nil { + t.Fatalf("failed to get chain ID: %v", err) + } + t.Logf("connected to %s (chain ID: %d)", network, chainID) + + tests := []contractTest{ + {"Payments", PaymentsAddresses[network]}, + {"StateView", WarmStorageStateViewAddresses[network]}, + {"PDPVerifier", PDPVerifierAddresses[network]}, + {"SPRegistry", SPRegistryAddresses[network]}, + {"SessionKeyRegistry", SessionKeyRegistryAddresses[network]}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // verify contract has code deployed + code, err := client.CodeAt(ctx, tc.address, nil) + if err != nil { + t.Fatalf("failed to get code at %s: %v", tc.address.Hex(), err) + } + if len(code) == 0 { + t.Fatalf("no code at %s - not a contract", tc.address.Hex()) + } + t.Logf("%s: %s (%d bytes)", tc.name, tc.address.Hex(), len(code)) + }) + } + + // deeper test: verify Payments contract responds to known method + t.Run("Payments_ABI", func(t *testing.T) { + parsed, err := abi.JSON(strings.NewReader(paymentsABI)) + if err != nil { + t.Fatalf("failed to parse ABI: %v", err) + } + + zeroAddr := common.Address{} + data, err := parsed.Pack("accounts", zeroAddr, zeroAddr) + if err != nil { + t.Fatalf("failed to pack accounts call: %v", err) + } + + paymentsAddr := PaymentsAddresses[network] + result, err := client.CallContract(ctx, ethereum.CallMsg{ + To: &paymentsAddr, + Data: data, + }, nil) + if err != nil { + t.Fatalf("accounts() call failed: %v", err) + } + + // unpack and verify we get two uint256 values + var balance, lockedBalance *big.Int + unpacked, err := parsed.Unpack("accounts", result) + if err != nil { + t.Fatalf("failed to unpack: %v", err) + } + balance = unpacked[0].(*big.Int) + lockedBalance = unpacked[1].(*big.Int) + t.Logf("Payments.accounts(0x0, 0x0) = {balance: %s, locked: %s}", balance, lockedBalance) + }) +} diff --git a/internal/generate/addresses.go b/internal/generate/addresses.go new file mode 100644 index 0000000..6a0923b --- /dev/null +++ b/internal/generate/addresses.go @@ -0,0 +1,207 @@ +//go:build ignore + +package main + +import ( + "context" + "fmt" + "os" + "strings" + "text/template" + "time" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" +) + +// root of trust - only these addresses are hardcoded +var ( + fwssMainnet = common.HexToAddress("0x8408502033C418E1bbC97cE9ac48E5528F371A9f") + fwssCalibration = common.HexToAddress("0x02925630df557F957f70E112bA06e50965417CA0") + + rpcMainnet = "https://api.node.glif.io/rpc/v1" + rpcCalibration = "https://api.calibration.node.glif.io/rpc/v1" +) + +// view functions on FWSS contract +var fwssABI = `[ + {"type":"function","name":"paymentsContractAddress","inputs":[],"outputs":[{"name":"","type":"address"}],"stateMutability":"view"}, + {"type":"function","name":"viewContractAddress","inputs":[],"outputs":[{"name":"","type":"address"}],"stateMutability":"view"}, + {"type":"function","name":"pdpVerifierAddress","inputs":[],"outputs":[{"name":"","type":"address"}],"stateMutability":"view"}, + {"type":"function","name":"serviceProviderRegistry","inputs":[],"outputs":[{"name":"","type":"address"}],"stateMutability":"view"}, + {"type":"function","name":"sessionKeyRegistry","inputs":[],"outputs":[{"name":"","type":"address"}],"stateMutability":"view"} +]` + +type networkAddresses struct { + FWSS common.Address + Payments common.Address + StateView common.Address + PDPVerifier common.Address + SPRegistry common.Address + SessionKeyRegistry common.Address +} + +func readAddresses(ctx context.Context, rpcURL string, fwssAddr common.Address) (*networkAddresses, error) { + client, err := ethclient.DialContext(ctx, rpcURL) + if err != nil { + return nil, fmt.Errorf("dial rpc: %w", err) + } + defer client.Close() + + parsed, err := abi.JSON(strings.NewReader(fwssABI)) + if err != nil { + return nil, fmt.Errorf("parse abi: %w", err) + } + + callView := func(method string) (common.Address, error) { + data, err := parsed.Pack(method) + if err != nil { + return common.Address{}, fmt.Errorf("pack %s: %w", method, err) + } + + result, err := client.CallContract(ctx, ethereum.CallMsg{ + To: &fwssAddr, + Data: data, + }, nil) + if err != nil { + return common.Address{}, fmt.Errorf("call %s: %w", method, err) + } + + var addr common.Address + if err := parsed.UnpackIntoInterface(&addr, method, result); err != nil { + return common.Address{}, fmt.Errorf("unpack %s: %w", method, err) + } + return addr, nil + } + + addrs := &networkAddresses{FWSS: fwssAddr} + + addrs.Payments, err = callView("paymentsContractAddress") + if err != nil { + return nil, err + } + + addrs.StateView, err = callView("viewContractAddress") + if err != nil { + return nil, err + } + + addrs.PDPVerifier, err = callView("pdpVerifierAddress") + if err != nil { + return nil, err + } + + addrs.SPRegistry, err = callView("serviceProviderRegistry") + if err != nil { + return nil, err + } + + addrs.SessionKeyRegistry, err = callView("sessionKeyRegistry") + if err != nil { + return nil, err + } + + return addrs, nil +} + +var tmpl = template.Must(template.New("addresses").Parse(`// Code generated by go generate; DO NOT EDIT. +// Source: FWSS contracts on mainnet and calibration + +package constants + +import "github.com/ethereum/go-ethereum/common" + +// root of trust - FWSS contract addresses +var ( + FWSSAddressMainnet = common.HexToAddress("{{ .Mainnet.FWSS.Hex }}") + FWSSAddressCalibration = common.HexToAddress("{{ .Calibration.FWSS.Hex }}") +) + +// derived addresses - read from FWSS contracts +var ( + PaymentsAddresses = map[Network]common.Address{ + NetworkMainnet: common.HexToAddress("{{ .Mainnet.Payments.Hex }}"), + NetworkCalibration: common.HexToAddress("{{ .Calibration.Payments.Hex }}"), + } + + WarmStorageStateViewAddresses = map[Network]common.Address{ + NetworkMainnet: common.HexToAddress("{{ .Mainnet.StateView.Hex }}"), + NetworkCalibration: common.HexToAddress("{{ .Calibration.StateView.Hex }}"), + } + + PDPVerifierAddresses = map[Network]common.Address{ + NetworkMainnet: common.HexToAddress("{{ .Mainnet.PDPVerifier.Hex }}"), + NetworkCalibration: common.HexToAddress("{{ .Calibration.PDPVerifier.Hex }}"), + } + + SPRegistryAddresses = map[Network]common.Address{ + NetworkMainnet: common.HexToAddress("{{ .Mainnet.SPRegistry.Hex }}"), + NetworkCalibration: common.HexToAddress("{{ .Calibration.SPRegistry.Hex }}"), + } + + SessionKeyRegistryAddresses = map[Network]common.Address{ + NetworkMainnet: common.HexToAddress("{{ .Mainnet.SessionKeyRegistry.Hex }}"), + NetworkCalibration: common.HexToAddress("{{ .Calibration.SessionKeyRegistry.Hex }}"), + } +) +`)) + +func main() { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + fmt.Println("reading mainnet addresses...") + mainnet, err := readAddresses(ctx, rpcMainnet, fwssMainnet) + if err != nil { + fmt.Fprintf(os.Stderr, "mainnet: %v\n", err) + os.Exit(1) + } + fmt.Printf(" payments: %s\n", mainnet.Payments.Hex()) + fmt.Printf(" stateview: %s\n", mainnet.StateView.Hex()) + fmt.Printf(" pdpverifier: %s\n", mainnet.PDPVerifier.Hex()) + fmt.Printf(" spregistry: %s\n", mainnet.SPRegistry.Hex()) + fmt.Printf(" sessionkey: %s\n", mainnet.SessionKeyRegistry.Hex()) + + fmt.Println("reading calibration addresses...") + calibration, err := readAddresses(ctx, rpcCalibration, fwssCalibration) + if err != nil { + fmt.Fprintf(os.Stderr, "calibration: %v\n", err) + os.Exit(1) + } + fmt.Printf(" payments: %s\n", calibration.Payments.Hex()) + fmt.Printf(" stateview: %s\n", calibration.StateView.Hex()) + fmt.Printf(" pdpverifier: %s\n", calibration.PDPVerifier.Hex()) + fmt.Printf(" spregistry: %s\n", calibration.SPRegistry.Hex()) + fmt.Printf(" sessionkey: %s\n", calibration.SessionKeyRegistry.Hex()) + + // go generate runs in the package's source directory + // if GOPACKAGE is set, we're running via go generate - output to current dir + // otherwise output to constants/ for manual invocation from repo root + outPath := "constants/addresses_generated.go" + if os.Getenv("GOPACKAGE") != "" { + outPath = "addresses_generated.go" + } + + f, err := os.Create(outPath) + if err != nil { + fmt.Fprintf(os.Stderr, "create file: %v\n", err) + os.Exit(1) + } + defer f.Close() + + err = tmpl.Execute(f, struct { + Mainnet *networkAddresses + Calibration *networkAddresses + }{ + Mainnet: mainnet, + Calibration: calibration, + }) + if err != nil { + fmt.Fprintf(os.Stderr, "execute template: %v\n", err) + os.Exit(1) + } + + fmt.Println("wrote constants/addresses_generated.go") +}