多人决策系统
引言
多人决策系统需要解决两个相互关联的问题:第一,如何将不同参与者的偏好转化为可计算、可比较的数值;第二,如何在参与者互不信任或不希望提前公开偏好的情况下,保证投票、报价和结算过程具有一定的保密性与可审计性。
本文分析的应用是一个运行于浏览器中的多人决策工具。它同时实现了以下两类机制:
- 基于平方成本的多人偏好聚合,即二次方投票+落选补偿;
- 针对多个物品分别执行的密封二价拍卖。
在非拍卖模式下,参与者为不同选项分配整数分数。分数越高,表达的偏好越强,但所消耗的积分按照平方增长。在拍卖模式下,每名参与者对多个物品提交独立的密封报价,每件物品由唯一最高报价者获得,但支付金额为第二高报价。
从计算机技术角度看,该系统没有使用后端服务器或数据库,而是采用原生 HTML、CSS 和 JavaScript 构成单页应用。密码派生、数据加密、解密和结果计算均在浏览器本地完成,持久化数据存储于 localStorage,敏感投票和报价则以 AES-GCM 密文形式保存。
相关技术及其细节
二次投票与平方成本机制
偏好强度的数值表达
设系统中共有 $n$ 名参与者和 $m$ 个候选选项。参与者 $i$ 对选项 $j$ 给出的分数记为 $x_{ij}$,并满足:
\[0 \leq x_{ij} \leq x_{\max}\]传统的一人一票机制只能表达选择方向,不能表达偏好强度。该应用允许参与者为不同选项分配不同分数,从而把“是否支持”扩展为“支持程度”。
系统对每个选项计算总分:
\[S_j=\sum_{i=1}^{n}x_{ij}\]在单选模式下,总分最高的选项成为候选结果;在多选模式下,系统选择总分排名前 $k$ 的选项;在排序模式下,系统直接按照 $S_j$ 从高到低生成完整排名。
当最高分或截止位出现并列时,系统不进行随机裁决,而是明确标记结果未完全确定。这种处理避免了引入不可审计的随机过程,但也意味着系统需要额外的线下规则来解决平局。
平方积分成本
该应用使用二次投票中的核心思想:参与者为某个选项投入 $x$ 分时,不是消耗 $x$ 个积分,而是消耗 $x^2$ 个积分。
参与者 $i$ 的总积分成本为:
\[C_i=\sum_{j=1}^{m}x_{ij}^2\]平方成本会使边际成本随分数增加而提高。将某个选项从 $x$ 分增加到 $x+1$ 分时,新增成本为:
\[\Delta C=(x+1)^2-x^2=2x+1\]例如,将某个选项从 $1$ 分提高到 $2$ 分只增加 $3$ 个积分,而从 $9$ 分提高到 $10$ 分则增加 $19$ 个积分。因此,参与者可以低成本地表达对多个选项的一般偏好,但表达极强偏好时必须承担显著更高的成本。
这种机制试图抑制极端投票,同时保留表达偏好强度的能力。
免费积分与额外积分购买
系统为每名参与者提供初始免费积分 $B$。当参与者的平方成本超过免费额度时,需要购买额外积分。
额外购买积分数量为:
\[Q_i=\max(0,C_i-B)\]若每个额外积分的价格为 $p$,则参与者 $i$ 的购买金额为:
\[M_i=pQ_i\]这部分金额被平均分配给其他参与者。当参与人数大于 $1$ 时,参与者 $i$ 向每名其他参与者支付的理论金额为:
\[P_{i\rightarrow k}^{\text{purchase}} = \frac{M_i}{n-1}, \qquad k\neq i\]在程序实现中,金额首先转换为整数分,即“人民币分”,然后执行最大余数法分配,从而避免 JavaScript 浮点数误差造成账目不平。
其核心思想可以表示为:
1
2
3
totalCents = Math.round(amount * 100);
base = Math.floor(totalCents / recipientCount);
remainder = totalCents % recipientCount;
前 remainder 名收款人额外获得一分钱,因此所有分配金额之和严格等于原始金额。
未实现价值补偿与结果结算
该系统不仅聚合分数,还引入了“未实现平方价值补偿”。
设最终入选选项集合为 $W$。对参与者 $i$ 而言,其在未入选选项上的平方价值为:
\[U_i = p\sum_{j\notin W}x_{ij}^2\]系统把 $U_i$ 视为参与者 $i$ 因未实现偏好而应获得的补偿。该补偿由其他 $n-1$ 名参与者平均承担,因此每名其他参与者向参与者 $i$ 支付:
\[P_{k\rightarrow i}^{\text{outcome}} = \frac{U_i}{n-1}, \qquad k\neq i\]这一设计并不是标准二次投票机制的必备组成部分,而是该应用自定义的结果补偿规则。
其经济含义是:当某名参与者对落选方案投入了大量平方积分时,其他人需要共同补偿其未实现偏好。
该机制提高了少数意见的经济权重,但也带来若干需要注意的问题:
- 补偿规则可能改变参与者真实表达偏好的激励,参与者可能产生策略性投票。
因此,从机制设计角度看,这部分更接近一种实验性补偿机制,而不是严格满足激励相容性的机制。
未实现的备选机制:随机Sink-VCG机制
Sink 的估值不参与方案选择,非 Sink 参与者按照 Clarke 支付规则缴费,剩余资金全部转移给 Sink,从而使机制按设计满足预算平衡。
参考论文:
- Efficiency and Budget Balance in General Quasi-linear Domains
- CS364A: Algorithmic Game Theory. Lecture 7: Multi-Parameter Mechanism Design and the VCG Mechanism
密封二价拍卖
二价拍卖的基本规则
在拍卖模式下,每名参与者可对多个物品分别提交报价。设参与者 $i$ 对物品 $j$ 的报价为 $b_{ij}$。
物品 $j$ 的最高报价为:
\[h_j=\max_i b_{ij}\]若最高报价仅由一名参与者提交,则该参与者成为赢家:
\[i_j^*=\arg\max_i b_{ij}\]赢家不支付自己的最高报价,而是支付第二高报价:
\[p_j=b_{(2)j}\]其中,$b_{(2)j}$ 表示物品 $j$ 的第二高报价。
这类机制通常称为维克里拍卖或二价密封拍卖。在独立私人价值、风险中性、没有预算约束等经典假设下,参与者如实提交自身估值通常构成占优策略。
直观而言,赢家的报价主要决定自己是否获胜,而实际支付金额由其他人的最高报价决定。因此,参与者没有必要通过压低报价来降低支付金额,也不应通过抬高报价来赢得一个价值低于支付价格的物品。
特殊边界情况
该应用对以下情况作出了明确处理。
当所有报价均为 $0$ 时,物品不分配:
\[\max_i b_{ij}=0\]当最高报价由多人并列提交时,系统不自动选择赢家。当只有一名参与者提交正报价时,第二高报价为 $0$,因此赢家免费获得该物品。
当前应用没有卖家、起拍价和保留价,因此更适合熟人群体内部的资源分配,而不是正式商业拍卖。
多物品拍卖
应用对每件物品分别执行二价拍卖,因此其模型隐含假设不同物品之间相互独立。
如果参与者的估值具有互补性,例如只有同时获得物品 $A$ 和物品 $B$ 才有价值,那么独立拍卖可能导致暴露风险:参与者可能只赢得其中一件,却仍然需要支付价格。若物品间存在替代性、互补性或组合约束,更适合采用组合拍卖。组合拍卖允许参与者对物品集合 $T$ 提交组合报价 $v_i(T)$,但赢家确定问题通常会转化为计算复杂度较高的组合优化问题。
净额结算与支付图
毛支付边
系统首先产生两类原始支付关系:
- 额外积分购买款;
- 未实现价值补偿款。
这些关系可以构成一个有向加权图:
\[G=(V,E)\]其中,每名参与者对应一个节点,支付关系对应一条有向边。若参与者 $a$ 应向参与者 $b$ 支付金额 $w_{ab}$,则存在边:
\[a\xrightarrow{w_{ab}}b\]直接执行所有毛支付边可能产生大量相互抵消的转账。例如,$A$ 需要向 $B$ 支付,同时 $B$ 也需要向 $A$ 支付。对应图出现环的情况。
系统先计算每名参与者的净余额。参与者 $i$ 的净余额定义为:
\[z_i=\sum_k w_{ki}-\sum_k w_{ik}\]若 $z_i>0$,参与者 $i$ 是净收款人;若 $z_i<0$,参与者 $i$ 是净付款人。
由于系统内部支付总额守恒,因此应满足:
\[\sum_{i=1}^{n}z_i=0\]债务人与债权人匹配
程序把所有参与者划分为债务人集合和债权人集合,然后使用贪心算法进行匹配。
每次选择一个尚未结清的债务人和债权人,支付金额为:
\[t=\min(d,c)\]其中,$d$ 是债务人的剩余应付款,$c$ 是债权人的剩余应收款。
完成转账后更新:
\[d\leftarrow d-t\] \[c\leftarrow c-t\]当任意一方余额归零时,移动到下一名参与者。该算法不能保证在所有约束条件下得到唯一结果,但能够快速产生一组正确的净额转账,并将转账数量控制在较低水平。
在没有额外转账限制的情况下,最终支付边数量通常不超过 $n-1$。
密码派生与身份验证
四位个人密码
每名参与者首次进入个人页面时,系统生成一个四位数字密码。密码由浏览器的密码学安全随机数生成器产生,而不是使用 Math.random()。
生成过程基于:
1
window.crypto.getRandomValues(array);
这比普通伪随机函数更适合生成密钥材料和认证凭证。
不过,四位数字密码只有 $10000$ 种可能,其理论熵约为:
\[H=\log_2(10000)\approx 13.29\]这意味着四位密码的安全性较低。即使采用高迭代次数的密码派生函数,只要攻击者获得加密状态文件,仍然可以离线尝试全部 $10000$ 种密码。
因此,四位密码适合降低误操作和同屏查看风险,但不能提供高等级密码安全。
PBKDF2 密钥派生
系统没有直接使用四位密码作为 AES 密钥,而是采用 PBKDF2 进行密钥派生。
其抽象形式为:
\[DK=\operatorname{PBKDF2}(P,S,c,\operatorname{HMAC\text{-}SHA256},512)\]其中:
- $P$ 为四位密码;
- $S$ 为随机生成的盐;
- $c=210000$ 为迭代次数;
- 输出长度为 $512$ 位。
系统将派生结果分成两个 $256$ 位部分:
\[DK=V\parallel K\]其中,$V$ 用作密码校验值,$K$ 用作 AES-GCM 加密密钥。
这一设计实现了认证材料和加密材料之间的逻辑分离。程序只保存盐、迭代次数和校验值,不保存明文密码。
验证密码时,系统重新执行 PBKDF2,并比较新生成的 $V’$ 与已保存的 $V$:
\[V'=V\]若匹配,则使用对应的 $K$ 解密个人数据。
AES-GCM 认证加密
加密对象
普通投票模式下,每名参与者拥有独立的选票密文;拍卖模式下,每名参与者拥有独立的报价密文。
明文首先序列化为 JSON:
1
JSON.stringify(payload)
随后编码为 UTF-8 字节并使用 AES-GCM 加密。
系统使用:
- AES 密钥长度:$256$ 位;
- 初始化向量长度:$96$ 位;
- GCM 认证标签长度:$128$ 位。
加密结果包含:
1
2
3
4
5
6
7
{
scheme: "AES-GCM-256",
kind: "ballot",
iv: "...",
ciphertext: "...",
updatedAt: "..."
}
AES-GCM 同时提供机密性和完整性。若密文、初始化向量或附加认证数据被修改,解密操作会失败。
附加认证数据
系统还使用附加认证数据绑定参与者身份和数据类型:
1
"quadratic-group-decision|" + kind + "|" + personId
可将其表示为:
\[AAD=\text{AppID}\parallel\text{Kind}\parallel\text{PersonID}\]附加认证数据本身不加密,但会参与认证标签计算。
这一设计可以防止以下密文替换攻击:
- 将参与者 $A$ 的密文替换为参与者 $B$ 的密文;
- 将普通选票密文冒充为拍卖报价密文;
- 将相同密文复制到不同数据槽位。
即使攻击者复制了密文,由于解密时使用的 $AAD$ 不一致,AES-GCM 认证也会失败。
随机初始化向量
每次保存数据时,系统都会重新生成一个 $12$ 字节随机初始化向量:
1
var iv = randomBytes(12);
AES-GCM 要求同一密钥下不能重复使用相同的初始化向量。随机生成 $96$ 位初始化向量可以把碰撞概率控制在较低水平。
若使用同一密钥执行 $q$ 次加密,随机初始化向量发生碰撞的近似概率可由生日界估算:
\[P_{\text{collision}} \approx \frac{q(q-1)}{2^{97}}\]对于普通浏览器应用中的保存次数,该概率可以忽略。
前端单页应用架构
原生 HTML、CSS 与 JavaScript
应用没有引入 React、Vue、Angular 或第三方 UI 框架,而是使用原生浏览器 API 实现。
其技术栈主要包括:
1
2
3
4
5
6
7
8
HTML5
CSS3
JavaScript
Web Crypto API
Web Storage API
FileReader API
Blob 与 Object URL
SVG
HTML 负责页面骨架,CSS 负责响应式布局和视觉组件,JavaScript 负责状态管理、事件绑定、加密、解密和金融计算。
这种方案的优点是部署简单。整个应用可以保存为单个 HTML 文件并在浏览器中直接打开。
缺点是所有渲染逻辑、业务逻辑和安全逻辑集中在一个脚本中,随着功能增加,代码维护成本会迅速上升。
基于步骤的状态机
应用将整个流程建模为顺序状态机。
若参与人数为 $n$,总页面数为:
\[T=n+2\]其中包括:
- 配置页面;
- $n$ 个参与者个人页面;
- 结果解锁页面。
当前状态由 currentStep 表示:
1
state.currentStep
页面跳转时,系统执行以下操作:
1
2
3
4
5
flushActiveSession()
clearSensitiveMemory()
updateCurrentStep()
saveState()
render()
这一流程保证参与者离开个人页面前,当前数据已经完成加密保存,同时解密密钥和明文数据从全局内存引用中移除。
手工响应式渲染
应用使用 innerHTML 重新生成页面,再重新绑定事件监听器。这是一种命令式渲染方式。
其大致模式为:
1
2
3
4
5
6
7
8
9
function render() {
if (isSetupStep()) {
renderSetup();
} else if (isResultStep()) {
renderResultsEntry();
} else {
renderParticipantEntry();
}
}
与虚拟 DOM 框架相比,这种方式依赖开发者手工维护状态与界面的一致性。
应用通过统一的 render() 入口降低了局部状态失配风险,但频繁使用 innerHTML 仍可能导致:
- 事件监听器需要重复绑定;
- DOM 状态无法自然保留;
- 大规模界面更新时性能较低;
- 业务逻辑和视图模板耦合较高。
输入转义与 XSS 防护
系统将参与者名称、选项名称和主题插入 HTML 前,调用自定义的转义函数:
1
2
3
4
5
6
7
8
function esc(value) {
return String(value)
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
该函数可以降低存储型跨站脚本攻击风险。
由于页面大量采用字符串拼接生成 HTML,所有用户可控字段都必须经过转义。只要出现一个遗漏点,就可能产生 XSS 漏洞。因此,在更大型的生产系统中,更适合使用安全模板引擎或 DOM 节点 API。
本地存储与数据导入导出
localStorage 持久化
应用使用 localStorage 保存完整状态:
1
2
3
4
localStorage.setItem(
STORAGE_KEY,
JSON.stringify(state)
);
持久化内容包括:
- 系统配置;
- 参与者信息;
- 选项或物品信息;
- PBKDF2 盐和密码校验值;
- AES-GCM 初始化向量和密文;
- 当前流程步骤。
正常情况下,明文密码、明文选票、明文报价和 AES 密钥不会写入 localStorage。
localStorage 的优点是使用简单、刷新页面后状态仍然存在。其缺点是:
- 数据只存在于当前浏览器环境;
- 没有跨设备同步;
- 没有事务和并发控制;
- 同源脚本可以访问存储内容;
- 存储容量有限;
- 浏览器清理站点数据后内容会丢失。
因此,该设计适用于单机、小规模和临时性的群体决策。
串行化异步保存
AES-GCM 加密是异步操作。若用户连续快速修改多个输入,多个加密任务可能同时执行,后完成的旧任务可能覆盖先完成的新任务。
应用通过 Promise 链串行化保存:
1
2
3
4
5
6
7
activeSavePromise = activeSavePromise
.then(function () {
return encryptPayload(...);
})
.then(function (sealed) {
saveCiphertext(sealed);
});
这种设计保证加密保存按照操作顺序完成,避免异步竞态条件导致状态回退。
JSON 导入导出
系统可以将完整状态导出为 JSON 文件。导出数据包括密文和密码校验材料,但不包含明文密码和明文报价。
文件导出使用:
1
2
3
new Blob(...)
URL.createObjectURL(...)
link.click()
文件导入则通过 FileReader 读取并解析 JSON。
导入后,系统会清除内存中的活动会话,并要求所有参与者重新输入密码。这意味着导出的状态文件可以用于备份和迁移,但不能绕过原有密码机制。
内存中的敏感数据管理
参与者成功验证密码后,应用会在内存中保存:
1
2
3
4
5
6
{
personId,
aesKey,
kind,
data
}
该对象代表当前参与者的活动会话。离开页面、进入下一名参与者页面或重新锁定结果时,系统执行:
1
2
3
activeSession = null;
resultBallots = null;
resultUnlocked = false;
这种设计遵循“尽量缩短明文驻留时间”的原则,但 JavaScript 中将变量设置为 null 并不等于立即安全擦除内存。垃圾回收器何时回收对象由浏览器决定,开发者不能保证明文立即从物理内存中消失。
因此,所谓“清除内存”更准确地说是删除应用层可访问引用,而不是完成密码学意义上的安全擦除。
整体方法
系统初始化
应用启动后首先检查浏览器是否支持以下能力:
1
2
3
4
window.crypto
window.crypto.getRandomValues
window.crypto.subtle
Promise
如果浏览器不支持 Web Crypto API,系统终止初始化并显示错误信息。
随后,应用尝试从 localStorage 读取历史状态。若读取失败、JSON 损坏或状态结构无效,则创建默认状态。
配置阶段
组织者首先配置决策主题、参与人数、选项数量和任务类型。
在二次投票模式下,还需要配置:
- 最终选择数量;
- 每人免费积分;
- 额外积分单价;
- 单项最高分;
- 自定义结果说明。
修改参与者数量时,如果删除某名参与者,其密码校验数据、加密选票和密封报价也会被删除。
修改选项数量时,系统保留仍然存在的选项,并为新增选项生成唯一标识符。
唯一标识符比数组下标更适合关联密文中的投票数据。即使选项名称发生变化,数据仍可通过稳定的 ID 进行映射。
个人密码建立阶段
对于尚未建立密码的参与者,系统执行以下步骤:
- 使用
crypto.getRandomValues()生成四位密码; - 生成 $16$ 字节随机盐;
- 通过 PBKDF2 派生 $512$ 位材料;
- 保存前 $256$ 位校验值;
- 使用后 $256$ 位作为 AES-GCM 密钥;
- 创建空白选票或空白报价;
- 立即加密并保存;
- 将四位密码只显示一次。
密码一旦离开当前页面便不会再次显示,系统也没有密码重置机制。
这种设计避免平台保存密码,但可用性风险较高:任何一名参与者忘记密码,都可能导致最终结果无法统一解锁。
个人投票或报价阶段
参与者再次进入个人页面时,需要输入四位密码。
系统重新派生校验值和 AES 密钥。如果校验值不匹配,则拒绝访问;如果校验成功,则使用 AES-GCM 解密个人数据。
在二次投票模式中,每次修改分数后,系统重新计算:
\[C_i=\sum_j x_{ij}^2\] \[Q_i=\max(0,C_i-B)\] \[M_i=pQ_i\]同时更新界面中的已用积分、剩余免费积分、需购买积分和购买金额。
在拍卖模式中,每名参与者在同一页面填写对所有物品的报价。报价为 $0$ 表示不参与该物品的竞拍。
每次数据发生变化,系统都会将完整个人数据重新序列化并加密,而不是只加密发生变化的字段。这种方式实现简单,也避免部分更新不一致,但数据规模较大时会增加加密成本。
全员解锁阶段
结果页面要求输入所有参与者的密码。
对每名参与者,系统分别执行:
- 检查密码是否为四位数字;
- 通过 PBKDF2 派生验证材料;
- 比较密码校验值;
- 使用派生出的 AES 密钥解密个人密文;
- 验证 AES-GCM 认证标签;
- 将明文暂存在结果页内存中。
系统使用 Promise.all() 等待所有参与者全部解密成功:
1
Promise.all(tasks)
只要任何一人的密码错误或密文认证失败,整个结果保持锁定。
这实现了一种简单的“全员参与解锁”规则,但它并不是真正的门限密码学。所有密文仍然分别由个人密码加密,而不是使用一个必须由多人联合重构的共享密钥。
结果计算阶段
二次投票结果
系统首先计算每个选项的总分:
\[S_j=\sum_i x_{ij}\]随后根据模式确定最终入选集合 $W$。
若结果唯一确定,则进一步计算:
- 每名参与者的额外积分购买款;
- 每名参与者的未实现价值补偿;
- 所有原始支付边;
- 每名参与者的净余额;
- 最终净额转账图。
若最高位或截止位并列,系统仍然显示排名,但不生成结果补偿支付边。
二价拍卖结果
对每件物品,系统独立完成以下处理:
- 收集所有参与者报价;
- 按报价从高到低排序;
- 过滤所有非正报价;
- 检查是否无人报价;
- 检查最高报价是否并列;
- 确定唯一赢家;
- 计算第二高报价;
- 输出赢家、最高报价、第二高报价和支付金额。
拍卖结果中会公开所有参与者的报价。这符合统一开标的产品设计,但意味着开标后报价不再保密。
结果重新锁定
查看结果后,用户可以点击“清除内存并重新锁定”。
系统会删除:
1
2
3
resultBallots
resultUnlocked
activeSession
随后返回全员密码输入页面。
持久化存储中的数据始终保持为密文。因此,页面刷新或重新打开文件后,仍然需要重新输入参与者密码。
技术评价
其主要限制包括:
- 四位密码熵过低,不能抵抗离线穷举攻击;
localStorage不适合保存高价值或高敏感度数据;- 没有真正的门限解密或多方安全计算;
- 没有数字签名,无法证明密文由特定参与者提交;
- 没有可信时间戳,无法严格证明提交顺序;
- 组织者可能修改前端源码或替换应用文件;
- 二次投票补偿机制可能产生策略性激励;
- 没有后端审计日志和权限控制;
- JavaScript 内存无法保证安全擦除。
因此,该系统更适合作为机制设计实验、教学演示、熟人群体内部决策或低风险资源分配工具,而不应直接用于高金额拍卖、正式选举或具有法律约束力的金融交易。
从工程演进角度看,若要将其升级为生产级系统,可以进一步引入:
- 前后端分离架构;
- 用户身份认证与公钥签名;
- Argon2id 等抗 GPU 密码派生算法;
- 更长的密码或硬件密钥;
- 数据库事务与不可篡改审计日志;
- 门限加密或秘密共享;
- 可验证洗牌和零知识证明;
- 服务端权限控制;
- 正式的机制激励分析;
- 组合拍卖求解器;
- 自动化测试和安全审计。
