msgctl 0.1.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- msg/__init__.py +2 -0
- msg/admin/__init__.py +0 -0
- msg/admin/backup_retirement.py +56 -0
- msg/admin/backups.py +341 -0
- msg/admin/custodial_check.py +77 -0
- msg/admin/diagnostics.py +750 -0
- msg/admin/market.py +91 -0
- msg/admin/market_check.py +341 -0
- msg/admin/money.py +322 -0
- msg/admin/preflight.py +142 -0
- msg/admin/recovery_replay.py +281 -0
- msg/admin/restore_database.py +37 -0
- msg/admin/root.py +347 -0
- msg/admin/rotation.py +197 -0
- msg/admin/token_delivery_check.py +54 -0
- msg/admin/upgrade_check.py +58 -0
- msg/application.py +163 -0
- msg/bootstrap.py +305 -0
- msg/cli.py +537 -0
- msg/client.py +725 -0
- msg/client_certificates.py +98 -0
- msg/client_content.py +43 -0
- msg/client_custodial.py +206 -0
- msg/client_market.py +104 -0
- msg/client_recovery.py +186 -0
- msg/client_secrets.py +58 -0
- msg/client_tokens.py +99 -0
- msg/client_upgrade.py +188 -0
- msg/config.py +318 -0
- msg/constants.py +11 -0
- msg/core/__init__.py +0 -0
- msg/core/batching.py +23 -0
- msg/core/codec.py +253 -0
- msg/core/contracts.py +128 -0
- msg/core/cursors.py +68 -0
- msg/core/email_address.py +33 -0
- msg/core/errors.py +22 -0
- msg/core/events.py +6 -0
- msg/core/execution_ports.py +20 -0
- msg/core/executor.py +193 -0
- msg/core/models.py +517 -0
- msg/core/packet.py +71 -0
- msg/core/permissions.py +14 -0
- msg/core/query.py +44 -0
- msg/core/read_query.py +60 -0
- msg/core/registry.py +173 -0
- msg/core/requests.py +58 -0
- msg/core/schema_policy.py +28 -0
- msg/core/schemas.py +30 -0
- msg/core/tags.py +21 -0
- msg/core/template_dsl.py +94 -0
- msg/core/text_patch.py +283 -0
- msg/core/tool_execution.py +28 -0
- msg/core/transfer.py +183 -0
- msg/daemon.py +255 -0
- msg/data/__init__.py +0 -0
- msg/data/bootstrap.json +72 -0
- msg/data/favicon.png +0 -0
- msg/data/logo-dark.svg +5 -0
- msg/data/logo.svg +5 -0
- msg/data/recovery-checkpoint.example.json +1 -0
- msg/data/recovery-checkpoint.schema.json +118 -0
- msg/data/shortcodes.json +1 -0
- msg/data/system/AGENTS.md +6 -0
- msg/data/system/rules/_index.md +17 -0
- msg/data/system/rules/auth.md +6 -0
- msg/data/system/rules/files.md +6 -0
- msg/data/system/rules/identity.md +6 -0
- msg/data/system/rules/protocol.md +6 -0
- msg/data/system/rules/read-write.md +6 -0
- msg/data/system/rules/recovery.md +6 -0
- msg/data/system/rules/security.md +10 -0
- msg/data/system/rules/topics.md +6 -0
- msg/extensions/__init__.py +1 -0
- msg/extensions/hosting.py +259 -0
- msg/extensions/keystore.py +96 -0
- msg/extensions/repositories.py +769 -0
- msg/extensions/rss.py +67 -0
- msg/extensions/ssh.py +264 -0
- msg/extensions/ssh_git.py +300 -0
- msg/extensions/tools.py +150 -0
- msg/hosting_runtime.py +138 -0
- msg/market/__init__.py +1 -0
- msg/market/arbitration.py +481 -0
- msg/market/delivery.py +338 -0
- msg/market/delivery_notifications.py +110 -0
- msg/market/delivery_targets.py +76 -0
- msg/market/email.py +100 -0
- msg/market/escrow.py +345 -0
- msg/market/orders.py +218 -0
- msg/market/policy.py +147 -0
- msg/market/rationale.py +91 -0
- msg/market/references.py +19 -0
- msg/market/targets.py +157 -0
- msg/plugins/__init__.py +28 -0
- msg/plugins/achievements.py +316 -0
- msg/plugins/batch.py +32 -0
- msg/plugins/bounty.py +366 -0
- msg/plugins/collaboration.py +261 -0
- msg/plugins/common.py +217 -0
- msg/plugins/communication.py +853 -0
- msg/plugins/content.py +855 -0
- msg/plugins/custodial_lifecycle.py +224 -0
- msg/plugins/delivery.py +230 -0
- msg/plugins/discovery.py +1266 -0
- msg/plugins/discussion.py +162 -0
- msg/plugins/extensions.py +10 -0
- msg/plugins/following.py +78 -0
- msg/plugins/hosting_capacity.py +52 -0
- msg/plugins/identity.py +1654 -0
- msg/plugins/money.py +221 -0
- msg/plugins/offers.py +183 -0
- msg/plugins/orders.py +273 -0
- msg/plugins/recovery.py +376 -0
- msg/plugins/schemas.py +5 -0
- msg/plugins/sharing.py +300 -0
- msg/plugins/store.py +249 -0
- msg/plugins/system.py +75 -0
- msg/plugins/transfer.py +249 -0
- msg/py.typed +0 -0
- msg/security/__init__.py +0 -0
- msg/security/age_keys.py +112 -0
- msg/security/authentication.py +141 -0
- msg/security/authorization.py +300 -0
- msg/security/backup_retirement.py +135 -0
- msg/security/capabilities.py +157 -0
- msg/security/certificates.py +183 -0
- msg/security/crypto.py +102 -0
- msg/security/custodial_migration.py +292 -0
- msg/security/custody_history.py +146 -0
- msg/security/network.py +81 -0
- msg/security/policy.py +67 -0
- msg/security/quarantine.py +19 -0
- msg/security/root_files.py +67 -0
- msg/security/rotation_journal.py +60 -0
- msg/security/sealed_box.py +70 -0
- msg/security/sharing_policy.py +29 -0
- msg/security/token_delivery.py +93 -0
- msg/security/vault.py +186 -0
- msg/storage/__init__.py +0 -0
- msg/storage/capacity.py +100 -0
- msg/storage/custodial_migration.py +22 -0
- msg/storage/git.py +435 -0
- msg/storage/ledger_migration.py +307 -0
- msg/storage/market_migration.py +49 -0
- msg/storage/postgres.py +663 -0
- msg/storage/query.py +39 -0
- msg/storage/read_only.py +126 -0
- msg/storage/session.py +403 -0
- msg/storage/sqlite.py +360 -0
- msg/storage/topic_event_migration.py +26 -0
- msg/storage/valkey_bus.py +45 -0
- msg/transports/__init__.py +0 -0
- msg/transports/client.py +183 -0
- msg/transports/dictionary.py +387 -0
- msg/transports/graphql.py +75 -0
- msg/transports/http.py +72 -0
- msg/transports/http_routes.py +1577 -0
- msg/transports/mcp.py +127 -0
- msg/transports/packet.py +42 -0
- msg/transports/read_tree_path.py +65 -0
- msg/transports/stdio.py +36 -0
- msg/transports/url_safety.py +123 -0
- msg/tui.py +368 -0
- msg/workers/__init__.py +1 -0
- msg/workers/effects.py +406 -0
- msg/workers/leases.py +19 -0
- msg/workers/mail.py +63 -0
- msg/workers/maintenance.py +310 -0
- msg/workers/sandbox.py +74 -0
- msg/workers/sandbox_child.py +189 -0
- msg/workers/webhook.py +160 -0
- msgctl-0.1.0a1.dist-info/METADATA +96 -0
- msgctl-0.1.0a1.dist-info/RECORD +176 -0
- msgctl-0.1.0a1.dist-info/WHEEL +4 -0
- msgctl-0.1.0a1.dist-info/entry_points.txt +4 -0
msg/admin/market.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Root-signed, physical-console arbitration configuration; never a network API."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import getpass
|
|
6
|
+
|
|
7
|
+
from msg.admin.money import _root_signer
|
|
8
|
+
from msg.admin.root import RootAdmin, require_local_console, root_envelope
|
|
9
|
+
from msg.constants import ROOT_SUBJECT
|
|
10
|
+
from msg.core.codec import canonical, digest, loads, wire
|
|
11
|
+
from msg.core.errors import require
|
|
12
|
+
from msg.core.models import AuditEvent, Event, ResourceRef
|
|
13
|
+
from msg.market.policy import DEFAULT_POLICY, validate
|
|
14
|
+
from msg.plugins.common import new_id
|
|
15
|
+
from msg.security.crypto import Ed25519Signer, open_private_key
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def apply_market(app, signer, *, action, operator, subject=None, policy=None, expected=None):
|
|
19
|
+
_root_signer(app, signer)
|
|
20
|
+
require(action in {'grant','revoke','publish'}, 'market_admin_action_invalid')
|
|
21
|
+
if action == 'publish':
|
|
22
|
+
policy = validate(policy)
|
|
23
|
+
require(policy['id'] != DEFAULT_POLICY['id'], 'builtin_policy_immutable')
|
|
24
|
+
async with app.metadata.transaction(write=True) as tx:
|
|
25
|
+
if action == 'publish':
|
|
26
|
+
require(tx.one('SELECT 1 FROM arbitration_policies WHERE id=?', (policy['id'],)) is None,
|
|
27
|
+
'arbitration_policy_exists')
|
|
28
|
+
for candidate in policy['candidates']:
|
|
29
|
+
identity = await tx.subject(candidate)
|
|
30
|
+
require(identity.kind == 'registered' and not identity.local_only, 'invalid_arbitrator')
|
|
31
|
+
tx.execute('INSERT INTO arbitration_policies(id,body,digest) VALUES (?,?,?)',
|
|
32
|
+
(policy['id'],canonical(policy).decode(),digest(policy)), write=True)
|
|
33
|
+
before, after = None, policy
|
|
34
|
+
else:
|
|
35
|
+
identity = await tx.subject(subject)
|
|
36
|
+
require(identity.kind == 'registered' and not identity.local_only and subject != ROOT_SUBJECT,
|
|
37
|
+
'invalid_arbitrator')
|
|
38
|
+
row = tx.one('SELECT epoch,active FROM arbitrator_roles WHERE subject=?', (subject,))
|
|
39
|
+
before = {'epoch':row[0],'active':row[1]} if row else None
|
|
40
|
+
if expected is not None:
|
|
41
|
+
require(before == expected, 'market_preview_stale')
|
|
42
|
+
require((action=='grant' and (not row or not row[1])) or
|
|
43
|
+
(action=='revoke' and row and row[1]), 'arbitrator_role_unchanged')
|
|
44
|
+
after = {'epoch': new_id('role') if action=='grant' else row[0], 'active': action=='grant'}
|
|
45
|
+
tx.execute('''INSERT INTO arbitrator_roles(subject,epoch,active) VALUES (?,?,?)
|
|
46
|
+
ON CONFLICT(subject) DO UPDATE SET epoch=excluded.epoch,active=excluded.active''',
|
|
47
|
+
(subject,after['epoch'],after['active']), write=True)
|
|
48
|
+
statement = {'action':action,'subject':subject,'before':before,'after':after,
|
|
49
|
+
'operator':operator,'at':wire(app.clock())}
|
|
50
|
+
proof = signer.sign(canonical(statement), purpose='market-admin')
|
|
51
|
+
event = Event(id=new_id('audit'), type='root.market.'+action, time=app.clock(),
|
|
52
|
+
request_id=new_id('local-market'), actor=ROOT_SUBJECT, subject=ROOT_SUBJECT,
|
|
53
|
+
resources=(ResourceRef(id=ROOT_SUBJECT),), data={**statement,'root_signature':wire(proof)})
|
|
54
|
+
await tx.append_event(event)
|
|
55
|
+
await tx.append_audit(AuditEvent(event=event,
|
|
56
|
+
authority=(ResourceRef(id=app.certificates.root_certificate.resource_id),),
|
|
57
|
+
before_digest=digest(before),after_digest=digest(after),previous_digest=None,
|
|
58
|
+
entry_digest='',result=action))
|
|
59
|
+
return {'action':action,'audit_event_id':event.id,'result':after}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class MarketAdmin:
|
|
63
|
+
def __init__(self, config_dir):
|
|
64
|
+
self.config_dir = config_dir
|
|
65
|
+
|
|
66
|
+
def execute(self, action, *, subject=None, policy_file=None):
|
|
67
|
+
operator = require_local_console(self.config_dir)
|
|
68
|
+
app = RootAdmin(self.config_dir)._app()
|
|
69
|
+
async def run():
|
|
70
|
+
await app.load()
|
|
71
|
+
try:
|
|
72
|
+
policy = loads(policy_file.read_bytes()) if policy_file is not None else None
|
|
73
|
+
resolved, before = subject, None
|
|
74
|
+
async with app.metadata.transaction(write=False) as tx:
|
|
75
|
+
if subject and subject.startswith('@'):
|
|
76
|
+
resolved = await tx.resolve('/'+subject)
|
|
77
|
+
if resolved:
|
|
78
|
+
row = tx.one('SELECT epoch,active FROM arbitrator_roles WHERE subject=?', (resolved,))
|
|
79
|
+
before = {'epoch':row[0],'active':row[1]} if row else None
|
|
80
|
+
preview = {'action':action,'subject':resolved,'before':before,'policy':policy}
|
|
81
|
+
print(canonical(preview).decode())
|
|
82
|
+
approval = 'CONFIRM MARKET '+digest(preview)
|
|
83
|
+
require(input('Type '+approval+' to continue: ') == approval, 'approval_cancelled')
|
|
84
|
+
pin = getpass.getpass('Root PIN/passphrase: ')
|
|
85
|
+
signer = Ed25519Signer.from_bytes(open_private_key(
|
|
86
|
+
loads(root_envelope(self.config_dir).read_bytes()), pin))
|
|
87
|
+
return await apply_market(app, signer, action=action, operator=operator,
|
|
88
|
+
subject=resolved,policy=policy,expected=before)
|
|
89
|
+
finally:
|
|
90
|
+
await app.close()
|
|
91
|
+
return asyncio.run(run())
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""Read-only financial invariants and a deliberately isolated Test Root scenario."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from copy import deepcopy
|
|
5
|
+
from dataclasses import replace
|
|
6
|
+
|
|
7
|
+
from msg.constants import ROOT_SUBJECT
|
|
8
|
+
from msg.core.codec import b64, canonical, decode, digest, loads, wire
|
|
9
|
+
from msg.core.errors import require
|
|
10
|
+
from msg.core.models import MailConfig, Signature
|
|
11
|
+
from msg.market.policy import contract, validate
|
|
12
|
+
from msg.plugins.money import MAX_MINOR, _balance, _supply
|
|
13
|
+
from msg.security.crypto import Ed25519Signer, verify
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def inspect_market(tx):
|
|
17
|
+
"""No initialization, repair, timer, statement, event, job or network call."""
|
|
18
|
+
count = 0
|
|
19
|
+
for oid,buyer,total,escrow,state in tx.rows(
|
|
20
|
+
'SELECT id,buyer,total_price_minor,escrow_subject,state FROM store_orders ORDER BY id'):
|
|
21
|
+
expected = total if state in {'funded','delivered','accepted','disputed'} else 0
|
|
22
|
+
require(_balance(tx,escrow)==expected, 'market_escrow_invariant')
|
|
23
|
+
require(tx.one('SELECT 1 FROM identities WHERE id=?',(escrow,)) is None and
|
|
24
|
+
tx.one('SELECT kind,subject_id FROM ledger_accounts WHERE id=?',(escrow,))==('order_escrow',None),
|
|
25
|
+
'market_escrow_identity')
|
|
26
|
+
snapshot = tx.one('SELECT body FROM order_contracts WHERE order_id=?',(oid,))
|
|
27
|
+
if snapshot:
|
|
28
|
+
locked = contract(tx,oid)
|
|
29
|
+
policy = locked['policy']
|
|
30
|
+
validate(policy['policy'])
|
|
31
|
+
require(digest(policy['policy'])==policy['policy_digest'] and
|
|
32
|
+
locked['buyer']==buyer and locked['total_price_minor']==total, 'market_snapshot_invariant')
|
|
33
|
+
row = tx.one('SELECT body FROM order_settlements WHERE order_id=?',(oid,))
|
|
34
|
+
if row:
|
|
35
|
+
fact = loads(row[0])
|
|
36
|
+
require(state in {'settled','refunded'} and fact['refund_minor']+fact['release_minor']==total,
|
|
37
|
+
'market_settlement_invariant')
|
|
38
|
+
actual = tx.one("SELECT COALESCE(SUM(amount_minor),0) FROM money_ledger WHERE debit_account=?",(escrow,))[0]
|
|
39
|
+
require(actual==total,'market_settlement_invariant')
|
|
40
|
+
elif snapshot:
|
|
41
|
+
require(state not in {'settled','refunded'},'market_settlement_missing')
|
|
42
|
+
count += 1
|
|
43
|
+
for raw,expected in tx.rows('SELECT body,digest FROM arbitration_policies'):
|
|
44
|
+
policy = loads(raw); validate(policy)
|
|
45
|
+
require(digest(policy)==expected,'arbitration_policy_corrupt')
|
|
46
|
+
for case_id,order_id,state in tx.rows('SELECT id,order_id,state FROM arbitration_cases'):
|
|
47
|
+
settlement = tx.one('SELECT decision_id FROM order_settlements WHERE order_id=?',(order_id,))
|
|
48
|
+
if state=='executed':
|
|
49
|
+
require(settlement is not None and settlement[0] is not None,'market_decision_unfunded')
|
|
50
|
+
from msg.market.arbitration import _read
|
|
51
|
+
from msg.market.rationale import record
|
|
52
|
+
row=tx.one('SELECT body FROM arbitration_decisions WHERE id=? AND case_id=?',
|
|
53
|
+
(settlement[0],case_id))
|
|
54
|
+
require(row is not None,'market_decision_missing')
|
|
55
|
+
decision=loads(row[0])
|
|
56
|
+
require(all(decision.get(k)==decision['proposal'].get(k)
|
|
57
|
+
for k in ('rationale_ref','rationale_digest')),'decision_rationale_mismatch')
|
|
58
|
+
record(tx,_read(tx,case_id),decision['proposal'])
|
|
59
|
+
return {'orders':count,'held_cases':tx.one("SELECT COUNT(*) FROM arbitration_cases WHERE state='held'")[0],
|
|
60
|
+
'escrow_invariants':True,'private_reads_only':True}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async def check_market(app,root,call,register,now):
|
|
64
|
+
"""20 MSG issue -> bank-funded 10 reward -> 5 bundle, entirely disposable."""
|
|
65
|
+
from msg.admin.market import apply_market
|
|
66
|
+
from msg.admin.money import apply_money
|
|
67
|
+
from msg.market.policy import DEFAULT_POLICY
|
|
68
|
+
from msg.workers.effects import EffectWorker
|
|
69
|
+
require(app.selftest_run_id is not None,'selftest_namespace_required')
|
|
70
|
+
bank,bank_id = await register('market-test-bank')
|
|
71
|
+
buyer,buyer_id = await register('market-test-buyer')
|
|
72
|
+
async def invoke(name,args,key=buyer,subject=buyer_id,**kw):
|
|
73
|
+
result = await call(name,args,key,subject,**kw)
|
|
74
|
+
require(result.status=='ok',result.error.code if result.error else 'market_selftest_failed')
|
|
75
|
+
return result
|
|
76
|
+
await apply_money(app,root,action='bank_add',operator='isolated-selftest',subject_id=bank_id)
|
|
77
|
+
await apply_money(app,root,action='mint',operator='isolated-selftest',amount_minor=20_000_000)
|
|
78
|
+
await apply_money(app,root,action='transfer',operator='isolated-selftest',subject_id=bank_id,amount_minor=20_000_000)
|
|
79
|
+
bounty = await invoke('bounty.create',{'name':'identity-test','terms':'One-use signature test',
|
|
80
|
+
'reward_minor':10_000_000,'budget_minor':10_000_000,'max_claims':1},bank,bank_id)
|
|
81
|
+
challenge = await invoke('bounty.challenge',{'listing_id':bounty.data['bounty']['listing_id']})
|
|
82
|
+
payload = challenge.data['challenge']
|
|
83
|
+
await invoke('bounty.claim',{'challenge_id':payload['challenge_id'],
|
|
84
|
+
'proof':wire(buyer.sign(canonical(payload),purpose='bounty-pop-v1'))})
|
|
85
|
+
listing = await invoke('store.listing_create',{'name':'store-selftest','item_kind':'bundle',
|
|
86
|
+
'price_minor':5_000_000,'currency_id':'primary','quantity':1,'delivery_mode':'managed_instant',
|
|
87
|
+
'escrow_policy':'escrow-v1','dispute_policy':'dispute-v1','terms':'Immutable test bundle.'},bank,bank_id)
|
|
88
|
+
async with app.metadata.transaction(write=False) as tx:
|
|
89
|
+
parent = tx.one("SELECT id FROM resources WHERE parent=? AND name='files'",(bank_id,))[0]
|
|
90
|
+
file = await invoke('content.file_put',{'parent':parent,'name':'hello.txt','media_type':'text/plain',
|
|
91
|
+
'data':b64(b'delivery-ok\n')},bank,bank_id)
|
|
92
|
+
package = await invoke('store.package_deposit',{'listing_id':listing.resources[0].id,
|
|
93
|
+
'listing_revision':listing.resources[0].revision,'manifest':{'text':'msg.lmm.best store selftest'},
|
|
94
|
+
'payload_refs':[wire(file.resources[0])]},bank,bank_id)
|
|
95
|
+
active = await invoke('store.listing_update',{'id':listing.resources[0].id,'state':'active',
|
|
96
|
+
'package_ref':package.data['package']['id']},bank,bank_id,
|
|
97
|
+
expected=((listing.resources[0].id,listing.data['generation']),))
|
|
98
|
+
old_settings = app.settings
|
|
99
|
+
class NoNetworkMail:
|
|
100
|
+
def __init__(self): self.messages=[]
|
|
101
|
+
async def send(self,job):
|
|
102
|
+
self.messages.append(dict(job.arguments)); return 'sent'
|
|
103
|
+
sender=NoNetworkMail()
|
|
104
|
+
app.settings=replace(app.settings,server=replace(app.settings.server,mail=MailConfig(enabled=True,
|
|
105
|
+
host='selftest.invalid',port=2525,tls='starttls',sender='test@selftest.invalid',credential_file=None)))
|
|
106
|
+
try:
|
|
107
|
+
sale = active.data['listing']
|
|
108
|
+
bought = await invoke('orders.buy',{'listing_id':sale['listing_id'],
|
|
109
|
+
'listing_revision':sale['listing_revision'],'quantity':1,'total_price_minor':5_000_000,
|
|
110
|
+
'currency_id':'primary','email':'buyer@selftest.invalid'},contract_version=3)
|
|
111
|
+
oid = bought.data['order']['id']
|
|
112
|
+
worker=EffectWorker(app,mail_sender=sender)
|
|
113
|
+
while await worker.run_once(): pass
|
|
114
|
+
require(len(sender.messages)==1 and oid not in sender.messages[0]['text'],'market_email_leak')
|
|
115
|
+
proof=loads(sender.messages[0]['text'])
|
|
116
|
+
await invoke('orders.email_verify',{'order_id':oid,'challenge_id':proof['challenge_id'],'token':proof['token']})
|
|
117
|
+
while await worker.run_once(): pass
|
|
118
|
+
require(len(sender.messages)==2 and set(loads(sender.messages[1]['text']))=={'order_id','handle','pickup'},
|
|
119
|
+
'market_email_leak')
|
|
120
|
+
delivery=await invoke('delivery.get',{'order_id':oid},contract_version=2)
|
|
121
|
+
require(delivery.data['delivery']['state']=='prepared' and
|
|
122
|
+
delivery.data['delivery']['payloads'][0]['data']==b64(b'delivery-ok\n'),'market_claim_invariant')
|
|
123
|
+
finally:
|
|
124
|
+
app.settings=old_settings
|
|
125
|
+
# A separate 1 MSG service dispute refunds the test buyer through a real
|
|
126
|
+
# fixed 2-of-3 panel. It leaves the official 5/15 final balances unchanged.
|
|
127
|
+
members={}
|
|
128
|
+
for i in range(3):
|
|
129
|
+
key,uid=await register('market-test-arbitrator-'+str(i)); members[uid]=key
|
|
130
|
+
await apply_market(app,root,action='grant',operator='isolated-selftest',subject=uid)
|
|
131
|
+
policy=deepcopy(DEFAULT_POLICY); policy.update(id='selftest-panel-v1',candidates=list(members))
|
|
132
|
+
await apply_market(app,root,action='publish',operator='isolated-selftest',policy=policy)
|
|
133
|
+
service=await invoke('store.listing_create',{'name':'case-selftest','item_kind':'service',
|
|
134
|
+
'price_minor':1_000_000,'currency_id':'primary','quantity':1,'delivery_mode':'service',
|
|
135
|
+
'escrow_policy':'escrow-v1','dispute_policy':policy['id'],'terms':'Test service dispute.'},bank,bank_id)
|
|
136
|
+
service=await invoke('store.listing_update',{'id':service.resources[0].id,'state':'active'},bank,bank_id,
|
|
137
|
+
expected=((service.resources[0].id,service.data['generation']),))
|
|
138
|
+
sale=service.data['listing']
|
|
139
|
+
bought=await invoke('orders.buy',{'listing_id':sale['listing_id'],'listing_revision':sale['listing_revision'],
|
|
140
|
+
'quantity':1,'total_price_minor':1_000_000,'currency_id':'primary'},contract_version=3)
|
|
141
|
+
opened=await invoke('orders.dispute_open',{'order_id':bought.data['order']['id'],'reason':'quality'})
|
|
142
|
+
case=await invoke('orders.dispute_get',{'case_id':opened.data['case_id']})
|
|
143
|
+
author=opened.data['panel'][0]
|
|
144
|
+
async with app.metadata.transaction(write=False) as tx:
|
|
145
|
+
parent=tx.one("SELECT id FROM resources WHERE parent=? AND name='files'",(author,))[0]
|
|
146
|
+
reason=await invoke('content.file_put',{'parent':parent,'name':'case-reason.txt',
|
|
147
|
+
'media_type':'text/plain','data':b64(b'Selftest evidence warrants a full refund.')},members[author],author)
|
|
148
|
+
bound=await invoke('orders.dispute_rationale',{'case_id':opened.data['case_id'],
|
|
149
|
+
'ref':wire(reason.resources[0])},members[author],author)
|
|
150
|
+
proposal={**case.data['proposal_base'],'outcome':'refund','refund_minor':1_000_000,
|
|
151
|
+
**{name:bound.data[name] for name in ('rationale_ref','rationale_digest')}}
|
|
152
|
+
for uid in opened.data['panel'][:2]:
|
|
153
|
+
voted=await invoke('orders.dispute_vote',{'proposal':proposal,
|
|
154
|
+
'signature':wire(members[uid].sign(canonical(proposal),purpose='arbitration-decision'))},members[uid],uid)
|
|
155
|
+
args={'case_id':opened.data['case_id'],'decision_id':voted.data['decision']['id']}
|
|
156
|
+
await invoke('orders.dispute_execute',args)
|
|
157
|
+
await invoke('orders.dispute_execute',args)
|
|
158
|
+
async with app.metadata.transaction(write=False) as tx:
|
|
159
|
+
await inspect_market(tx)
|
|
160
|
+
require(_balance(tx,buyer_id)==5_000_000 and _balance(tx,bank_id)==15_000_000 and
|
|
161
|
+
tx.one("SELECT SUM(amount_minor) FROM money_ledger WHERE kind='mint'")[0]==20_000_000,
|
|
162
|
+
'market_conservation_failed')
|
|
163
|
+
for (escrow,) in tx.rows("SELECT id FROM ledger_accounts WHERE kind IN ('order_escrow','bounty_escrow')"):
|
|
164
|
+
require(_balance(tx,escrow)==0,'market_escrow_invariant')
|
|
165
|
+
return True
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def inspect_clearing(app, tx):
|
|
169
|
+
"""Never migrate, mint, seed a bank, repair a row or unlock the Root key."""
|
|
170
|
+
tables = ('ledger_accounts','money_ledger','money_bank_roles','server_offers',
|
|
171
|
+
'money_purchases','resource_entitlements','bounty_listings','bounty_claims',
|
|
172
|
+
'store_orders','store_deliveries','store_order_events')
|
|
173
|
+
for table in tables:
|
|
174
|
+
require(tx.one('SELECT to_regclass(?)',(table,))[0] is not None, 'market_schema_missing')
|
|
175
|
+
require(tx.one("SELECT 1 FROM information_schema.columns WHERE table_name='money_ledger' "
|
|
176
|
+
"AND column_name='entry_key'") is not None, 'market_schema_missing')
|
|
177
|
+
balances = {account:int(amount) for account,amount in tx.rows('''
|
|
178
|
+
SELECT account,SUM(amount) FROM (
|
|
179
|
+
SELECT credit_account account,amount_minor amount FROM money_ledger WHERE credit_account IS NOT NULL
|
|
180
|
+
UNION ALL
|
|
181
|
+
SELECT debit_account account,-amount_minor amount FROM money_ledger WHERE debit_account IS NOT NULL
|
|
182
|
+
) entries GROUP BY account''')}
|
|
183
|
+
supply = _supply(tx)
|
|
184
|
+
require(0 <= supply <= MAX_MINOR and all(0 <= n <= MAX_MINOR for n in balances.values())
|
|
185
|
+
and sum(balances.values()) == supply, 'money_conservation_failure')
|
|
186
|
+
require(tx.one("SELECT 1 FROM ledger_accounts a JOIN identities i ON i.id=a.id "
|
|
187
|
+
"WHERE a.kind<>'subject' LIMIT 1") is None, 'escrow_has_identity')
|
|
188
|
+
public = Ed25519Signer.from_bytes((app.settings.service_keys/'receipt.key').read_bytes()).public_key
|
|
189
|
+
ledger = {}
|
|
190
|
+
for row in tx.rows('''SELECT id,kind,amount_minor,debit_account,credit_account,seq,
|
|
191
|
+
policy_version,policy_digest,receipt,actor,request_id,entry_key FROM money_ledger'''):
|
|
192
|
+
receipt = loads(row[8]); body = receipt['body']
|
|
193
|
+
expected = dict(zip(('transaction_id','kind','amount_minor','from_subject','to_subject',
|
|
194
|
+
'ledger_sequence','policy_version','policy_digest'),row[:8]))
|
|
195
|
+
require(all(body.get(k)==v for k,v in expected.items()), 'money_receipt_mismatch')
|
|
196
|
+
if row[6] >= 2:
|
|
197
|
+
require((body.get('actor'),body.get('request_id'),body.get('entry_key')) == row[9:],
|
|
198
|
+
'money_receipt_mismatch')
|
|
199
|
+
verify(public,canonical(body),decode(Signature,receipt['signature']),purpose='money-receipt')
|
|
200
|
+
ledger[row[0]] = body
|
|
201
|
+
for oid,escrow,total,state,quote,refs in tx.rows('''SELECT id,escrow_subject,total_price_minor,
|
|
202
|
+
state,quote_digest,receipt_refs FROM store_orders'''):
|
|
203
|
+
require(state != 'accepted', 'incomplete_order_commit')
|
|
204
|
+
expected = total if state in {'funded','delivered','disputed'} else 0
|
|
205
|
+
require(balances.get(escrow,0)==expected, 'order_escrow_mismatch')
|
|
206
|
+
require(all(ref in ledger for ref in loads(refs)), 'order_receipt_missing')
|
|
207
|
+
if quote is not None:
|
|
208
|
+
last=tx.one('SELECT to_state FROM store_order_events WHERE order_id=? ORDER BY seq DESC LIMIT 1',(oid,))
|
|
209
|
+
require(last is not None and last[0]==state, 'order_history_mismatch')
|
|
210
|
+
if state=='settled':
|
|
211
|
+
delivery=tx.one('SELECT d.state,o.escrow_policy FROM store_deliveries d '
|
|
212
|
+
'JOIN store_orders o ON o.id=d.order_id WHERE o.id=?',(oid,))
|
|
213
|
+
current=tx.one('SELECT 1 FROM order_contracts WHERE order_id=?',(oid,))
|
|
214
|
+
if current is not None:
|
|
215
|
+
# V3 managed-instant settlement is a fulfillment/payment fact,
|
|
216
|
+
# not buyer acknowledgement: its delivery intentionally stays
|
|
217
|
+
# prepared until the separate signed accept. Reuse the current
|
|
218
|
+
# immutable contract validator instead of weakening this check
|
|
219
|
+
# for arbitrary prepared legacy orders.
|
|
220
|
+
from msg.market.policy import contract
|
|
221
|
+
locked=contract(tx,oid)
|
|
222
|
+
prepared=(delivery is not None and delivery[0]=='prepared' and
|
|
223
|
+
locked['listing']['delivery_mode']=='managed_instant')
|
|
224
|
+
require(delivery is not None and (delivery[0]=='claimed' or prepared),
|
|
225
|
+
'settled_delivery_missing')
|
|
226
|
+
else:
|
|
227
|
+
require(delivery is not None and (delivery[0]=='claimed' or
|
|
228
|
+
delivery==('prepared','escrow-instant-v1')), 'settled_delivery_missing')
|
|
229
|
+
for escrow,total,state,eid in tx.rows('SELECT escrow_account,total_minor,state,entitlement_id FROM money_purchases'):
|
|
230
|
+
require(balances.get(escrow,0)==(total if state=='pending' else 0),'purchase_escrow_mismatch')
|
|
231
|
+
require((state=='settled') == (eid is not None), 'purchase_entitlement_mismatch')
|
|
232
|
+
for lid,escrow,budget,reward,maximum,state in tx.rows('''SELECT listing_id,escrow_subject,
|
|
233
|
+
budget_minor,reward_minor,max_claims,state FROM bounty_listings'''):
|
|
234
|
+
claims=tx.rows('SELECT claimant,reward_minor,transaction_id,challenge_id FROM bounty_claims WHERE listing_id=?',(lid,))
|
|
235
|
+
require(len(claims)<=maximum, 'bounty_limit_mismatch')
|
|
236
|
+
require(balances.get(escrow,0)==(0 if state=='closed' else budget-sum(c[1] for c in claims)),
|
|
237
|
+
'bounty_escrow_mismatch')
|
|
238
|
+
for claimant,amount,tid,cid in claims:
|
|
239
|
+
receipt=ledger.get(tid,{})
|
|
240
|
+
require(amount==reward and (receipt.get('from_subject'),receipt.get('to_subject'),receipt.get('amount_minor'))
|
|
241
|
+
== (escrow,claimant,amount), 'bounty_receipt_mismatch')
|
|
242
|
+
challenge=tx.one('SELECT consumed_at FROM bounty_challenges WHERE id=?',(cid,))
|
|
243
|
+
require(challenge is not None and challenge[0] is not None, 'bounty_nonce_mismatch')
|
|
244
|
+
return {'currency_id':'primary','scale':6,'total_supply_minor':supply,
|
|
245
|
+
'banks':tx.one("SELECT COUNT(*) FROM money_bank_roles WHERE status='active'")[0],
|
|
246
|
+
'enabled_offers':tx.one('SELECT COUNT(*) FROM server_offers WHERE enabled=TRUE')[0],
|
|
247
|
+
'ledger_entries':len(ledger),'conserved':True,'signed_receipts_verified':True,
|
|
248
|
+
'zero_default':'No mint, bank or offer is created by initialization or inspection.'}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
async def check_market_e2e(app, root, call, register):
|
|
252
|
+
"""Only TestConsole IO is replaced; command parsing/approvals/clearing are real.
|
|
253
|
+
|
|
254
|
+
Production MoneyAdmin still requires an OS-root physical console and hidden
|
|
255
|
+
PIN. This fixture cannot be selected by configuration or a network request.
|
|
256
|
+
"""
|
|
257
|
+
require(app.selftest_run_id is not None and app.settings.service_url=='https://selftest.invalid',
|
|
258
|
+
'isolated_market_selftest_required')
|
|
259
|
+
from msg.admin.money import _confirmed_money
|
|
260
|
+
from msg.daemon import parser
|
|
261
|
+
async with app.metadata.transaction(write=False) as tx:
|
|
262
|
+
baseline=inspect_clearing(app,tx)
|
|
263
|
+
bank_key,bank=await register('bank-test')
|
|
264
|
+
buyer_key,buyer=await register('market-buyer')
|
|
265
|
+
|
|
266
|
+
async def command(argv, expected_actions):
|
|
267
|
+
args=parser().parse_args(argv)
|
|
268
|
+
action='bank_fund' if args.money_command=='bank' and args.bank_command=='fund' else args.money_command
|
|
269
|
+
previews=[]; approvals=[]
|
|
270
|
+
def emit(value): previews.append(loads(value))
|
|
271
|
+
def confirm(prompt):
|
|
272
|
+
approval='CONFIRM MONEY '+digest(previews[-1])
|
|
273
|
+
require(prompt=='Type '+approval+' to continue: ', 'selftest_approval_unbound')
|
|
274
|
+
approvals.append(previews[-1]['approval_for'])
|
|
275
|
+
return approval
|
|
276
|
+
def unlock():
|
|
277
|
+
require(approvals==expected_actions, 'selftest_unlock_before_confirmation')
|
|
278
|
+
return root # Fresh disposable Test Root; never opens production key material.
|
|
279
|
+
return await _confirmed_money(app,action,operator='isolated-TestConsole',amount=args.amount,
|
|
280
|
+
subject_id=getattr(args,'subject_id',None),emit=emit,confirm=confirm,unlock=unlock)
|
|
281
|
+
|
|
282
|
+
await command(['money','mint','20'], ['mint'])
|
|
283
|
+
funding=await command(['money','bank','fund','@bank-test','20'], ['bank_add','transfer'])
|
|
284
|
+
require(len(funding['audit_event_ids'])==2 and funding['root_balance_minor']==0, 'selftest_bank_fund_failed')
|
|
285
|
+
async def checked(name,args,key=None,subject=None,**kwargs):
|
|
286
|
+
result=await call(name,args,key,subject,**kwargs)
|
|
287
|
+
require(result.status=='ok','market_selftest_'+name.replace('.','_'))
|
|
288
|
+
return result
|
|
289
|
+
bounty=await checked('bounty.create',{'name':'signature-reward',
|
|
290
|
+
'terms':'Control of the current IdentityKey only, not human or Sybil proof.',
|
|
291
|
+
'reward_minor':10_000_000,'budget_minor':10_000_000,'max_claims':1,
|
|
292
|
+
'claim_limit_per_subject':1},bank_key,bank)
|
|
293
|
+
lid=bounty.data['bounty']['listing_id']
|
|
294
|
+
challenge=await checked('bounty.challenge',{'listing_id':lid},buyer_key,buyer)
|
|
295
|
+
proof=challenge.data['challenge']
|
|
296
|
+
claimed=await checked('bounty.claim',{'challenge_id':proof['challenge_id'],
|
|
297
|
+
'proof':wire(buyer_key.sign(canonical(proof),purpose='bounty-pop-v1'))},buyer_key,buyer)
|
|
298
|
+
require(claimed.data['reward']['body']['amount_minor']==10_000_000 and
|
|
299
|
+
claimed.data['escrow_balance_minor']==0,'selftest_bounty_not_paid')
|
|
300
|
+
listing=await checked('store.listing_create',{'name':'test-delivery','item_kind':'bundle',
|
|
301
|
+
'price_minor':5_000_000,'currency_id':'primary','quantity':1,
|
|
302
|
+
'delivery_mode':'managed_instant','escrow_policy':'escrow-instant-v1',
|
|
303
|
+
'dispute_policy':'dispute-v1','terms':'Fixed text and file; immediate in-site delivery.'},bank_key,bank)
|
|
304
|
+
async with app.metadata.transaction(write=False) as tx:
|
|
305
|
+
files=await tx.resolve((await tx.path(bank))+'/files')
|
|
306
|
+
payload=await checked('content.file_put',{'parent':files,'name':'hello.txt','data':b64(b'delivery-ok\n'),
|
|
307
|
+
'media_type':'text/plain'},bank_key,bank)
|
|
308
|
+
listing_id=listing.resources[0].id
|
|
309
|
+
package=await checked('store.package_deposit',{'listing_id':listing_id,
|
|
310
|
+
'listing_revision':listing.resources[0].revision,'manifest':{'text':'msg.lmm.best store selftest'},
|
|
311
|
+
'payload_refs':[wire(payload.resources[0])]},bank_key,bank)
|
|
312
|
+
active=await checked('store.listing_update',{'id':listing_id,'state':'active','package_ref':package.data['package']['id']},
|
|
313
|
+
bank_key,bank,expected=((listing_id,listing.data['generation']),))
|
|
314
|
+
quote=active.data['listing']
|
|
315
|
+
args={'listing_id':listing_id,'listing_revision':quote['listing_revision'],'quantity':1,
|
|
316
|
+
'currency_id':'primary','total_price_minor':5_000_000,
|
|
317
|
+
'package_digest':package.data['package']['digest'],'auto_accept':True}
|
|
318
|
+
# No seller request occurs after publishing. The signed buyer request alone
|
|
319
|
+
# validates the immutable deposit, delivers, then releases the exact escrow.
|
|
320
|
+
bought=await checked('orders.buy',args,buyer_key,buyer,request_id='market-e2e-buy',
|
|
321
|
+
contract_version=2)
|
|
322
|
+
require(bought.data['order']['state']=='settled','selftest_order_not_settled')
|
|
323
|
+
delivery=await checked('delivery.get',{'order_id':bought.data['order']['id']},buyer_key,buyer)
|
|
324
|
+
content=delivery.data['delivery']
|
|
325
|
+
require(content['manifest']=={'text':'msg.lmm.best store selftest'} and
|
|
326
|
+
content['payloads'][0]['data']==b64(b'delivery-ok\n') and
|
|
327
|
+
content['package_digest']==package.data['package']['digest'],'selftest_delivery_mismatch')
|
|
328
|
+
repeated=await checked('orders.buy',args,buyer_key,buyer,request_id='market-e2e-buy',
|
|
329
|
+
contract_version=2)
|
|
330
|
+
require(repeated.replayed and repeated.data==bought.data,'selftest_market_replay_failed')
|
|
331
|
+
buyer_balance=await checked('money.balance',{},buyer_key,buyer)
|
|
332
|
+
bank_balance=await checked('money.balance',{},bank_key,bank)
|
|
333
|
+
require(buyer_balance.data['balance_minor']==5_000_000 and bank_balance.data['balance_minor']==15_000_000,
|
|
334
|
+
'selftest_market_balance_mismatch')
|
|
335
|
+
async with app.metadata.transaction(write=False) as tx:
|
|
336
|
+
checked_state=inspect_clearing(app,tx)
|
|
337
|
+
require(checked_state['total_supply_minor']==baseline['total_supply_minor']+20_000_000 and
|
|
338
|
+
checked_state['banks']==baseline['banks']+1 and
|
|
339
|
+
checked_state['enabled_offers']==baseline['enabled_offers'],
|
|
340
|
+
'selftest_market_supply_mismatch')
|
|
341
|
+
return True
|