Try   HackMD

PoC1 : 구현(Implementation)

Authors

Jake Song(Onther Inc.)
Carl Park(Onther Inc.)


Genesis 블록에 Stamina 컨트랙트를 넣어두고 내부적으로 컨트랙트 함수를 호출하여 가스 비용을
위임하는 것을 목적으로 하였다. 이를 위해 EVM 실행 함수를 사용하여 컨트랙트 실행 함수를 만들었고
tx-execution, tx-pool, state-transition 단계에 적용하였다.

1. go-ethereum 구현

1.1 Stamina 컨트랙트 관련 parameter 설정

  1. Stamina 컨트랙트 주소 및 bytecode 설정
var StaminaContractAddressHex = "0x000000000000000000000000000000000000dead"
var StaminaContractAddress = common.HexToAddress(StaminaContractAddressHex)
 // deployed bytecode
var StaminaContractBin = "0x6080604052600436106100d0576000f..."
  1. Blockchain account 설정
  • 내부적으로 Stamina 컨트랙트 함수 호출하기 위한 sender 설정
  1. Stamina account 설정
  • 내부적으로 Stamina 컨트랙트 함수 호출하기 위한 Stamina Contract account 설정
type accountWrapper struct {
	address common.Address
}
 func (a accountWrapper) Address() common.Address {
	return a.address
}
var BlockchainAccount = accountWrapper{common.HexToAddress("0x00")}
var StaminaAccount = accountWrapper{StaminaContractAddress}
var StaminaABI, _ = abi.JSON(strings.NewReader(contract.StaminaABI))

1.2 Stamina 컨트랙트 실행 함수 정의

컨트랙트 함수를 불러오기 위해 자체적으로 EVM을 실행하는 것을 정의

  1. 거래를 보낸 sender의 delegatee를 조회하는 함수
func GetDelegatee(evm *vm.EVM, from common.Address) (common.Address, error) {
	data, err := staminaCommon.StaminaABI.Pack("getDelegatee", from)
	if err != nil {
		return common.Address{}, err
	}
 	ret, _, err := evm.StaticCall(staminaCommon.BlockchainAccount, staminaCommon.StaminaContractAddress, data, 1000000)
 	if err != nil {
		return common.Address{}, err
	}
 	return common.BytesToAddress(ret), nil
}
  1. delegatee의 Stamina를 조회하는 함수
func GetStamina(evm *vm.EVM, delegatee common.Address) (*big.Int, error) {
	data, err := staminaCommon.StaminaABI.Pack("getStamina", delegatee)
	if err != nil {
		return big.NewInt(0), err
	}
 	ret, _, err := evm.StaticCall(staminaCommon.BlockchainAccount, staminaCommon.StaminaContractAddress, data, 1000000)
 	if err != nil {
		return big.NewInt(0), err
	}
 	stamina := new(big.Int)
	stamina.SetBytes(ret)
 	return stamina, nil
}
  1. delegatee의 Stamina를 증가시키는 함수
func AddStamina(evm *vm.EVM, delegatee common.Address, gas *big.Int) error {
	data, err := staminaCommon.StaminaABI.Pack("addStamina", delegatee, gas)
	if err != nil {
		return err
	}
 	_, _, err = evm.Call(staminaCommon.BlockchainAccount, staminaCommon.StaminaContractAddress, data, 1000000, big.NewInt(0))
 	return err
}
  1. delegatee의 Stamina를 감소시키는 함수
func SubtractStamina(evm *vm.EVM, delegatee common.Address, gas *big.Int) error {
	data, err := staminaCommon.StaminaABI.Pack("subtractStamina", delegatee, gas)
	if err != nil {
		return err
	}
 	_, _, err = evm.Call(staminaCommon.BlockchainAccount, staminaCommon.StaminaContractAddress, data, 1000000, big.NewInt(0))
 	return err
}

1.3 tx-pool 검증 (static evm 정의)

  1. tx-pool에 포함되기 위한 거래검증을 위해 임의의 static evm 정의
// moscow - arbitrary msg & header & author
func (pool *TxPool) newStaticEVM() *vm.EVM {
	msg := types.NewMessage(
		staminaCommon.BlockchainAccount.Address(),
		&staminaCommon.StaminaContractAddress,
		0,
		big.NewInt(0),
		1000000,
		big.NewInt(1e9),
		nil,
		false,
	)
 	vmConfig := vm.Config{}
 	ctx := NewEVMContext(
		msg,
		&types.Header{
			Number:     big.NewInt(0),
			Time:       big.NewInt(0),
			Difficulty: big.NewInt(0),
		},
		pool.chain,
		&common.Address{},
	)
 	return vm.NewEVM(
		ctx,
		pool.currentState,
		pool.chainconfig,
		vmConfig,
	)
}

1.4 Genesis블록 환경 구성(Stamina 컨트랙트 구성)

  1. Stamina 컨트랙트 바이트 코드를 Genesis블록에 삽입
var err error
	staminaBinBytes, err := hex.DecodeString(staminaCommon.StaminaContractBin[2:])
	if err != nil {
		panic(err)
	}
    
staminaCommon.StaminaContractAddress: {
            Code:    staminaBinBytes,
            Balance: big.NewInt(0),
        },

1.5 tx-execution / tx-pool / tx-validation 수정

  1. state transition 적용
  • Stamina로 가스 구입하기
func (st *StateTransition) buyDelegateeGas(delegatee common.Address) error {
	mgval := new(big.Int).Mul(new(big.Int).SetUint64(st.msg.Gas()), st.gasPrice)
	balance, err := stamina.GetStamina(st.evm, delegatee)
	if err != nil {
		return err
	}
 	if balance.Cmp(mgval) < 0 {
		return errInsufficientBalanceForGas
	}
	if err := st.gp.SubGas(st.msg.Gas()); err != nil {
		return err
	}
	st.gas += st.msg.Gas()
 	st.initialGas = st.msg.Gas()
	stamina.SubtractStamina(st.evm, delegatee, mgval)
	return nil
}
  • nonce를 확인하여 유효한 거래인지 검증하고 유효하다면 buyDelegateeGas 실행
func (st *StateTransition) preDelegateeCheck(delegatee common.Address) error {
	// Make sure this transaction's nonce is correct.
	if st.msg.CheckNonce() {
		nonce := st.state.GetNonce(st.msg.From())
		if nonce < st.msg.Nonce() {
			return ErrNonceTooHigh
		} else if nonce > st.msg.Nonce() {
			return ErrNonceTooLow
		}
	}
 	return st.buyDelegateeGas(delegatee)
}
  1. state transition에 적용
  • delegatee 있는지 확인
  • delegatee가 있다면 delegatee가 위임하여 가스비용만큼의 stamina 부담
// get delegatee
	delegatee, _ := stamina.GetDelegatee(evm, msg.From())
	log.Info("GetDelegatee", "from", msg.From(), "delegatee", delegatee)
 	// moscow - if has delegatee
	if delegatee != common.HexToAddress("0x00") {
		if err = st.preDelegateeCheck(delegatee); err != nil {
			return
		}
 		// Pay intrinsic gas
		gas, err := IntrinsicGas(st.data, contractCreation, homestead)
		if err != nil {
			return nil, 0, false, err
		}
		if err = st.useGas(gas); err != nil {
			return nil, 0, false, err
		}
 		if contractCreation {
			ret, _, st.gas, vmerr = evm.Create(sender, st.data, st.gas, st.value)
		} else {
			// Increment the nonce for the next transaction
			st.state.SetNonce(msg.From(), st.state.GetNonce(sender.Address())+1)
			ret, st.gas, vmerr = evm.Call(sender, st.to(), st.data, st.gas, st.value)
		}
		if vmerr != nil {
			log.Debug("VM returned with error", "err", vmerr)
			// The only possible consensus-error would be if there wasn't
			// sufficient balance to make the transfer happen. The first
			// balance transfer may never fail.
			if vmerr == vm.ErrInsufficientBalance {
				return nil, 0, false, vmerr
			}
		}
		st.refundDelegateeGas(delegatee)
		// TODO: gas fee to miner
		st.state.AddBalance(st.evm.Coinbase, new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), st.gasPrice))
 		return ret, st.gasUsed(), vmerr != nil, err
	}
 	// moscow - original version
	if err = st.preCheck(); err != nil {
		return
	}
  • remaining stamina가 있다면 delegatee에게 환불
func (st *StateTransition) refundDelegateeGas(delegatee common.Address) {
	// Apply refund counter, capped to half of the used gas.
	refund := st.gasUsed() / 2
	if refund > st.state.GetRefund() {
		refund = st.state.GetRefund()
	}
	st.gas += refund
 	// Return ETH for remaining gas, exchanged at the original rate.
	remaining := new(big.Int).Mul(new(big.Int).SetUint64(st.gas), st.gasPrice)
	stamina.AddStamina(st.evm, delegatee, remaining)
 	// Also return remaining gas to the block gas counter so it is
	// available for the next transaction.
	st.gp.AddGas(st.gas)
}
  1. tx pool에 적용
  • delegatee의 stamina, transaction sender의 balance가 충분한지 확인
	evm := pool.newStaticEVM()
	delegatee, err := stamina.GetDelegatee(evm, from)
	if err != nil {
		return ErrStaminaGetDelegatee
	}
 	if delegatee != common.HexToAddress("0x00") {
		mgval := new(big.Int).Mul(tx.GasPrice(), big.NewInt(int64(tx.Gas())))
 		// delegatee should have enough stemina
		// cost == GP * GL
		if stamina, _ := stamina.GetStamina(evm, delegatee); stamina.Cmp(mgval) < 0 {
			return ErrInsufficientStamina
		}
		// sender should have enough value
		if pool.currentState.GetBalance(from).Cmp(tx.Value()) < 0 {
			return ErrInsufficientValue
		}
	} else {
		// Transactor should have enough funds to cover the costs
		// cost == V + GP * GL
		if pool.currentState.GetBalance(from).Cmp(tx.Cost()) < 0 {
			return ErrInsufficientFunds
		}
	}
  • stamina와 balance 충분한 거래는 tx-pool에 포함하고 충분하지 않다면 tx-pool에서 제외
// Drop all transactions that are too costly (low balance or out of gas)
		drops, _ := list.Filter(pool.currentState.GetBalance(addr), pool.currentMaxGas)
		// moscow - do not drop tx if delegatee has enough stamina
		evm := pool.newStaticEVM()
		delegatee, _ := stamina.GetDelegatee(evm, addr)
		stamina, _ := stamina.GetStamina(evm, delegatee)
		balance := pool.currentState.GetBalance(addr)
 		var costlimit *big.Int
 		if stamina.Cmp(balance) >= 0 {
			costlimit = stamina
		} else {
			costlimit = balance
		}
 		drops, _ := list.Filter(costlimit, pool.currentMaxGas)

3.2 py-evm 구현

3.2.1 py-evm 개요 및 구조

py-evm은 Ethereum의 하드포크에 따른 EVM을 정의하고 각 EVM을 상속하여 client를 구현하고 있다.
Frontier:
Homestead:
Tangerine Whistle:
Suprious Dragon:
Byzantium:

3.2.2 구현

Byzantium EVM을 상속하는 Moscow EVM을 정의하여 가스비 위임 모델을 구현하였다.

3.2.3 Stamina 컨트랙트 실행함수 EVM 실행 함수 정의

EVM 실행 함수를 정의하고 Stamina 컨트랙트 함수를 실행하기 위한 함수를 정의함

  1. EVM 실행함수 정의
  • message 객체를 만든다
  • transaction context를 만든다
  • state, message, transaction context를 인자로 받아 VM을 실행한다
def execute_bytecode(state,
                     origin,
                     gas_price,
                     gas,
                     to,
                     sender,
                     value,
                     data,
                     code,
                     code_address=None,
                     ):
    """
    Execute raw bytecode in the context of the current state of
    the virtual machine.
    """
    if origin is None:
        origin = sender

    # Construct a message
    message = Message(
        gas=gas,
        to=to,
        sender=sender,
        value=value,
        data=data,
        code=code,
        code_address=code_address,
    )

    # Construction a tx context
    transaction_context = state.get_transaction_context_class()(
        gas_price=gas_price,
        origin=origin,
    )

    # Execute it in the VM
    return state.get_computation(message, transaction_context).apply_computation(
        state,
        message,
        transaction_context,
    )

  1. 컨트랙트 함수 호출하는 함수 정의
  • delegatee가 있는 지 확인하는 함수
def getDelegate(state, delegator) :
    fdata = keccak("getDelegate(address)".encode())
    fnsig = fdata[0:4]
    adata = encode_single('address', delegator)
    data = fnsig + adata
    vm_state = state
    code = vm_state.account_db.get_code(STAMINA)

    # params: (state, origin, gas_price, gas, to, sender, value, data, code, code_address=None)
    computation = execute_bytecode(state, None, 100, 100, STAMINA, BLOCKCHAIN, 0, data, code, None)

    assert(computation.is_success)

    addr = computation._memory._bytes
    return addr
  • delegatee의 Stamina를 가져오는 함수
def getStamina(state, delegate) :
    fdata = keccak("getStamina(address)".encode())
    fnsig = fdata[0:4]
    adata = encode_single('address', delegate)
    data = fnsig + adata
    vm_state = state
    code = vm_state.account_db.get_code(STAMINA)

    # params: (state, origin, gas_price, gas, to, sender, value, data, code, code_address=None)
    computation = execute_bytecode(state, None, 100, 100, STAMINA, BLOCKCHAIN, 0, data, code, None)

    assert(computation.is_success)

    ret = computation.output
    return ret
  • Stamina를 증가시키는 함수
def addStamina(state, delegate, val) :
​   fdata = keccak("addStamina(address,uint256)".encode())
​   fnsig = fdata[0:4]
​   adata = encode_single('(address,uint256)', [delegate,val])
​   data = fnsig + adata
​   vm_state = state
​   code = vm_state.account_db.get_code(STAMINA)

​   # params: (state, origin, gas_price, gas, to, sender, value, data, code, code_address=None)
​   computation = execute_bytecode(state, None, 100, 100, STAMINA, BLOCKCHAIN, 0, data, code, None)

​   assert(computation.is_success)
  • Stamina를 감소시키는 함수
def subtractStamina(state, delegate, val) :
    fdata = keccak("subtractStamina(address,uint256)".encode())
    fnsig = fdata[0:4]
    adata = encode_single('(address,uint256)', [delegate,val])
    data = fnsig + adata
    vm_state = state
    code = vm_state.account_db.get_code(STAMINA)

    # params: (state, origin, gas_price, gas, to, sender, value, data, code, code_address=None)
    computation = execute_bytecode(state, None, 100, 100, STAMINA, BLOCKCHAIN, 0, data, code, None)

    assert(computation.is_success)

3.2.3 state-transition 수정

  1. Moscow VM이라는 Custom VM을 정의하여 수정

3.2.4 Script 파일 작성

delegatee설정 -> deposit -> transfer 과정을 만들고 가스비가 위임되는 것을 시나리오 형태로 구현

  1. Account 설정
ADDRESS_1_PRIVATE_KEY = keys.PrivateKey(
    decode_hex('0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8')
)
ADDRESS_2_PRIVATE_KEY = keys.PrivateKey(
    decode_hex('0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d0')
)
ADDRESS_3_PRIVATE_KEY = keys.PrivateKey(
    decode_hex('0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d1')
)
ADDRESS_4_PRIVATE_KEY = keys.PrivateKey(
    decode_hex('0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d2')
)

ADDRESS_1 = Address(ADDRESS_1_PRIVATE_KEY.public_key.to_canonical_address())
ADDRESS_2 = Address(ADDRESS_2_PRIVATE_KEY.public_key.to_canonical_address())
ADDRESS_3 = Address(ADDRESS_3_PRIVATE_KEY.public_key.to_canonical_address())
ADDRESS_4 = Address(ADDRESS_4_PRIVATE_KEY.public_key.to_canonical_address())

BLOCKCHAIN = Address(b'\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x0a')
STAMINA =  Address(b'\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x02')
  1. Genesis 블록 설정
GENESIS_STATE = {
    ADDRESS_1: {
        "balance" : 10**19,
        "nonce" : 0,
        "code" : b"",
        "storage" : {}
    },
    ADDRESS_2: {
        "balance" : 10**19,
        "nonce" : 0,
        "code" : b"",
        "storage" : {}
    },
    ADDRESS_3: {
        "balance" : 10**19,
        "nonce" : 0,
        "code" : b"",
        "storage" : {}
    },
    ADDRESS_4: {
        "balance" : 10**19,
        "nonce" : 0,
        "code" : b"",
        "storage" : {}
    },
    STAMINA: {
        "balance" : 10**19,
        "nonce" : 0,
        "code" :
        b'0x6080604052600436106100fc5...',
        "storage" : {}
    }
}
  1. Chain 설정(Mining Chain, MemoryDB, Gensis Block)
klass = chains.base.MiningChain.configure(
    __name__='TestChain',
    vm_configuration=(
        (constants.GENESIS_BLOCK_NUMBER, ByzantiumVM),
    ))

chain = klass.from_genesis(MemoryDB(), GENESIS_PARAMS, GENESIS_STATE)
  1. TX1 - Stamina 컨트랙트 init 함수 실행
  • minDeposit(최소 예치금) 설정
######### TX1 ###########################
# init
vm = chain.get_vm()

fdata = keccak("init(uint256)".encode())
fnsig = fdata[0:4]
adata = encode_single('uint256', 1000000000000000000)
data = fnsig + adata

nonce = vm.state.account_db.get_nonce(ADDRESS_1)
tx1 = vm.create_unsigned_transaction(
    nonce=nonce,
    gas_price=100,
    gas=100000,
    to=STAMINA,
    value=0,
    data=data,
)

signed_tx1 = tx1.as_signed_transaction(ADDRESS_1_PRIVATE_KEY)

new_header, receipt, computation = chain.apply_transaction(signed_tx1)

# We have to finalize the block first in order to be able read the
# attributes that are important for the PoW algorithm
block = chain.get_vm().finalize_block(chain.get_block())

# based on mining_hash, block number and difficulty we can perform
# the actual Proof of Work (PoW) mechanism to mine the correct
# nonce and mix_hash for this block
nonce, mix_hash = mine_pow_nonce(
    block.number,
    block.header.mining_hash,
    block.header.difficulty)

block = chain.mine_block(mix_hash=mix_hash, nonce=nonce)
vm = chain.get_vm()
print("########### init (from: ADDRESS_1) ###############")
print("BLOCK1 ADDRESS_1 BALANCE : {}".format(vm.state.account_db.get_balance(ADDRESS_1)))
print("BLOCK1 ADDRESS_2 BALANCE : {}".format(vm.state.account_db.get_balance(ADDRESS_2)))
print("BLOCK1 ADDRESS_3 BALANCE : {}".format(vm.state.account_db.get_balance(ADDRESS_3)))
print("BLOCK1 ADDRESS_4 BALANCE : {}".format(vm.state.account_db.get_balance(ADDRESS_4)))
print("-----------------------")
  1. TX2 - setDelegatee 실행
  • delegatee가 delegator 지정
############ TX2 ##################
# setDelegatee
vm = chain.get_vm()

fdata = keccak("setDelegatee(address)".encode())
fnsig = fdata[0:4]
adata = encode_single('address', ADDRESS_2)
data = fnsig + adata

nonce = vm.state.account_db.get_nonce(ADDRESS_3)
tx2 = vm.create_unsigned_transaction(
    nonce=nonce,
    gas_price=100,
    gas=100000,
    to=STAMINA,
    value=0,
    data=data,
)

signed_tx2 = tx2.as_signed_transaction(ADDRESS_3_PRIVATE_KEY)

_, _, computation = chain.apply_transaction(signed_tx2)

# We have to finalize the block first in order to be able read the
# attributes that are important for the PoW algorithm
block = chain.get_vm().finalize_block(chain.get_block())

# based on mining_hash, block number and difficulty we can perform
# the actual Proof of Work (PoW) mechanism to mine the correct
# nonce and mix_hash for this block
nonce, mix_hash = mine_pow_nonce(
    block.number,
    block.header.mining_hash,
    block.header.difficulty)

block = chain.mine_block(mix_hash=mix_hash, nonce=nonce)

vm = chain.get_vm()
print("########### setDelegatee (delegate: ADDRESS_3, delegator: ADDRESS_2)###############")
print("BLOCK2 ADDRESS_1 BALANCE : {}".format(vm.state.account_db.get_balance(ADDRESS_1)))
print("BLOCK2 ADDRESS_2 BALANCE : {}".format(vm.state.account_db.get_balance(ADDRESS_2)))
print("BLOCK2 ADDRESS_3 BALANCE : {}".format(vm.state.account_db.get_balance(ADDRESS_3)))
print("BLOCK2 ADDRESS_4 BALANCE : {}".format(vm.state.account_db.get_balance(ADDRESS_4)))
print("-----------------------")
  1. deposit 실행
  • delegator가 delegatee에게 deposit함
############ TX3 ##################
# deposit
vm = chain.get_vm()

fdata = keccak("deposit(address)".encode())
fnsig = fdata[0:4]
adata = encode_single('address', ADDRESS_3)
data = fnsig + adata

nonce = vm.state.account_db.get_nonce(ADDRESS_2)
tx3 = vm.create_unsigned_transaction(
    nonce=nonce,
    gas_price=100,
    gas=100000,
    to=STAMINA,
    value=1000000000000000000,
    data=data,
)

signed_tx3 = tx3.as_signed_transaction(ADDRESS_2_PRIVATE_KEY)

_, _, computation = chain.apply_transaction(signed_tx3)

# We have to finalize the block first in order to be able read the
# attributes that are important for the PoW algorithm
block = chain.get_vm().finalize_block(chain.get_block())

# based on mining_hash, block number and difficulty we can perform
# the actual Proof of Work (PoW) mechanism to mine the correct
# nonce and mix_hash for this block
nonce, mix_hash = mine_pow_nonce(
    block.number,
    block.header.mining_hash,
    block.header.difficulty)

block = chain.mine_block(mix_hash=mix_hash, nonce=nonce)

vm = chain.get_vm()

print("########### deposit (delegate: ADDRESS_3, delegator: ADDRESS_2) ###############")
print("BLOCK3 ADDRESS_1 BALANCE : {}".format(vm.state.account_db.get_balance(ADDRESS_1)))
print("BLOCK3 ADDRESS_2 BALANCE : {}".format(vm.state.account_db.get_balance(ADDRESS_2)))
print("BLOCK3 ADDRESS_3 BALANCE : {}".format(vm.state.account_db.get_balance(ADDRESS_3)))
print("BLOCK3 ADDRESS_4 BALANCE : {}".format(vm.state.account_db.get_balance(ADDRESS_4)))
print("-----------------------")
  1. transfer 실행
############ TX4 ##################
# transfer
vm = chain.get_vm()

nonce = vm.state.account_db.get_nonce(ADDRESS_2)
tx4 = vm.create_unsigned_transaction(
    nonce=nonce,
    gas_price=100,
    gas=100000,
    to=ADDRESS_4,
    value=1000000000000000000,
    data=data,
)

signed_tx4 = tx4.as_signed_transaction(ADDRESS_2_PRIVATE_KEY)

_, _, computation = chain.apply_transaction(signed_tx4)

# We have to finalize the block first in order to be able read the
# attributes that are important for the PoW algorithm
block = chain.get_vm().finalize_block(chain.get_block())

# based on mining_hash, block number and difficulty we can perform
# the actual Proof of Work (PoW) mechanism to mine the correct
# nonce and mix_hash for this block
nonce, mix_hash = mine_pow_nonce(
    block.number,
    block.header.mining_hash,
    block.header.difficulty)

block = chain.mine_block(mix_hash=mix_hash, nonce=nonce)

vm = chain.get_vm()

print("########### transfer (from: ADDRESS_2, to: ADDRESS_4)###############")
print("BLOCK4 ADDRESS_1 BALANCE : {}".format(vm.state.account_db.get_balance(ADDRESS_1)))
print("BLOCK4 ADDRESS_2 BALANCE : {}".format(vm.state.account_db.get_balance(ADDRESS_2)))
print("BLOCK4 ADDRESS_3 BALANCE : {}".format(vm.state.account_db.get_balance(ADDRESS_3)))
print("BLOCK4 ADDRESS_4 BALANCE : {}".format(vm.state.account_db.get_balance(ADDRESS_4)))
print("-----------------------")