diff --git a/.changeset/calm-crews-unite.md b/.changeset/calm-crews-unite.md new file mode 100644 index 00000000..5698ae64 --- /dev/null +++ b/.changeset/calm-crews-unite.md @@ -0,0 +1,5 @@ +--- +"chainlink-deployments-framework": patch +--- + +fixes for sui provider diff --git a/chain/sui/provider/ctf_provider.go b/chain/sui/provider/ctf_provider.go index 652712ef..660fe6d7 100644 --- a/chain/sui/provider/ctf_provider.go +++ b/chain/sui/provider/ctf_provider.go @@ -10,7 +10,10 @@ import ( "testing" "time" + "github.com/go-resty/resty/v2" + "github.com/avast/retry-go/v4" + "github.com/block-vision/sui-go-sdk/models" sui_sdk "github.com/block-vision/sui-go-sdk/sui" chainsel "github.com/smartcontractkit/chain-selectors" "github.com/smartcontractkit/chainlink-testing-framework/framework" @@ -108,16 +111,17 @@ func (p *CTFChainProvider) Initialize(_ context.Context) (chain.BlockChain, erro } // Start the CTF Container - url, client := p.startContainer(chainID, deployerSigner) + url, faucetUrl, client := p.startContainer(chainID, deployerSigner) // Construct the chain p.chain = &sui.Chain{ ChainMetadata: sui.ChainMetadata{ Selector: p.selector, }, - Client: client, - Signer: deployerSigner, - URL: url, + Client: client, + Signer: deployerSigner, + URL: url, + FaucetURL: faucetUrl, // TODO: Implement ConfirmTransaction when available } @@ -144,11 +148,11 @@ func (p *CTFChainProvider) BlockChain() chain.BlockChain { // It returns the URL of the Sui node and the client to interact with it. func (p *CTFChainProvider) startContainer( chainID string, account sui.SuiSigner, -) (string, sui_sdk.ISuiAPI) { +) (string, string, sui_sdk.ISuiAPI) { var ( - attempts = uint(10) - url string - containerName string + attempts = uint(10) + url string + fauceturl string ) // initialize the docker network used by CTF @@ -161,6 +165,7 @@ func (p *CTFChainProvider) startContainer( type containerResult struct { url string + faucetPort string containerName string } @@ -204,13 +209,15 @@ func (p *CTFChainProvider) startContainer( if rerr != nil { // Return the ports to freeport to avoid leaking them during retries freeport.Return([]int{port, faucetPort}) + return containerResult{}, rerr } testcontainers.CleanupContainer(p.t, output.Container) return containerResult{ - url: output.Nodes[0].ExternalHTTPUrl + "/v1", + url: output.Nodes[0].ExternalHTTPUrl, + faucetPort: input.FaucetPort, containerName: output.ContainerName, }, nil }, @@ -225,7 +232,7 @@ func (p *CTFChainProvider) startContainer( require.NoError(p.t, err, "Failed to start CTF Sui container after %d attempts", attempts) url = result.url - containerName = result.containerName + fauceturl = fmt.Sprintf("http://%s:%s", "127.0.0.1", result.faucetPort) client := sui_sdk.NewSuiClient(url) @@ -240,14 +247,24 @@ func (p *CTFChainProvider) startContainer( } require.True(p.t, ready, "Sui network not ready") - dc, err := framework.NewDockerClient() + err = fundAccount(fauceturl, address) require.NoError(p.t, err) - _, err = dc.ExecContainer(containerName, []string{ - "sui", "client", "faucet", - "--address", address, - }) - require.NoError(p.t, err) + return url, fauceturl, client +} - return url, client +func fundAccount(url string, address string) error { + r := resty.New().SetBaseURL(url) + b := &models.FaucetRequest{ + FixedAmountRequest: &models.FaucetFixedAmountRequest{ + Recipient: address, + }, + } + resp, err := r.R().SetBody(b).SetHeader("Content-Type", "application/json").Post("/gas") + if err != nil { + return err + } + framework.L.Info().Any("Resp", resp).Msg("Address is funded!") + + return nil } diff --git a/chain/sui/signer.go b/chain/sui/signer.go index 408f328a..7e67e9bc 100644 --- a/chain/sui/signer.go +++ b/chain/sui/signer.go @@ -1,13 +1,13 @@ package sui import ( + "crypto/ed25519" "encoding/base64" "encoding/hex" - "errors" "fmt" - "github.com/block-vision/sui-go-sdk/constant" "github.com/block-vision/sui-go-sdk/signer" + "golang.org/x/crypto/blake2b" ) // TODO: Everything in this file should come from chainlink-sui when available @@ -46,24 +46,58 @@ func NewSignerFromHexPrivateKey(hexPrivateKey string) (SuiSigner, error) { } func (s *suiSigner) Sign(message []byte) ([]string, error) { - if s.signer == nil { - return nil, errors.New("signer is nil") - } + // Add intent scope for transaction data (0x00, 0x00, 0x00) + intentMessage := append([]byte{0x00, 0x00, 0x00}, message...) - // Sign the message as a transaction message - b64Message := base64.StdEncoding.EncodeToString(message) - signedMsg, err := s.signer.SignMessage(b64Message, constant.TransactionDataIntentScope) - if err != nil { - return nil, fmt.Errorf("failed to sign message: %w", err) - } + // Hash the message with blake2b + hash := blake2b.Sum256(intentMessage) + + // Sign the hash + signature := ed25519.Sign(s.signer.PriKey, hash[:]) + + // Get public key + publicKey := s.signer.PriKey.Public().(ed25519.PublicKey) + + // Create serialized signature: flag + signature + pubkey + serializedSig := make([]byte, 1+len(signature)+len(publicKey)) + serializedSig[0] = 0x00 // Ed25519 flag + copy(serializedSig[1:], signature) + copy(serializedSig[1+len(signature):], publicKey) + + // Encode to base64 + encoded := base64.StdEncoding.EncodeToString(serializedSig) - return []string{signedMsg.Signature}, nil + return []string{encoded}, nil } func (s *suiSigner) GetAddress() (string, error) { - if s.signer == nil { - return "", errors.New("signer is nil") + publicKey := s.signer.PriKey.Public().(ed25519.PublicKey) + + // For Ed25519, the signature scheme is 0x00 + const signatureScheme = 0x00 + + // Create the data to hash: signature scheme byte || public key + data := append([]byte{signatureScheme}, publicKey...) + + // Hash using Blake2b-256 + hash := blake2b.Sum256(data) + + // The Sui address is the hex representation of the hash + return "0x" + hex.EncodeToString(hash[:]), nil +} + +// PublicKeyBytes extracts the raw 32-byte ed25519 public key from a SuiSigner. +func PublicKeyBytes(s SuiSigner) ([]byte, error) { + impl, ok := s.(*suiSigner) + if !ok { + return nil, fmt.Errorf("unsupported signer type %T", s) + } + priv := []byte(impl.signer.PriKey) + if len(priv) != ed25519.PrivateKeySize { + return nil, fmt.Errorf("unexpected ed25519 key length: %d", len(priv)) } + pub := make([]byte, ed25519.PublicKeySize) + copy(pub, priv[32:]) // last 32 bytes are the pubkey - return s.signer.Address, nil + return pub, nil } diff --git a/chain/sui/sui_chain.go b/chain/sui/sui_chain.go index fd4c4f05..63888a38 100644 --- a/chain/sui/sui_chain.go +++ b/chain/sui/sui_chain.go @@ -11,8 +11,10 @@ type ChainMetadata = common.ChainMetadata // Chain represents an Sui chain. type Chain struct { ChainMetadata - Client sui.ISuiAPI - Signer SuiSigner - URL string + Client sui.ISuiAPI + Signer SuiSigner + URL string + FaucetURL string + // TODO: Implement ConfirmTransaction. Current tooling relies on node local execution } diff --git a/go.mod b/go.mod index 9da41b99..72e2e84e 100644 --- a/go.mod +++ b/go.mod @@ -51,6 +51,7 @@ require ( github.com/xssnick/tonutils-go v1.13.0 github.com/zksync-sdk/zksync2-go v1.1.1-0.20250620124214-2c742ee399c6 go.uber.org/zap v1.27.0 + golang.org/x/crypto v0.40.0 golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc golang.org/x/oauth2 v0.30.0 google.golang.org/grpc v1.74.2 @@ -280,7 +281,6 @@ require ( go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/ratelimit v0.3.1 // indirect - golang.org/x/crypto v0.40.0 // indirect golang.org/x/net v0.42.0 // indirect golang.org/x/sync v0.16.0 // indirect golang.org/x/sys v0.34.0 // indirect