Skip to content

Commit

Permalink
Orchid generative AI demo node.
Browse files Browse the repository at this point in the history
  • Loading branch information
danopato committed Feb 27, 2024
1 parent 0b2fda4 commit afb9b52
Show file tree
Hide file tree
Showing 6 changed files with 456 additions and 0 deletions.
20 changes: 20 additions & 0 deletions gai-backend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
__pycache__

# IntelliJ related
*.iml
*.ipr
*.iws
.idea/

*~

33 changes: 33 additions & 0 deletions gai-backend/billing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import json

disconnect_threshold = -0.002

def invoice(amt):
return json.dumps({'type': 'invoice', 'amount': amt})

class Billing:
def __init__(self, prices):
self.ledger = {}
self.prices = prices

def credit(self, id, type=None, amount=0):
self.adjust(id, type, amount, 1)

def debit(self, id, type=None, amount=0):
self.adjust(id, type, amount, -1)

def adjust(self, id, type, amount, sign):
amount_ = self.prices[type] if type is not None else amount
if id in self.ledger:
self.ledger[id] = self.ledger[id] + sign * amount_
else:
self.ledger[id] = sign * amount_

def min_balance(self):
return 2 * (self.prices['invoice'] + self.prices['payment'])

def balance(self, id):
if id in self.ledger:
return self.ledger[id]
else:
return 0
50 changes: 50 additions & 0 deletions gai-backend/jobs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import asyncio
import random
import datetime
import json
import requests

import websockets

class jobs:
def __init__(self, model, url):
self.queue = asyncio.PriorityQueue()
self.sessions = {}
self.model = model
self.url = url

def get_queues(self, id):
squeue = asyncio.Queue(maxsize=10)
rqueue = asyncio.Queue(maxsize=10)
self.sessions[id] = {'send': squeue, 'recv': rqueue}
return [rqueue, squeue]

async def add_job(self, id, bid, job):
print(f'add_job({id}, {bid}, {job})')
priority = 0.1
await self.queue.put((priority, [id, bid, job]))

async def process_jobs(self):
while True:
priority, job_params = await self.queue.get()
print(f"process_jobs: got job: {job_params}")
id, bid, job = job_params
await self.sessions[id]['send'].put(json.dumps({'type': 'started'}))
result = ""
data = {'model': self.model, "temperature": 0.7, "top_p": 1,
"max_tokens": 4000, "stream": False, "safe_prompt": False, "random_seed": None,
'messages': [{'role': 'user', 'content': job['prompt']}]}
r = requests.post(self.url, data=json.dumps(data))
result = r.json()
print(result)
if result['object'] != 'chat.completion':
print('*** Error from llm')
continue
response = result['choices'][0]['message']['content']
model = result['model']
reason = result['choices'][0]['finish_reason']
usage = result['usage']
await self.sessions[id]['send'].put(json.dumps({'type': 'complete', 'response': response,
'model': model, 'reason': reason,
'usage': usage}))

82 changes: 82 additions & 0 deletions gai-backend/lottery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import json
import web3

from ticket import Ticket

# Gnosis - default
rpc_url_default = 'https://rpc.gnosischain.com/'
chain_id_default = 100

# Polygon - default
# rpc_url_default = 'https://polygon-rpc.com'
# chain_id_default = 137

# Gas default
gas_amount_default = 100000

uint64 = pow(2, 64) - 1 # 18446744073709551615
uint128 = pow(2, 128) - 1 # 340282366920938463463374607431768211455

def to_32byte_hex(val):
return web3.Web3.to_hex(web3.Web3.to_bytes(hexstr=val).rjust(32, b'\0'))


class Lottery:
addr_type = pow(2, 20 * 8) - 1
contract_addr = '0x6dB8381b2B41b74E17F5D4eB82E8d5b04ddA0a82'
token = '0x' + '0' * 40
contract_abi_str = "[ { \"inputs\": [ { \"internalType\": \"uint64\", \"name\": \"day\", \"type\": \"uint64\" } ], \"stateMutability\": \"nonpayable\", \"type\": \"constructor\" }, { \"anonymous\": false, \"inputs\": [ { \"indexed\": true, \"internalType\": \"contract IERC20\", \"name\": \"token\", \"type\": \"address\" }, { \"indexed\": true, \"internalType\": \"address\", \"name\": \"funder\", \"type\": \"address\" }, { \"indexed\": true, \"internalType\": \"address\", \"name\": \"signer\", \"type\": \"address\" } ], \"name\": \"Create\", \"type\": \"event\" }, { \"anonymous\": false, \"inputs\": [ { \"indexed\": true, \"internalType\": \"bytes32\", \"name\": \"key\", \"type\": \"bytes32\" }, { \"indexed\": false, \"internalType\": \"uint256\", \"name\": \"unlock_warned\", \"type\": \"uint256\" } ], \"name\": \"Delete\", \"type\": \"event\" }, { \"anonymous\": false, \"inputs\": [ { \"indexed\": true, \"internalType\": \"address\", \"name\": \"funder\", \"type\": \"address\" }, { \"indexed\": true, \"internalType\": \"address\", \"name\": \"recipient\", \"type\": \"address\" } ], \"name\": \"Enroll\", \"type\": \"event\" }, { \"anonymous\": false, \"inputs\": [ { \"indexed\": true, \"internalType\": \"bytes32\", \"name\": \"key\", \"type\": \"bytes32\" }, { \"indexed\": false, \"internalType\": \"uint256\", \"name\": \"escrow_amount\", \"type\": \"uint256\" } ], \"name\": \"Update\", \"type\": \"event\" }, { \"inputs\": [ { \"internalType\": \"contract IERC20\", \"name\": \"token\", \"type\": \"address\" }, { \"internalType\": \"address\", \"name\": \"recipient\", \"type\": \"address\" }, { \"components\": [ { \"internalType\": \"bytes32\", \"name\": \"data\", \"type\": \"bytes32\" }, { \"internalType\": \"bytes32\", \"name\": \"reveal\", \"type\": \"bytes32\" }, { \"internalType\": \"uint256\", \"name\": \"packed0\", \"type\": \"uint256\" }, { \"internalType\": \"uint256\", \"name\": \"packed1\", \"type\": \"uint256\" }, { \"internalType\": \"bytes32\", \"name\": \"r\", \"type\": \"bytes32\" }, { \"internalType\": \"bytes32\", \"name\": \"s\", \"type\": \"bytes32\" } ], \"internalType\": \"struct OrchidLottery1.Ticket[]\", \"name\": \"tickets\", \"type\": \"tuple[]\" }, { \"internalType\": \"bytes32[]\", \"name\": \"refunds\", \"type\": \"bytes32[]\" } ], \"name\": \"claim\", \"outputs\": [], \"stateMutability\": \"nonpayable\", \"type\": \"function\" }, { \"inputs\": [ { \"internalType\": \"contract IERC20\", \"name\": \"token\", \"type\": \"address\" }, { \"internalType\": \"uint256\", \"name\": \"amount\", \"type\": \"uint256\" }, { \"internalType\": \"address\", \"name\": \"signer\", \"type\": \"address\" }, { \"internalType\": \"int256\", \"name\": \"adjust\", \"type\": \"int256\" }, { \"internalType\": \"int256\", \"name\": \"warn\", \"type\": \"int256\" }, { \"internalType\": \"uint256\", \"name\": \"retrieve\", \"type\": \"uint256\" } ], \"name\": \"edit\", \"outputs\": [], \"stateMutability\": \"nonpayable\", \"type\": \"function\" }, { \"inputs\": [ { \"internalType\": \"address\", \"name\": \"signer\", \"type\": \"address\" }, { \"internalType\": \"int256\", \"name\": \"adjust\", \"type\": \"int256\" }, { \"internalType\": \"int256\", \"name\": \"warn\", \"type\": \"int256\" }, { \"internalType\": \"uint256\", \"name\": \"retrieve\", \"type\": \"uint256\" } ], \"name\": \"edit\", \"outputs\": [], \"stateMutability\": \"payable\", \"type\": \"function\" }, { \"inputs\": [ { \"internalType\": \"bool\", \"name\": \"cancel\", \"type\": \"bool\" }, { \"internalType\": \"address[]\", \"name\": \"recipients\", \"type\": \"address[]\" } ], \"name\": \"enroll\", \"outputs\": [], \"stateMutability\": \"nonpayable\", \"type\": \"function\" }, { \"inputs\": [ { \"internalType\": \"address\", \"name\": \"funder\", \"type\": \"address\" }, { \"internalType\": \"address\", \"name\": \"recipient\", \"type\": \"address\" } ], \"name\": \"enrolled\", \"outputs\": [ { \"internalType\": \"uint256\", \"name\": \"\", \"type\": \"uint256\" } ], \"stateMutability\": \"view\", \"type\": \"function\" }, { \"inputs\": [ { \"internalType\": \"contract IERC20\", \"name\": \"token\", \"type\": \"address\" }, { \"internalType\": \"address\", \"name\": \"signer\", \"type\": \"address\" }, { \"internalType\": \"uint64\", \"name\": \"marked\", \"type\": \"uint64\" } ], \"name\": \"mark\", \"outputs\": [], \"stateMutability\": \"nonpayable\", \"type\": \"function\" }, { \"inputs\": [ { \"internalType\": \"address\", \"name\": \"sender\", \"type\": \"address\" }, { \"internalType\": \"uint256\", \"name\": \"amount\", \"type\": \"uint256\" }, { \"internalType\": \"bytes\", \"name\": \"data\", \"type\": \"bytes\" } ], \"name\": \"onTokenTransfer\", \"outputs\": [ { \"internalType\": \"bool\", \"name\": \"\", \"type\": \"bool\" } ], \"stateMutability\": \"nonpayable\", \"type\": \"function\" }, { \"inputs\": [ { \"internalType\": \"contract IERC20\", \"name\": \"token\", \"type\": \"address\" }, { \"internalType\": \"address\", \"name\": \"funder\", \"type\": \"address\" }, { \"internalType\": \"address\", \"name\": \"signer\", \"type\": \"address\" } ], \"name\": \"read\", \"outputs\": [ { \"internalType\": \"uint256\", \"name\": \"\", \"type\": \"uint256\" }, { \"internalType\": \"uint256\", \"name\": \"\", \"type\": \"uint256\" } ], \"stateMutability\": \"view\", \"type\": \"function\" }, { \"inputs\": [ { \"internalType\": \"uint256\", \"name\": \"count\", \"type\": \"uint256\" }, { \"internalType\": \"bytes32\", \"name\": \"seed\", \"type\": \"bytes32\" } ], \"name\": \"save\", \"outputs\": [], \"stateMutability\": \"nonpayable\", \"type\": \"function\" }, { \"inputs\": [ { \"internalType\": \"address\", \"name\": \"sender\", \"type\": \"address\" }, { \"internalType\": \"uint256\", \"name\": \"amount\", \"type\": \"uint256\" }, { \"internalType\": \"bytes\", \"name\": \"data\", \"type\": \"bytes\" } ], \"name\": \"tokenFallback\", \"outputs\": [], \"stateMutability\": \"nonpayable\", \"type\": \"function\" }]"
contract_abi= None

contract= None
rpc_url= None
chain_id = None
gas_amount = None

def __init__(self, rpc_url=rpc_url_default, chain_id=chain_id_default, gas_amount=gas_amount_default):
self.rpc_url = rpc_url
self.chain_id = chain_id
self.gas_amount = gas_amount

self.contract_abi = json.loads(self.contract_abi_str)

def init_contract(self, web3):
self.web3 = web3
self.contract = self.web3.eth.contract(address=self.contract_addr, abi=self.contract_abi)


@staticmethod
def prepareTicket(tk:Ticket, reveal):
return [tk.data, to_32byte_hex(reveal), tk.packed0, tk.packed1, to_32byte_hex(tk.sig_r), to_32byte_hex(tk.sig_s)]
return [tk.data.hex(), reveal, tk.packed0, tk.packed1, tk.sig_r, tk.sig_s]

# Ticket object, L1 address & key
def claim_ticket(self, ticket, recipient, executor_key, reveal):
tk = Lottery.prepareTicket(ticket, reveal)
executor_address = self.web3.eth.account.from_key(executor_key).address
l1nonce = self.web3.eth.get_transaction_count(executor_address)
func = self.contract.functions.claim(self.token, recipient, [tk], [])

tx = func.build_transaction({
'chainId': self.chain_id,
'gas': self.gas_amount,
'maxFeePerGas': self.web3.to_wei('100', 'gwei'),
'maxPriorityFeePerGas': self.web3.to_wei('40', 'gwei'),
'nonce': l1nonce
})

# Polygon Estimates
# if (self.chain_id == 137):
# gas_estimate = self.web3.eth.estimate_gas(tx)
# print("gas ", gas_estimate)
# tx.update({'gas': gas_estimate})

signed = self.web3.eth.account.sign_transaction(tx, private_key=executor_key)
txhash = self.web3.eth.send_raw_transaction(signed.rawTransaction)
return txhash.hex()

def check_balance(self, addressL1, addressL2):
escrow_amount = self.contract.functions.read(self.token, addressL1, addressL2).call(block_identifier='latest')[0]
balance = float(escrow_amount & uint128) / pow(10,18)
escrow = float(escrow_amount >> 128) / pow(10,18)
return balance, escrow
151 changes: 151 additions & 0 deletions gai-backend/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import asyncio
import websockets
import functools
import json
import hashlib
import random

import web3
import ethereum

import billing
import jobs
import ticket
import lottery
import os
import traceback
import sys

prices = {
'invoice': 0.0001,
'payment': 0.0001,
'connection': 0.0001,
'error': 0.0001,
'job': 0.01,
'complete': 0.001,
'started': 0.0001
}

internal_messages = ['charge']
disconnect_threshold = -25

def invoice(amt, commit, recipient):
return json.dumps({'type': 'invoice', 'amount': amt, 'commit': '0x' + str(commit), 'recipient': recipient})

def process_tickets(tix, recip, reveal, commit, lotto, key):
try:
# print(f'Got ticket: {tix[0]}')
tk = ticket.Ticket.deserialize_ticket(tix[0], reveal, commit, recip, lotaddr=lottery_address)
# tk.print_ticket()
if tk.is_winner(reveal):
hash = lotto.claim_ticket(tk, recip, key, reveal)
print(f"Claim tx: {hash}")
reveal, commit = new_reveal()
return tk.face_value() / pow(10,18), reveal, commit
except Exception:
print('process_ticket() failed')
exc_type, exc_value, exc_traceback = sys.exc_info()
traceback.print_exception(exc_type, exc_value, exc_traceback, limit=20, file=sys.stdout)
return 0, reveal, commit

async def send_error(ws, code):
await ws.send(json.dumps({'type': 'error', 'code': code}))

def new_reveal():
num = hex(random.randrange(pow(2,256)))[2:]
reveal = '0x' + num[2:].zfill(64)
# print(f'new_reveal: {reveal}')
try:
commit = ethereum.utils.sha3(bytes.fromhex(reveal[2:])).hex()
except:
exc_type, exc_value, exc_traceback = sys.exc_info()
traceback.print_exception(exc_type, exc_value, exc_traceback, limit=20, file=sys.stdout)
return reveal, commit

async def session(websocket, bills=None, job=None, recipient='0x0', key=''):
print("New client connection")
reserve_price = 0.00006
lotto = lottery.Lottery()
w3 = web3.Web3(web3.Web3.HTTPProvider('https://rpc.gnosischain.com/'))
lotto.init_contract(w3)
id = websocket.id
bills.debit(id, type='invoice')
send_queue, recv_queue = job.get_queues(id)
reveal, commit = new_reveal()
await websocket.send(invoice(2 * bills.min_balance(), commit, recipient))
sources = [websocket.recv, recv_queue.get]
tasks = [None, None]
while True:
if bills.balance(id) < disconnect_threshold:
await websocket.close(reason='Balance too low')
break
try:
for i in range(2):
if tasks[i] is None:
tasks[i] = asyncio.create_task(sources[i]())
done, pending = await asyncio.wait(tasks, return_when = asyncio.FIRST_COMPLETED)
for i, task in enumerate(tasks):
if task in done:
tasks[i] = None
for task in done:
message_ = task.result()
message = json.loads(message_)
if message['type'] == 'payment':
try:
amt, reveal, commit = process_tickets(message['tickets'], recipient, reveal, commit, lotto, key)
bills.credit(id, amount=amt)
except:
print('outer failure in processing payment')
exc_type, exc_value, exc_traceback = sys.exc_info()
traceback.print_tb(exc_traceback, limit=1, file=sys.stdout)
bills.debit(id, type='error')
await send_error(websocket, -6001)
if bills.balance(id) < bills.min_balance():
bills.debit(id, type='invoice')
await websocket.send(invoice(2 * bills.min_balance() - bills.balance(id), commit, recipient))
if message['type'] not in internal_messages:
bills.debit(id, type=message['type'])
if message['type'] == 'job':
jid = hashlib.sha256(bytes(message['prompt'], 'utf-8')).hexdigest()
if reserve_price != 0 and float(message['bid']) < reserve_price:
await websocket.send(json.dumps({'type': 'bid_low'}))
continue
await job.add_job(id, message['bid'],
{'id': jid, 'prompt': message['prompt']})
if message['type'] == 'charge':
try:
bills.debit(id, amount=message['amount'])
await send_queue.put(True)
except:
print('exception in charge handler')
if message['type'] == 'complete':
await websocket.send(json.dumps({'type': 'job_complete', "output": message['response'],
'model': message['model'], 'reason': message['reason'],
'usage': message['usage']}))
if message['type'] == 'started':
await websocket.send(json.dumps({'type': 'job_started'}))
except (websockets.exceptions.ConnectionClosedOK, websockets.exceptions.ConnectionClosedError):
print('connection closed')
break


async def main(model, url, bind_addr, bind_port, recipient_key):
recipient_addr = web3.Account.from_key(recipient_key).address
bills = billing.Billing(prices)
job = jobs.jobs('', '')
print("\n*****")
print(f"* Server starting up at {bind_addr} {bind_port}")
print(f"* Connecting to back end at {url}")
print(f"* With model {model}")
print(f"* Using wallet at {recipient_addr}")
print("******\n\n")
async with websockets.serve(functools.partial(session, bills=bills, job=job, recipient=recipient_addr, key=recipient_key), bind_addr, bind_port):
await asyncio.wait([asyncio.create_task(job.process_jobs())])

if __name__ == "__main__":
bind_addr = os.environ['ORCHID_GENAI_ADDR']
bind_port = os.environ['ORCHID_GENAI_PORT']
recipient_key = os.environ['ORCHID_GENAI_RECIPIENT_KEY']
url = sys.argv[1]
model = sys.argv[2]
asyncio.run(main(model, url, bind_addr, bind_port, recipient_key))
Loading

0 comments on commit afb9b52

Please sign in to comment.