-
Notifications
You must be signed in to change notification settings - Fork 120
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
237 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,87 @@ | ||
package signer | ||
|
||
import "github.com/zeta-chain/node/zetaclient/chains/base" | ||
import ( | ||
"context" | ||
"crypto/sha256" | ||
|
||
"github.com/block-vision/sui-go-sdk/models" | ||
"github.com/pkg/errors" | ||
|
||
"github.com/zeta-chain/node/pkg/contracts/sui" | ||
cctypes "github.com/zeta-chain/node/x/crosschain/types" | ||
"github.com/zeta-chain/node/zetaclient/chains/base" | ||
) | ||
|
||
// Signer Sui outbound transaction signer. | ||
type Signer struct { | ||
*base.Signer | ||
client RPC | ||
gateway *sui.Gateway | ||
withdrawCap *withdrawCap | ||
} | ||
|
||
// RPC represents Sui rpc. | ||
type RPC interface { | ||
GetOwnedObjectID(ctx context.Context, ownerAddress, structType string) (string, error) | ||
|
||
MoveCall(ctx context.Context, req models.MoveCallRequest) (models.TxnMetaData, error) | ||
SuiExecuteTransactionBlock( | ||
ctx context.Context, | ||
req models.SuiExecuteTransactionBlockRequest, | ||
) (models.SuiTransactionBlockResponse, error) | ||
} | ||
|
||
// New Signer constructor. | ||
func New(baseSigner *base.Signer) *Signer { | ||
return &Signer{Signer: baseSigner} | ||
func New(baseSigner *base.Signer, client RPC, gateway *sui.Gateway) *Signer { | ||
return &Signer{ | ||
Signer: baseSigner, | ||
client: client, | ||
gateway: gateway, | ||
withdrawCap: &withdrawCap{}, | ||
} | ||
} | ||
|
||
// ProcessCCTX schedules outbound cross-chain transaction. | ||
func (s *Signer) ProcessCCTX(ctx context.Context, cctx *cctypes.CrossChainTx, zetaHeight uint64) error { | ||
// todo ... vote if confirmed, etc ... | ||
|
||
nonce := cctx.GetCurrentOutboundParam().TssNonce | ||
|
||
tx, err := s.buildWithdrawal(ctx, cctx) | ||
if err != nil { | ||
return errors.Wrap(err, "unable to build withdrawal tx") | ||
} | ||
|
||
sig, err := s.signTx(ctx, tx, zetaHeight, nonce) | ||
if err != nil { | ||
return errors.Wrap(err, "unable to sign tx") | ||
} | ||
|
||
if err := s.broadcast(ctx, tx, sig); err != nil { | ||
return errors.Wrap(err, "unable to broadcast tx") | ||
} | ||
|
||
// todo ... | ||
|
||
return nil | ||
} | ||
|
||
func (s *Signer) signTx(ctx context.Context, tx models.TxnMetaData, zetaHeight, nonce uint64) ([65]byte, error) { | ||
digest, err := sui.Digest(tx) | ||
if err != nil { | ||
return [65]byte{}, errors.Wrap(err, "unable to get digest") | ||
} | ||
|
||
// Another hashing is required for ECDSA. | ||
// https://docs.sui.io/concepts/cryptography/transaction-auth/signatures#signature-requirements | ||
digestWrapped := sha256.Sum256(digest[:]) | ||
|
||
// Send TSS signature request. | ||
return s.TSS().Sign( | ||
ctx, | ||
digestWrapped[:], | ||
zetaHeight, | ||
nonce, | ||
s.Chain().ChainId, | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
package signer | ||
|
||
import ( | ||
"context" | ||
"strconv" | ||
|
||
"github.com/block-vision/sui-go-sdk/models" | ||
"github.com/pkg/errors" | ||
|
||
"github.com/zeta-chain/node/pkg/coin" | ||
"github.com/zeta-chain/node/pkg/contracts/sui" | ||
cctypes "github.com/zeta-chain/node/x/crosschain/types" | ||
) | ||
|
||
const funcWithdraw = "withdraw" | ||
|
||
// buildWithdrawal builds unsigned withdrawal transaction using CCTX and Sui RPC | ||
// https://github.com/zeta-chain/protocol-contracts-sui/blob/0245ad3a2eb4001381625070fd76c87c165589b2/sources/gateway.move#L117 | ||
func (s *Signer) buildWithdrawal(ctx context.Context, cctx *cctypes.CrossChainTx) (tx models.TxnMetaData, err error) { | ||
params := cctx.GetCurrentOutboundParam() | ||
coinType := "" | ||
|
||
// Basic common-sense validation & coin-type determination | ||
switch { | ||
case params.ReceiverChainId != s.Chain().ChainId: | ||
return tx, errors.Errorf("invalid receiver chain id %d", params.ReceiverChainId) | ||
case cctx.ProtocolContractVersion != cctypes.ProtocolContractVersion_V2: | ||
v := cctx.ProtocolContractVersion | ||
return tx, errors.Errorf("invalid protocol version %q", v) | ||
case cctx.InboundParams == nil: | ||
return tx, errors.New("inbound params are nil") | ||
case cctx.InboundParams.IsCrossChainCall: | ||
return tx, errors.New("withdrawAndCall is not supported yet") | ||
case params.CoinType == coin.CoinType_Gas: | ||
coinType = string(sui.SUI) | ||
case params.CoinType == coin.CoinType_ERC20: | ||
coinType = cctx.InboundParams.Asset | ||
default: | ||
return tx, errors.Errorf("unsupported coin type %q", params.CoinType.String()) | ||
} | ||
|
||
var ( | ||
nonce = strconv.FormatUint(params.TssNonce, 10) | ||
recipient = params.Receiver | ||
amount = params.Amount.String() | ||
gasBudget = strconv.FormatUint(params.CallOptions.GasLimit, 10) | ||
) | ||
|
||
withdrawCapID, err := s.getWithdrawCapIDCached(ctx) | ||
if err != nil { | ||
return tx, errors.Wrap(err, "unable to get withdraw cap ID") | ||
} | ||
|
||
req := models.MoveCallRequest{ | ||
Signer: s.TSS().PubKey().AddressSui(), | ||
PackageObjectId: s.gateway.PackageID(), | ||
Module: s.gateway.Module(), | ||
Function: funcWithdraw, | ||
TypeArguments: []any{coinType}, | ||
Arguments: []any{s.gateway.ObjectID(), amount, nonce, recipient, withdrawCapID}, | ||
GasBudget: gasBudget, | ||
} | ||
|
||
return s.client.MoveCall(ctx, req) | ||
} | ||
|
||
// broadcast attaches signature to tx and broadcasts it to Sui network | ||
func (s *Signer) broadcast(ctx context.Context, tx models.TxnMetaData, sig [65]byte) error { | ||
sigBase64, err := sui.SerializeSignatureECDSA(sig, s.TSS().PubKey().Bytes(true)) | ||
if err != nil { | ||
return errors.Wrap(err, "unable to serialize signature") | ||
} | ||
|
||
req := models.SuiExecuteTransactionBlockRequest{ | ||
TxBytes: tx.TxBytes, | ||
Signature: []string{sigBase64}, | ||
} | ||
|
||
_, err = s.client.SuiExecuteTransactionBlock(ctx, req) | ||
if err != nil { | ||
return errors.Wrap(err, "unable to execute tx block") | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
package signer | ||
|
||
import ( | ||
"context" | ||
"sync" | ||
"time" | ||
|
||
"github.com/pkg/errors" | ||
) | ||
|
||
const withdrawCapTTL = 5 * time.Minute | ||
|
||
// withdrawCap represents WithdrawCap (capability) object | ||
// that is required as a "permission" to withdraw funds. | ||
// Should belong to TSS address on Sui. | ||
type withdrawCap struct { | ||
objectID string | ||
mu sync.RWMutex | ||
fetchedAt time.Time | ||
} | ||
|
||
func (wc *withdrawCap) valid() bool { | ||
wc.mu.RLock() | ||
defer wc.mu.RUnlock() | ||
|
||
if wc.objectID == "" { | ||
return false | ||
} | ||
|
||
return time.Now().Sub(wc.fetchedAt) < withdrawCapTTL | ||
} | ||
|
||
func (wc *withdrawCap) set(objectID string) { | ||
wc.mu.Lock() | ||
defer wc.mu.Unlock() | ||
|
||
wc.objectID = objectID | ||
wc.fetchedAt = time.Now() | ||
} | ||
|
||
// getWithdrawCapIDCached getWithdrawCapID with withdrawCapTTL cache. | ||
func (s *Signer) getWithdrawCapIDCached(ctx context.Context) (string, error) { | ||
if s.withdrawCap.valid() { | ||
return s.withdrawCap.objectID, nil | ||
} | ||
|
||
s.Logger().Std.Info().Msg("WithdrawCap cache expired, fetching new objectID") | ||
|
||
objectID, err := s.getWithdrawCapID(ctx) | ||
if err != nil { | ||
return "", errors.Wrap(err, "unable to get withdraw cap ID") | ||
} | ||
|
||
s.withdrawCap.set(objectID) | ||
|
||
s.Logger().Std.Info().Str("sui.object_id", objectID).Msg("WithdrawCap objectID fetched") | ||
|
||
return objectID, nil | ||
} | ||
|
||
// getWithdrawCapID returns the objectID of the WithdrawCap. Should belong to TSS address on Sui. | ||
func (s *Signer) getWithdrawCapID(ctx context.Context) (string, error) { | ||
owner := s.TSS().PubKey().AddressSui() | ||
structType := s.gateway.WithdrawCapType() | ||
|
||
objectID, err := s.client.GetOwnedObjectID(ctx, owner, structType) | ||
if err != nil { | ||
return "", errors.Wrap(err, "unable to get owned object ID") | ||
} | ||
|
||
return objectID, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters