github.com/iotexproject/iotex-core@v1.14.1-rc1/action/consignment_transfer.go (about)

     1  // Copyright (c) 2020 IoTeX Foundation
     2  // This source code is provided 'as is' and no warranties are given as to title or non-infringement, merchantability
     3  // or fitness for purpose and, to the extent permitted by law, all liability for your use of the code is disclaimed.
     4  // This source code is governed by Apache License 2.0 that can be found in the LICENSE file.
     5  
     6  package action
     7  
     8  import (
     9  	"encoding/hex"
    10  	"encoding/json"
    11  
    12  	"github.com/pkg/errors"
    13  
    14  	"github.com/ethereum/go-ethereum/accounts"
    15  
    16  	"github.com/iotexproject/go-pkgs/crypto"
    17  	"github.com/iotexproject/iotex-address/address"
    18  )
    19  
    20  const (
    21  	_reclaim = "This is to certify I am transferring the ownership of said bucket to said recipient on IoTeX blockchain"
    22  )
    23  
    24  // Errors
    25  var (
    26  	ErrNotSupported = errors.New("signature type not supported")
    27  )
    28  
    29  type (
    30  	// a consignment is a transaction signed by transferee which contains an embedded message signed by transferor
    31  	//
    32  	// transferee: the entity/address to receive the ownership of an asset/object
    33  	// transferor: the entity/address to transfer the ownership of the asset/object
    34  	//
    35  	// the transaction contains 2 part:
    36  	// 1. an embedded message that clearly identifies (1) the transferee, (2) the transferor, (3) nonce of the transferee,
    37  	// and (4) a unique ID of the asset/object to be transferred
    38  	// 2. a payload that constitutes a valid ECC signature of the message, by verifying that:
    39  	// (1) the signature is valid
    40  	// (2) signer matches the transferor in the message
    41  	// (3) signer matches the actual owner of the asset/object
    42  	// (4) asset ID in the message matches the ID of asset/object to be transferred on blockchain
    43  	// (5) nonce in the message matches transferee's nonce on blockchain
    44  	//
    45  	// successful verification of above is considered a consent that transferor does own the asset/object and transfer it
    46  	// to transferee, because transferee was able to present such a valid signature (hence the name "consignment"), which
    47  	// can only be generated by transferor
    48  
    49  	// Consignment represents a consignment
    50  	Consignment interface {
    51  		Transferor() address.Address
    52  		Transferee() address.Address
    53  		AssetID() uint64
    54  		TransfereeNonce() uint64
    55  	}
    56  
    57  	// ConsignMsgEther is the consignment message format of Ethereum
    58  	ConsignMsgEther struct {
    59  		BucketIdx uint64 `json:"bucket"`
    60  		Nonce     uint64 `json:"nonce"`
    61  		Recipient string `json:"recipient"`
    62  		Reclaim   string `json:"reclaim"`
    63  	}
    64  
    65  	// ConsignJSON is the JSON format of a consignment, it contains the type, message, and signature
    66  	ConsignJSON struct {
    67  		Type string `json:"type"`
    68  		Msg  string `json:"msg"`
    69  		Sig  string `json:"sig"`
    70  	}
    71  
    72  	consignment struct {
    73  		index     uint64
    74  		nonce     uint64
    75  		signer    address.Address
    76  		recipient address.Address
    77  	}
    78  )
    79  
    80  // NewConsignment creates a consignment from data
    81  func NewConsignment(data []byte) (Consignment, error) {
    82  	c := ConsignJSON{}
    83  	if err := json.Unmarshal(data, &c); err != nil {
    84  		return nil, err
    85  	}
    86  
    87  	switch c.Type {
    88  	case "Ethereum":
    89  		return processConsignmentEther(c)
    90  	default:
    91  		return nil, ErrNotSupported
    92  	}
    93  }
    94  
    95  func processConsignmentEther(c ConsignJSON) (Consignment, error) {
    96  	// parse embedded msg
    97  	msg := ConsignMsgEther{}
    98  	if err := json.Unmarshal([]byte(c.Msg), &msg); err != nil {
    99  		return nil, err
   100  	}
   101  	if msg.Reclaim != _reclaim {
   102  		return nil, errors.New("reclaim text does not match")
   103  	}
   104  
   105  	// verify signature
   106  	sig, err := hex.DecodeString(c.Sig)
   107  	if err != nil {
   108  		return nil, err
   109  	}
   110  	pk, err := RecoverPubkeyFromEccSig(c.Type, []byte(c.Msg), sig)
   111  	if err != nil {
   112  		return nil, err
   113  	}
   114  
   115  	con := consignment{}
   116  	con.signer = pk.Address()
   117  	if con.signer == nil {
   118  		return nil, errors.New("failed to get address")
   119  	}
   120  	con.recipient, err = address.FromString(msg.Recipient)
   121  	if err != nil {
   122  		return nil, err
   123  	}
   124  	con.index = uint64(msg.BucketIdx)
   125  	con.nonce = uint64(msg.Nonce)
   126  	return &con, nil
   127  }
   128  
   129  func (c *consignment) Transferor() address.Address {
   130  	return c.signer
   131  }
   132  
   133  func (c *consignment) Transferee() address.Address {
   134  	return c.recipient
   135  }
   136  
   137  func (c *consignment) AssetID() uint64 {
   138  	return c.index
   139  }
   140  
   141  func (c *consignment) TransfereeNonce() uint64 {
   142  	return c.nonce
   143  }
   144  
   145  // NewConsignMsg creates a consignment message from inputs
   146  func NewConsignMsg(sigType, recipient string, bucketIdx, nonce uint64) ([]byte, error) {
   147  	switch sigType {
   148  	case "Ethereum":
   149  		msg := ConsignMsgEther{
   150  			BucketIdx: bucketIdx,
   151  			Nonce:     nonce,
   152  			Recipient: recipient,
   153  			Reclaim:   _reclaim,
   154  		}
   155  		msgBytes, err := json.Marshal(msg)
   156  		if err != nil {
   157  			return nil, err
   158  		}
   159  		return msgBytes, nil
   160  	default:
   161  		return nil, ErrNotSupported
   162  	}
   163  }
   164  
   165  // NewConsignJSON creates a consignment JSON from inputs
   166  func NewConsignJSON(sigType, recipient, sig string, bucketIdx, nonce uint64) ([]byte, error) {
   167  	msgBytes, err := NewConsignMsg(sigType, recipient, bucketIdx, nonce)
   168  	if err != nil {
   169  		return nil, err
   170  	}
   171  
   172  	msgJSON := ConsignJSON{
   173  		Type: sigType,
   174  		Msg:  string(msgBytes),
   175  		Sig:  sig,
   176  	}
   177  	msgBytes, err = json.Marshal(msgJSON)
   178  	if err != nil {
   179  		return nil, err
   180  	}
   181  	return msgBytes, nil
   182  }
   183  
   184  // RecoverPubkeyFromEccSig recovers public key from ECC signature
   185  func RecoverPubkeyFromEccSig(sigType string, msg, sig []byte) (crypto.PublicKey, error) {
   186  	h, err := MsgHash(sigType, msg)
   187  	if err != nil {
   188  		return nil, err
   189  	}
   190  	pk, err := crypto.RecoverPubkey(h, sig)
   191  	if err != nil {
   192  		return nil, err
   193  	}
   194  	if pk.Verify(h, sig) {
   195  		return pk, nil
   196  	}
   197  	return nil, crypto.ErrInvalidKey
   198  }
   199  
   200  // MsgHash calculate the hash of msg
   201  func MsgHash(sigType string, msg []byte) ([]byte, error) {
   202  	switch sigType {
   203  	case "Ethereum":
   204  		h, _ := accounts.TextAndHash(msg)
   205  		return h, nil
   206  	default:
   207  		return nil, ErrNotSupported
   208  	}
   209  }