Skip to content

Commit 5fde074

Browse files
committed
Validate incoming transfer actor against tracked counterparty
1 parent 2f2db7d commit 5fde074

File tree

3 files changed

+64
-1
lines changed

3 files changed

+64
-1
lines changed

packages/stackflow-agent/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,5 +135,5 @@ const result = await agent.acceptIncomingTransfer({
135135
4. Watcher retries are idempotent for already-disputed closures (same closure txid is skipped on later polls).
136136
5. Read-only polling isolates per-pipe failures (`getPipeState` errors on one pipe do not stop others).
137137
6. Event scan mode intentionally holds the cursor when any dispute submission errors occur, so failed disputes are retried on next run.
138-
7. Incoming transfer validation enforces tracked contract/pipe/principals/token consistency; mismatched `pipeId`, `pipeKey`, or token payloads are rejected.
138+
7. Incoming transfer validation enforces tracked contract/pipe/principals/token consistency; mismatched `pipeId`, `pipeKey`, `actor`, or token payloads are rejected.
139139
8. For production hardening, add alerting, signer balance checks, and idempotency audit logs.

packages/stackflow-agent/src/agent-service.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,12 @@ export class StackflowAgentService {
268268
reason: "actor-missing",
269269
};
270270
}
271+
if (actor !== tracked.counterpartyPrincipal) {
272+
return {
273+
valid: false,
274+
reason: "actor-mismatch",
275+
};
276+
}
271277
const latest = this.stateStore.getLatestSignatureState(
272278
tracked.pipeId,
273279
tracked.localPrincipal,

tests/stackflow-agent.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,63 @@ describe("stackflow agent", () => {
691691
store.close();
692692
});
693693

694+
it("rejects incoming transfer requests with actor mismatch", () => {
695+
const dbFile = tempDbFile("agent-sign-actor-mismatch");
696+
const store = new AgentStateStore({ dbFile });
697+
const contractId = "ST1TESTABC.contract";
698+
const pipeKey = {
699+
"principal-1": "ST1LOCAL",
700+
"principal-2": "ST1OTHER",
701+
token: null,
702+
};
703+
const pipeId = buildPipeId({ contractId, pipeKey });
704+
705+
store.upsertTrackedPipe({
706+
pipeId,
707+
contractId,
708+
pipeKey,
709+
localPrincipal: "ST1LOCAL",
710+
counterpartyPrincipal: "ST1OTHER",
711+
token: null,
712+
});
713+
714+
const agent = new StackflowAgentService({
715+
stateStore: store,
716+
signer: {
717+
async sip018Sign() {
718+
return "0x" + "44".repeat(65);
719+
},
720+
async submitDispute() {
721+
return { txid: "0x1" };
722+
},
723+
async callContract() {
724+
return { ok: true };
725+
},
726+
},
727+
network: "devnet",
728+
});
729+
730+
const validation = agent.validateIncomingTransfer({
731+
pipeId,
732+
payload: {
733+
contractId,
734+
forPrincipal: "ST1LOCAL",
735+
withPrincipal: "ST1OTHER",
736+
token: null,
737+
myBalance: "90",
738+
theirBalance: "10",
739+
nonce: "1",
740+
action: "1",
741+
actor: "ST1THIRD",
742+
theirSignature: "0x" + "22".repeat(65),
743+
},
744+
});
745+
746+
expect(validation.valid).toBe(false);
747+
expect(validation.reason).toBe("actor-mismatch");
748+
store.close();
749+
});
750+
694751
it("rejects incoming transfer requests with pipe id mismatch", () => {
695752
const dbFile = tempDbFile("agent-sign-pipeid-mismatch");
696753
const store = new AgentStateStore({ dbFile });

0 commit comments

Comments
 (0)